security-mcp 1.1.1 → 1.1.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 (70) hide show
  1. package/README.md +15 -12
  2. package/dist/ci/pr-gate.js +18 -1
  3. package/dist/cli/onboarding.js +78 -7
  4. package/dist/gate/checks/api.js +93 -0
  5. package/dist/gate/checks/ci-pipeline.js +135 -0
  6. package/dist/gate/checks/crypto.js +91 -22
  7. package/dist/gate/checks/database.js +5 -1
  8. package/dist/gate/checks/dependencies.js +297 -2
  9. package/dist/gate/checks/dlp.js +6 -1
  10. package/dist/gate/checks/graphql.js +6 -1
  11. package/dist/gate/checks/k8s.js +229 -181
  12. package/dist/gate/checks/nuclei.js +133 -0
  13. package/dist/gate/checks/runtime.js +32 -18
  14. package/dist/gate/checks/scanners.js +2 -1
  15. package/dist/gate/diff.js +2 -0
  16. package/dist/gate/policy.js +47 -4
  17. package/dist/gate/result.js +7 -1
  18. package/dist/mcp/audit-chain.js +253 -0
  19. package/dist/mcp/learning.js +228 -0
  20. package/dist/mcp/model-router.js +544 -0
  21. package/dist/mcp/orchestration.js +22 -4
  22. package/dist/mcp/server.js +92 -1
  23. package/dist/review/store.js +10 -0
  24. package/package.json +1 -1
  25. package/skills/_TEMPLATE/SKILL.md +99 -0
  26. package/skills/advanced-dos-tester/SKILL.md +225 -0
  27. package/skills/ai-model-supply-chain-agent/SKILL.md +198 -0
  28. package/skills/anti-replay-tester/SKILL.md +195 -0
  29. package/skills/binary-auth-validator/SKILL.md +184 -0
  30. package/skills/bot-detection-specialist/SKILL.md +221 -0
  31. package/skills/capec-code-mapper/SKILL.md +163 -0
  32. package/skills/cert-pin-rotation-specialist/SKILL.md +200 -0
  33. package/skills/compliance-lifecycle-tracker/SKILL.md +169 -0
  34. package/skills/credential-stuffing-specialist/SKILL.md +192 -0
  35. package/skills/csa-ccm-mapper/SKILL.md +178 -0
  36. package/skills/csf2-governance-mapper/SKILL.md +159 -0
  37. package/skills/deep-link-fuzzer/SKILL.md +195 -0
  38. package/skills/device-integrity-aggregator/SKILL.md +221 -0
  39. package/skills/dos-resilience-tester/SKILL.md +184 -0
  40. package/skills/dread-scorer/SKILL.md +157 -0
  41. package/skills/egress-policy-enforcer/SKILL.md +208 -0
  42. package/skills/file-upload-attacker/SKILL.md +208 -0
  43. package/skills/git-history-secret-scanner/SKILL.md +182 -0
  44. package/skills/iam-privesc-graph-builder/SKILL.md +216 -0
  45. package/skills/incident-responder/SKILL.md +192 -0
  46. package/skills/json-ambiguity-tester/SKILL.md +175 -0
  47. package/skills/kill-switch-engineer/SKILL.md +205 -0
  48. package/skills/linddun-privacy-analyst/SKILL.md +196 -0
  49. package/skills/mobile-binary-hardener/SKILL.md +199 -0
  50. package/skills/mobile-webview-auditor/SKILL.md +200 -0
  51. package/skills/multipart-abuse-tester/SKILL.md +146 -0
  52. package/skills/oauth-pkce-specialist/SKILL.md +191 -0
  53. package/skills/parser-exhaustion-tester/SKILL.md +177 -0
  54. package/skills/quantum-migration-planner/SKILL.md +184 -0
  55. package/skills/registry-mirror-enforcer/SKILL.md +142 -0
  56. package/skills/rotation-validation-agent/SKILL.md +188 -0
  57. package/skills/samm-assessor/SKILL.md +168 -0
  58. package/skills/secrets-mask-bypass-tester/SKILL.md +167 -0
  59. package/skills/session-timeout-tester/SKILL.md +197 -0
  60. package/skills/slsa-level3-enforcer/SKILL.md +185 -0
  61. package/skills/slsa-provenance-enforcer/SKILL.md +181 -0
  62. package/skills/ssrf-detection-validator/SKILL.md +229 -0
  63. package/skills/step-up-auth-enforcer/SKILL.md +176 -0
  64. package/skills/threat-infrastructure-analyst/SKILL.md +167 -0
  65. package/skills/token-reuse-detector/SKILL.md +203 -0
  66. package/skills/trike-risk-modeler/SKILL.md +139 -0
  67. package/skills/unicode-homograph-tester/SKILL.md +179 -0
  68. package/skills/waf-rule-lifecycle-agent/SKILL.md +213 -0
  69. package/skills/webhook-security-tester/SKILL.md +184 -0
  70. package/skills/zero-trust-architect/SKILL.md +211 -0
package/README.md CHANGED
@@ -19,8 +19,8 @@ Works with **Claude Code, GitHub Copilot, Cursor, Codex, Replit**, and any MCP-c
19
19
 
