Skip to content

chore: update inspec controls to CIS 4 benchmark #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 46 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
f4408ff
feat: add 1.16 control
lxndrblz May 22, 2025
0c57a08
feat: add 1.17 control
lxndrblz May 22, 2025
81ea41d
fix: update control 1.06 to include serviceAccountTokenCreator
lxndrblz May 22, 2025
d173f86
feat: add control 3.10
lxndrblz May 22, 2025
20fe47b
feat: add control 8.1
lxndrblz May 22, 2025
d222603
feat: add control 7.4
lxndrblz May 22, 2025
b421c7d
feat: add control 6.2.8 and update remaining 6.2.X controls
lxndrblz May 22, 2025
3aba165
feat: add missing controls 2.12-2.16, 4.09-4.12
lxndrblz May 25, 2025
b08162a
chore: update 5.01-storage.rb
lxndrblz May 26, 2025
ae71b0a
chore: update 5.02-storage.rb
lxndrblz May 26, 2025
f675454
feat: update 7.01-bq.rb
lxndrblz May 26, 2025
2904acd
feat: update 7.02-bq.rb
lxndrblz May 26, 2025
0337d2a
feat: update 7.03-bq.rb
lxndrblz May 26, 2025
0695f69
chore: update 7.04-bq.rb
lxndrblz May 26, 2025
25fdf97
chore: updates control descriptions 1.X-3.X
lxndrblz May 26, 2025
acb865a
chore: update inspec and README
lxndrblz May 26, 2025
e292532
fix: add missing permission
lxndrblz May 26, 2025
a789208
chore: update 6.1.X
lxndrblz May 26, 2025
0c57f45
chore: update 8.01-dp.rb
lxndrblz May 26, 2025
c6605ea
chore: update docs for better readability
lxndrblz Jun 4, 2025
3b174ea
feat: add devcontainer
lxndrblz Jun 4, 2025
8d25971
chore: disable new cops in rubocop
lxndrblz Jun 4, 2025
a60645d
chore: fix rubocop warnings
lxndrblz Jun 4, 2025
1d0ccc0
chore: update dev container to latest version
lxndrblz Jun 4, 2025
25183ec
fix: make control 1.2 unscored
lxndrblz Jun 6, 2025
2c2c55f
fix: control 1.01
lxndrblz Jun 6, 2025
d9bd60f
fix: control 1.04
lxndrblz Jun 6, 2025
7384519
fix: make control 1.16 unscored
lxndrblz Jun 6, 2025
dc5fa42
Revert "fix: make control 1.16 unscored"
lxndrblz Jun 6, 2025
e7ef4f1
chore: remove broken helpers library
lxndrblz Jun 6, 2025
f5ee74f
feat: include custom helper classes
lxndrblz Jun 6, 2025
af00c2f
fix: broken control 8.01
lxndrblz Jun 6, 2025
2b58eff
fix: make control 1.16 unscored
lxndrblz Jun 6, 2025
5ea26a5
fix: make control 2.12 unscored
lxndrblz Jun 6, 2025
6a9d701
fix: make control 2.13 unscored
lxndrblz Jun 6, 2025
7507714
fix: make control 2.15 unscored
lxndrblz Jun 6, 2025
0f01004
fix: make control 2.16 unscored
lxndrblz Jun 6, 2025
a2cbee3
fix: broken control 4.11
lxndrblz Jun 6, 2025
9fb7f69
chore: update doc strings control 6.01
lxndrblz Jun 6, 2025
c264ae4
chore: update doc strings control 6.02
lxndrblz Jun 6, 2025
1f2165b
chore: remove libraries in favour of custom library
lxndrblz Jun 6, 2025
f4e325d
fix: error handling of control 1.01
lxndrblz Jun 6, 2025
876d640
fix: filter out disabled service accounts
lxndrblz Jun 6, 2025
dca0976
fix: various linting errors in control 1.01
lxndrblz Jun 6, 2025
ccad0c8
fix: remove check for disabled service accounts
lxndrblz Jun 6, 2025
95109db
fix: skip disabled user managed keys
lxndrblz Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM cincproject/auditor:6.8.24

