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.
- package/README.md +356 -885
- package/defaults/cloud-controls/aws.json +10712 -0
- package/defaults/cloud-controls/azure.json +7201 -0
- package/defaults/cloud-controls/gcp.json +4061 -0
- package/defaults/control-catalog.json +24 -0
- package/dist/ci/pr-gate.js +22 -5
- package/dist/cli/index.js +73 -2
- package/dist/cli/install.js +4 -55
- package/dist/cli/onboarding.js +18 -10
- package/dist/gate/checks/agentic-instructions.js +515 -0
- package/dist/gate/checks/ai-governance.js +132 -0
- package/dist/gate/checks/ai.js +1 -1
- package/dist/gate/checks/cloud-controls.js +69 -0
- package/dist/gate/checks/crypto.js +1 -1
- package/dist/gate/checks/data-platform.js +954 -0
- package/dist/gate/checks/dependencies.js +14 -3
- package/dist/gate/checks/docker-deep.js +1236 -0
- package/dist/gate/checks/gitops.js +724 -0
- package/dist/gate/checks/iac.js +1230 -0
- package/dist/gate/checks/k8s.js +841 -1
- package/dist/gate/checks/secrets.js +49 -37
- package/dist/gate/cloud-controls/apply.js +115 -0
- package/dist/gate/cloud-controls/bicep.js +36 -0
- package/dist/gate/cloud-controls/cfn.js +125 -0
- package/dist/gate/cloud-controls/detect.js +104 -0
- package/dist/gate/cloud-controls/hcl.js +140 -0
- package/dist/gate/cloud-controls/types.js +87 -0
- package/dist/gate/exceptions.js +78 -7
- package/dist/gate/findings.js +15 -2
- package/dist/gate/policy.js +40 -3
- package/dist/gate/threat-intel.js +6 -0
- package/dist/mcp/audit-chain.js +9 -0
- package/dist/mcp/model-router.js +3 -3
- package/dist/mcp/orchestration.js +194 -41
- package/dist/mcp/server.js +124 -17
- package/dist/mcp/tool-audit.js +193 -0
- package/dist/repo/fs.js +14 -1
- package/dist/review/store.js +4 -2
- package/dist/tests/run.js +124 -1
- package/package.json +3 -3
- package/skills/advanced-dos-tester/SKILL.md +9 -0
- package/skills/agentic-instruction-auditor/SKILL.md +111 -0
- package/skills/agentic-loop-exploiter/SKILL.md +9 -0
- package/skills/ai-llm-redteam/SKILL.md +9 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +9 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +9 -0
- package/skills/android-penetration-tester/SKILL.md +9 -0
- package/skills/anti-replay-tester/SKILL.md +9 -0
- package/skills/appsec-code-auditor/SKILL.md +9 -0
- package/skills/artifact-integrity-analyst/SKILL.md +9 -0
- package/skills/attack-navigator/SKILL.md +9 -0
- package/skills/auth-session-hacker/SKILL.md +9 -0
- package/skills/aws-penetration-tester/SKILL.md +54 -0
- package/skills/azure-penetration-tester/SKILL.md +52 -0
- package/skills/binary-auth-validator/SKILL.md +9 -0
- package/skills/bot-detection-specialist/SKILL.md +9 -0
- package/skills/business-logic-attacker/SKILL.md +9 -0
- package/skills/capec-code-mapper/SKILL.md +9 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +9 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +9 -0
- package/skills/ciso-orchestrator/SKILL.md +11 -0
- package/skills/cloud-infra-specialist/SKILL.md +9 -0
- package/skills/compliance-gap-analyst/SKILL.md +9 -0
- package/skills/compliance-grc/SKILL.md +9 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +9 -0
- package/skills/container-hardening-auditor/SKILL.md +125 -0
- package/skills/credential-stuffing-specialist/SKILL.md +9 -0
- package/skills/crypto-pki-specialist/SKILL.md +9 -0
- package/skills/csa-ccm-mapper/SKILL.md +9 -0
- package/skills/csf2-governance-mapper/SKILL.md +9 -0
- package/skills/data-platform-auditor/SKILL.md +125 -0
- package/skills/deep-link-fuzzer/SKILL.md +9 -0
- package/skills/dependency-confusion-attacker/SKILL.md +9 -0
- package/skills/device-integrity-aggregator/SKILL.md +9 -0
- package/skills/dos-resilience-tester/SKILL.md +9 -0
- package/skills/dread-scorer/SKILL.md +9 -0
- package/skills/egress-policy-enforcer/SKILL.md +9 -0
- package/skills/evidence-collector/SKILL.md +9 -0
- package/skills/file-upload-attacker/SKILL.md +9 -0
- package/skills/gcp-penetration-tester/SKILL.md +51 -0
- package/skills/git-history-secret-scanner/SKILL.md +9 -0
- package/skills/gitops-delivery-auditor/SKILL.md +120 -0
- package/skills/iac-security-auditor/SKILL.md +125 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +9 -0
- package/skills/incident-responder/SKILL.md +9 -0
- package/skills/injection-specialist/SKILL.md +9 -0
- package/skills/ios-security-auditor/SKILL.md +9 -0
- package/skills/json-ambiguity-tester/SKILL.md +0 -0
- package/skills/k8s-container-escaper/SKILL.md +22 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +9 -0
- package/skills/kill-switch-engineer/SKILL.md +9 -0
- package/skills/linddun-privacy-analyst/SKILL.md +9 -0
- package/skills/logic-race-fuzzer/SKILL.md +9 -0
- package/skills/mobile-api-network-attacker/SKILL.md +9 -0
- package/skills/mobile-binary-hardener/SKILL.md +9 -0
- package/skills/mobile-security-specialist/SKILL.md +9 -0
- package/skills/mobile-webview-auditor/SKILL.md +9 -0
- package/skills/model-extraction-attacker/SKILL.md +9 -0
- package/skills/multipart-abuse-tester/SKILL.md +9 -0
- package/skills/oauth-pkce-specialist/SKILL.md +9 -0
- package/skills/parser-exhaustion-tester/SKILL.md +9 -0
- package/skills/pentest-infra/SKILL.md +9 -0
- package/skills/pentest-social/SKILL.md +9 -0
- package/skills/pentest-team/SKILL.md +9 -0
- package/skills/pentest-web-api/SKILL.md +9 -0
- package/skills/privacy-flow-analyst/SKILL.md +9 -0
- package/skills/prompt-injection-specialist/SKILL.md +9 -0
- package/skills/quantum-migration-planner/SKILL.md +9 -0
- package/skills/rag-poisoning-specialist/SKILL.md +9 -0
- package/skills/registry-mirror-enforcer/SKILL.md +9 -0
- package/skills/rotation-validation-agent/SKILL.md +9 -0
- package/skills/samm-assessor/SKILL.md +9 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +9 -0
- package/skills/senior-security-engineer/SKILL.md +11 -0
- package/skills/serialization-memory-attacker/SKILL.md +9 -0
- package/skills/session-timeout-tester/SKILL.md +9 -0
- package/skills/slsa-level3-enforcer/SKILL.md +9 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +9 -0
- package/skills/ssrf-detection-validator/SKILL.md +9 -0
- package/skills/step-up-auth-enforcer/SKILL.md +9 -0
- package/skills/stride-pasta-analyst/SKILL.md +9 -0
- package/skills/supply-chain-devsecops/SKILL.md +9 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +9 -0
- package/skills/threat-modeler/SKILL.md +9 -0
- package/skills/tls-certificate-auditor/SKILL.md +9 -0
- package/skills/token-reuse-detector/SKILL.md +9 -0
- package/skills/trike-risk-modeler/SKILL.md +9 -0
- package/skills/unicode-homograph-tester/SKILL.md +9 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +9 -0
- package/skills/webhook-security-tester/SKILL.md +9 -0
- 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
|
+
}
|
package/dist/gate/exceptions.js
CHANGED
|
@@ -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,
|
package/dist/gate/findings.js
CHANGED
|
@@ -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
|
}
|
package/dist/gate/policy.js
CHANGED
|
@@ -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
|
-
"
|
|
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
|
-
|
|
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),
|
package/dist/mcp/audit-chain.js
CHANGED
|
@@ -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,
|
package/dist/mcp/model-router.js
CHANGED
|
@@ -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 {
|