20
20
  - [What Problem Does This Solve?](#what-problem-does-this-solve)
21
21
  - [Who Is This For?](#who-is-this-for)
22
- - [Two Modes - Pick Your Depth](#two-modes--pick-your-depth)
23
- - [Quick Start - Install in 60 Seconds](#quick-start--install-in-60-seconds)
22
+ - [Two Modes - Pick Your Depth](#two-modes---pick-your-depth)
23
+ - [Quick Start - Install in 60 Seconds](#quick-start---install-in-60-seconds)
24
24
  - [Step-by-Step Installation Guide](#step-by-step-installation-guide)
25
25
  - [Claude Code](#step-by-step-claude-code)
26
26
  - [Cursor](#step-by-step-cursor)
@@ -77,19 +77,19 @@ A single elite security engineer agent that reviews your code, finds vulnerabili
77
77
 
78
78
  ### `/ciso-orchestrator` - A Full Security Program in One Command
79
79
 
80
- 40 specialist agents running in parallel across 3 phases: threat modeling, deep code and infrastructure attack simulation, then compliance synthesis. Every domain gets its own specialist - a dedicated injection attacker, a JWT/OAuth hacker, a cloud privilege escalation analyst, a prompt injection specialist, a TLS auditor, a pentest team that reads the threat model as its attack brief, and a compliance analyst that maps every finding to PCI DSS 4.0, SOC 2, ISO 27001, NIST 800-53, HIPAA, and GDPR. Agents learn from each run and improve over time. Optionally fetches live CVE, CISA KEV, and ATT&CK data. Produces a merged findings report with full compliance mapping and a signed attestation.
80
+ 39 specialist agents across 3 phases. Phase 1: 7 lead agents run in parallel, each commanding its own team of sub-agents — threat modeling, deep code analysis, cloud infrastructure, supply chain, AI/LLM red team, mobile, and cryptography. Phase 2: adversarial penetration testing and compliance synthesis run in parallel after Phase 1 completes. Phase 3: findings are merged, deduplicated, and attested. Every domain has a dedicated specialist an injection attacker, a JWT/OAuth hacker, a cloud privilege escalation analyst, a prompt injection specialist, a TLS auditor, a pentest team that reads the threat model as its attack brief, and a compliance analyst mapping every finding to PCI DSS 4.0, SOC 2, ISO 27001, NIST 800-53, HIPAA, and GDPR. Agents learn from each run and improve over time. 86 specialist skills registered in the registry — loaded on demand based on detected stack. Optionally fetches live CVE, CISA KEV, and ATT&CK data. Produces a merged findings report with full compliance mapping and a signed attestation.
81
81
 
82
- **Use this before major releases, compliance audits, or security reviews. -> [See the full 40-agent architecture](#ciso-orchestrator-flow-40-agents)**
82
+ **Use this before major releases, compliance audits, or security reviews. -> [See the full 39-agent architecture](#ciso-orchestrator-flow-39-agents)**
83
83
 
84
84
  ---
85
85
 
86
86
  | | `/senior-security-engineer` | `/ciso-orchestrator` |
87
87
  | --- | --- | --- |
88
- | **What it is** | Single expert agent | 40-agent parallel security program |
88
+ | **What it is** | Single expert agent | 39-agent multi-phase security program |
89
89
  | **Best for** | Daily development, PR reviews, targeted hardening | Pre-launch audits, compliance prep, incident response |
90
90
  | **Speed** | Seconds to minutes | Minutes to hours |
91
91
  | **Scope** | You choose: recent changes, full codebase, or specific files | Always full - every surface, every framework |
92
- | **Agents** | 1 | 40 (9 leads + 30 specialists) |
92
+ | **Agents** | 1 | 39 (9 leads + 30 sub-agents) |
93
93
  | **Output** | Inline code fixes + SHA-256 attestation | Full domain reports + merged findings + attestation |
94
94
  | **API cost** | Low | High |
95
95
  | **Internet** | Not required | Optional (enriches findings with live CVEs, CISA KEV, MITRE ATT&CK) |
@@ -112,7 +112,7 @@ Restart your editor. Then in Claude Code:
112
112
 
113
113
  That's it. The engineer will ask how you want to scope the review, then find and fix security issues in your code.
114
114
 
115
- For a full 40-agent deep audit:
115
+ For a full 39-agent deep audit:
116
116
 
117
117
  ```text
118
118
  /ciso-orchestrator
@@ -398,7 +398,7 @@ The orchestrator will ask:
398
398
  - **Yes** - agents enrich findings with live threat intelligence. More accurate, more current.
399
399
  - **No** - agents use cached intel. Still comprehensive, no external calls made.
400
400
 
401
- **Step 3 - Wait for Phase 1 (7 lead agents + 30 sub-agents, all parallel).**
401
+ **Step 3 - Wait for Phase 1 (7 lead agents running in parallel, each commanding their domain-specific sub-agents 25 sub-agents total across Phase 1).**
402
402
 
403
403
  Each agent writes findings to `.mcp/agent-runs/{agentRunId}/`.
404
404
 
@@ -468,7 +468,7 @@ The gate runs **18 checks in parallel** against your diff:
468
468
  | Category | What It Catches |
469
469
  | --- | --- |
470
470
  | **Secrets** | Hardcoded API keys, tokens, passwords, private keys (via Gitleaks patterns) |
471
- | **Dependencies** | CRITICAL/HIGH CVEs in npm/pip/go/maven packages; CISA Known Exploited Vulnerabilities |
471
+ | **Dependencies** | CRITICAL/HIGH CVEs in npm/pip/go/maven packages; CISA KEV cross-check and EPSS >50% auto-escalation via live threat-intel (24h cached) |
472
472
  | **Cryptography** | MD5, SHA-1, DES, RC4, ECB mode, `Math.random()` for tokens, short JWT secrets |
473
473
  | **Authentication** | Missing rate limiting, no account lockout, JWT `alg:none`, weak session config |
474
474
  | **Injection** | SQL, NoSQL, command injection, path traversal, SSRF, prototype pollution |
@@ -485,6 +485,7 @@ The gate runs **18 checks in parallel** against your diff:
485
485
  | **Runtime** | HTTP security headers and TLS config on live staging URL (if configured) |
486
486
  | **AI red-team** | Static + optional dynamic probes against AI endpoints |
487
487
  | **Exceptions** | Validates any active security exceptions are non-expired and properly approved |
488
+ | **Baseline regression** | Detects when previously-satisfied controls go missing (BASELINE_REGRESSION HIGH finding injected on regression) |
488
489
 
489
490
  ### Customize the Gate Policy
490
491
 
@@ -620,7 +621,7 @@ app.use(helmet({
620
621
  │ Your Editor (Claude Code) │
621
622
  │ │
622
623
  │ /senior-security-engineer /ciso-orchestrator │
623
- │ (single expert agent) (40-agent security program) │
624
+ │ (single expert agent) (39-agent security program) │
624
625
  │ │ │ │
625
626
  └──────────┼────────────────────────────────┼───────────────────┘
626
627
  │ │
@@ -701,7 +702,7 @@ User: /senior-security-engineer
701
702
  └── SHA-256 integrity hash
702
703
  ```
703
704
 
704
- ### `/ciso-orchestrator` Flow (40 Agents)
705
+ ### `/ciso-orchestrator` Flow (39 Agents)
705
706
 
706
707
  ```text
707
708
  User: /ciso-orchestrator
@@ -715,7 +716,7 @@ User: /ciso-orchestrator
715
716
  │ -> stackContext: { languages, frameworks, cloudProvider, hasAI, hasMobile, ... }
716
717
  ├── security.start_review() -> runId
717
718
  ├── orchestration.create_agent_run() -> agentRunId + manifest.json
718
- └── orchestration.ensure_skill(×39) -> download skills if not cached
719
+ └── orchestration.ensure_skill(×N) -> download stack-relevant skills from 86-skill registry
719
720
 
720
721
 
721
722
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -994,6 +995,8 @@ Edit `.mcp/exceptions/security-exceptions.json`:
994
995
  | `SECURITY_GATE_POLICY` | `.mcp/policies/security-policy.json` | Path to policy file |
995
996
  | `SECURITY_GATE_SCANNERS` | built-in | Path to custom scanner config (must be within project directory) |
996
997
  | `SECURITY_GATE_EXCEPTIONS` | `.mcp/exceptions/security-exceptions.json` | Path to exceptions file (must be within project directory) |
998
+ | `SECURITY_GATE_MODE` | `full` | Set to `file_by_file` for scoped per-file scanning |
999
+ | `SECURITY_GATE_TARGETS` | (all changed files) | Comma-separated file paths to restrict the scan surface |
997
1000
 
998
1001
  ### Integrations (all optional)
999
1002
 
@@ -1,6 +1,8 @@
1
1
  import { runPrGate } from "../gate/policy.js";
2
2
  // Allow safe git revision operators (~ and ^) plus ref/path characters. CWE-88.
3
3
  const SAFE_REF_RE = /^[a-zA-Z0-9_./~^-]+$/;
4
+ // Allow relative file/folder paths for targets. CWE-88.
5
+ const SAFE_TARGET_RE = /^[a-zA-Z0-9_./ -]+$/;
4
6
  function safeEnvRef(envVar, defaultValue) {
5
7
  const val = process.env[envVar] || defaultValue;
6
8
  if (!SAFE_REF_RE.test(val)) {
@@ -9,11 +11,26 @@ function safeEnvRef(envVar, defaultValue) {
9
11
  }
10
12
  return val;
11
13
  }
14
+ function safeEnvTargets(envVar) {
15
+ const raw = process.env[envVar];
16
+ if (!raw)
17
+ return undefined;
18
+ const targets = raw.split(",").map((t) => t.trim()).filter(Boolean);
19
+ return targets.filter((t) => {
20
+ if (!SAFE_TARGET_RE.test(t) || t.includes("..")) {
21
+ console.error(`Skipping unsafe target: "${t}"`);
22
+ return false;
23
+ }
24
+ return true;
25
+ });
26
+ }
12
27
  async function main() {
13
28
  const baseRef = safeEnvRef("SECURITY_GATE_BASE_REF", "origin/main");
14
29
  const headRef = safeEnvRef("SECURITY_GATE_HEAD_REF", "HEAD");
15
30
  const policyPath = process.env.SECURITY_GATE_POLICY || ".mcp/policies/security-policy.json";
16
- const result = await runPrGate({ baseRef, headRef, policyPath });
31
+ const mode = (process.env.SECURITY_GATE_MODE ?? "recent_changes");
32
+ const targets = safeEnvTargets("SECURITY_GATE_TARGETS");
33
+ const result = await runPrGate({ baseRef, headRef, policyPath, mode, targets });
17
34
  // Print result for Actions logs
18
35
  console.log(JSON.stringify(result, null, 2));
19
36
  if (result.status !== "PASS") {
@@ -13,10 +13,12 @@
13
13
  import { createInterface } from "node:readline/promises";
14
14
  import { stdin as input, stdout as output } from "node:process";
15
15
  import { spawnSync } from "node:child_process";
16
- import { platform, arch, homedir } from "node:os";
17
- import { mkdirSync, createWriteStream, chmodSync, existsSync } from "node:fs";
16
+ import { platform, arch, homedir, tmpdir } from "node:os";
17
+ import { mkdirSync, createWriteStream, chmodSync, existsSync, writeFileSync, unlinkSync } from "node:fs";
18
18
  import { join } from "node:path";
19
19
  import { pipeline } from "node:stream/promises";
20
+ import { createHash } from "node:crypto";
21
+ import { readFile as readFileAsync } from "node:fs/promises";
20
22
  // ─── Constants ────────────────────────────────────────────────────────────────
21
23
  const PROJECT_TYPES = [
22
24
  {
@@ -220,6 +222,39 @@ function run(cmd, args) {
220
222
  const result = spawnSync(cmd, args, { stdio: "inherit" });
221
223
  return result.status === 0;
222
224
  }
225
+ // ─── Binary integrity helpers ─────────────────────────────────────────────────
226
+ // CWE-494: verify downloaded binary against publisher SHA-256 checksum before install.
227
+ async function fetchChecksumFile(assets) {
228
+ const checksumAsset = assets.find((a) => /checksums?\.txt$/i.test(a.name) || /\.sha256(sums?)?$/i.test(a.name));
229
+ if (!checksumAsset)
230
+ return null;
231
+ try {
232
+ const res = await fetch(checksumAsset.browser_download_url);
233
+ if (!res.ok)
234
+ return null;
235
+ return await res.text();
236
+ }
237
+ catch {
238
+ return null;
239
+ }
240
+ }
241
+ function parseExpectedHash(checksumContent, filename) {
242
+ for (const line of checksumContent.split("\n")) {
243
+ const parts = line.trim().split(/\s+/);
244
+ if (parts.length >= 2) {
245
+ const hash = parts[0];
246
+ const name = (parts.at(-1) ?? "").replace(/^\*/, "");
247
+ if (name === filename && /^[0-9a-f]{64}$/i.test(hash)) {
248
+ return hash.toLowerCase();
249
+ }
250
+ }
251
+ }
252
+ return null;
253
+ }
254
+ async function verifyIntegrity(filePath, expectedHash) {
255
+ const content = await readFileAsync(filePath);
256
+ return createHash("sha256").update(content).digest("hex") === expectedHash;
257
+ }
223
258
  // ─── GitHub binary download ───────────────────────────────────────────────────
224
259
  async function fetchLatestRelease(repo) {
225
260
  try {
@@ -282,6 +317,29 @@ async function installFromGitHub(tool, os) {
282
317
  print(` Download failed.`);
283
318
  return false;
284
319
  }
320
+ // CWE-494: verify SHA-256 integrity before executing anything
321
+ const checksumContent = await fetchChecksumFile(release.assets);
322
+ if (checksumContent) {
323
+ const expectedHash = parseExpectedHash(checksumContent, fileName);
324
+ if (expectedHash) {
325
+ const valid = await verifyIntegrity(tmpFile, expectedHash);
326
+ if (!valid) {
327
+ print(` Integrity check FAILED for ${fileName} — aborting install.`);
328
+ try {
329
+ unlinkSync(tmpFile);
330
+ }
331
+ catch { /* ignore cleanup failure */ }
332
+ return false;
333
+ }
334
+ print(` Integrity verified (SHA-256 matched).`);
335
+ }
336
+ else {
337
+ print(` Warning: checksum file found but no entry for ${fileName} — proceeding without verification.`);
338
+ }
339
+ }
340
+ else {
341
+ print(` Warning: no checksum file in release assets — cannot verify binary integrity.`);
342
+ }
285
343
  const destDir = "/usr/local/bin";
286
344
  if (tool.tarball) {
287
345
  // Extract the binary from the archive
@@ -347,12 +405,24 @@ async function tryDnf(tool) {
347
405
  const mgr = commandExists("dnf") ? "dnf" : commandExists("yum") ? "yum" : null;
348
406
  if (!mgr)
349
407
  return false;
350
- // Add Aqua Security rpm repo
351
- const repoContent = "[trivy]\\nname=Trivy repository\\n" +
352
- "baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$releasever/$basearch/\\n" +
353
- "gpgcheck=0\\nenabled=1";
408
+ // CWE-78: avoid bash -c shell construction — write repo file to a temp path
409
+ // then move it into place with sudo (no shell, no injection surface).
410
+ const repoLines = [
411
+ "[trivy]",
412
+ "name=Trivy repository",
413
+ "baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/$releasever/$basearch/",
414
+ "gpgcheck=0",
415
+ "enabled=1"
416
+ ];
417
+ const tmpRepoFile = join(tmpdir(), `trivy-${Date.now()}.repo`);
354
418
  print(` Adding Aqua Security yum/dnf repository...`);
355
- run("bash", ["-c", `echo -e "${repoContent}" | sudo tee /etc/yum.repos.d/trivy.repo`]);
419
+ try {
420
+ writeFileSync(tmpRepoFile, repoLines.join("\n") + "\n", "utf-8");
421
+ }
422
+ catch {
423
+ return false;
424
+ }
425
+ run("sudo", ["mv", tmpRepoFile, "/etc/yum.repos.d/trivy.repo"]);
356
426
  print(` sudo ${mgr} install -y trivy`);
357
427
  return run("sudo", [mgr, "install", "-y", "trivy"]);
358
428
  }
@@ -513,6 +583,7 @@ export async function runOnboarding() {
513
583
  print("");
514
584
  print(" This applies the right compliance controls automatically,");
515
585
  print(" such as PCI DSS for payment cards or HIPAA for health data.");
586
+ print(" You can select multiple options (e.g. 1 2 or 1,2).");
516
587
  print("");
517
588
  for (const d of SENSITIVE_DATA_OPTIONS) {
518
589
  print(` ${d.key}. ${d.label}`);
@@ -1,4 +1,7 @@
1
+ import { sanitizeErrorMessage } from "../result.js";
1
2
  import { searchRepo } from "../../repo/search.js";
3
+ import fg from "fast-glob";
4
+ import { readFileSafe } from "../../repo/fs.js";
2
5
  export async function checkApi(_) {
3
6
  const findings = [];
4
7
  const zodHits = await searchRepo({ query: "zod|valibot|yup|joi", isRegex: true, maxMatches: 200 });
@@ -135,5 +138,95 @@ export async function checkApi(_) {
135
138
  ]
136
139
  });
137
140
  }
141
+ // 5. API schema drift (OpenAPI/Swagger spec vs code routes)
142
+ findings.push(...await checkApiSchemaDrift());
143
+ return findings;
144
+ }
145
+ function parseDeclaredPaths(specContent) {
146
+ const paths = new Set();
147
+ for (const match of specContent.matchAll(/^\s{0,4}(\/[a-zA-Z0-9/{}_-]+)\s*:/gm)) {
148
+ paths.add(match[1]);
149
+ }
150
+ return paths;
151
+ }
152
+ function findShadowRoutes(codeRouteHits, declaredPaths) {
153
+ const shadows = [];
154
+ for (const hit of codeRouteHits) {
155
+ const routeMatch = /['"](\/?[a-zA-Z0-9/{}_-]+)['"]/.exec(hit.preview);
156
+ if (!routeMatch)
157
+ continue;
158
+ const route = routeMatch[1].startsWith("/") ? routeMatch[1] : `/${routeMatch[1]}`;
159
+ const normalised = route.replaceAll(/:([a-zA-Z_]+)/g, "{$1}");
160
+ if (!declaredPaths.has(normalised) && !declaredPaths.has(route)) {
161
+ shadows.push(`${hit.file}:${hit.line} — ${route}`);
162
+ }
163
+ }
164
+ return shadows;
165
+ }
166
+ async function checkApiSchemaDrift() {
167
+ const findings = [];
168
+ try {
169
+ const specFiles = await fg([
170
+ "openapi.{yaml,yml,json}",
171
+ "swagger.{yaml,yml,json}",
172
+ "**/openapi.{yaml,yml,json}",
173
+ "**/swagger.{yaml,yml,json}",
174
+ "**/api-spec.{yaml,yml,json}",
175
+ "**/openapi/**/*.{yaml,yml,json}"
176
+ ], { ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"], dot: true });
177
+ const codeRouteHits = await searchRepo({
178
+ query: String.raw `(?:router|app|fastify|server)\.(?:get|post|put|delete|patch)\s*\(\s*['"](/[^'"]+)['"]`,
179
+ isRegex: true,
180
+ maxMatches: 300
181
+ });
182
+ if (specFiles.length === 0) {
183
+ if (codeRouteHits.length > 0) {
184
+ findings.push({
185
+ id: "API_NO_OPENAPI_SPEC",
186
+ title: "API routes detected but no OpenAPI/Swagger specification found",
187
+ severity: "MEDIUM",
188
+ evidence: codeRouteHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
189
+ requiredActions: [
190
+ "Create an OpenAPI 3.x specification (openapi.yaml) that documents all API routes.",
191
+ "An API contract enables automated schema validation, client SDK generation, and drift detection.",
192
+ "Use tools like `zod-to-openapi` or `tsoa` to generate the spec from existing TypeScript code."
193
+ ]
194
+ });
195
+ }
196
+ return findings;
197
+ }
198
+ const specContent = await readFileSafe(specFiles[0]);
199
+ const declaredPaths = parseDeclaredPaths(specContent);
200
+ const shadowRoutes = findShadowRoutes(codeRouteHits, declaredPaths);
201
+ if (shadowRoutes.length > 0) {
202
+ findings.push({
203
+ id: "API_SHADOW_ENDPOINT",
204
+ title: `${shadowRoutes.length} API route(s) in code not declared in OpenAPI spec — shadow endpoints`,
205
+ severity: "HIGH",
206
+ evidence: [...new Set(shadowRoutes)].slice(0, 15),
207
+ requiredActions: [
208
+ "Add all undocumented routes to the OpenAPI specification.",
209
+ "Shadow endpoints bypass API gateway policies, rate limiting, and schema validation.",
210
+ "Automate spec generation (tsoa, zod-to-openapi) to prevent drift from recurring."
211
+ ]
212
+ });
213
+ }
214
+ if (/type:\s+object/.test(specContent) && !/properties:/.test(specContent)) {
215
+ findings.push({
216
+ id: "API_PERMISSIVE_SCHEMA",
217
+ title: "OpenAPI spec contains `type: object` without `properties` — accepts any payload shape",
218
+ severity: "MEDIUM",
219
+ files: [specFiles[0]],
220
+ requiredActions: [
221
+ "Define explicit `properties` for all object schemas in the OpenAPI spec.",
222
+ "Permissive schemas allow attackers to inject unexpected fields (mass assignment, prototype pollution).",
223
+ "Set `additionalProperties: false` on request body schemas to enforce strict validation."
224
+ ]
225
+ });
226
+ }
227
+ }
228
+ catch (err) {
229
+ console.warn("[checkApiSchemaDrift] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
230
+ }
138
231
  return findings;
139
232
  }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * GitHub Actions CI/CD pipeline hardening checks.
3
+ * Covers supply chain attack vectors specific to GitHub Actions workflows.
4
+ */
5
+ import { sanitizeErrorMessage } from "../result.js";
6
+ import fg from "fast-glob";
7
+ import { readFileSafe } from "../../repo/fs.js";
8
+ export async function runCiPipelineChecks(_opts) {
9
+ const findings = [];
10
+ try {
11
+ const workflowFiles = await fg([".github/workflows/*.yml", ".github/workflows/*.yaml"], {
12
+ dot: true,
13
+ ignore: ["**/node_modules/**", "**/.git/**"]
14
+ });
15
+ if (workflowFiles.length === 0) {
16
+ return [];
17
+ }
18
+ const unpinnedFiles = [];
19
+ const pwnTargetFiles = [];
20
+ const secretEchoFiles = [];
21
+ const noPermissionsFiles = [];
22
+ const selfHostedFiles = [];
23
+ for (const file of workflowFiles) {
24
+ let content;
25
+ try {
26
+ content = await readFileSafe(file);
27
+ }
28
+ catch {
29
+ continue;
30
+ }
31
+ // Check 1: Third-party actions not pinned to a full 40-char SHA
32
+ // Matches `uses: owner/repo@tag` but NOT `uses: owner/repo@<40hex chars>`
33
+ // Also skip `uses: ./.github/actions/` (local actions are fine)
34
+ const actionLines = content.split("\n").filter((line) => /uses:\s+[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+@/.test(line));
35
+ const unpinnedActions = actionLines.filter((line) => {
36
+ // Skip local actions
37
+ if (/uses:\s+\.\//.test(line))
38
+ return false;
39
+ // Flag anything not pinned to a 40-char hex SHA
40
+ return !/uses:\s+[a-zA-Z0-9_.\-/]+@[0-9a-f]{40}/.test(line);
41
+ });
42
+ if (unpinnedActions.length > 0) {
43
+ unpinnedFiles.push(file);
44
+ }
45
+ // Check 2: pull_request_target + dynamic ref usage (pwn-request vector)
46
+ // Attacker controls the ref/sha when a PR from a fork triggers pull_request_target
47
+ if (/pull_request_target/.test(content) &&
48
+ /\$\{\{\s*github\.event\.pull_request\.head\.(sha|ref)\s*\}\}/.test(content)) {
49
+ pwnTargetFiles.push(file);
50
+ }
51
+ // Check 3: Secrets printed to logs via echo
52
+ if (/echo\s+\$\{\{\s*secrets\./.test(content)) {
53
+ secretEchoFiles.push(file);
54
+ }
55
+ // Check 4: No top-level permissions block
56
+ // Without explicit permissions, the default is write access to all scopes
57
+ if (!/^permissions:/m.test(content)) {
58
+ noPermissionsFiles.push(file);
59
+ }
60
+ // Check 5: Self-hosted runners (broader attack surface — runner compromise = code execution)
61
+ if (/runs-on:\s+self-hosted/.test(content)) {
62
+ selfHostedFiles.push(file);
63
+ }
64
+ }
65
+ if (unpinnedFiles.length > 0) {
66
+ findings.push({
67
+ id: "CI_UNPINNED_ACTION",
68
+ title: "GitHub Actions using mutable tags instead of pinned SHA digests",
69
+ severity: "HIGH",
70
+ files: unpinnedFiles.slice(0, 10),
71
+ requiredActions: [
72
+ "Pin all third-party actions to a full 40-character commit SHA (e.g. `uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683`).",
73
+ "Mutable tags like @v3 can be silently redirected to malicious commits — SHA pinning prevents supply chain substitution.",
74
+ "Use a tool like `pin-github-action` or Dependabot to automate SHA pinning."
75
+ ]
76
+ });
77
+ }
78
+ if (pwnTargetFiles.length > 0) {
79
+ findings.push({
80
+ id: "CI_PWNTARGET_SHA",
81
+ title: "pull_request_target workflow uses attacker-controlled ref/SHA — pwn-request vector",
82
+ severity: "CRITICAL",
83
+ files: pwnTargetFiles.slice(0, 10),
84
+ requiredActions: [
85
+ "Never use `${{ github.event.pull_request.head.sha }}` or `head.ref` inside a `pull_request_target` workflow that checks out or runs code from the PR.",
86
+ "`pull_request_target` runs with write permissions and secrets access; the PR head is attacker-controlled.",
87
+ "Use `pull_request` (not `pull_request_target`) for code that executes untrusted contributions, or add explicit guard conditions."
88
+ ]
89
+ });
90
+ }
91
+ if (secretEchoFiles.length > 0) {
92
+ findings.push({
93
+ id: "CI_SECRET_ECHO",
94
+ title: "GitHub Actions workflow echoes secrets to logs",
95
+ severity: "CRITICAL",
96
+ files: secretEchoFiles.slice(0, 10),
97
+ requiredActions: [
98
+ "Remove any `echo ${{ secrets.* }}` statements — secrets printed to logs are visible to anyone with read access to the repository.",
99
+ "GitHub masks known secret values in logs, but this is not reliable for all encodings.",
100
+ "Pass secrets via environment variables (`env: MY_SECRET: ${{ secrets.MY_SECRET }}`) and read them in code, never echo them."
101
+ ]
102
+ });
103
+ }
104
+ if (noPermissionsFiles.length > 0) {
105
+ findings.push({
106
+ id: "CI_NO_PERMISSIONS",
107
+ title: "GitHub Actions workflows without explicit permissions block",
108
+ severity: "MEDIUM",
109
+ files: noPermissionsFiles.slice(0, 10),
110
+ requiredActions: [
111
+ "Add an explicit `permissions:` block at the top of each workflow (or at the job level) to grant only the minimum required scopes.",
112
+ "Without explicit permissions, the default is determined by org/repo settings — often write-all.",
113
+ "Example minimal read-only: `permissions: { contents: read }`."
114
+ ]
115
+ });
116
+ }
117
+ if (selfHostedFiles.length > 0) {
118
+ findings.push({
119
+ id: "CI_SELF_HOSTED_RUNNER",
120
+ title: "GitHub Actions using self-hosted runners",
121
+ severity: "MEDIUM",
122
+ files: selfHostedFiles.slice(0, 10),
123
+ requiredActions: [
124
+ "Self-hosted runners executing untrusted fork PRs can be compromised — restrict `pull_request` triggers on self-hosted runner workflows.",
125
+ "Ensure self-hosted runners are ephemeral (destroyed after each job) and isolated from production networks.",
126
+ "Use GitHub-hosted runners for public repositories or untrusted code paths."
127
+ ]
128
+ });
129
+ }
130
+ }
131
+ catch (err) {
132
+ console.warn("[runCiPipelineChecks] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
133
+ }
134
+ return findings;
135
+ }
@@ -1,4 +1,31 @@
1
+ /**
2
+ * Weak cryptography detection.
3
+ * Mapped to NIST SP 800-131A Rev 2.
4
+ */
5
+ import { sanitizeErrorMessage } from "../result.js";
1
6
  import { searchRepo } from "../../repo/search.js";
7
+ function checkPbkdf2Iterations(hits) {
8
+ for (const hit of hits) {
9
+ const iterMatch = /pbkdf2(?:Sync)?\s*\([^)]*?,\s*[^,]+,\s*(\d+)/.exec(hit.preview);
10
+ if (!iterMatch)
11
+ continue;
12
+ const iters = Number.parseInt(iterMatch[1], 10);
13
+ if (iters < 600000) {
14
+ return {
15
+ id: "CRYPTO_LOW_PBKDF2_ITERATIONS",
16
+ title: `PBKDF2 iteration count too low (${iters} < 600,000)`,
17
+ severity: "HIGH",
18
+ evidence: [`${hit.file}:${hit.line}:${hit.preview}`],
19
+ files: [hit.file],
20
+ requiredActions: [
21
+ "Use ≥ 600,000 iterations for PBKDF2-SHA256 (OWASP 2023 recommendation).",
22
+ "Prefer bcrypt (cost ≥ 12) or Argon2id instead."
23
+ ]
24
+ };
25
+ }
26
+ }
27
+ return null;
28
+ }
2
29
  export async function checkCrypto(_opts) {
3
30
  const findings = [];
4
31
  try {
@@ -86,27 +113,9 @@ export async function checkCrypto(_opts) {
86
113
  isRegex: true,
87
114
  maxMatches: 200
88
115
  });
89
- // Check for numeric iteration counts in the context
90
- for (const hit of pbkdf2Hits) {
91
- const iterMatch = /pbkdf2(?:Sync)?\s*\([^)]*?,\s*[^,]+,\s*(\d+)/.exec(hit.preview);
92
- if (iterMatch) {
93
- const iters = parseInt(iterMatch[1], 10);
94
- if (iters < 600000) {
95
- findings.push({
96
- id: "CRYPTO_LOW_PBKDF2_ITERATIONS",
97
- title: `PBKDF2 iteration count too low (${iters} < 600,000)`,
98
- severity: "HIGH",
99
- evidence: [`${hit.file}:${hit.line}:${hit.preview}`],
100
- files: [hit.file],
101
- requiredActions: [
102
- "Use ≥ 600,000 iterations for PBKDF2-SHA256 (OWASP 2023 recommendation).",
103
- "Prefer bcrypt (cost ≥ 12) or Argon2id instead."
104
- ]
105
- });
106
- break;
107
- }
108
- }
109
- }
116
+ const pbkdf2Finding = checkPbkdf2Iterations(pbkdf2Hits);
117
+ if (pbkdf2Finding)
118
+ findings.push(pbkdf2Finding);
110
119
  // 6. Hardcoded IV/nonce
111
120
  const hardcodedIvHits = await searchRepo({
112
121
  query: String.raw `iv\s*[:=]\s*(?:Buffer\.from\(['"][0-9a-fA-F]+['"]\)|['"][0-9a-fA-F]{16,}['"])`,
@@ -145,9 +154,69 @@ export async function checkCrypto(_opts) {
145
154
  ]
146
155
  });
147
156
  }
157
+ // 8. Post-quantum readiness: RSA-1024
158
+ const rsa1024Hits = await searchRepo({
159
+ query: String.raw `modulusLength\s*:\s*1024|generateKeyPair\s*\(\s*['"]rsa['"][^)]*1024`,
160
+ isRegex: true,
161
+ maxMatches: 200
162
+ });
163
+ if (rsa1024Hits.length > 0) {
164
+ findings.push({
165
+ id: "CRYPTO_RSA_1024",
166
+ title: "RSA-1024 key detected — cryptographically broken",
167
+ severity: "CRITICAL",
168
+ evidence: rsa1024Hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
169
+ files: [...new Set(rsa1024Hits.slice(0, 10).map((m) => m.file))],
170
+ requiredActions: [
171
+ "Upgrade to RSA-4096 minimum, or migrate to ML-DSA (FIPS 204) / SLH-DSA (FIPS 205) for new key material.",
172
+ "RSA-1024 is fully broken — NIST deprecated it in 2013 (SP 800-131A).",
173
+ "For TLS certificates, reissue with RSA-4096 or ECDSA P-384 immediately."
174
+ ]
175
+ });
176
+ }
177
+ // 9. Post-quantum readiness: RSA-2048 warning
178
+ const rsa2048Hits = await searchRepo({
179
+ query: String.raw `modulusLength\s*:\s*2048|generateKeyPair\s*\(\s*['"]rsa['"][^)]*2048`,
180
+ isRegex: true,
181
+ maxMatches: 200
182
+ });
183
+ if (rsa2048Hits.length > 0) {
184
+ findings.push({
185
+ id: "CRYPTO_RSA_2048_PQC",
186
+ title: "RSA-2048 detected — quantum-vulnerable; plan migration to post-quantum algorithms",
187
+ severity: "MEDIUM",
188
+ evidence: rsa2048Hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
189
+ files: [...new Set(rsa2048Hits.slice(0, 10).map((m) => m.file))],
190
+ requiredActions: [
191
+ "RSA-2048 is currently secure against classical computers but will be broken by sufficiently large quantum computers.",
192
+ "NIST finalized post-quantum standards in 2024: ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205).",
193
+ "For long-lived keys or data requiring 10+ year secrecy: migrate to ML-DSA or use a hybrid classical+PQC scheme."
194
+ ]
195
+ });
196
+ }
197
+ // 10. Post-quantum readiness: ECDSA P-256 (informational)
198
+ const p256Hits = await searchRepo({
199
+ query: String.raw `prime256v1|secp256r1|namedCurve\s*:\s*['"]P-256['"]|namedCurve\s*:\s*['"]p256['"]`,
200
+ isRegex: true,
201
+ maxMatches: 200
202
+ });
203
+ if (p256Hits.length > 0) {
204
+ findings.push({
205
+ id: "CRYPTO_ECDSA_P256_PQC",
206
+ title: "ECDSA P-256 detected — quantum-vulnerable in the long term",
207
+ severity: "LOW",
208
+ evidence: p256Hits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
209
+ files: [...new Set(p256Hits.slice(0, 10).map((m) => m.file))],
210
+ requiredActions: [
211
+ "P-256 (secp256r1) is secure today but vulnerable to Shor's algorithm on a sufficiently large quantum computer.",
212
+ "NIST post-quantum signature standards: ML-DSA (FIPS 204) and SLH-DSA (FIPS 205) are the recommended replacements.",
213
+ "For new systems handling sensitive long-lived data, evaluate hybrid ECDSA+ML-DSA or pure ML-DSA."
214
+ ]
215
+ });
216
+ }
148
217
  }
149
218
  catch (err) {
150
- console.warn("[checkCrypto] Internal error:", err instanceof Error ? err.message : String(err));
219
+ console.warn("[checkCrypto] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
151
220
  }
152
221
  return findings;
153
222
  }
@@ -1,3 +1,7 @@
1
+ /**
2
+ * Database security checks.
3
+ */
4
+ import { sanitizeErrorMessage } from "../result.js";
1
5
  import { searchRepo } from "../../repo/search.js";
2
6
  export async function checkDatabase(_opts) {
3
7
  const findings = [];
@@ -138,7 +142,7 @@ export async function checkDatabase(_opts) {
138
142
  }
139
143
  }
140
144
  catch (err) {
141
- console.warn("[checkDatabase] Internal error:", err instanceof Error ? err.message : String(err));
145
+ console.warn("[checkDatabase] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
142
146
  }
143
147
  return findings;
144
148
  }