cloud-audit 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CHANGELOG.md +74 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/PKG-INFO +18 -2
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/README.md +16 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/mkdocs.yml +1 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/pyproject.toml +9 -2
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/cli.py +139 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/models.py +9 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/provider.py +2 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/__init__.py +105 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/cloudtrail_tampering.py +195 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/cryptomining_role.py +193 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/datazone_overgrant.py +173 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/lambda_function_url.py +238 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/mmdsv1_in_use.py +258 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/quarantine_policy.py +191 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/roles_anywhere_abuse.py +166 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/ses_phishing.py +225 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/trufflehog_ua.py +202 -0
- cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/whoami_confusion.py +230 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/__init__.py +0 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_cloudtrail_tampering.py +159 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_cryptomining_role.py +169 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_datazone_overgrant.py +178 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_lambda_function_url.py +275 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_mmdsv1_in_use.py +199 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_quarantine_policy.py +287 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_roles_anywhere_abuse.py +140 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_ses_phishing.py +187 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_trufflehog_ua.py +138 -0
- cloud_audit-2.2.0/tests/aws/threat_feed/test_whoami_confusion.py +181 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.cloud-audit.example.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/FUNDING.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/dependabot.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/ci.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/docs.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/example-scan.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/release.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.gitignore +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.mcp.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.pre-commit-hooks.yaml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CODEOWNERS +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CODE_OF_CONDUCT.md +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CONTRIBUTING.md +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/Dockerfile +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/LICENSE +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/Makefile +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/ROADMAP.md +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/SECURITY.md +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/action.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/demo.gif +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/logo-nobg.png +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/logo.png +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/report-preview.png +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/social-preview.png +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/examples/daily-scan-with-diff.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/examples/github-actions.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/examples/post-deploy-scan.yml +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/overrides/main.html +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/scripts/generate_demo_gif.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/scripts/generate_report_screenshot.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/server.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/__main__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/engine.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/bsi_c5_2020.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/cis_aws_v3.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/hipaa_security.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/iso27001_2022.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/nis2_directive.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/soc2_type2.json +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/config.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/correlate.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/cost_model.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/diff.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/history.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/mcp_server.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/account.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/backup.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/bedrock.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/cloudtrail.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/cloudwatch.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/config_.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/ec2.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/ecs.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/efs.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/eip.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/guardduty.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/iam.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/inspector.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/kms.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/lambda_.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/rds.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/s3.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/sagemaker.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/secrets.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/securityhub.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/ssm.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/vpc.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/waf.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/iam_analyzer.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/iam_trust_graph.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/base.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/py.typed +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/compliance_html.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/compliance_markdown.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/diff_markdown.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/html.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/markdown.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/sarif.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/templates/report.html.j2 +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/root_cause.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/scanner.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/simulate.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/__init__.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_bedrock.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_cis_checks.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_cloudtrail.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_cloudwatch.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_config.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_ec2.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_ecs.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_eip.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_guardduty.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_iam.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_iam_analyzer.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_iam_trust_graph.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_kms.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_lambda.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_rds.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_s3.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_sagemaker.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_secrets.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_ssm.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_vpc.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/conftest.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_cli.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_cli_scan.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_compliance_frameworks.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_config.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_correlate.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_cost_model.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_diff.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_history.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_html.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_markdown.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_mcp_server.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_models.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_provider.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_root_cause.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_sarif.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_scanner.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_simulate.py +0 -0
- {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_soc2_framework.py +0 -0
|
@@ -7,6 +7,80 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.2.0] - 2026-05-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Threat Feed v1** — new `cloud-audit threat-feed` command and a dedicated
|
|
15
|
+
detector pipeline (`providers/aws/threat_feed/`) that flags ACTIVE abuse
|
|
16
|
+
indicators rather than misconfiguration. Each pattern has a versioned
|
|
17
|
+
`TF-XXX` ID, maps to the new `Category.THREAT`, and carries external
|
|
18
|
+
references (research reports, CVE links) on every Finding for credibility.
|
|
19
|
+
Rules pack version: **2026-Q2**.
|
|
20
|
+
|
|
21
|
+
Ten patterns shipped:
|
|
22
|
+
|
|
23
|
+
- `TF-001-ses-phishing-setup` (MEDIUM/HIGH) — SES email/domain identities
|
|
24
|
+
verified within the last 14 days, with severity escalating when an
|
|
25
|
+
out-of-sandbox account hosts a typosquat-style email identity that has
|
|
26
|
+
no matching domain identity. Tracks the Wiz May 2025 + BleepingComputer
|
|
27
|
+
May 2026 SES abuse campaigns.
|
|
28
|
+
- `TF-002-lambda-function-url-persistence` (HIGH/CRITICAL) — Lambda
|
|
29
|
+
functions exposed via `AuthType=NONE` Function URLs, escalating to
|
|
30
|
+
CRITICAL when the execution role grants admin-class permissions
|
|
31
|
+
(matching the role profile of the Nov-Dec 2025 cryptomining campaign).
|
|
32
|
+
- `TF-003-quarantine-policy` (CRITICAL) — IAM principals with
|
|
33
|
+
`AWSCompromisedKeyQuarantineV1/V2/V3` attached. AWS auto-attaches these
|
|
34
|
+
after detecting credential exposure (typically a public GitHub commit).
|
|
35
|
+
- `TF-004-trufflehog-ua-cloudtrail` (CRITICAL) — `sts:GetCallerIdentity`
|
|
36
|
+
calls in the last 24h whose user-agent matches known leaked-credentials
|
|
37
|
+
discovery scanners (TruffleHog, gitleaks, CloudGrappler, DetentionDodger,
|
|
38
|
+
NoseyParker). Confirmed credential validation by an external scanner.
|
|
39
|
+
- `TF-005-cryptomining-role` (HIGH/CRITICAL) — IAM roles created within
|
|
40
|
+
the last 48 hours that carry broad compute managed policies (EC2 Full,
|
|
41
|
+
PowerUser, Admin, ECS Full, Lambda Full). Escalates to CRITICAL when
|
|
42
|
+
the same role also has SES sending permissions (mining + email-spam
|
|
43
|
+
combo from the documented late-2025 campaign cluster).
|
|
44
|
+
- `TF-006-mmdsv1-in-use` (HIGH/CRITICAL) — EC2 instances where
|
|
45
|
+
`HttpTokens != required` (IMDSv1 still callable) and Bedrock AgentCore
|
|
46
|
+
agents on `metadataVersion=v1` (CRITICAL — addresses Unit 42 'Cracks in
|
|
47
|
+
the Bedrock' research and the Feb 2026 MMDSv2 default).
|
|
48
|
+
- `TF-007-whoami-confusion` (MEDIUM) — IAM roles trusted by CI/CD
|
|
49
|
+
identities (codebuild service principals, GitHub OIDC, GitLab OIDC,
|
|
50
|
+
Buildkite federation) that have a broad EC2 managed policy attached —
|
|
51
|
+
the precondition for the Datadog Feb 2025 whoAMI confusion attack.
|
|
52
|
+
- `TF-008-cloudtrail-tampering` (HIGH/CRITICAL) — CloudTrail trails with
|
|
53
|
+
`IsLogging=False` (CRITICAL — canonical post-credential-theft attacker
|
|
54
|
+
behaviour, AiTM phishing follow-on per Datadog March 2026) or with a
|
|
55
|
+
populated `LatestDeliveryError` (HIGH — S3 destination broken).
|
|
56
|
+
- `TF-009-roles-anywhere-abuse` (HIGH/MEDIUM) — IAM Roles Anywhere trust
|
|
57
|
+
anchors with `sourceType=CERTIFICATE_BUNDLE` instead of the recommended
|
|
58
|
+
AWS_ACM_PCA. Anyone able to issue a chain-valid cert can mint AWS
|
|
59
|
+
credentials (fwd:cloudsec 2025 'Let's Encrypt for AWS Console').
|
|
60
|
+
- `TF-010-datazone-overgrant` (HIGH) — `AmazonDataZoneFullAccess` attached
|
|
61
|
+
to non-admin principals (the "easy" onboarding policy that bridges
|
|
62
|
+
identity, Glue catalog, and S3 storage in a single grant).
|
|
63
|
+
|
|
64
|
+
CLI: `cloud-audit threat-feed [--list] [--pattern <id>] [--regions ...]
|
|
65
|
+
[--profile ...] [--threat-feed-version 2026-Q2]`. Exits 1 when CRITICAL
|
|
66
|
+
or HIGH detected (CI gate friendly). Patterns also surface in standard
|
|
67
|
+
`cloud-audit scan --categories threat` output (JSON, SARIF, HTML).
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
|
|
71
|
+
- `Category` enum gains `THREAT` value for active-abuse findings (separate
|
|
72
|
+
from `SECURITY` misconfiguration).
|
|
73
|
+
- `Finding` model gains `threat_pattern_id: str | None` and
|
|
74
|
+
`references: list[str]` for backing research links.
|
|
75
|
+
- 23rd registered AWS check module (`threat_feed`) loaded by `AWSProvider`.
|
|
76
|
+
|
|
77
|
+
### Tests
|
|
78
|
+
|
|
79
|
+
- 638 -> 742 (+104). Each pattern ships 9-12 unit tests covering positive
|
|
80
|
+
detection, negative cases, false-positive guards, severity escalation,
|
|
81
|
+
multi-resource aggregation, AccessDenied resilience, and metadata
|
|
82
|
+
exposure.
|
|
83
|
+
|
|
10
84
|
## [2.1.0] - 2026-04-28
|
|
11
85
|
|
|
12
86
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloud-audit
|
|
3
|
-
Version: 2.
|
|
4
|
-
Summary: Open-source AWS security scanner
|
|
3
|
+
Version: 2.2.0
|
|
4
|
+
Summary: Open-source AWS security scanner. Threat Feed v1 (10 active-abuse patterns from 2025-2026 incidents), 64 IAM escalation methods, What-If simulator, security trends, AI-SPM (Bedrock/SageMaker), 6 compliance frameworks, 31 attack chain rules, breach cost estimation, and MCP server. Every finding includes CLI + Terraform remediation.
|
|
5
5
|
Project-URL: Homepage, https://haitmg.pl/cloud-audit/
|
|
6
6
|
Project-URL: Documentation, https://haitmg.pl/cloud-audit/
|
|
7
7
|
Project-URL: Source, https://github.com/gebalamariusz/cloud-audit
|
|
@@ -83,6 +83,7 @@ Description-Content-Type: text/markdown
|
|
|
83
83
|
<a href="https://haitmg.pl/cloud-audit/compliance/overview/">Compliance</a> -
|
|
84
84
|
<a href="https://haitmg.pl/cloud-audit/features/attack-chains/">Attack Chains</a> -
|
|
85
85
|
<a href="https://haitmg.pl/cloud-audit/features/iam-escalation/">IAM Escalation</a> -
|
|
86
|
+
<a href="https://haitmg.pl/cloud-audit/features/threat-feed/">Threat Feed</a> -
|
|
86
87
|
<a href="https://haitmg.pl/cloud-audit/features/simulate/">Simulator</a> -
|
|
87
88
|
<a href="https://haitmg.pl/cloud-audit/features/mcp-server/">MCP Server</a>
|
|
88
89
|
</p>
|
|
@@ -100,6 +101,21 @@ Uses your default AWS credentials and region. Try without an AWS account:
|
|
|
100
101
|
cloud-audit demo
|
|
101
102
|
```
|
|
102
103
|
|
|
104
|
+
### NEW in v2.2: Threat Feed
|
|
105
|
+
|
|
106
|
+
Detect ACTIVE abuse patterns from 2025-2026 incidents (cryptomining campaigns,
|
|
107
|
+
SES phishing setup, leaked-credential scanner activity, AgentCore CVEs):
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cloud-audit threat-feed # scan all 10 patterns
|
|
111
|
+
cloud-audit threat-feed --list # show registered patterns
|
|
112
|
+
cloud-audit threat-feed --pattern aws-tf-003 # one pattern only
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each pattern carries external research references (Wiz, Datadog Security Labs,
|
|
116
|
+
Unit 42, Permiso) on every finding. Exit code 1 when CRITICAL/HIGH detected
|
|
117
|
+
(CI gate friendly). See [Threat Feed docs](https://haitmg.pl/cloud-audit/features/threat-feed/).
|
|
118
|
+
|
|
103
119
|
---
|
|
104
120
|
|
|
105
121
|
## Why It's Different
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
<a href="https://haitmg.pl/cloud-audit/compliance/overview/">Compliance</a> -
|
|
37
37
|
<a href="https://haitmg.pl/cloud-audit/features/attack-chains/">Attack Chains</a> -
|
|
38
38
|
<a href="https://haitmg.pl/cloud-audit/features/iam-escalation/">IAM Escalation</a> -
|
|
39
|
+
<a href="https://haitmg.pl/cloud-audit/features/threat-feed/">Threat Feed</a> -
|
|
39
40
|
<a href="https://haitmg.pl/cloud-audit/features/simulate/">Simulator</a> -
|
|
40
41
|
<a href="https://haitmg.pl/cloud-audit/features/mcp-server/">MCP Server</a>
|
|
41
42
|
</p>
|
|
@@ -53,6 +54,21 @@ Uses your default AWS credentials and region. Try without an AWS account:
|
|
|
53
54
|
cloud-audit demo
|
|
54
55
|
```
|
|
55
56
|
|
|
57
|
+
### NEW in v2.2: Threat Feed
|
|
58
|
+
|
|
59
|
+
Detect ACTIVE abuse patterns from 2025-2026 incidents (cryptomining campaigns,
|
|
60
|
+
SES phishing setup, leaked-credential scanner activity, AgentCore CVEs):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
cloud-audit threat-feed # scan all 10 patterns
|
|
64
|
+
cloud-audit threat-feed --list # show registered patterns
|
|
65
|
+
cloud-audit threat-feed --pattern aws-tf-003 # one pattern only
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Each pattern carries external research references (Wiz, Datadog Security Labs,
|
|
69
|
+
Unit 42, Permiso) on every finding. Exit code 1 when CRITICAL/HIGH detected
|
|
70
|
+
(CI gate friendly). See [Threat Feed docs](https://haitmg.pl/cloud-audit/features/threat-feed/).
|
|
71
|
+
|
|
56
72
|
---
|
|
57
73
|
|
|
58
74
|
## Why It's Different
|
|
@@ -60,6 +60,7 @@ nav:
|
|
|
60
60
|
- Features:
|
|
61
61
|
- Attack Chains: features/attack-chains.md
|
|
62
62
|
- IAM Privilege Escalation: features/iam-escalation.md
|
|
63
|
+
- Threat Feed: features/threat-feed.md
|
|
63
64
|
- What-If Simulator: features/simulate.md
|
|
64
65
|
- Security Posture Trend: features/trend.md
|
|
65
66
|
- AI-SPM (Bedrock/SageMaker): features/ai-spm.md
|
|
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cloud-audit"
|
|
7
|
-
version = "2.
|
|
8
|
-
description = "Open-source AWS security scanner
|
|
7
|
+
version = "2.2.0"
|
|
8
|
+
description = "Open-source AWS security scanner. Threat Feed v1 (10 active-abuse patterns from 2025-2026 incidents), 64 IAM escalation methods, What-If simulator, security trends, AI-SPM (Bedrock/SageMaker), 6 compliance frameworks, 31 attack chain rules, breach cost estimation, and MCP server. Every finding includes CLI + Terraform remediation."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
@@ -81,6 +81,13 @@ select = ["E", "F", "I", "N", "W", "UP", "S", "B", "A", "C4", "SIM", "TCH", "RUF
|
|
|
81
81
|
"src/cloud_audit/reports/compliance_markdown.py" = ["E501"]
|
|
82
82
|
"src/cloud_audit/cli.py" = ["TC003"]
|
|
83
83
|
"src/cloud_audit/mcp_server.py" = ["TC003"]
|
|
84
|
+
# Threat-feed modules and tests intentionally use boto3-style CamelCase
|
|
85
|
+
# kwargs (UserName, RoleName, FunctionName, EmailIdentity) to match the
|
|
86
|
+
# AWS API surface they wrap. N803 (lower_snake_case argument) is wrong here.
|
|
87
|
+
# Inner exception classes used to simulate boto3 errors don't need the
|
|
88
|
+
# Error suffix N818 mandates. Lambda/SES helpers also use Optional implicitly.
|
|
89
|
+
"src/cloud_audit/providers/aws/threat_feed/*.py" = ["N803", "N818", "RUF013", "E501", "S112"]
|
|
90
|
+
"tests/aws/threat_feed/*.py" = ["S101", "N803", "N806", "N818", "RUF013", "E501", "TC003", "E402"]
|
|
84
91
|
"tests/**" = ["S101", "TC003", "E402"]
|
|
85
92
|
|
|
86
93
|
[tool.mypy]
|
|
@@ -1448,6 +1448,145 @@ def simulate(
|
|
|
1448
1448
|
console.print()
|
|
1449
1449
|
|
|
1450
1450
|
|
|
1451
|
+
@app.command(name="threat-feed")
|
|
1452
|
+
def threat_feed_cmd(
|
|
1453
|
+
profile: Annotated[str | None, typer.Option("--profile", help="AWS profile")] = None,
|
|
1454
|
+
regions: Annotated[
|
|
1455
|
+
str | None,
|
|
1456
|
+
typer.Option("--regions", "-r", help="Comma-separated regions, or 'all' for every enabled region"),
|
|
1457
|
+
] = None,
|
|
1458
|
+
pattern: Annotated[
|
|
1459
|
+
str | None,
|
|
1460
|
+
typer.Option("--pattern", help="Run only one pattern (e.g. aws-tf-003); default = all"),
|
|
1461
|
+
] = None,
|
|
1462
|
+
list_patterns: Annotated[
|
|
1463
|
+
bool,
|
|
1464
|
+
typer.Option("--list", help="List registered patterns and exit (no scan)"),
|
|
1465
|
+
] = False,
|
|
1466
|
+
threat_feed_version: Annotated[
|
|
1467
|
+
str | None,
|
|
1468
|
+
typer.Option("--threat-feed-version", help="Pin a rules-pack version (informational, default = current)"),
|
|
1469
|
+
] = None,
|
|
1470
|
+
) -> None:
|
|
1471
|
+
"""Detect ACTIVE abuse patterns from 2025-2026 threat reports.
|
|
1472
|
+
|
|
1473
|
+
Distinct from regular `scan`: looks for indicators that an attacker has ALREADY
|
|
1474
|
+
acted on the account (quarantine policies AWS attached after credential leak,
|
|
1475
|
+
public Lambda URLs created as persistence, DataZone over-grants, etc.) rather
|
|
1476
|
+
than mere misconfigurations.
|
|
1477
|
+
|
|
1478
|
+
Examples:
|
|
1479
|
+
cloud-audit threat-feed # scan all patterns
|
|
1480
|
+
cloud-audit threat-feed --pattern aws-tf-003 # one pattern only
|
|
1481
|
+
cloud-audit threat-feed --list # show registered patterns
|
|
1482
|
+
"""
|
|
1483
|
+
from cloud_audit.providers.aws import threat_feed as tf_module
|
|
1484
|
+
|
|
1485
|
+
if list_patterns:
|
|
1486
|
+
table = Table(title=f"Registered threat patterns (rules pack {tf_module.THREAT_FEED_VERSION})")
|
|
1487
|
+
table.add_column("Pattern ID", style="bold")
|
|
1488
|
+
table.add_column("Check ID")
|
|
1489
|
+
table.add_column("Severity")
|
|
1490
|
+
table.add_column("Name")
|
|
1491
|
+
table.add_column("Doc")
|
|
1492
|
+
for p in tf_module.list_patterns():
|
|
1493
|
+
sev_color = SEVERITY_COLORS.get(Severity(p["severity"]), "white")
|
|
1494
|
+
table.add_row(
|
|
1495
|
+
p["pattern_id"],
|
|
1496
|
+
p["check_id"],
|
|
1497
|
+
f"[{sev_color}]{p['severity'].upper()}[/{sev_color}]",
|
|
1498
|
+
p["name"],
|
|
1499
|
+
p["doc_url"],
|
|
1500
|
+
)
|
|
1501
|
+
console.print(table)
|
|
1502
|
+
console.print(f"\n[dim]{len(tf_module.list_patterns())} patterns registered.[/dim]")
|
|
1503
|
+
return
|
|
1504
|
+
|
|
1505
|
+
active_version = threat_feed_version or tf_module.THREAT_FEED_VERSION
|
|
1506
|
+
if threat_feed_version and threat_feed_version != tf_module.THREAT_FEED_VERSION:
|
|
1507
|
+
console.print(
|
|
1508
|
+
f"[yellow]Note: requested rules pack '{threat_feed_version}' differs from installed "
|
|
1509
|
+
f"'{tf_module.THREAT_FEED_VERSION}'. Pinning is informational in this build.[/yellow]"
|
|
1510
|
+
)
|
|
1511
|
+
|
|
1512
|
+
from cloud_audit.providers.aws.provider import AWSProvider
|
|
1513
|
+
|
|
1514
|
+
region_list = None
|
|
1515
|
+
if regions:
|
|
1516
|
+
region_list = ["all"] if regions.strip() == "all" else [r.strip() for r in regions.split(",")]
|
|
1517
|
+
|
|
1518
|
+
try:
|
|
1519
|
+
provider = AWSProvider(profile=profile, regions=region_list)
|
|
1520
|
+
except Exception as exc:
|
|
1521
|
+
console.print(f"[red]Failed to initialize AWS provider: {exc}[/red]")
|
|
1522
|
+
raise typer.Exit(2) from exc
|
|
1523
|
+
|
|
1524
|
+
threat_checks = [c for c in provider.get_checks() if getattr(c, "category", None) and c.category.value == "threat"]
|
|
1525
|
+
if pattern:
|
|
1526
|
+
threat_checks = [c for c in threat_checks if c.check_id == pattern]
|
|
1527
|
+
if not threat_checks:
|
|
1528
|
+
console.print(f"[red]No registered threat pattern with check_id '{pattern}'.[/red]")
|
|
1529
|
+
console.print("Run [cyan]cloud-audit threat-feed --list[/cyan] to see available patterns.")
|
|
1530
|
+
raise typer.Exit(2)
|
|
1531
|
+
|
|
1532
|
+
console.print(
|
|
1533
|
+
Panel(
|
|
1534
|
+
f"Scanning [bold]{len(threat_checks)}[/bold] threat patterns (rules pack [cyan]{active_version}[/cyan])",
|
|
1535
|
+
title="[bold]Threat Feed[/bold]",
|
|
1536
|
+
border_style="magenta",
|
|
1537
|
+
)
|
|
1538
|
+
)
|
|
1539
|
+
|
|
1540
|
+
all_findings: list[Finding] = []
|
|
1541
|
+
errors: list[tuple[str, str]] = []
|
|
1542
|
+
for check in threat_checks:
|
|
1543
|
+
try:
|
|
1544
|
+
result = check()
|
|
1545
|
+
except Exception as exc: # defensive - check should already capture
|
|
1546
|
+
errors.append((check.check_id, str(exc)))
|
|
1547
|
+
continue
|
|
1548
|
+
if result.error:
|
|
1549
|
+
errors.append((check.check_id, result.error))
|
|
1550
|
+
all_findings.extend(result.findings)
|
|
1551
|
+
|
|
1552
|
+
if errors:
|
|
1553
|
+
console.print()
|
|
1554
|
+
for check_id, err in errors:
|
|
1555
|
+
console.print(f"[yellow]! {check_id}: {err}[/yellow]")
|
|
1556
|
+
|
|
1557
|
+
if not all_findings:
|
|
1558
|
+
console.print("\n[green]No active abuse patterns detected.[/green]\n")
|
|
1559
|
+
raise typer.Exit(0)
|
|
1560
|
+
|
|
1561
|
+
table = Table(title=f"Detected threat patterns ({len(all_findings)} findings)", show_lines=True)
|
|
1562
|
+
table.add_column("Severity", no_wrap=True)
|
|
1563
|
+
table.add_column("Pattern", style="bold")
|
|
1564
|
+
table.add_column("Resource")
|
|
1565
|
+
table.add_column("Region")
|
|
1566
|
+
table.add_column("References")
|
|
1567
|
+
|
|
1568
|
+
for f in sorted(all_findings, key=lambda x: list(SEVERITY_COLORS).index(x.severity)):
|
|
1569
|
+
sev_color = SEVERITY_COLORS[f.severity]
|
|
1570
|
+
refs = "\n".join(f.references[:2]) if f.references else "-"
|
|
1571
|
+
table.add_row(
|
|
1572
|
+
f"[{sev_color}]{f.severity.value.upper()}[/{sev_color}]",
|
|
1573
|
+
f"{f.threat_pattern_id or f.check_id}\n[dim]{rich_escape(f.title)}[/dim]",
|
|
1574
|
+
rich_escape(f.resource_id),
|
|
1575
|
+
f.region,
|
|
1576
|
+
refs,
|
|
1577
|
+
)
|
|
1578
|
+
|
|
1579
|
+
console.print(table)
|
|
1580
|
+
console.print(
|
|
1581
|
+
"\n[dim]Use [cyan]cloud-audit scan --categories threat -o json[/cyan] for machine-readable output "
|
|
1582
|
+
"(includes full remediation + references).[/dim]\n"
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
# Exit 1 when CRITICAL/HIGH detected (CI gate)
|
|
1586
|
+
blocking = [f for f in all_findings if f.severity in (Severity.CRITICAL, Severity.HIGH)]
|
|
1587
|
+
raise typer.Exit(1 if blocking else 0)
|
|
1588
|
+
|
|
1589
|
+
|
|
1451
1590
|
@app.command()
|
|
1452
1591
|
def version() -> None:
|
|
1453
1592
|
"""Show version."""
|
|
@@ -22,6 +22,7 @@ class Category(str, Enum):
|
|
|
22
22
|
COST = "cost"
|
|
23
23
|
RELIABILITY = "reliability"
|
|
24
24
|
PERFORMANCE = "performance"
|
|
25
|
+
THREAT = "threat"
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class Effort(str, Enum):
|
|
@@ -75,6 +76,14 @@ class Finding(BaseModel):
|
|
|
75
76
|
remediation: Remediation | None = Field(default=None, description="Structured remediation details")
|
|
76
77
|
compliance_refs: list[str] = Field(default_factory=list, description="Compliance references, e.g. ['CIS 1.5']")
|
|
77
78
|
cost_estimate: CostEstimateData | None = Field(default=None, description="Estimated breach cost range")
|
|
79
|
+
threat_pattern_id: str | None = Field(
|
|
80
|
+
default=None,
|
|
81
|
+
description="Threat feed pattern identifier, e.g. 'TF-003-quarantine-policy' (None for regular checks)",
|
|
82
|
+
)
|
|
83
|
+
references: list[str] = Field(
|
|
84
|
+
default_factory=list,
|
|
85
|
+
description="External references (research reports, CVE links, blog posts) backing this finding",
|
|
86
|
+
)
|
|
78
87
|
|
|
79
88
|
|
|
80
89
|
class CheckResult(BaseModel):
|
|
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
8
8
|
import boto3
|
|
9
9
|
from botocore.config import Config
|
|
10
10
|
|
|
11
|
+
from cloud_audit.providers.aws import threat_feed
|
|
11
12
|
from cloud_audit.providers.aws.checks import (
|
|
12
13
|
account,
|
|
13
14
|
backup,
|
|
@@ -65,6 +66,7 @@ _CHECK_MODULES = [
|
|
|
65
66
|
waf,
|
|
66
67
|
bedrock,
|
|
67
68
|
sagemaker,
|
|
69
|
+
threat_feed,
|
|
68
70
|
]
|
|
69
71
|
|
|
70
72
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Threat Feed - active abuse pattern detectors.
|
|
2
|
+
|
|
3
|
+
Each pattern in this package detects an attack technique observed in real-world
|
|
4
|
+
2025-2026 incidents (cryptomining campaigns, SES phishing, leaked credentials,
|
|
5
|
+
AgentCore vulnerabilities). Patterns are versioned via THREAT_FEED_VERSION so
|
|
6
|
+
operators can pin a known rules pack.
|
|
7
|
+
|
|
8
|
+
Patterns vs regular checks:
|
|
9
|
+
- Regular checks (providers/aws/checks/) detect MISCONFIGURATION
|
|
10
|
+
- Threat patterns (this package) detect ACTIVE ABUSE INDICATORS or
|
|
11
|
+
conditions an attacker exploited in a documented incident.
|
|
12
|
+
|
|
13
|
+
Each pattern produces standard Finding objects with:
|
|
14
|
+
- category=Category.THREAT
|
|
15
|
+
- threat_pattern_id="TF-XXX-name"
|
|
16
|
+
- references=[<URL to research report or CVE>]
|
|
17
|
+
|
|
18
|
+
Adding a new pattern:
|
|
19
|
+
1. Create src/cloud_audit/providers/aws/threat_feed/<name>.py
|
|
20
|
+
2. Implement: def detect(provider) -> CheckResult
|
|
21
|
+
3. Register in _PATTERN_MODULES below
|
|
22
|
+
4. Add tests in tests/aws/threat_feed/test_<name>.py
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
from cloud_audit.models import Category
|
|
30
|
+
from cloud_audit.providers.aws.threat_feed import (
|
|
31
|
+
cloudtrail_tampering,
|
|
32
|
+
cryptomining_role,
|
|
33
|
+
datazone_overgrant,
|
|
34
|
+
lambda_function_url,
|
|
35
|
+
mmdsv1_in_use,
|
|
36
|
+
quarantine_policy,
|
|
37
|
+
roles_anywhere_abuse,
|
|
38
|
+
ses_phishing,
|
|
39
|
+
trufflehog_ua,
|
|
40
|
+
whoami_confusion,
|
|
41
|
+
)
|
|
42
|
+
from cloud_audit.providers.base import make_check
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING:
|
|
45
|
+
from cloud_audit.providers.aws.provider import AWSProvider
|
|
46
|
+
from cloud_audit.providers.base import CheckFn
|
|
47
|
+
|
|
48
|
+
THREAT_FEED_VERSION = "2026-Q2"
|
|
49
|
+
"""Versioned rules pack identifier - bump when patterns added/removed/changed.
|
|
50
|
+
|
|
51
|
+
Operators pin via `cloud-audit threat-feed --threat-feed-version 2026-Q2`
|
|
52
|
+
to get reproducible scans across releases.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_PATTERN_MODULES = [
|
|
56
|
+
ses_phishing,
|
|
57
|
+
lambda_function_url,
|
|
58
|
+
quarantine_policy,
|
|
59
|
+
trufflehog_ua,
|
|
60
|
+
cryptomining_role,
|
|
61
|
+
mmdsv1_in_use,
|
|
62
|
+
whoami_confusion,
|
|
63
|
+
cloudtrail_tampering,
|
|
64
|
+
roles_anywhere_abuse,
|
|
65
|
+
datazone_overgrant,
|
|
66
|
+
]
|
|
67
|
+
"""Registry of all threat feed pattern modules.
|
|
68
|
+
|
|
69
|
+
Order matters for output stability - keep it deterministic.
|
|
70
|
+
New patterns: append to the end so existing TF-IDs remain stable.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_checks(provider: AWSProvider) -> list[CheckFn]:
|
|
75
|
+
"""Return all threat feed pattern check functions bound to the provider.
|
|
76
|
+
|
|
77
|
+
Each pattern module exposes:
|
|
78
|
+
- detect(provider) -> CheckResult
|
|
79
|
+
- PATTERN_ID: str (e.g. "TF-003-quarantine-policy")
|
|
80
|
+
- CHECK_ID: str (e.g. "aws-tf-003")
|
|
81
|
+
"""
|
|
82
|
+
checks: list[CheckFn] = []
|
|
83
|
+
for module in _PATTERN_MODULES:
|
|
84
|
+
check = make_check(
|
|
85
|
+
module.detect,
|
|
86
|
+
provider,
|
|
87
|
+
check_id=module.CHECK_ID,
|
|
88
|
+
category=Category.THREAT,
|
|
89
|
+
)
|
|
90
|
+
checks.append(check)
|
|
91
|
+
return checks
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def list_patterns() -> list[dict[str, str]]:
|
|
95
|
+
"""Return metadata for all registered patterns - used by CLI list/help."""
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
"pattern_id": m.PATTERN_ID,
|
|
99
|
+
"check_id": m.CHECK_ID,
|
|
100
|
+
"name": m.PATTERN_NAME,
|
|
101
|
+
"severity": m.PATTERN_SEVERITY.value,
|
|
102
|
+
"doc_url": m.DOC_URL,
|
|
103
|
+
}
|
|
104
|
+
for m in _PATTERN_MODULES
|
|
105
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""TF-008: CloudTrail tampering precursors.
|
|
2
|
+
|
|
3
|
+
After compromising credentials, attackers commonly try to blind CloudTrail
|
|
4
|
+
before doing further damage so subsequent activity is not logged. AWS has
|
|
5
|
+
documented this pattern repeatedly (the AiTM phishing follow-on actions
|
|
6
|
+
documented by Datadog Security Labs in March 2026 included CloudTrail stop
|
|
7
|
+
attempts as the second-stage action).
|
|
8
|
+
|
|
9
|
+
We surface tampering PRECURSORS - signals that may indicate either active
|
|
10
|
+
tampering OR a misconfiguration that achieves the same blind-spot effect:
|
|
11
|
+
- Trails that exist but have IsLogging=False (intentionally stopped)
|
|
12
|
+
- Trails with LatestDeliveryError populated (S3 destination broken / bucket
|
|
13
|
+
removed - whether by attacker or by mistake, your evidence is gone)
|
|
14
|
+
- Trails that lack IncludeManagementEvents in their event selectors
|
|
15
|
+
(control-plane API calls bypass the trail entirely)
|
|
16
|
+
|
|
17
|
+
The cloud-audit `cloudtrail.py` checks cover MISCONFIGURATION (trail not
|
|
18
|
+
enabled at all, log file validation off). This pattern covers ACTIVE
|
|
19
|
+
tampering / state-drift signals on existing trails.
|
|
20
|
+
|
|
21
|
+
References:
|
|
22
|
+
- https://securitylabs.datadoghq.com/articles/behind-the-console-aws-aitm-phishing-campaign/
|
|
23
|
+
- https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-faqs.html
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
|
|
30
|
+
from cloud_audit.models import Category, CheckResult, Effort, Finding, Remediation, Severity
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from cloud_audit.providers.aws.provider import AWSProvider
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
PATTERN_ID = "TF-008-cloudtrail-tampering"
|
|
37
|
+
CHECK_ID = "aws-tf-008"
|
|
38
|
+
PATTERN_NAME = "CloudTrail tampering precursor (logging stopped or delivery failing)"
|
|
39
|
+
PATTERN_SEVERITY = Severity.HIGH
|
|
40
|
+
DOC_URL = "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-faqs.html"
|
|
41
|
+
|
|
42
|
+
_REFERENCES = [
|
|
43
|
+
"https://securitylabs.datadoghq.com/articles/behind-the-console-aws-aitm-phishing-campaign/",
|
|
44
|
+
"https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-faqs.html",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_logging_stopped_finding(trail_name: str, trail_arn: str, region: str) -> Finding:
|
|
49
|
+
return Finding(
|
|
50
|
+
check_id=CHECK_ID,
|
|
51
|
+
title=f"CloudTrail '{trail_name}' has IsLogging=False (logging stopped)",
|
|
52
|
+
severity=Severity.CRITICAL,
|
|
53
|
+
category=Category.THREAT,
|
|
54
|
+
resource_type="AWS::CloudTrail::Trail",
|
|
55
|
+
resource_id=trail_arn,
|
|
56
|
+
region=region,
|
|
57
|
+
description=(
|
|
58
|
+
f"Trail '{trail_name}' exists but is currently NOT logging. Either someone "
|
|
59
|
+
"called cloudtrail:StopLogging recently (canonical attacker behaviour after "
|
|
60
|
+
"credential theft - documented by Datadog Security Labs March 2026 in the "
|
|
61
|
+
"AWS AiTM phishing campaign) or it was disabled administratively and never "
|
|
62
|
+
"re-enabled. Either way, your CloudTrail evidence path is broken right now."
|
|
63
|
+
),
|
|
64
|
+
recommendation=(
|
|
65
|
+
"(1) Re-enable logging immediately if this is unexpected. (2) Audit who "
|
|
66
|
+
"called StopLogging and when - the management event for that call is itself "
|
|
67
|
+
"logged (in another trail or, before this trail was stopped, in this one). "
|
|
68
|
+
"(3) Add an EventBridge rule that fires on cloudtrail:StopLogging and pages "
|
|
69
|
+
"your on-call. (4) Add a Service Control Policy (SCP) that denies "
|
|
70
|
+
"cloudtrail:StopLogging and cloudtrail:DeleteTrail to all non-break-glass "
|
|
71
|
+
"principals."
|
|
72
|
+
),
|
|
73
|
+
remediation=Remediation(
|
|
74
|
+
cli=(
|
|
75
|
+
f"# 1) Restore logging immediately:\n"
|
|
76
|
+
f"aws cloudtrail start-logging --name {trail_name} --region {region}\n"
|
|
77
|
+
f"\n# 2) Find who stopped it (look in any other active trail):\n"
|
|
78
|
+
f"aws cloudtrail lookup-events \\\n"
|
|
79
|
+
f" --lookup-attributes AttributeKey=EventName,AttributeValue=StopLogging \\\n"
|
|
80
|
+
f" --region {region}\n"
|
|
81
|
+
f"\n# 3) Add an EventBridge rule for future visibility:\n"
|
|
82
|
+
f"aws events put-rule --name detect-cloudtrail-stop \\\n"
|
|
83
|
+
f' --event-pattern \'{{"source":["aws.cloudtrail"],"detail-type":"AWS API Call via CloudTrail",'
|
|
84
|
+
f'"detail":{{"eventName":["StopLogging","DeleteTrail"]}}}}\''
|
|
85
|
+
),
|
|
86
|
+
terraform=(
|
|
87
|
+
'resource "aws_cloudwatch_event_rule" "detect_trail_stop" {\n'
|
|
88
|
+
' name = "detect-cloudtrail-tampering"\n'
|
|
89
|
+
' description = "Alert on CloudTrail stop or delete"\n'
|
|
90
|
+
" event_pattern = jsonencode({\n"
|
|
91
|
+
' source = ["aws.cloudtrail"]\n'
|
|
92
|
+
' "detail-type" = ["AWS API Call via CloudTrail"]\n'
|
|
93
|
+
" detail = {\n"
|
|
94
|
+
' eventName = ["StopLogging", "DeleteTrail", "PutEventSelectors", "UpdateTrail"]\n'
|
|
95
|
+
" }\n"
|
|
96
|
+
" })\n"
|
|
97
|
+
"}"
|
|
98
|
+
),
|
|
99
|
+
doc_url=DOC_URL,
|
|
100
|
+
effort=Effort.LOW,
|
|
101
|
+
),
|
|
102
|
+
threat_pattern_id=PATTERN_ID,
|
|
103
|
+
references=_REFERENCES,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _build_delivery_error_finding(trail_name: str, trail_arn: str, region: str, error: str) -> Finding:
|
|
108
|
+
return Finding(
|
|
109
|
+
check_id=CHECK_ID,
|
|
110
|
+
title=f"CloudTrail '{trail_name}' has S3 delivery error - evidence path is broken",
|
|
111
|
+
severity=Severity.HIGH,
|
|
112
|
+
category=Category.THREAT,
|
|
113
|
+
resource_type="AWS::CloudTrail::Trail",
|
|
114
|
+
resource_id=trail_arn,
|
|
115
|
+
region=region,
|
|
116
|
+
description=(
|
|
117
|
+
f"Trail '{trail_name}' is technically logging but its S3 delivery is failing. "
|
|
118
|
+
f"LatestDeliveryError reports: '{error}'. Common causes are an attacker "
|
|
119
|
+
"who deleted the destination bucket (or its bucket policy), bucket KMS "
|
|
120
|
+
"key removal, or accidental cross-account permission revocation. Result is "
|
|
121
|
+
"the same: events are dropped, your evidence is incomplete."
|
|
122
|
+
),
|
|
123
|
+
recommendation=(
|
|
124
|
+
"Investigate the underlying bucket/KMS/policy state. After fixing, verify "
|
|
125
|
+
"with `get-trail-status` that LatestDeliveryError is no longer set."
|
|
126
|
+
),
|
|
127
|
+
remediation=Remediation(
|
|
128
|
+
cli=(
|
|
129
|
+
f"aws cloudtrail get-trail-status --name {trail_name} --region {region}\n"
|
|
130
|
+
f"# Then inspect the destination bucket policy and KMS key state."
|
|
131
|
+
),
|
|
132
|
+
terraform="# Diagnostic only - fix is on the destination bucket / KMS key.",
|
|
133
|
+
doc_url=DOC_URL,
|
|
134
|
+
effort=Effort.MEDIUM,
|
|
135
|
+
),
|
|
136
|
+
threat_pattern_id=PATTERN_ID,
|
|
137
|
+
references=_REFERENCES,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _scan_region(provider: AWSProvider, region: str) -> tuple[int, list[Finding]]:
|
|
142
|
+
ct = provider.client("cloudtrail", region_name=region)
|
|
143
|
+
findings: list[Finding] = []
|
|
144
|
+
scanned = 0
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
trails = ct.describe_trails(includeShadowTrails=False).get("trailList", [])
|
|
148
|
+
except Exception as exc:
|
|
149
|
+
code = getattr(exc, "response", {}).get("Error", {}).get("Code", "")
|
|
150
|
+
if code in ("AccessDeniedException", "UnrecognizedClientException"):
|
|
151
|
+
return 0, []
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
for trail in trails:
|
|
155
|
+
# Skip shadow trails (multi-region trails appear in every region; we want home only)
|
|
156
|
+
if trail.get("HomeRegion") and trail.get("HomeRegion") != region:
|
|
157
|
+
continue
|
|
158
|
+
scanned += 1
|
|
159
|
+
trail_name = trail.get("Name", "")
|
|
160
|
+
trail_arn = trail.get("TrailARN", "")
|
|
161
|
+
try:
|
|
162
|
+
status = ct.get_trail_status(Name=trail_arn or trail_name)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
code = getattr(exc, "response", {}).get("Error", {}).get("Code", "")
|
|
165
|
+
if code in ("AccessDenied", "TrailNotFoundException"):
|
|
166
|
+
continue
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
is_logging = bool(status.get("IsLogging", False))
|
|
170
|
+
delivery_error = status.get("LatestDeliveryError") or ""
|
|
171
|
+
|
|
172
|
+
if not is_logging:
|
|
173
|
+
findings.append(_build_logging_stopped_finding(trail_name, trail_arn, region))
|
|
174
|
+
# Don't double-flag the same trail with delivery error if logging is stopped
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
if delivery_error:
|
|
178
|
+
findings.append(_build_delivery_error_finding(trail_name, trail_arn, region, delivery_error))
|
|
179
|
+
|
|
180
|
+
return scanned, findings
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def detect(provider: AWSProvider) -> CheckResult:
|
|
184
|
+
"""Scan all regions for CloudTrail trails showing tampering precursors."""
|
|
185
|
+
result = CheckResult(check_id=CHECK_ID, check_name=PATTERN_NAME)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
for region in provider.regions:
|
|
189
|
+
scanned, findings = _scan_region(provider, region)
|
|
190
|
+
result.resources_scanned += scanned
|
|
191
|
+
result.findings.extend(findings)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
result.error = str(e)
|
|
194
|
+
|
|
195
|
+
return result
|