security-mcp 1.3.1 → 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 (131) hide show
  1. package/README.md +356 -885
  2. package/defaults/cloud-controls/aws.json +10712 -0
  3. package/defaults/cloud-controls/azure.json +7201 -0
  4. package/defaults/cloud-controls/gcp.json +4061 -0
  5. package/defaults/control-catalog.json +24 -0
  6. package/dist/ci/pr-gate.js +22 -5
  7. package/dist/cli/index.js +73 -2
  8. package/dist/cli/install.js +4 -55
  9. package/dist/cli/onboarding.js +18 -10
  10. package/dist/gate/checks/agentic-instructions.js +515 -0
  11. package/dist/gate/checks/ai-governance.js +132 -0
  12. package/dist/gate/checks/ai.js +1 -1
  13. package/dist/gate/checks/cloud-controls.js +69 -0
  14. package/dist/gate/checks/crypto.js +1 -1
  15. package/dist/gate/checks/data-platform.js +954 -0
  16. package/dist/gate/checks/dependencies.js +14 -3
  17. package/dist/gate/checks/docker-deep.js +1236 -0
  18. package/dist/gate/checks/gitops.js +724 -0
  19. package/dist/gate/checks/iac.js +1230 -0
  20. package/dist/gate/checks/k8s.js +841 -1
  21. package/dist/gate/checks/secrets.js +49 -37
  22. package/dist/gate/cloud-controls/apply.js +115 -0
  23. package/dist/gate/cloud-controls/bicep.js +36 -0
  24. package/dist/gate/cloud-controls/cfn.js +125 -0
  25. package/dist/gate/cloud-controls/detect.js +104 -0
  26. package/dist/gate/cloud-controls/hcl.js +140 -0
  27. package/dist/gate/cloud-controls/types.js +87 -0
  28. package/dist/gate/exceptions.js +78 -7
  29. package/dist/gate/findings.js +15 -2
  30. package/dist/gate/policy.js +40 -3
  31. package/dist/gate/threat-intel.js +6 -0
  32. package/dist/mcp/audit-chain.js +9 -0
  33. package/dist/mcp/model-router.js +3 -3
  34. package/dist/mcp/orchestration.js +194 -41
  35. package/dist/mcp/server.js +124 -17
  36. package/dist/mcp/tool-audit.js +193 -0
  37. package/dist/repo/fs.js +14 -1
  38. package/dist/review/store.js +4 -2
  39. package/dist/tests/run.js +124 -1
  40. package/package.json +3 -3
  41. package/skills/advanced-dos-tester/SKILL.md +9 -0
  42. package/skills/agentic-instruction-auditor/SKILL.md +111 -0
  43. package/skills/agentic-loop-exploiter/SKILL.md +9 -0
  44. package/skills/ai-llm-redteam/SKILL.md +9 -0
  45. package/skills/ai-model-supply-chain-agent/SKILL.md +9 -0
  46. package/skills/algorithm-implementation-reviewer/SKILL.md +9 -0
  47. package/skills/android-penetration-tester/SKILL.md +9 -0
  48. package/skills/anti-replay-tester/SKILL.md +9 -0
  49. package/skills/appsec-code-auditor/SKILL.md +9 -0
  50. package/skills/artifact-integrity-analyst/SKILL.md +9 -0
  51. package/skills/attack-navigator/SKILL.md +9 -0
  52. package/skills/auth-session-hacker/SKILL.md +9 -0
  53. package/skills/aws-penetration-tester/SKILL.md +54 -0
  54. package/skills/azure-penetration-tester/SKILL.md +52 -0
  55. package/skills/binary-auth-validator/SKILL.md +9 -0
  56. package/skills/bot-detection-specialist/SKILL.md +9 -0
  57. package/skills/business-logic-attacker/SKILL.md +9 -0
  58. package/skills/capec-code-mapper/SKILL.md +9 -0
  59. package/skills/cert-pin-rotation-specialist/SKILL.md +9 -0
  60. package/skills/cicd-pipeline-hijacker/SKILL.md +9 -0
  61. package/skills/ciso-orchestrator/SKILL.md +11 -0
  62. package/skills/cloud-infra-specialist/SKILL.md +9 -0
  63. package/skills/compliance-gap-analyst/SKILL.md +9 -0
  64. package/skills/compliance-grc/SKILL.md +9 -0
  65. package/skills/compliance-lifecycle-tracker/SKILL.md +9 -0
  66. package/skills/container-hardening-auditor/SKILL.md +125 -0
  67. package/skills/credential-stuffing-specialist/SKILL.md +9 -0
  68. package/skills/crypto-pki-specialist/SKILL.md +9 -0
  69. package/skills/csa-ccm-mapper/SKILL.md +9 -0
  70. package/skills/csf2-governance-mapper/SKILL.md +9 -0
  71. package/skills/data-platform-auditor/SKILL.md +125 -0
  72. package/skills/deep-link-fuzzer/SKILL.md +9 -0
  73. package/skills/dependency-confusion-attacker/SKILL.md +9 -0
  74. package/skills/device-integrity-aggregator/SKILL.md +9 -0
  75. package/skills/dos-resilience-tester/SKILL.md +9 -0
  76. package/skills/dread-scorer/SKILL.md +9 -0
  77. package/skills/egress-policy-enforcer/SKILL.md +9 -0
  78. package/skills/evidence-collector/SKILL.md +9 -0
  79. package/skills/file-upload-attacker/SKILL.md +9 -0
  80. package/skills/gcp-penetration-tester/SKILL.md +51 -0
  81. package/skills/git-history-secret-scanner/SKILL.md +9 -0
  82. package/skills/gitops-delivery-auditor/SKILL.md +120 -0
  83. package/skills/iac-security-auditor/SKILL.md +125 -0
  84. package/skills/iam-privesc-graph-builder/SKILL.md +9 -0
  85. package/skills/incident-responder/SKILL.md +9 -0
  86. package/skills/injection-specialist/SKILL.md +9 -0
  87. package/skills/ios-security-auditor/SKILL.md +9 -0
  88. package/skills/json-ambiguity-tester/SKILL.md +0 -0
  89. package/skills/k8s-container-escaper/SKILL.md +22 -0
  90. package/skills/key-management-lifecycle-analyst/SKILL.md +9 -0
  91. package/skills/kill-switch-engineer/SKILL.md +9 -0
  92. package/skills/linddun-privacy-analyst/SKILL.md +9 -0
  93. package/skills/logic-race-fuzzer/SKILL.md +9 -0
  94. package/skills/mobile-api-network-attacker/SKILL.md +9 -0
  95. package/skills/mobile-binary-hardener/SKILL.md +9 -0
  96. package/skills/mobile-security-specialist/SKILL.md +9 -0
  97. package/skills/mobile-webview-auditor/SKILL.md +9 -0
  98. package/skills/model-extraction-attacker/SKILL.md +9 -0
  99. package/skills/multipart-abuse-tester/SKILL.md +9 -0
  100. package/skills/oauth-pkce-specialist/SKILL.md +9 -0
  101. package/skills/parser-exhaustion-tester/SKILL.md +9 -0
  102. package/skills/pentest-infra/SKILL.md +9 -0
  103. package/skills/pentest-social/SKILL.md +9 -0
  104. package/skills/pentest-team/SKILL.md +9 -0
  105. package/skills/pentest-web-api/SKILL.md +9 -0
  106. package/skills/privacy-flow-analyst/SKILL.md +9 -0
  107. package/skills/prompt-injection-specialist/SKILL.md +9 -0
  108. package/skills/quantum-migration-planner/SKILL.md +9 -0
  109. package/skills/rag-poisoning-specialist/SKILL.md +9 -0
  110. package/skills/registry-mirror-enforcer/SKILL.md +9 -0
  111. package/skills/rotation-validation-agent/SKILL.md +9 -0
  112. package/skills/samm-assessor/SKILL.md +9 -0
  113. package/skills/secrets-mask-bypass-tester/SKILL.md +9 -0
  114. package/skills/senior-security-engineer/SKILL.md +11 -0
  115. package/skills/serialization-memory-attacker/SKILL.md +9 -0
  116. package/skills/session-timeout-tester/SKILL.md +9 -0
  117. package/skills/slsa-level3-enforcer/SKILL.md +9 -0
  118. package/skills/slsa-provenance-enforcer/SKILL.md +9 -0
  119. package/skills/ssrf-detection-validator/SKILL.md +9 -0
  120. package/skills/step-up-auth-enforcer/SKILL.md +9 -0
  121. package/skills/stride-pasta-analyst/SKILL.md +9 -0
  122. package/skills/supply-chain-devsecops/SKILL.md +9 -0
  123. package/skills/threat-infrastructure-analyst/SKILL.md +9 -0
  124. package/skills/threat-modeler/SKILL.md +9 -0
  125. package/skills/tls-certificate-auditor/SKILL.md +9 -0
  126. package/skills/token-reuse-detector/SKILL.md +9 -0
  127. package/skills/trike-risk-modeler/SKILL.md +9 -0
  128. package/skills/unicode-homograph-tester/SKILL.md +9 -0
  129. package/skills/waf-rule-lifecycle-agent/SKILL.md +9 -0
  130. package/skills/webhook-security-tester/SKILL.md +9 -0
  131. package/skills/zero-trust-architect/SKILL.md +9 -0
