security-mcp 1.1.4 → 1.3.3

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 (158) hide show
  1. package/README.md +341 -1018
  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/cloud-controls/aws.json +10712 -0
  9. package/defaults/cloud-controls/azure.json +7201 -0
  10. package/defaults/cloud-controls/gcp.json +4061 -0
  11. package/defaults/control-catalog.json +24 -0
  12. package/defaults/security-policy.json +2 -2
  13. package/dist/ci/pr-gate.js +22 -5
  14. package/dist/cli/index.js +73 -2
  15. package/dist/cli/install.js +4 -55
  16. package/dist/cli/onboarding.js +18 -10
  17. package/dist/gate/baseline.js +82 -7
  18. package/dist/gate/catalog.js +10 -2
  19. package/dist/gate/checks/agentic-instructions.js +515 -0
  20. package/dist/gate/checks/ai-governance.js +132 -0
  21. package/dist/gate/checks/ai.js +757 -39
  22. package/dist/gate/checks/auth-deep.js +920 -216
  23. package/dist/gate/checks/business-logic.js +751 -0
  24. package/dist/gate/checks/ci-pipeline.js +399 -4
  25. package/dist/gate/checks/cloud-controls.js +69 -0
  26. package/dist/gate/checks/crypto.js +423 -2
  27. package/dist/gate/checks/data-platform.js +954 -0
  28. package/dist/gate/checks/dependencies.js +582 -15
  29. package/dist/gate/checks/docker-deep.js +1236 -0
  30. package/dist/gate/checks/gitops.js +724 -0
  31. package/dist/gate/checks/graphql.js +201 -19
  32. package/dist/gate/checks/iac.js +1230 -0
  33. package/dist/gate/checks/infra.js +246 -1
  34. package/dist/gate/checks/injection-deep.js +827 -184
  35. package/dist/gate/checks/k8s.js +955 -2
  36. package/dist/gate/checks/mobile-android.js +917 -3
  37. package/dist/gate/checks/mobile-ios.js +797 -5
  38. package/dist/gate/checks/required-artifacts.js +194 -0
  39. package/dist/gate/checks/runtime.js +178 -0
  40. package/dist/gate/checks/secrets.js +256 -13
  41. package/dist/gate/checks/supply-chain-deep.js +787 -0
  42. package/dist/gate/checks/web-nextjs.js +572 -48
  43. package/dist/gate/cloud-controls/apply.js +115 -0
  44. package/dist/gate/cloud-controls/bicep.js +36 -0
  45. package/dist/gate/cloud-controls/cfn.js +125 -0
  46. package/dist/gate/cloud-controls/detect.js +104 -0
  47. package/dist/gate/cloud-controls/hcl.js +140 -0
  48. package/dist/gate/cloud-controls/types.js +87 -0
  49. package/dist/gate/diff.js +17 -5
  50. package/dist/gate/evidence.js +8 -1
  51. package/dist/gate/exceptions.js +202 -9
  52. package/dist/gate/findings.js +15 -2
  53. package/dist/gate/policy.js +316 -130
  54. package/dist/gate/threat-intel.js +6 -0
  55. package/dist/mcp/audit-chain.js +131 -28
  56. package/dist/mcp/auth.js +169 -0
  57. package/dist/mcp/learning.js +129 -4
  58. package/dist/mcp/model-router.js +161 -24
  59. package/dist/mcp/orchestration.js +377 -89
  60. package/dist/mcp/server.js +460 -69
  61. package/dist/mcp/tool-audit.js +193 -0
  62. package/dist/repo/fs.js +37 -1
  63. package/dist/repo/search.js +31 -6
  64. package/dist/review/store.js +56 -3
  65. package/dist/tests/run.js +124 -1
  66. package/package.json +9 -9
  67. package/skills/_TEMPLATE/SKILL.md +99 -0
  68. package/skills/advanced-dos-tester/SKILL.md +118 -0
  69. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  70. package/skills/agentic-loop-exploiter/SKILL.md +377 -0
  71. package/skills/ai-llm-redteam/SKILL.md +113 -0
  72. package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
  73. package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
  74. package/skills/android-penetration-tester/SKILL.md +464 -46
  75. package/skills/anti-replay-tester/SKILL.md +115 -0
  76. package/skills/appsec-code-auditor/SKILL.md +94 -0
  77. package/skills/artifact-integrity-analyst/SKILL.md +450 -0
  78. package/skills/attack-navigator/SKILL.md +476 -8
  79. package/skills/auth-session-hacker/SKILL.md +111 -0
  80. package/skills/aws-penetration-tester/SKILL.md +510 -0
  81. package/skills/azure-penetration-tester/SKILL.md +542 -3
  82. package/skills/binary-auth-validator/SKILL.md +120 -0
  83. package/skills/bot-detection-specialist/SKILL.md +118 -0
  84. package/skills/business-logic-attacker/SKILL.md +240 -0
  85. package/skills/capec-code-mapper/SKILL.md +93 -0
  86. package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
  87. package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
  88. package/skills/ciso-orchestrator/SKILL.md +465 -43
  89. package/skills/cloud-infra-specialist/SKILL.md +127 -0
  90. package/skills/compliance-gap-analyst/SKILL.md +431 -0
  91. package/skills/compliance-grc/SKILL.md +94 -0
  92. package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
  93. package/skills/container-hardening-auditor/SKILL.md +125 -0
  94. package/skills/credential-stuffing-specialist/SKILL.md +111 -0
  95. package/skills/crypto-pki-specialist/SKILL.md +96 -0
  96. package/skills/csa-ccm-mapper/SKILL.md +93 -0
  97. package/skills/csf2-governance-mapper/SKILL.md +93 -0
  98. package/skills/data-platform-auditor/SKILL.md +125 -0
  99. package/skills/deep-link-fuzzer/SKILL.md +118 -0
  100. package/skills/dependency-confusion-attacker/SKILL.md +424 -0
  101. package/skills/device-integrity-aggregator/SKILL.md +117 -0
  102. package/skills/dos-resilience-tester/SKILL.md +106 -0
  103. package/skills/dread-scorer/SKILL.md +93 -0
  104. package/skills/egress-policy-enforcer/SKILL.md +108 -0
  105. package/skills/evidence-collector/SKILL.md +107 -0
  106. package/skills/file-upload-attacker/SKILL.md +118 -0
  107. package/skills/gcp-penetration-tester/SKILL.md +510 -2
  108. package/skills/git-history-secret-scanner/SKILL.md +115 -0
  109. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  110. package/skills/iac-security-auditor/SKILL.md +125 -0
  111. package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
  112. package/skills/incident-responder/SKILL.md +120 -0
  113. package/skills/injection-specialist/SKILL.md +111 -0
  114. package/skills/ios-security-auditor/SKILL.md +291 -0
  115. package/skills/json-ambiguity-tester/SKILL.md +145 -0
  116. package/skills/k8s-container-escaper/SKILL.md +406 -0
  117. package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
  118. package/skills/kill-switch-engineer/SKILL.md +111 -0
  119. package/skills/linddun-privacy-analyst/SKILL.md +111 -0
  120. package/skills/logic-race-fuzzer/SKILL.md +452 -0
  121. package/skills/mobile-api-network-attacker/SKILL.md +430 -0
  122. package/skills/mobile-binary-hardener/SKILL.md +111 -0
  123. package/skills/mobile-security-specialist/SKILL.md +94 -0
  124. package/skills/mobile-webview-auditor/SKILL.md +105 -0
  125. package/skills/model-extraction-attacker/SKILL.md +228 -0
  126. package/skills/multipart-abuse-tester/SKILL.md +93 -0
  127. package/skills/oauth-pkce-specialist/SKILL.md +113 -0
  128. package/skills/parser-exhaustion-tester/SKILL.md +151 -0
  129. package/skills/pentest-infra/SKILL.md +107 -0
  130. package/skills/pentest-social/SKILL.md +210 -0
  131. package/skills/pentest-team/SKILL.md +96 -0
  132. package/skills/pentest-web-api/SKILL.md +107 -0
  133. package/skills/privacy-flow-analyst/SKILL.md +243 -0
  134. package/skills/prompt-injection-specialist/SKILL.md +403 -0
  135. package/skills/quantum-migration-planner/SKILL.md +105 -0
  136. package/skills/rag-poisoning-specialist/SKILL.md +367 -0
  137. package/skills/registry-mirror-enforcer/SKILL.md +93 -0
  138. package/skills/rotation-validation-agent/SKILL.md +121 -0
  139. package/skills/samm-assessor/SKILL.md +94 -0
  140. package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
  141. package/skills/senior-security-engineer/SKILL.md +178 -0
  142. package/skills/serialization-memory-attacker/SKILL.md +341 -0
  143. package/skills/session-timeout-tester/SKILL.md +170 -0
  144. package/skills/slsa-level3-enforcer/SKILL.md +121 -0
  145. package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
  146. package/skills/ssrf-detection-validator/SKILL.md +117 -0
  147. package/skills/step-up-auth-enforcer/SKILL.md +93 -0
  148. package/skills/stride-pasta-analyst/SKILL.md +429 -0
  149. package/skills/supply-chain-devsecops/SKILL.md +107 -0
  150. package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
  151. package/skills/threat-modeler/SKILL.md +94 -0
  152. package/skills/tls-certificate-auditor/SKILL.md +582 -18
  153. package/skills/token-reuse-detector/SKILL.md +104 -0
  154. package/skills/trike-risk-modeler/SKILL.md +93 -0
  155. package/skills/unicode-homograph-tester/SKILL.md +93 -0
  156. package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
  157. package/skills/webhook-security-tester/SKILL.md +111 -0
  158. package/skills/zero-trust-architect/SKILL.md +118 -0
