Skip to content

Lab 08 — Cloud Security Configuration Auditor

Difficulty: Advanced
Estimated Time: 150–210 minutes
Primary Language: Python
Security Domain: Cloud Security / CSPM / Security Automation / Governance
Cloud Coverage: AWS / Microsoft Azure / Google Cloud
Environment: Local controlled lab with synthetic cloud configuration data
Automation Type: Read-Only Cloud Security Assessment

Your task is to build a reusable cloud security configuration auditor.

Instead of manually checking individual cloud resources, your Python application will process configuration data representing:

AWS
MICROSOFT AZURE
GOOGLE CLOUD

and evaluate selected defensive security controls.

You will review:

IDENTITY
LOGGING
ENCRYPTION
PUBLIC EXPOSURE
NETWORK SECURITY
STORAGE SECURITY
SECURITY SERVICES
RESOURCE OWNERSHIP
SECURITY CONFIGURATION

The final workflow will be:

CLOUD CONFIGURATION
INGEST
VALIDATE
NORMALIZE
SECURITY CHECKS
FINDINGS
PRIORITIZE
REPORT

Cloud environments are dynamic.

An enterprise may have:

10 ACCOUNTS

or:

500 SUBSCRIPTIONS

or:

THOUSANDS OF PROJECTS

and potentially millions of cloud resources.

Manual inspection does not scale.

Cloud security teams therefore rely on automated configuration assessment to answer questions such as:

IS AUDIT LOGGING ENABLED?
IS STORAGE PUBLIC?
IS ENCRYPTION ENABLED?
ARE PRIVILEGED IDENTITIES PROTECTED?
ARE SECURITY SERVICES ENABLED?
ARE MANAGEMENT PORTS EXPOSED?
DO CRITICAL RESOURCES HAVE OWNERS?
ARE NETWORK CONTROLS TOO PERMISSIVE?

This lab introduces the core engineering pattern behind:

CLOUD SECURITY POSTURE MANAGEMENT

or:

CSPM

By completing this lab, you should be able to:

MODEL CLOUD SECURITY DATA
PROCESS MULTI-CLOUD CONFIGURATION
NORMALIZE PROVIDER-SPECIFIC DATA
BUILD READ-ONLY SECURITY CHECKS
REVIEW CLOUD IDENTITY
REVIEW AUDIT LOGGING
REVIEW STORAGE EXPOSURE
REVIEW ENCRYPTION
REVIEW NETWORK EXPOSURE
REVIEW SECURITY SERVICES
IDENTIFY MISSING OWNERS
BUILD A COMMON FINDING SCHEMA
ASSIGN REVIEW PRIORITIES
EXPORT CSV
EXPORT JSON
GENERATE MARKDOWN REPORTS
DESIGN FOR FUTURE CLOUD API INTEGRATION
CLOUD ENVIRONMENTS
┌──────────────┼──────────────┐
↓ ↓ ↓
AWS AZURE GCP
↓ ↓ ↓
└──────────────┼──────────────┘
CONFIGURATION DATA
VALIDATION
NORMALIZATION
SECURITY CHECK ENGINE
┌───────────┼───────────┐
↓ ↓ ↓
IDENTITY LOGGING STORAGE
↓ ↓ ↓
NETWORK ENCRYPTION SERVICES
└───────────┼───────────┘
FINDINGS
PRIORITIZE
┌───────────┼───────────┐
↓ ↓ ↓
CSV JSON MARKDOWN

This lab is:

READ ONLY

and uses:

SYNTHETIC CONFIGURATION DATA

Do not perform cloud assessment activities against accounts, subscriptions, projects, or resources unless you own them or have explicit authorization.

The auditor should:

READ
ASSESS
REPORT

It should not automatically:

DELETE RESOURCES
CHANGE IAM
DISABLE USERS
MODIFY FIREWALLS
ROTATE KEYS
BLOCK NETWORK TRAFFIC
SHUT DOWN WORKLOADS

Create:

cloud-security-auditor/
|
+-- data/
|
+-- reports/
|
+-- src/
|
+-- tests/
|
+-- README.md

Linux/macOS:

Terminal window
mkdir -p cloud-security-auditor/{data,reports,src,tests}
cd cloud-security-auditor

PowerShell:

Terminal window
mkdir cloud-security-auditor
cd cloud-security-auditor
mkdir data
mkdir reports
mkdir src
mkdir tests

Run:

Terminal window
python --version

Recommended:

Python 3.10+

03 — Define the Multi-Cloud Security Model

Section titled “03 — Define the Multi-Cloud Security Model”

For this lab, your auditor will evaluate six control families:

IDENTITY
LOGGING
ENCRYPTION
STORAGE
NETWORK
SECURITY SERVICES

The providers use different terminology.

Example:

Security Concept AWS Azure Google Cloud
Account boundary Account Subscription Project
Identity service IAM Entra ID / RBAC Cloud IAM
Audit logging CloudTrail Activity Log Cloud Audit Logs
Object storage S3 Blob Storage Cloud Storage
Security posture service Security Hub Defender for Cloud Security Command Center

Your internal security model should remain:

CONSISTENT

even when provider terminology differs.

Create:

data/aws.json

Add:

{
"provider": "aws",
"account_id": "111122223333",
"account_name": "production",
"owner": "Cloud Platform Team",
"logging": {
"cloudtrail_enabled": true,
"multi_region": true,
"log_validation": true
},
"security_services": {
"security_hub_enabled": true,
"guardduty_enabled": true
},
"identities": [
{
"name": "cloud-admin",
"type": "user",
"privileged": true,
"mfa_enabled": true,
"access_key_age_days": 24
},
{
"name": "legacy-admin",
"type": "user",
"privileged": true,
"mfa_enabled": false,
"access_key_age_days": 180
},
{
"name": "app-role",
"type": "role",
"privileged": false,
"mfa_enabled": null,
"access_key_age_days": null
}
],
"storage": [
{
"name": "prod-application-data",
"type": "s3",
"public": false,
"encrypted": true,
"criticality": "critical",
"owner": "Application Team"
},
{
"name": "legacy-public-files",
"type": "s3",
"public": true,
"encrypted": false,
"criticality": "high",
"owner": ""
}
],
"network_rules": [
{
"resource": "web-sg",
"protocol": "tcp",
"port": 443,
"source": "0.0.0.0/0",
"purpose": "public-web"
},
{
"resource": "admin-sg",
"protocol": "tcp",
"port": 22,
"source": "0.0.0.0/0",
"purpose": "administration"
}
]
}

The synthetic AWS account contains:

GOOD CLOUDTRAIL CONFIGURATION
SECURITY HUB ENABLED
GUARDDUTY ENABLED
ONE PRIVILEGED USER WITHOUT MFA
ONE OLD ACCESS KEY
ONE PUBLIC S3-LIKE BUCKET
ONE UNENCRYPTED STORAGE RESOURCE
ONE MANAGEMENT PORT OPEN TO THE INTERNET
ONE RESOURCE WITHOUT OWNER

Create:

data/azure.json

Add:

{
"provider": "azure",
"subscription_id": "00000000-1111-2222-3333-444444444444",
"subscription_name": "enterprise-prod",
"owner": "Azure Platform Team",
"logging": {
"activity_log_enabled": true,
"diagnostic_settings_enabled": false
},
"security_services": {
"defender_for_cloud_enabled": true,
"sentinel_connected": true
},
"identities": [
{
"name": "Global Admin 01",
"type": "user",
"privileged": true,
"mfa_enabled": true
},
{
"name": "Legacy Cloud Admin",
"type": "user",
"privileged": true,
"mfa_enabled": false
},
{
"name": "app-managed-identity",
"type": "managed_identity",
"privileged": false,
"mfa_enabled": null
}
],
"storage": [
{
"name": "prodstorage01",
"type": "blob",
"public": false,
"encrypted": true,
"criticality": "critical",
"owner": "Application Team"
},
{
"name": "publicarchive01",
"type": "blob",
"public": true,
"encrypted": true,
"criticality": "medium",
"owner": "Archive Team"
}
],
"network_rules": [
{
"resource": "web-nsg",
"protocol": "tcp",
"port": 443,
"source": "Internet",
"purpose": "public-web"
},
{
"resource": "management-nsg",
"protocol": "tcp",
"port": 3389,
"source": "Internet",
"purpose": "administration"
}
]
}

