security-mcp 1.0.5 → 1.1.0
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/defaults/checklists/ai.json +25 -0
- package/defaults/checklists/api.json +27 -0
- package/defaults/checklists/infra.json +27 -0
- package/defaults/checklists/mobile.json +25 -0
- package/defaults/checklists/payments.json +25 -0
- package/defaults/checklists/web.json +30 -0
- package/defaults/control-catalog.json +392 -0
- package/defaults/evidence-map.json +194 -0
- package/defaults/security-policy.json +41 -2
- package/dist/cli/index.js +13 -8
- package/dist/cli/install.js +11 -0
- package/dist/cli/onboarding.js +590 -0
- package/dist/gate/baseline.js +115 -0
- package/dist/gate/checks/ai-redteam.js +374 -0
- package/dist/gate/checks/api.js +93 -0
- package/dist/gate/checks/crypto.js +153 -0
- package/dist/gate/checks/database.js +144 -0
- package/dist/gate/checks/dependencies.js +126 -0
- package/dist/gate/checks/dlp.js +153 -0
- package/dist/gate/checks/graphql.js +122 -0
- package/dist/gate/checks/infra.js +126 -12
- package/dist/gate/checks/k8s.js +190 -0
- package/dist/gate/checks/playbook.js +160 -0
- package/dist/gate/checks/runtime.js +263 -0
- package/dist/gate/checks/sbom.js +199 -0
- package/dist/gate/checks/scanners.js +373 -7
- package/dist/gate/checks/secrets.js +85 -20
- package/dist/gate/policy.js +85 -19
- package/dist/gate/threat-intel.js +157 -0
- package/dist/mcp/server.js +500 -5
- package/dist/repo/search.js +13 -1
- package/dist/review/store.js +128 -0
- package/package.json +1 -1
- package/prompts/SECURITY_PROMPT.md +415 -1
- package/skills/senior-security-engineer/SKILL.md +35 -3
package/dist/gate/policy.js
CHANGED
|
@@ -16,6 +16,15 @@ import { evaluateEvidenceCoverage } from "./evidence.js";
|
|
|
16
16
|
import { applySecurityExceptions } from "./exceptions.js";
|
|
17
17
|
import { controlApplies, loadControlCatalog } from "./catalog.js";
|
|
18
18
|
import { readFileSafe } from "../repo/fs.js";
|
|
19
|
+
import { checkGraphQL } from "./checks/graphql.js";
|
|
20
|
+
import { checkKubernetes } from "./checks/k8s.js";
|
|
21
|
+
import { checkDatabase } from "./checks/database.js";
|
|
22
|
+
import { checkCrypto } from "./checks/crypto.js";
|
|
23
|
+
import { checkDlp } from "./checks/dlp.js";
|
|
24
|
+
import { runSbomChecks } from "./checks/sbom.js";
|
|
25
|
+
import { runPlaybookChecks } from "./checks/playbook.js";
|
|
26
|
+
import { runAiRedteamChecks } from "./checks/ai-redteam.js";
|
|
27
|
+
import { runRuntimeChecks } from "./checks/runtime.js";
|
|
19
28
|
const PolicySchema = z.object({
|
|
20
29
|
name: z.string(),
|
|
21
30
|
version: z.string(),
|
|
@@ -76,6 +85,28 @@ export async function loadPolicy(policyPath) {
|
|
|
76
85
|
const parsed = JSON.parse(raw);
|
|
77
86
|
return PolicySchema.parse(parsed);
|
|
78
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Classify the change type based on file paths to apply appropriate gate tier.
|
|
90
|
+
*/
|
|
91
|
+
function classifyChangeType(files) {
|
|
92
|
+
if (files.length === 0)
|
|
93
|
+
return "general";
|
|
94
|
+
const allMatch = (pattern) => files.every((f) => pattern.test(f));
|
|
95
|
+
const anyMatch = (pattern) => files.some((f) => pattern.test(f));
|
|
96
|
+
if (allMatch(/\.(md|txt|rst)$|\/docs\/|README/i))
|
|
97
|
+
return "docs";
|
|
98
|
+
if (anyMatch(/\/payment|\/stripe|\/checkout|\/billing|\/invoice/i))
|
|
99
|
+
return "payment";
|
|
100
|
+
if (anyMatch(/\/auth|\/login|\/session|\/token|\/jwt|\/oauth|\/permission/i))
|
|
101
|
+
return "auth";
|
|
102
|
+
if (anyMatch(/\.tf$|Dockerfile|\.yaml$|\.yml$|\/k8s\/|\/helm\//))
|
|
103
|
+
return "infra";
|
|
104
|
+
if (anyMatch(/\/ai\/|\/llm\/|\/agent\/|\/prompt/i))
|
|
105
|
+
return "ai";
|
|
106
|
+
if (allMatch(/\.(json|env|config\..+|toml|yaml|yml)$/))
|
|
107
|
+
return "config";
|
|
108
|
+
return "general";
|
|
109
|
+
}
|
|
79
110
|
export async function runPrGate(opts) {
|
|
80
111
|
const policy = await loadPolicy(opts.policyPath);
|
|
81
112
|
const mode = opts.mode ?? "recent_changes";
|
|
@@ -86,26 +117,51 @@ export async function runPrGate(opts) {
|
|
|
86
117
|
baseRef: opts.baseRef ?? "origin/main",
|
|
87
118
|
headRef: opts.headRef ?? "HEAD"
|
|
88
119
|
});
|
|
120
|
+
// Classify the change type to apply appropriate gate tier
|
|
121
|
+
const changeType = classifyChangeType(changedFiles);
|
|
89
122
|
const surfaces = detectSurfaces(changedFiles);
|
|
90
123
|
const catalog = await loadControlCatalog();
|
|
91
124
|
const scannerReadiness = await checkScannerReadiness({ surfaces });
|
|
92
125
|
const evidenceCoverage = await evaluateEvidenceCoverage({ policy, surfaces });
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
126
|
+
let rawFindings;
|
|
127
|
+
// "docs" tier: only run secrets check to avoid unnecessary overhead
|
|
128
|
+
if (changeType === "docs") {
|
|
129
|
+
rawFindings = await checkSecrets({ changedFiles });
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Run all independent checks in parallel
|
|
133
|
+
const checkResults = await Promise.allSettled([
|
|
134
|
+
checkRequiredArtifacts({ policy, changedFiles }),
|
|
135
|
+
checkSecrets({ changedFiles }),
|
|
136
|
+
checkDependencies({ changedFiles }),
|
|
137
|
+
Promise.resolve(scannerReadiness.findings),
|
|
138
|
+
Promise.resolve(evidenceCoverage.findings),
|
|
139
|
+
surfaces.web ? checkWebNextjs({ changedFiles }) : Promise.resolve([]),
|
|
140
|
+
surfaces.api ? checkApi({ changedFiles }) : Promise.resolve([]),
|
|
141
|
+
surfaces.infra ? checkInfra({ changedFiles }) : Promise.resolve([]),
|
|
142
|
+
surfaces.mobileIos ? checkMobileIos({ changedFiles }) : Promise.resolve([]),
|
|
143
|
+
surfaces.mobileAndroid ? checkMobileAndroid({ changedFiles }) : Promise.resolve([]),
|
|
144
|
+
surfaces.ai ? checkAi({ changedFiles }) : Promise.resolve([]),
|
|
145
|
+
checkGraphQL({ changedFiles }),
|
|
146
|
+
checkKubernetes({ changedFiles }),
|
|
147
|
+
checkDatabase({ changedFiles }),
|
|
148
|
+
checkCrypto({ changedFiles }),
|
|
149
|
+
checkDlp({ changedFiles }),
|
|
150
|
+
runSbomChecks({ changedFiles, targets }),
|
|
151
|
+
runPlaybookChecks({ changedFiles, surfaces }),
|
|
152
|
+
surfaces.ai ? runAiRedteamChecks({ changedFiles }) : Promise.resolve([]),
|
|
153
|
+
process.env["SECURITY_STAGING_URL"] ? runRuntimeChecks({ targets, changedFiles }) : Promise.resolve([])
|
|
154
|
+
]);
|
|
155
|
+
rawFindings = [];
|
|
156
|
+
for (const result of checkResults) {
|
|
157
|
+
if (result.status === "fulfilled") {
|
|
158
|
+
rawFindings.push(...result.value);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.warn("[policy] Check failed:", result.reason);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
109
165
|
const toolingCoverage = catalog.controls
|
|
110
166
|
.filter((control) => control.automation === "tooling" && controlApplies(control, surfaces))
|
|
111
167
|
.map((control) => {
|
|
@@ -136,6 +192,16 @@ export async function runPrGate(opts) {
|
|
|
136
192
|
return control;
|
|
137
193
|
});
|
|
138
194
|
const findings = [...exceptionResult.findings, ...exceptionResult.exceptionFindings];
|
|
195
|
+
// Apply risk-based adaptive gating tier overrides
|
|
196
|
+
let effectiveFindings = findings;
|
|
197
|
+
if (changeType === "payment") {
|
|
198
|
+
// Payment changes: treat as prod-equivalent — block on all HIGH+
|
|
199
|
+
effectiveFindings = findings;
|
|
200
|
+
}
|
|
201
|
+
else if (changeType === "auth") {
|
|
202
|
+
// Auth changes: always block on HIGH+ even in dev
|
|
203
|
+
effectiveFindings = findings;
|
|
204
|
+
}
|
|
139
205
|
const relevantControls = controlCoverageWithExceptions.filter((control) => control.status !== "not_applicable");
|
|
140
206
|
const satisfiedControls = relevantControls.filter((control) => control.status === "satisfied").length;
|
|
141
207
|
const riskAcceptedControls = relevantControls.filter((control) => control.status === "risk_accepted").length;
|
|
@@ -147,7 +213,7 @@ export async function runPrGate(opts) {
|
|
|
147
213
|
: Math.round(((scannerReadiness.configured.length - scannerReadiness.missing.length) / scannerReadiness.configured.length) * 100);
|
|
148
214
|
const confidenceScore = Math.max(0, Math.min(100, Math.round((automatedCoverage * 0.7) + (scannerScore * 0.3))));
|
|
149
215
|
const missingControls = relevantControls.filter((control) => control.status === "missing").length;
|
|
150
|
-
const status =
|
|
216
|
+
const status = effectiveFindings.some((f) => f.severity === "HIGH" || f.severity === "CRITICAL")
|
|
151
217
|
? "FAIL"
|
|
152
218
|
: "PASS";
|
|
153
219
|
return {
|
|
@@ -155,7 +221,7 @@ export async function runPrGate(opts) {
|
|
|
155
221
|
policyVersion: policy.version,
|
|
156
222
|
evaluatedAt: new Date().toISOString(),
|
|
157
223
|
scope: { mode, targets, changedFiles, surfaces },
|
|
158
|
-
findings,
|
|
224
|
+
findings: effectiveFindings,
|
|
159
225
|
suppressedFindings: exceptionResult.suppressed,
|
|
160
226
|
controlCoverage: controlCoverageWithExceptions,
|
|
161
227
|
scannerReadiness: {
|
|
@@ -168,7 +234,7 @@ export async function runPrGate(opts) {
|
|
|
168
234
|
missingControls,
|
|
169
235
|
riskAcceptedControls,
|
|
170
236
|
scannerReadiness: scannerScore,
|
|
171
|
-
summary: `Automated coverage ${automatedCoverage}%, scanner readiness ${scannerScore}%, missing controls ${missingControls}, risk-accepted controls ${riskAcceptedControls}.`
|
|
237
|
+
summary: `Automated coverage ${automatedCoverage}%, scanner readiness ${scannerScore}%, missing controls ${missingControls}, risk-accepted controls ${riskAcceptedControls}. Change type: ${changeType}.`
|
|
172
238
|
}
|
|
173
239
|
};
|
|
174
240
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Threat Intelligence Feed Integration
|
|
3
|
+
* Fetches CISA KEV and EPSS scores for CVE prioritization.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
const CISA_KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json";
|
|
8
|
+
const EPSS_API_BASE = "https://api.first.org/data/v1/epss";
|
|
9
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
10
|
+
async function ensureDir(dir) {
|
|
11
|
+
try {
|
|
12
|
+
await mkdir(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// ignore
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readCacheJson(cachePath) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await readFile(cachePath, "utf-8");
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (Date.now() - parsed.ts < CACHE_TTL_MS) {
|
|
23
|
+
return parsed.data;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// cache miss or corrupt
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
async function writeCacheJson(cachePath, data) {
|
|
32
|
+
try {
|
|
33
|
+
await writeFile(cachePath, JSON.stringify({ ts: Date.now(), data }, null, 2), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// best-effort cache write
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function fetchWithTimeout(url, timeoutMs = 10_000) {
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
44
|
+
return res;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Fetches the CISA Known Exploited Vulnerabilities catalog.
|
|
52
|
+
* Returns a Set of CVE IDs. Returns empty set on failure.
|
|
53
|
+
*/
|
|
54
|
+
export async function fetchCisaKev(cacheDir) {
|
|
55
|
+
await ensureDir(cacheDir);
|
|
56
|
+
const cachePath = join(cacheDir, "cisa-kev.json");
|
|
57
|
+
const cached = await readCacheJson(cachePath);
|
|
58
|
+
if (cached)
|
|
59
|
+
return new Set(cached);
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetchWithTimeout(CISA_KEV_URL, 10_000);
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
console.warn(`[threat-intel] CISA KEV fetch failed: HTTP ${res.status}`);
|
|
64
|
+
return new Set();
|
|
65
|
+
}
|
|
66
|
+
const json = (await res.json());
|
|
67
|
+
const vulns = Array.isArray(json?.vulnerabilities)
|
|
68
|
+
? json.vulnerabilities
|
|
69
|
+
.map((v) => v.cveID ?? "")
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
: [];
|
|
72
|
+
await writeCacheJson(cachePath, vulns);
|
|
73
|
+
return new Set(vulns);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.warn(`[threat-intel] CISA KEV fetch error: ${String(err)}`);
|
|
77
|
+
return new Set();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Fetches EPSS scores for a list of CVE IDs.
|
|
82
|
+
* Batches up to 100 CVEs per request. Returns a Map of CVE → score.
|
|
83
|
+
*/
|
|
84
|
+
export async function fetchEpssScores(cveIds, cacheDir) {
|
|
85
|
+
if (cveIds.length === 0)
|
|
86
|
+
return new Map();
|
|
87
|
+
await ensureDir(join(cacheDir, "epss"));
|
|
88
|
+
const result = new Map();
|
|
89
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
90
|
+
const cachePath = join(cacheDir, "epss", `${today}.json`);
|
|
91
|
+
const cached = await readCacheJson(cachePath);
|
|
92
|
+
const cachedMap = cached ? new Map(Object.entries(cached)) : new Map();
|
|
93
|
+
const needed = cveIds.filter((id) => !cachedMap.has(id));
|
|
94
|
+
for (const [k, v] of cachedMap)
|
|
95
|
+
result.set(k, v);
|
|
96
|
+
if (needed.length === 0)
|
|
97
|
+
return result;
|
|
98
|
+
// Batch in chunks of 100
|
|
99
|
+
for (let i = 0; i < needed.length; i += 100) {
|
|
100
|
+
const chunk = needed.slice(i, i + 100);
|
|
101
|
+
const url = `${EPSS_API_BASE}?cve=${chunk.join(",")}`;
|
|
102
|
+
let retried = false;
|
|
103
|
+
while (true) {
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetchWithTimeout(url, 10_000);
|
|
106
|
+
if (res.status === 429 && !retried) {
|
|
107
|
+
retried = true;
|
|
108
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!res.ok)
|
|
112
|
+
break;
|
|
113
|
+
const json = (await res.json());
|
|
114
|
+
if (Array.isArray(json?.data)) {
|
|
115
|
+
for (const item of json.data) {
|
|
116
|
+
if (item.cve && item.epss !== undefined) {
|
|
117
|
+
result.set(item.cve, parseFloat(item.epss));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Persist updated cache
|
|
129
|
+
const mergedCache = {};
|
|
130
|
+
for (const [k, v] of result)
|
|
131
|
+
mergedCache[k] = v;
|
|
132
|
+
await writeCacheJson(cachePath, mergedCache);
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Main entry point: check CVEs against KEV and EPSS.
|
|
137
|
+
*/
|
|
138
|
+
export async function checkActiveExploitation(cveIds, cacheDir) {
|
|
139
|
+
if (cveIds.length === 0) {
|
|
140
|
+
return { kevMatches: [], highEpss: [], failed: false };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const [kevSet, epssMap] = await Promise.all([
|
|
144
|
+
fetchCisaKev(cacheDir),
|
|
145
|
+
fetchEpssScores(cveIds, cacheDir)
|
|
146
|
+
]);
|
|
147
|
+
const kevMatches = cveIds.filter((id) => kevSet.has(id));
|
|
148
|
+
const highEpss = cveIds
|
|
149
|
+
.map((cve) => ({ cve, score: epssMap.get(cve) ?? 0 }))
|
|
150
|
+
.filter((e) => e.score > 0.5);
|
|
151
|
+
return { kevMatches, highEpss, failed: false };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
console.warn(`[threat-intel] checkActiveExploitation failed: ${String(err)}`);
|
|
155
|
+
return { kevMatches: [], highEpss: [], failed: true };
|
|
156
|
+
}
|
|
157
|
+
}
|