security-mcp 1.1.4 → 1.3.1

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 (129) hide show
  1. package/README.md +116 -264
  2. package/defaults/checklists/ai.json +20 -1
  3. package/defaults/checklists/api.json +35 -1
  4. package/defaults/checklists/infra.json +34 -1
  5. package/defaults/checklists/mobile.json +23 -1
  6. package/defaults/checklists/payments.json +15 -1
  7. package/defaults/checklists/web.json +11 -1
  8. package/defaults/security-policy.json +2 -2
  9. package/dist/cli/index.js +0 -0
  10. package/dist/gate/baseline.js +82 -7
  11. package/dist/gate/catalog.js +10 -2
  12. package/dist/gate/checks/ai.js +757 -39
  13. package/dist/gate/checks/auth-deep.js +920 -216
  14. package/dist/gate/checks/business-logic.js +751 -0
  15. package/dist/gate/checks/ci-pipeline.js +399 -4
  16. package/dist/gate/checks/crypto.js +423 -2
  17. package/dist/gate/checks/dependencies.js +571 -15
  18. package/dist/gate/checks/graphql.js +201 -19
  19. package/dist/gate/checks/infra.js +246 -1
  20. package/dist/gate/checks/injection-deep.js +827 -184
  21. package/dist/gate/checks/k8s.js +114 -1
  22. package/dist/gate/checks/mobile-android.js +917 -3
  23. package/dist/gate/checks/mobile-ios.js +797 -5
  24. package/dist/gate/checks/required-artifacts.js +194 -0
  25. package/dist/gate/checks/runtime.js +178 -0
  26. package/dist/gate/checks/secrets.js +244 -13
  27. package/dist/gate/checks/supply-chain-deep.js +787 -0
  28. package/dist/gate/checks/web-nextjs.js +572 -48
  29. package/dist/gate/diff.js +17 -5
  30. package/dist/gate/evidence.js +8 -1
  31. package/dist/gate/exceptions.js +131 -9
  32. package/dist/gate/policy.js +280 -131
  33. package/dist/mcp/audit-chain.js +122 -28
  34. package/dist/mcp/auth.js +169 -0
  35. package/dist/mcp/learning.js +129 -4
  36. package/dist/mcp/model-router.js +158 -21
  37. package/dist/mcp/orchestration.js +186 -51
  38. package/dist/mcp/server.js +337 -53
  39. package/dist/repo/fs.js +24 -1
  40. package/dist/repo/search.js +31 -6
  41. package/dist/review/store.js +52 -1
  42. package/package.json +7 -7
  43. package/skills/_TEMPLATE/SKILL.md +99 -0
  44. package/skills/advanced-dos-tester/SKILL.md +109 -0
  45. package/skills/agentic-loop-exploiter/SKILL.md +368 -0
  46. package/skills/ai-llm-redteam/SKILL.md +104 -0
  47. package/skills/ai-model-supply-chain-agent/SKILL.md +103 -0
  48. package/skills/algorithm-implementation-reviewer/SKILL.md +98 -0
  49. package/skills/android-penetration-tester/SKILL.md +455 -46
  50. package/skills/anti-replay-tester/SKILL.md +106 -0
  51. package/skills/appsec-code-auditor/SKILL.md +85 -0
  52. package/skills/artifact-integrity-analyst/SKILL.md +441 -0
  53. package/skills/attack-navigator/SKILL.md +467 -8
  54. package/skills/auth-session-hacker/SKILL.md +102 -0
  55. package/skills/aws-penetration-tester/SKILL.md +456 -0
  56. package/skills/azure-penetration-tester/SKILL.md +490 -3
  57. package/skills/binary-auth-validator/SKILL.md +111 -0
  58. package/skills/bot-detection-specialist/SKILL.md +109 -0
  59. package/skills/business-logic-attacker/SKILL.md +231 -0
  60. package/skills/capec-code-mapper/SKILL.md +84 -0
  61. package/skills/cert-pin-rotation-specialist/SKILL.md +112 -0
  62. package/skills/cicd-pipeline-hijacker/SKILL.md +405 -0
  63. package/skills/ciso-orchestrator/SKILL.md +454 -43
  64. package/skills/cloud-infra-specialist/SKILL.md +118 -0
  65. package/skills/compliance-gap-analyst/SKILL.md +422 -0
  66. package/skills/compliance-grc/SKILL.md +85 -0
  67. package/skills/compliance-lifecycle-tracker/SKILL.md +84 -0
  68. package/skills/credential-stuffing-specialist/SKILL.md +102 -0
  69. package/skills/crypto-pki-specialist/SKILL.md +87 -0
  70. package/skills/csa-ccm-mapper/SKILL.md +84 -0
  71. package/skills/csf2-governance-mapper/SKILL.md +84 -0
  72. package/skills/deep-link-fuzzer/SKILL.md +109 -0
  73. package/skills/dependency-confusion-attacker/SKILL.md +415 -0
  74. package/skills/device-integrity-aggregator/SKILL.md +108 -0
  75. package/skills/dos-resilience-tester/SKILL.md +97 -0
  76. package/skills/dread-scorer/SKILL.md +84 -0
  77. package/skills/egress-policy-enforcer/SKILL.md +99 -0
  78. package/skills/evidence-collector/SKILL.md +98 -0
  79. package/skills/file-upload-attacker/SKILL.md +109 -0
  80. package/skills/gcp-penetration-tester/SKILL.md +459 -2
  81. package/skills/git-history-secret-scanner/SKILL.md +106 -0
  82. package/skills/iam-privesc-graph-builder/SKILL.md +152 -0
  83. package/skills/incident-responder/SKILL.md +111 -0
  84. package/skills/injection-specialist/SKILL.md +102 -0
  85. package/skills/ios-security-auditor/SKILL.md +282 -0
  86. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  87. package/skills/k8s-container-escaper/SKILL.md +384 -0
  88. package/skills/key-management-lifecycle-analyst/SKILL.md +98 -0
  89. package/skills/kill-switch-engineer/SKILL.md +102 -0
  90. package/skills/linddun-privacy-analyst/SKILL.md +102 -0
  91. package/skills/logic-race-fuzzer/SKILL.md +443 -0
  92. package/skills/mobile-api-network-attacker/SKILL.md +421 -0
  93. package/skills/mobile-binary-hardener/SKILL.md +102 -0
  94. package/skills/mobile-security-specialist/SKILL.md +85 -0
  95. package/skills/mobile-webview-auditor/SKILL.md +96 -0
  96. package/skills/model-extraction-attacker/SKILL.md +219 -0
  97. package/skills/multipart-abuse-tester/SKILL.md +84 -0
  98. package/skills/oauth-pkce-specialist/SKILL.md +104 -0
  99. package/skills/parser-exhaustion-tester/SKILL.md +142 -0
  100. package/skills/pentest-infra/SKILL.md +98 -0
  101. package/skills/pentest-social/SKILL.md +201 -0
  102. package/skills/pentest-team/SKILL.md +87 -0
  103. package/skills/pentest-web-api/SKILL.md +98 -0
  104. package/skills/privacy-flow-analyst/SKILL.md +234 -0
  105. package/skills/prompt-injection-specialist/SKILL.md +394 -0
  106. package/skills/quantum-migration-planner/SKILL.md +96 -0
  107. package/skills/rag-poisoning-specialist/SKILL.md +358 -0
  108. package/skills/registry-mirror-enforcer/SKILL.md +84 -0
  109. package/skills/rotation-validation-agent/SKILL.md +112 -0
  110. package/skills/samm-assessor/SKILL.md +85 -0
  111. package/skills/secrets-mask-bypass-tester/SKILL.md +100 -0
  112. package/skills/senior-security-engineer/SKILL.md +167 -0
  113. package/skills/serialization-memory-attacker/SKILL.md +332 -0
  114. package/skills/session-timeout-tester/SKILL.md +161 -0
  115. package/skills/slsa-level3-enforcer/SKILL.md +112 -0
  116. package/skills/slsa-provenance-enforcer/SKILL.md +102 -0
  117. package/skills/ssrf-detection-validator/SKILL.md +108 -0
  118. package/skills/step-up-auth-enforcer/SKILL.md +84 -0
  119. package/skills/stride-pasta-analyst/SKILL.md +420 -0
  120. package/skills/supply-chain-devsecops/SKILL.md +98 -0
  121. package/skills/threat-infrastructure-analyst/SKILL.md +84 -0
  122. package/skills/threat-modeler/SKILL.md +85 -0
  123. package/skills/tls-certificate-auditor/SKILL.md +573 -18
  124. package/skills/token-reuse-detector/SKILL.md +95 -0
  125. package/skills/trike-risk-modeler/SKILL.md +84 -0
  126. package/skills/unicode-homograph-tester/SKILL.md +84 -0
  127. package/skills/waf-rule-lifecycle-agent/SKILL.md +97 -0
  128. package/skills/webhook-security-tester/SKILL.md +102 -0
  129. package/skills/zero-trust-architect/SKILL.md +109 -0