The Azure subscription contains:

ACTIVITY LOG ENABLED
MISSING DIAGNOSTIC SETTINGS
DEFENDER FOR CLOUD ENABLED
SENTINEL CONNECTED
PRIVILEGED USER WITHOUT MFA
PUBLIC BLOB STORAGE
RDP EXPOSED TO INTERNET

Create:

data/gcp.json

Add:

{
"provider": "gcp",
"project_id": "ghc-production-001",
"project_name": "production-platform",
"owner": "",
"logging": {
"audit_logs_enabled": true,
"data_access_logs_enabled": false
},
"security_services": {
"security_command_center_enabled": true
},
"identities": [
{
"name": "cloud-admin@example.com",
"type": "user",
"privileged": true,
"mfa_enabled": true
},
{
"name": "legacy-admin@example.com",
"type": "user",
"privileged": true,
"mfa_enabled": false
},
{
"name": "app-service-account",
"type": "service_account",
"privileged": false,
"mfa_enabled": null
}
],
"storage": [
{
"name": "prod-secure-data",
"type": "cloud-storage",
"public": false,
"encrypted": true,
"criticality": "critical",
"owner": "Data Platform Team"
},
{
"name": "public-training-content",
"type": "cloud-storage",
"public": true,
"encrypted": true,
"criticality": "low",
"owner": "Training Team"
}
],
"network_rules": [
{
"resource": "public-web-rule",
"protocol": "tcp",
"port": 443,
"source": "0.0.0.0/0",
"purpose": "public-web"
},
{
"resource": "ssh-admin-rule",
"protocol": "tcp",
"port": 22,
"source": "0.0.0.0/0",
"purpose": "administration"
}
]
}

The project includes:

AUDIT LOGGING ENABLED
DATA ACCESS LOGGING DISABLED
SECURITY COMMAND CENTER ENABLED
PROJECT OWNER MISSING
PRIVILEGED USER WITHOUT MFA
PUBLIC STORAGE
SSH OPEN TO INTERNET

Create:

src/cloud_security_auditor.py

Start with:

from pathlib import Path
from collections import Counter
import csv
import json
import logging

Add:

BASE_DIR = Path(__file__).resolve().parent.parent
DATA_DIR = BASE_DIR / "data"
REPORT_DIR = BASE_DIR / "reports"
REPORT_DIR.mkdir(
parents=True,
exist_ok=True
)

Add:

logging.basicConfig(
level=logging.INFO,
format=(
"%(asctime)s "
"%(levelname)s "
"%(message)s"
)
)

Add:

PROVIDER_FILES = [
DATA_DIR / "aws.json",
DATA_DIR / "azure.json",
DATA_DIR / "gcp.json"
]

Every finding should eventually look like:

{
"provider": "aws",
"scope": "production",
"resource_type": "identity",
"resource": "legacy-admin",
"control": "privileged-mfa",
"status": "fail",
"severity": "high",
"title": "Privileged identity does not use MFA",
"evidence": "mfa_enabled=false",
"recommendation": "Review and enforce approved strong authentication controls."
}

Without normalization:

AWS FINDING
AZURE FINDING
GCP FINDING

may all look different.

With normalization:

MULTI-CLOUD DATA
COMMON FINDING MODEL
ONE REPORTING ENGINE

Add:

def create_finding(
provider,
scope,
resource_type,
resource,
control,
status,
severity,
title,
evidence,
recommendation
):
return {
"provider":
provider,
"scope":
scope,
"resource_type":
resource_type,
"resource":
resource,
"control":
control,
"status":
status,
"severity":
severity,
"title":
title,
"evidence":
evidence,
"recommendation":
recommendation
}

Create:

def load_json_file(
path
):
with path.open(
"r",
encoding="utf-8"
) as file:
return json.load(
file
)

Improve:

def load_json_file(
path
):
try:
with path.open(
"r",
encoding="utf-8"
) as file:
return json.load(
file
)
except FileNotFoundError:
logging.error(
"Missing file: %s",
path
)
return None
except json.JSONDecodeError as error:
logging.error(
"Invalid JSON in %s: %s",
path,
error
)
return None

Create:

def load_cloud_data():
records = []
for path in PROVIDER_FILES:
data = load_json_file(
path
)
if data:
records.append(
data
)
return records

Supported values:

SUPPORTED_PROVIDERS = {
"aws",
"azure",
"gcp"
}

Create:

def validate_provider(
data
):
provider = str(
data.get(
"provider",
""
)
).strip().lower()
return (
provider
in
SUPPORTED_PROVIDERS
)

AWS uses:

account_name

Azure:

subscription_name

Google Cloud:

project_name

Create:

def get_scope_name(
data
):
provider = data[
"provider"
]
if provider == "aws":
return data.get(
"account_name",
"unknown"
)
if provider == "azure":
return data.get(
"subscription_name",
"unknown"
)
if provider == "gcp":
return data.get(
"project_name",
"unknown"
)
return "unknown"

23 — Security Control 01: Scope Ownership

Section titled “23 — Security Control 01: Scope Ownership”

Cloud accounts should have clear ownership.

Create:

def check_scope_owner(
data
):
provider = data[
"provider"
]
scope = get_scope_name(
data
)
owner = str(
data.get(
"owner",
""
)
).strip()
if owner:
return create_finding(
provider,
scope,
"cloud-scope",
scope,
"scope-owner",
"pass",
"informational",
"Cloud scope has documented owner",
f"owner={owner}",
"Maintain current ownership information."
)
return create_finding(
provider,
scope,
"cloud-scope",
scope,
"scope-owner",
"fail",
"high",
"Cloud scope has no documented owner",
"owner is empty",
"Assign an accountable business or technical owner."
)

Without clear ownership:

WHO APPROVES ACCESS?
WHO RESPONDS TO INCIDENTS?
WHO REVIEWS FINDINGS?
WHO ACCEPTS RISK?
WHO OWNS REMEDIATION?

25 — Security Control 02: Privileged MFA

Section titled “25 — Security Control 02: Privileged MFA”

Create:

def check_privileged_mfa(
data
):
findings = []
provider = data[
"provider"
]
scope = get_scope_name(
data
)
for identity in data.get(
"identities",
[]
):
if not identity.get(
"privileged"
):
continue
mfa = identity.get(
"mfa_enabled"
)
if mfa is True:
status = "pass"
severity = "informational"
title = (
"Privileged identity uses MFA"
)
recommendation = (
"Maintain strong authentication "
"and periodically review privilege."
)
else:
status = "fail"
severity = "high"
title = (
"Privileged identity does not use MFA"
)
recommendation = (
"Review the identity and enforce "
"approved strong authentication controls "
"where applicable."
)
findings.append(
create_finding(
provider,
scope,
"identity",
identity.get(
"name",
"unknown"
),
"privileged-mfa",
status,
severity,
title,
f"mfa_enabled={mfa}",
recommendation
)
)
return findings

MFA generally applies to:

HUMAN IDENTITIES

It may not apply directly to:

WORKLOAD IDENTITIES
SERVICE ACCOUNTS
MANAGED IDENTITIES
IAM ROLES

Those should instead use controls such as:

SHORT-LIVED CREDENTIALS
WORKLOAD IDENTITY
LEAST PRIVILEGE
KEY ROTATION
NO STATIC SECRETS

27 — Security Control 03: AWS Access Key Age

Section titled “27 — Security Control 03: AWS Access Key Age”

For the synthetic AWS data, evaluate long-lived human access keys.

Create:

def check_aws_access_key_age(
data
):
findings = []
if data[
"provider"
] != "aws":
return findings
scope = get_scope_name(
data
)
for identity in data.get(
"identities",
[]
):
age = identity.get(
"access_key_age_days"
)
if age is None:
continue
if age > 90:
status = "fail"
severity = "medium"
title = (
"Long-lived access key requires review"
)
recommendation = (
"Review whether the access key is still "
"required and follow the approved credential "
"rotation or workload-identity standard."
)
else:
status = "pass"
severity = "informational"
title = (
"Access key age is within lab threshold"
)
recommendation = (
"Continue credential lifecycle monitoring."
)
findings.append(
create_finding(
"aws",
scope,
"identity",
identity[
"name"
],
"access-key-age",
status,
severity,
title,
f"age_days={age}",
recommendation
)
)
return findings

The:

90 DAYS

value is a training threshold.

Use your organization’s:

IDENTITY STANDARD
CLOUD POLICY
REGULATORY REQUIREMENTS

for production decisions.

29 — Security Control 04: AWS CloudTrail

Section titled “29 — Security Control 04: AWS CloudTrail”

Create:

def check_aws_logging(
data
):
findings = []
if data[
"provider"
] != "aws":
return findings
scope = get_scope_name(
data
)
logging_config = data.get(
"logging",
{}
)
checks = [
(
"cloudtrail_enabled",
"cloudtrail-enabled",
"AWS CloudTrail is enabled"
),
(
"multi_region",
"cloudtrail-multi-region",
"CloudTrail multi-region coverage is enabled"
),
(
"log_validation",
"cloudtrail-log-validation",
"CloudTrail log validation is enabled"
)
]
for field, control, title in checks:
value = logging_config.get(
field
)
findings.append(
create_finding(
"aws",
scope,
"logging",
"CloudTrail",
control,
(
"pass"
if value
else "fail"
),
(
"informational"
if value
else "high"
),
title,
f"{field}={value}",
(
"Maintain current configuration."
if value
else
"Enable and validate required audit logging."
)
)
)
return findings

Create:

def check_azure_logging(
data
):
findings = []
if data[
"provider"
] != "azure":
return findings
scope = get_scope_name(
data
)
config = data.get(
"logging",
{}
)
activity = config.get(
"activity_log_enabled"
)
findings.append(
create_finding(
"azure",
scope,
"logging",
"Activity Log",
"activity-log-enabled",
(
"pass"
if activity
else "fail"
),
(
"informational"
if activity
else "high"
),
"Azure Activity Log configuration",
f"enabled={activity}",
(
"Maintain current logging."
if activity
else
"Enable required activity logging."
)
)
)
diagnostics = config.get(
"diagnostic_settings_enabled"
)
findings.append(
create_finding(
"azure",
scope,
"logging",
"Diagnostic Settings",
"diagnostic-settings",
(
"pass"
if diagnostics
else "fail"
),
(
"informational"
if diagnostics
else "medium"
),
"Azure diagnostic settings configuration",
f"enabled={diagnostics}",
(
"Maintain required diagnostic routing."
if diagnostics
else
"Review diagnostic settings and route "
"required logs to approved destinations."
)
)
)
return findings

31 — Security Control 06: Google Cloud Logging

Section titled “31 — Security Control 06: Google Cloud Logging”

Create:

def check_gcp_logging(
data
):
findings = []
if data[
"provider"
] != "gcp":
return findings
scope = get_scope_name(
data
)
config = data.get(
"logging",
{}
)
audit = config.get(
"audit_logs_enabled"
)
findings.append(
create_finding(
"gcp",
scope,
"logging",
"Cloud Audit Logs",
"audit-logs-enabled",
(
"pass"
if audit
else "fail"
),
(
"informational"
if audit
else "high"
),
"Google Cloud audit logging configuration",
f"enabled={audit}",
(
"Maintain audit logging."
if audit
else
"Enable required Cloud Audit Logs."
)
)
)
data_access = config.get(
"data_access_logs_enabled"
)
findings.append(
create_finding(
"gcp",
scope,
"logging",
"Data Access Logs",
"data-access-logging",
(
"pass"
if data_access
else "fail"
),
(
"informational"
if data_access
else "medium"
),
"Google Cloud Data Access logging configuration",
f"enabled={data_access}",
(
"Maintain required logging."
if data_access
else
"Review whether Data Access logs are "
"required for the project and data sensitivity."
)
)
)
return findings

Cloud audit logs help answer:

WHO?
DID WHAT?
TO WHICH RESOURCE?
WHEN?
FROM WHERE?

Without adequate logs:

INVESTIGATION

becomes significantly harder.

33 — Security Control 07: Public Storage

Section titled “33 — Security Control 07: Public Storage”

Create:

def check_public_storage(
data
):
findings = []
provider = data[
"provider"
]
scope = get_scope_name(
data
)
for storage in data.get(
"storage",
[]
):
public = storage.get(
"public"
)
if public:
status = "fail"
severity = (
"high"
if storage.get(
"criticality"
) in {
"critical",
"high"
}
else "medium"
)
title = (
"Storage resource is publicly accessible"
)
recommendation = (
"Validate whether public access is "
"business-required. Remove unnecessary "
"public exposure through approved change control."
)
else:
status = "pass"
severity = "informational"
title = (
"Storage resource is not publicly accessible"
)
recommendation = (
"Maintain least-exposure configuration."
)
findings.append(
create_finding(
provider,
scope,
"storage",
storage[
"name"
],
"storage-public-access",
status,
severity,
title,
f"public={public}",
recommendation
)
)
return findings

34 — Public Does Not Automatically Mean Wrong

Section titled “34 — Public Does Not Automatically Mean Wrong”

Some resources intentionally host:

PUBLIC WEBSITE CONTENT
DOCUMENTATION
SOFTWARE DOWNLOADS

Therefore:

PUBLIC ACCESS

means:

REVIEW

not automatically:

SECURITY INCIDENT

35 — Security Control 08: Storage Encryption

Section titled “35 — Security Control 08: Storage Encryption”

Create:

def check_storage_encryption(
data
):
findings = []
provider = data[
"provider"
]
scope = get_scope_name(
data
)
for storage in data.get(
"storage",
[]
):
encrypted = storage.get(
"encrypted"
)
findings.append(
create_finding(
provider,
scope,
"storage",
storage[
"name"
],
"storage-encryption",
(
"pass"
if encrypted
else "fail"
),
(
"informational"
if encrypted
else "high"
),
(
"Storage encryption enabled"
if encrypted
else
"Storage encryption is not enabled"
),
f"encrypted={encrypted}",
(
"Maintain encryption controls."
if encrypted
else
"Enable approved encryption for stored data."
)
)
)
return findings

Cloud encryption review should eventually include:

PROVIDER-MANAGED KEYS
CUSTOMER-MANAGED KEYS
KEY ROTATION
KEY ACCESS
KEY OWNERSHIP
BACKUP ENCRYPTION
DATA CLASSIFICATION

For this lab, use only:

ENCRYPTED = TRUE / FALSE

37 — Security Control 09: Storage Ownership

Section titled “37 — Security Control 09: Storage Ownership”

Create:

def check_storage_owner(
data
):
findings = []
provider = data[
"provider"
]
scope = get_scope_name(
data
)
for storage in data.get(
"storage",
[]
):
owner = str(
storage.get(
"owner",
""
)
).strip()
findings.append(
create_finding(
provider,
scope,
"storage",
storage[
"name"
],
"storage-owner",
(
"pass"
if owner
else "fail"
),
(
"informational"
if owner
else "medium"
),
(
"Storage resource has documented owner"
if owner
else
"Storage resource has no documented owner"
),
(
f"owner={owner}"
if owner
else "owner is empty"
),
(
"Maintain ownership information."
if owner
else
"Assign an accountable owner."
)
)
)
return findings

38 — Security Control 10: Internet Management Ports

Section titled “38 — Security Control 10: Internet Management Ports”

Management services such as:

SSH
RDP

should not generally be broadly exposed without a justified and secured architecture.

Define:

MANAGEMENT_PORTS = {
22: "SSH",
3389: "RDP"
}

Create:

BROAD_SOURCES = {
"0.0.0.0/0",
"::/0",
"internet",
"Internet"
}

