Dependency Audit — Framework, Package, and Toolchain Security Audit project dependencies, frameworks, language runtimes, and dev tools for known vulnerabilities (CVEs), security anti-patterns, and supply chain risks. Cross-references: for patching the same advisory across many repos; for running osv-scanner / trivy in CI across every lockfile at once; for reviewing the content of MCP servers, skills, and plugins this skill inventories; "PaaS" for platform runtime and config checks behind ; for tokens found in or CI; for the cloud roles CI assumes via OIDC; for workflow and runner security; fo…

\n```\n\nFinding when a `.pyc` with no source is imported at runtime (a sibling module imports its name, or it sits on `sys.path` ahead of the source). Otherwise it's a hardening note: remove it and add `__pycache__/` and `*.pyc` to `.gitignore` and the package's exclude list. Other concealed executables (prebuilt `.so` / `.node` / `.exe` / `.jar` files in a source package) deserve the same check: is there a build recipe that produces them from source in this repo?\n\n## Sources\n\n- NVIDIA SkillSpector (Apache-2.0): [`docs/DEPENDENCY_SOURCE_REDIRECTION.md`](https://github.com/NVIDIA/SkillSpector/blob/main/docs/DEPENDENCY_SOURCE_REDIRECTION.md), [`mcp_rug_pull.py`](https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/mcp_rug_pull.py), [README supply-chain patterns](https://github.com/NVIDIA/SkillSpector/blob/main/README.md#supply-chain-10-patterns). Patterns paraphrased; greps written for this repo.\n- Alex Birsan, \"Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies\" (2021)\n- pip documentation, \"Secure installs\" and `--extra-index-url` notes; Cargo book, \"Source replacement\"; Maven \"Using Mirrors for Repositories\"\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":10856,"content_sha256":"1c8782b26e96e7e9e06598c07433d1030146f2d214f30e80a50ca0edc73b7933"},{"filename":"references/to-sarif.mjs","content":"#!/usr/bin/env node\n// to-sarif.mjs: convert findings.json to SARIF 2.1.0 (e.g. for GitHub code scanning). Zero dependencies.\n//\n// Usage: node to-sarif.mjs findings.json [--include-needs-validation] [-o out.sarif]\n// (\"-\" reads stdin; output goes to stdout unless -o is given)\n// Exit: 0 written, 1 findings.json failed validation (nothing written), 2 usage / read / parse error.\n//\n// Mapping:\n// - Only `confirmed` records become results by default. `rejected` records are never exported.\n// - --include-needs-validation also exports `needs_validation` records at level \"note\", with no\n// security-severity, properties.verdict = \"needs_validation\", and the blockers + validation plan in the message.\n// - Confirmed with disposition \"Fixed\" is skipped (nothing left to alert on). \"Accepted Risk\" and\n// \"Mitigated (platform/config)\" are exported with an accepted external suppression, so they show as dismissed.\n// - fingerprint -> partialFingerprints[\"cybersecuritySkills/v1\"] and the rule id (one rule per finding, so\n// each rule's security-severity matches its single result).\n// - severity.overall -> rule + result property \"security-severity\" (critical 9.5, high 8.0, medium 5.5, low 3.0)\n// and level (critical/high -> error, medium -> warning, low -> note).\n// - trace[] -> codeFlows[0].threadFlows[0].locations; evidence[] -> relatedLocations.\n// - ext (skill-specific fields) -> result properties.ext, unchanged. Exploitation signals are also surfaced:\n// ext.in_kev === true adds the tag \"kev\" (rule and result); a numeric ext.epss -> result properties.epss.\n// - Paths are emitted relative to uriBaseId \"%SRCROOT%\".\n\nimport { readFileSync, writeFileSync, realpathSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\nimport { validateFindings, readJson } from \"./validate-findings.mjs\";\n\nexport const SECURITY_SEVERITY = { critical: \"9.5\", high: \"8.0\", medium: \"5.5\", low: \"3.0\" };\nexport const LEVEL = { critical: \"error\", high: \"error\", medium: \"warning\", low: \"note\" };\nconst FP_KEY = \"cybersecuritySkills/v1\";\nconst SUPPRESSED = new Set([\"Accepted Risk\", \"Mitigated (platform/config)\"]);\n\nfunction physical(file, line) {\n return {\n physicalLocation: {\n artifactLocation: { uri: file.split(\"/\").map(encodeURIComponent).join(\"/\"), uriBaseId: \"%SRCROOT%\" },\n region: { startLine: line },\n },\n };\n}\n\nfunction ruleFor(rec) {\n const tags = [\"security\"];\n if (rec.cwe) tags.push(`external/cwe/${rec.cwe.toLowerCase()}`);\n if (rec.owasp_category) tags.push(`owasp/${rec.owasp_category}`);\n if (rec.ext?.in_kev === true) tags.push(\"kev\");\n const properties = { tags, skill: rec.skill };\n if (rec.verdict === \"confirmed\") properties[\"security-severity\"] = SECURITY_SEVERITY[rec.severity.overall];\n const rule = {\n id: rec.fingerprint,\n shortDescription: { text: rec.title },\n properties,\n };\n if (rec.cwe) rule.helpUri = `https://cwe.mitre.org/data/definitions/${rec.cwe.slice(4)}.html`;\n return rule;\n}\n\nfunction messageFor(rec) {\n if (rec.verdict === \"confirmed\") {\n const s = rec.severity;\n return `${rec.title}. Severity ${s.overall} (likelihood ${s.likelihood.score}: ${s.likelihood.reason}; impact ${s.impact.score}: ${s.impact.reason}).`;\n }\n return `Needs validation: ${rec.title}. Blockers: ${rec.blockers.join(\"; \")}. Validation plan: ${rec.validation_plan}`;\n}\n\nfunction resultFor(rec, ruleIndex) {\n const result = {\n ruleId: rec.fingerprint,\n ruleIndex,\n level: rec.verdict === \"confirmed\" ? LEVEL[rec.severity.overall] : \"note\",\n message: { text: messageFor(rec) },\n locations: [physical(rec.location.file, rec.location.line)],\n partialFingerprints: { [FP_KEY]: rec.fingerprint },\n properties: { verdict: rec.verdict, confidence: rec.confidence, skill: rec.skill },\n };\n if (rec.verdict === \"confirmed\") {\n result.properties[\"security-severity\"] = SECURITY_SEVERITY[rec.severity.overall];\n if (rec.disposition) result.properties.disposition = rec.disposition;\n if (SUPPRESSED.has(rec.disposition))\n result.suppressions = [{ kind: \"external\", status: \"accepted\", justification: `Disposition: ${rec.disposition}` }];\n }\n if (rec.ext) result.properties.ext = rec.ext;\n if (rec.ext?.in_kev === true) result.properties.tags = [\"kev\"];\n if (typeof rec.ext?.epss === \"number\") result.properties.epss = rec.ext.epss;\n if (rec.trace.length > 0) {\n result.codeFlows = [\n {\n threadFlows: [\n {\n locations: rec.trace.map((t) => ({\n location: { ...physical(t.file, t.line), message: { text: t.description } },\n kinds: [t.kind],\n })),\n },\n ],\n },\n ];\n }\n if (rec.evidence.length > 0) {\n result.relatedLocations = rec.evidence.map((e, id) => ({ id, ...physical(e.file, e.line), message: { text: e.description } }));\n }\n return result;\n}\n\n/**\n * Convert a findings array (must already be valid) to a SARIF 2.1.0 log object.\n * @param {object[]} findings\n * @param {{includeNeedsValidation?: boolean}} [opts]\n */\nexport function toSarif(findings, opts = {}) {\n const selected = findings.filter((r) => {\n if (r.verdict === \"confirmed\") return r.disposition !== \"Fixed\";\n if (r.verdict === \"needs_validation\") return Boolean(opts.includeNeedsValidation);\n return false;\n });\n const rules = selected.map(ruleFor);\n const results = selected.map((r, i) => resultFor(r, i));\n return {\n $schema: \"https://json.schemastore.org/sarif-2.1.0.json\",\n version: \"2.1.0\",\n runs: [\n {\n tool: {\n driver: {\n name: \"cybersecurity-skills\",\n informationUri: \"https://github.com/briiirussell/cybersecurity-skills\",\n rules,\n },\n },\n originalUriBaseIds: { \"%SRCROOT%\": { description: { text: \"Repository root\" } } },\n results,\n },\n ],\n };\n}\n\nfunction main(argv) {\n const args = argv.slice(2);\n let out = null;\n let includeNV = false;\n const pos = [];\n for (let i = 0; i \u003c args.length; i++) {\n if (args[i] === \"-o\" || args[i] === \"--output\") out = args[++i] ?? \"\";\n else if (args[i] === \"--include-needs-validation\") includeNV = true;\n else pos.push(args[i]);\n }\n if (pos.length !== 1 || pos[0] === \"-h\" || pos[0] === \"--help\" || out === \"\") {\n console.error(\"usage: node to-sarif.mjs \u003cfindings.json | -> [--include-needs-validation] [-o out.sarif]\");\n return 2;\n }\n let data;\n try {\n data = readJson(pos[0]);\n } catch (err) {\n console.error(`error: cannot read ${pos[0]}: ${err.message}`);\n return 2;\n }\n const errors = validateFindings(data);\n if (errors.length) {\n for (const e of errors) console.error(e);\n console.error(`\\n${pos[0]}: INVALID, no SARIF written. Fix the problems above (node validate-findings.mjs ${pos[0]}).`);\n return 1;\n }\n const sarif = JSON.stringify(toSarif(data, { includeNeedsValidation: includeNV }), null, 2) + \"\\n\";\n if (out) {\n writeFileSync(out, sarif);\n console.error(`wrote ${out}`);\n } else process.stdout.write(sarif);\n return 0;\n}\n\nfunction isMain() {\n try {\n return Boolean(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;\n } catch {\n return false;\n }\n}\n\nif (isMain()) {\n process.exitCode = main(process.argv);\n}\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":7306,"content_sha256":"70ee7a3d056a60a15b9aebdd654a1e026377a3b19f9c760179dd21845fc0ed5e"},{"filename":"references/validate-findings.mjs","content":"#!/usr/bin/env node\n// validate-findings.mjs: zero-dependency validator for findings.json.\n//\n// Usage: node validate-findings.mjs findings.json (or \"-\" to read stdin)\n// Exit: 0 valid, 1 invalid (one message per problem on stderr), 2 usage / read / JSON parse error.\n//\n// Hand-written checks that mirror findings.schema.json and add the rules JSON Schema cannot express:\n// sort order, unique fingerprints, path safety, trace order, overall \u003c= impact, and confidence `high`\n// requiring a reproduced finding. This script is authoritative where the two disagree.\n//\n// The verdict-shaped record design and the idea of a zero-dep validator are adapted from\n// cloudflare/security-audit-skill (MIT, Copyright (c) Cloudflare, Inc.). The code is this repo's own.\n\nimport { readFileSync, realpathSync } from \"node:fs\";\nimport { pathToFileURL } from \"node:url\";\n\nexport const VERDICTS = [\"confirmed\", \"needs_validation\", \"rejected\"];\nexport const LEVELS = [\"low\", \"medium\", \"high\", \"critical\"]; // ascending\nexport const CONFIDENCE = [\"high\", \"medium\", \"low\"];\nexport const DISPOSITIONS = [\"Fixed\", \"Deferred\", \"Accepted Risk\", \"Mitigated (platform/config)\", \"Requires major migration\"];\nexport const TRACE_KINDS = [\"entrypoint\", \"propagation\", \"sink\"];\nexport const CONDITION_KINDS = [\n \"authentication_level\",\n \"authorization_role\",\n \"user_interaction\",\n \"system_configuration\",\n \"network_routing\",\n \"environmental_dependency\",\n \"data_state\",\n \"timing_dependency\",\n \"third_party_dependency\",\n];\n\nconst COMMON = [\"fingerprint\", \"verdict\", \"skill\", \"title\", \"location\", \"trace\", \"evidence\", \"counterevidence\", \"confidence\"];\nconst OPTIONAL_COMMON = [\"cwe\", \"owasp_category\", \"ext\"];\nconst SHAPES = {\n confirmed: { required: [...COMMON, \"severity\", \"conditions\"], optional: [...OPTIONAL_COMMON, \"reproduced\", \"disposition\"] },\n needs_validation: { required: [...COMMON, \"blockers\", \"validation_plan\"], optional: OPTIONAL_COMMON },\n rejected: { required: [...COMMON, \"ruled_out_by\"], optional: OPTIONAL_COMMON },\n};\n\nconst FINGERPRINT_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{2,199}$/;\nconst SKILL_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;\nconst CWE_RE = /^CWE-[1-9][0-9]*$/;\nconst EXT_KEY_RE = /^[a-z][a-z0-9_]{0,63}$/;\n\nconst isObj = (v) => v !== null && typeof v === \"object\" && !Array.isArray(v);\nconst isText = (v) => typeof v === \"string\" && /\\S/.test(v);\n\n/** Returns a reason string if `p` is not a safe repo-relative path, else null. */\nexport function unsafePathReason(p) {\n if (typeof p !== \"string\" || p.length === 0) return \"must be a non-empty string\";\n if (p.includes(\"\\0\")) return \"contains a NUL byte\";\n if (p.includes(\"\\\\\")) return \"uses a backslash (use forward slashes)\";\n if (p.startsWith(\"/\")) return \"is absolute (must be repo-relative)\";\n if (p.startsWith(\"~\")) return \"starts with ~ (must be repo-relative)\";\n if (/^[A-Za-z]:/.test(p)) return \"has a drive letter (must be repo-relative)\";\n if (/^[A-Za-z][A-Za-z0-9+.-]*:\\/\\//.test(p)) return \"is a URL (must be a repo-relative file path)\";\n if (p.split(\"/\").some((seg) => seg === \"..\")) return \"contains a '..' segment\";\n return null;\n}\n\nfunction checkKeys(obj, required, optional, where, errors) {\n for (const k of required) if (!(k in obj)) errors.push(`${where}: missing required field \"${k}\"`);\n const allowed = new Set([...required, ...optional]);\n for (const k of Object.keys(obj)) if (!allowed.has(k)) errors.push(`${where}: unexpected field \"${k}\"`);\n}\n\nfunction checkText(v, where, errors) {\n if (!isText(v)) errors.push(`${where}: must be a non-empty string`);\n}\n\nfunction checkEnum(v, values, where, errors) {\n if (!values.includes(v)) errors.push(`${where}: must be one of ${values.join(\" | \")} (got ${JSON.stringify(v)})`);\n}\n\nfunction checkPath(v, where, errors) {\n const reason = unsafePathReason(v);\n if (reason) errors.push(`${where}: path ${JSON.stringify(v)} ${reason}`);\n}\n\nfunction checkLine(v, where, errors) {\n if (!Number.isInteger(v) || v \u003c 1) errors.push(`${where}: must be an integer >= 1 (got ${JSON.stringify(v)})`);\n}\n\nfunction checkLocation(v, where, errors) {\n if (!isObj(v)) return errors.push(`${where}: must be an object {file, line}`);\n checkKeys(v, [\"file\", \"line\"], [], where, errors);\n if (\"file\" in v) checkPath(v.file, `${where}.file`, errors);\n if (\"line\" in v) checkLine(v.line, `${where}.line`, errors);\n}\n\nfunction checkFileLineDesc(v, where, errors, extraRequired = []) {\n if (!isObj(v)) return errors.push(`${where}: must be an object`);\n checkKeys(v, [...extraRequired, \"file\", \"line\", \"description\"], [], where, errors);\n if (\"file\" in v) checkPath(v.file, `${where}.file`, errors);\n if (\"line\" in v) checkLine(v.line, `${where}.line`, errors);\n if (\"description\" in v) checkText(v.description, `${where}.description`, errors);\n}\n\nfunction checkTrace(trace, where, errors, minItems) {\n if (!Array.isArray(trace)) return errors.push(`${where}: must be an array`);\n if (trace.length \u003c minItems) errors.push(`${where}: needs at least ${minItems} steps (entrypoint ... sink)`);\n trace.forEach((step, i) => {\n checkFileLineDesc(step, `${where}[${i}]`, errors, [\"kind\"]);\n if (isObj(step) && \"kind\" in step) checkEnum(step.kind, TRACE_KINDS, `${where}[${i}].kind`, errors);\n });\n if (trace.length > 0) {\n if (trace[0]?.kind !== \"entrypoint\") errors.push(`${where}: must start with an \"entrypoint\" step`);\n if (trace[trace.length - 1]?.kind !== \"sink\") errors.push(`${where}: must end with a \"sink\" step`);\n trace.slice(1, -1).forEach((step, j) => {\n if (step?.kind === \"entrypoint\" || step?.kind === \"sink\")\n errors.push(`${where}[${j + 1}]: \"${step.kind}\" may only appear at the ${step.kind === \"entrypoint\" ? \"start\" : \"end\"} of the trace`);\n });\n }\n}\n\n// ext holds a skill's own fields (a PCI requirement, a cloud account, a MASVS control). Keep it flat so reports and\n// SARIF property bags can show it as-is: snake_case keys, scalar values or arrays of scalars.\nfunction checkExt(v, where, errors) {\n if (!isObj(v)) return errors.push(`${where}: must be an object of skill-specific fields`);\n const keys = Object.keys(v);\n if (keys.length === 0) errors.push(`${where}: must not be empty (leave ext out instead)`);\n const scalar = (x) => (typeof x === \"string\" && x.length > 0) || typeof x === \"boolean\" || (typeof x === \"number\" && Number.isFinite(x));\n for (const k of keys) {\n if (!EXT_KEY_RE.test(k)) errors.push(`${where}: key ${JSON.stringify(k)} must be snake_case ([a-z][a-z0-9_]*)`);\n const x = v[k];\n if (Array.isArray(x) ? x.length === 0 || !x.every(scalar) : !scalar(x))\n errors.push(`${where}.${k}: must be a non-empty string, a number, a boolean, or a non-empty array of those`);\n }\n}\n\nfunction checkScored(v, where, errors) {\n if (!isObj(v)) return errors.push(`${where}: must be an object {score, reason}`);\n checkKeys(v, [\"score\", \"reason\"], [], where, errors);\n if (\"score\" in v) checkEnum(v.score, LEVELS, `${where}.score`, errors);\n if (\"reason\" in v) checkText(v.reason, `${where}.reason`, errors);\n}\n\nfunction checkSeverity(v, where, errors) {\n if (!isObj(v)) return errors.push(`${where}: must be an object {likelihood, impact, overall}`);\n checkKeys(v, [\"likelihood\", \"impact\", \"overall\"], [], where, errors);\n if (\"likelihood\" in v) checkScored(v.likelihood, `${where}.likelihood`, errors);\n if (\"impact\" in v) checkScored(v.impact, `${where}.impact`, errors);\n if (\"overall\" in v) checkEnum(v.overall, LEVELS, `${where}.overall`, errors);\n const o = LEVELS.indexOf(v.overall);\n const i = LEVELS.indexOf(v.impact?.score);\n if (o >= 0 && i >= 0 && o > i)\n errors.push(`${where}.overall: \"${v.overall}\" exceeds impact \"${v.impact.score}\" (overall severity must not exceed demonstrated impact)`);\n}\n\n/** Validate one record. Returns an array of error strings. */\nexport function validateRecord(rec, where = \"record\") {\n const errors = [];\n if (!isObj(rec)) return [`${where}: must be an object`];\n\n if (!VERDICTS.includes(rec.verdict)) {\n errors.push(`${where}.verdict: must be one of ${VERDICTS.join(\" | \")} (got ${JSON.stringify(rec.verdict)})`);\n return errors;\n }\n const shape = SHAPES[rec.verdict];\n\n // Verdict-specific guidance before the generic unexpected-field message.\n if (rec.verdict !== \"confirmed\" && \"severity\" in rec)\n errors.push(`${where}: severity is only allowed on confirmed findings (verdict is \"${rec.verdict}\")`);\n if (rec.verdict !== \"confirmed\" && \"disposition\" in rec)\n errors.push(`${where}: disposition is only allowed on confirmed findings (a false positive is verdict \"rejected\")`);\n if (rec.verdict !== \"needs_validation\" && (\"blockers\" in rec || \"validation_plan\" in rec))\n errors.push(`${where}: blockers / validation_plan belong only on needs_validation records`);\n if (rec.verdict !== \"rejected\" && \"ruled_out_by\" in rec)\n errors.push(`${where}: ruled_out_by belongs only on rejected records`);\n const handled = new Set([\"severity\", \"disposition\", \"blockers\", \"validation_plan\", \"ruled_out_by\"]);\n const allowed = new Set([...shape.required, ...shape.optional]);\n for (const k of shape.required) if (!(k in rec)) errors.push(`${where}: missing required field \"${k}\" for verdict \"${rec.verdict}\"`);\n for (const k of Object.keys(rec))\n if (!allowed.has(k) && !handled.has(k)) errors.push(`${where}: unexpected field \"${k}\"`);\n\n if (\"fingerprint\" in rec) {\n if (typeof rec.fingerprint !== \"string\" || !FINGERPRINT_RE.test(rec.fingerprint))\n errors.push(`${where}.fingerprint: must be 3-200 chars of [A-Za-z0-9._:/@+-], starting alphanumeric`);\n else if (/:\\d+(?=$|[:/#])/.test(rec.fingerprint))\n errors.push(`${where}.fingerprint: looks like it contains a line number (\":${rec.fingerprint.match(/:(\\d+)(?=$|[:/#])/)[1]}\"); fingerprints identify the root cause and must survive code moving`);\n }\n if (\"skill\" in rec && (typeof rec.skill !== \"string\" || !SKILL_RE.test(rec.skill)))\n errors.push(`${where}.skill: must be a lowercase kebab-case skill name`);\n if (\"title\" in rec) checkText(rec.title, `${where}.title`, errors);\n if (\"cwe\" in rec && (typeof rec.cwe !== \"string\" || !CWE_RE.test(rec.cwe)))\n errors.push(`${where}.cwe: must look like \"CWE-89\"`);\n if (\"owasp_category\" in rec) checkText(rec.owasp_category, `${where}.owasp_category`, errors);\n if (\"ext\" in rec) checkExt(rec.ext, `${where}.ext`, errors);\n if (\"location\" in rec) checkLocation(rec.location, `${where}.location`, errors);\n if (\"trace\" in rec) checkTrace(rec.trace, `${where}.trace`, errors, rec.verdict === \"confirmed\" ? 2 : 0);\n if (\"evidence\" in rec) {\n if (!Array.isArray(rec.evidence)) errors.push(`${where}.evidence: must be an array`);\n else {\n if (rec.verdict === \"confirmed\" && rec.evidence.length === 0) errors.push(`${where}.evidence: confirmed findings need at least one evidence item`);\n rec.evidence.forEach((e, i) => checkFileLineDesc(e, `${where}.evidence[${i}]`, errors));\n }\n }\n if (\"counterevidence\" in rec) checkText(rec.counterevidence, `${where}.counterevidence`, errors);\n if (\"confidence\" in rec) checkEnum(rec.confidence, CONFIDENCE, `${where}.confidence`, errors);\n\n if (rec.verdict === \"confirmed\") {\n if (\"reproduced\" in rec && typeof rec.reproduced !== \"boolean\") errors.push(`${where}.reproduced: must be a boolean`);\n if (rec.confidence === \"high\" && rec.reproduced !== true)\n errors.push(`${where}.confidence: \"high\" requires \"reproduced\": true (a static-only trace caps confidence at medium)`);\n if (\"severity\" in rec) checkSeverity(rec.severity, `${where}.severity`, errors);\n if (\"conditions\" in rec) {\n if (!Array.isArray(rec.conditions)) errors.push(`${where}.conditions: must be an array`);\n else\n rec.conditions.forEach((c, i) => {\n const w = `${where}.conditions[${i}]`;\n if (!isObj(c)) return errors.push(`${w}: must be an object {kind, description}`);\n checkKeys(c, [\"kind\", \"description\"], [], w, errors);\n if (\"kind\" in c) checkEnum(c.kind, CONDITION_KINDS, `${w}.kind`, errors);\n if (\"description\" in c) checkText(c.description, `${w}.description`, errors);\n });\n }\n if (\"disposition\" in rec) checkEnum(rec.disposition, DISPOSITIONS, `${where}.disposition`, errors);\n }\n\n if (rec.verdict === \"needs_validation\") {\n if (\"blockers\" in rec) {\n if (!Array.isArray(rec.blockers) || rec.blockers.length === 0)\n errors.push(`${where}.blockers: needs_validation requires a non-empty array of blockers`);\n else rec.blockers.forEach((b, i) => checkText(b, `${where}.blockers[${i}]`, errors));\n }\n if (\"validation_plan\" in rec) checkText(rec.validation_plan, `${where}.validation_plan`, errors);\n }\n\n if (rec.verdict === \"rejected\" && \"ruled_out_by\" in rec) {\n const r = rec.ruled_out_by;\n const w = `${where}.ruled_out_by`;\n if (!isObj(r)) errors.push(`${w}: must be an object {control, location}`);\n else {\n checkKeys(r, [\"control\", \"location\"], [], w, errors);\n if (\"control\" in r) checkText(r.control, `${w}.control`, errors);\n if (\"location\" in r) checkLocation(r.location, `${w}.location`, errors);\n }\n }\n\n return errors;\n}\n\n/** Validate a parsed findings.json document. Returns an array of error strings (empty = valid). */\nexport function validateFindings(data) {\n if (!Array.isArray(data)) return [\"top level: findings.json must be a JSON array of records\"];\n const errors = [];\n const seen = new Map();\n data.forEach((rec, i) => {\n const fp = isObj(rec) && typeof rec.fingerprint === \"string\" ? rec.fingerprint : null;\n const where = fp ? `[${i}] (${fp})` : `[${i}]`;\n errors.push(...validateRecord(rec, where));\n if (fp) {\n if (seen.has(fp)) errors.push(`${where}: duplicate fingerprint (also at index ${seen.get(fp)})`);\n else seen.set(fp, i);\n }\n });\n for (let i = 1; i \u003c data.length; i++) {\n const a = data[i - 1]?.fingerprint;\n const b = data[i]?.fingerprint;\n if (typeof a === \"string\" && typeof b === \"string\" && a > b) {\n errors.push(`[${i}]: records must be sorted by fingerprint (\"${b}\" must come before \"${a}\"); sort with: jq 'sort_by(.fingerprint)'`);\n break;\n }\n }\n return errors;\n}\n\n/** Read and parse a findings file (\"-\" = stdin). Throws on read/parse failure. */\nexport function readJson(file) {\n const text = readFileSync(file === \"-\" ? 0 : file, \"utf8\");\n return JSON.parse(text);\n}\n\nfunction main(argv) {\n const args = argv.slice(2);\n if (args.length !== 1 || args[0] === \"-h\" || args[0] === \"--help\") {\n console.error(\"usage: node validate-findings.mjs \u003cfindings.json | ->\");\n return 2;\n }\n let data;\n try {\n data = readJson(args[0]);\n } catch (err) {\n console.error(`error: cannot read ${args[0]}: ${err.message}`);\n return 2;\n }\n const errors = validateFindings(data);\n if (errors.length) {\n for (const e of errors) console.error(e);\n console.error(`\\n${args[0]}: INVALID (${errors.length} problem${errors.length === 1 ? \"\" : \"s\"})`);\n return 1;\n }\n const counts = Object.fromEntries(VERDICTS.map((v) => [v, data.filter((r) => r.verdict === v).length]));\n console.log(`${args[0]}: OK (${data.length} records: ${counts.confirmed} confirmed, ${counts.needs_validation} needs_validation, ${counts.rejected} rejected)`);\n return 0;\n}\n\nfunction isMain() {\n try {\n return Boolean(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;\n } catch {\n return false;\n }\n}\n\nif (isMain()) {\n process.exitCode = main(process.argv);\n}\n","content_type":"text/javascript; charset=utf-8","language":"javascript","size":15518,"content_sha256":"e26d295f029294905e83ab991b0618be43b0d7b36063e6f8af2c714446f26cd1"},{"filename":"references/verification.md","content":"# Verification — Per-Finding Refutation Pass\n\nShared reference for audit skills (`owasp-audit`, `api-audit`, `business-logic-testing`, `web-pentest`) and for\n`finding-triage`. Every candidate finding goes through this pass **before** it reaches `findings.json` or a report.\nThe goal is to try to disprove the candidate, then close it with one of three verdicts.\n\nThe pass works because the person checking a finding is not the person who found it. Someone who wrote the candidate\ntends to re-read their own reasoning instead of the code. A verifier that only sees the claim and the cited lines\nreads the code.\n\n## Two stages: fixed exclusions, then the verifier\n\nFalse positives get filtered twice, in this order.\n\n**Stage 1: fixed exclusion rules.** Before any verifier runs, drop the candidates that match a rule below. The rules\nare mechanical and the same on every run, so two runs over the same code drop the same candidates. Apply them by where\nthe code lives and whether it ships, never by how likely the bug looks.\n\n| Rule | Drops a candidate in | Unless |\n|---|---|---|\n| Test code | `test/`, `tests/`, `spec/`, `__tests__/`, `*.test.*`, `*.spec.*`, `*_test.go`, `test_*.py` | Production code imports it, the route or handler is mounted in the deployed build, or the build ships the file |\n| Fixtures and sample data | `fixtures/`, `testdata/`, `__mocks__/`, seed files for local dev | The deployed environment loads it (a production seed, a default config read at boot) |\n| Generated code | Files marked `Code generated ... DO NOT EDIT` or `@generated`, compiled output (`dist/`, `build/`, `.next/`) | Never. Raise it against the generator input or template instead, which is where the fix goes |\n| Vendored and third-party code | `vendor/`, `node_modules/`, `third_party/`, copied-in libraries | The repo has modified the copy. An unmodified copy is a version question for `dependency-audit` |\n| Docs and examples | `*.md`, `docs/`, `examples/`, snippets in comments | The example is deployed, or the repo's own code copies it |\n| Documented hardening note | The gap is already on the run's hardening-notes list | The candidate brings a traced impact the note didn't have. Then it's a new candidate |\n| Prior rejection | The fingerprint matches a `rejected` record from the last run | The code at `ruled_out_by.location` changed. Then it's a new candidate |\n\nSome things are never excluded by location. A credential that looks live is a finding wherever it sits, including a\ntest fixture (`secrets-audit` owns it). CI workflow files run with real tokens, so they are not \"config\". A debug or\ntest route is in scope as soon as the deployed build serves it.\n\nRecord what stage 1 dropped and which rule dropped it: a count per rule in the report, and, where the skill keeps a\ncoverage ledger, an `out_of_scope` unit whose check names the rule. Excluded candidates get no `findings.json` record.\n\n**Stage 2: the per-finding verifier.** Every candidate that survives stage 1 goes through the pass below. The verifier\ndoes not invent new exclusions. \"It's probably test code\" is a stage 1 question with a fixed answer, not a reason a\nverifier can give.\n\nThis repo deliberately does not drop whole bug classes in stage 1. Some filters exclude every denial-of-service,\nrate-limiting or open-redirect report outright. Several skills here report those and calibrate severity instead (see\n`severity-calibration.md`), so class-level filtering would hide real findings.\n\n## When to run it\n\n- After the checklist / hunting phase, once per candidate. Don't batch several candidates into one verifier: a\n verifier that rejects one candidate tends to wave the next one through.\n- **Small targets** (a few thousand lines, a dozen or more candidates): one verifier per candidate can mean 20\n subagents for a 150-line app. There you may group candidates that share no code path, up to about five per\n verifier, with each candidate written up and closed separately. Two candidates stay in separate verifiers when they\n touch the same handler or control, or when either one would be confirmed at High or Critical.\n- Again whenever a verdict would move **up** (rejected → needs_validation, needs_validation → confirmed, or a\n severity increase) or the evidence changes materially. **An upgrade needs a new, independent verifier.** The verifier\n that produced the lower verdict does not get to overturn itself.\n- On re-runs, for every carried-forward confirmed finding whose code changed (see `orchestration.md` where present).\n\nA downgrade (confirmed → needs_validation, or a lower severity) can be applied directly. Record why.\n\n## How to run it\n\n**With subagents** (Claude Code `Task`/Agent tool, or any platform that can spawn an isolated agent): spawn one fresh\nverifier per candidate. Give it **only**:\n\n1. The candidate record: title, claimed location, `trace[]`, `evidence[]`, and the claimed impact\n2. Read access to the repository\n3. The verdict definitions and the \"does NOT rule out\" list below\n4. The record shape it must return (see `findings-format.md` where present)\n\nDo **not** pass the hunter's reasoning, chat history, or the rest of the report.\n\nIn Claude Code with this plugin installed, use the plugin's `finding-verifier` subagent. It carries this procedure, the\nrationalizations table below and the return shape, and it has only `Read`, `Grep` and `Glob`. Give it one candidate per\ncall. Other platforms use the template below.\n\n**Without subagents** (single-context agents, Cursor, Codex): run the pass as a separate, explicit step after hunting\nis finished. For each candidate, start from the cited `file:line` and re-derive the path from scratch. Don't re-read\nyour own earlier notes about the candidate until you have written down a verdict.\n\n### Verifier prompt (template)\n\nAdapt this to the platform. Keep the substance.\n\n```text\nYou are verifying a security finding that someone else wrote. Assume it is wrong until the code proves otherwise.\nYour job is to refute it.\n\nCandidate:\n\u003ccandidate record: title, location, trace[], evidence[], claimed impact>\n\nDo this:\n1. Open every file:line cited in trace[] and evidence[]. Confirm each line says what the candidate claims.\n If a cited line does not exist or says something else, note it.\n2. Walk the path from the entrypoint to the sink. At each hop, list every control that could stop the attack:\n middleware, auth/ownership checks, validation, type coercion, framework escaping, database constraints,\n platform config in the repo.\n3. Name the STRONGEST control you found. For that control, decide: does it run on EVERY path an attacker can reach\n before the sink, at the right time, and fail closed?\n4. Decide whether the claimed impact actually follows. Who is the lower-trust principal, what boundary is crossed,\n and what concrete result occurs?\n5. Return exactly one verdict: confirmed | needs_validation | rejected, with the fields listed for that verdict.\n Fill `counterevidence` with what you tried and what you found, even when the verdict is confirmed.\n```\n\n## The three verdicts\n\n| Verdict | Bar | Required fields | Severity? |\n|---|---|---|---|\n| `confirmed` | A complete trace from attacker-controlled entrypoint → (missing or bypassable control) → sink → concrete impact, **or** a working proof-of-concept against an environment you're authorized to test | `trace[]` (entrypoint first, sink last), `evidence[]`, `counterevidence`, `confidence`, `severity`, `conditions[]` | Yes |\n| `needs_validation` | The code supports the claim, but **one decisive fact lives outside the code**: proxy or CDN behaviour, IdP settings, deploy-time env, a managed-service default, the headers a deployed origin actually sends | `blockers[]` (each names the exact unknown fact), `validation_plan` (a check the owner can run), `counterevidence`, `confidence` | **No.** Severity is assigned only after the blocker resolves to confirmed |\n| `rejected` | A **specific named control, at a specific location**, runs on **every** path an attacker can reach before the sink and fails closed | `ruled_out_by {control, location}`, `counterevidence` | No |\n\n### confirmed\n\n- The trace has to be complete. \"User input reaches `exec()`\" is not a trace until you show where the input enters,\n each hop, and why nothing on the way stops it.\n- `confidence` is honest. **A static-only trace caps at `medium`.** `high` needs a reproduced PoC, a failing test, or\n equivalent runtime evidence, recorded as `\"reproduced\": true` (the validator rejects `high` without it). Missing deployment evidence lowers confidence; it does not lower the severity floor of\n what the code demonstrably does.\n- Severity follows [severity-calibration.md](severity-calibration.md): likelihood × impact, and `overall ≤ impact`. If you can't state the\n concrete damage, the severity is lower than it feels.\n- Record `conditions[]` that limit exploitation (`authentication_level`, `authorization_role`, `user_interaction`,\n `system_configuration`, `network_routing`, `environmental_dependency`, `data_state`, `timing_dependency`,\n `third_party_dependency`).\n\n### needs_validation\n\nThis replaces the old \"Potential\" label. It is **not** a parking place for a hunch. Use it only when the code really does\nsupport the claim and a single external fact decides it.\n\n- Each `blockers[]` entry names one fact: \"Whether Vercel strips `x-forwarded-host` before it reaches the app\",\n not \"deployment config unknown\".\n- `validation_plan` is a check the **owner** can run, local and/or against their deployment, with the expected result\n for each outcome. Example:\n\n ```text\n Run against your production origin: curl -sI https://app.example.com/embed | grep -iE 'x-frame-options|content-security-policy'\n If both `frame-ancestors *` and `X-Frame-Options: DENY` appear → confirmed (contradictory framing policy).\n If only one appears → rejected; ruled out by the deployed header set.\n ```\n\n- If you can't write a blocker and a plan, the candidate is either confirmed (you already know enough) or it's a\n hardening note or a rejected claim. It isn't needs_validation.\n\n### rejected\n\n- Name the control and where it lives: `ruled_out_by: { control: \"requireOrgMember() guard\", location:\n \"src/server/trpc.ts:41\" }`. \"Looks safe\" or \"framework handles it\" is not a rejection.\n- The control must sit on **every** reachable path, run **before** the sink, and **fail closed**.\n- Keep rejected records in `findings.json`. Re-runs read them so the same claim isn't raised again. A rejection\n suppresses **only that exact claim** (same fingerprint). A sibling path or a changed control is a new candidate.\n\nSome rejections have no guard to point at. `ruled_out_by` then names the fact that rules the claim out and\n`location` points at the evidence for it:\n\n| Why it's ruled out | `control` | `location` |\n|---|---|---|\n| Never reached: the vulnerable function or package is never called | \"`_.template` is never called; lodash is used only for `merge` and `pick`\" | The only import or call site of the package. If nothing imports it, the line in the manifest or lockfile that declares it |\n| Known-inert value: a vendor's published example key, a documented test card, a placeholder | \"AWS's documented example access key ID; it can't authenticate\" | The line where the value appears |\n| Control inside a dependency: the library's own guard runs on every path | \"jsonwebtoken 9.0.2 `verify()` rejects `alg: none` when a secret is given\" | The lockfile or manifest line that pins that version |\n\nA control inside a dependency rejects only when the installed version is pinned and that version's behaviour is the\none you checked: read in the vendored or installed source, the changelog, or the advisory's fixed-in version. If the\nrange in the manifest could resolve to a version without the guard and there's no lockfile, it's `needs_validation`\nwith the resolved version as the blocker. \"The library probably handles it\" is not a rejection (see below).\n\n## What does NOT rule out a candidate\n\nNone of these is a reason to reject. Each one is a reason to keep looking:\n\n- **General trust in a library or framework** (\"Prisma parameterizes\", \"React escapes\") without checking that this call\n site uses the safe API. Raw-query escape hatches and `dangerouslySetInnerHTML` exist for a reason.\n- **A control on a different path.** The REST route checks ownership. The server action that writes the same table\n doesn't.\n- **A control that runs at the wrong time.** Validation after the side effect, auth checked at enqueue but not in the\n worker, a signature verified after the body was already stored.\n- **A control that can fail open.** Unset env var interpolation, `catch {}` around the check, a rate limiter that\n allows on Redis error, a feature flag defaulting to off.\n- **A safe sibling.** Three handlers use the guard. That says nothing about the fourth.\n- **Missing information.** \"I couldn't find where this is called\" is a blocker or an unresolved coverage item. It isn't\n a rejection.\n- **Difficulty.** \"Requires guessing a UUID\" or \"requires a race\" lowers likelihood. It doesn't remove the finding.\n- **The setting is operator-configurable.** If the shipped default is unsafe, the default is the finding (see \"Unsafe\n shipped defaults\" below).\n- **\"It's internal.\"** Internal services get reached through SSRF, compromised neighbours, and misrouted ingress.\n Record the network assumption as a `network_routing` condition instead.\n- **Tests pass.** Tests check intended behaviour, not adversarial input.\n\n## Rationalizations to reject\n\nThe list above says what can't close a candidate. This table is for the verifier's own reasoning, in both directions.\nEach row is an argument that feels like a conclusion but isn't one, and the evidence that would actually settle it.\nWhen you notice yourself making one of these arguments, go and get that evidence. If you can't get it, the\nargument doesn't change the verdict.\n\nModels in particular lean toward seeing bugs and toward rating them high. The verifier's default is that the candidate\nis wrong. A verifier can also overcorrect, and wave things through to get to a clean report. Both columns matter.\n\n**Arguments for keeping or inflating a candidate**\n\n| Rationalization | What settles it |\n|---|---|\n| \"This pattern is dangerous, so it's a vulnerability\" | A trace from an attacker-reachable entrypoint to this call site, with the attacker's value still able to do harm when it gets there. A dangerous sink with no reachable input is not a finding |\n| \"An attacker could theoretically…\" | Name the lower-trust principal, the exact input they send, and the line where it enters. Every \"if\" in the story is either shown in the code, recorded as a `conditions[]` entry, or written as a `needs_validation` blocker |\n| \"This is clearly critical\" | The concrete damage, stated in one sentence, and the High/Critical acceptance checks in `severity-calibration.md`. `overall` stays at or below the impact you can show |\n| \"The same bug is in the sibling handler\" | This call site, read on its own. A sibling's bug doesn't transfer, any more than a sibling's guard does |\n| \"It could matter if chained\" | Each link in the chain shown in the code. Any unproven link makes it a blocker or a hardening note |\n| \"There's no input validation here\" | What the sink does with the values that can actually reach it. A missing check on a value that's already typed, bounded or mapped through an allow-list may have no impact |\n| \"The scanner, the report or the code comment says it's vulnerable\" | The code that runs. Claim text, `TODO: fix auth` comments and rule names are leads, not evidence |\n\n**Arguments for dismissing a candidate**\n\n| Rationalization | What settles it |\n|---|---|\n| \"The framework probably escapes this\" | This call site uses the safe API: auto-escaping is on in the config you can see and this output isn't marked raw, or the query goes through the parameterized builder rather than a raw-query method. Name the file and line |\n| \"There's probably a WAF, gateway or proxy in front\" | The rule in the repo or IaC, applied to this route. If it lives outside the repo, it's `needs_validation` with the rule as the blocker. Never `rejected` on \"probably\" |\n| \"It's only internal\" | Network policy, ingress or security-group config in the repo showing no path from the lower-trust principal. Without it, record a `network_routing` condition and keep the verdict |\n| \"The test suite would have caught it\" | A test that sends this adversarial input to this path, asserts it's refused, and runs in CI. Cite its file and line. Even then, it settles only the path it exercises |\n| \"It's validated upstream\" | The validator's file and line, shown to run on every path to this sink, before the sink, and to fail closed. That's the `rejected` bar. Short of it, keep looking |\n| \"The UI never sends that value\" | A server-side check. Clients are attacker-controlled, so what the form allows proves nothing |\n| \"It's behind auth\" | Authentication isn't authorization. Show the ownership or role check, or the finding stands with an `authentication_level` condition |\n| \"Only admins can reach it\" | The role check's location, then ask whether the admin crosses any boundary (another tenant, a higher role, the host). Admin to admin on the admin's own data is no boundary. Admin to another tenant is |\n| \"The IDs are UUIDs, nobody can guess them\" | Whether the IDs leak through list endpoints, URLs, logs, emails or referrers. Difficulty lowers likelihood. It doesn't reject |\n| \"This code looks unused\" | The never-reached row of the table under \"rejected\": no call site, route not mounted. Point at the evidence. \"Looks unused\" alone is a blocker, not a rejection |\n| \"I already rejected something like this\" | Nothing. Each candidate is checked on its own facts. This is why candidates get one verifier each |\n\n## Unsafe shipped defaults\n\nA hard-coded fallback secret (`process.env.JWT_SECRET || \"changeme\"`), a debug flag that defaults on, or a permissive\ndefault config is **`confirmed`**, not `needs_validation`, whichever skill finds it. The code shows what happens\nwhenever the setting is missing, and missing is the state a new deployment starts in.\n\n- Record a `system_configuration` condition: \"deployments that don't set `JWT_SECRET`, or set it empty\".\n- Rate severity by what the default gives an attacker, as if it were live. Whether production overrides it is a\n deployment fact you can't see, so it caps `confidence` at `medium`. It doesn't lower the verdict or the severity.\n- Put the owner's check (is the variable set, and to what length) in the finding's fix-verification text, not in a\n `validation_plan`.\n- If you *can* see the deployment and it sets a strong value, the record is still `confirmed`, since a fresh deploy\n or a new environment gets the default. Rate likelihood lower and say why.\n\nThe same root cause should get the same verdict and severity from every skill that sees it. A hard-coded JWT fallback\nthat lets anyone forge an admin token is an authentication bypass to any account, wherever it's reported.\n\n## Hardening notes are not verdicts\n\nIf the verifier finds the gap is real but there's **no traced impact** (Layer A already stops the attack and Layer B is\nmissing; a missing header with no exploit path; a checklist deviation), close the candidate as a **Hardening note**. It\ngoes in the report's \"Hardening notes (not findings)\" section with no severity. It gets no `findings.json` record.\nKeep hardening notes in a short list next to the JSON (for example `hardening-notes.md` in the audit output directory)\nso the report can render them. The coverage unit that produced the note can still be `covered`.\n\nIf a hardening note later picks up a traced impact (say, a missing CSP turns out to matter because a stored XSS sink\nexists), it becomes a new candidate and goes through this pass.\n\n## Live probing\n\nLive checks stay allowed **when authorized**. For example, `curl -I` against the user's own deployed origin, or\nconfirming that a leaked key is live as part of secrets triage. The rules:\n\n1. The authorization check of the consuming skill applies to the live target, not just the source. Only probe hosts\n the user owns or is authorized to test, and only with non-destructive requests (`HEAD`/`GET`, no state changes, no\n real customer credentials).\n2. **A check you describe but don't run is a `needs_validation` item.** Write the exact command into\n `validation_plan`. Don't mark it confirmed or rejected on the strength of what the config file \"should\" produce.\n3. A check you actually ran becomes evidence. Record the command, timestamp, and relevant response lines in\n `evidence[]` / `counterevidence`, then close the verdict normally.\n4. For paired evidence (baseline request next to attack request), see `web-pentest`.\n\n## Output of the pass\n\nFor each candidate, one record with the verdict-specific fields above, ready to be written to `findings.json`. The\nconsuming skill writes `findings.json`, validates it (`validate-findings.mjs` where present), and renders the report\nfrom it. Prose never changes a verdict, severity, blocker, or demonstrated impact. If the report and the JSON disagree,\nthe JSON wins and the report is regenerated.\n\n## Sources\n\nParaphrased and adapted, not copied:\n\n- Cloudflare security-audit-skill (MIT): [VALIDATION-AND-REPORTING.md](https://github.com/cloudflare/security-audit-skill/blob/main/skills/security-audit/VALIDATION-AND-REPORTING.md)\n (candidate verifier, re-verification for upgrades), [HUNTING.md](https://github.com/cloudflare/security-audit-skill/blob/main/skills/security-audit/HUNTING.md)\n (candidate gate), [README](https://github.com/cloudflare/security-audit-skill) (independent verifier principle)\n- Strix (Apache-2.0): [analysis/counterevidence.md](https://github.com/usestrix/strix/blob/main/strix/skills/analysis/counterevidence.md)\n (what does and doesn't rule out a claim), [tools/reporting/tool.py](https://github.com/usestrix/strix/blob/main/strix/tools/reporting/tool.py)\n (required counterevidence and confidence fields)\n- Trail of Bits skills (CC BY-SA 4.0): [fp-check](https://github.com/trailofbits/skills/tree/main/plugins/fp-check)\n (the \"rationalizations to reject\" idea and its framing that models lean toward finding bugs and over-rating them;\n read-only verifier agents). The table above is this repo's own wording and covers both directions\n- anthropics/claude-code-security-review (MIT): [findings_filter.py](https://github.com/anthropics/claude-code-security-review/blob/main/claudecode/findings_filter.py)\n (hard exclusion rules first, then a model-based filter per finding). This repo excludes by location only, not by bug class\n- This repo's decision on live probing (issue #78): kept, when authorized, and recorded as needs_validation unless run.\n This deliberately departs from Cloudflare's no-live-probing rule.\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":22974,"content_sha256":"62062060314072a4959b8ee307813a95a76d582cccabd9c39f16d7dd2bd0c17e"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Dependency Audit — Framework, Package, and Toolchain Security","type":"text"}]},{"type":"paragraph","content":[{"text":"Audit project dependencies, frameworks, language runtimes, and dev tools for known vulnerabilities (CVEs), security anti-patterns, and supply chain risks.","type":"text"}]},{"type":"paragraph","content":[{"text":"Cross-references: ","type":"text"},{"text":"patch-sweep","type":"text","marks":[{"type":"code_inline"}]},{"text":" for patching the same advisory across many repos; ","type":"text"},{"text":"scanner-pipeline","type":"text","marks":[{"type":"code_inline"}]},{"text":" for running osv-scanner / trivy in CI across every lockfile at once; ","type":"text"},{"text":"agent-supply-chain-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" for reviewing the content of MCP servers, skills, and plugins this skill inventories; ","type":"text"},{"text":"cloud-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" \"PaaS\" for platform runtime and config checks behind ","type":"text"},{"text":"Mitigated (platform/config)","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"secrets-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" for tokens found in ","type":"text"},{"text":".npmrc","type":"text","marks":[{"type":"code_inline"}]},{"text":" or CI; ","type":"text"},{"text":"iam-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the cloud roles CI assumes via OIDC; ","type":"text"},{"text":"cicd-security","type":"text","marks":[{"type":"code_inline"}]},{"text":" for workflow and runner security; ","type":"text"},{"text":"finding-triage","type":"text","marks":[{"type":"code_inline"}]},{"text":" for dispositioning individual findings.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Authorization","type":"text"}]},{"type":"paragraph","content":[{"text":"Confirm the user owns or is authorized to audit the repo, CI system, and registries before running anything. Audit tools are read-only, but fix commands (","type":"text"},{"text":"audit fix","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"bundle update","type":"text","marks":[{"type":"code_inline"}]},{"text":") rewrite lockfiles. Run them only when asked, on a branch, never on the production branch directly.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Methodology","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 0: Confirm What Ships","type":"text"}]},{"type":"paragraph","content":[{"text":"Before inventorying anything, confirm you are auditing the code that is actually deployed:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Production branch:","type":"text","marks":[{"type":"strong"}]},{"text":" the branch the platform or deploy workflow builds from (Vercel/Netlify production branch, Heroku pipeline, ","type":"text"},{"text":"on: push: branches:","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the deploy workflow). It is not always ","type":"text"},{"text":"main","type":"text","marks":[{"type":"code_inline"}]},{"text":". In one field sweep, ","type":"text"},{"text":"main","type":"text","marks":[{"type":"code_inline"}]},{"text":" held only a README.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Monorepos:","type":"text","marks":[{"type":"strong"}]},{"text":" which app or package is deployed, and its lockfile or workspace slice.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Package manager:","type":"text","marks":[{"type":"strong"}]},{"text":" which lockfile is committed and the ","type":"text"},{"text":"packageManager","type":"text","marks":[{"type":"code_inline"}]},{"text":" field. This decides the audit command in Step 2.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"If the deployed branch can't be confirmed from the repo, say which branch you audited and mark it ","type":"text"},{"text":"needs_validation","type":"text","marks":[{"type":"code_inline"}]},{"text":". Details: ","type":"text"},{"text":"references/fixing-safely.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/fixing-safely.md","title":null}}]},{"text":" §10.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 1: Inventory the Stack","type":"text"}]},{"type":"paragraph","content":[{"text":"Identify everything in use — not just direct dependencies but the full chain:","type":"text"}]},{"type":"paragraph","content":[{"text":"Package manifests — read and catalog:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"Node/JS: package.json, package-lock.json, yarn.lock, pnpm-lock.yaml\nPython: requirements.txt, Pipfile.lock, pyproject.toml, poetry.lock\nRuby: Gemfile, Gemfile.lock\nGo: go.mod, go.sum\nRust: Cargo.toml, Cargo.lock\nJava: pom.xml, build.gradle\nPHP: composer.json, composer.lock\n.NET: *.csproj, packages.config","type":"text"}]},{"type":"paragraph","content":[{"text":"Framework and runtime versions:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check framework version (Next.js, Django, Rails, Spring, Laravel, Express, etc.)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check language/runtime version (Node.js, Python, Ruby, Go, Java, PHP, .NET)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check infrastructure tools (Docker base images, Terraform providers, Kubernetes versions)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Dev tools and CI/CD:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check CI/CD pipeline configs (.github/workflows, .gitlab-ci.yml, Jenkinsfile)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check pre-commit hooks, linters, formatters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check container base images and their update status","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check IaC tool versions (Terraform, Pulumi, CDK)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Agent extensions","type":"text","marks":[{"type":"strong"}]},{"text":" (code a coding agent loads and runs at launch):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MCP server configs: ","type":"text"},{"text":".mcp.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".cursor/mcp.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".vscode/mcp.json","type":"text","marks":[{"type":"code_inline"}]},{"text":". Record each server's ","type":"text"},{"text":"command","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"args","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or ","type":"text"},{"text":"url","type":"text","marks":[{"type":"code_inline"}]},{"text":" for remote servers) and whether the version is pinned.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Installed skills and agent rules: ","type":"text"},{"text":"SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" folders, ","type":"text"},{"text":".claude/agents/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".claude/commands/","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".cursor/rules/*.mdc","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"AGENTS.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Plugin marketplaces and enabled plugins: ","type":"text"},{"text":"extraKnownMarketplaces","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"enabledPlugins","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"enableAllProjectMcpServers","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":".claude/settings*.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inventory and pinning only here. Defer review of what the extension tells the agent to do to ","type":"text"},{"text":"agent-supply-chain-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":". Greps: ","type":"text"},{"text":"references/source-redirection.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/source-redirection.md","title":null}}]},{"text":" §2.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Edge cases in package manifests:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"optionalDependencies","type":"text","marks":[{"type":"code_inline"}]},{"text":" — installed but not audited by default","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"peerDependencies","type":"text","marks":[{"type":"code_inline"}]},{"text":" — version range may not match what's installed","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"overrides","type":"text","marks":[{"type":"code_inline"}]},{"text":" / yarn ","type":"text"},{"text":"resolutions","type":"text","marks":[{"type":"code_inline"}]},{"text":" / pnpm ","type":"text"},{"text":"overrides","type":"text","marks":[{"type":"code_inline"}]},{"text":" — check if used to ","type":"text"},{"text":"silence","type":"text","marks":[{"type":"em"}]},{"text":" advisories rather than fix them","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Monorepos: read every ","type":"text"},{"text":"packages/*/package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"apps/*/package.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", not just the root","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"engines","type":"text","marks":[{"type":"code_inline"}]},{"text":" field — older-than-LTS Node makes other audits moot","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2: Run Automated Audit Tools","type":"text"}]},{"type":"paragraph","content":[{"text":"Run the appropriate audit command for the project:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Node.js\nnpm audit --json # full structured output\nnpm audit --omit=dev --json # production-only: filters dev/build-time vulns\n# Compare the two — vulns only in dev/build tooling do not ship to users\n# and should be triaged as lower priority. Don't bury this in the report.\n\n# pnpm / yarn — use their own audit; npm audit in a pnpm or yarn repo has no\n# package-lock.json and fails or audits the wrong tree without a clear error\npnpm audit --prod --json # use the pnpm major matching the lockfile (corepack)\nyarn audit --groups dependencies --json # yarn v1\nyarn npm audit --environment production -R # yarn Berry (v2+)\n\n# Python\npip audit # If pip-audit installed\nsafety check # If safety installed\n\n# Ruby\nbundle audit\n\n# Go\ngovulncheck ./...\n\n# Rust\ncargo audit\n\n# PHP\ncomposer audit\n\n# .NET\ndotnet list package --vulnerable\n\n# Docker\ndocker scout cves \u003cimage>\ntrivy image \u003cimage>\n\n# General (if Trivy is available)\ntrivy fs .","type":"text"}]},{"type":"paragraph","content":[{"text":"Applying fixes — read before you ","type":"text","marks":[{"type":"strong"}]},{"text":"--force","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":":","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"npm audit fix # safe: upgrades within stated ranges\nnpm audit fix --dry-run --force # ALWAYS dry-run first\nnpm audit fix --force # only after reviewing the dry-run","type":"text"}]},{"type":"paragraph","content":[{"text":"npm audit fix --force","type":"text","marks":[{"type":"code_inline"}]},{"text":" can resolve an advisory by DOWNGRADING a package to an older version that doesn't trigger the audit signature. This is almost always wrong (e.g. downgrading ","type":"text"},{"text":"next@16","type":"text","marks":[{"type":"code_inline"}]},{"text":" to ","type":"text"},{"text":"next@9","type":"text","marks":[{"type":"code_inline"}]},{"text":" to \"fix\" a transitive postcss CVE). Inspect dry-run output for \"Will install X@Y, which is a breaking change\" — that's the tool trying to downgrade.","type":"text"}]},{"type":"paragraph","content":[{"text":"When ","type":"text","marks":[{"type":"strong"}]},{"text":"npm audit fix","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" cannot resolve an advisory","type":"text","marks":[{"type":"strong"}]},{"text":" (transitive dep pinned by an upstream package):","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Determine reachability","type":"text","marks":[{"type":"strong"}]},{"text":" — is the vulnerable code path actually invoked in your usage? ","type":"text"},{"text":"npm ls \u003cpackage>","type":"text","marks":[{"type":"code_inline"}]},{"text":" + reading the parent's source can rule it out as unreachable.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Consider a ","type":"text","marks":[{"type":"strong"}]},{"text":"package.json","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" ","type":"text","marks":[{"type":"strong"}]},{"text":"overrides","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" pin","type":"text","marks":[{"type":"strong"}]},{"text":" to a patched version (test thoroughly — overrides can break the parent).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Consider swapping the parent provider entirely.","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If none apply:","type":"text","marks":[{"type":"strong"}]},{"text":" document explicitly, track upstream, and note in the audit report rather than silently dropping the finding.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Package-manager traps that pass the build but break at runtime","type":"text","marks":[{"type":"strong"}]},{"text":" (open-ended ","type":"text"},{"text":">=","type":"text","marks":[{"type":"code_inline"}]},{"text":" overrides from ","type":"text"},{"text":"pnpm audit --fix","type":"text","marks":[{"type":"code_inline"}]},{"text":", duplicated singletons, exact pins in a parent blocking the fix, a raised ","type":"text"},{"text":"engines","type":"text","marks":[{"type":"code_inline"}]},{"text":" floor): ","type":"text"},{"text":"references/fixing-safely.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/fixing-safely.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Verify after fixing","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"checklist with commands","type":"text","marks":[{"type":"link","attrs":{"href":"references/fixing-safely.md","title":null}}]},{"text":"): advisory gone on re-audit, not suppressed; no downgrades or major jumps in the lockfile diff; no duplicate singletons; engines floor met; clean frozen install; full tests, then a preview smoke test of the features those packages power.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 2b: Enrich with Exploitation Signals","type":"text"}]},{"type":"paragraph","content":[{"text":"For every advisory, check ","type":"text"},{"text":"CISA KEV","type":"text","marks":[{"type":"strong"}]},{"text":" (exploited now) and ","type":"text"},{"text":"EPSS","type":"text","marks":[{"type":"strong"}]},{"text":" (likely in 30 days), via its CVE id (OSV ","type":"text"},{"text":"aliases","type":"text","marks":[{"type":"code_inline"}]},{"text":" maps a GHSA id). They set the order of work, never severity above impact. Offline: ","type":"text"},{"text":"not checked","type":"text","marks":[{"type":"code_inline"}]},{"text":"; never infer KEV from memory. No scanner? Use the OSV batch API. Commands, fallback, ","type":"text"},{"text":"ext","type":"text","marks":[{"type":"code_inline"}]},{"text":" keys: ","type":"text"},{"text":"references/exploitation-signals.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/exploitation-signals.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 3: Research Framework-Specific Known Issues","type":"text"}]},{"type":"paragraph","content":[{"text":"Beyond CVEs in packages, check for known vulnerability patterns specific to the framework in use. Search for recent advisories and common misconfiguration issues. Runtime defaults that widen a kernel CVE's reach (Go ≥ 1.24 listens with MPTCP) are in ","type":"text"},{"text":"references/runtime-exposure.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/runtime-exposure.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"For every direct dependency, cross-reference the installed version against:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"https://github.com/advisories?ecosystem=npm&query=\u003cpackage>","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"https://github.com/\u003corg>/\u003crepo>/security/advisories","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"paragraph","content":[{"text":"The framework-specific patterns below cover ","type":"text"},{"text":"evergreen","type":"text","marks":[{"type":"em"}]},{"text":" anti-patterns (mass assignment, debug-in-prod, etc.). Recent CVEs need a fresh check because hardcoded advisory lists rot fast and LLM training data is often 6+ months behind the latest.","type":"text"}]},{"type":"paragraph","content":[{"text":"Runtime and framework upgrades","type":"text","marks":[{"type":"strong"}]},{"text":" (detail in ","type":"text"},{"text":"references/fixing-safely.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/fixing-safely.md","title":null}}]},{"text":" §6–9):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Move every runtime pin together.","type":"text","marks":[{"type":"strong"}]},{"text":" Ruby: ","type":"text"},{"text":".ruby-version","type":"text","marks":[{"type":"code_inline"}]},{"text":", Gemfile ","type":"text"},{"text":"ruby","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Gemfile.lock","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"RUBY VERSION","type":"text","marks":[{"type":"code_inline"}]},{"text":", Dockerfile ","type":"text"},{"text":"ARG","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"FROM","type":"text","marks":[{"type":"code_inline"}]},{"text":", CI image, buildpack/stack. Node: ","type":"text"},{"text":".nvmrc","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".node-version","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"engines","type":"text","marks":[{"type":"code_inline"}]},{"text":", Dockerfile, CI ","type":"text"},{"text":"node-version","type":"text","marks":[{"type":"code_inline"}]},{"text":", platform setting. One missed pin keeps the vulnerable runtime running there.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ruby gotchas:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"bundle update --conservative \u003cgem>","type":"text","marks":[{"type":"code_inline"}]},{"text":" can still jump a shared dependency a major (","type":"text"},{"text":"json","type":"text","marks":[{"type":"code_inline"}]},{"text":" 2→3 broke ","type":"text"},{"text":"httparty","type":"text","marks":[{"type":"code_inline"}]},{"text":" at runtime), so constrain it (","type":"text"},{"text":"~> 2.21","type":"text","marks":[{"type":"code_inline"}]},{"text":"). To pin without a permanent Gemfile change, tighten the constraint, resolve, then restore it; Bundler keeps the locked version. Brakeman fails CI when it isn't the latest release, so expect a red patch PR. Patch-level Rails releases can still break specs.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No in-place fix exists","type":"text","marks":[{"type":"strong"}]},{"text":" when the framework or runtime line gets no backport (an out-of-support Next.js major, an EOL Ruby minor). Report it as ","type":"text"},{"text":"Requires major migration","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the minimum fixed version and an effort estimate, not next to one-command fixes.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Platform or config mitigation","type":"text","marks":[{"type":"strong"}]},{"text":" can be the right answer: a config flag that blocks the vulnerable path (for example, libvips ","type":"text"},{"text":"VIPS_BLOCK_UNTRUSTED=1","type":"text","marks":[{"type":"code_inline"}]},{"text":", which only exists in libvips ≥ 8.13), or a managed runtime that patches itself (Vercel-managed Node). Record it as ","type":"text"},{"text":"Mitigated (platform/config)","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the control, where it's set, the version prerequisite, and how it was verified. Unverified platform state is ","type":"text"},{"text":"needs_validation","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Next.js / React:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Server Actions exposing internal endpoints (pre-14.1.1 middleware bypass CVE-2025-29927)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"dangerouslySetInnerHTML","type":"text","marks":[{"type":"code_inline"}]},{"text":" without sanitization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SSRF through image optimization (","type":"text"},{"text":"next/image","type":"text","marks":[{"type":"code_inline"}]},{"text":" with unrestricted domains)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exposed ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" files in public directory or client bundle (","type":"text"},{"text":"NEXT_PUBLIC_","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix leaking secrets)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Middleware auth bypass patterns — check middleware.ts matches all protected routes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Server Component / Client Component boundary leaking server-only data:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Any module reading ","type":"text"},{"text":"process.env.SECRET","type":"text","marks":[{"type":"code_inline"}]},{"text":" or instantiating a DB client should start with ","type":"text"},{"text":"import \"server-only\";","type":"text","marks":[{"type":"code_inline"}]},{"text":" — fails the build if imported from a Client Component","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Grep for: files in ","type":"text"},{"text":"lib/","type":"text","marks":[{"type":"code_inline"}]},{"text":" that touch ","type":"text"},{"text":"process.env.[A-Z_]+","type":"text","marks":[{"type":"code_inline"}]},{"text":" but do NOT import ","type":"text"},{"text":"server-only","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Inverse check: any file with ","type":"text"},{"text":"\"use client\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" importing from such a module is a leak","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Outdated ","type":"text"},{"text":"next.config.js","type":"text","marks":[{"type":"code_inline"}]},{"text":" security headers","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Django:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"DEBUG=True in production","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ALLOWED_HOSTS misconfigured (wildcard ","type":"text"},{"text":"*","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing CSRF middleware or ","type":"text"},{"text":"@csrf_exempt","type":"text","marks":[{"type":"code_inline"}]},{"text":" on state-changing views","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Raw SQL via ","type":"text"},{"text":"extra()","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"raw()","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"RawSQL","type":"text","marks":[{"type":"code_inline"}]},{"text":" without parameterization","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pickle deserialization in sessions (use JSON serializer)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Secret key committed to source control","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Rails:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mass assignment without strong parameters","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SQL injection via ","type":"text"},{"text":"where(\"column = '#{input}'\")","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unpatched Action Pack, Action View, or Active Record CVEs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Insecure deserialization in cookies (verify secret_key_base rotation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CSRF token bypass in API-only mode","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Express / Node.js:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Prototype pollution through ","type":"text"},{"text":"Object.assign","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"lodash.merge","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"deep-extend","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"ReDoS (Regular Expression Denial of Service) in validation patterns","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Path traversal through ","type":"text"},{"text":"req.params","type":"text","marks":[{"type":"code_inline"}]},{"text":" in file serving routes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing rate limiting on auth endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"eval()","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"Function()","type":"text","marks":[{"type":"code_inline"}]},{"text":" with user input","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Event loop blocking with synchronous operations","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Serverless / edge runtimes (Vercel, Lambda, Cloud Run, Workers):","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"In-memory state ≠ rate limit.","type":"text","marks":[{"type":"strong"}]},{"text":" A module-scoped ","type":"text"},{"text":"Map","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"Set","type":"text","marks":[{"type":"code_inline"}]},{"text":" for rate limiting, sessions, or caches is per-instance. Cold starts reset state; load spreads across instances; attackers bypass trivially.","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Grep for: ","type":"text"},{"text":"const rateLimitMap = new Map","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"const cache = new Map","type":"text","marks":[{"type":"code_inline"}]},{"text":" in server-action / API-route files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Fix: shared store — Vercel KV, Upstash Ratelimit, Redis, DynamoDB","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unbounded in-memory collections","type":"text","marks":[{"type":"strong"}]},{"text":" leak memory under traffic. Cap size and evict (LRU or FIFO).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"x-forwarded-for","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" trust:","type":"text","marks":[{"type":"strong"}]},{"text":" only trustworthy when the edge overwrites it. Behind misconfigured proxy chains it's attacker-spoofable. A fallback to a single ","type":"text"},{"text":"\"unknown\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" bucket throttles all anonymous traffic together; random-fallback silently disables the limit.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Spring / Java:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Spring4Shell and related RCE vulnerabilities","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Deserialization attacks (Java native serialization, Jackson polymorphic types)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SpEL injection in Spring Expression Language","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing CSRF protection on state-changing endpoints","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Actuator endpoints exposed without authentication","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Laravel / PHP:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"APP_DEBUG=true in production (leaks env vars in error pages)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SQL injection via raw DB queries without bindings","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mass assignment without ","type":"text"},{"text":"$fillable","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"$guarded","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"File upload without type validation (PHP execution via uploaded .php)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Insecure deserialization in queued jobs","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"WordPress:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Outdated core, theme, or plugin versions (most common attack vector)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"File editor enabled in wp-admin (allows code injection if admin is compromised)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"XML-RPC enabled (brute force amplification, SSRF)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Default admin username, weak passwords","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unpatched plugin vulnerabilities (check WPScan database)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 4: Check for Supply Chain Risks","type":"text"}]},{"type":"paragraph","content":[{"text":"Beyond known CVEs, look for supply chain attack indicators:","type":"text"}]},{"type":"paragraph","content":[{"text":"Dependency confusion / substitution:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Private package names that could be claimed on public registries","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing ","type":"text"},{"text":".npmrc","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"pip.conf","type":"text","marks":[{"type":"code_inline"}]},{"text":" scoping to private registry","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"No lockfile integrity verification","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Package-source redirection","type":"text","marks":[{"type":"strong"}]},{"text":" — any config, CLI call, or env var that adds or replaces where packages come from. Full per-ecosystem table and greps in ","type":"text"},{"text":"references/source-redirection.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/source-redirection.md","title":null}}]},{"text":" §1:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Ecosystem","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Surfaces to check","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"npm / pnpm","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".npmrc","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"registry=","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"@scope:registry=","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"npm config set registry","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"--registry","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"NPM_CONFIG_REGISTRY","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"npm_config_registry","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Yarn","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".yarnrc","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"registry","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":".yarnrc.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"npmRegistryServer","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"npmScopes.*.npmRegistryServer","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"unsafeHttpWhitelist","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"YARN_NPM_REGISTRY_SERVER","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pip / uv","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pip.conf","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"pip.ini","type":"text","marks":[{"type":"code_inline"}]},{"text":" / requirements ","type":"text"},{"text":"--index-url","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--extra-index-url","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"--trusted-host","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"PIP_INDEX_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"PIP_EXTRA_INDEX_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"UV_INDEX_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"UV_EXTRA_INDEX_URL","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Poetry","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"[[tool.poetry.source]]","type":"text","marks":[{"type":"code_inline"}]},{"text":" and its ","type":"text"},{"text":"priority","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"poetry source add","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"POETRY_REPOSITORIES_\u003cNAME>_URL","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Maven / Gradle","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"settings.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"\u003cmirrors>","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"\u003cmirrorOf>","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"pom.xml","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"\u003crepositories>","type":"text","marks":[{"type":"code_inline"}]},{"text":"; Gradle ","type":"text"},{"text":"repositories { maven { url } }","type":"text","marks":[{"type":"code_inline"}]},{"text":", init scripts","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cargo","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":".cargo/config.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"[source.crates-io] replace-with","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"[registries.*]","type":"text","marks":[{"type":"code_inline"}]},{"text":"; ","type":"text"},{"text":"CARGO_REGISTRIES_\u003cNAME>_INDEX","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"CARGO_REGISTRY_DEFAULT","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"grep -rnE \"registry\\s*=|npmRegistryServer|(extra-)?index-url|trusted-host|\u003cmirrorOf>|replace-with|NPM_CONFIG_REGISTRY|PIP_(EXTRA_)?INDEX_URL|CARGO_REGISTRIES_|poetry source add\" \\\n .npmrc .yarnrc .yarnrc.yml pip.conf pyproject.toml requirements*.txt settings.xml pom.xml .cargo/ .github/ Dockerfile* scripts/ 2>/dev/null","type":"text"}]},{"type":"paragraph","content":[{"text":"PIP_EXTRA_INDEX_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"--extra-index-url","type":"text","marks":[{"type":"code_inline"}]},{"text":" is the classic dependency-confusion setup: pip queries every index and installs the highest version from any of them, so a public package with a private package's name and a higher version wins. A default-registry replacement pointing at a host the org doesn't control, over ","type":"text"},{"text":"http://","type":"text","marks":[{"type":"code_inline"}]},{"text":", or added in the PR under review is a candidate finding.","type":"text"}]},{"type":"paragraph","content":[{"text":"Agent extensions (unpinned / unreviewed):","type":"text","marks":[{"type":"strong"}]},{"text":" MCP server commands from the Step 1 inventory that fetch the latest version on every launch:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# npx -y pkg without @x.y.z, uvx/pipx pkg without ==, docker run image without a tag or digest\ngrep -nE '\"(-y|--yes)\",\\s*\"(@[a-z0-9._-]+/)?[a-z0-9._-]+(@latest)?\"' .mcp.json .cursor/mcp.json .vscode/mcp.json 2>/dev/null\ngrep -nE '\"(uvx|pipx)\"' -A3 .mcp.json .cursor/mcp.json 2>/dev/null | grep -vE '==[0-9]'\ngrep -nE '\"docker\"' -A8 .mcp.json .cursor/mcp.json 2>/dev/null | grep -vE ':[0-9][^\"]*\"|@sha256:'","type":"text"}]},{"type":"paragraph","content":[{"text":"Pinned looks like ","type":"text"},{"text":"[email protected]","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"tool==0.9.1","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"image@sha256:...","type":"text","marks":[{"type":"code_inline"}]},{"text":". Hand each unpinned or unreviewed extension to ","type":"text"},{"text":"agent-supply-chain-audit","type":"text","marks":[{"type":"code_inline"}]},{"text":" for content review.","type":"text"}]},{"type":"paragraph","content":[{"text":"Shipped bytecode:","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":".pyc","type":"text","marks":[{"type":"code_inline"}]},{"text":" files tracked in git or packaged without a matching ","type":"text"},{"text":".py","type":"text","marks":[{"type":"code_inline"}]},{"text":" can run code nobody reviewed. ","type":"text"},{"text":"git ls-files | grep -E '\\.py[co]$|__pycache__/'","type":"text","marks":[{"type":"code_inline"}]},{"text":", plus the no-matching-source check in ","type":"text"},{"text":"references/source-redirection.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/source-redirection.md","title":null}}]},{"text":" §3.","type":"text"}]},{"type":"paragraph","content":[{"text":"Typosquatting:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Package names that are close misspellings of popular packages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Recently published packages with very few downloads","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Packages that changed ownership recently","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Malicious packages:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Postinstall scripts that make network requests or execute code (","type":"text"},{"text":"scripts.postinstall","type":"text","marks":[{"type":"code_inline"}]},{"text":" in package.json)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Packages with obfuscated code","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Excessive permission requests relative to functionality","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Maintenance risk:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unmaintained packages (no commits in 2+ years, archived repos)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Single-maintainer packages for critical functionality","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Packages with known but unpatched vulnerabilities (maintainer unresponsive)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Lockfile integrity:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Lockfile committed? ","type":"text"},{"text":"git ls-files | grep -E 'package-lock\\.json|yarn\\.lock|pnpm-lock\\.yaml|Gemfile\\.lock|poetry\\.lock|composer\\.lock|Cargo\\.lock|go\\.sum'","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CI installs from lockfile?","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Check ","type":"text"},{"text":".github/workflows/*.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":".gitlab-ci.yml","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Jenkinsfile","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"vercel.json","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"netlify.toml","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Dockerfile","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"npm install","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bad) vs ","type":"text"},{"text":"npm ci","type":"text","marks":[{"type":"code_inline"}]},{"text":" (good); ","type":"text"},{"text":"yarn install","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bad) vs ","type":"text"},{"text":"yarn install --immutable","type":"text","marks":[{"type":"code_inline"}]},{"text":" (good); ","type":"text"},{"text":"pip install -r","type":"text","marks":[{"type":"code_inline"}]},{"text":" (bad) vs ","type":"text"},{"text":"pip install --require-hashes -r","type":"text","marks":[{"type":"code_inline"}]},{"text":" (good)","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"integrity","type":"text","marks":[{"type":"code_inline"}]},{"text":" hashes present in the lockfile? (modern npm/pnpm yes by default)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 5: Check Dev Tool and CI/CD Security","type":"text"}]},{"type":"paragraph","content":[{"text":"GitHub Actions and other CI:","type":"text","marks":[{"type":"strong"}]},{"text":" workflow security (pull_request_target, script injection, token permissions, SHA pinning, fork secrets, self-hosted runners, agents in CI) → ","type":"text"},{"text":"cicd-security","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Docker:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Running as root in container (missing ","type":"text"},{"text":"USER","type":"text","marks":[{"type":"code_inline"}]},{"text":" directive)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Base image with known CVEs (check with ","type":"text"},{"text":"trivy","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"docker scout","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Secrets baked into image layers (visible via ","type":"text"},{"text":"docker history","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"latest","type":"text","marks":[{"type":"code_inline"}]},{"text":" tag instead of pinned version","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Terraform / IaC:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hardcoded secrets in ","type":"text"},{"text":".tf","type":"text","marks":[{"type":"code_inline"}]},{"text":" files","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unpinned provider versions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Missing state file encryption","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Over-permissive IAM in provider configuration","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Step 5b: CI and Release Trust Handoffs","type":"text"}]},{"type":"paragraph","content":[{"text":"CI configuration is authorization code: it decides who can run what, with which credentials, against which targets. Most pipeline compromises go through a handoff, where a lower-trust job writes something a higher-trust job later restores, executes, or ships. Check each handoff below. Greps, validation rules, and reporting fields are in ","type":"text"},{"text":"references/ci-release.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ci-release.md","title":null}}]},{"text":". Handoffs 1–4, 7 and 8 belong to ","type":"text"},{"text":"cicd-security","type":"text","marks":[{"type":"code_inline"}]},{"text":"; this step keeps 5 and 6.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"#","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Handoff","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Starting grep (","type":"text"},{"text":".github/workflows/*.y*ml","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"1","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"workflow_run","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"pull_request_target","type":"text","marks":[{"type":"code_inline"}]},{"text":" restoring artifacts or caches into a privileged job","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -lE \"workflow_run|pull_request_target\" … | xargs grep -nE \"download-artifact|actions/cache|gh run download\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"2","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"actions/cache","type":"text","marks":[{"type":"code_inline"}]},{"text":" keys or ","type":"text"},{"text":"restore-keys","type":"text","marks":[{"type":"code_inline"}]},{"text":" an untrusted PR run can write","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -nE \"actions/cache|restore-keys:|cache:\\s*(npm|yarn|pnpm|pip)\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"3","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"permissions:","type":"text","marks":[{"type":"code_inline"}]},{"text":" missing or ","type":"text"},{"text":"write-all","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"combined with","type":"text","marks":[{"type":"strong"}]},{"text":" a privileged step an untrusted actor can reach","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -L \"permissions:\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"grep -nE \"write-all|:\\s*write\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"4","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OIDC ","type":"text"},{"text":"id-token: write","type":"text","marks":[{"type":"code_inline"}]},{"text":" plus a cloud trust policy whose ","type":"text"},{"text":"sub","type":"text","marks":[{"type":"code_inline"}]},{"text":" condition is a wildcard or missing","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -nE \"id-token:\\s*write|role-to-assume\"","type":"text","marks":[{"type":"code_inline"}]},{"text":"; IaC: ","type":"text"},{"text":"grep -rnE \"githubusercontent\\.com:sub|repo:[^\\\"]*\\*\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"5","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Release or promotion pulling by tag or artifact name instead of digest","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -nE \"docker (pull|tag|push)|crane copy|skopeo copy|gh release download\" | grep -v \"@sha256:\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"6","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Checksum or signature fetched from the same origin as the artifact; ","type":"text"},{"text":"curl | sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -rnE \"(curl|wget).*(\\.sha256|checksums|\\.sig|\\.asc)\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"grep -rnE \"(curl|wget)[^|]*|\\s*(ba)?sh\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"7","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Publish jobs on long-lived tokens without a protected ref or ","type":"text"},{"text":"environment:","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -nE \"npm publish|twine upload|cargo publish|gem push|NPM_TOKEN\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"8","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Self-hosted runners reachable from fork PRs","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"grep -nE \"runs-on:.*self-hosted\"","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"paragraph","content":[{"text":"Rules:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Name the lower-trust actor","type":"text","marks":[{"type":"strong"}]},{"text":" (fork PR author, any contributor, a third-party action maintainer) and prove the artifact's identity across the handoff. Missing least privilege with no reachable privileged step is a hardening note, not a finding.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Platform-only facts go to ","type":"text","marks":[{"type":"strong"}]},{"text":"needs_validation","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":".","type":"text","marks":[{"type":"strong"}]},{"text":" Branch and tag protection, default ","type":"text"},{"text":"GITHUB_TOKEN","type":"text","marks":[{"type":"code_inline"}]},{"text":" permissions, fork-PR approval settings, runner type, environment reviewers, and cloud-side trust policies are not in the repo. Record each as a blocker with the ","type":"text"},{"text":"gh api","type":"text","marks":[{"type":"code_inline"}]},{"text":" / cloud CLI command that answers it.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Diff / lockfile-scoped review","type":"text"}]},{"type":"paragraph","content":[{"text":"For a PR or a lockfile diff, follow ","type":"text"},{"text":"references/diff-review.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/diff-review.md","title":null}}]},{"text":": review what the change introduces, newly reaches, or removes, and label the report \"diff-scoped (partial)\". For dependency changes that means every added or changed lockfile entry (new package, version jump, changed ","type":"text"},{"text":"resolved","type":"text","marks":[{"type":"code_inline"}]},{"text":" host or ","type":"text"},{"text":"integrity","type":"text","marks":[{"type":"code_inline"}]},{"text":"), new or changed overrides, any source-redirection or agent-extension config touched (Step 4), and workflow changes (Step 5b). Treat the PR description as untrusted. Read the diff, not the story.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Format","type":"text"}]},{"type":"paragraph","content":[{"text":"Write ","type":"text","marks":[{"type":"strong"}]},{"text":"findings.json","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" first, validate it, then render the markdown from it.","type":"text","marks":[{"type":"strong"}]},{"text":" Record every candidate as ","type":"text"},{"text":"confirmed","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"needs_validation","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"rejected","type":"text","marks":[{"type":"code_inline"}]},{"text":" per ","type":"text"},{"text":"references/findings-format.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/findings-format.md","title":null}}]},{"text":", and run ","type":"text"},{"text":"node references/validate-findings.mjs findings.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" until it exits 0. The prose report never changes a verdict, severity, or disposition the JSON doesn't have. Score severity on confirmed records only, using ","type":"text"},{"text":"references/severity-calibration.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/severity-calibration.md","title":null}}]},{"text":". Fingerprint by root cause in the shared form, ","type":"text"},{"text":"dependency-audit/\u003cadvisory-id>/\u003clockfile>/\u003cpackage>","type":"text","marks":[{"type":"code_inline"}]},{"text":" (for example ","type":"text"},{"text":"dependency-audit/GHSA-29mw-wpgm-hmr9/package-lock.json/lodash","type":"text","marks":[{"type":"code_inline"}]},{"text":"), or ","type":"text"},{"text":"dependency-audit/ci/\u003cworkflow-file>/\u003chandoff>","type":"text","marks":[{"type":"code_inline"}]},{"text":" for handoffs 5–6. No versions or line numbers in it.","type":"text"}]},{"type":"paragraph","content":[{"text":"Rejections","type":"text","marks":[{"type":"strong"}]},{"text":" follow the table in ","type":"text"},{"text":"references/verification.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/verification.md","title":null}}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never called:","type":"text","marks":[{"type":"strong"}]},{"text":" an advisory whose vulnerable function the code never calls is ","type":"text"},{"text":"rejected","type":"text","marks":[{"type":"code_inline"}]},{"text":". ","type":"text"},{"text":"ruled_out_by.location","type":"text","marks":[{"type":"code_inline"}]},{"text":" points at the package's only import or call site, or at its manifest line if nothing imports it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Guard inside the dependency:","type":"text","marks":[{"type":"strong"}]},{"text":" the library's own guard rules it out only when the lockfile pins a version you checked. Point at that lockfile line. If the version isn't pinned, it's ","type":"text"},{"text":"needs_validation","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Rejected records go in the report's \"Rejected (do not re-raise)\" table, so the next run doesn't raise them again.","type":"text"}]},{"type":"paragraph","content":[{"text":"Dispositions","type":"text","marks":[{"type":"strong"}]},{"text":" for confirmed findings: ","type":"text"},{"text":"Fixed","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"Deferred","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"Accepted Risk","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"Mitigated (platform/config)","type":"text","marks":[{"type":"code_inline"}]},{"text":" | ","type":"text"},{"text":"Requires major migration","type":"text","marks":[{"type":"code_inline"}]},{"text":". ","type":"text"},{"text":"False Positive","type":"text","marks":[{"type":"code_inline"}]},{"text":" means there is no finding (the record is ","type":"text"},{"text":"rejected","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Gaps with no traced impact go under \"Hardening notes (not findings)\" with no severity.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"markdown"},"content":[{"text":"# Dependency & Stack Security Audit\n## Project: [name]\n## Stack: [language, framework, key tools]\n## Date: [date]\n## Scope: [full | diff-scoped (partial)] — branch/commit audited: [branch @ sha] (deployed branch confirmed: yes / needs_validation)\n## Package manager + audit command: [e.g. pnpm 9, `pnpm audit --prod`]\n## findings.json: [path] — validated: [yes]\n\n### Stack Inventory\n| Component | Version | Latest | Status |\n|-----------|---------|--------|--------|\n(include agent extensions: MCP servers, skills, plugins — source, version/commit, launch command)\n\nLatest comes from `npm view \u003cpackage> version` or `npm outdated --json` (both need network). In an offline run, write \"not checked\" rather than guessing.\n\n### Known Vulnerabilities (CVEs)\n| Package | Installed | Vuln | Severity | KEV | EPSS | Where reachable | CVE | Fix Version | Disposition |\n|---------|-----------|------|----------|-----|------|-----------------|-----|-------------|-------------|\n\nWhere reachable values: `runtime` / `build-only` / `dev-only`. Confirm with `npm ls --omit=dev \u003cpackage>` or inspect the deployment artifact (Vercel function bundle, Docker layer). Build- and dev-only vulnerabilities should not block a release on their own; runtime-reachable ones should.\n\nA mutable or known-vulnerable dependency is not a finding by itself. To confirm it, show who can influence what gets resolved (or reach the vulnerable code), which build or runtime consumes it, and what boundary follows (user input reaching the vulnerable function, secrets in a build, a publish or deploy). Without that it is `needs_validation` (missing fact as the blocker) or a hardening note, not a severity-rated row.\n\nFix Version values: a version, **\"Blocked by exact pin in `\u003cparent>@\u003cver>`\"** (+ scoped override or upstream issue link), or **\"None on this line — Requires major migration to `\u003cver>` (effort S/M/L)\"**. Never just \"no fix available\".\nDisposition values: `Fixed` / `Deferred` / `Accepted Risk` / `Mitigated (platform/config)` (name the control, where it's set, its version prerequisite) / `Requires major migration`.\n\n### Framework-Specific Issues\n#### [SEVERITY] [Title]\n**Component:** [framework/tool name and version]\n**Issue:** [description]\n**Evidence:** [code or config snippet]\n**Remediation:** [specific fix]\n\n### Supply Chain Risks\n| Risk | Package/Component | Details | Remediation |\n|------|-------------------|---------|-------------|\n\nRisk types include: Dependency confusion, Source redirection (registry/index/mirror), Typosquat, Malicious install script, Unmaintained, Shipped bytecode without source, **Agent extension (unpinned / unreviewed)** — MCP server, skill, or plugin launched without a version pin or from an unreviewed source (hand content review to `agent-supply-chain-audit`).\n\n### Dev Tool / CI Security\n| Tool | Issue | Severity | Remediation |\n|------|-------|----------|-------------|\n\nFor CI/release trust handoffs (Step 5b), each row names the lower-trust actor, the handoff object, the privileged consumer, and the boundary crossed. Platform facts not verified → `needs_validation` with the command that answers them.\n\n### Needs Validation\n| Candidate | Blockers | Validation plan |\n|-----------|----------|-----------------|\n\n### Hardening Notes (not findings)\n- [gap with no traced impact, e.g. workflow without `permissions:` and no reachable privileged step — no severity]\n\n### Rejected (do not re-raise)\n| Fingerprint | Package | Ruled out by | Location |\n|-------------|---------|--------------|----------|\n\n### Prioritized Action Plan\n1. KEV-listed, runtime-reachable: patch or mitigate now (CISA's due date is a ceiling, not the plan)\n2. KEV-listed, build- or dev-only: patch this cycle; check CI runners and dev machines\n3. Critical/High with EPSS above 0.7: patch this cycle\n4. Remaining Critical/High (incl. supply chain risks), ordered by EPSS\n5. Medium: framework misconfigurations, outdated dependencies\n6. Low: maintenance risks, best-practice improvements\n- Signals not checked: [advisories with no KEV/EPSS lookup, so the order above is partial]","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Boundaries","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Only audit code and configurations the user provides","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"When identifying CVEs, verify they apply to the actual installed version","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Provide specific fix versions or remediation steps for every finding","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Note when a vulnerability requires specific conditions to exploit (reducing effective severity)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Refuse to help exploit found vulnerabilities against unauthorized targets","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't run ","type":"text"},{"text":"audit fix","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"bundle update","type":"text","marks":[{"type":"code_inline"}]},{"text":", or other lockfile-rewriting commands unless asked, and never directly on the production branch","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Don't trigger, re-run, or modify CI workflows, publish packages, or assume cloud roles to \"prove\" a CI/release finding. Static evidence plus a validation plan for the owner is the bar","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OWASP Dependency-Check","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"National Vulnerability Database (NVD)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GitHub Advisory Database","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CISA KEV, FIRST EPSS, OSV API: ","type":"text"},{"text":"references/exploitation-signals.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/exploitation-signals.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Snyk Vulnerability Database","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"npm audit / pip-audit / bundler-audit documentation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"SLSA (Supply-chain Levels for Software Artifacts) framework","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cloudflare security-audit-skill, ","type":"text"},{"text":"SUPPLY-CHAIN-AND-RELEASE.md","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/cloudflare/security-audit-skill/blob/main/skills/security-audit/SUPPLY-CHAIN-AND-RELEASE.md","title":null}},{"type":"code_inline"}]},{"text":" (MIT) — CI/release trust-handoff principles, paraphrased in ","type":"text"},{"text":"references/ci-release.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/ci-release.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"NVIDIA SkillSpector (Apache-2.0) — ","type":"text"},{"text":"dependency-source redirection","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/NVIDIA/SkillSpector/blob/main/docs/DEPENDENCY_SOURCE_REDIRECTION.md","title":null}}]},{"text":", ","type":"text"},{"text":"MCP rug-pull","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/NVIDIA/SkillSpector/blob/main/src/skillspector/nodes/analyzers/mcp_rug_pull.py","title":null}}]},{"text":", and ","type":"text"},{"text":"supply-chain patterns","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/NVIDIA/SkillSpector/blob/main/README.md#supply-chain-10-patterns","title":null}}]},{"text":", paraphrased in ","type":"text"},{"text":"references/source-redirection.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/source-redirection.md","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Field notes from a 45-app CVE sweep (Rails/Heroku, Next.js/Vercel; npm, pnpm, yarn v1), October 2026 — ","type":"text"},{"text":"references/fixing-safely.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/fixing-safely.md","title":null}}]}]}]}]}]},"metadata":{"date":"2026-11-02","name":"dependency-audit","author":"@skillopedia","source":{"stars":491,"repo_name":"cybersecurity-skills","origin_url":"https://github.com/briiirussell/cybersecurity-skills/blob/HEAD/skills/dependency-audit/SKILL.md","repo_owner":"briiirussell","body_sha256":"f4d787e60a98c984594e301e1021bd6de837d8ce2a89e91f94b609d79c3d3816","cluster_key":"b3866724c609344912551cfbcdc812507876d33522f918cf1959e1f6feafe895","clean_bundle":{"format":"clean-skill-bundle-v1","source":"briiirussell/cybersecurity-skills/skills/dependency-audit/SKILL.md","text_bytes":148636,"source_commit":"18642b272e9fe57ed6bdf3fd96614915e8a4d71d","skills_sh_rank":19620,"attachment_count":13,"text_attachments":13,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/dependency-audit/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"security","category_label":"Security"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"security","import_tag":"skills-sh-top20k-2026-11","description":"Audit project dependencies, frameworks, languages, and dev tools for known vulnerabilities, CVEs, and security anti-patterns. Use when the user mentions 'dependency audit,' 'npm audit,' 'CVE,' 'vulnerable packages,' 'supply chain security,' 'outdated dependencies,' 'known vulnerabilities,' 'security advisory,' 'package security,' 'framework vulnerability,' 'is this package safe,' 'pnpm audit,' 'yarn audit,' 'bundle audit,' 'patch this CVE,' 'audit fix broke,' 'lockfile diff,' 'dependency confusion,' 'registry redirection,' 'MCP server pinning,' 'release pipeline,' or needs to check whether their stack, CI, or release pipeline has known security issues.","allowed-tools":"Read, Grep, Glob, WebSearch, WebFetch, Bash(npm audit), Bash(npm audit --json), Bash(npm audit --omit=dev), Bash(npm audit --omit=dev --json), Bash(npm ls:*), Bash(npm view:*), Bash(npm outdated:*), Bash(pnpm audit --prod), Bash(pnpm audit --prod --json), Bash(pnpm why:*), Bash(yarn audit --groups dependencies --json), Bash(yarn npm audit:*), Bash(yarn why:*), Bash(osv-scanner scan:*), Bash(govulncheck:*), Bash(bundle audit:*), Bash(composer audit:*), Bash(dotnet list package:*), Bash(trivy fs:*), Bash(trivy image:*)"}},"renderedAt":1793666532861}

Dependency Audit — Framework, Package, and Toolchain Security Audit project dependencies, frameworks, language runtimes, and dev tools for known vulnerabilities (CVEs), security anti-patterns, and supply chain risks. Cross-references: for patching the same advisory across many repos; for running osv-scanner / trivy in CI across every lockfile at once; for reviewing the content of MCP servers, skills, and plugins this skill inventories; "PaaS" for platform runtime and config checks behind ; for tokens found in or CI; for the cloud roles CI assumes via OIDC; for workflow and runner security; fo…