mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
feat(gcp): add check for OS Login 2FA enabled at project level (#9839)
This commit is contained in:
@@ -21,6 +21,7 @@ All notable changes to the **Prowler SDK** are documented in this file.
|
|||||||
- `compute_instance_single_network_interface` check for GCP provider [(#9702)](https://github.com/prowler-cloud/prowler/pull/9702)
|
- `compute_instance_single_network_interface` check for GCP provider [(#9702)](https://github.com/prowler-cloud/prowler/pull/9702)
|
||||||
- `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718)
|
- `compute_image_not_publicly_shared` check for GCP provider [(#9718)](https://github.com/prowler-cloud/prowler/pull/9718)
|
||||||
- `compute_snapshot_not_outdated` check for GCP provider [(#9774)](https://github.com/prowler-cloud/prowler/pull/9774)
|
- `compute_snapshot_not_outdated` check for GCP provider [(#9774)](https://github.com/prowler-cloud/prowler/pull/9774)
|
||||||
|
- `compute_project_os_login_2fa_enabled` check for GCP provider [(#9839)](https://github.com/prowler-cloud/prowler/pull/9839)
|
||||||
- `compute_instance_on_host_maintenance_migrate` check for GCP provider [(#9834)](https://github.com/prowler-cloud/prowler/pull/9834)
|
- `compute_instance_on_host_maintenance_migrate` check for GCP provider [(#9834)](https://github.com/prowler-cloud/prowler/pull/9834)
|
||||||
- CIS 1.12 compliance framework for Kubernetes [(#9778)](https://github.com/prowler-cloud/prowler/pull/9778)
|
- CIS 1.12 compliance framework for Kubernetes [(#9778)](https://github.com/prowler-cloud/prowler/pull/9778)
|
||||||
- CIS 6.0 for M365 provider [(#9779)](https://github.com/prowler-cloud/prowler/pull/9779)
|
- CIS 6.0 for M365 provider [(#9779)](https://github.com/prowler-cloud/prowler/pull/9779)
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"Provider": "gcp",
|
||||||
|
"CheckID": "compute_project_os_login_2fa_enabled",
|
||||||
|
"CheckTitle": "GCP project has OS Login with 2FA enabled",
|
||||||
|
"CheckType": [],
|
||||||
|
"ServiceName": "compute",
|
||||||
|
"SubServiceName": "",
|
||||||
|
"ResourceIdTemplate": "",
|
||||||
|
"Severity": "high",
|
||||||
|
"ResourceType": "compute.googleapis.com/Project",
|
||||||
|
"ResourceGroup": "governance",
|
||||||
|
"Description": "OS Login **Two-Factor Authentication (2FA)** requires users to verify their identity with a second factor when connecting via SSH to VM instances.\n\nThis provides an additional security layer beyond passwords or SSH keys, significantly reducing the risk of unauthorized access even if credentials are compromised.",
|
||||||
|
"Risk": "Without 2FA enforcement, compromised credentials (stolen SSH keys or passwords) grant immediate access to VM instances. Attackers could:\n\n- Gain unauthorized shell access to production systems\n- Exfiltrate sensitive data or deploy malware\n- Move laterally within the infrastructure\n\nThis single point of failure significantly increases the attack surface.",
|
||||||
|
"RelatedUrl": "",
|
||||||
|
"AdditionalURLs": [
|
||||||
|
"https://cloud.google.com/compute/docs/oslogin/set-up-oslogin",
|
||||||
|
"https://www.trendmicro.com/cloudoneconformity/knowledge-base/gcp/ComputeEngine/enable-os-login-with-2fa-authentication.html"
|
||||||
|
],
|
||||||
|
"Remediation": {
|
||||||
|
"Code": {
|
||||||
|
"CLI": "gcloud compute project-info add-metadata --metadata enable-oslogin=TRUE,enable-oslogin-2fa=TRUE",
|
||||||
|
"NativeIaC": "",
|
||||||
|
"Other": "1. Navigate to **Compute Engine** > **Metadata** in Google Cloud Console\n2. Click **Edit**\n3. Add or update metadata entry with key `enable-oslogin-2fa` and value `TRUE`\n4. Ensure `enable-oslogin` is also set to `TRUE`\n5. Click **Save**",
|
||||||
|
"Terraform": "```hcl\nresource \"google_compute_project_metadata\" \"example_resource\" {\n metadata = {\n enable-oslogin = \"TRUE\"\n enable-oslogin-2fa = \"TRUE\" # Enables 2FA for OS Login\n }\n}\n```"
|
||||||
|
},
|
||||||
|
"Recommendation": {
|
||||||
|
"Text": "Enable OS Login with 2FA at the project level to enforce multi-factor authentication for all SSH connections. This adds a critical security layer by requiring users to complete a second verification step, protecting against credential theft and unauthorized access.",
|
||||||
|
"Url": "https://hub.prowler.com/check/compute_project_os_login_2fa_enabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Categories": [
|
||||||
|
"identity-access"
|
||||||
|
],
|
||||||
|
"DependsOn": [
|
||||||
|
"compute_project_os_login_enabled"
|
||||||
|
],
|
||||||
|
"RelatedTo": [
|
||||||
|
"compute_project_os_login_enabled"
|
||||||
|
],
|
||||||
|
"Notes": "OS Login 2FA requires OS Login to be enabled first. Users must have 2-Step Verification configured in their Google account. For organizations, 2FA can be enforced via Google Workspace or Cloud Identity policies."
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from prowler.lib.check.models import Check, Check_Report_GCP
|
||||||
|
from prowler.providers.gcp.services.compute.compute_client import compute_client
|
||||||
|
|
||||||
|
|
||||||
|
class compute_project_os_login_2fa_enabled(Check):
|
||||||
|
"""Ensure that OS Login with 2FA is enabled for a GCP project.
|
||||||
|
|
||||||
|
This check verifies that OS Login Two-Factor Authentication (2FA) is enabled
|
||||||
|
at the project level to enforce an additional layer of security for SSH access
|
||||||
|
to VM instances.
|
||||||
|
|
||||||
|
- PASS: Project has OS Login 2FA enabled (enable-oslogin-2fa=TRUE).
|
||||||
|
- FAIL: Project does not have OS Login 2FA enabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def execute(self) -> list[Check_Report_GCP]:
|
||||||
|
findings = []
|
||||||
|
for project in compute_client.compute_projects:
|
||||||
|
report = Check_Report_GCP(
|
||||||
|
metadata=self.metadata(),
|
||||||
|
resource=compute_client.projects[project.id],
|
||||||
|
project_id=project.id,
|
||||||
|
location=compute_client.region,
|
||||||
|
resource_name=(
|
||||||
|
compute_client.projects[project.id].name
|
||||||
|
if compute_client.projects[project.id].name
|
||||||
|
else "GCP Project"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
report.status = "PASS"
|
||||||
|
report.status_extended = f"Project {project.id} has OS Login 2FA enabled."
|
||||||
|
if not project.enable_oslogin_2fa:
|
||||||
|
report.status = "FAIL"
|
||||||
|
report.status_extended = (
|
||||||
|
f"Project {project.id} does not have OS Login 2FA enabled."
|
||||||
|
)
|
||||||
|
findings.append(report)
|
||||||
|
|
||||||
|
return findings
|
||||||
@@ -80,6 +80,7 @@ class Compute(GCPService):
|
|||||||
for project_id in self.project_ids:
|
for project_id in self.project_ids:
|
||||||
try:
|
try:
|
||||||
enable_oslogin = False
|
enable_oslogin = False
|
||||||
|
enable_oslogin_2fa = False
|
||||||
response = (
|
response = (
|
||||||
self.client.projects()
|
self.client.projects()
|
||||||
.get(project=project_id)
|
.get(project=project_id)
|
||||||
@@ -88,8 +89,14 @@ class Compute(GCPService):
|
|||||||
for item in response["commonInstanceMetadata"].get("items", []):
|
for item in response["commonInstanceMetadata"].get("items", []):
|
||||||
if item["key"] == "enable-oslogin" and item["value"] == "TRUE":
|
if item["key"] == "enable-oslogin" and item["value"] == "TRUE":
|
||||||
enable_oslogin = True
|
enable_oslogin = True
|
||||||
|
if item["key"] == "enable-oslogin-2fa" and item["value"] == "TRUE":
|
||||||
|
enable_oslogin_2fa = True
|
||||||
self.compute_projects.append(
|
self.compute_projects.append(
|
||||||
Project(id=project_id, enable_oslogin=enable_oslogin)
|
Project(
|
||||||
|
id=project_id,
|
||||||
|
enable_oslogin=enable_oslogin,
|
||||||
|
enable_oslogin_2fa=enable_oslogin_2fa,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -733,6 +740,7 @@ class Firewall(BaseModel):
|
|||||||
class Project(BaseModel):
|
class Project(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
enable_oslogin: bool
|
enable_oslogin: bool
|
||||||
|
enable_oslogin_2fa: bool = False
|
||||||
|
|
||||||
|
|
||||||
class LoadBalancer(BaseModel):
|
class LoadBalancer(BaseModel):
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
from re import search
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from prowler.providers.gcp.models import GCPProject
|
||||||
|
from tests.providers.gcp.gcp_fixtures import GCP_PROJECT_ID, set_mocked_gcp_provider
|
||||||
|
|
||||||
|
|
||||||
|
class Test_compute_project_os_login_2fa_enabled:
|
||||||
|
def test_compute_no_project(self):
|
||||||
|
compute_client = mock.MagicMock()
|
||||||
|
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||||
|
compute_client.projects = []
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||||
|
return_value=set_mocked_gcp_provider(),
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled.compute_client",
|
||||||
|
new=compute_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled import (
|
||||||
|
compute_project_os_login_2fa_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = compute_project_os_login_2fa_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
assert len(result) == 0
|
||||||
|
|
||||||
|
def test_one_compliant_project_2fa_enabled(self):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_service import Project
|
||||||
|
|
||||||
|
project = Project(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
enable_oslogin=True,
|
||||||
|
enable_oslogin_2fa=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
compute_client = mock.MagicMock()
|
||||||
|
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||||
|
compute_client.compute_projects = [project]
|
||||||
|
compute_client.projects = {
|
||||||
|
GCP_PROJECT_ID: GCPProject(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
number="123456789012",
|
||||||
|
name="test",
|
||||||
|
labels={},
|
||||||
|
lifecycle_state="ACTIVE",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
compute_client.region = "global"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||||
|
return_value=set_mocked_gcp_provider(),
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled.compute_client",
|
||||||
|
new=compute_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled import (
|
||||||
|
compute_project_os_login_2fa_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = compute_project_os_login_2fa_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "PASS"
|
||||||
|
assert search(
|
||||||
|
f"Project {project.id} has OS Login 2FA enabled",
|
||||||
|
result[0].status_extended,
|
||||||
|
)
|
||||||
|
assert result[0].resource_id == project.id
|
||||||
|
assert result[0].resource_name == "test"
|
||||||
|
assert result[0].location == "global"
|
||||||
|
assert result[0].project_id == GCP_PROJECT_ID
|
||||||
|
|
||||||
|
def test_one_non_compliant_project_2fa_disabled(self):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_service import Project
|
||||||
|
|
||||||
|
project = Project(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
enable_oslogin=True,
|
||||||
|
enable_oslogin_2fa=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
compute_client = mock.MagicMock()
|
||||||
|
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||||
|
compute_client.compute_projects = [project]
|
||||||
|
compute_client.projects = {
|
||||||
|
GCP_PROJECT_ID: GCPProject(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
number="123456789012",
|
||||||
|
name="test",
|
||||||
|
labels={},
|
||||||
|
lifecycle_state="ACTIVE",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
compute_client.region = "global"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||||
|
return_value=set_mocked_gcp_provider(),
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled.compute_client",
|
||||||
|
new=compute_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled import (
|
||||||
|
compute_project_os_login_2fa_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = compute_project_os_login_2fa_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert search(
|
||||||
|
f"Project {project.id} does not have OS Login 2FA enabled",
|
||||||
|
result[0].status_extended,
|
||||||
|
)
|
||||||
|
assert result[0].resource_id == project.id
|
||||||
|
assert result[0].resource_name == "test"
|
||||||
|
assert result[0].location == "global"
|
||||||
|
assert result[0].project_id == GCP_PROJECT_ID
|
||||||
|
|
||||||
|
def test_one_non_compliant_project_oslogin_disabled_2fa_disabled(self):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_service import Project
|
||||||
|
|
||||||
|
project = Project(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
enable_oslogin=False,
|
||||||
|
enable_oslogin_2fa=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
compute_client = mock.MagicMock()
|
||||||
|
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||||
|
compute_client.compute_projects = [project]
|
||||||
|
compute_client.projects = {
|
||||||
|
GCP_PROJECT_ID: GCPProject(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
number="123456789012",
|
||||||
|
name="test",
|
||||||
|
labels={},
|
||||||
|
lifecycle_state="ACTIVE",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
compute_client.region = "global"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||||
|
return_value=set_mocked_gcp_provider(),
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled.compute_client",
|
||||||
|
new=compute_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled import (
|
||||||
|
compute_project_os_login_2fa_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = compute_project_os_login_2fa_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert search(
|
||||||
|
f"Project {project.id} does not have OS Login 2FA enabled",
|
||||||
|
result[0].status_extended,
|
||||||
|
)
|
||||||
|
assert result[0].resource_id == project.id
|
||||||
|
assert result[0].resource_name == "test"
|
||||||
|
assert result[0].location == "global"
|
||||||
|
assert result[0].project_id == GCP_PROJECT_ID
|
||||||
|
|
||||||
|
def test_one_compliant_project_empty_project_name(self):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_service import Project
|
||||||
|
|
||||||
|
project = Project(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
enable_oslogin=True,
|
||||||
|
enable_oslogin_2fa=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
compute_client = mock.MagicMock()
|
||||||
|
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||||
|
compute_client.compute_projects = [project]
|
||||||
|
compute_client.projects = {
|
||||||
|
GCP_PROJECT_ID: GCPProject(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
number="123456789012",
|
||||||
|
name="",
|
||||||
|
labels={},
|
||||||
|
lifecycle_state="ACTIVE",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
compute_client.region = "global"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||||
|
return_value=set_mocked_gcp_provider(),
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled.compute_client",
|
||||||
|
new=compute_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled import (
|
||||||
|
compute_project_os_login_2fa_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = compute_project_os_login_2fa_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "PASS"
|
||||||
|
assert search(
|
||||||
|
f"Project {project.id} has OS Login 2FA enabled",
|
||||||
|
result[0].status_extended,
|
||||||
|
)
|
||||||
|
assert result[0].resource_id == project.id
|
||||||
|
assert result[0].resource_name == "GCP Project"
|
||||||
|
assert result[0].location == "global"
|
||||||
|
assert result[0].project_id == GCP_PROJECT_ID
|
||||||
|
|
||||||
|
def test_one_non_compliant_project_empty_project_name(self):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_service import Project
|
||||||
|
|
||||||
|
project = Project(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
enable_oslogin=True,
|
||||||
|
enable_oslogin_2fa=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
compute_client = mock.MagicMock()
|
||||||
|
compute_client.project_ids = [GCP_PROJECT_ID]
|
||||||
|
compute_client.compute_projects = [project]
|
||||||
|
compute_client.projects = {
|
||||||
|
GCP_PROJECT_ID: GCPProject(
|
||||||
|
id=GCP_PROJECT_ID,
|
||||||
|
number="123456789012",
|
||||||
|
name="",
|
||||||
|
labels={},
|
||||||
|
lifecycle_state="ACTIVE",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
compute_client.region = "global"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||||
|
return_value=set_mocked_gcp_provider(),
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled.compute_client",
|
||||||
|
new=compute_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
from prowler.providers.gcp.services.compute.compute_project_os_login_2fa_enabled.compute_project_os_login_2fa_enabled import (
|
||||||
|
compute_project_os_login_2fa_enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
check = compute_project_os_login_2fa_enabled()
|
||||||
|
result = check.execute()
|
||||||
|
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0].status == "FAIL"
|
||||||
|
assert search(
|
||||||
|
f"Project {project.id} does not have OS Login 2FA enabled",
|
||||||
|
result[0].status_extended,
|
||||||
|
)
|
||||||
|
assert result[0].resource_id == project.id
|
||||||
|
assert result[0].resource_name == "GCP Project"
|
||||||
|
assert result[0].location == "global"
|
||||||
|
assert result[0].project_id == GCP_PROJECT_ID
|
||||||
Reference in New Issue
Block a user