@@ -0,0 +1,87 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { z } from "zod";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ // src/gate/cloud-controls -> repo root is three levels up (dist/gate/cloud-controls at runtime).
7
+ const PKG_ROOT = resolve(__dirname, "../../..");
8
+ export const CloudProviderSchema = z.enum(["aws", "gcp", "azure"]);
9
+ const DetectSchema = z.object({
10
+ // How the rule body is matched:
11
+ // "terraform" — HCL resource blocks (supports auto-fix).
12
+ // "cloudformation" — CloudFormation/SAM resources in JSON or YAML (detect-only).
13
+ // "bicep" — Bicep resource declarations (detect-only).
14
+ // Only "terraform" supports auto-remediation; the others are emit-and-fix-manually.
15
+ target: z.enum(["terraform", "cloudformation", "bicep"]),
16
+ // Resource type for the target: Terraform "aws_instance", CloudFormation
17
+ // "AWS::S3::Bucket", or Bicep "Microsoft.Storage/storageAccounts".
18
+ resourceType: z.string(),
19
+ // Regex; if it matches inside the resource block the resource is INSECURE.
20
+ forbid: z.string().optional(),
21
+ // Regex; if it is ABSENT from the resource block the resource is insecure-by-omission.
22
+ require: z.string().optional(),
23
+ // Cross-resource: the resource is insecure unless a companion resource of this
24
+ // Terraform type exists in the same file and references it by local name.
25
+ requireCompanionType: z.string().optional()
26
+ });
27
+ const RemediateSchema = z.object({
28
+ strategy: z.enum(["set-attr", "insert-block", "companion-resource", "manual"]),
29
+ // Dotted attribute path -> raw HCL value literal. Depth up to 2 (parent.child).
30
+ // e.g. { "metadata_options.http_tokens": "\"required\"" }.
31
+ ensure: z.record(z.string(), z.string()).optional(),
32
+ // Companion resource HCL template. "${name}" is substituted with the offending
33
+ // resource's local name. Used by strategy "companion-resource".
34
+ companion: z.string().optional(),
35
+ // Hardened snippet / guidance emitted when the fix cannot be applied automatically.
36
+ snippet: z.string().optional()
37
+ });
38
+ const RuleSchema = z.object({
39
+ ruleId: z.string(),
40
+ // The attack this misconfiguration enables — why it matters, not "it's non-compliant".
41
+ threat: z.string(),
42
+ // Framework labels for context only, e.g. ["AWS FSBP EC2.8", "CIS AWS Foundations Benchmark 5.6"].
43
+ frameworks: z.array(z.string()).default([]),
44
+ severity: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
45
+ title: z.string(),
46
+ detect: DetectSchema,
47
+ remediate: RemediateSchema,
48
+ requiredActions: z.array(z.string()).min(1)
49
+ });
50
+ const RegistrySchema = z.object({
51
+ version: z.string(),
52
+ rules: z.array(RuleSchema)
53
+ });
54
+ const PROVIDER_FILES = {
55
+ aws: "defaults/cloud-controls/aws.json",
56
+ gcp: "defaults/cloud-controls/gcp.json",
57
+ azure: "defaults/cloud-controls/azure.json"
58
+ };
59
+ async function loadProvider(cloud) {
60
+ const path = resolve(PKG_ROOT, PROVIDER_FILES[cloud]);
61
+ let raw;
62
+ try {
63
+ raw = await readFile(path, "utf-8");
64
+ }
65
+ catch {
66
+ return [];
67
+ }
68
+ const parsed = RegistrySchema.parse(JSON.parse(raw));
69
+ return parsed.rules.map((rule) => ({ ...rule, cloud }));
70
+ }
71
+ /** Load every cloud-control rule across all providers, tagged with its cloud. */
72
+ export async function loadCloudRules(providers) {
73
+ const list = providers ?? ["aws", "gcp", "azure"];
74
+ const groups = await Promise.all(list.map(loadProvider));
75
+ const seen = new Set();
76
+ const rules = [];
77
+ for (const group of groups) {
78
+ for (const rule of group) {
79
+ if (seen.has(rule.ruleId)) {
80
+ throw new Error(`Duplicate cloud-control ruleId: ${rule.ruleId}`);
81
+ }
82
+ seen.add(rule.ruleId);
83
+ rules.push(rule);
84
+ }
85
+ }
86
+ return rules;
87
+ }
@@ -67,7 +67,7 @@ async function readExceptionsJson() {
67
67
  throw new Error(`SECURITY_GATE_EXCEPTIONS path '${overridePath}' escapes the project directory`);
68
68
  }