Create:

def check_management_exposure(
data
):
findings = []
provider = data[
"provider"
]
scope = get_scope_name(
data
)
for rule in data.get(
"network_rules",
[]
):
port = rule.get(
"port"
)
source = rule.get(
"source"
)
if (
port
not in
MANAGEMENT_PORTS
):
continue
exposed = (
source
in
BROAD_SOURCES
)
service = (
MANAGEMENT_PORTS[
port
]
)
findings.append(
create_finding(
provider,
scope,
"network-rule",
rule[
"resource"
],
"management-port-exposure",
(
"fail"
if exposed
else "pass"
),
(
"high"
if exposed
else "informational"
),
(
f"{service} management port is broadly exposed"
if exposed
else
f"{service} management port is restricted"
),
(
f"port={port}; "
f"source={source}"
),
(
"Restrict administrative access through "
"approved management paths, trusted sources, "
"or identity-aware access controls."
if exposed
else
"Maintain restricted administration paths."
)
)
)
return findings

Your synthetic cloud data also contains:

443 FROM INTERNET

for public web services.

That may be fully expected.

Therefore do not blindly treat:

0.0.0.0/0

as a vulnerability.

Interpret:

SOURCE
+
PORT
+
RESOURCE PURPOSE

42 — Security Control 11: AWS Security Services

Section titled “42 — Security Control 11: AWS Security Services”

Create:

def check_aws_security_services(
data
):
findings = []
if data[
"provider"
] != "aws":
return findings
scope = get_scope_name(
data
)
services = data.get(
"security_services",
{}
)
checks = [
(
"security_hub_enabled",
"Security Hub",
"security-hub-enabled"
),
(
"guardduty_enabled",
"GuardDuty",
"guardduty-enabled"
)
]
for field, resource, control in checks:
enabled = services.get(
field
)
findings.append(
create_finding(
"aws",
scope,
"security-service",
resource,
control,
(
"pass"
if enabled
else "fail"
),
(
"informational"
if enabled
else "medium"
),
(
f"{resource} is enabled"
if enabled
else
f"{resource} is not enabled"
),
f"enabled={enabled}",
(
"Maintain current security monitoring."
if enabled
else
"Review whether the service is required "
"by the approved AWS security baseline."
)
)
)
return findings

43 — Security Control 12: Azure Security Services

Section titled “43 — Security Control 12: Azure Security Services”

Create:

def check_azure_security_services(
data
):
findings = []
if data[
"provider"
] != "azure":
return findings
scope = get_scope_name(
data
)
services = data.get(
"security_services",
{}
)
checks = [
(
"defender_for_cloud_enabled",
"Defender for Cloud",
"defender-for-cloud"
),
(
"sentinel_connected",
"Microsoft Sentinel",
"sentinel-connected"
)
]
for field, resource, control in checks:
enabled = services.get(
field
)
findings.append(
create_finding(
"azure",
scope,
"security-service",
resource,
control,
(
"pass"
if enabled
else "fail"
),
(
"informational"
if enabled
else "medium"
),
(
f"{resource} is enabled"
if enabled
else
f"{resource} requires review"
),
f"enabled={enabled}",
(
"Maintain current configuration."
if enabled
else
"Review against the approved Azure "
"security monitoring baseline."
)
)
)
return findings

44 — Security Control 13: Google Cloud Security Services

Section titled “44 — Security Control 13: Google Cloud Security Services”

Create:

def check_gcp_security_services(
data
):
findings = []
if data[
"provider"
] != "gcp":
return findings
scope = get_scope_name(
data
)
services = data.get(
"security_services",
{}
)
enabled = services.get(
"security_command_center_enabled"
)
findings.append(
create_finding(
"gcp",
scope,
"security-service",
"Security Command Center",
"security-command-center",
(
"pass"
if enabled
else "fail"
),
(
"informational"
if enabled
else "medium"
),
(
"Security Command Center is enabled"
if enabled
else
"Security Command Center requires review"
),
f"enabled={enabled}",
(
"Maintain current security posture monitoring."
if enabled
else
"Review against the approved Google Cloud "
"security baseline."
)
)
)
return findings

Create:

def assess_cloud_scope(
data
):
findings = []
if not validate_provider(
data
):
return findings
findings.append(
check_scope_owner(
data
)
)
findings.extend(
check_privileged_mfa(
data
)
)
findings.extend(
check_public_storage(
data
)
)
findings.extend(
check_storage_encryption(
data
)
)
findings.extend(
check_storage_owner(
data
)
)
findings.extend(
check_management_exposure(
data
)
)
provider = data[
"provider"
]
if provider == "aws":
findings.extend(
check_aws_access_key_age(
data
)
)
findings.extend(
check_aws_logging(
data
)
)
findings.extend(
check_aws_security_services(
data
)
)
elif provider == "azure":
findings.extend(
check_azure_logging(
data
)
)
findings.extend(
check_azure_security_services(
data
)
)
elif provider == "gcp":
findings.extend(
check_gcp_logging(
data
)
)
findings.extend(
check_gcp_security_services(
data
)
)
return findings

Create:

def run_assessment():
all_findings = []
for cloud_data in load_cloud_data():
provider = cloud_data.get(
"provider",
"unknown"
)
logging.info(
"Assessing provider: %s",
provider
)
all_findings.extend(
assess_cloud_scope(
cloud_data
)
)
return all_findings

Temporarily:

findings = run_assessment()
print(
json.dumps(
findings,
indent=2
)
)

Run:

Terminal window
python src/cloud_security_auditor.py

You should identify findings around:

AWS LEGACY ADMIN MFA
AWS OLD ACCESS KEY
AWS PUBLIC STORAGE
AWS UNENCRYPTED STORAGE
AWS SSH INTERNET EXPOSURE
AZURE PRIVILEGED MFA
AZURE DIAGNOSTIC SETTINGS
AZURE PUBLIC STORAGE
AZURE RDP INTERNET EXPOSURE
GCP PROJECT OWNER
GCP PRIVILEGED MFA
GCP DATA ACCESS LOGGING
GCP PUBLIC STORAGE
GCP SSH INTERNET EXPOSURE

Create:

def get_failed_findings(
findings
):
return [
item
for item in findings
if item[
"status"
] == "fail"
]

Create:

def count_failures_by_provider(
findings
):
return Counter(
item[
"provider"
]
for item
in get_failed_findings(
findings
)
)

Create:

def count_failures_by_severity(
findings
):
return Counter(
item[
"severity"
]
for item
in get_failed_findings(
findings
)
)

Use:

resource_type

Create:

def count_failures_by_type(
findings
):
return Counter(
item[
"resource_type"
]
for item
in get_failed_findings(
findings
)
)

Create:

SEVERITY_SCORE = {
"critical": 100,
"high": 75,
"medium": 50,
"low": 25,
"informational": 0
}

Create:

def add_priority_scores(
findings
):
results = []
for finding in findings:
item = finding.copy()
item[
"priority_score"
] = (
SEVERITY_SCORE.get(
item[
"severity"
],
0
)
if item[
"status"
] == "fail"
else 0
)
results.append(
item
)
return results

This is intentionally simple.

Production prioritization should consider:

RESOURCE CRITICALITY
DATA CLASSIFICATION
INTERNET EXPOSURE
BUSINESS SERVICE
KNOWN ATTACK PATHS
THREAT ACTIVITY
COMPENSATING CONTROLS

Create:

def build_summary(
findings
):
failures = get_failed_findings(
findings
)
return {
"total_checks":
len(
findings
),
"passed_checks":
len([
item
for item
in findings
if item[
"status"
] == "pass"
]),
"failed_checks":
len(
failures
),
"failures_by_provider":
dict(
count_failures_by_provider(
findings
)
),
"failures_by_severity":
dict(
count_failures_by_severity(
findings
)
),
"failures_by_resource_type":
dict(
count_failures_by_type(
findings
)
)
}

Create:

