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.
Files changed (162) hide show
  1. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CHANGELOG.md +74 -0
  2. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/PKG-INFO +18 -2
  3. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/README.md +16 -0
  4. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/mkdocs.yml +1 -0
  5. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/pyproject.toml +9 -2
  6. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/cli.py +139 -0
  7. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/models.py +9 -0
  8. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/provider.py +2 -0
  9. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/__init__.py +105 -0
  10. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/cloudtrail_tampering.py +195 -0
  11. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/cryptomining_role.py +193 -0
  12. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/datazone_overgrant.py +173 -0
  13. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/lambda_function_url.py +238 -0
  14. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/mmdsv1_in_use.py +258 -0
  15. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/quarantine_policy.py +191 -0
  16. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/roles_anywhere_abuse.py +166 -0
  17. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/ses_phishing.py +225 -0
  18. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/trufflehog_ua.py +202 -0
  19. cloud_audit-2.2.0/src/cloud_audit/providers/aws/threat_feed/whoami_confusion.py +230 -0
  20. cloud_audit-2.2.0/tests/aws/threat_feed/__init__.py +0 -0
  21. cloud_audit-2.2.0/tests/aws/threat_feed/test_cloudtrail_tampering.py +159 -0
  22. cloud_audit-2.2.0/tests/aws/threat_feed/test_cryptomining_role.py +169 -0
  23. cloud_audit-2.2.0/tests/aws/threat_feed/test_datazone_overgrant.py +178 -0
  24. cloud_audit-2.2.0/tests/aws/threat_feed/test_lambda_function_url.py +275 -0
  25. cloud_audit-2.2.0/tests/aws/threat_feed/test_mmdsv1_in_use.py +199 -0
  26. cloud_audit-2.2.0/tests/aws/threat_feed/test_quarantine_policy.py +287 -0
  27. cloud_audit-2.2.0/tests/aws/threat_feed/test_roles_anywhere_abuse.py +140 -0
  28. cloud_audit-2.2.0/tests/aws/threat_feed/test_ses_phishing.py +187 -0
  29. cloud_audit-2.2.0/tests/aws/threat_feed/test_trufflehog_ua.py +138 -0
  30. cloud_audit-2.2.0/tests/aws/threat_feed/test_whoami_confusion.py +181 -0
  31. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.cloud-audit.example.yml +0 -0
  32. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/FUNDING.yml +0 -0
  33. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  34. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  35. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  36. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/dependabot.yml +0 -0
  37. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/ci.yml +0 -0
  38. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/docs.yml +0 -0
  39. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/example-scan.yml +0 -0
  40. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.github/workflows/release.yml +0 -0
  41. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.gitignore +0 -0
  42. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.mcp.json +0 -0
  43. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/.pre-commit-hooks.yaml +0 -0
  44. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CODEOWNERS +0 -0
  45. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CODE_OF_CONDUCT.md +0 -0
  46. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/CONTRIBUTING.md +0 -0
  47. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/Dockerfile +0 -0
  48. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/LICENSE +0 -0
  49. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/Makefile +0 -0
  50. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/ROADMAP.md +0 -0
  51. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/SECURITY.md +0 -0
  52. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/action.yml +0 -0
  53. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/demo.gif +0 -0
  54. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/logo-nobg.png +0 -0
  55. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/logo.png +0 -0
  56. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/report-preview.png +0 -0
  57. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/assets/social-preview.png +0 -0
  58. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/examples/daily-scan-with-diff.yml +0 -0
  59. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/examples/github-actions.yml +0 -0
  60. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/examples/post-deploy-scan.yml +0 -0
  61. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/overrides/main.html +0 -0
  62. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/scripts/generate_demo_gif.py +0 -0
  63. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/scripts/generate_report_screenshot.py +0 -0
  64. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/server.json +0 -0
  65. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/__init__.py +0 -0
  66. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/__main__.py +0 -0
  67. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/__init__.py +0 -0
  68. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/engine.py +0 -0
  69. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/bsi_c5_2020.json +0 -0
  70. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/cis_aws_v3.json +0 -0
  71. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/hipaa_security.json +0 -0
  72. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/iso27001_2022.json +0 -0
  73. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/nis2_directive.json +0 -0
  74. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/compliance/frameworks/soc2_type2.json +0 -0
  75. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/config.py +0 -0
  76. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/correlate.py +0 -0
  77. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/cost_model.py +0 -0
  78. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/diff.py +0 -0
  79. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/history.py +0 -0
  80. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/mcp_server.py +0 -0
  81. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/__init__.py +0 -0
  82. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/__init__.py +0 -0
  83. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/__init__.py +0 -0
  84. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/account.py +0 -0
  85. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/backup.py +0 -0
  86. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/bedrock.py +0 -0
  87. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/cloudtrail.py +0 -0
  88. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/cloudwatch.py +0 -0
  89. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/config_.py +0 -0
  90. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/ec2.py +0 -0
  91. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/ecs.py +0 -0
  92. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/efs.py +0 -0
  93. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/eip.py +0 -0
  94. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/guardduty.py +0 -0
  95. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/iam.py +0 -0
  96. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/inspector.py +0 -0
  97. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/kms.py +0 -0
  98. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/lambda_.py +0 -0
  99. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/rds.py +0 -0
  100. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/s3.py +0 -0
  101. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/sagemaker.py +0 -0
  102. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/secrets.py +0 -0
  103. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/securityhub.py +0 -0
  104. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/ssm.py +0 -0
  105. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/vpc.py +0 -0
  106. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/checks/waf.py +0 -0
  107. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/iam_analyzer.py +0 -0
  108. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/aws/iam_trust_graph.py +0 -0
  109. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/providers/base.py +0 -0
  110. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/py.typed +0 -0
  111. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/__init__.py +0 -0
  112. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/compliance_html.py +0 -0
  113. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/compliance_markdown.py +0 -0
  114. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/diff_markdown.py +0 -0
  115. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/html.py +0 -0
  116. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/markdown.py +0 -0
  117. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/sarif.py +0 -0
  118. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/reports/templates/report.html.j2 +0 -0
  119. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/root_cause.py +0 -0
  120. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/scanner.py +0 -0
  121. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/src/cloud_audit/simulate.py +0 -0
  122. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/__init__.py +0 -0
  123. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/__init__.py +0 -0
  124. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_bedrock.py +0 -0
  125. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_cis_checks.py +0 -0
  126. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_cloudtrail.py +0 -0
  127. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_cloudwatch.py +0 -0
  128. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_config.py +0 -0
  129. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_ec2.py +0 -0
  130. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_ecs.py +0 -0
  131. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_eip.py +0 -0
  132. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_guardduty.py +0 -0
  133. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_iam.py +0 -0
  134. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_iam_analyzer.py +0 -0
  135. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_iam_trust_graph.py +0 -0
  136. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_kms.py +0 -0
  137. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_lambda.py +0 -0
  138. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_rds.py +0 -0
  139. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_s3.py +0 -0
  140. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_sagemaker.py +0 -0
  141. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_secrets.py +0 -0
  142. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_ssm.py +0 -0
  143. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/aws/test_vpc.py +0 -0
  144. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/conftest.py +0 -0
  145. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_cli.py +0 -0
  146. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_cli_scan.py +0 -0
  147. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_compliance_frameworks.py +0 -0
  148. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_config.py +0 -0
  149. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_correlate.py +0 -0
  150. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_cost_model.py +0 -0
  151. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_diff.py +0 -0
  152. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_history.py +0 -0
  153. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_html.py +0 -0
  154. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_markdown.py +0 -0
  155. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_mcp_server.py +0 -0
  156. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_models.py +0 -0
  157. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_provider.py +0 -0
  158. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_root_cause.py +0 -0
  159. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_sarif.py +0 -0
  160. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_scanner.py +0 -0
  161. {cloud_audit-2.1.0 → cloud_audit-2.2.0}/tests/test_simulate.py +0 -0
  162. {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.1.0
4
- Summary: Open-source AWS security scanner with IAM escalation detection, What-If simulator, security trends, AI-SPM (Bedrock/SageMaker), 6 compliance frameworks, 31 attack chain rules, breach cost estimation, and MCP server. 94 checks across 23 services. Every finding includes CLI + Terraform remediation.
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.1.0"
8
- description = "Open-source AWS security scanner with IAM escalation detection, What-If simulator, security trends, AI-SPM (Bedrock/SageMaker), 6 compliance frameworks, 31 attack chain rules, breach cost estimation, and MCP server. 94 checks across 23 services. Every finding includes CLI + Terraform remediation."
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