@@ -1,3 +1,4 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
1
2
  import { readFile } from "node:fs/promises";
2
3
  import { dirname, join, resolve } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
@@ -13,13 +14,51 @@ const ExceptionSchema = z.object({
13
14
  owner: z.string(),
14
15
  approver: z.string(),
15
16
  approval_role: z.string(),
17
+ // Fix 1: enforce YYYY-MM-DD format and max 365-day TTL
16
18
  expires_on: z.string()
19
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "expires_on must be YYYY-MM-DD")
20
+ .refine((val) => {
21
+ const expiry = new Date(val);
22
+ const maxExpiry = new Date();
23
+ maxExpiry.setDate(maxExpiry.getDate() + 365);
24
+ return expiry <= maxExpiry;
25
+ }, { message: "expires_on cannot be more than 365 days in the future" })
26
+ })
27
+ // Fix 2: prevent self-approval (owner === approver)
28
+ .superRefine((data, ctx) => {
29
+ if (data.owner.toLowerCase() === data.approver.toLowerCase()) {
30
+ ctx.addIssue({
31
+ code: z.ZodIssueCode.custom,
32
+ message: `Exception self-approval is not permitted: owner and approver must be different (got '${data.owner}')`,
33
+ path: ["approver"]
34
+ });
35
+ }
17
36
  });