def export_findings_csv(
findings
):
output = (
REPORT_DIR /
"cloud-security-findings.csv"
)
fields = [
"provider",
"scope",
"resource_type",
"resource",
"control",
"status",
"severity",
"priority_score",
"title",
"evidence",
"recommendation"
]
with output.open(
"w",
encoding="utf-8",
newline=""
) as file:
writer = csv.DictWriter(
file,
fieldnames=fields
)
writer.writeheader()
writer.writerows(
findings
)

Create:

def export_failed_findings(
findings
):
output = (
REPORT_DIR /
"cloud-security-review-queue.csv"
)
failures = sorted(
get_failed_findings(
findings
),
key=lambda item:
item[
"priority_score"
],
reverse=True
)
fields = [
"provider",
"scope",
"resource_type",
"resource",
"control",
"severity",
"priority_score",
"title",
"evidence",
"recommendation"
]
with output.open(
"w",
encoding="utf-8",
newline=""
) as file:
writer = csv.DictWriter(
file,
fieldnames=fields,
extrasaction="ignore"
)
writer.writeheader()
writer.writerows(
failures
)

Create:

def export_json(
findings,
summary
):
output = (
REPORT_DIR /
"cloud-security-assessment.json"
)
with output.open(
"w",
encoding="utf-8"
) as file:
json.dump(
{
"summary":
summary,
"findings":
findings
},
file,
indent=2
)

Create:

def generate_markdown_report(
findings,
summary
):
failures = sorted(
get_failed_findings(
findings
),
key=lambda item:
item[
"priority_score"
],
reverse=True
)
lines = []
lines.append(
"# Cloud Security Configuration Assessment"
)
lines.append("")
lines.append(
"## Executive Summary"
)
lines.append("")
lines.append(
f"- Total checks: "
f"{summary['total_checks']}"
)
lines.append(
f"- Passed checks: "
f"{summary['passed_checks']}"
)
lines.append(
f"- Failed checks: "
f"{summary['failed_checks']}"
)
lines.append("")
lines.append(
"## Findings Requiring Review"
)
lines.append("")
for item in failures:
lines.append(
f"### {item['severity'].upper()} — "
f"{item['title']}"
)
lines.append("")
lines.append(
f"- Provider: {item['provider']}"
)
lines.append(
f"- Scope: {item['scope']}"
)
lines.append(
f"- Resource: {item['resource']}"
)
lines.append(
f"- Control: {item['control']}"
)
lines.append(
f"- Evidence: {item['evidence']}"
)
lines.append(
f"- Recommendation: "
f"{item['recommendation']}"
)
lines.append("")
lines.append(
"## Assessment Guidance"
)
lines.append("")
lines.append(
"This lab identifies configuration states "
"that require review. A failed check does not "
"automatically prove compromise or business risk. "
"Validate resource purpose, approved exceptions, "
"data classification, exposure, compensating controls, "
"and organizational cloud-security standards before "
"making remediation decisions."
)
output = (
REPORT_DIR /
"cloud-security-assessment.md"
)
output.write_text(
"\n".join(
lines
),
encoding="utf-8"
)

Create:

def main():
logging.info(
"Cloud security assessment started"
)
findings = run_assessment()
findings = add_priority_scores(
findings
)
summary = build_summary(
findings
)
export_findings_csv(
findings
)
export_failed_findings(
findings
)
export_json(
findings,
summary
)
generate_markdown_report(
findings,
summary
)
logging.info(
"Cloud security assessment completed"
)
print(
f"Reports saved to: "
f"{REPORT_DIR}"
)

Add:

if __name__ == "__main__":
main()

Run:

Terminal window
python src/cloud_security_auditor.py

Expected:

Cloud security assessment started
Assessing provider: aws
Assessing provider: azure
Assessing provider: gcp
Cloud security assessment completed

Expected:

reports/
|
+-- cloud-security-findings.csv
|
+-- cloud-security-review-queue.csv
|
+-- cloud-security-assessment.json
|
+-- cloud-security-assessment.md

The review queue should prioritize failed checks such as:

PRIVILEGED USER WITHOUT MFA
MANAGEMENT PORT INTERNET EXPOSURE
UNENCRYPTED STORAGE
PUBLIC HIGH-CRITICALITY STORAGE
MISSING CLOUD OWNER

Example:

AWS
Passed: X
Failed: X
AZURE
Passed: X
Failed: X
GCP
Passed: X
Failed: X

Create:

def provider_summary(
findings
):
summary = {}
for provider in (
"aws",
"azure",
"gcp"
):
provider_findings = [
item
for item
in findings
if item[
"provider"
] == provider
]
summary[
provider
] = {
"total":
len(
provider_findings
),
"passed":
len([
item
for item
in provider_findings
if item[
"status"
] == "pass"
]),
"failed":
len([
item
for item
in provider_findings
if item[
"status"
] == "fail"
])
}
return summary

Create:

def calculate_pass_rate(
findings
):
if not findings:
return 0.0
passed = len([
item
for item
in findings
if item[
"status"
] == "pass"
])
return round(
100.0 *
passed
/
len(findings),
2
)

68 — Be Careful With Compliance Percentages

Section titled “68 — Be Careful With Compliance Percentages”

A result such as:

90% PASS

does not automatically mean:

LOW RISK

The remaining 10% could contain:

PUBLIC ADMINISTRATIVE ACCESS
NO AUDIT LOGGING
CRITICAL PUBLIC STORAGE

Always review the actual findings.

Your code currently says:

IF CONDITION
THEN FINDING

A more mature architecture uses:

POLICY BASELINE
EXPECTED STATE
CURRENT STATE
COMPARE
FINDING

Create:

data/baseline.json

Add:

{
"privileged_mfa_required": true,
"public_management_ports_allowed": false,
"storage_encryption_required": true,
"resource_owner_required": true,
"cloud_audit_logging_required": true
}

Without a baseline:

SECURITY POLICY
IS HARD-CODED
INSIDE PYTHON

With a baseline:

POLICY
CONFIGURATION
AUDITOR

This is easier to:

REVIEW
VERSION
APPROVE
CHANGE
SECURITY REQUIREMENT
MACHINE-READABLE POLICY
AUTOMATED CHECK
EVIDENCE
FINDING

A professional finding should have identifiers such as:

IAM-001
LOG-001
STO-001
NET-001

Example:

"control_id": "IAM-001"

They enable:

TRACKING
MAPPING
REPORTING
EXCEPTIONS
RETESTING

Future findings may include:

{
"control_id": "LOG-001",
"frameworks": {
"CIS": ["example-control"],
"NIST": ["AU"],
"ISO27001": ["logging-related-control"]
}
}

Do not invent certification compliance simply because a technical check passes.

Remember:

TECHNICAL CONTROL CHECK
FULL COMPLIANCE

Compliance may require:

POLICY
PROCESS
EVIDENCE
APPROVAL
OWNERSHIP
MONITORING
REVIEW
DOCUMENTATION

Real enterprises need exceptions.

Example:

{
"control": "storage-public-access",
"resource": "public-training-content",
"approved": true,
"reason": "Public training content",
"expires": "2026-12-31"
}
FINDING
BUSINESS REQUIREMENT?
EXCEPTION REQUEST
RISK REVIEW
APPROVAL
EXPIRY
PERIODIC REVIEW

79 — Never Make Exceptions Permanent by Default

Section titled “79 — Never Make Exceptions Permanent by Default”

Exceptions should have:

OWNER
APPROVER
JUSTIFICATION
EXPIRY DATE
COMPENSATING CONTROLS

80 — Add Resource Criticality to Findings

Section titled “80 — Add Resource Criticality to Findings”

Storage already includes:

criticality

Future finding schema could include:

resource_criticality

Then your prioritization can distinguish:

PUBLIC LOW-RISK CONTENT

from:

PUBLIC CRITICAL DATABASE BACKUP

Example training model:

HIGH SEVERITY
+
INTERNET EXPOSED
=
HIGHER REVIEW PRIORITY

A mature model can consider:

CONTROL FAILURE
RESOURCE CRITICALITY
PUBLIC EXPOSURE
DATA SENSITIVITY
PRIVILEGE
THREAT CONTEXT
BUSINESS SERVICE
EXCEPTION STATUS

