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.
- package/README.md +341 -1018
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- 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/defaults/security-policy.json +2 -2
- 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/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/agentic-instructions.js +515 -0
- package/dist/gate/checks/ai-governance.js +132 -0
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +920 -216
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/cloud-controls.js +69 -0
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/data-platform.js +954 -0
- package/dist/gate/checks/dependencies.js +582 -15
- package/dist/gate/checks/docker-deep.js +1236 -0
- package/dist/gate/checks/gitops.js +724 -0
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/iac.js +1230 -0
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +827 -184
- package/dist/gate/checks/k8s.js +955 -2
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +256 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- 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/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +202 -9
- package/dist/gate/findings.js +15 -2
- package/dist/gate/policy.js +316 -130
- package/dist/gate/threat-intel.js +6 -0
- package/dist/mcp/audit-chain.js +131 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +161 -24
- package/dist/mcp/orchestration.js +377 -89
- package/dist/mcp/server.js +460 -69
- package/dist/mcp/tool-audit.js +193 -0
- package/dist/repo/fs.js +37 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +56 -3
- package/dist/tests/run.js +124 -1
- package/package.json +9 -9
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +118 -0
- package/skills/agentic-instruction-auditor/SKILL.md +111 -0
- package/skills/agentic-loop-exploiter/SKILL.md +377 -0
- package/skills/ai-llm-redteam/SKILL.md +113 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
- package/skills/android-penetration-tester/SKILL.md +464 -46
- package/skills/anti-replay-tester/SKILL.md +115 -0
- package/skills/appsec-code-auditor/SKILL.md +94 -0
- package/skills/artifact-integrity-analyst/SKILL.md +450 -0
- package/skills/attack-navigator/SKILL.md +476 -8
- package/skills/auth-session-hacker/SKILL.md +111 -0
- package/skills/aws-penetration-tester/SKILL.md +510 -0
- package/skills/azure-penetration-tester/SKILL.md +542 -3
- package/skills/binary-auth-validator/SKILL.md +120 -0
- package/skills/bot-detection-specialist/SKILL.md +118 -0
- package/skills/business-logic-attacker/SKILL.md +240 -0
- package/skills/capec-code-mapper/SKILL.md +93 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
- package/skills/ciso-orchestrator/SKILL.md +465 -43
- package/skills/cloud-infra-specialist/SKILL.md +127 -0
- package/skills/compliance-gap-analyst/SKILL.md +431 -0
- package/skills/compliance-grc/SKILL.md +94 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
- package/skills/container-hardening-auditor/SKILL.md +125 -0
- package/skills/credential-stuffing-specialist/SKILL.md +111 -0
- package/skills/crypto-pki-specialist/SKILL.md +96 -0
- package/skills/csa-ccm-mapper/SKILL.md +93 -0
- package/skills/csf2-governance-mapper/SKILL.md +93 -0
- package/skills/data-platform-auditor/SKILL.md +125 -0
- package/skills/deep-link-fuzzer/SKILL.md +118 -0
- package/skills/dependency-confusion-attacker/SKILL.md +424 -0
- package/skills/device-integrity-aggregator/SKILL.md +117 -0
- package/skills/dos-resilience-tester/SKILL.md +106 -0
- package/skills/dread-scorer/SKILL.md +93 -0
- package/skills/egress-policy-enforcer/SKILL.md +108 -0
- package/skills/evidence-collector/SKILL.md +107 -0
- package/skills/file-upload-attacker/SKILL.md +118 -0
- package/skills/gcp-penetration-tester/SKILL.md +510 -2
- package/skills/git-history-secret-scanner/SKILL.md +115 -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 +161 -0
- package/skills/incident-responder/SKILL.md +120 -0
- package/skills/injection-specialist/SKILL.md +111 -0
- package/skills/ios-security-auditor/SKILL.md +291 -0
- package/skills/json-ambiguity-tester/SKILL.md +145 -0
- package/skills/k8s-container-escaper/SKILL.md +406 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
- package/skills/kill-switch-engineer/SKILL.md +111 -0
- package/skills/linddun-privacy-analyst/SKILL.md +111 -0
- package/skills/logic-race-fuzzer/SKILL.md +452 -0
- package/skills/mobile-api-network-attacker/SKILL.md +430 -0
- package/skills/mobile-binary-hardener/SKILL.md +111 -0
- package/skills/mobile-security-specialist/SKILL.md +94 -0
- package/skills/mobile-webview-auditor/SKILL.md +105 -0
- package/skills/model-extraction-attacker/SKILL.md +228 -0
- package/skills/multipart-abuse-tester/SKILL.md +93 -0
- package/skills/oauth-pkce-specialist/SKILL.md +113 -0
- package/skills/parser-exhaustion-tester/SKILL.md +151 -0
- package/skills/pentest-infra/SKILL.md +107 -0
- package/skills/pentest-social/SKILL.md +210 -0
- package/skills/pentest-team/SKILL.md +96 -0
- package/skills/pentest-web-api/SKILL.md +107 -0
- package/skills/privacy-flow-analyst/SKILL.md +243 -0
- package/skills/prompt-injection-specialist/SKILL.md +403 -0
- package/skills/quantum-migration-planner/SKILL.md +105 -0
- package/skills/rag-poisoning-specialist/SKILL.md +367 -0
- package/skills/registry-mirror-enforcer/SKILL.md +93 -0
- package/skills/rotation-validation-agent/SKILL.md +121 -0
- package/skills/samm-assessor/SKILL.md +94 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
- package/skills/senior-security-engineer/SKILL.md +178 -0
- package/skills/serialization-memory-attacker/SKILL.md +341 -0
- package/skills/session-timeout-tester/SKILL.md +170 -0
- package/skills/slsa-level3-enforcer/SKILL.md +121 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
- package/skills/ssrf-detection-validator/SKILL.md +117 -0
- package/skills/step-up-auth-enforcer/SKILL.md +93 -0
- package/skills/stride-pasta-analyst/SKILL.md +429 -0
- package/skills/supply-chain-devsecops/SKILL.md +107 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
- package/skills/threat-modeler/SKILL.md +94 -0
- package/skills/tls-certificate-auditor/SKILL.md +582 -18
- package/skills/token-reuse-detector/SKILL.md +104 -0
- package/skills/trike-risk-modeler/SKILL.md +93 -0
- package/skills/unicode-homograph-tester/SKILL.md +93 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
- package/skills/webhook-security-tester/SKILL.md +111 -0
- package/skills/zero-trust-architect/SKILL.md +118 -0
package/dist/gate/exceptions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
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
|
}
|