18
37
  const ExceptionFileSchema = z.object({
19
38
  version: z.string(),
20
- exceptions: z.array(ExceptionSchema).default([])
39
+ exceptions: z.array(ExceptionSchema).default([]),
40
+ hmacSha256: z.string().optional()
21
41
  });
42
+ // Fix 3: HMAC helper — sign the exceptions array for tamper detection
43
+ export function signExceptionsFile(exceptions, key) {
44
+ const canonical = JSON.stringify(exceptions, (_, v) => (v && typeof v === "object" && !Array.isArray(v)
45
+ ? Object.fromEntries(Object.entries(v).sort())
46
+ : v));
47
+ return createHmac("sha256", key).update(canonical, "utf-8").digest("hex");
48
+ }
49
+ const EXCEPTIONS_HMAC_MIN_KEY_BYTES = 32;
50
+ function getExceptionsHmacKey() {
51
+ const key = process.env["SECURITY_POLICY_HMAC_KEY"];
52
+ if (!key)
53
+ return null;
54
+ if (Buffer.byteLength(key, "utf-8") < EXCEPTIONS_HMAC_MIN_KEY_BYTES) {
55
+ throw new Error(`SECURITY_POLICY_HMAC_KEY is too short (${Buffer.byteLength(key, "utf-8")} bytes). ` +
56
+ `Minimum ${EXCEPTIONS_HMAC_MIN_KEY_BYTES} bytes required.`);
57
+ }
58
+ return key;
59
+ }
22
60
  async function readExceptionsJson() {
61
+ const warnings = [];
23
62
  const overridePath = process.env["SECURITY_GATE_EXCEPTIONS"];
24
63
  if (overridePath) {
25
64
  // CWE-22: ensure path stays within the project directory
@@ -27,25 +66,112 @@ async function readExceptionsJson() {
27
66
  if (!resolved.startsWith(process.cwd() + "/") && resolved !== process.cwd()) {
28
67
  throw new Error(`SECURITY_GATE_EXCEPTIONS path '${overridePath}' escapes the project directory`);
29
68
  }
30
- return await readFile(resolved, "utf-8");
69
+ const raw = await readFile(resolved, "utf-8");
70
+ return { raw, isCiFile: false, isOverride: true, source: overridePath, warnings };
71
+ }
72
+ // Project-level CI exceptions file (suppresses self-scan false positives)
73
+ try {
74
+ const raw = await readFile(join(process.cwd(), ".github", "security-exceptions-ci.json"), "utf-8");
75
+ // Fix 4: warn when CI exceptions are loaded outside CI context
76
+ const isCI = !!(process.env["CI"] || process.env["GITHUB_ACTIONS"]);
77
+ if (!isCI) {
78
+ const count = (() => {
79
+ try {
80
+ const parsed = JSON.parse(raw);
81
+ return Array.isArray(parsed.exceptions) ? parsed.exceptions.length : 0;
82
+ }
83
+ catch {
84
+ return 0;
85
+ }
86
+ })();
87
+ warnings.push({
88
+ id: "CI_EXCEPTIONS_IN_LOCAL_SCAN",
89
+ title: "CI self-scan exceptions applied to local scan",
90
+ severity: "HIGH",
91
+ evidence: [
92
+ "CI exceptions file: .github/security-exceptions-ci.json",
93
+ `Suppressed controls: ${count}`,
94
+ "CI env var not set — this appears to be a local scan"
95
+ ],
96
+ requiredActions: [
97
+ `CI self-scan exceptions (.github/security-exceptions-ci.json) are being applied to a local scan. This suppresses ${count} controls. Set SECURITY_GATE_EXCEPTIONS to point to your project's exceptions file.`
98
+ ]
99
+ });
100
+ }
101
+ return { raw, isCiFile: true, isOverride: false, source: ".github/security-exceptions-ci.json", warnings };
31
102
  }
103
+ catch { /* not present — continue */ }
32
104
  try {
33
- return await readFile(join(process.cwd(), ".mcp", "exceptions", "security-exceptions.json"), "utf-8");
105
+ const raw = await readFile(join(process.cwd(), ".mcp", "exceptions", "security-exceptions.json"), "utf-8");
106
+ return { raw, isCiFile: false, isOverride: false, source: ".mcp/exceptions/security-exceptions.json", warnings };
34
107
  }
35
108
  catch {
36
- return await readFile(join(PKG_ROOT, "defaults", "security-exceptions.json"), "utf-8");
109
+ const raw = await readFile(join(PKG_ROOT, "defaults", "security-exceptions.json"), "utf-8");
110
+ return { raw, isCiFile: false, isOverride: false, source: "defaults/security-exceptions.json", warnings };
37
111
  }
38
112
  }
39
113
  export async function loadSecurityExceptions() {
40
- const raw = await readExceptionsJson();
41
- return ExceptionFileSchema.parse(JSON.parse(raw)).exceptions;
114
+ const { raw, warnings, isOverride, isCiFile, source } = await readExceptionsJson();
115
+ const parsed = ExceptionFileSchema.parse(JSON.parse(raw));
116
+ // Fix 3: HMAC verification of exceptions file
117
+ const hmacKey = getExceptionsHmacKey();
118
+ const extraWarnings = [];
119
+ // #9 fix — opt-in fail-closed: when SECURITY_REQUIRE_SIGNED_EXCEPTIONS is set, an
120
+ // unsigned (or unverifiable) exceptions file is REJECTED rather than trusted. This
121
+ // closes the blanket-suppression bypass (an attacker editing an unsigned exceptions
122
+ // file to silence findings) for operators who enable it. Default-off preserves the
123
+ // backwards-compatible behavior (and self-scan workflows that use unsigned CI files).
124
+ const requireSigned = process.env["SECURITY_REQUIRE_SIGNED_EXCEPTIONS"] === "1" ||
125
+ process.env["SECURITY_REQUIRE_SIGNED_EXCEPTIONS"] === "true";
126
+ if (requireSigned) {
127
+ if (!hmacKey) {
128
+ throw new Error("[loadSecurityExceptions] SECURITY_REQUIRE_SIGNED_EXCEPTIONS is set but SECURITY_POLICY_HMAC_KEY " +
129
+ "is not configured — cannot verify exceptions integrity. Set a ≥32-byte key and sign the exceptions file.");
130
+ }
131
+ if (!parsed.hmacSha256) {
132
+ throw new Error("[loadSecurityExceptions] SECURITY_REQUIRE_SIGNED_EXCEPTIONS is set but the exceptions file is unsigned " +
133
+ "(no hmacSha256). Refusing to apply unsigned exceptions. Sign it with the signExceptionsFile helper.");
134
+ }
135
+ }
136
+ if (hmacKey) {
137
+ if (!parsed.hmacSha256) {
138
+ extraWarnings.push({
139
+ id: "EXCEPTIONS_FILE_UNSIGNED",
140
+ title: "Security exceptions file is not integrity-protected",
141
+ severity: "MEDIUM",
142
+ evidence: [
143
+ "SECURITY_POLICY_HMAC_KEY is set but exceptions file has no hmacSha256 field"
144
+ ],
145
+ requiredActions: [
146
+ "Security exceptions file is not integrity-protected. Set SECURITY_POLICY_HMAC_KEY and re-save exceptions to enable tamper detection."
147
+ ]
148
+ });
149
+ }
150
+ else {
151
+ const expected = signExceptionsFile(parsed.exceptions, hmacKey);
152
+ const storedBuf = Buffer.from(parsed.hmacSha256, "hex");
153
+ const expectedBuf = Buffer.from(expected, "hex");
154
+ const valid = storedBuf.length === expectedBuf.length &&
155
+ timingSafeEqual(storedBuf, expectedBuf);
156
+ if (!valid) {
157
+ throw new Error("[loadSecurityExceptions] HMAC verification failed for exceptions file — file may have been tampered. " +
158
+ "Re-sign exceptions with the signExceptionsFile helper and store the result in hmacSha256.");
159
+ }
160
+ }
161
+ }
162
+ return {
163
+ exceptions: parsed.exceptions,
164
+ warnings: [...warnings, ...extraWarnings],
165
+ integrity: { verified: Boolean(hmacKey && parsed.hmacSha256), isOverride, isCiFile, source }
166
+ };
42
167
  }
43
- export async function applySecurityExceptions(findings) {
44
- const exceptions = await loadSecurityExceptions();
168
+ export async function applySecurityExceptions(findings, opts) {
169
+ const { exceptions, warnings, integrity } = await loadSecurityExceptions();
45
170
  const active = [];
46
171
  const suppressed = [];
47
172
  const exceptionFindings = [];
48
173
  const activeControlExceptionIds = new Set();
174
+ const HIGH_SEVERITIES = new Set(["HIGH", "CRITICAL"]);
49
175
  for (const entry of exceptions) {
50
176
  const expiresAt = new Date(entry.expires_on);
51
177
  if (!Number.isNaN(expiresAt.getTime()) && expiresAt.getTime() >= Date.now()) {
@@ -75,16 +201,83 @@ export async function applySecurityExceptions(findings) {
75
201
  });
76
202
  continue;
77
203
  }
204
+ // Fix 7: enforce require_ticket when set in policy
205
+ if (opts?.requireTicket && !match.ticket) {
206
+ active.push(finding);
207
+ exceptionFindings.push({
208
+ id: "EXCEPTION_MISSING_TICKET",
209
+ title: `Exception ${match.id} is missing a required ticket reference`,
210
+ severity: "MEDIUM",
211
+ evidence: [`Finding: ${finding.id}`, `Exception: ${match.id}`, `Owner: ${match.owner}`],
212
+ requiredActions: [
213
+ `Exception ${match.id} for finding ${finding.id} is missing a required ticket reference. Set require_ticket: false in policy or add a ticket field.`
214
+ ]
215
+ });
216
+ continue;
217
+ }
218
+ // #9 fix C/F — DEFAULT POSTURE: an unsigned/unverified exceptions file may NOT suppress
219
+ // HIGH/CRITICAL findings (it may still suppress LOW/MEDIUM noise). This makes the
220
+ // dangerous bypass — silently hiding a HIGH/CRITICAL by editing an unsigned file —
221
+ // fail by default on the override, default (defaults/...), and project (.mcp/...) paths.
222
+ // Exemption: the named CI self-scan file `.github/security-exceptions-ci.json` (isCiFile)
223
+ // is the project suppressing its OWN intentional test fixtures; its use is already
224
+ // surfaced loudly (CI_EXCEPTIONS_IN_LOCAL_SCAN + EXCEPTIONS_UNSIGNED_SUPPRESSION), so it
225
+ // stays allowed to avoid breaking self-scan workflows. Sign it to remove the exemption.
226
+ // Break-glass: SECURITY_ALLOW_UNSIGNED_HIGH_SUPPRESSION=1 restores the legacy behavior
227
+ // on all paths (use only for scanning intentionally-vulnerable fixtures).
228
+ const allowUnsignedHigh = process.env["SECURITY_ALLOW_UNSIGNED_HIGH_SUPPRESSION"] === "1" ||
229
+ process.env["SECURITY_ALLOW_UNSIGNED_HIGH_SUPPRESSION"] === "true";
230
+ if (!integrity.verified && !allowUnsignedHigh && !integrity.isCiFile && HIGH_SEVERITIES.has(finding.severity)) {
231
+ active.push(finding);
232
+ exceptionFindings.push({
233
+ id: "EXCEPTION_UNSIGNED_HIGH_BLOCKED",
234
+ title: `Unsigned exceptions may not suppress ${finding.severity} finding ${finding.id}`,
235
+ severity: finding.severity,
236
+ evidence: [
237
+ `Finding: ${finding.id} (${finding.severity})`,
238
+ `Exception: ${match.id}`,
239
+ `Source: ${integrity.source}${integrity.isOverride ? " (SECURITY_GATE_EXCEPTIONS override)" : ""} — not integrity-verified`
240
+ ],
241
+ requiredActions: [
242
+ `Sign the exceptions file (set SECURITY_POLICY_HMAC_KEY ≥32 bytes + store its hmacSha256) to suppress ${finding.severity} findings, or resolve the finding. Break-glass for fixtures: SECURITY_ALLOW_UNSIGNED_HIGH_SUPPRESSION=1.`
243
+ ]
244
+ });
245
+ continue;
246
+ }
78
247
  suppressed.push({
79
248
  finding,
80
249
  exceptionId: match.id,
81
250
  expiresOn: match.expires_on
82
251
  });
83
252
  }
253
+ // #9 fix B — loud, unsuppressible visibility (default-on, ZERO breakage). When an
254
+ // UNSIGNED exceptions file suppressed anything, emit one finding (emitted here, AFTER
255
+ // the suppression loop, so it can never itself be suppressed) so the bypass is never
256
+ // silent. Severity is intentionally MEDIUM — visible in every report without auto-failing
257
+ // the gate (which would break legitimate unsigned self-scan workflows). To make unsigned
258
+ // suppression actually BLOCK, set SECURITY_REQUIRE_SIGNED_EXCEPTIONS=1 (fail-closed).
259
+ if (!integrity.verified && suppressed.length > 0) {
260
+ const highHidden = suppressed.filter((s) => HIGH_SEVERITIES.has(s.finding.severity));
261
+ exceptionFindings.push({
262
+ id: "EXCEPTIONS_UNSIGNED_SUPPRESSION",
263
+ title: `${suppressed.length} finding(s) suppressed by an unsigned exceptions file`,
264
+ severity: "MEDIUM",
265
+ evidence: [
266
+ `Source: ${integrity.source}${integrity.isOverride ? " (SECURITY_GATE_EXCEPTIONS override)" : ""}${integrity.isCiFile ? " (CI self-scan)" : ""}`,
267
+ "File is not integrity-protected (no verified hmacSha256) — its suppressions are not tamper-evident.",
268
+ `Total suppressed: ${suppressed.length}; HIGH/CRITICAL hidden: ${highHidden.length}`,
269
+ ...(highHidden.length > 0 ? [`Hidden HIGH/CRITICAL: ${highHidden.map((s) => `${s.finding.id} (${s.finding.severity})`).slice(0, 20).join(", ")}`] : [])
270
+ ],
271
+ requiredActions: [
272
+ "Sign the exceptions file (set SECURITY_POLICY_HMAC_KEY ≥32 bytes and store its hmacSha256) so suppressions are tamper-evident, or set SECURITY_REQUIRE_SIGNED_EXCEPTIONS=1 to reject unsigned exceptions entirely."
273
+ ]
274
+ });
275
+ }
84
276
  return {
85
277
  findings: active,
86
278
  suppressed,
87
279
  exceptionFindings,
88
- activeControlExceptionIds: Array.from(activeControlExceptionIds)
280
+ activeControlExceptionIds: Array.from(activeControlExceptionIds),
281
+ warnings
89
282
  };
90
283
  }
@@ -3,9 +3,22 @@ export function detectSurfaces(changedFiles) {
3
3
  return {
4
4
  web: has(/^(app|pages|components|src)\/.*\.(ts|tsx|js|jsx)$/) || has(/^next\.config\./),
5
5
  api: has(/^(app\/api|src\/api|api|server)\//),
6
- infra: has(/^(infra|terraform|iac|k8s|helm|cloudbuild|\.github\/workflows)\//),
6
+ infra: has(/^(infra|terraform|iac|k8s|helm|cloudbuild|argo(cd)?|flux|gitops|\.github\/workflows)\//) ||
7
+ has(/\.(tf|tfvars)(\.json)?$/) ||
8
+ has(/\.(bicep)$/i) ||
9
+ has(/(databricks|snowflake|cloudformation|cfn|template\.ya?ml)/i) ||
10
+ has(/(^|\/)docker-compose(\.[\w-]+)?\.ya?ml$/i),
7
11
  mobileIos: has(/^(ios|.*\.xcodeproj|.*\.xcworkspace|.*Info\.plist|Podfile)/),
8
12
  mobileAndroid: has(/^(android|.*\/AndroidManifest\.xml|.*\/build\.gradle(\.kts)?|gradle\.properties)/),
9
- ai: has(/^(ai|llm|prompt|rag|agents)\//) || has(/(openai|anthropic|vertexai|langchain|llamaindex)/)
13
+ ai: has(/^(ai|llm|prompt|rag|agents)\//) || has(/(openai|anthropic|vertexai|langchain|llamaindex)/),
14
+ // Agentic-instruction surface: files an AI coding agent ingests as authority
15
+ // the moment it opens the repo. Path-based and evaluated for ANY repo, since
16
+ // a poisoned instruction file is the attack vector even in non-AI projects.
17
+ agentic: has(/(^|\/)(SKILL|AGENTS|CLAUDE)\.md$/i) ||
18
+ has(/(^|\/)\.claude\//) ||
19
+ has(/(^|\/)\.cursor(rules)?(\/|$)/i) ||
20
+ has(/(^|\/)\.windsurfrules$/i) ||
21
+ has(/(^|\/)\.github\/copilot-instructions\.md$/i) ||
22
+ has(/(^|\/)\.mcp\.json$/)
10
23
  };
11
24
  }