Count privileged identities by provider.

Create:

def count_privileged_identities(
cloud_data
):
return len([
identity
for identity
in cloud_data.get(
"identities",
[]
)
if identity.get(
"privileged"
)
])

A useful metric:

PRIVILEGED IDENTITIES
/
TOTAL HUMAN IDENTITIES

A high ratio can indicate excessive privilege or poor role design.

Do not use the metric alone as proof of a security problem.

Future checks could identify:

STATIC SERVICE ACCOUNT KEYS
LONG-LIVED ACCESS KEYS
OVER-PRIVILEGED ROLES
UNUSED SERVICE ACCOUNTS
CROSS-ACCOUNT TRUST
IDENTITY
REQUIRED JOB
REQUIRED ACTIONS
REQUIRED RESOURCES
MINIMUM PERMISSION

Future network checks could evaluate:

ANY-ANY RULES
ALL PORTS
MANAGEMENT PORTS
PUBLIC DATABASE PORTS
UNRESTRICTED EGRESS
UNUSED SECURITY GROUPS
NETWORK LOGGING

Examples that may require review when broadly exposed include:

1433
1521
3306
5432
27017

Do not use the presence of a port alone to conclude a vulnerability.

Validate:

SERVICE
SOURCE RANGE
FIREWALL
AUTHENTICATION
BUSINESS PURPOSE

Future checks could evaluate:

LOG RETENTION
CENTRAL LOG ROUTING
IMMUTABLE STORAGE
LOG ENCRYPTION
ADMINISTRATIVE EVENT COVERAGE
DATA ACCESS EVENT COVERAGE

Future checks could review:

DISK ENCRYPTION
DATABASE ENCRYPTION
BACKUP ENCRYPTION
QUEUE ENCRYPTION
SECRET ENCRYPTION
CUSTOMER-MANAGED KEYS

Future AWS checks:

SECURITY HUB
GUARDDUTY
CONFIG
INSPECTOR
MACIE

Future Azure checks:

DEFENDER FOR CLOUD
SENTINEL
ENTRA IDENTITY PROTECTION
POLICY

Future Google Cloud checks:

SECURITY COMMAND CENTER
EVENT THREAT DETECTION
ORG POLICY
CLOUD ASSET INVENTORY

Create:

data/invalid.json

with:

{
"provider": "unknown-cloud"
}

Confirm:

NO ASSESSMENT

is generated for unsupported providers.

Remove:

logging

from a copy of one provider file.

Your code should:

FAIL THE RELEVANT CHECKS
OR
MARK DATA UNAVAILABLE

rather than crash.

This distinction matters.

logging_enabled = false

means:

CONTROL DISABLED

but:

logging field missing

may mean:

COLLECTION FAILED

or:

DATA UNAVAILABLE

These are different.

A mature finding model should support:

PASS
FAIL
UNKNOWN
NOT_APPLICABLE

Do not convert:

WE COULD NOT CHECK IT

into:

IT IS SECURE

Example:

MFA CHECK

for:

MANAGED IDENTITY

may be:

NOT APPLICABLE

rather than:

PASS

or:

FAIL

Future schema:

{
"status": "pass|fail|unknown|not_applicable"
}

This makes the assessment more accurate.

A cloud finding should eventually record:

COLLECTION TIME

Create:

from datetime import datetime, timezone

Then:

COLLECTION_TIME = (
datetime.now(
timezone.utc
).isoformat()
)

Cloud configuration can change quickly.

A finding from:

MONDAY

may no longer exist:

FRIDAY

Always know:

WHEN WAS THIS STATE OBSERVED?

Future finding schema should include:

source

Examples:

aws-api
azure-api
gcp-api
configuration-export

Consider preserving:

RAW API RESPONSE

along with:

NORMALIZED FINDING

for troubleshooting and audit purposes.

For important assessment artifacts, create:

SHA-256 HASH

to support integrity checking.

Example:

import hashlib

Create:

def sha256_file(
path
):
digest = hashlib.sha256()
with path.open(
"rb"
) as file:
for block in iter(
lambda:
file.read(
8192
),
b""
):
digest.update(
block
)
return digest.hexdigest()

105 — Cloud API Integration Architecture

Section titled “105 — Cloud API Integration Architecture”

Your synthetic JSON files represent what would eventually come from:

AWS APIs
AZURE RESOURCE MANAGER / MICROSOFT GRAPH
GOOGLE CLOUD APIs

Architecture:

CLOUD API
COLLECTOR
NORMALIZER
CHECK ENGINE
FINDINGS

When moving to real cloud APIs, create credentials with:

READ-ONLY SECURITY INVENTORY

permissions where possible.

Do not give a posture-auditing script:

ADMINISTRATOR

simply because it is convenient.

Conceptually:

AWS API
IAM
CLOUDTRAIL
S3
EC2
SECURITY HUB
GUARDDUTY

The collector should:

DESCRIBE
LIST
GET

rather than:

DELETE
MODIFY
STOP

Conceptually:

AZURE APIs
SUBSCRIPTIONS
RBAC
STORAGE
NETWORK
ACTIVITY LOG
DEFENDER FOR CLOUD

Use:

READER

or another appropriately scoped read role rather than broad contributor permissions.

Conceptually:

GOOGLE CLOUD APIs
IAM POLICY
LOGGING
CLOUD STORAGE
VPC FIREWALL
SECURITY COMMAND CENTER

Use minimum viewer-style roles required for the assessment.

Prefer:

WORKLOAD IDENTITY
MANAGED IDENTITY
SHORT-LIVED CREDENTIALS
FEDERATED ACCESS

over long-lived:

STATIC CLOUD ACCESS KEYS

A mature enterprise auditor may handle:

100 AWS ACCOUNTS
50 AZURE SUBSCRIPTIONS
80 GCP PROJECTS

Architecture:

ORGANIZATION INVENTORY
ACCOUNT / SUBSCRIPTION / PROJECT LIST
READ-ONLY COLLECTION
NORMALIZATION
CENTRAL FINDINGS

Cloud APIs have:

RATE LIMITS
THROTTLING
PAGINATION

Reuse lessons from:

Lab 07 — Security API Integration

Use retries only for:

TRANSIENT FAILURES
THROTTLING
SERVER ERRORS

not:

ACCESS DENIED
BAD REQUEST
INVALID CONFIGURATION

114 — Access Denied Is Important Evidence

Section titled “114 — Access Denied Is Important Evidence”

If an auditor receives:

403
ACCESS DENIED

do not silently treat the resource as:

SECURE

Record:

COLLECTION GAP

Track:

EXPECTED CLOUD SCOPES
SUCCESSFULLY ASSESSED
FAILED COLLECTIONS
PARTIAL COLLECTIONS
AWS Accounts Expected: 10
Assessed: 9
Failed: 1

A:

90% assessment coverage

must not be represented as:

100% cloud security visibility

Run the auditor regularly and compare:

YESTERDAY

with:

TODAY

Identify:

NEW PUBLIC STORAGE
NEW ADMIN
LOGGING DISABLED
NEW INTERNET RULE
SECURITY SERVICE DISABLED
KNOWN STATE
TIME
NEW STATE
COMPARE
SECURITY DRIFT

You can store:

approved-baseline.json

and compare each cloud scope against it.

Example:

EXPECTED:
CloudTrail = Enabled
CURRENT:
CloudTrail = Disabled
RESULT:
FAIL

Your auditor could later identify:

CONTROL PASSED YESTERDAY
CONTROL FAILED TODAY

This should usually receive higher attention than a long-standing known exception.

To track the same finding over time:

provider
+
scope
+
control
+
resource

Create:

def finding_fingerprint(
finding
):
return "|".join([
finding[
"provider"
],
finding[
"scope"
],
finding[
"control"
],
finding[
"resource"
]
])

They help distinguish:

NEW FINDING
EXISTING FINDING
RESOLVED FINDING
REOPENED FINDING

A future CSPM-like workflow:

NEW
OPEN
ACKNOWLEDGED
REMEDIATION
RESOLVED
VERIFIED

Never assume:

TICKET CLOSED
=
CLOUD FINDING FIXED

Instead:

RE-RUN CHECK
CURRENT CONFIGURATION
PASS?
VERIFIED

For this lab:

AUDITOR
FINDING
ANALYST

For future remediation:

FINDING
VALIDATE
OWNER
CHANGE APPROVAL
REMEDIATE
VERIFY

126 — Do Not Build Automatic Destructive Remediation Yet

Section titled “126 — Do Not Build Automatic Destructive Remediation Yet”

Avoid:

PUBLIC STORAGE FOUND
AUTOMATICALLY DELETE BUCKET

or:

ADMIN PORT FOUND
DELETE FIREWALL RULE

These actions can cause:

OUTAGES
DATA LOSS
BROKEN APPLICATIONS
LOCKOUT

Lower-risk actions can include:

CREATE TICKET
SEND REVIEW NOTIFICATION
ADD REPORT TAG
REQUEST OWNER REVIEW

rather than changing cloud configuration directly.

Create a synthetic storage object:

{
"name": "test-bucket",
"public": true,
"encrypted": true,
"criticality": "critical",
"owner": "Security Team"
}

Expected:

PUBLIC ACCESS
→ FAIL

Set:

encrypted = false

Expected:

STORAGE ENCRYPTION
→ FAIL

Set:

owner = ""

Expected:

OWNER CONTROL
→ FAIL

Create:

privileged = true
mfa_enabled = false

Expected:

PRIVILEGED MFA
→ FAIL

Create:

privileged = false
mfa_enabled = false

Your current control should not generate a privileged-MFA failure.

Create:

port = 22
source = 0.0.0.0/0

Expected:

HIGH REVIEW FINDING

Change:

source = 10.10.0.0/16

Expected:

PASS

under the simple training check.

A production check should still validate whether the internal range is appropriate.

Create:

port = 443
source = 0.0.0.0/0

Your management-port check should:

NOT FLAG IT

simply because it is public.

Rename:

gcp.json

Run the script.

Expected:

ERROR LOGGED
AWS AND AZURE STILL ASSESSED

Break one JSON file intentionally.

Verify:

INVALID JSON LOGGED

and the remaining providers continue.

Then restore the file.

Create:

tests/test_cloud_security_auditor.py
def test_provider_validation():
assert validate_provider(
{
"provider":
"aws"
}
)
def test_invalid_provider():
assert not validate_provider(
{
"provider":
"unknown"
}
)
def test_missing_owner():
data = {
"provider": "gcp",
"project_name":
"training",
"owner": ""
}
finding = (
check_scope_owner(
data
)
)
assert (
finding[
"status"
]
== "fail"
)

Build a minimal cloud object with:

SSH
0.0.0.0/0

and verify:

FAIL

Create:

encrypted = false

and verify:

FAIL

Create:

public = true

and confirm:

FAIL

145 — Challenge 01 — Add Database Checks

Section titled “145 — Challenge 01 — Add Database Checks”

Extend the cloud dataset with:

databases

Evaluate:

PUBLIC EXPOSURE
ENCRYPTION
BACKUP
OWNER

146 — Challenge 02 — Add Compute Checks

Section titled “146 — Challenge 02 — Add Compute Checks”

Add:

virtual_machines

or:

instances

Review:

PUBLIC IP
DISK ENCRYPTION
OWNER
CRITICALITY
SECURITY AGENT

147 — Challenge 03 — Add Secret Management

Section titled “147 — Challenge 03 — Add Secret Management”

Add:

secrets

Review:

ROTATION
AGE
OWNER
ACCESS MODEL

Do not include actual secret values in your dataset or reports.

148 — Challenge 04 — Add Key Management

Section titled “148 — Challenge 04 — Add Key Management”

Model:

KMS KEYS
KEY VAULT KEYS
CLOUD KMS KEYS

Review:

ROTATION
ENABLED STATUS
OWNER
ACCESS

149 — Challenge 05 — Add Database Public Exposure

Section titled “149 — Challenge 05 — Add Database Public Exposure”

Create a check for synthetic database resources where:

public = true

and criticality is:

high

or:

critical

150 — Challenge 06 — Add Resource Tags

Section titled “150 — Challenge 06 — Add Resource Tags”

Evaluate tags such as:

owner
environment
criticality
cost_center
data_classification

151 — Challenge 07 — Add Environment Awareness

Section titled “151 — Challenge 07 — Add Environment Awareness”

A security finding on:

Production

may be prioritized differently from:

Sandbox

Add:

environment

to the finding schema.

152 — Challenge 08 — Add Data Classification

Section titled “152 — Challenge 08 — Add Data Classification”

Storage could contain:

Public
Internal
Confidential
Restricted

Then prioritize public exposure accordingly.

Create:

data/exceptions.json

and suppress or annotate only:

APPROVED
NON-EXPIRED

exceptions.

154 — Challenge 10 — Add Baseline Version

Section titled “154 — Challenge 10 — Add Baseline Version”

Add:

baseline_version

to every report.

Example:

Cloud Security Baseline v1.2

Generate:

cloud-security-assessment.html

with sections for:

EXECUTIVE SUMMARY
AWS
AZURE
GCP
HIGH FINDINGS
MEDIUM FINDINGS
PASSED CONTROLS

Store findings in:

cloud-security.db

Then run:

SQL ANALYTICS

across cloud providers.

Architecture:

AWS
AZURE
GCP
NORMALIZED FINDINGS
SQLITE
SECURITY ANALYTICS

157 — Challenge 13 — Add API Collectors

Section titled “157 — Challenge 13 — Add API Collectors”

Replace static files gradually.

Start:

STATIC JSON

Then:

AUTHORIZED READ-ONLY CLOUD API

Keep the:

CHECK ENGINE

unchanged.

AWS COLLECTOR
NORMALIZED CLOUD MODEL
AZURE COLLECTOR
NORMALIZED CLOUD MODEL
GCP COLLECTOR
NORMALIZED CLOUD MODEL
CHECK ENGINE

Keep:

COLLECTION
NORMALIZATION
ASSESSMENT
REPORTING

separate.

Do not create one giant function that does everything.

A mature version could look like:

cloud-security-auditor/
|
+-- collectors/
| +-- aws.py
| +-- azure.py
| +-- gcp.py
|
+-- checks/
| +-- identity.py
| +-- logging.py
| +-- network.py
| +-- storage.py
| +-- encryption.py
|
+-- reporting/
| +-- csv_report.py
| +-- json_report.py
| +-- markdown_report.py
|
+-- policies/
| +-- baseline.json
|
+-- src/
| +-- main.py

As your auditor grows from:

10 CHECKS

to:

500 CHECKS

maintainability becomes critical.

Use clear names such as:

IAM-001 — Privileged MFA
LOG-001 — Audit Logging
STO-001 — Public Storage
ENC-001 — Storage Encryption
NET-001 — Public Management Port
GOV-001 — Resource Ownership

Every failed check should answer:

WHAT DID YOU OBSERVE?

Example:

resource=legacy-admin
privileged=true
mfa_enabled=false

Recommendations should be:

ACTIONABLE

but not blindly destructive.

Prefer:

Review the administrative access path and
restrict broad management exposure through
the approved cloud change process.

instead of:

DELETE RULE NOW

When reviewing a finding, ask:

IS THE CONFIGURATION REAL?
IS THE RESOURCE STILL ACTIVE?
IS THERE AN APPROVED EXCEPTION?
WHAT BUSINESS SERVICE DEPENDS ON IT?
WHAT DATA DOES IT HANDLE?
IS IT INTERNET EXPOSED?
WHAT COMPENSATING CONTROLS EXIST?
WHO OWNS REMEDIATION?

Do not memorize only provider product names.

Think:

IDENTITY
LOGGING
NETWORK
STORAGE
ENCRYPTION
MONITORING
OWNERSHIP

Then map each cloud provider to those control families.