69
69
  const raw = await readFile(resolved, "utf-8");
70
- return { raw, isCiFile: false, warnings };
70
+ return { raw, isCiFile: false, isOverride: true, source: overridePath, warnings };
71
71
  }
72
72
  // Project-level CI exceptions file (suppresses self-scan false positives)
73
73
  try {
@@ -98,24 +98,41 @@ async function readExceptionsJson() {
98
98
  ]
99
99
  });
100
100
  }
101
- return { raw, isCiFile: true, warnings };
101
+ return { raw, isCiFile: true, isOverride: false, source: ".github/security-exceptions-ci.json", warnings };
102
102
  }
103
103
  catch { /* not present — continue */ }
104
104
  try {
105
105
  const raw = await readFile(join(process.cwd(), ".mcp", "exceptions", "security-exceptions.json"), "utf-8");
106
- return { raw, isCiFile: false, warnings };
106
+ return { raw, isCiFile: false, isOverride: false, source: ".mcp/exceptions/security-exceptions.json", warnings };
107
107
  }
108
108
  catch {
109
109
  const raw = await readFile(join(PKG_ROOT, "defaults", "security-exceptions.json"), "utf-8");
110
- return { raw, isCiFile: false, warnings };
110
+ return { raw, isCiFile: false, isOverride: false, source: "defaults/security-exceptions.json", warnings };
111
111
  }