RUN apt-get update && \
apt-get install -y \
build-essential \
ruby-dev \
zlib1g-dev \
libssl-dev \
liblzma-dev \
&& rm -rf /var/lib/apt/lists/*

# Install the bundler gem
RUN gem install bundler --no-document

# Install the rubocop gem
RUN gem install rubocop --no-document

# Default command when the container starts (optional, but good for interactive shells)
CMD ["/bin/bash"]
29 changes: 29 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "InSpec Custom Controls Development",
"build": {
"dockerfile": "Dockerfile"
},
"customizations": {
"vscode": {
"extensions": [
"burtlo.inspec",
"misogi.ruby-rubocop",
"Shopify.ruby-lsp"
],
// VS Code settings specific to this dev container
"settings": {
// Use the Ruby LSP as the default formatter
"[ruby]": {
"editor.defaultFormatter": "Shopify.ruby-lsp",
"editor.formatOnSave": true,
"editor.tabSize": 2,
"editor.insertSpaces": true
},
// Optional: Configure rdbg for debugging if you use it
"ruby_lsp.debug.rdbg.attachUrl": "tcp://localhost:1234",
"ruby_lsp.debug.rdbg.logOutput": true
}
}
},
"postCreateCommand": "bundle check || bundle install"
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ Gemfile.lock
inputs.yml
vendor
.DS_Store
credentials/*
3 changes: 2 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
AllCops:
TargetRubyVersion: 2.6.3p62
TargetRubyVersion: 3.1.6p260
NewCops: disable
Exclude:
- Gemfile

Expand Down
41 changes: 26 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
# GCP CIS 1.2.0 Benchmark Inspec Profile
# GCP CIS 4.0.0 Benchmark Inspec Profile

This repository holds the [Google Cloud Platform (GCP)](https://cloud.google.com/) [Center for Internet Security (CIS)](https://www.cisecurity.org) [version 1.2 Benchmark](https://www.cisecurity.org/benchmark/google_cloud_computing_platform/) [Inspec](https://www.inspec.io/) Profile.
This repository holds the [Google Cloud Platform (GCP)](https://cloud.google.com/) [Center for Internet Security (CIS)](https://www.cisecurity.org) [version 4.0 Benchmark](https://www.cisecurity.org/benchmark/google_cloud_computing_platform/) [Inspec](https://www.inspec.io/) Profile.

## Required Disclaimer
## 🚧 Required Disclaimer

This is not an officially supported Google product. This code is intended to help users assess their security posture on the Google Cloud against the CIS Benchmark. This code is not certified by CIS.

## Coverage
## Coverage

The following GCP CIS v1.2.0 Benchmark Controls are not covered:
The following GCP CIS v4.0.0 Benchmark Controls are not covered:

- Identity and Access Management 1.2 - "Ensure that multi-factor authentication is enabled for all non-service accounts"
- Identity and Access Management 1.3 - "Ensure that Security Key Enforcement is enabled for all admin accounts"
- Identity and Access Management 1.12 - "Ensure API keys are not created for a project"
- Identity and Access Management 1.13 - "Ensure API keys are restricted to use by only specified Hosts and Apps"
- Identity and Access Management 1.14 - "Ensure API keys are restricted to only APIs that application needs access"
- Identity and Access Management 1.15 - "Ensure API keys are rotated every 90 days"
- Cloud SQL Database Services 6.3 - "Ensure that MySql database instance does not allow anyone to connect with administrative privileges"
- Cloud SQL Database Services 6.4 - "Ensure that MySQL Database Instance does not allows root login from any Host"
- Identity and Access Management 1.17 - "Ensure Secrets are Not Stored in Cloud Functions Environment Variables by Using Secret Manager"
- Logging 2.14 - "Ensure 'Access Transparency' is 'Enabled'"
- Networking 3.10 - "Use Identity Aware Proxy (IAP) to Ensure Only Traffic From Google IP Addresses are 'Allowed'"
- VMs 4.10 - " Ensure That App Engine Applications Enforce HTTPS Connections"
- VMs 4.12 - "Ensure the Latest Operating System Updates Are Installed On Your Virtual Machines in All Projects"
- MySQL 6.1 - "Ensure that MySql database instances are secure"
- BigQuery 7.4 - "Ensure that MySQL Database Instance does not allows root login from any Host"

## Usage
## 🚀 Usage

### Profile Inputs (see `inspec.yml` file)

This profile uses InSpec Inputs to make the tests more flexible. You are able to provide inputs at runtime either via the `cli` or via `YAML files` to help the profile work best in your deployment.

**pro tip**: Do not change the inputs in the `inspec.yml` file directly, either:
**pro-tip**: Do not change the inputs in the `inspec.yml` file directly, either:

- update them via the cli - via the `--input` flag
- pass them in via a YAML file as shown in the `Example` - via the `--input-file` flag
Expand All @@ -49,17 +53,19 @@ Use this Cloud Shell Walkthrough for a hands-on example.

### CLI Example

#### Ruby Gem
#### 💎 Ruby Gem

```
#install inspec
# install inspec
$ gem install inspec-bin -v 4.26.15 --no-document --quiet
```

```
# make sure you're authenticated to GCP
$ gcloud auth list
```

```
# acquire credentials to use with Application Default Credentials
$ gcloud auth application-default login

Expand All @@ -73,7 +79,7 @@ Profile Summary: 48 successful controls, 5 control failures, 7 controls skipped
Test Summary: 166 successful, 7 failures, 7 skipped
```

#### Docker
#### 🐳 Docker
```
# pull inspec image
$ docker pull chef/inspec:4.26.15
Expand All @@ -82,7 +88,9 @@ $ docker pull chef/inspec:4.26.15
```
# make sure you're authenticated to GCP
$ gcloud auth list
```

```
# acquire credentials to use with Application Default Credentials
$ gcloud auth application-default login

Expand All @@ -91,15 +99,17 @@ $ gcloud auth application-default login
```
# create function for convenience
$ function inspec-docker { docker run -it -e GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS=true --rm -v ~/.config:/root/.config -v $(pwd):/share chef/inspec:4.26.15 "$@"; }
```

```
# scan a project with this profile, replace {{project-id}} with your project ID
$ inspec-docker exec https://github.com/GoogleCloudPlatform/inspec-gcp-cis-benchmark.git -t gcp:// --input gcp_project_id={{project-id}} --reporter cli json:{{project-id}}_scan.json
...snip...
Profile Summary: 48 successful controls, 5 control failures, 7 controls skipped
Test Summary: 166 successful, 7 failures, 7 skipped
```

### Required APIs
## ⚙️ Required APIs

Consider these GCP projects, which may all be the same or different:

Expand All @@ -119,7 +129,7 @@ The following GCP APIs should be enabled in **all** of these projects:
- sqladmin.googleapis.com
- storage-api.googleapis.com

### Required Permissions
## 🔑 Required Permissions

The following permissions are required to run the CIS benchmark profile:

Expand Down Expand Up @@ -160,6 +170,7 @@ On project level:
- logging.logMetrics.list
- logging.sinks.get
- logging.sinks.list
- monitoring.alertPolicies.get
- monitoring.alertPolicies.list
- resourcemanager.projects.get
- resourcemanager.projects.getIamPolicy
Expand Down
114 changes: 96 additions & 18 deletions controls/1.01-iam.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
control_id = '1.1'
control_abbrev = 'iam'

# Initialize the IAMBindingsCache outside the control for efficiency
# This resource likely holds the IAM bindings for the project.
iam_bindings_cache = IAMBindingsCache(project: gcp_project_id)

control "cis-gcp-#{control_id}-#{control_abbrev}" do
Expand All @@ -40,29 +42,105 @@
ref 'CIS Benchmark', url: cis_url.to_s
ref 'GCP Docs', url: 'https://cloud.google.com/docs/enterprise/best-practices-for-enterprise-organizations#use_corporate_login_credentials'

# determine the organization's email domain
case google_project(project: gcp_project_id).parent.type
when 'organization'
org_domain = google_organization(name: "organizations/#{google_project(project: gcp_project_id).parent.id}").display_name
when 'folder'
parent = 'folder'
folder_id = google_project(project: gcp_project_id).parent.id
while parent == 'folder'
if google_resourcemanager_folder(name: "folders/#{folder_id}").parent.include?('folders')
folder_id = google_resourcemanager_folder(name: "folders/#{folder_id}").parent.sub('folders/', '')
org_domain = nil
project_parent = google_project(project: gcp_project_id).parent

if project_parent.nil?
# This project is likely a standalone project with no organization parent.
# In this scenario, checking corporate login credentials might not be applicable
# or you might need a different way to define the 'corporate domain'.
# For now, we'll skip if no parent is found.
describe "Project #{gcp_project_id} has no organization or folder parent." do
skip 'Cannot determine corporate domain. Skipping check for corporate login credentials.'
end
else
case project_parent.type
when 'organization'
org_id = project_parent.id
org_resource = google_organization(name: "organizations/#{org_id}")
if org_resource.exists?
org_domain = org_resource.display_name
else
parent = 'organization'
org_domain = google_organization(name: google_resourcemanager_folder(name: "folders/#{folder_id}").parent.to_s).display_name
# Fallback if organization resource doesn't exist or is inaccessible
describe "Could not retrieve organization details for ID: #{org_id}." do
skip 'Cannot determine corporate domain. Skipping check for corporate login credentials.'
end
end
when 'folder'
current_folder_id = project_parent.id
found_organization = false
# Loop upwards until an organization is found or we run out of parents
while current_folder_id
folder_resource = google_resourcemanager_folder(name: "folders/#{current_folder_id}")

if folder_resource.exists? # rubocop:disable Metrics/BlockNesting
parent_name = folder_resource.parent # This will be like "folders/123" or "organizations/456"

if parent_name.include?('organizations/')
org_id = parent_name.sub('organizations/', '')
org_resource = google_organization(name: "organizations/#{org_id}")
if org_resource.exists?
org_domain = org_resource.display_name
found_organization = true
break # Exit loop once organization is found
else
# Organization parent exists but cannot be retrieved
describe "Could not retrieve organization details for ID: #{org_id} (parent of folder #{current_folder_id})." do
skip 'Cannot determine corporate domain. Skipping check for corporate login credentials.'
end
break # Exit loop as we hit an issue
end
elsif parent_name.include?('folders/')
current_folder_id = parent_name.sub('folders/', '') # Move up to the next folder
else
# Unexpected parent type for a folder
describe "Unexpected parent type '#{parent_name}' for folder #{current_folder_id}." do
skip 'Cannot determine corporate domain. Skipping check for corporate login credentials.'
end
break # Exit loop
end
else
# Folder resource itself doesn't exist or is inaccessible
describe "Folder resource 'folders/#{current_folder_id}' not found or inaccessible." do
skip 'Cannot determine corporate domain. Skipping check for corporate login credentials.'
end
break # Exit loop
end
end

unless found_organization
describe "Could not find an organization parent for project #{gcp_project_id} through its folder hierarchy." do
skip 'Cannot determine corporate domain. Skipping check for corporate login credentials.'
end
end
else
# Handle other potential parent types (e.g., `None` for standalone projects, or future types)
describe "Project #{gcp_project_id} has an unsupported parent type: #{project_parent.type}." do
skip 'Cannot determine corporate domain. Skipping check for corporate login credentials.'
end
end
end

iam_bindings_cache.iam_binding_roles.each do |role|
iam_bindings_cache.iam_bindings[role].members.each do |member|
next if member.to_s.end_with?('.gserviceaccount.com')
describe "[#{gcp_project_id}] [Role:#{role}] Its member #{member}" do
subject { member.to_s }
it { should match(/@#{org_domain}/) }
# Proceed with IAM checks only if org_domain was successfully determined
if org_domain.nil?
describe 'Corporate domain could not be determined.' do
skip 'Skipping IAM member checks as corporate domain is unknown.'
end
else
iam_bindings_cache.iam_binding_roles.each do |role|
iam_bindings_cache.iam_bindings[role].members.each do |member|
# Skip service accounts
next if member.to_s.end_with?('.gserviceaccount.com')
# Skip allUsers and allAuthenticatedUsers
next if %w[allUsers allAuthenticatedUsers].include?(member.to_s)

describe "[#{gcp_project_id}] [Role:#{role}] Its member #{member}" do
subject { member.to_s }
# Using `should not match` for personal accounts implies any non-corporate domain
# If the intent is strictly to allow ONLY corporate domains, then `should match`
# but usually the goal is to exclude personal ones.
it { should match(/@#{Regexp.escape(org_domain)}/) } # Use Regexp.escape for safety
end
end
end
end
Expand Down
12 changes: 7 additions & 5 deletions controls/1.02-iam.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# controls/1.02-iam.rb (assuming 1.02 is the actual file name)

# Copyright 2019 The inspec-gcp-cis-benchmark Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -17,18 +19,17 @@
gcp_project_id = input('gcp_project_id')
cis_version = input('cis_version')
cis_url = input('cis_url')
control_id = '1.2'
control_id = '1.2' # This should match the actual control ID
control_abbrev = 'iam'

control "cis-gcp-#{control_id}-#{control_abbrev}" do
impact 'medium'

title "[#{control_abbrev.upcase}] Ensure that multi-factor authentication is enabled for all non-service accounts"

desc 'Setup multi-factor authentication for Google Cloud Platform accounts.'
desc 'rationale', 'Multi-factor authentication requires more than one mechanism to authenticate a user. This secures your logins from attackers exploiting stolen or weak credentials.'
desc 'rationale', 'Multi-factor authentication requires more than one mechanism to authenticate a user. This secures user logins from attackers exploiting stolen or weak credentials.'
desc 'notes', 'This control generally requires manual verification because InSpec does not directly query user MFA status from Cloud Identity or Google Workspace. Verification steps are provided in the references.'

tag cis_scored: false
tag cis_scored: false # Keep this as false
tag cis_level: 1
tag cis_gcp: control_id.to_s
tag cis_version: cis_version.to_s
Expand All @@ -37,6 +38,7 @@

ref 'CIS Benchmark', url: cis_url.to_s
ref 'GCP Docs', url: 'https://cloud.google.com/solutions/securing-gcp-account-u2f'
ref 'GCP Docs', url: 'https://support.google.com/accounts/answer/185839'

describe 'This control is not scored' do
skip 'This control is not scored'
Expand Down
3 changes: 1 addition & 2 deletions controls/1.03-iam.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@

control "cis-gcp-#{control_id}-#{control_abbrev}" do
impact 'medium'

title "[#{control_abbrev.upcase}] Ensure that Security Key Enforcement is enabled for all admin accounts"

desc 'Setup Security Key Enforcement for Google Cloud Platform admin accounts.'
desc 'rationale', 'Google Cloud Platform users with Organization Administrator roles have the highest level of privilege in the organization. These accounts should be protected with the strongest form of two-factor authentication: Security Key Enforcement. Ensure that admins use Security Keys to log in instead of weaker second factors like SMS or one-time passwords (OTP). Security Keys are actual physical keys used to access Google Organization Administrator Accounts. They send an encrypted signature rather than a code, ensuring that logins cannot be phished.'

Expand All @@ -37,6 +35,7 @@

ref 'CIS Benchmark', url: cis_url.to_s
ref 'GCP Docs', url: 'https://cloud.google.com/security-key/'
ref 'GCP Docs', url: 'https://gsuite.google.com/learn-more/key_for_working_smarter_faster_and_more_securely.html'

describe 'This control is not scored' do
skip 'This control is not scored'
Expand Down
Loading