@@ -6,6 +6,191 @@ import { sanitizeErrorMessage } from "../result.js";
6
6
  import { searchRepo } from "../../repo/search.js";
7
7
  import fg from "fast-glob";
8
8
  import { readFileSafe } from "../../repo/fs.js";
9
+ async function checkGraphqlIntrospection() {
10
+ const findings = [];
11
+ // Find GraphQL server instantiation sites
12
+ const serverHits = await searchRepo({
13
+ query: String.raw `ApolloServer|createServer|buildSchema|makeExecutableSchema|new GraphQL`,
14
+ isRegex: true,
15
+ maxMatches: 200
16
+ });
17
+ if (serverHits.length === 0)
18
+ return findings;
19
+ // Check for explicit disabling of introspection near server setup
20
+ const disableHits = await searchRepo({
21
+ query: String.raw `introspection\s*:\s*false|disableIntrospection|NoIntrospection|validationRules.*introspection`,
22
+ isRegex: true,
23
+ maxMatches: 200
24
+ });
25
+ // Check for introspection: true explicitly set
26
+ const alwaysOnHits = await searchRepo({
27
+ query: String.raw `introspection\s*:\s*true`,
28
+ isRegex: true,
29
+ maxMatches: 200
30
+ });
31
+ // Filter always-on hits that have no NODE_ENV guard
32
+ const unguardedAlwaysOn = alwaysOnHits.filter((m) => !/NODE_ENV|process\.env/i.test(m.preview));
33
+ if (unguardedAlwaysOn.length > 0) {
34
+ findings.push({
35
+ id: "GRAPHQL_INTROSPECTION_ALWAYS_ON",
36
+ title: "GraphQL introspection is explicitly enabled without a NODE_ENV guard",
37
+ severity: "CRITICAL",
38
+ evidence: unguardedAlwaysOn.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
39
+ files: [...new Set(unguardedAlwaysOn.slice(0, 10).map((m) => m.file))],
40
+ requiredActions: [
41
+ "Disable introspection unconditionally in production.",
42
+ "Use `introspection: process.env.NODE_ENV !== 'production'` at minimum."
43
+ ]
44
+ });
45
+ }
46
+ else if (disableHits.length === 0) {
47
+ // Server setup found but introspection is not explicitly disabled
48
+ findings.push({
49
+ id: "GRAPHQL_INTROSPECTION_ENABLED",
50
+ title: "GraphQL introspection is enabled by default; ensure it is disabled in production with `introspection: process.env.NODE_ENV !== 'production'`",
51
+ severity: "HIGH",
52
+ evidence: serverHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
53
+ files: [...new Set(serverHits.slice(0, 10).map((m) => m.file))],
54
+ requiredActions: [
55
+ "Disable introspection in non-dev environments.",
56
+ "Use persisted queries instead of ad-hoc introspection in production."
57
+ ]
58
+ });
59
+ }
60
+ return findings;
61
+ }
62
+ async function checkGraphqlAliasAmplification(graphqlInUse) {
63
+ if (!graphqlInUse)
64
+ return [];
65
+ const findings = [];
66
+ const complexityHits = await searchRepo({
67
+ query: String.raw `complexityPlugin|costAnalysis|queryComplexity|createComplexityRule`,
68
+ isRegex: true,
69
+ maxMatches: 200
70
+ });
71
+ if (complexityHits.length === 0) {
72
+ findings.push({
73
+ id: "GRAPHQL_NO_COMPLEXITY_LIMIT",
74
+ title: "No GraphQL query complexity limiter detected",
75
+ severity: "HIGH",
76
+ requiredActions: [
77
+ "Add graphql-query-complexity or graphql-cost-analysis to limit query cost.",
78
+ "Set a maximum complexity budget to prevent amplified alias abuse."
79
+ ]
80
+ });
81
+ return findings;
82
+ }
83
+ // Complexity limiter found — check if it accounts for aliases
84
+ const aliasHits = await searchRepo({
85
+ query: String.raw `aliasCost|aliasMultiplier|alias.*cost|fieldCost.*alias`,
86
+ isRegex: true,
87
+ maxMatches: 200
88
+ });
89
+ if (aliasHits.length === 0) {
90
+ findings.push({
91
+ id: "GRAPHQL_ALIAS_AMPLIFICATION",
92
+ title: "GraphQL complexity limiter found but alias cost not configured — alias amplification attacks possible",
93
+ severity: "HIGH",
94
+ evidence: complexityHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
95
+ files: [...new Set(complexityHits.slice(0, 10).map((m) => m.file))],
96
+ requiredActions: [
97
+ "Configure alias cost or alias multiplier in the complexity plugin.",
98
+ "Without alias accounting, attackers can use field aliasing to bypass complexity limits."
99
+ ]
100
+ });
101
+ }
102
+ return findings;
103
+ }
104
+ async function checkGraphqlResolverInjection() {
105
+ const findings = [];
106
+ const resolverInjectionHits = await searchRepo({
107
+ query: String.raw `(?:resolve|resolver)\s*\([^)]*\)\s*\{[^}]*(?:SELECT|INSERT|UPDATE|DELETE|\$where|\$regex|aggregate)\s*['"].*\$\{args\.`,
108
+ isRegex: true,
109
+ maxMatches: 200
110
+ });
111
+ const resolverInjectionBroadHits = await searchRepo({
112
+ query: String.raw `resolve.*\{[^}]*(?:SELECT|INSERT)[^}]*\$\{args`,
113
+ isRegex: true,
114
+ maxMatches: 200
115
+ });
116
+ const allHits = [...resolverInjectionHits, ...resolverInjectionBroadHits];
117
+ const uniqueHits = allHits.filter((hit, idx, arr) => arr.findIndex((h) => h.file === hit.file && h.line === hit.line) === idx);
118
+ if (uniqueHits.length > 0) {
119
+ findings.push({
120
+ id: "GRAPHQL_RESOLVER_INJECTION",
121
+ title: "GraphQL resolver argument concatenated into raw SQL/NoSQL query — injection via resolver args (CWE-89/CWE-943)",
122
+ severity: "CRITICAL",
123
+ evidence: uniqueHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
124
+ files: [...new Set(uniqueHits.slice(0, 10).map((m) => m.file))],
125
+ requiredActions: [
126
+ "Never interpolate resolver args directly into SQL or NoSQL queries.",
127
+ "Use parameterized queries or an ORM to pass resolver arguments safely.",
128
+ "Validate and sanitize all args before use in any query expression."
129
+ ]
130
+ });
131
+ }
132
+ return findings;
133
+ }
134
+ async function checkGraphqlAliasBatching() {
135
+ const findings = [];
136
+ const serverHits = await searchRepo({
137
+ query: String.raw `(?:ApolloServer|makeExecutableSchema|buildSchema|graphqlHTTP)\s*\(`,
138
+ isRegex: true,
139
+ maxMatches: 200
140
+ });
141
+ if (serverHits.length === 0)
142
+ return findings;
143
+ const aliasLimitHits = await searchRepo({
144
+ query: String.raw `(?:maxAliasCount|depthLimit|complexityLimit|queryComplexity|fieldExtensions.*complexity)`,
145
+ isRegex: true,
146
+ maxMatches: 200
147
+ });
148
+ if (aliasLimitHits.length === 0) {
149
+ findings.push({
150
+ id: "GRAPHQL_ALIAS_BATCHING",
151
+ title: "GraphQL server without alias count limit — N+1 batching enables account enumeration and DoS (CWE-770)",
152
+ severity: "HIGH",
153
+ evidence: serverHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
154
+ files: [...new Set(serverHits.slice(0, 10).map((m) => m.file))],
155
+ requiredActions: [
156
+ "Add a maxAliasCount or equivalent alias limit to the GraphQL server configuration.",
157
+ "Use graphql-query-complexity or graphql-depth-limit to bound alias expansion.",
158
+ "Without limits, attackers can batch aliased fields to enumerate data or exhaust backend resources."
159
+ ]
160
+ });
161
+ }
162
+ return findings;
163
+ }
164
+ async function checkGraphqlCircularFragments(graphqlInUse) {
165
+ if (!graphqlInUse)
166
+ return [];
167
+ const findings = [];
168
+ const fragmentProtectionHits = await searchRepo({
169
+ query: String.raw `NoSchemaIntrospectionCustomRule|maxFragmentDepth|FragmentDepthLimit`,
170
+ isRegex: true,
171
+ maxMatches: 200
172
+ });
173
+ const validationRulesHits = await searchRepo({
174
+ query: String.raw `specifiedRules|validationRules`,
175
+ isRegex: true,
176
+ maxMatches: 200
177
+ });
178
+ const hasFragmentProtection = fragmentProtectionHits.length > 0 ||
179
+ validationRulesHits.some((m) => /maxFragmentDepth|FragmentDepthLimit/i.test(m.preview));
180
+ if (!hasFragmentProtection) {
181
+ findings.push({
182
+ id: "GRAPHQL_CIRCULAR_FRAGMENT_RISK",
183
+ title: "No GraphQL fragment depth limiting detected — circular fragment DoS risk",
184
+ severity: "MEDIUM",
185
+ requiredActions: [
186
+ "Add fragment depth limiting via a custom validation rule.",
187
+ "Use graphql-depth-limit or implement NoSchemaIntrospectionCustomRule with fragment cycle detection.",
188
+ "Circular fragments can be used to exhaust server resources."
189
+ ]
190
+ });
191
+ }
192
+ return findings;
193
+ }
9
194
  export async function checkGraphQL(_opts) {
10
195
  const findings = [];
11
196
  try {
@@ -18,25 +203,10 @@ export async function checkGraphQL(_opts) {
18
203
  if (graphqlHits.length === 0) {
19
204
  return [];
20
205
  }
21
- // 2. Introspection enabled in prod
22
- const introspectionHits = await searchRepo({
23
- query: String.raw `introspection.*true|disableIntrospection.*false|GraphQLSchema.*introspection`,
24
- isRegex: true,
25
- maxMatches: 200
26
- });
27
- if (introspectionHits.length > 0) {
28
- findings.push({
29
- id: "GRAPHQL_INTROSPECTION_ENABLED",
30
- title: "GraphQL introspection is enabled — exposes full schema to attackers",
31
- severity: "HIGH",
32
- evidence: introspectionHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
33
- files: [...new Set(introspectionHits.slice(0, 10).map((m) => m.file))],
34
- requiredActions: [
35
- "Disable introspection in non-dev environments.",
36
- "Use persisted queries instead of ad-hoc introspection in production."
37
- ]
38
- });
39
- }
206
+ const graphqlInUse = true;
207
+ // 2. Introspection check (corrected: fire when NOT explicitly disabled)
208
+ const introspectionFindings = await checkGraphqlIntrospection();
209
+ findings.push(...introspectionFindings);
40
210
  // 3. No query depth/complexity limiting
41
211
  const depthLimitHits = await searchRepo({
42
212
  query: String.raw `depthLimit|complexityLimit|queryComplexity|createComplexityRule|maxDepth`,
@@ -119,6 +289,18 @@ export async function checkGraphQL(_opts) {
119
289
  ]
120
290
  });
121
291
  }
292
+ // 7. Alias amplification detection
293
+ const aliasFindings = await checkGraphqlAliasAmplification(graphqlInUse);
294
+ findings.push(...aliasFindings);
295
+ // 8. Circular fragment protection
296
+ const fragmentFindings = await checkGraphqlCircularFragments(graphqlInUse);
297
+ findings.push(...fragmentFindings);
298
+ // 9. Resolver injection
299
+ const resolverInjectionFindings = await checkGraphqlResolverInjection();
300
+ findings.push(...resolverInjectionFindings);
301
+ // 10. Alias batching without limit
302
+ const aliasBatchingFindings = await checkGraphqlAliasBatching();
303
+ findings.push(...aliasBatchingFindings);
122
304
  }
123
305
  catch (err) {
124
306
  console.warn("[checkGraphQL] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
@@ -24,7 +24,7 @@ const IAM_WILDCARD_PATTERN = String.raw `"Action"\s*:\s*"\*"|` + // AWS IAM wild
24
24
  String.raw `roles/owner|roles/editor|` + // GCP over-privileged built-in roles
25
25
  String.raw `allUsers|allAuthenticatedUsers|` + // GCP public IAM
26
26
  String.raw `"role"\s*:\s*"roles/owner"|` + // GCP Terraform owner binding
27
- String.raw `contributor|Owner\b.*roleDefinitionId`; // Azure Contributor/Owner
27
+ String.raw `\bContributor\b.*roleDefinitionId|\bOwner\b.*roleDefinitionId`; // Azure Contributor/Owner (word-bounded to avoid matching variable names)
28
28
  // Public network exposure — Terraform, K8s, CloudFormation, ARM, CDK
29
29
  const PUBLIC_INGRESS_PATTERN = String.raw `0\.0\.0\.0/0|::/0|` +
30
30
  String.raw `public\s*=\s*true|` +
@@ -148,5 +148,250 @@ export async function checkInfra(_) {
148
148
  ]
149
149
  });
150
150
  }