112
112
  }
113
113
  export async function loadSecurityExceptions() {
114
- const { raw, warnings } = await readExceptionsJson();
114
+ const { raw, warnings, isOverride, isCiFile, source } = await readExceptionsJson();
115
115
  const parsed = ExceptionFileSchema.parse(JSON.parse(raw));
116
116
  // Fix 3: HMAC verification of exceptions file
117
117
  const hmacKey = getExceptionsHmacKey();
118
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
+ }
119
136
  if (hmacKey) {
120
137
  if (!parsed.hmacSha256) {
121
138
  extraWarnings.push({
@@ -144,15 +161,17 @@ export async function loadSecurityExceptions() {
144
161
  }
145
162
  return {
146
163
  exceptions: parsed.exceptions,
147
- warnings: [...warnings, ...extraWarnings]
164
+ warnings: [...warnings, ...extraWarnings],
165
+ integrity: { verified: Boolean(hmacKey && parsed.hmacSha256), isOverride, isCiFile, source }
148
166
  };
149
167
  }
150
168
  export async function applySecurityExceptions(findings, opts) {
151
- const { exceptions, warnings } = await loadSecurityExceptions();
169
+ const { exceptions, warnings, integrity } = await loadSecurityExceptions();
152
170
  const active = [];
153
171
  const suppressed = [];
154
172
  const exceptionFindings = [];
155
173
  const activeControlExceptionIds = new Set();
174
+ const HIGH_SEVERITIES = new Set(["HIGH", "CRITICAL"]);
156
175
  for (const entry of exceptions) {
157
176
  const expiresAt = new Date(entry.expires_on);
158
177
  if (!Number.isNaN(expiresAt.getTime()) && expiresAt.getTime() >= Date.now()) {
@@ -196,12 +215,64 @@ export async function applySecurityExceptions(findings, opts) {
196
215
  });
197
216
  continue;
198
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
+ }
199
247
  suppressed.push({
200
248
  finding,
201
249
  exceptionId: match.id,
202
250
  expiresOn: match.expires_on
203
251
  });
204
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
+ }
205
276
  return {
206
277
  findings: active,
207
278
  suppressed,
@@ -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
  }
@@ -34,6 +34,13 @@ import { checkInjectionDeep } from "./checks/injection-deep.js";
34
34
  import { checkAuthDeep } from "./checks/auth-deep.js";
35
35
  import { checkSupplyChainDeep } from "./checks/supply-chain-deep.js";
36
36
  import { checkBusinessLogic } from "./checks/business-logic.js";
37
+ import { checkAgenticInstructions } from "./checks/agentic-instructions.js";
38
+ import { checkAiGovernance } from "./checks/ai-governance.js";
39
+ import { checkIac } from "./checks/iac.js";
40
+ import { checkGitOps } from "./checks/gitops.js";
41
+ import { checkDataPlatform } from "./checks/data-platform.js";
42
+ import { checkDockerDeep } from "./checks/docker-deep.js";
43
+ import { checkCloudControls } from "./checks/cloud-controls.js";
37
44
  const PolicySchema = z.object({
38
45
  name: z.string(),
39
46
  version: z.string(),
@@ -228,7 +235,15 @@ const CHECK_NAMES = [
228
235
  "auth-deep",
229
236
  "supply-chain-deep",
230
237
  "business-logic",
231
- "scanners-run"
238
+ "docker",
239
+ "scanners-run",
240
+ "agentic-instructions",
241
+ "ai-governance",
242
+ "iac",
243
+ "gitops",
244
+ "data-platform",
245
+ "docker-deep",
246
+ "cloud-controls"
232
247
  ];
233
248
  /** Run every applicable security check in parallel and collect findings. */
234
249
  async function runAllChecks(opts) {
@@ -263,7 +278,14 @@ async function runAllChecks(opts) {
263
278
  checkSupplyChainDeep({ changedFiles }),
264
279
  checkBusinessLogic({ changedFiles }),
265
280
  runDockerChecks({ changedFiles }),
266
- runScanners({ surfaces, changedFiles })
281
+ runScanners({ surfaces, changedFiles }),
282
+ surfaces.agentic ? checkAgenticInstructions({ changedFiles }) : Promise.resolve([]),
283
+ surfaces.ai ? checkAiGovernance({ changedFiles }) : Promise.resolve([]),
284
+ checkIac({ changedFiles }),
285
+ checkGitOps({ changedFiles }),
286
+ checkDataPlatform({ changedFiles }),
287
+ checkDockerDeep({ changedFiles }),
288
+ checkCloudControls({ changedFiles })
267
289
  ]);
268
290
  const findings = [];
269
291
  // Fix 5: crashed check modules generate HIGH findings instead of silent console.warn
@@ -408,7 +430,22 @@ export async function runPrGate(opts) {
408
430
  baselineDiff = br.diff;
409
431
  }
410
432
  // Fix 6: read severity_block from policy instead of hardcoding HIGH/CRITICAL
411
- const blockedSeverities = policy.severity_block ?? ["HIGH", "CRITICAL"];
433
+ let blockedSeverities = policy.severity_block ?? ["HIGH", "CRITICAL"];
434
+ // SECURITY (silent-bypass hardening): when the policy file is NOT integrity-verified
435
+ // (no SECURITY_POLICY_HMAC_KEY — the default), an attacker who can edit the unsigned
436
+ // .mcp/policies/security-policy.json could set "severity_block": [] and force every
437
+ // verdict to PASS with unlimited HIGH/CRITICAL findings. Refuse to let an unverified
438
+ // policy RELAX the gate below the safe HIGH/CRITICAL floor. To intentionally weaken it,
439
+ // operators must sign the policy (SECURITY_POLICY_HMAC_KEY + `security-mcp sign-policy`).
440
+ // When a key IS set, loadPolicy has already HMAC-verified the file (or thrown), so the
441
+ // operator's configured severity_block is trusted as-is.
442
+ const policyIntegrityVerified = !!process.env["SECURITY_POLICY_HMAC_KEY"];
443
+ if (!policyIntegrityVerified) {
444
+ for (const floor of ["HIGH", "CRITICAL"]) {
445
+ if (!blockedSeverities.includes(floor))
446
+ blockedSeverities = [...blockedSeverities, floor];
447
+ }
448
+ }
412
449
  const status = effectiveFindings.some((f) => blockedSeverities.includes(f.severity))
413
450
  ? "FAIL" : "PASS";
414
451
  const result = {
@@ -139,6 +139,12 @@ export async function checkActiveExploitation(cveIds, cacheDir) {
139
139
  if (cveIds.length === 0) {
140
140
  return { kevMatches: [], highEpss: [], failed: false };
141
141
  }
142
+ // CWE-200: the EPSS lookup places this repo's CVE IDs in a cleartext query to
143
+ // a third party (api.first.org). Operators of private repos can disable all
144
+ // threat-intel egress with SECURITY_OFFLINE so the unpatched-CVE set never leaves.
145
+ if (process.env["SECURITY_OFFLINE"] === "1" || process.env["SECURITY_OFFLINE"] === "true") {
146
+ return { kevMatches: [], highEpss: [], failed: false };
147
+ }
142
148
  try {
143
149
  const [kevSet, epssMap] = await Promise.all([
144
150
  fetchCisaKev(cacheDir),
@@ -62,6 +62,15 @@ function hmacSha256(key, data) {
62
62
  function hashFindings(findings) {
63
63
  return sha256(JSON.stringify(findings));
64
64
  }
65
+ /**
66
+ * Public helper: compute the canonical SHA-256 of a findings array exactly as
67
+ * `attestAgent` does. Used by orchestration.mergeAgentFindings to verify that an
68
+ * agent's findings file matches the hash that agent attested to — i.e. that the
69
+ * inter-agent payload was not tampered with between attestation and merge.
70
+ */
71
+ export function computeFindingsHash(findings) {
72
+ return hashFindings(findings);
73
+ }
65
74
  function buildChainPayload(record) {
66
75
  return [
67
76
  record.agentRunId,
@@ -231,7 +231,7 @@ export const TASK_TIER_MAP = {
231
231
  // Storage helpers
232
232
  // ---------------------------------------------------------------------------
233
233
  async function ensureMemoryDir() {
234
- await mkdir(MEMORY_DIR, { recursive: true });
234
+ await mkdir(MEMORY_DIR, { recursive: true, mode: 0o700 });
235
235
  }
236
236
  async function loadUsageStore() {
237
237
  try {
@@ -245,7 +245,7 @@ async function loadUsageStore() {
245
245
  async function saveUsageStore(store) {
246
246
  await ensureMemoryDir();
247
247
  store.updatedAt = new Date().toISOString();
248
- await writeFile(USAGE_FILE, JSON.stringify(store, null, 2) + "\n", "utf-8");
248
+ await writeFile(USAGE_FILE, JSON.stringify(store, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
249
249
  }
250
250
  async function loadHealthStore() {
251
251
  try {
@@ -259,7 +259,7 @@ async function loadHealthStore() {
259
259
  async function saveHealthStore(store) {
260
260
  await ensureMemoryDir();
261
261
  store.updatedAt = new Date().toISOString();
262
- await writeFile(HEALTH_FILE, JSON.stringify(store, null, 2) + "\n", "utf-8");
262
+ await writeFile(HEALTH_FILE, JSON.stringify(store, null, 2) + "\n", { encoding: "utf-8", mode: 0o600 });
263
263
  }
264
264
  async function loadMaxBudget() {
265
265
  try {