Your cloud provider secures:

THE CLOUD PLATFORM

but customers remain responsible for many areas including:

IDENTITY CONFIGURATION
RESOURCE PERMISSIONS
DATA ACCESS
NETWORK CONFIGURATION
WORKLOAD SECURITY
LOGGING CONFIGURATION

depending on the service model.

168 — Cloud Misconfiguration Is Contextual

Section titled “168 — Cloud Misconfiguration Is Contextual”

Examples:

PUBLIC OBJECT STORAGE

may be:

INTENTIONAL

for a public website.

But:

PUBLIC OBJECT STORAGE
+
RESTRICTED CUSTOMER DATA

would be a much more serious scenario.

CONFIGURATION
+
RESOURCE PURPOSE
+
DATA CLASSIFICATION
+
EXPOSURE
+
IDENTITY
+
BUSINESS CRITICALITY
=
SECURITY RISK CONTEXT

Avoid:

USING ADMIN CREDENTIALS FOR READ-ONLY AUDITS
HARDCODING CLOUD KEYS
NO PAGINATION
NO RATE-LIMIT HANDLING
TREATING MISSING DATA AS PASS
TREATING EVERY PUBLIC RESOURCE AS VULNERABLE
NO EXCEPTION PROCESS
NO RESOURCE OWNERSHIP
NO COLLECTION TIMESTAMP
NO BASELINE VERSION
NO EVIDENCE
AUTOMATICALLY CHANGING CLOUD RESOURCES
NO REMEDIATION VERIFICATION

Create:

README.md

Include:

PROJECT OVERVIEW
SECURITY PURPOSE
AUTHORIZED USE
MULTI-CLOUD MODEL
SUPPORTED PROVIDERS
SUPPORTED CONTROLS
INPUT DATA
HOW TO RUN
OUTPUT REPORTS
PRIORITY MODEL
SECURITY CONSIDERATIONS
LIMITATIONS
FUTURE API INTEGRATION

Include:

This project performs defensive, read-only
cloud configuration assessment.
The baseline lab uses synthetic configuration
data and does not modify cloud resources.
Any future integration with real cloud APIs
must use explicitly authorized, least-privilege,
read-only credentials wherever possible.

Your README should list:

GOV-001
Cloud scope ownership
IAM-001
Privileged MFA
IAM-002
AWS access key age
LOG-001
Audit logging
LOG-002
Enhanced diagnostic / data access logging
STO-001
Public storage
ENC-001
Storage encryption
GOV-002
Storage ownership
NET-001
Broad management-port exposure
MON-001
Cloud security service enablement

The lab does not provide:

COMPLETE CIS BENCHMARK COVERAGE
FULL CLOUD API INTEGRATION
FULL ORGANIZATION HIERARCHY
KUBERNETES SECURITY
CONTAINER SECURITY
SERVERLESS SECURITY
DATABASE CONFIGURATION ASSESSMENT
SECRET SCANNING
VULNERABILITY SCANNING
ATTACK PATH ANALYSIS
FULL COMPLIANCE CERTIFICATION

Your final project should contain:

AWS SYNTHETIC CONFIGURATION
AZURE SYNTHETIC CONFIGURATION
GCP SYNTHETIC CONFIGURATION
PYTHON AUDITOR
NORMALIZED FINDINGS
REVIEW QUEUE
JSON REPORT
MARKDOWN ASSESSMENT
UNIT TESTS
README
ARCHITECTURE DIAGRAM
cloud-security-auditor/
|
+-- data/
| +-- aws.json
| +-- azure.json
| +-- gcp.json
| +-- baseline.json
|
+-- src/
| +-- cloud_security_auditor.py
|
+-- tests/
| +-- test_cloud_security_auditor.py
|
+-- reports/
| +-- cloud-security-findings.csv
| +-- cloud-security-review-queue.csv
| +-- cloud-security-assessment.json
| +-- cloud-security-assessment.md
|
+-- README.md
|
+-- architecture.md

Confirm:

  • Python environment prepared
  • Lab directory created
  • AWS dataset created
  • Azure dataset created
  • Google Cloud dataset created
  • Multi-cloud model understood
  • JSON loading implemented
  • Invalid JSON handled
  • Provider validation implemented
  • Common finding schema created
  • Scope ownership reviewed
  • Privileged MFA reviewed
  • AWS access-key age reviewed
  • AWS CloudTrail reviewed
  • Azure Activity Log reviewed
  • Azure diagnostic settings reviewed
  • Google Cloud Audit Logs reviewed
  • Google Cloud Data Access logs reviewed
  • Public storage reviewed
  • Storage encryption reviewed
  • Storage ownership reviewed
  • Management-port exposure reviewed
  • AWS Security Hub reviewed
  • AWS GuardDuty reviewed
  • Azure Defender for Cloud reviewed
  • Microsoft Sentinel connection reviewed
  • Google Security Command Center reviewed
  • Findings normalized
  • Failures counted by provider
  • Failures counted by severity
  • Priority scores added
  • CSV report generated
  • Review queue generated
  • JSON report generated
  • Markdown report generated
  • Missing-data risks understood
  • No cloud resources modified
  • No real cloud credentials embedded
  • Least-privilege design documented
  • Limitations documented

You started with:

AWS CONFIGURATION
AZURE CONFIGURATION
GOOGLE CLOUD CONFIGURATION

Each provider had different:

TERMINOLOGY
SERVICES
RESOURCE TYPES

You transformed them into:

MULTI-CLOUD DATA
NORMALIZATION
COMMON CONTROL MODEL
SECURITY CHECKS
COMMON FINDINGS
PRIORITIZED REVIEW
REPORT

You now have the foundation of a multi-cloud security posture assessment tool capable of reviewing:

CLOUD OWNERSHIP
PRIVILEGED IDENTITY
MFA
ACCESS-KEY AGE
AUDIT LOGGING
DIAGNOSTIC LOGGING
STORAGE EXPOSURE
STORAGE ENCRYPTION
NETWORK MANAGEMENT EXPOSURE
CLOUD SECURITY SERVICES

across:

AWS
MICROSOFT AZURE
GOOGLE CLOUD

The central lesson from this lab is:

CLOUD SECURITY AUTOMATION
SHOULD NORMALIZE
SECURITY CONCEPTS,
NOT JUST PROVIDER PRODUCTS

AWS may call a service:

CloudTrail

Azure may call one:

Activity Log

Google Cloud may call one:

Cloud Audit Logs

But the security question is:

DO WE HAVE
RELIABLE AUDIT LOGGING?

Whenever you assess cloud configuration, think:

WHAT CLOUD SCOPE IS THIS?
WHO OWNS IT?
WHO HAS PRIVILEGE?
HOW ARE IDENTITIES PROTECTED?
IS ACTIVITY LOGGED?
IS DATA ENCRYPTED?
WHAT IS PUBLIC?
WHAT MANAGEMENT ACCESS IS EXPOSED?
ARE SECURITY SERVICES ENABLED?
WHAT DATA IS MISSING?
WHAT REQUIRES HUMAN REVIEW?

The goal is not:

CREATE HUNDREDS
OF CLOUD FINDINGS

The goal is:

TURN CLOUD CONFIGURATION
INTO CONSISTENT,
PRIORITIZED,
ACTIONABLE
SECURITY CONTEXT

➡️ Lab 09 — SOC Alert Triage Automation

The next lab moves from cloud posture assessment into SOC security operations.

You will build:

RAW SOC ALERT
VALIDATE
NORMALIZE
IDENTITY CONTEXT
ASSET CONTEXT
IOC CONTEXT
SEVERITY
RISK SCORE
TRIAGE RECOMMENDATION
ANALYST REVIEW

You will combine skills from:

Lab 01
Security Log Analysis
Lab 02
IOC Processing
Lab 05
SQL Security Analytics
Lab 06
Risk Prioritization
Lab 07
Security API Integration
Lab 08
Cloud Security Configuration Auditing

to build a controlled SOC alert-triage pipeline that helps analysts prioritize investigations without automatically taking disruptive response actions.