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.
Files changed (35) hide show
  1. package/defaults/checklists/ai.json +25 -0
  2. package/defaults/checklists/api.json +27 -0
  3. package/defaults/checklists/infra.json +27 -0
  4. package/defaults/checklists/mobile.json +25 -0
  5. package/defaults/checklists/payments.json +25 -0
  6. package/defaults/checklists/web.json +30 -0
  7. package/defaults/control-catalog.json +392 -0
  8. package/defaults/evidence-map.json +194 -0
  9. package/defaults/security-policy.json +41 -2
  10. package/dist/cli/index.js +13 -8
  11. package/dist/cli/install.js +11 -0
  12. package/dist/cli/onboarding.js +590 -0
  13. package/dist/gate/baseline.js +115 -0
  14. package/dist/gate/checks/ai-redteam.js +374 -0
  15. package/dist/gate/checks/api.js +93 -0
  16. package/dist/gate/checks/crypto.js +153 -0
  17. package/dist/gate/checks/database.js +144 -0
  18. package/dist/gate/checks/dependencies.js +126 -0
  19. package/dist/gate/checks/dlp.js +153 -0
  20. package/dist/gate/checks/graphql.js +122 -0
  21. package/dist/gate/checks/infra.js +126 -12
  22. package/dist/gate/checks/k8s.js +190 -0
  23. package/dist/gate/checks/playbook.js +160 -0
  24. package/dist/gate/checks/runtime.js +263 -0
  25. package/dist/gate/checks/sbom.js +199 -0
  26. package/dist/gate/checks/scanners.js +373 -7
  27. package/dist/gate/checks/secrets.js +85 -20
  28. package/dist/gate/policy.js +85 -19
  29. package/dist/gate/threat-intel.js +157 -0
  30. package/dist/mcp/server.js +500 -5
  31. package/dist/repo/search.js +13 -1
  32. package/dist/review/store.js +128 -0
  33. package/package.json +1 -1
  34. package/prompts/SECURITY_PROMPT.md +415 -1
  35. package/skills/senior-security-engineer/SKILL.md +35 -3
@@ -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
- const rawFindings = [
94
- // Required artifacts first: threat models/checklists.
95
- ...(await checkRequiredArtifacts({ policy, changedFiles })),
96
- // Baseline scans / checks
97
- ...(await checkSecrets({ changedFiles })),
98
- ...(await checkDependencies({ changedFiles })),
99
- ...scannerReadiness.findings,
100
- ...evidenceCoverage.findings,
101
- // Surface-specific checks (only run if that surface is impacted or exists)
102
- ...(surfaces.web ? await checkWebNextjs({ changedFiles }) : []),
103
- ...(surfaces.api ? await checkApi({ changedFiles }) : []),
104
- ...(surfaces.infra ? await checkInfra({ changedFiles }) : []),
105
- ...(surfaces.mobileIos ? await checkMobileIos({ changedFiles }) : []),
106
- ...(surfaces.mobileAndroid ? await checkMobileAndroid({ changedFiles }) : []),
107
- ...(surfaces.ai ? await checkAi({ changedFiles }) : [])
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 = findings.some((f) => f.severity === "HIGH" || f.severity === "CRITICAL")
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
+ }