151
+ // 6–16. Additional cloud-specific checks (all searches run in parallel)
152
+ const [imdsv1Results, lambdaUrlNoAuthResults, ecrNoScanResults, ecsHostNetworkResults, cloudtrailNotMultiregionResults, s3NoAccessLoggingResults, vpcNoFlowLogsResults, assumeRoleResults, externalIdResults, gcpDefaultSaResults, gcpProjectSshResults, gcpExternalIpResults, azurePublicNetworkResults, dbNoDeletionProtectionResults, vpcEndpointResults, awsInfraResults, guarddutyResults, securityHubResults,] = await Promise.all([
153
+ // hop_limit [2-9] misses values >= 10; use \d{2,}|[2-9] to catch all insecure values
154
+ searchRepo({ query: String.raw `http_tokens\s*=\s*"optional"|http_put_response_hop_limit\s*=\s*(?:[2-9]|\d{2,})`, isRegex: true, maxMatches: 200 }),
155
+ searchRepo({ query: String.raw `(?:FunctionUrlAuthType|authorization_type)\s*[=:]\s*"NONE"`, isRegex: true, maxMatches: 200 }),
156
+ searchRepo({ query: String.raw `scan_on_push\s*=\s*false`, isRegex: true, maxMatches: 200 }),
157
+ searchRepo({ query: String.raw `(?:network_mode|networkMode)\s*[=:]\s*"host"`, isRegex: true, maxMatches: 200 }),
158
+ searchRepo({ query: String.raw `is_multi_region_trail\s*=\s*false|"IsMultiRegionTrail"\s*:\s*false`, isRegex: true, maxMatches: 200 }),
159
+ searchRepo({ query: String.raw `"LoggingEnabled"\s*:\s*\{\s*\}|target_bucket\s*=\s*""`, isRegex: true, maxMatches: 200 }),
160
+ // aws_vpc has no enable_flow_log attr; use aws_flow_log resource absence as the signal instead
161
+ searchRepo({ query: String.raw `aws_flow_log`, isRegex: true, maxMatches: 200 }),
162
+ searchRepo({ query: String.raw `"Action"\s*:\s*"sts:AssumeRole"`, isRegex: true, maxMatches: 200 }),
163
+ searchRepo({ query: String.raw `sts:ExternalId`, isRegex: true, maxMatches: 200 }),
164
+ searchRepo({ query: String.raw `-compute@developer\.gserviceaccount\.com`, isRegex: true, maxMatches: 200 }),
165
+ searchRepo({ query: String.raw `"ssh-keys"\s*:`, isRegex: true, maxMatches: 200 }),
166
+ // access_config {} catches only ephemeral IPs; access_config { nat_ip = ... } (static) also exposes external IP
167
+ searchRepo({ query: String.raw `access_config\s*\{`, isRegex: true, maxMatches: 200 }),
168
+ searchRepo({ query: String.raw `public_network_access_enabled\s*=\s*true`, isRegex: true, maxMatches: 200 }),
169
+ searchRepo({ query: String.raw `(?:deletion_protection|enable_deletion_protection)\s*=\s*false`, isRegex: true, maxMatches: 200 }),
170
+ searchRepo({ query: String.raw `aws_vpc_endpoint`, isRegex: true, maxMatches: 200 }),
171
+ searchRepo({ query: String.raw `aws_(?:instance|ecs_service|lambda_function)`, isRegex: true, maxMatches: 200 }),
172
+ searchRepo({ query: String.raw `aws_guardduty_detector`, isRegex: true, maxMatches: 200 }),
173
+ searchRepo({ query: String.raw `aws_securityhub_account`, isRegex: true, maxMatches: 200 }),
174
+ ]);
175
+ // 6. IMDSv1 accessible
176
+ if (imdsv1Results.length > 0) {
177
+ findings.push({
178
+ id: "INFRA_IMDSV1_ACCESSIBLE",
179
+ title: "IMDSv1 still accessible on EC2 — SSRF attackers can reach 169.254.169.254 for IAM credentials",
180
+ severity: "CRITICAL",
181
+ evidence: imdsv1Results.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
182
+ requiredActions: [
183
+ "Set http_tokens = \"required\" on all aws_instance and launch template resources.",
184
+ "Set http_put_response_hop_limit = 1 to prevent hop-based SSRF escalation.",
185
+ "Enforce IMDSv2-only at the AWS account level via EC2 default metadata options."
186
+ ]
187
+ });
188
+ }
189
+ // 7. Lambda URL no auth
190
+ if (lambdaUrlNoAuthResults.length > 0) {
191
+ findings.push({
192
+ id: "INFRA_LAMBDA_URL_NO_AUTH",
193
+ title: "Lambda function URL with no authentication — publicly invocable by anyone",
194
+ severity: "CRITICAL",
195
+ evidence: lambdaUrlNoAuthResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
196
+ requiredActions: [
197
+ "Set authorization_type = \"AWS_IAM\" on all aws_lambda_function_url resources.",
198
+ "Use a CloudFront distribution with signed URLs or an API Gateway with IAM/Cognito auth as an alternative.",
199
+ "If public invocation is intentional, add CORS restrictions and rate limiting."
200
+ ]
201
+ });
202
+ }
203
+ // 8. ECR scan on push disabled
204
+ if (ecrNoScanResults.length > 0) {
205
+ findings.push({
206
+ id: "INFRA_ECR_NO_SCAN",
207
+ title: "ECR scan-on-push disabled — container images deployed without CVE scanning",
208
+ severity: "HIGH",
209
+ evidence: ecrNoScanResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
210
+ requiredActions: [
211
+ "Set scan_on_push = true on all aws_ecr_repository resources.",
212
+ "Enable ECR Enhanced Scanning (Inspector-based) for continuous vulnerability monitoring.",
213
+ "Gate deployments on zero critical/high CVEs using CI checks against ECR scan results."
214
+ ]
215
+ });
216
+ }
217
+ // 9. ECS host network
218
+ if (ecsHostNetworkResults.length > 0) {
219
+ findings.push({
220
+ id: "INFRA_ECS_HOST_NETWORK",
221
+ title: "ECS task using host network mode — bypasses container network isolation",
222
+ severity: "HIGH",
223
+ evidence: ecsHostNetworkResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
224
+ requiredActions: [
225
+ "Use network_mode = \"awsvpc\" for Fargate or bridge mode for EC2 ECS tasks.",
226
+ "Host networking exposes all host ports to the container — remove unless strictly required.",
227
+ "Apply security groups at the task level when using awsvpc mode."
228
+ ]
229
+ });
230
+ }
231
+ // 10. CloudTrail not multi-region
232
+ if (cloudtrailNotMultiregionResults.length > 0) {
233
+ findings.push({
234
+ id: "INFRA_CLOUDTRAIL_NOT_MULTIREGION",
235
+ title: "CloudTrail is not multi-region — attacker actions in secondary regions go unlogged",
236
+ severity: "HIGH",
237
+ evidence: cloudtrailNotMultiregionResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
238
+ requiredActions: [
239
+ "Set is_multi_region_trail = true on all aws_cloudtrail resources.",
240
+ "Enable CloudTrail in all opted-in regions including global service events.",
241
+ "Send CloudTrail logs to a dedicated, cross-account S3 bucket with MFA delete enabled."
242
+ ]
243
+ });
244
+ }
245
+ // 11. S3 server access logging disabled
246
+ if (s3NoAccessLoggingResults.length > 0) {
247
+ findings.push({
248
+ id: "INFRA_S3_NO_ACCESS_LOGGING",
249
+ title: "S3 server access logging not enabled — exfiltration events undetectable post-incident",
250
+ severity: "MEDIUM",
251
+ evidence: s3NoAccessLoggingResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
252
+ requiredActions: [
253
+ "Configure server access logging on all S3 buckets by specifying a target_bucket.",
254
+ "Use AWS CloudTrail data events as a supplementary audit trail for S3 object-level operations.",
255
+ "Retain access logs for at least 90 days and ship to a SIEM for alerting."
256
+ ]
257
+ });
258
+ }
259
+ // 12. VPC flow logs missing (absence check: AWS infra present but no aws_flow_log resource found)
260
+ if (awsInfraResults.length > 0 && vpcNoFlowLogsResults.length === 0) {
261
+ findings.push({
262
+ id: "INFRA_VPC_NO_FLOW_LOGS",
263
+ title: "No aws_flow_log resource found — VPC network traffic is unlogged, lateral movement and exfiltration undetectable",
264
+ severity: "MEDIUM",
265
+ requiredActions: [
266
+ "Add an aws_flow_log resource for each VPC and ship logs to CloudWatch Logs or S3.",
267
+ "Set flow log aggregation interval to 1 minute for near-real-time detection.",
268
+ "Create CloudWatch metric filters and alarms for rejected traffic spikes."
269
+ ]
270
+ });
271
+ }
272
+ // 13. Cross-account trust without ExternalId
273
+ if (assumeRoleResults.length > 0 && externalIdResults.length === 0) {
274
+ findings.push({
275
+ id: "INFRA_CROSS_ACCOUNT_NO_EXTERNAL_ID",
276
+ title: "Cross-account IAM trust without sts:ExternalId condition — confused deputy attack possible",
277
+ severity: "HIGH",
278
+ evidence: assumeRoleResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
279
+ requiredActions: [
280
+ "Add a Condition block with sts:ExternalId to all cross-account AssumeRole trust policies.",
281
+ "Use a unique, unguessable ExternalId per third-party relationship and rotate periodically.",
282
+ "Audit all cross-account role trusts with AWS IAM Access Analyzer."
283
+ ]
284
+ });
285
+ }
286
+ // 14. GCP default service account
287
+ if (gcpDefaultSaResults.length > 0) {
288
+ findings.push({
289
+ id: "INFRA_GCP_DEFAULT_SERVICE_ACCOUNT",
290
+ title: "GCP instance uses default Compute Engine service account — broad project-level API permissions",
291
+ severity: "HIGH",
292
+ evidence: gcpDefaultSaResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
293
+ requiredActions: [
294
+ "Create a dedicated, least-privilege service account for each GCP compute resource.",
295
+ "Disable the default Compute Engine service account or remove the Editor role binding.",
296
+ "Use Workload Identity Federation instead of service account keys where possible."
297
+ ]
298
+ });
299
+ }
300
+ // 15. GCP project-level SSH keys
301
+ if (gcpProjectSshResults.length > 0) {
302
+ findings.push({
303
+ id: "INFRA_GCP_PROJECT_SSH_KEYS",
304
+ title: "GCP project-level SSH keys set — single key compromise grants access to all instances",
305
+ severity: "MEDIUM",
306
+ evidence: gcpProjectSshResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
307
+ requiredActions: [
308
+ "Remove project-level SSH keys from project metadata and use instance-level keys only.",
309
+ "Prefer OS Login over metadata-based SSH keys for centralized IAM-controlled access.",
310
+ "Rotate any existing project-level SSH keys immediately and audit which instances they reached."
311
+ ]
312
+ });
313
+ }
314
+ // 16. GCP compute external IP
315
+ if (gcpExternalIpResults.length > 0) {
316
+ findings.push({
317
+ id: "INFRA_GCP_EXTERNAL_IP",
318
+ title: "GCP compute instance has external IP — directly internet-reachable without load balancer",
319
+ severity: "MEDIUM",
320
+ evidence: gcpExternalIpResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
321
+ requiredActions: [
322
+ "Remove access_config blocks from google_compute_instance resources to disable external IPs.",
323
+ "Route traffic through a GCP Cloud Load Balancer or Cloud NAT instead of direct external IPs.",
324
+ "Use Identity-Aware Proxy (IAP) for admin access rather than exposing SSH/RDP externally."
325
+ ]
326
+ });
327
+ }
328
+ // 17. Azure public network access
329
+ if (azurePublicNetworkResults.length > 0) {
330
+ findings.push({
331
+ id: "INFRA_AZURE_PUBLIC_NETWORK_ACCESS",
332
+ title: "Azure managed service with public_network_access_enabled=true — reachable from internet",
333
+ severity: "HIGH",
334
+ evidence: azurePublicNetworkResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
335
+ requiredActions: [
336
+ "Set public_network_access_enabled = false on all Azure managed services.",
337
+ "Use Private Endpoints and Private DNS Zones for internal connectivity.",
338
+ "Apply Azure Firewall or NSG rules to restrict any legitimately public-facing services."
339
+ ]
340
+ });
341
+ }
342
+ // 18. Database deletion protection disabled
343
+ if (dbNoDeletionProtectionResults.length > 0) {
344
+ findings.push({
345
+ id: "INFRA_DB_NO_DELETION_PROTECTION",
346
+ title: "Database resource missing deletion protection — single terraform apply can permanently destroy prod DB",
347
+ severity: "HIGH",
348
+ evidence: dbNoDeletionProtectionResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
349
+ requiredActions: [
350
+ "Set deletion_protection = true on all aws_db_instance, aws_rds_cluster, and equivalent resources.",
351
+ "Enable automated backups with a retention window of at least 7 days.",
352
+ "Use Terraform prevent_destroy lifecycle rules as an additional safeguard."
353
+ ]
354
+ });
355
+ }
356
+ // 19. Missing VPC endpoint for S3/ECR
357
+ if (awsInfraResults.length > 0 && vpcEndpointResults.length === 0) {
358
+ findings.push({
359
+ id: "INFRA_NO_VPC_ENDPOINT",
360
+ title: "No VPC endpoint found for AWS services — traffic routes over public internet",
361
+ severity: "MEDIUM",
362
+ evidence: awsInfraResults.slice(0, 20).map((m) => `${m.file}:${m.line}: ${m.preview}`),
363
+ requiredActions: [
364
+ "Create aws_vpc_endpoint resources for S3, ECR (api + dkr), and other frequently used AWS services.",
365
+ "Use Gateway endpoints for S3/DynamoDB (free) and Interface endpoints for other services.",
366
+ "Set the VPC endpoint policy to restrict access to specific S3 buckets or ECR repositories."
367
+ ]
368
+ });
369
+ }
370
+ // 20. GuardDuty not enabled
371
+ if (awsInfraResults.length > 0 && guarddutyResults.length === 0) {
372
+ findings.push({
373
+ id: "INFRA_GUARDDUTY_MISSING",
374
+ title: "No GuardDuty detector resource found — threat detection (credential misuse, crypto-mining) disabled",
375
+ severity: "HIGH",
376
+ requiredActions: [
377
+ "Add an aws_guardduty_detector resource with enable = true to your Terraform.",
378
+ "Enable GuardDuty in all AWS regions and aggregate findings into a delegated admin account.",
379
+ "Subscribe to GuardDuty findings via EventBridge and route high-severity alerts to your on-call channel."
380
+ ]
381
+ });
382
+ }
383
+ // 21. Security Hub not enabled
384
+ if (awsInfraResults.length > 0 && securityHubResults.length === 0) {
385
+ findings.push({
386
+ id: "INFRA_SECURITY_HUB_MISSING",
387
+ title: "AWS Security Hub not enabled — findings from GuardDuty/Inspector/Macie not centrally aggregated",
388
+ severity: "MEDIUM",
389
+ requiredActions: [
390
+ "Add an aws_securityhub_account resource to your Terraform.",
391
+ "Enable the AWS Foundational Security Best Practices and CIS AWS Foundations standards.",
392
+ "Aggregate Security Hub findings across regions and accounts into a central delegated admin."
393
+ ]
394
+ });
395
+ }
151
396
  return findings;
152
397
  }