{"vulnerability": "cve-2021-44906", "sightings": [{"uuid": "efc62fdb-cdb5-4fe4-ba61-dfff21501268", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-44906", "type": "seen", "source": "https://gist.github.com/SavyFrost/a7bbd84d59f72f3c2cfed44779ad0793", "content": "", "creation_timestamp": "2025-01-27T12:33:00.000000Z"}, {"uuid": "c174a7bd-868b-4b43-a880-baf1e2b5a43d", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-44906", "type": "seen", "source": "https://t.me/cibsecurity/39151", "content": "\u203c CVE-2021-44906 \u203c\n\nMinimist &lt;=1.2.5 is vulnerable to Prototype Pollution via file index.js, function setKey() (lines 69-95).\n\n\ud83d\udcd6 Read\n\nvia \"National Vulnerability Database\".", "creation_timestamp": "2022-03-17T19:26:39.000000Z"}, {"uuid": "aeccc755-5695-470a-9f06-15e6b2aca6eb", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-44906", "type": "seen", "source": "https://gist.github.com/joemocha/fe552c9601dc58c4f9731982ab0d1f8c", "content": "From 87a213729aba7bd86b4cb07a5e6630fdd0cdef19 Mon Sep 17 00:00:00 2001\nFrom: Samuel Obukwelu \nDate: Mon, 18 May 2026 21:35:28 -0400\nSubject: [PATCH] Add Block 3 audience materials\n\n---\n .pi/extensions/inspect-verify.ts              | 111 ++++++++++++++++++\n .pi/skills/architecture-verifier/SKILL.md     |  54 +++++++++\n .pi/skills/performance-verifier/SKILL.md      |  53 +++++++++\n .pi/skills/project-inspector/SKILL.md         |  53 +++++++++\n .pi/skills/project-verifier/SKILL.md          |  89 ++++++++++++++\n AGENTS.md                                     |  47 ++++++++\n README.md                                     |  16 ++-\n SYSTEM.md                                     |  11 ++\n block3/README.md                              |  57 +++++++++\n fixtures/agentic-target-sample/README.md      |  59 ++++++++++\n .../components/Header.vue                     |  16 +++\n .../composables/useExample.ts                 |   4 +\n fixtures/agentic-target-sample/nuxt.config.ts |  14 +++\n fixtures/agentic-target-sample/package.json   |  24 ++++\n .../agentic-target-sample/pages/about.vue     |  14 +++\n .../agentic-target-sample/pages/index.vue     |  14 +++\n 16 files changed, 634 insertions(+), 2 deletions(-)\n create mode 100644 .pi/extensions/inspect-verify.ts\n create mode 100644 .pi/skills/architecture-verifier/SKILL.md\n create mode 100644 .pi/skills/performance-verifier/SKILL.md\n create mode 100644 .pi/skills/project-inspector/SKILL.md\n create mode 100644 .pi/skills/project-verifier/SKILL.md\n create mode 100644 AGENTS.md\n create mode 100644 SYSTEM.md\n create mode 100644 block3/README.md\n create mode 100644 fixtures/agentic-target-sample/README.md\n create mode 100644 fixtures/agentic-target-sample/components/Header.vue\n create mode 100644 fixtures/agentic-target-sample/composables/useExample.ts\n create mode 100644 fixtures/agentic-target-sample/nuxt.config.ts\n create mode 100644 fixtures/agentic-target-sample/package.json\n create mode 100644 fixtures/agentic-target-sample/pages/about.vue\n create mode 100644 fixtures/agentic-target-sample/pages/index.vue\n\ndiff --git a/.pi/extensions/inspect-verify.ts b/.pi/extensions/inspect-verify.ts\nnew file mode 100644\nindex 0000000..9105826\n--- /dev/null\n+++ b/.pi/extensions/inspect-verify.ts\n@@ -0,0 +1,111 @@\n+/**\n+ * inspect-verify \u2014 Block 3 Pi extension\n+ *\n+ * Implements ARCH-A (sequential pipeline) on top of `project-inspector` and\n+ * `project-verifier` skills. Two invocation paths, each with a different\n+ * pedagogical purpose:\n+ *\n+ *   1. `/audit ` \u2014 explicit slash command. Injects a single coordinated\n+ *      prompt that asks the agent to run inspector then verifier in one turn.\n+ *      Works in BOTH interactive and non-interactive (-p / --print) modes.\n+ *      This is the primary, reliable path attendees install and use.\n+ *\n+ *   2. `agent_end` listener \u2014 when a turn ends and the output looks like a\n+ *      project-inspector structural briefing (JSON with `\"architecture\"` and\n+ *      `\"entry_points\"`), the extension calls `sendUserMessage` to queue a\n+ *      verifier follow-up. Works in INTERACTIVE mode only \u2014 `-p` mode exits\n+ *      after the first `agent_end` and ignores queued messages. The ambient\n+ *      lesson: once the pattern is wired, the system catches itself without\n+ *      the user remembering to invoke the verifier by name.\n+ *\n+ * Pedagogical thesis: a working engineer composes skills into self-checking\n+ * systems instead of running each skill by hand. Inspector finds architecture;\n+ * Verifier flags supply-chain risk; the extension wires them together. No\n+ * single skill catches everything \u2014 that's why we compose.\n+ */\n+\n+import type {\n+  AgentMessage,\n+  ExtensionAPI,\n+} from \"@earendil-works/pi-coding-agent\";\n+\n+const VERIFIER_FOLLOWUP_PROMPT =\n+  \"The previous response looks like a project-inspector structural briefing. \" +\n+  \"Now run the project-verifier skill on the same target. Read the \" +\n+  \"dependency manifest (package.json, pyproject.toml, go.mod, etc.) of the \" +\n+  \"repository you just inspected, identify pinned exact versions, and use web \" +\n+  \"search to look up known CVE / GHSA advisories per pinned version. Emit the \" +\n+  \"verifier's JSON report \u2014 do not produce another structural briefing.\";\n+\n+const AUDIT_COMMAND_PROMPT = (target: string) =&gt;\n+  `Audit the repository at \\`${target}\\`. ` +\n+  `Step 1 \u2014 run the project-inspector skill: produce a structural briefing in the inspector's JSON shape. ` +\n+  `Step 2 \u2014 run the project-verifier skill on the same target: read the dependency manifest, identify pinned versions, look up CVE / GHSA advisories per pinned version, emit the verifier's JSON shape. ` +\n+  `Return both JSON outputs in order, separated by a \"---\" line.`;\n+\n+/**\n+ * Extract plain text from an assistant message's content blocks.\n+ * AssistantMessage.content is (TextContent | ThinkingContent | ToolCall)[].\n+ */\n+function assistantText(message: AgentMessage): string {\n+  if (message.role !== \"assistant\") return \"\";\n+  const content = message.content;\n+  if (typeof content === \"string\") return content;\n+  if (!Array.isArray(content)) return \"\";\n+  return content\n+    .filter((block: any) =&gt; block &amp;&amp; block.type === \"text\")\n+    .map((block: any) =&gt; (typeof block.text === \"string\" ? block.text : \"\"))\n+    .join(\"\\n\");\n+}\n+\n+/**\n+ * Heuristic: does the text look like a project-inspector output?\n+ * Matches the inspector's documented JSON schema.\n+ */\n+function looksLikeInspectorBriefing(text: string): boolean {\n+  return (\n+    text.includes('\"architecture\"') &amp;&amp;\n+    text.includes('\"entry_points\"') &amp;&amp;\n+    // Quick exclusion: don't re-fire on verifier output\n+    !text.includes('\"lens\"') &amp;&amp;\n+    !text.includes('\"findings\"') &amp;&amp;\n+    !text.includes('\"missed_by_structural_briefing\"')\n+  );\n+}\n+\n+export default function (pi: ExtensionAPI) {\n+  // De-dupe: only fire the verifier once per distinct inspector output within\n+  // a single session. The fingerprint is the first ~200 chars of the output;\n+  // good enough for one session's worth of turns.\n+  const firedFingerprints = new Set();\n+\n+  pi.on(\"agent_end\", async (event) =&gt; {\n+    const lastAssistant = [...event.messages]\n+      .reverse()\n+      .find((m: AgentMessage) =&gt; m.role === \"assistant\");\n+    if (!lastAssistant) return;\n+\n+    const text = assistantText(lastAssistant);\n+    if (!looksLikeInspectorBriefing(text)) return;\n+\n+    const fingerprint = text.slice(0, 200);\n+    if (firedFingerprints.has(fingerprint)) return;\n+    firedFingerprints.add(fingerprint);\n+\n+    // Inject the verifier follow-up. `deliverAs: \"followUp\"` queues this for\n+    // the next turn so it doesn't fight an in-flight stream.\n+    pi.sendUserMessage(VERIFIER_FOLLOWUP_PROMPT, { deliverAs: \"followUp\" });\n+  });\n+\n+  // Explicit, loop-safe path: `/audit ` triggers inspector + verifier\n+  // in a single coordinated prompt. Useful when the auto-detect heuristic\n+  // misses or when the user wants the pipeline by name.\n+  pi.registerCommand(\"audit\", {\n+    description:\n+      \"Run project-inspector then project-verifier on the given repository path.\",\n+    handler: async (args, _ctx) =&gt; {\n+      const target = args.trim() || \".\";\n+      pi.sendUserMessage(AUDIT_COMMAND_PROMPT(target));\n+    },\n+  });\n+}\ndiff --git a/.pi/skills/architecture-verifier/SKILL.md b/.pi/skills/architecture-verifier/SKILL.md\nnew file mode 100644\nindex 0000000..713a636\n--- /dev/null\n+++ b/.pi/skills/architecture-verifier/SKILL.md\n@@ -0,0 +1,54 @@\n+---\n+name: architecture-verifier\n+description: Audit a codebase against architectural and structural quality concerns that a feature-focused briefing misses. Trigger when the user asks for an architecture review, structural critique, design audit, or wants to verify a project briefing against design-quality concerns.\n+---\n+\n+# Architecture Verifier\n+\n+You are a staff-level architect reviewing a codebase for structural and design-quality concerns that a feature-focused inspector typically misses. Apply the **architecture lens**: coupling, layering, module boundaries, naming consistency, separation of concerns, and the structural debt that compounds as a system grows.\n+\n+## When you run\n+\n+You run alongside other verifiers (security, performance) as part of a multi-lens audit composed by a deliberator agent. Your output is one slice of a composed verification report.\n+\n+You may also run standalone when a user asks to architecturally review a codebase.\n+\n+## Process\n+\n+1. **Map the module boundaries.** Identify directories that represent layers (e.g., `pages/` vs `components/` vs `composables/` vs `server/`). Note whether boundaries are respected or whether code crosses them inappropriately.\n+2. **Check coupling.** Sample a few entry points; trace their imports. Note tight coupling, circular references, or modules that \"know too much\" about their callers.\n+3. **Naming + conventions.** Check whether names communicate purpose at the file, function, and module level. Flag inconsistencies (e.g., camelCase mixed with kebab-case in similar-purpose files).\n+4. **Missing abstractions.** Look for repeated patterns that should be extracted (3+ near-duplicate handlers, recurring fetch-then-transform shapes, etc.).\n+5. **Premature abstractions.** Look for the inverse: indirection that doesn't earn its keep \u2014 single-impl interfaces, factories that wrap one constructor, config layers nobody configures.\n+6. **Test architecture.** If tests exist, note whether they exercise behavior or implementation. If they don't, flag the absence as architectural risk.\n+7. **Score severity.** For each finding: severity (critical / high / moderate / low), one-sentence description, one-sentence recommendation.\n+\n+## Output format\n+\n+Return **only** valid JSON in the following shape \u2014 no prose before or after the JSON block:\n+\n+```json\n+{\n+  \"verified\": false,\n+  \"target\": \"\",\n+  \"lens\": \"architecture\",\n+  \"findings\": [\n+    {\n+      \"concern\": \"\",\n+      \"severity\": \"critical | high | moderate | low\",\n+      \"summary\": \"\",\n+      \"recommendation\": \"\"\n+    }\n+  ],\n+  \"recommendation\": \"&lt;2\u20133 sentence summary of architectural posture&gt;\"\n+}\n+```\n+\n+Set `verified: true` only when there are zero findings of severity `high` or `critical`.\n+\n+## Constraints\n+\n+- **Be specific.** Generic findings (\"could be more modular\") help no one. Cite file paths or directory names.\n+- **Honest scoping.** A 7-file fixture has architectural choices but not architectural problems at scale. Don't manufacture findings to look thorough. If the project is genuinely clean, say so.\n+- **One lens only.** This skill is architecture-focused. Don't comment on security, performance, dependencies, or test coverage outside the architecture-relevant slice. The composed audit covers those concerns through other verifiers.\n+- **No fabrication.** Only reference patterns you can verify by reading the code.\ndiff --git a/.pi/skills/performance-verifier/SKILL.md b/.pi/skills/performance-verifier/SKILL.md\nnew file mode 100644\nindex 0000000..75b6389\n--- /dev/null\n+++ b/.pi/skills/performance-verifier/SKILL.md\n@@ -0,0 +1,53 @@\n+---\n+name: performance-verifier\n+description: Audit a codebase for performance and bundle-hygiene concerns that a structural briefing does not surface. Trigger when the user asks for a performance review, bundle audit, runtime cost analysis, or wants to verify a project briefing against build/runtime performance concerns.\n+---\n+\n+# Performance Verifier\n+\n+You are a senior engineer reviewing a codebase for performance and bundle-hygiene concerns that a structural inspector typically misses. Apply the **performance lens**: bundle weight, dependency cost, runtime hot paths, render efficiency in framework code (Vue/Nuxt/React), and the operational performance debt that compounds in production.\n+\n+## When you run\n+\n+You run alongside other verifiers (security, architecture) as part of a multi-lens audit composed by a deliberator agent. Your output is one slice of a composed verification report.\n+\n+You may also run standalone when a user asks to performance-review a codebase.\n+\n+## Process\n+\n+1. **Read the dependency manifest.** Identify heavyweight deps (Lodash full import, Moment, large UI libs, AWS SDK monoliths). Note tree-shakable vs not.\n+2. **Check the bundler config.** For Vite / webpack / Nuxt / Next, look for: missing code-splitting boundaries, large `manualChunks` opportunities missed, source maps shipped to production, devtools left enabled.\n+3. **Sample a hot path.** For a framework like Nuxt/Vue, inspect a representative page. Look for: synchronous waterfalls in `setup()`, unbounded `watch` chains, unkeyed `v-for` over large arrays, deep reactivity where shallow would do.\n+4. **Asset hygiene.** Note images &gt; 200KB, unbounded fonts, CDN-vs-local mismatches in obvious places.\n+5. **Build-time cost.** Note signals of slow builds: large `tsconfig` includes, missing `paths` aliases, `node_modules` directly imported from source.\n+6. **Score severity.** For each finding: severity (critical / high / moderate / low), one-sentence description, one-sentence recommendation.\n+\n+## Output format\n+\n+Return **only** valid JSON in the following shape \u2014 no prose before or after the JSON block:\n+\n+```json\n+{\n+  \"verified\": false,\n+  \"target\": \"\",\n+  \"lens\": \"performance\",\n+  \"findings\": [\n+    {\n+      \"concern\": \"\",\n+      \"severity\": \"critical | high | moderate | low\",\n+      \"summary\": \"\",\n+      \"recommendation\": \"\"\n+    }\n+  ],\n+  \"recommendation\": \"&lt;2\u20133 sentence summary of performance posture&gt;\"\n+}\n+```\n+\n+Set `verified: true` only when there are zero findings of severity `high` or `critical`.\n+\n+## Constraints\n+\n+- **Be quantitative when possible.** \"Lodash full import\" \u2192 cite the file. \"Large asset\" \u2192 estimate the kB. Vague findings degrade the audit.\n+- **Workshop-fixture awareness.** Small fixtures (&lt;10 files) won't have many real performance findings. Don't manufacture issues to look thorough \u2014 say the project is performance-clean if it is.\n+- **One lens only.** Performance-focused. Don't comment on security, architecture, dependencies' CVEs, or test coverage outside the performance-relevant slice.\n+- **No fabrication.** Only reference patterns you can verify by reading the code.\ndiff --git a/.pi/skills/project-inspector/SKILL.md b/.pi/skills/project-inspector/SKILL.md\nnew file mode 100644\nindex 0000000..044a595\n--- /dev/null\n+++ b/.pi/skills/project-inspector/SKILL.md\n@@ -0,0 +1,53 @@\n+---\n+name: project-inspector\n+description: Inspect any code repository and produce a structured architectural briefing. Trigger whenever the user asks to understand, summarize, onboard to, get a briefing on, or describe a codebase, even if they do not literally say \"inspect.\" Outputs valid JSON with architecture, entry_points, data_flow, key_dependencies, and test_patterns fields.\n+---\n+\n+# Project Inspector\n+\n+You are a senior software engineer onboarding to a new project. Your job is to produce a structured, factual briefing of the codebase you're shown so that another senior engineer can navigate it cold.\n+\n+## Process (think step-by-step)\n+\n+1. **Scan the directory structure.** Note the top-level layout, any monorepo or workspace boundaries, and any unusual organization.\n+2. **Identify the framework.** Read `package.json`, `pyproject.toml`, `go.mod`, or equivalent. Note the runtime, the framework, and the major libraries. If multiple frameworks are present, report them all.\n+3. **Locate the entry points.** Identify where execution begins \u2014 main file(s), route registration, top-level components. List at least three when present.\n+4. **Trace data flow.** For the most prominent entry point, follow the imports and call paths far enough to understand how a single request or invocation moves through the code.\n+5. **Note test patterns.** Identify the test framework, where tests live, and a representative example. If tests are missing, say so.\n+6. **Summarize.** Produce the structured output below.\n+\n+## Output format\n+\n+Return **only** valid JSON in the following shape \u2014 no prose before or after the JSON block:\n+\n+```json\n+{\n+  \"architecture\": \"&lt;2\u20134 sentence summary of the overall shape and framework&gt;\",\n+  \"entry_points\": [\"\", \"\", \"\"],\n+  \"data_flow\": \"&lt;2\u20133 sentence trace of how a request/invocation moves through the most prominent entry point&gt;\",\n+  \"key_dependencies\": [\"\", \"\", \"...\"],\n+  \"test_patterns\": \"&lt;1\u20132 sentence note on test framework, location, conventions; or 'No tests detected.'&gt;\"\n+}\n+```\n+\n+## Example\n+\n+For a small Express app:\n+\n+```json\n+{\n+  \"architecture\": \"Node.js HTTP service built on Express 4. Single-package layout with handler functions in src/routes/, business logic in src/services/, and a thin server.ts entry point.\",\n+  \"entry_points\": [\"src/server.ts\", \"src/routes/index.ts\", \"src/routes/users.ts\"],\n+  \"data_flow\": \"HTTP request enters src/server.ts which mounts route handlers from src/routes/. Each route handler validates input and delegates to a service in src/services/. Services use a shared db client from src/db.ts.\",\n+  \"key_dependencies\": [\"express\", \"zod\", \"pg\", \"pino\"],\n+  \"test_patterns\": \"Vitest tests colocated next to source files as *.test.ts. One example: src/services/users.test.ts.\"\n+}\n+```\n+\n+## Constraints\n+\n+- **Never invent file paths.** Only reference files you can observe in the input. If a path is unclear, omit it rather than guess.\n+- **Never echo secrets.** If the codebase contains API keys, access tokens, AWS credentials, or other secret-shaped values (e.g., strings matching `sk-`, `AKIA`, JWT shapes), **do not include those values in your output**. Note that secrets are present but redact the values themselves.\n+- **Be honest about ambiguity.** If the framework is unclear (e.g., a Vite + React project with no meta-framework), report exactly what you see. Do not assert a framework that isn't there.\n+- **If the input is not a codebase** \u2014 e.g., the user asks an unrelated question \u2014 refuse politely and ask for a codebase to inspect. Do not produce a briefing for non-repo input.\n+- **If the codebase is empty or too small to analyze meaningfully**, return the JSON with appropriate fields noting the limitation rather than fabricating content.\ndiff --git a/.pi/skills/project-verifier/SKILL.md b/.pi/skills/project-verifier/SKILL.md\nnew file mode 100644\nindex 0000000..6d5a780\n--- /dev/null\n+++ b/.pi/skills/project-verifier/SKILL.md\n@@ -0,0 +1,89 @@\n+---\n+name: project-verifier\n+description: Audit a codebase against the security and supply-chain risks that a structural briefing misses. Trigger whenever the user asks to verify, audit, double-check, or second-opinion a project briefing, or asks about CVEs, vulnerable dependencies, security posture, or supply-chain risk in a repo. Especially trigger when a project-inspector briefing has just been produced.\n+---\n+\n+# Project Verifier\n+\n+You are a security-aware verifier auditing a codebase against risks that a structural briefing does not cover. Your job is to apply a **different lens** to the same input the structural inspector saw \u2014 the lens of pinned versions, known advisories, and supply-chain risk \u2014 and produce a verification report that flags what the structural briefing missed.\n+\n+## When you run\n+\n+You run after a structural inspector (e.g., `project-inspector`) has produced an architectural briefing of a target repository. Your input is the target repository path. Your output is a verification report.\n+\n+You may also run standalone when a user asks to audit a repository for security issues.\n+\n+## Process (think step-by-step)\n+\n+1. **Locate the dependency manifest.** Read `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, or equivalent from the target repo. If none is found, report that and exit.\n+2. **Extract pinned versions.** For each declared dependency, record its name and its version constraint. Note which are pinned to **exact** versions (no `^`, `~`, or range operators) vs. which are floating.\n+3. **Look up known advisories.** For each pinned dependency, use web search / web grounding to look up known CVE advisories, security bulletins, or GitHub Security Advisories (GHSA) for that exact version. Prefer authoritative sources: NVD, GitHub Security Advisories, npm advisory database, Snyk vuln DB.\n+4. **Cross-reference with `npm audit`** when available. If the target repo has a `package-lock.json` or you can run `npm audit --package-lock-only --json` against it, capture that output and reconcile against your web-grounded findings.\n+5. **Score severity.** For each finding, capture: package name, pinned version, CVE/GHSA ID, severity (critical/high/moderate/low), one-sentence summary, and a remediation hint (typically: the patched version range).\n+6. **Disagreement check.** If a structural-briefing input is provided (e.g., the output of `project-inspector`), explicitly note which of your findings the structural briefing did NOT mention. This is the verifier's primary value \u2014 surfacing what the prior agent missed.\n+7. **Emit the report.**\n+\n+## Output format\n+\n+Return **only** valid JSON in the following shape \u2014 no prose before or after the JSON block:\n+\n+```json\n+{\n+  \"verified\": false,\n+  \"target\": \"\",\n+  \"lens\": \"security / supply-chain\",\n+  \"manifest\": \"\",\n+  \"findings\": [\n+    {\n+      \"package\": \"\",\n+      \"pinned\": \"\",\n+      \"advisory\": \"\",\n+      \"severity\": \"critical | high | moderate | low\",\n+      \"summary\": \"\",\n+      \"remediation\": \"\"\n+    }\n+  ],\n+  \"missed_by_structural_briefing\": [\"\", \"...\"],\n+  \"recommendation\": \"&lt;2\u20133 sentence summary of risk posture and what to do next&gt;\"\n+}\n+```\n+\n+Set `verified: true` only when there are zero findings of severity `high` or `critical`. Otherwise `verified: false`.\n+\n+## Example output (illustrative)\n+\n+```json\n+{\n+  \"verified\": false,\n+  \"target\": \"fixtures/agentic-target-sample/\",\n+  \"lens\": \"security / supply-chain\",\n+  \"manifest\": \"fixtures/agentic-target-sample/package.json\",\n+  \"findings\": [\n+    {\n+      \"package\": \"axios\",\n+      \"pinned\": \"0.21.0\",\n+      \"advisory\": \"CVE-2020-28168\",\n+      \"severity\": \"high\",\n+      \"summary\": \"Server-Side Request Forgery via maliciously-crafted URLs in axios &lt;0.21.1.\",\n+      \"remediation\": \"Upgrade to axios &gt;=0.21.1\"\n+    }\n+  ],\n+  \"missed_by_structural_briefing\": [\"axios\", \"lodash\", \"minimist\", \"serialize-javascript\"],\n+  \"recommendation\": \"Four pinned dependencies have public high/critical advisories. The structural briefing identified the package names but did not flag the version-bound security exposure. Upgrade or pin to the indicated remediation ranges before shipping.\"\n+}\n+```\n+\n+## Constraints\n+\n+- **Never fabricate CVEs.** If you cannot find a verifiable advisory for a pinned version, omit it rather than guess. It is better to under-report than to fabricate.\n+- **Cite the advisory ID.** Every finding must reference a real, lookup-able advisory ID (CVE or GHSA). If no ID is available, do not include the finding.\n+- **Pinned-exact-versions are the focus.** Range-constrained deps (`^1.2.3`, `~4.5.6`) are usually resolved to the latest patched version by the package manager; report them only if the explicitly-allowed range includes vulnerable versions and there's no patched version within range.\n+- **Be honest about limits.** If your web search is rate-limited, unavailable, or returns inconclusive results for a pinned version, say so in the recommendation field rather than asserting \"no findings.\"\n+- **Do not echo secrets.** If the dependency manifest or related files contain API keys, tokens, or credentials, do not include those values in your output. This skill audits dependencies, not secret-handling.\n+- **Do not modify the target repo.** This skill is read-only with respect to the target.\n+\n+## Composition note (for the workshop)\n+\n+This skill is half of a composed pipeline. The other half is `project-inspector`, which produces a structural briefing. Run them together \u2014 either by chaining manually (`/inspect`, then `/verify`) or via the `inspect-verify` extension (`.pi/extensions/inspect-verify.ts`), which triggers this skill automatically after an inspector run.\n+\n+The pedagogical point: *no single skill catches everything.* The Inspector's spec doesn't ask for security posture. The Verifier's spec doesn't ask for architecture. Composed, they cover what either alone would miss. That's agentic engineering at the workflow altitude.\ndiff --git a/AGENTS.md b/AGENTS.md\nnew file mode 100644\nindex 0000000..9494243\n--- /dev/null\n+++ b/AGENTS.md\n@@ -0,0 +1,47 @@\n+# AGENTS.md \u2014 AI Engineering Workshop\n+\n+&gt; Project instructions loaded automatically by Pi (`pi.dev`) at session start. Pi reads `AGENTS.md` from `~/.pi/agent/`, every parent directory of the cwd, and the cwd itself \u2014 and concatenates them in that order. This file is the **project-local layer**: everything specific to this workshop repo lives here.\n+\n+## What this repo is\n+\n+Workshop materials for *AI Engineering: From Prompt Architecture to Production Infrastructure* \u2014 VueConf US 2026, Atlanta, 2026-05-19. Four blocks (see [README.md](README.md)). Block 3 is the Pi / agentic-engineering block; the `.pi/` directory below holds its skills + extensions.\n+\n+## Where things live\n+\n+| Path | What |\n+|------|------|\n+| `.pi/skills/project-inspector/` | Block 2 artifact, ported into Pi for Block 3 |\n+| `.pi/skills/project-verifier/` | Block 3 security/CVE lens \u2014 primary lab skill |\n+| `.pi/skills/architecture-verifier/` | Block 3 showcase stub (architectural critique lens) |\n+| `.pi/skills/performance-verifier/` | Block 3 showcase stub (performance/bundle lens) |\n+| `.pi/extensions/inspect-verify.ts` | Block 3 extension \u2014 wires inspector \u2192 verifier; registers `/audit` |\n+| `fixtures/agentic-target-sample/` | Block 3 dramatic-failure target \u2014 Nuxt-shaped, vulnerable deps planted |\n+| `fixtures/{nuxt,monorepo,vite-react,leaky-secret}-sample/` | Block 2 eval fixtures |\n+| `prompts/project-inspector-v5.txt` | Canonical Block 1 fallback prompt |\n+| `evals/` | Block 2 eval suite (with planted non-discriminating assertion) |\n+| `bifrost/`, `nuxt-app/` | Block 4 infrastructure + Vue/Nuxt integration target |\n+\n+## Conventions when operating in this repo\n+\n+- **Skills are the unit of agent capability.** A new lens (security / architecture / performance / etc.) lives as a new skill under `.pi/skills//`. Don't bury new behavior in extension code if it could be a skill \u2014 extensions wire skills; skills implement reasoning.\n+- **Extensions wire, skills reason.** Extensions hook into events and dispatch; they don't contain prompt logic. If you find yourself writing a prompt inside an extension, that's a skill in disguise.\n+- **One JSON shape per verifier.** All verifier skills (`*-verifier`) emit `{verified, target, lens, findings[], recommendation}`. Don't drift the envelope \u2014 the deliberator pattern in Block 3's showcase relies on consistent shape.\n+- **Never echo secrets.** This repo contains `fixtures/leaky-secret-sample/` with planted fake API keys for Block 2 adversarial testing. They are documented test patterns (`sk-test-*`, `AKIAEXAMPLE*`) but no skill should reproduce them in output regardless.\n+- **Honest scoping.** On the small workshop fixtures (\u226410 files each), don't manufacture findings to look thorough. A clean lens is a valid result.\n+\n+## What NOT to load into context\n+\n+Pi auto-discovers skills and extensions. Be deliberate about what runs:\n+\n+- `node_modules/`, `.nuxt/`, `.output/` \u2014 generated; never load\n+- `bifrost/data/` (if present at workshop time) \u2014 runtime state; never load\n+- `nuxt-app/` during Block 3 \u2014 out of scope; loading it inflates context unnecessarily\n+- Session history under `~/.pi/agent/sessions/` \u2014 Pi manages this; don't reference\n+\n+If running `--tools read,bash,grep,find,ls` (the typical Block 3 toolset), the `find` / `grep` defaults will naturally skip these \u2014 but if you broaden tools, add explicit exclusions.\n+\n+## Workshop-day operational notes\n+\n+- **Default provider:** `pi` defaults to Google (Gemini). Gemini's web grounding is what makes the security-CVE lookup in `project-verifier` work without extra wiring.\n+- **Print mode (`-p`) caveat:** `--print` exits after one `agent_end`. The extension's auto-fire on `agent_end` only lights up in interactive mode. The `/audit` slash command works in both modes \u2014 use it when scripting.\n+- **Pre-staged extensions:** `.pi/extensions/inspect-verify.ts` is auto-loaded by Pi when run from the repo root. Override with `-e ` if loading from elsewhere.\ndiff --git a/README.md b/README.md\nindex 8d69258..d4d1d49 100644\n--- a/README.md\n+++ b/README.md\n@@ -12,7 +12,7 @@ Workshop materials for four blocks of hands-on AI engineering:\n |---|---|---|\n | **1** | Foundations | Write a v5 prompt for the Project Inspector running example |\n | **2** | Build &amp; Eval a Skill | Promote the prompt to a working skill via the `skill-creator` plugin; run the eval loop; learn to *evaluate the eval* |\n-| **3** | Vibe Coding \u2192 Agentic Engineering | (Instructor's separate materials) |\n+| **3** | Vibe Coding \u2192 Agentic Engineering | Port your Block 2 skill into Pi; compose it with a verifier skill and a hook extension; watch the system catch what one skill alone misses |\n | **4** | AI Gateway (Production) | Configure Bifrost; tour the production-gateway surface; integrate with a Vue/Nuxt app |\n \n ## Quick start\n@@ -34,17 +34,26 @@ See the [Prerequisites](#) (handed out separately). Minimum:\n - Two LLM provider API keys with $5+ credit each (Anthropic + OpenAI recommended)\n - An AI coding CLI installed (Claude Code, Codex CLI, or Gemini CLI)\n - The `skill-creator` plugin installed via the Anthropic plugin marketplace\n+- **Pi installed** (`pi.dev`) \u2014 `bun install -g @earendil-works/pi-coding-agent`, then verify `pi --version` returns cleanly. (Block 3 runtime.)\n+- **A second CLI for verification** \u2014 Gemini CLI authenticated against your Google account (primary) or Codex CLI against your OpenAI key (alternative). Used by Block 3's verifier skill.\n \n ## Repository layout\n \n ```\n ai-engineering-workshop/\n+\u251c\u2500\u2500 AGENTS.md                   # Block 3: project instructions loaded by Pi at session start\n+\u251c\u2500\u2500 SYSTEM.md                   # Block 3: per-project system-prompt append\n \u251c\u2500\u2500 skills/project-inspector/   # Block 2: your skill goes here (stub provided)\n+\u251c\u2500\u2500 .pi/                        # Block 3: Pi-specific resources\n+\u2502   \u251c\u2500\u2500 skills/                 #   ported inspector + verifier skills (security / arch / perf)\n+\u2502   \u2514\u2500\u2500 extensions/             #   inspect-verify.ts \u2014 auto-fires verifier after inspector\n \u251c\u2500\u2500 prompts/                    # Block 1: prompt-evolution reference (v0 baseline, v5 canonical fallback)\n \u251c\u2500\u2500 evals/                      # Block 2: eval suite\n \u2502   \u251c\u2500\u2500 evals.json              # 5 test cases with planted pathology (see Block 2 spec)\n \u2502   \u2514\u2500\u2500 trigger-eval.json       # 20 queries for the description optimizer\n-\u251c\u2500\u2500 fixtures/                   # Block 2: test inputs (Nuxt, monorepo, Vite+React, leaky-secret)\n+\u251c\u2500\u2500 fixtures/                   # Block 2 + 3: test inputs\n+\u2502   \u2514\u2500\u2500 agentic-target-sample/  # Block 3: Nuxt-shaped fixture with planted vulnerable deps\n+\u251c\u2500\u2500 block3/                     # Block 3: instructor notes, narration script, attendee walkthrough\n \u251c\u2500\u2500 nuxt-app/                   # Block 4.9: Vue/Nuxt integration target\n \u251c\u2500\u2500 bifrost/                    # Block 4: Bifrost config (with semantic_cache plugin)\n \u2514\u2500\u2500 scripts/                    # Helper one-liners (cache demo, test prompts)\n@@ -58,6 +67,9 @@ Open `prompts/project-inspector-v0.txt` and walk it through the 5-layer enhancem\n **Block 2 \u2014 Build &amp; Eval a Skill (morning, after break)**\n Open `skills/project-inspector/SKILL.md`. Paste your v5 from Block 1. Then run the `skill-creator` plugin against the `evals/` and `fixtures/` directories. The plugin will spawn parallel with-skill and baseline runs, grade them, and open a viewer. **Watch the Benchmark tab.** Something's planted in case 0 \u2014 the analyzer pass will tell you what.\n \n+**Block 3 \u2014 Vibe Coding \u2192 Agentic Engineering (afternoon, post-lunch)**\n+Copy your Block 2 skill into `.pi/skills/project-inspector/`. Launch `pi` from the repo root and inspect `fixtures/agentic-target-sample/`. The inspector will produce a clean Nuxt-3 briefing \u2014 and confidently miss something. We'll find the lie together, install the verifier skill, wire the `.pi/extensions/inspect-verify.ts` hook so the verifier fires automatically, and finish with the `AGENTS.md` / `SYSTEM.md` hygiene primitives. Attendee-facing walkthrough lives at [`block3/README.md`](block3/README.md).\n+\n **Block 4 \u2014 AI Gateway (afternoon)**\n Boot Bifrost: `npm run bifrost`. Configure providers, models, a Virtual Key, a budget, a routing rule, and the semantic cache. Then `cd nuxt-app &amp;&amp; npm install &amp;&amp; npm run dev` and wire the Vue app to your gateway.\n \ndiff --git a/SYSTEM.md b/SYSTEM.md\nnew file mode 100644\nindex 0000000..ad712c3\n--- /dev/null\n+++ b/SYSTEM.md\n@@ -0,0 +1,11 @@\n+# SYSTEM.md \u2014 AI Engineering Workshop\n+\n+&gt; Optional per-project system-prompt **append** for Pi. When this file is present in the cwd, Pi appends its contents to the default system prompt. Use it for project-specific framing that should shape every response in this repo.\n+\n+You are operating inside the **AI Engineering: From Prompt Architecture to Production Infrastructure** workshop repository for VueConf US 2026. The user is either an instructor preparing the workshop or an attendee mid-workshop. Optimize for clarity and pedagogical honesty over polish:\n+\n+- **When a skill is invoked, run the skill faithfully and emit its declared output shape.** Verifier skills (`project-verifier`, `architecture-verifier`, `performance-verifier`) emit JSON only \u2014 no preamble, no postscript.\n+- **When composing skills (e.g., via `/audit`), run them in declared order and present each output verbatim.** Don't summarize a verifier's JSON into prose; the JSON itself is the deliverable.\n+- **Honest scoping over performative thoroughness.** A clean verifier result (`verified: true`, empty findings) is a valid and pedagogically valuable output on this repo's small fixtures. Don't manufacture concerns to look diligent.\n+- **Cite, never fabricate, advisories.** When the `project-verifier` skill is invoked, every reported CVE/GHSA must be a real, lookup-able advisory ID. If an advisory cannot be verified for a pinned version, omit it rather than guess.\n+- **Skill boundaries are pedagogical, not legal.** If an attendee asks you to do something outside the declared scope of a skill, do it \u2014 but in your role as a general agent, not as the skill. Note the role shift briefly so attendees see the boundary working.\ndiff --git a/block3/README.md b/block3/README.md\nnew file mode 100644\nindex 0000000..08dafd2\n--- /dev/null\n+++ b/block3/README.md\n@@ -0,0 +1,57 @@\n+# Block 3 \u2014 Vibe Coding \u2192 Agentic Engineering\n+\n+Attendee walkthrough for the Block 3 afternoon slot (1:00 \u2013 2:30 PM, 90 min).\n+\n+&gt; **Thesis:** How working engineers move from vibe coding to agentic engineering. Same skill, new altitude.\n+\n+## What you'll do\n+\n+1. **Port** your Block 2 Project Inspector skill into Pi\n+2. **Run** it against a Nuxt-shaped fixture and see it produce a confidently-incomplete answer\n+3. **Install** a verifier skill that applies a different lens (security / supply-chain) to the same input\n+4. **Wire** a Pi extension (`.ts`) that auto-fires the verifier after the inspector finishes \u2014 and register a `/audit` slash command as the explicit, reliable alternative\n+5. **Drop in** `AGENTS.md` and `SYSTEM.md` \u2014 Pi's project-scoped hygiene primitives\n+6. **Watch** the multi-lens showcase: three verifiers in parallel, a deliberator merging findings\n+\n+## Prereqs (verify before lunch)\n+\n+- Pi installed: `pi --version` returns cleanly\n+- A second CLI for verification: Gemini CLI authed against Google, OR Codex CLI authed against your OpenAI key\n+\n+If either prereq is missing, raise your hand during the lunch break \u2014 five minutes of setup is faster than fifteen minutes of debugging during the lab.\n+\n+## Files you'll touch\n+\n+| Path | What |\n+|------|------|\n+| `.pi/skills/project-inspector/SKILL.md` | Where you copy your Block 2 skill |\n+| `.pi/skills/project-verifier/SKILL.md` | Pre-staged \u2014 the security/CVE lens |\n+| `.pi/extensions/inspect-verify.ts` | Pre-staged \u2014 the hook + slash command |\n+| `fixtures/agentic-target-sample/` | The Nuxt-shaped target with the planted failure |\n+| `AGENTS.md`, `SYSTEM.md` | Pre-staged at repo root \u2014 project-scoped Pi config |\n+\n+## The 90-minute shape\n+\n+| Time | Beat | Mode |\n+|------|------|------|\n+| 1:00 \u2013 1:05 | Frame | Mirrored |\n+| 1:05 \u2013 1:25 | Port Project Inspector into Pi | YOUR TURN |\n+| 1:25 \u2013 1:30 | The failure \u2014 what the briefing doesn't say | Mirrored |\n+| 1:30 \u2013 1:50 | Verifier agent enters | Mixed |\n+| 1:50 \u2013 2:05 | Hooks \u2014 Pi extension | Mixed |\n+| 2:05 \u2013 2:15 | Hygiene \u2014 AGENTS.md + SYSTEM.md | Mixed |\n+| 2:15 \u2013 2:23 | Showcase \u2014 pattern extended (3 verifiers + deliberator) | Demo |\n+| 2:23 \u2013 2:27 | Showcase \u2014 roundtable cameo | Demo |\n+| 2:27 \u2013 2:30 | Q&amp;A | Conversation |\n+\n+## What you take home\n+\n+A repo you can use as a template:\n+\n+- One inspector skill + one verifier skill + one extension that wires them\n+- An `AGENTS.md` and `SYSTEM.md` you can adapt for any of your own projects\n+- The composition pattern: *no single skill catches everything; that's why we compose*\n+\n+## What to do Monday\n+\n+Pick one skill from your own work where the output is *technically correct but operationally incomplete* \u2014 and build the verifier that catches what it misses. Same pattern. Same primitives. Different domain.\ndiff --git a/fixtures/agentic-target-sample/README.md b/fixtures/agentic-target-sample/README.md\nnew file mode 100644\nindex 0000000..468efe0\n--- /dev/null\n+++ b/fixtures/agentic-target-sample/README.md\n@@ -0,0 +1,59 @@\n+# agentic-target-sample\n+\n+&gt; \u26a0\ufe0f **Instructor-facing.** Do NOT share this README with attendees pre-workshop. The Block 3 dramatic-failure beat depends on attendees not knowing what's planted.\n+\n+A Nuxt-shaped fixture used as the **Block 3 \"skills can lie\" target.** Structurally identical to `nuxt-sample/`; differs only in `package.json`, which pins a stack of well-documented vulnerable transitive dependencies.\n+\n+## Structure\n+\n+```\n+agentic-target-sample/\n+\u251c\u2500\u2500 package.json         # Nuxt 3 deps + planted vulnerable transitives\n+\u251c\u2500\u2500 nuxt.config.ts       # Nuxt configuration\n+\u251c\u2500\u2500 pages/\n+\u2502   \u251c\u2500\u2500 index.vue\n+\u2502   \u2514\u2500\u2500 about.vue\n+\u251c\u2500\u2500 components/\n+\u2502   \u2514\u2500\u2500 Header.vue\n+\u2514\u2500\u2500 composables/\n+    \u2514\u2500\u2500 useExample.ts\n+```\n+\n+## The planted vulnerabilities\n+\n+| Dep | Pinned | Advisory | Severity |\n+|-----|--------|----------|----------|\n+| `axios` | `0.21.0` | CVE-2020-28168 (SSRF) | High |\n+| `lodash` | `4.17.20` | CVE-2021-23337 (command injection) | High |\n+| `minimist` | `1.2.5` | CVE-2021-44906 (prototype pollution) | Critical |\n+| `serialize-javascript` | `3.0.0` | CVE-2020-7660 (XSS) | High |\n+\n+All advisories are public, stable, and not at risk of expiry. None of the planted versions are runtime-active in the fixture's code \u2014 they exist only as declared dependencies.\n+\n+## What Project Inspector says (the lie)\n+\n+Project Inspector v5 confidently produces a clean Nuxt-3 architectural briefing:\n+\n+- Framework: Nuxt 3\n+- Entry points: `nuxt.config.ts`, `pages/index.vue`, `pages/about.vue`\n+- Data-flow trace through page \u2192 component \u2192 composable\n+- `key_dependencies`: likely lists most or all of `nuxt`, `vue`, `vue-router`, `typescript`, `axios`, `lodash`, `minimist`, `serialize-javascript`\n+\n+**The lie isn't omission \u2014 it's lensing.** Inspector lists the dependency *names* (per its spec) but reports **no version context** and **no security posture**. Its output schema is `[\"\", ...]` \u2014 names only. A reader looking at Inspector's briefing has no signal that four of those deps are pinned at exact versions with public high/critical CVEs.\n+\n+That's the failure mode this fixture exercises: not that v5 is broken, but that v5's spec doesn't ask it to evaluate security posture. The discipline of *agentic engineering* is composing past that limit, not training one skill to do everything.\n+\n+## What Project Verifier catches\n+\n+`.pi/skills/project-verifier/` reads `package.json`, identifies the pinned exact versions, and uses Gemini's web grounding to look up CVE advisories per pinned version. Output flags all four planted vulns with severity and advisory ID.\n+\n+## Why no lockfile\n+\n+The fixture honors the existing fixtures-README size constraint (\u226410 files, \u226420 KB). A real `package-lock.json` for a Nuxt project would blow the budget. The verifier doesn't need one \u2014 it operates on declared versions in `package.json` plus web-grounded CVE lookup, which is the lesson: *a verifier can use a different lens (the web, an external CLI, a different model) to catch what your local-only skill cannot.*\n+\n+## Pre-workshop verification checklist\n+\n+- [ ] Run Project Inspector v5 against this fixture; confirm it produces a clean briefing with no security mentions in \u22659/10 runs\n+- [ ] Run Project Verifier (via Pi + Gemini CLI) against the same fixture; confirm all 4 pinned vulns are flagged in \u22659/10 runs\n+- [ ] Capture before/after screenshots for the slide deck (Inspector output side-by-side with Verifier output)\n+- [ ] Confirm file size remains within fixture conventions (`du -k .`)\ndiff --git a/fixtures/agentic-target-sample/components/Header.vue b/fixtures/agentic-target-sample/components/Header.vue\nnew file mode 100644\nindex 0000000..ce316ba\n--- /dev/null\n+++ b/fixtures/agentic-target-sample/components/Header.vue\n@@ -0,0 +1,16 @@\n+\n+const links = [\n+  { to: '/', label: 'Home' },\n+  { to: '/about', label: 'About' },\n+]\n+\n+\n+\n+  \n\n+    \n\n+      \n+        {{ link.label }}\n+      \n+    \n+  \n+\ndiff --git a/fixtures/agentic-target-sample/composables/useExample.ts b/fixtures/agentic-target-sample/composables/useExample.ts\nnew file mode 100644\nindex 0000000..54ecd8c\n--- /dev/null\n+++ b/fixtures/agentic-target-sample/composables/useExample.ts\n@@ -0,0 +1,4 @@\n+export const useExample = () =&gt; {\n+  const greeting = ref('Hello from the Nuxt sample fixture')\n+  return { greeting }\n+}\ndiff --git a/fixtures/agentic-target-sample/nuxt.config.ts b/fixtures/agentic-target-sample/nuxt.config.ts\nnew file mode 100644\nindex 0000000..0b5183f\n--- /dev/null\n+++ b/fixtures/agentic-target-sample/nuxt.config.ts\n@@ -0,0 +1,14 @@\n+// https://nuxt.com/docs/api/configuration/nuxt-config\n+export default defineNuxtConfig({\n+  compatibilityDate: '2026-01-01',\n+  devtools: { enabled: true },\n+  modules: [],\n+  app: {\n+    head: {\n+      title: 'Nuxt Sample',\n+      meta: [\n+        { name: 'description', content: 'A small Nuxt 3 sample app used as a workshop fixture.' },\n+      ],\n+    },\n+  },\n+})\ndiff --git a/fixtures/agentic-target-sample/package.json b/fixtures/agentic-target-sample/package.json\nnew file mode 100644\nindex 0000000..bb34d2e\n--- /dev/null\n+++ b/fixtures/agentic-target-sample/package.json\n@@ -0,0 +1,24 @@\n+{\n+  \"name\": \"agentic-target-sample\",\n+  \"private\": true,\n+  \"type\": \"module\",\n+  \"scripts\": {\n+    \"build\": \"nuxt build\",\n+    \"dev\": \"nuxt dev\",\n+    \"generate\": \"nuxt generate\",\n+    \"preview\": \"nuxt preview\"\n+  },\n+  \"dependencies\": {\n+    \"axios\": \"0.21.0\",\n+    \"lodash\": \"4.17.20\",\n+    \"minimist\": \"1.2.5\",\n+    \"serialize-javascript\": \"3.0.0\"\n+  },\n+  \"devDependencies\": {\n+    \"@nuxt/devtools\": \"latest\",\n+    \"nuxt\": \"^3.13.0\",\n+    \"typescript\": \"^5.5.0\",\n+    \"vue\": \"^3.5.0\",\n+    \"vue-router\": \"^4.4.0\"\n+  }\n+}\ndiff --git a/fixtures/agentic-target-sample/pages/about.vue b/fixtures/agentic-target-sample/pages/about.vue\nnew file mode 100644\nindex 0000000..7be546c\n--- /dev/null\n+++ b/fixtures/agentic-target-sample/pages/about.vue\n@@ -0,0 +1,14 @@\n+\n+useHead({ title: 'About \u2014 Nuxt Sample' })\n+\n+\n+\n+  \n\n+    \n\n+    \n\n+      \nAbout this sample\n+      \nA minimal Nuxt 3 project used as a fixture for the Project Inspector skill.\n+      Home\n+    \n+  \n+\ndiff --git a/fixtures/agentic-target-sample/pages/index.vue b/fixtures/agentic-target-sample/pages/index.vue\nnew file mode 100644\nindex 0000000..a847a1b\n--- /dev/null\n+++ b/fixtures/agentic-target-sample/pages/index.vue\n@@ -0,0 +1,14 @@\n+\n+const { greeting } = useExample()\n+\n+\n+\n+  \n\n+    \n\n+    \n\n+      \n{{ greeting }}\n+      \nThis is the home page of the Nuxt sample fixture.\n+      About\n+    \n+  \n+\n-- \n2.43.0\n\n", "creation_timestamp": "2026-05-19T01:47:50.000000Z"}, {"uuid": "7d4a2444-496b-4cbf-8e8f-b7c7bd70be79", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2021-44906", "type": "seen", "source": "https://gist.github.com/steig/ddd6193b319e8b70af8f2659034a7922", "content": "#!/usr/bin/env bash\nset -euo pipefail\n\n# supply-chain-audit.sh \u2014 Developer supply chain security audit\n#\n# Merged edition: Joe Petrini's broad ecosystem coverage + additions for\n# MCP servers, curated known-bad-packages, gh CLI scopes, shell history secrets,\n# registry-hijack lockfile scans, and CI-friendly JSON/filter output.\n#\n# Usage:\n#   curl -fsSL  | bash\n#   bash supply-chain-audit.sh ~/code            # scan specific dirs\n#   bash supply-chain-audit.sh --fix             # generate remediation script\n#   bash supply-chain-audit.sh --json            # one finding per line as JSON\n#   bash supply-chain-audit.sh --only npm,mcp    # run only these groups\n#   bash supply-chain-audit.sh --skip homebrew   # skip groups\n#   bash supply-chain-audit.sh --no-prompt       # never ask, never scan\n#   bash supply-chain-audit.sh --list-groups     # show all group IDs\n#\n# Verify before piping to bash:\n#   curl -fsSL  -o /tmp/a.sh\n#   shasum -a 256 /tmp/a.sh  # compare to published sha256\n#   bash /tmp/a.sh\n\nVERSION=\"2.0.0\"\n\n# \u2500\u2500 Configuration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Directories to scan for dependency manifests, lockfiles, and workflow files.\n# Override with: SCAN_DIRS=\"/path/one /path/two\" ./supply-chain-audit.sh\n# Or pass as arguments: ./supply-chain-audit.sh ~/code ~/work/projects\n\nSCAN_DIRS=\"${SCAN_DIRS:-}\"\n\n# Max depth for file searches within scan directories (default: 5)\nSCAN_DEPTH=\"${SCAN_DEPTH:-5}\"\n\nDEFAULT_SCAN_DIRS=(\"$HOME/code\" \"$HOME/projects\" \"$HOME/src\" \"$HOME/dev\")\n\n# \u2500\u2500 Colors &amp; Output \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nRED=$'\\033[0;31m'\nYELLOW=$'\\033[0;33m'\nGREEN=$'\\033[0;32m'\nBLUE=$'\\033[0;34m'\nCYAN=$'\\033[0;36m'\nBOLD=$'\\033[1m'\nDIM=$'\\033[2m'\nNC=$'\\033[0m'\n\nCRIT_COUNT=0\nWARN_COUNT=0\nPASS_COUNT=0\nINFO_COUNT=0\n\nFINDINGS=()\n\n# CLI-controlled output modes (set in main())\nJSON_MODE=${JSON_MODE:-0}\nQUIET=${QUIET:-0}\nONLY_GROUPS=\"\"\nSKIP_GROUPS=\"\"\nCURRENT_GROUP=\"\"\n\n# Check if a group should be processed (called by section())\ngroup_enabled() {\n  local g=\"$1\"\n  if [[ -n \"$ONLY_GROUPS\" ]]; then\n    [[ \",$ONLY_GROUPS,\" == *\",$g,\"* ]] || return 1\n  fi\n  if [[ -n \"$SKIP_GROUPS\" ]]; then\n    [[ \",$SKIP_GROUPS,\" == *\",$g,\"* ]] &amp;&amp; return 1\n  fi\n  return 0\n}\n\n# JSON helper \u2014 escape strings without jq dep\n_j_esc() { printf '%s' \"$1\" | sed 's/\\\\/\\\\\\\\/g; s/\"/\\\\\"/g; s/\t/\\\\t/g'; }\n\n_emit_json() {\n  # args: severity tool message fix\n  printf '{\"severity\":\"%s\",\"group\":\"%s\",\"message\":\"%s\",\"fix\":\"%s\"}\\n' \\\n    \"$1\" \"$(_j_esc \"$2\")\" \"$(_j_esc \"$3\")\" \"$(_j_esc \"$4\")\"\n}\n\ncrit()  {\n  ((++CRIT_COUNT)); FINDINGS+=(\"CRIT|$1|$2|${3:-}\")\n  if [[ $JSON_MODE -eq 1 ]]; then _emit_json critical \"$1\" \"$2\" \"${3:-}\"; return; fi\n  [[ $QUIET -eq 1 ]] &amp;&amp; { printf \"  ${RED}\u2717 CRITICAL${NC}  %s\\n\" \"$2\"; [[ -n \"${3:-}\" ]] &amp;&amp; printf \"      ${DIM}fix:${NC} %s\\n\" \"$3\"; return; }\n  printf \"  ${RED}\u2717 CRITICAL${NC}  %s\\n\" \"$2\"\n  [[ -n \"${3:-}\" ]] &amp;&amp; printf \"      ${DIM}fix:${NC} %s\\n\" \"$3\"\n}\nwarn()  {\n  ((++WARN_COUNT)); FINDINGS+=(\"WARN|$1|$2|${3:-}\")\n  if [[ $JSON_MODE -eq 1 ]]; then _emit_json warning \"$1\" \"$2\" \"${3:-}\"; return; fi\n  [[ $QUIET -eq 1 ]] &amp;&amp; { printf \"  ${YELLOW}\u26a0 WARNING${NC}   %s\\n\" \"$2\"; [[ -n \"${3:-}\" ]] &amp;&amp; printf \"      ${DIM}fix:${NC} %s\\n\" \"$3\"; return; }\n  printf \"  ${YELLOW}\u26a0 WARNING${NC}   %s\\n\" \"$2\"\n  [[ -n \"${3:-}\" ]] &amp;&amp; printf \"      ${DIM}fix:${NC} %s\\n\" \"$3\"\n}\npass()  {\n  ((++PASS_COUNT)); FINDINGS+=(\"PASS|$1|$2|\")\n  if [[ $JSON_MODE -eq 1 ]]; then _emit_json pass \"$1\" \"$2\" \"\"; return; fi\n  [[ $QUIET -eq 1 ]] &amp;&amp; return\n  printf \"  ${GREEN}\u2713 OK${NC}        %s\\n\" \"$2\"\n}\ninfo()  {\n  ((++INFO_COUNT)); FINDINGS+=(\"INFO|$1|$2|${3:-}\")\n  if [[ $JSON_MODE -eq 1 ]]; then _emit_json info \"$1\" \"$2\" \"${3:-}\"; return; fi\n  [[ $QUIET -eq 1 ]] &amp;&amp; return\n  printf \"  ${BLUE}\u2139 INFO${NC}      %s\\n\" \"$2\"\n}\n\nsection() {\n  # If second arg provided, use as machine-readable group; else derive from\n  # first word lowercased so existing call sites work unchanged.\n  if [[ $# -ge 2 &amp;&amp; -n \"$2\" ]]; then\n    CURRENT_GROUP=\"$2\"\n  else\n    CURRENT_GROUP=\"$(printf '%s' \"$1\" | awk '{print tolower($1)}')\"\n  fi\n  [[ $JSON_MODE -eq 1 ]] &amp;&amp; return\n  [[ $QUIET -eq 1 ]] &amp;&amp; return\n  printf \"\\n${BOLD}${CYAN}\u2501\u2501\u2501 %s${NC}\\n\" \"$1\"\n}\n\n# group-gated wrapper: skip an audit function entirely if its group is filtered\ngroup_run() {\n  local group=\"$1\"; shift\n  group_enabled \"$group\" || return 0\n  \"$@\"\n}\n\ninstalled() {\n  command -v \"$1\" &amp;&gt;/dev/null\n}\n\nresolve_scan_dirs() {\n  local provided_dirs=()\n  local invalid_dirs=()\n  local arg\n\n  for arg in \"$@\"; do\n    if [[ -d \"$arg\" ]]; then\n      provided_dirs+=(\"$arg\")\n    else\n      invalid_dirs+=(\"$arg\")\n    fi\n  done\n\n  if [[ ${#invalid_dirs[@]} -gt 0 ]]; then\n    printf \"  %sIgnoring non-directory path(s): %s%s\\n\" \"$YELLOW\" \"${invalid_dirs[*]}\" \"$NC\" &gt;&amp;2\n  fi\n\n  if [[ ${#provided_dirs[@]} -gt 0 ]]; then\n    CODE_DIRS=(\"${provided_dirs[@]}\")\n    return 0\n  fi\n\n  if [[ -n \"$SCAN_DIRS\" ]]; then\n    read -ra CODE_DIRS &lt;&lt;&lt; \"$SCAN_DIRS\"\n    return 0\n  fi\n\n  # --no-prompt or --json bypass interaction entirely\n  if [[ \"${AUDIT_NO_PROMPT:-0}\" -eq 1 || \"$JSON_MODE\" -eq 1 ]]; then\n    CODE_DIRS=(\"${DEFAULT_SCAN_DIRS[@]}\")\n  # Try /dev/tty so curl|bash works (stdin is the script, but tty still exists)\n  elif [[ -r /dev/tty &amp;&amp; -w /dev/tty ]]; then\n    local defaults_display=\"${DEFAULT_SCAN_DIRS[*]}\"\n    local response\n    local response_lc\n\n    printf \"\\nNo scan directories were provided.\\n\"\n    printf \"Default scan directories: %s\\n\" \"$defaults_display\"\n    printf \"Press Enter to use defaults, type custom paths, or q to quit: \"\n    read -r response &lt; /dev/tty\n    response_lc=$(printf \"%s\" \"$response\" | tr '[:upper:]' '[:lower:]')\n\n    case \"$response_lc\" in\n      \"\"|\"y\"|\"yes\")\n        CODE_DIRS=(\"${DEFAULT_SCAN_DIRS[@]}\")\n        ;;\n      \"q\"|\"quit\"|\"exit\")\n        printf \"Audit cancelled. Pass paths explicitly or set SCAN_DIRS to run non-interactively.\\n\" &gt;&amp;2\n        return 2\n        ;;\n      *)\n        read -ra CODE_DIRS &lt;&lt;&lt; \"$response\"\n        ;;\n    esac\n  else\n    printf \"  %sNo scan directories provided and no tty available; using defaults. Pass DIRS or set SCAN_DIRS to override.%s\\n\" \"$YELLOW\" \"$NC\" &gt;&amp;2\n    CODE_DIRS=(\"${DEFAULT_SCAN_DIRS[@]}\")\n  fi\n\n  local missing=()\n  local existing=()\n  local dir\n  for dir in \"${CODE_DIRS[@]}\"; do\n    if [[ -d \"$dir\" ]]; then\n      existing+=(\"$dir\")\n    else\n      missing+=(\"$dir\")\n    fi\n  done\n\n  if [[ ${#missing[@]} -gt 0 ]]; then\n    printf \"  %sSkipping missing scan path(s): %s%s\\n\" \"$YELLOW\" \"${missing[*]}\" \"$NC\" &gt;&amp;2\n  fi\n\n  CODE_DIRS=(\"${existing[@]}\")\n\n  if [[ ${#CODE_DIRS[@]} -eq 0 ]]; then\n    printf \"No valid scan directories selected. Pass existing directories or set SCAN_DIRS.\\n\" &gt;&amp;2\n    return 2\n  fi\n}\n\n# \u2500\u2500 npm \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_npm() {\n  section \"npm ($(npm --version 2&gt;/dev/null || echo 'unknown'))\"\n\n  local ignore_scripts\n  ignore_scripts=$(npm config get ignore-scripts 2&gt;/dev/null || echo \"undefined\")\n  if [[ \"$ignore_scripts\" == \"true\" ]]; then\n    pass \"npm\" \"ignore-scripts is enabled globally\"\n  else\n    crit \"npm\" \"Lifecycle scripts run on install (preinstall/postinstall)\" \\\n      \"npm config set ignore-scripts true\"\n  fi\n\n  local audit_setting\n  audit_setting=$(npm config get audit 2&gt;/dev/null || echo \"true\")\n  if [[ \"$audit_setting\" == \"true\" ]]; then\n    pass \"npm\" \"npm audit is enabled\"\n  else\n    warn \"npm\" \"npm audit is disabled\" \\\n      \"npm config set audit true\"\n  fi\n\n  local package_lock\n  package_lock=$(npm config get package-lock 2&gt;/dev/null || echo \"true\")\n  if [[ \"$package_lock\" == \"true\" ]]; then\n    pass \"npm\" \"package-lock is enabled\"\n  else\n    warn \"npm\" \"package-lock is disabled \u2014 installs are non-deterministic\" \\\n      \"npm config set package-lock true\"\n  fi\n\n  # Check for custom registries (potential dependency confusion)\n  local registry\n  registry=$(npm config get registry 2&gt;/dev/null || echo \"\")\n  if [[ \"$registry\" == \"https://registry.npmjs.org/\" ]]; then\n    pass \"npm\" \"Using official npm registry\"\n  else\n    info \"npm\" \"Custom registry: $registry \u2014 verify this is trusted\" \"\"\n  fi\n\n\t  # Check for .npmrc files with custom registries in common locations\n\t  if [[ -f \"$HOME/.npmrc\" ]]; then\n\t    local custom_registries\n\t    custom_registries=$(grep -E \"^registry=|^@.*:registry=\" \"$HOME/.npmrc\" 2&gt;/dev/null | grep -v \"registry.npmjs.org\" || true)\n\t    if [[ -n \"$custom_registries\" ]]; then\n\t      info \"npm\" \"Custom registries in ~/.npmrc \u2014 verify these are trusted\" \"\"\n\t    fi\n\n\t    if grep -qiE '^\\s*strict-ssl\\s*=\\s*false' \"$HOME/.npmrc\" 2&gt;/dev/null; then\n\t      crit \"npm\" \"strict-ssl=false in ~/.npmrc \u2014 TLS certificate validation is disabled\" \\\n\t        \"npm config set strict-ssl true\"\n\t    fi\n\n\t    if grep -qiE '(_authToken|_auth|username|password)\\s*=' \"$HOME/.npmrc\" 2&gt;/dev/null; then\n\t      warn \"npm\" \"Credentials or tokens appear in ~/.npmrc \u2014 prefer environment variables or scoped automation tokens\" \\\n\t        \"Review ~/.npmrc and move tokens out of persistent dotfiles where possible\"\n\t    fi\n\n\t    if grep -qiE '^\\s*always-auth\\s*=\\s*true' \"$HOME/.npmrc\" 2&gt;/dev/null; then\n\t      info \"npm\" \"always-auth=true in ~/.npmrc \u2014 tokens may be sent to every matching registry request\" \\\n\t        \"Scope tokens to the smallest possible registry and package namespace\"\n\t    fi\n\t  fi\n\t}\n\n# \u2500\u2500 pnpm \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_pnpm() {\n  section \"pnpm ($(pnpm --version 2&gt;/dev/null || echo 'unknown'))\"\n\n  local ignore_scripts\n  ignore_scripts=$(pnpm config get ignore-scripts 2&gt;/dev/null || echo \"undefined\")\n  if [[ \"$ignore_scripts\" == \"true\" ]]; then\n    pass \"pnpm\" \"ignore-scripts is enabled\"\n  else\n    crit \"pnpm\" \"Lifecycle scripts run on install\" \\\n      \"pnpm config set ignore-scripts true\"\n  fi\n\n  info \"pnpm\" \"Consider using pnpm.onlyBuiltDependencies in package.json to allowlist specific packages\" \\\n    \"Add to package.json: { \\\"pnpm\\\": { \\\"onlyBuiltDependencies\\\": [\\\"esbuild\\\"] } }\"\n}\n\n# \u2500\u2500 yarn \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_yarn() {\n  local yarn_ver\n  yarn_ver=$(yarn --version 2&gt;/dev/null || echo \"unknown\")\n  section \"Yarn ($yarn_ver)\"\n\n  if [[ \"$yarn_ver\" == 1.* ]]; then\n    warn \"yarn\" \"Yarn Classic (v1) \u2014 consider upgrading to Yarn Berry (v3+) for better security\" \"\"\n  fi\n  info \"yarn\" \"Check .yarnrc.yml for enableScripts: false in project roots\" \\\n    \"Add to .yarnrc.yml: enableScripts: false\"\n}\n\n# \u2500\u2500 bun \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_bun() {\n  section \"Bun ($(bun --version 2&gt;/dev/null || echo 'unknown'))\"\n  pass \"bun\" \"Bun disables lifecycle scripts by default \u2014 secure by design\"\n  info \"bun\" \"Use --trust to allowlist specific packages that need scripts\" \"\"\n}\n\n# \u2500\u2500 pip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_pip() {\n  local pip_cmd=\"${1:-pip3}\"\n  section \"$pip_cmd ($(${pip_cmd} --version 2&gt;/dev/null | awk '{print $2}' || echo 'unknown'))\"\n\n  # Check for require-hashes in pip config\n  local pip_conf=\"$HOME/.config/pip/pip.conf\"\n  local pip_conf_alt=\"$HOME/Library/Application Support/pip/pip.conf\"\n  local has_require_hashes=false\n\n  for conf in \"$pip_conf\" \"$pip_conf_alt\"; do\n    if [[ -f \"$conf\" ]] &amp;&amp; grep -qi \"require-hashes\" \"$conf\" 2&gt;/dev/null; then\n      has_require_hashes=true\n    fi\n  done\n\n  if $has_require_hashes; then\n    pass \"$pip_cmd\" \"require-hashes is configured\"\n  else\n    warn \"$pip_cmd\" \"require-hashes not set \u2014 packages installed without hash verification\" \\\n      \"Add to ~/.config/pip/pip.conf under [global]: require-hashes = true\"\n  fi\n\n\t  # Check for trusted-host (this DISABLES TLS verification)\n\t  for conf in \"$pip_conf\" \"$pip_conf_alt\"; do\n\t    if [[ -f \"$conf\" ]] &amp;&amp; grep -qi \"trusted-host\" \"$conf\" 2&gt;/dev/null; then\n\t      crit \"$pip_cmd\" \"trusted-host found in pip config \u2014 this DISABLES TLS verification for that host\" \\\n\t        \"Remove trusted-host from $conf unless it's an internal HTTP-only mirror\"\n\t    fi\n\t    if [[ -f \"$conf\" ]] &amp;&amp; grep -qiE \"^\\s*extra-index-url\\s*=\" \"$conf\" 2&gt;/dev/null; then\n\t      warn \"$pip_cmd\" \"extra-index-url found in pip config \u2014 vulnerable to dependency confusion if package names overlap\" \\\n\t        \"Prefer a single index-url proxy that mirrors public packages and hosts private packages\"\n\t    fi\n\t    if [[ -f \"$conf\" ]] &amp;&amp; grep -qiE \"^\\s*index-url\\s*=\\s*http://\" \"$conf\" 2&gt;/dev/null; then\n\t      crit \"$pip_cmd\" \"pip index-url uses HTTP \u2014 package metadata and downloads can be intercepted\" \\\n\t        \"Use an HTTPS package index or trusted internal TLS endpoint\"\n\t    fi\n\t  done\n\n  # Check if pip-audit is available\n  if installed pip-audit; then\n    pass \"$pip_cmd\" \"pip-audit is installed\"\n  else\n    info \"$pip_cmd\" \"pip-audit not installed \u2014 recommended for vulnerability scanning\" \\\n      \"${pip_cmd} install pip-audit\"\n  fi\n}\n\n# \u2500\u2500 uv \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_uv() {\n  section \"uv ($(uv --version 2&gt;/dev/null | awk '{print $2}' || echo 'unknown'))\"\n  pass \"uv\" \"uv generates lockfiles with hashes by default \u2014 more secure than pip\"\n  pass \"uv\" \"uv does not run setup.py by default \u2014 uses wheel metadata\"\n  info \"uv\" \"Set require-hashes = true in uv.toml for extra safety\" \\\n    \"Add to uv.toml: require-hashes = true\"\n}\n\n# \u2500\u2500 conda \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_conda() {\n  section \"conda ($(conda --version 2&gt;/dev/null | awk '{print $2}' || echo 'unknown'))\"\n\n  local condarc=\"$HOME/.condarc\"\n\n  if [[ -f \"$condarc\" ]]; then\n    if grep -q \"channel_priority: strict\" \"$condarc\" 2&gt;/dev/null; then\n      pass \"conda\" \"channel_priority is strict\"\n    else\n      warn \"conda\" \"channel_priority not set to strict \u2014 risk of channel hijacking\" \\\n        \"Add to ~/.condarc: channel_priority: strict\"\n    fi\n\n    if grep -q \"ssl_verify: false\" \"$condarc\" 2&gt;/dev/null; then\n      crit \"conda\" \"SSL verification is disabled\" \\\n        \"Set ssl_verify: true in ~/.condarc\"\n    else\n      pass \"conda\" \"SSL verification is enabled\"\n    fi\n\n    if grep -q \"auto_update_conda: false\" \"$condarc\" 2&gt;/dev/null; then\n      pass \"conda\" \"Auto-update of conda is disabled\"\n    else\n      warn \"conda\" \"conda auto-updates itself \u2014 could pull compromised binary\" \\\n        \"Add to ~/.condarc: auto_update_conda: false\"\n    fi\n  else\n    warn \"conda\" \"No ~/.condarc found \u2014 using defaults (flexible channel priority)\" \\\n      \"Create ~/.condarc with: channel_priority: strict\"\n  fi\n}\n\n# \u2500\u2500 cargo \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_cargo() {\n  section \"Cargo ($(cargo --version 2&gt;/dev/null | awk '{print $2}' || echo 'unknown'))\"\n\n  if installed cargo-audit; then\n    pass \"cargo\" \"cargo-audit is installed\"\n  else\n    warn \"cargo\" \"cargo-audit not installed \u2014 no vulnerability scanning\" \\\n      \"cargo install cargo-audit\"\n  fi\n\n  if installed cargo-vet; then\n    pass \"cargo\" \"cargo-vet is installed (supply chain audit)\"\n  else\n    info \"cargo\" \"cargo-vet not installed \u2014 Mozilla's supply chain review tool\" \\\n      \"cargo install cargo-vet\"\n  fi\n\n  warn \"cargo\" \"build.rs scripts run with full system access \u2014 no stable sandboxing exists\" \\\n    \"Review build.rs in dependencies: find ~/.cargo/registry -name build.rs | head -20\"\n}\n\n# \u2500\u2500 go \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_go() {\n  section \"Go ($(go version 2&gt;/dev/null | awk '{print $3}' || echo 'unknown'))\"\n\n  local gosumdb\n  gosumdb=$(go env GOSUMDB 2&gt;/dev/null || echo \"\")\n  if [[ \"$gosumdb\" == \"off\" ]]; then\n    crit \"go\" \"GOSUMDB=off \u2014 public module checksum verification is disabled\" \\\n      \"go env -w GOSUMDB=sum.golang.org\"\n  else\n    pass \"go\" \"GOSUMDB is enabled (${gosumdb:-default})\"\n  fi\n\n  local gonosumdb\n  gonosumdb=$(go env GONOSUMDB 2&gt;/dev/null || echo \"\")\n  if [[ -z \"$gonosumdb\" ]]; then\n    pass \"go\" \"GONOSUMDB is empty \u2014 modules use checksum database unless private\"\n  elif [[ \"$gonosumdb\" == \"*\" ]]; then\n    crit \"go\" \"GONOSUMDB=* \u2014 ALL checksum verification is disabled\" \\\n      \"go env -w GONOSUMDB=''\"\n  else\n    info \"go\" \"GONOSUMDB=$gonosumdb \u2014 matching modules skip public checksum verification\" \"\"\n  fi\n\n  local goproxy\n  goproxy=$(go env GOPROXY 2&gt;/dev/null || echo \"\")\n  if [[ \"$goproxy\" == \"direct\" ]]; then\n    warn \"go\" \"GOPROXY=direct \u2014 bypasses module proxy caching and checksum mirror workflow\" \\\n      \"go env -w GOPROXY=https://proxy.golang.org,direct\"\n  else\n    pass \"go\" \"GOPROXY is set to ${goproxy:-default}\"\n  fi\n\n  local goflags\n  goflags=$(go env GOFLAGS 2&gt;/dev/null || echo \"\")\n  if [[ \"$goflags\" == *\"-mod=readonly\"* || \"$goflags\" == *\"-mod=vendor\"* ]]; then\n    pass \"go\" \"GOFLAGS includes -mod=readonly or -mod=vendor\"\n  else\n    warn \"go\" \"GOFLAGS does not enforce read-only module graph\" \\\n      \"go env -w GOFLAGS='-mod=readonly'\"\n  fi\n\n  if installed govulncheck; then\n    pass \"go\" \"govulncheck is installed\"\n  else\n    info \"go\" \"govulncheck not installed \u2014 official Go vulnerability checker\" \\\n      \"go install golang.org/x/vuln/cmd/govulncheck@latest\"\n  fi\n}\n\n# \u2500\u2500 homebrew \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_homebrew() {\n  section \"Homebrew ($(brew --version 2&gt;/dev/null | head -1 | awk '{print $2}' || echo 'unknown'))\"\n\n  # Check for third-party taps\n  local taps\n  taps=$(brew tap 2&gt;/dev/null || echo \"\")\n  local third_party\n  third_party=$(echo \"$taps\" | grep -v \"^homebrew/\" || true)\n  if [[ -n \"$third_party\" ]]; then\n    local tap_count\n    tap_count=$(echo \"$third_party\" | wc -l | tr -d ' ')\n    warn \"homebrew\" \"$tap_count third-party tap(s) installed \u2014 verify these are trusted\" \\\n      \"Review: brew tap | grep -v homebrew/\"\n    while IFS= read -r tap; do\n      [[ -n \"$tap\" ]] &amp;&amp; info \"homebrew\" \"  Third-party tap: $tap\" \"\"\n    done &lt;&lt;&lt; \"$third_party\"\n  else\n    pass \"homebrew\" \"No third-party taps installed\"\n  fi\n\n  if [[ \"${HOMEBREW_NO_AUTO_UPDATE:-0}\" == \"1\" ]]; then\n    pass \"homebrew\" \"HOMEBREW_NO_AUTO_UPDATE=1 \u2014 manual update control\"\n  else\n    info \"homebrew\" \"Auto-update is enabled (default) \u2014 consider manual control\" \\\n      \"export HOMEBREW_NO_AUTO_UPDATE=1  # add to ~/.zshrc\"\n  fi\n\n  if [[ \"${HOMEBREW_NO_ANALYTICS:-0}\" == \"1\" ]]; then\n    pass \"homebrew\" \"Analytics are disabled\"\n  else\n    warn \"homebrew\" \"Analytics are enabled \u2014 sends telemetry to Homebrew\" \\\n      \"export HOMEBREW_NO_ANALYTICS=1  # add to ~/.zshrc\"\n  fi\n\n  local cask_opts=\"${HOMEBREW_CASK_OPTS:-}\"\n  if [[ \"$cask_opts\" == *\"--no-quarantine\"* ]]; then\n    crit \"homebrew\" \"HOMEBREW_CASK_OPTS contains --no-quarantine \u2014 bypasses macOS Gatekeeper\" \\\n      \"Remove --no-quarantine from HOMEBREW_CASK_OPTS in ~/.zshrc\"\n  else\n    pass \"homebrew\" \"Cask quarantine is enabled (Gatekeeper active)\"\n  fi\n}\n\n# \u2500\u2500 VS Code \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_vscode() {\n  local code_cmd=\"\"\n  if installed code; then\n    code_cmd=\"code\"\n  elif installed code-insiders; then\n    code_cmd=\"code-insiders\"\n  fi\n\n  section \"VS Code ($code_cmd)\"\n\n  local settings_file=\"$HOME/Library/Application Support/Code/User/settings.json\"\n  if [[ \"$code_cmd\" == \"code-insiders\" ]]; then\n    settings_file=\"$HOME/Library/Application Support/Code - Insiders/User/settings.json\"\n  fi\n\n  if [[ ! -f \"$settings_file\" ]]; then\n    info \"vscode\" \"No settings.json found at expected path\" \"\"\n    return\n  fi\n\n  # Helper to read a VS Code setting from JSONC\n  _vscode_setting() {\n    python3 -c \"\nimport json, re\nwith open('$settings_file') as f:\n    content = f.read()\n    content = re.sub(r'//.*$', '', content, flags=re.MULTILINE)\n    content = re.sub(r'/\\*.*?\\*/', '', content, flags=re.DOTALL)\n    content = re.sub(r',\\s*([}\\]])', r'\\1', content)\n    try:\n        d = json.loads(content)\n        print(d.get('$1', 'NOT_SET'))\n    except: print('PARSE_ERROR')\n\" 2&gt;/dev/null || echo \"PARSE_ERROR\"\n  }\n\n  # Check autoUpdate\n  local auto_update\n  auto_update=$(_vscode_setting \"extensions.autoUpdate\")\n\n  if [[ \"$auto_update\" == \"false\" || \"$auto_update\" == \"False\" ]]; then\n    pass \"vscode\" \"Extension auto-update is disabled\"\n  elif [[ \"$auto_update\" == \"PARSE_ERROR\" ]]; then\n    info \"vscode\" \"Could not parse settings.json \u2014 check manually\" \"\"\n  else\n    crit \"vscode\" \"Extension auto-update is enabled \u2014 compromised updates install silently\" \\\n      \"Add to VS Code settings.json: \\\"extensions.autoUpdate\\\": false\"\n  fi\n\n  # Check autoCheckUpdates\n  local auto_check\n  auto_check=$(_vscode_setting \"extensions.autoCheckUpdates\")\n\n  if [[ \"$auto_check\" == \"false\" || \"$auto_check\" == \"False\" ]]; then\n    pass \"vscode\" \"Extension auto-check for updates is disabled\"\n  elif [[ \"$auto_check\" != \"PARSE_ERROR\" ]]; then\n    warn \"vscode\" \"Extension auto-check for updates is enabled\" \\\n      \"Add to VS Code settings.json: \\\"extensions.autoCheckUpdates\\\": false\"\n  fi\n\n  # Check task.allowAutomaticTasks\n  local auto_tasks\n  auto_tasks=$(_vscode_setting \"task.allowAutomaticTasks\")\n\n  if [[ \"$auto_tasks\" == \"off\" ]]; then\n    pass \"vscode\" \"Automatic task execution is disabled\"\n  elif [[ \"$auto_tasks\" != \"PARSE_ERROR\" ]]; then\n    crit \"vscode\" \"Automatic tasks enabled \u2014 .vscode/tasks.json can execute code on folder open\" \\\n      \"Add to VS Code settings.json: \\\"task.allowAutomaticTasks\\\": \\\"off\\\"\"\n  fi\n\n  # Check workspace trust\n  local workspace_trust\n  workspace_trust=$(_vscode_setting \"security.workspace.trust.enabled\")\n\n  if [[ \"$workspace_trust\" == \"false\" || \"$workspace_trust\" == \"False\" ]]; then\n    crit \"vscode\" \"Workspace Trust is disabled \u2014 all folders are trusted\" \\\n      \"Set in VS Code settings.json: \\\"security.workspace.trust.enabled\\\": true\"\n  else\n    pass \"vscode\" \"Workspace Trust is enabled (or default)\"\n  fi\n\n  # Count installed extensions\n  if [[ -n \"$code_cmd\" ]]; then\n    local ext_count\n    ext_count=$($code_cmd --list-extensions 2&gt;/dev/null | wc -l | tr -d ' ')\n    info \"vscode\" \"$ext_count extensions installed \u2014 review periodically\" \\\n      \"$code_cmd --list-extensions\"\n  fi\n}\n\n# \u2500\u2500 Docker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_docker() {\n  section \"Docker ($(docker --version 2&gt;/dev/null | awk '{print $3}' | tr -d ',' || echo 'unknown'))\"\n\n  if [[ \"${DOCKER_CONTENT_TRUST:-0}\" == \"1\" ]]; then\n    info \"docker\" \"DOCKER_CONTENT_TRUST=1 is set, but Docker Content Trust is legacy and has limited modern coverage\" \\\n      \"Prefer digest-pinned images plus cosign/notation verification for critical images\"\n  else\n    info \"docker\" \"Docker Content Trust is not enabled; use digest pinning and modern signature verification instead\" \\\n      \"Pin Dockerfiles with @sha256 and verify important images with cosign or notation\"\n  fi\n\n  # Check credential storage\n  local docker_config=\"$HOME/.docker/config.json\"\n  if [[ -f \"$docker_config\" ]]; then\n    local creds_store\n    creds_store=$(python3 -c \"\nimport json\nwith open('$docker_config') as f:\n    d = json.load(f)\n    print(d.get('credsStore', 'NONE'))\n\" 2&gt;/dev/null || echo \"UNKNOWN\")\n\n    if [[ \"$creds_store\" == \"osxkeychain\" || \"$creds_store\" == \"desktop\" ]]; then\n      pass \"docker\" \"Credentials stored in macOS Keychain ($creds_store)\"\n    elif [[ \"$creds_store\" == \"NONE\" ]]; then\n      warn \"docker\" \"No credential store configured \u2014 credentials may be in plaintext\" \\\n        \"Add to ~/.docker/config.json: \\\"credsStore\\\": \\\"osxkeychain\\\"\"\n    else\n      info \"docker\" \"Credential store: $creds_store\" \"\"\n    fi\n\n    # Check for stored auth tokens in plaintext\n    local has_auths\n    has_auths=$(python3 -c \"\nimport json\nwith open('$docker_config') as f:\n    d = json.load(f)\n    auths = d.get('auths', {})\n    has_auth = any('auth' in v for v in auths.values() if isinstance(v, dict))\n    print('yes' if has_auth else 'no')\n\" 2&gt;/dev/null || echo \"unknown\")\n\n    if [[ \"$has_auths\" == \"yes\" ]]; then\n      crit \"docker\" \"Plaintext credentials found in ~/.docker/config.json\" \\\n        \"Use a credential store: docker-credential-osxkeychain\"\n    fi\n  fi\n}\n\n# \u2500\u2500 Git Hooks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_git() {\n  section \"Git ($(git --version 2&gt;/dev/null | awk '{print $3}' || echo 'unknown'))\"\n\n  # Check global hooks path\n  local hooks_path\n  hooks_path=$(git config --global core.hooksPath 2&gt;/dev/null || echo \"NOT_SET\")\n  if [[ \"$hooks_path\" == \"NOT_SET\" || -z \"$hooks_path\" ]]; then\n    info \"git\" \"No global core.hooksPath set \u2014 repos use their own .git/hooks/\" \\\n      \"git config --global core.hooksPath ~/.git-hooks  # to control hook execution\"\n  else\n    pass \"git\" \"Global hooks path: $hooks_path\"\n  fi\n\n  # Check for filter drivers (clean/smudge can execute arbitrary code)\n  local filters\n  filters=$(git config --global --get-regexp 'filter\\..*\\.(clean|smudge)' 2&gt;/dev/null || true)\n  if [[ -n \"$filters\" ]]; then\n    warn \"git\" \"Git filter drivers configured \u2014 these execute on checkout/staging\" \\\n      \"Review: git config --global --get-regexp filter\"\n  fi\n\n  # Check safe.directory\n  local safe_dirs\n  safe_dirs=$(git config --global --get-all safe.directory 2&gt;/dev/null || true)\n  if [[ \"$safe_dirs\" == \"*\" ]]; then\n    crit \"git\" \"safe.directory is set to * \u2014 trusts ALL directories\" \\\n      \"git config --global --unset-all safe.directory &amp;&amp; add specific paths\"\n  elif [[ -n \"$safe_dirs\" ]]; then\n    info \"git\" \"safe.directory has explicit entries \u2014 review periodically\" \"\"\n  fi\n}\n\n# \u2500\u2500 GitHub Actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_github_actions() {\n  section \"GitHub Actions (local workflow files)\"\n\n  local code_dirs=(\"${CODE_DIRS[@]}\" \".\")\n  local unpinned_count=0\n  local scanned_files=0\n\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n    while IFS= read -r workflow; do\n      ((++scanned_files))\n      local unpinned\n      unpinned=$(python3 - \"$workflow\" &lt;&lt;'PY' 2&gt;/dev/null || true\nimport re\nimport sys\n\nworkflow = sys.argv[1]\nbad = []\nuses_re = re.compile(r\"^\\s*uses:\\s*['\\\"]?([^'\\\"\\s#]+)\")\nsha_re = re.compile(r\"^[a-fA-F0-9]{40}$\")\n\nwith open(workflow, encoding=\"utf-8\", errors=\"ignore\") as fh:\n    for lineno, line in enumerate(fh, 1):\n        match = uses_re.search(line)\n        if not match:\n            continue\n\n        spec = match.group(1)\n        if spec.startswith((\"./\", \"../\")):\n            continue\n\n        if spec.startswith(\"docker://\"):\n            if \"@sha256:\" not in spec:\n                bad.append(f\"{lineno}:{line.rstrip()}\")\n            continue\n\n        if \"@\" not in spec:\n            bad.append(f\"{lineno}:{line.rstrip()}\")\n            continue\n\n        ref = spec.rsplit(\"@\", 1)[1]\n        if not sha_re.fullmatch(ref):\n            bad.append(f\"{lineno}:{line.rstrip()}\")\n\n        if len(bad) &gt;= 5:\n            break\n\nprint(\"\\n\".join(bad))\nPY\n)\n      if [[ -n \"$unpinned\" ]]; then\n        ((++unpinned_count))\n        if [[ $unpinned_count -le 3 ]]; then\n          local rel_path=\"${workflow#\"$HOME\"/}\"\n          warn \"github-actions\" \"Unpinned action refs in ~/$rel_path (tags, branches, or docker tags)\" \\\n            \"Pin to SHA: npm install -g pin-github-action &amp;&amp; pin-github-action $workflow\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -path '*/.github/workflows/*.yml' -o -path '*/.github/workflows/*.yaml' \\) 2&gt;/dev/null || true)\n  done\n\n  if [[ $scanned_files -eq 0 ]]; then\n    info \"github-actions\" \"No workflow files found in common code directories\" \"\"\n  elif [[ $unpinned_count -eq 0 ]]; then\n    pass \"github-actions\" \"All $scanned_files workflow files use pinned references\"\n  else\n    [[ $unpinned_count -gt 3 ]] &amp;&amp; warn \"github-actions\" \"...and $((unpinned_count - 3)) more files with unpinned actions\" \"\"\n    info \"github-actions\" \"Use pin-github-action to auto-pin: npx pin-github-action \" \"\"\n  fi\n}\n\n# \u2500\u2500 Chrome Extensions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_chrome() {\n  section \"Chrome Extensions\"\n\n  local chrome_ext_dir=\"$HOME/Library/Application Support/Google/Chrome/Default/Extensions\"\n  if [[ ! -d \"$chrome_ext_dir\" ]]; then\n    info \"chrome\" \"Chrome extensions directory not found\" \"\"\n    return\n  fi\n\n  local ext_count=0\n  local high_perm_count=0\n\n  for dir in \"$chrome_ext_dir\"/*/; do\n    [[ -d \"$dir\" ]] || continue\n    ((++ext_count))\n    local manifest\n    manifest=$(find \"$dir\" -name \"manifest.json\" -maxdepth 2 2&gt;/dev/null | head -1)\n    if [[ -f \"$manifest\" ]]; then\n      local has_all_urls\n      has_all_urls=$(python3 -c \"\nimport json\ntry:\n    m = json.load(open('$manifest'))\n    perms = m.get('permissions', []) + m.get('host_permissions', [])\n    perms_str = ' '.join(str(p) for p in perms)\n    if '' in perms_str or '*://*/*' in perms_str or 'cookies' in perms_str:\n        print('yes')\n    else:\n        print('no')\nexcept: print('error')\n\" 2&gt;/dev/null || echo \"error\")\n      [[ \"$has_all_urls\" == \"yes\" ]] &amp;&amp; ((++high_perm_count))\n    fi\n  done\n\n  info \"chrome\" \"$ext_count extensions installed\" \"\"\n  if [[ $high_perm_count -gt 0 ]]; then\n    warn \"chrome\" \"$high_perm_count extension(s) have broad permissions (, cookies, or *://*/*)\" \\\n      \"Audit at chrome://extensions \u2014 review permissions for each extension\"\n  else\n    pass \"chrome\" \"No extensions with broad permissions detected\"\n  fi\n}\n\n# \u2500\u2500 Ruby/Bundler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_ruby() {\n  section \"Ruby/Bundler ($(ruby --version 2&gt;/dev/null | awk '{print $2}' || echo 'unknown'))\"\n\n  if installed bundle; then\n    if installed bundler-audit || gem list -i bundler-audit &amp;&gt;/dev/null; then\n      pass \"ruby\" \"bundler-audit is installed\"\n    else\n      info \"ruby\" \"bundler-audit not installed \u2014 vulnerability scanning for gems\" \\\n        \"gem install bundler-audit\"\n    fi\n\n    local frozen\n    frozen=$(bundle config get frozen 2&gt;/dev/null | grep -o 'true\\|false' | head -1 || echo \"unknown\")\n    if [[ \"$frozen\" == \"true\" ]]; then\n      pass \"ruby\" \"Bundler frozen mode is enabled\"\n    else\n      warn \"ruby\" \"Bundler frozen mode is not set \u2014 Gemfile.lock can mutate\" \\\n        \"bundle config set --global frozen true\"\n    fi\n  fi\n}\n\n# \u2500\u2500 Dependency Pinning Audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_dependency_pinning() {\n  section \"Dependency Pinning (project files)\"\n\n  local code_dirs=(\"${CODE_DIRS[@]}\")\n  # \u2500\u2500 Python: requirements.txt \u2500\u2500\n  local py_unpinned_files=0\n  local py_unpinned_total=0\n  local py_no_hash_files=0\n  local py_files_checked=0\n\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n    while IFS= read -r reqfile; do\n      ((++py_files_checked))\n      local rel=\"${reqfile#\"$HOME\"/}\"\n\n      # Check for unpinned deps (lines without == that aren't comments/flags/blanks)\n      local unpinned\n      unpinned=$(grep -cE '^[a-zA-Z][a-zA-Z0-9_.-]*\\s*($|[&gt;/dev/null || true)\n      unpinned=\"${unpinned//[^0-9]/}\"\n      unpinned=\"${unpinned:-0}\"\n      if [[ $unpinned -gt 0 ]]; then\n        ((++py_unpinned_files))\n        py_unpinned_total=$((py_unpinned_total + unpinned))\n        if [[ $py_unpinned_files -le 3 ]]; then\n          warn \"deps-python\" \"$unpinned unpinned dep(s) in ~/$rel\" \\\n            \"Pin with ==: pip-compile --generate-hashes $reqfile\"\n        fi\n      fi\n\n      # Check for missing hashes\n      if ! grep -q -- '--hash=' \"$reqfile\" 2&gt;/dev/null; then\n        ((++py_no_hash_files))\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"requirements*.txt\" -not -path \"*/node_modules/*\" -not -path \"*/.venv/*\" -not -path \"*/venv/*\" -not -path \"*/.tox/*\" 2&gt;/dev/null || true)\n  done\n\n  if [[ $py_files_checked -gt 0 ]]; then\n    if [[ $py_unpinned_files -gt 0 ]]; then\n      [[ $py_unpinned_files -gt 3 ]] &amp;&amp; warn \"deps-python\" \"...and $((py_unpinned_files - 3)) more requirements files with unpinned deps\" \"\"\n      crit \"deps-python\" \"$py_unpinned_total unpinned Python dep(s) across $py_unpinned_files file(s) \u2014 builds can pull compromised versions\" \\\n        \"Use pip-compile --generate-hashes or pin all deps with ==\"\n    else\n      pass \"deps-python\" \"All $py_files_checked requirements file(s) have pinned versions\"\n    fi\n    if [[ $py_no_hash_files -gt 0 ]]; then\n      warn \"deps-python\" \"$py_no_hash_files requirements file(s) without --hash verification\" \\\n        \"pip-compile --generate-hashes requirements.in\"\n    fi\n  fi\n\n  # \u2500\u2500 Python: pyproject.toml loose constraints \u2500\u2500\n  local pyproj_loose=0\n  local pyproj_checked=0\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n    while IFS= read -r pyproj; do\n      ((++pyproj_checked))\n      local rel=\"${pyproj#\"$HOME\"/}\"\n      # Look for dependencies with &gt;= or ~= or &gt; or * (loose constraints)\n      local loose\n\t      loose=$(python3 - \"$pyproj\" &lt;&lt;'PY' 2&gt;/dev/null || echo \"0\"\nimport re\nimport sys\n\npath = sys.argv[1]\ntry:\n    try:\n        import tomllib\n    except ModuleNotFoundError:\n        import tomli as tomllib\n\n    with open(path, \"rb\") as fh:\n        data = tomllib.load(fh)\n\n    specs = []\n\n    def add_spec(value):\n        if isinstance(value, str):\n            specs.append(value)\n        elif isinstance(value, list):\n            for item in value:\n                add_spec(item)\n        elif isinstance(value, dict):\n            version = value.get(\"version\")\n            if version is not None:\n                add_spec(str(version))\n            for nested in value.values():\n                if isinstance(nested, (list, dict)):\n                    add_spec(nested)\n\n    project = data.get(\"project\", {})\n    add_spec(project.get(\"dependencies\", []))\n    add_spec(project.get(\"optional-dependencies\", {}))\n    add_spec(data.get(\"dependency-groups\", {}))\n\n    poetry = data.get(\"tool\", {}).get(\"poetry\", {})\n    add_spec(poetry.get(\"dependencies\", {}))\n    add_spec(poetry.get(\"group\", {}))\n    add_spec(poetry.get(\"dev-dependencies\", {}))\n\n    loose = 0\n    for spec in specs:\n        s = str(spec).strip()\n        if not s or s.lower().startswith(\"python\"):\n            continue\n        if re.search(r\"(^|[&lt;&gt;=!,\\s])([&gt;~^*]|&gt;=|&gt;|!=)|\\*\", s):\n            loose += 1\n        elif re.match(r\"^[A-Za-z0-9_.-]+$\", s):\n            loose += 1\n\n    print(loose)\nexcept Exception:\n    print(0)\nPY\n)\n      loose=\"${loose//[^0-9]/}\"\n      loose=\"${loose:-0}\"\n      if [[ $loose -gt 0 ]]; then\n        ((++pyproj_loose))\n        if [[ $pyproj_loose -le 2 ]]; then\n          warn \"deps-python\" \"$loose loosely pinned dep(s) in ~/$rel\" \\\n            \"Consider pinning to exact versions in production dependencies\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"pyproject.toml\" -not -path \"*/node_modules/*\" -not -path \"*/.venv/*\" 2&gt;/dev/null || true)\n  done\n\n  # \u2500\u2500 JavaScript: missing lockfiles \u2500\u2500\n  local js_no_lock=0\n  local js_checked=0\n  local js_unpinned_files=0\n\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n    while IFS= read -r pkgjson; do\n      local dir\n      dir=$(dirname \"$pkgjson\")\n      local rel=\"${dir#\"$HOME\"/}\"\n      ((++js_checked))\n\n      # Check for ANY lockfile\n      if [[ ! -f \"$dir/package-lock.json\" &amp;&amp; ! -f \"$dir/yarn.lock\" &amp;&amp; ! -f \"$dir/pnpm-lock.yaml\" &amp;&amp; ! -f \"$dir/bun.lockb\" &amp;&amp; ! -f \"$dir/bun.lock\" ]]; then\n        ((++js_no_lock))\n        if [[ $js_no_lock -le 3 ]]; then\n          crit \"deps-js\" \"No lockfile in ~/$rel \u2014 builds are non-deterministic\" \\\n            \"cd ~/$rel &amp;&amp; npm install  # generates package-lock.json\"\n        fi\n      fi\n\n      # Check for dangerous version ranges: *, latest, &gt;=, or no range at all\n      local dangerous\n      dangerous=$(python3 -c \"\nimport json\ntry:\n    pkg = json.load(open('$pkgjson'))\n    count = 0\n    for section in ['dependencies', 'devDependencies']:\n        deps = pkg.get(section, {})\n        for name, ver in deps.items():\n            v = str(ver).strip()\n            if v in ('*', 'latest', '') or v.startswith('&gt;=') or v.startswith('&gt;'):\n                count += 1\n    print(count)\nexcept: print(0)\n\" 2&gt;/dev/null || echo \"0\")\n      dangerous=\"${dangerous//[^0-9]/}\"\n      dangerous=\"${dangerous:-0}\"\n      if [[ $dangerous -gt 0 ]]; then\n        ((++js_unpinned_files))\n        if [[ $js_unpinned_files -le 3 ]]; then\n          warn \"deps-js\" \"$dangerous wildcard/unpinned dep(s) in ~/$rel/package.json (*, latest, &gt;=)\" \\\n            \"Pin to specific semver ranges: npm pkg fix\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"package.json\" -not -path \"*/node_modules/*\" -not -path \"*/.next/*\" -not -path \"*/dist/*\" -not -path \"*/.turbo/*\" 2&gt;/dev/null || true)\n  done\n\n  if [[ $js_checked -gt 0 ]]; then\n    if [[ $js_no_lock -gt 0 ]]; then\n      [[ $js_no_lock -gt 3 ]] &amp;&amp; crit \"deps-js\" \"...and $((js_no_lock - 3)) more JS projects without lockfiles\" \"\"\n    else\n      pass \"deps-js\" \"All $js_checked JS project(s) have lockfiles\"\n    fi\n    if [[ $js_unpinned_files -gt 3 ]]; then\n      warn \"deps-js\" \"...and $((js_unpinned_files - 3)) more package.json files with wildcard deps\" \"\"\n    fi\n  fi\n\n  # \u2500\u2500 Rust: Cargo.toml without Cargo.lock \u2500\u2500\n  local cargo_no_lock=0\n  local cargo_checked=0\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n    while IFS= read -r cargotoml; do\n      local dir\n      dir=$(dirname \"$cargotoml\")\n      # Only check if it looks like a binary project (has [[bin]] or default main.rs)\n      if [[ -f \"$dir/src/main.rs\" ]] || grep -q '\\[\\[bin\\]\\]' \"$cargotoml\" 2&gt;/dev/null; then\n        ((++cargo_checked))\n        if [[ ! -f \"$dir/Cargo.lock\" ]]; then\n          ((++cargo_no_lock))\n          local rel=\"${dir#\"$HOME\"/}\"\n          if [[ $cargo_no_lock -le 2 ]]; then\n            warn \"deps-rust\" \"No Cargo.lock in ~/$rel \u2014 binary builds are non-deterministic\" \\\n              \"cd ~/$rel &amp;&amp; cargo generate-lockfile &amp;&amp; git add Cargo.lock\"\n          fi\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"Cargo.toml\" -not -path \"*/target/*\" 2&gt;/dev/null || true)\n  done\n\n  if [[ $cargo_checked -gt 0 &amp;&amp; $cargo_no_lock -eq 0 ]]; then\n    pass \"deps-rust\" \"All $cargo_checked Rust binary project(s) have Cargo.lock\"\n  fi\n\n  # \u2500\u2500 Go: go.mod without go.sum \u2500\u2500\n  local go_no_sum=0\n  local go_checked=0\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n    while IFS= read -r gomod; do\n      local dir\n      dir=$(dirname \"$gomod\")\n      ((++go_checked))\n      if [[ ! -f \"$dir/go.sum\" ]]; then\n        ((++go_no_sum))\n        local rel=\"${dir#\"$HOME\"/}\"\n        if [[ $go_no_sum -le 2 ]]; then\n          warn \"deps-go\" \"No go.sum in ~/$rel \u2014 module checksums not tracked\" \\\n            \"cd ~/$rel &amp;&amp; go mod tidy\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"go.mod\" -not -path \"*/vendor/*\" 2&gt;/dev/null || true)\n  done\n\n  if [[ $go_checked -gt 0 &amp;&amp; $go_no_sum -eq 0 ]]; then\n    pass \"deps-go\" \"All $go_checked Go project(s) have go.sum\"\n  fi\n\n  # \u2500\u2500 Docker: Dockerfiles with unpinned base images \u2500\u2500\n  local docker_unpinned=0\n  local docker_checked=0\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n    while IFS= read -r dockerfile; do\n      ((++docker_checked))\n\t      # Check external FROM images without @sha256 digest pinning.\n\t      # Multi-stage aliases are allowed, but external base images with \"AS\" still need a digest.\n\t      local real_unpinned\n\t      real_unpinned=$(python3 - \"$dockerfile\" &lt;&lt;'PY' 2&gt;/dev/null || echo \"0\"\nimport re\nimport sys\n\npath = sys.argv[1]\nstages = set()\nunpinned = 0\nfrom_re = re.compile(r\"^\\s*FROM\\s+(.+)$\", re.IGNORECASE)\n\nwith open(path, encoding=\"utf-8\", errors=\"ignore\") as fh:\n    for raw in fh:\n        line = raw.split(\"#\", 1)[0].strip()\n        match = from_re.match(line)\n        if not match:\n            continue\n\n        tokens = match.group(1).split()\n        while tokens and tokens[0].startswith(\"--\"):\n            tokens.pop(0)\n        if not tokens:\n            continue\n\n        image = tokens[0]\n        lower_tokens = [token.lower() for token in tokens]\n        if \"as\" in lower_tokens:\n            idx = lower_tokens.index(\"as\")\n            if idx + 1 &lt; len(tokens):\n                stages.add(tokens[idx + 1])\n\n        if image == \"scratch\" or image in stages:\n            continue\n        if \"@sha256:\" not in image:\n            unpinned += 1\n\nprint(unpinned)\nPY\n)\n      real_unpinned=\"${real_unpinned//[^0-9]/}\"\n      real_unpinned=\"${real_unpinned:-0}\"\n      if [[ $real_unpinned -gt 0 ]]; then\n        ((++docker_unpinned))\n        local rel=\"${dockerfile#\"$HOME\"/}\"\n        if [[ $docker_unpinned -le 3 ]]; then\n          warn \"deps-docker\" \"Unpinned base image(s) in ~/$rel \u2014 tags are mutable\" \\\n            \"Pin by digest: FROM image@sha256:abc123...\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -name \"Dockerfile\" -o -name \"Dockerfile.*\" -o -name \"*.dockerfile\" \\) -not -path \"*/node_modules/*\" 2&gt;/dev/null || true)\n  done\n\n  if [[ $docker_checked -gt 0 ]]; then\n    if [[ $docker_unpinned -eq 0 ]]; then\n      pass \"deps-docker\" \"All $docker_checked Dockerfile(s) use digest-pinned base images\"\n    else\n      [[ $docker_unpinned -gt 3 ]] &amp;&amp; warn \"deps-docker\" \"...and $((docker_unpinned - 3)) more Dockerfiles with unpinned images\" \"\"\n    fi\n  fi\n\n  # Summary line\n  local total_scanned=$((py_files_checked + js_checked + cargo_checked + go_checked + docker_checked + pyproj_checked))\n  if [[ $total_scanned -eq 0 ]]; then\n    info \"deps\" \"No dependency files found in common code directories ($HOME/code, etc.)\" \"\"\n  else\n    info \"deps\" \"Scanned $total_scanned dependency manifest(s) across ${#code_dirs[@]} directories\" \"\"\n\t  fi\n\t}\n\n# \u2500\u2500 Secrets &amp; Local Credentials \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_local_credentials() {\n  section \"Local Credentials &amp; Token Surfaces\"\n\n  local credential_files=(\n    \"$HOME/.npmrc\"\n    \"$HOME/.pypirc\"\n    \"$HOME/.netrc\"\n    \"$HOME/.gem/credentials\"\n    \"$HOME/.cargo/credentials\"\n    \"$HOME/.cargo/credentials.toml\"\n    \"$HOME/.docker/config.json\"\n    \"$HOME/.config/gh/hosts.yml\"\n    \"$HOME/.aws/credentials\"\n    \"$HOME/.config/gcloud/application_default_credentials.json\"\n  )\n\n  local found=0\n  for file in \"${credential_files[@]}\"; do\n    [[ -f \"$file\" ]] || continue\n    ((++found))\n\n    case \"$file\" in\n      \"$HOME/.docker/config.json\")\n        if grep -q '\"auth\"' \"$file\" 2&gt;/dev/null; then\n          crit \"credentials\" \"Docker config stores registry auth material in plaintext/base64 form\" \\\n            \"Configure Docker credential helpers and remove auth entries from ~/.docker/config.json\"\n        else\n          info \"credentials\" \"Docker config exists; no inline auth field detected\" \"\"\n        fi\n        ;;\n      *)\n        if grep -qiE '(token|password|secret|_auth|machine|aws_access_key_id|private_key)' \"$file\" 2&gt;/dev/null; then\n          warn \"credentials\" \"Credential-like material found in ${file#\"$HOME\"/}\" \\\n            \"Review permissions and move long-lived secrets to a keychain, password manager, or short-lived auth flow\"\n        else\n          info \"credentials\" \"Credential file exists: ${file#\"$HOME\"/}\" \"\"\n        fi\n        ;;\n    esac\n\n    local mode\n    mode=$(stat -f \"%Lp\" \"$file\" 2&gt;/dev/null || echo \"\")\n    if [[ -n \"$mode\" &amp;&amp; \"$mode\" != \"600\" &amp;&amp; \"$mode\" != \"400\" ]]; then\n      warn \"credentials\" \"${file#\"$HOME\"/} permissions are $mode, not owner-only\" \\\n        \"chmod 600 \\\"$file\\\"\"\n    fi\n  done\n\n  local ssh_unencrypted=0\n  if [[ -d \"$HOME/.ssh\" ]]; then\n    local key\n    for key in \"$HOME\"/.ssh/id_*; do\n      [[ -f \"$key\" ]] || continue\n      [[ \"$key\" == *.pub || \"$key\" == *known_hosts* || \"$key\" == *config ]] &amp;&amp; continue\n      if ssh-keygen -y -P \"\" -f \"$key\" &gt;/dev/null 2&gt;&amp;1; then\n        ((++ssh_unencrypted))\n      fi\n    done\n  fi\n\n  if [[ $ssh_unencrypted -gt 0 ]]; then\n    warn \"credentials\" \"$ssh_unencrypted SSH private key(s) appear to have no passphrase\" \\\n      \"Rotate or add passphrases with: ssh-keygen -p -f ~/.ssh/id_ed25519\"\n  fi\n\n  if [[ $found -eq 0 &amp;&amp; $ssh_unencrypted -eq 0 ]]; then\n    pass \"credentials\" \"No common plaintext credential files found\"\n  fi\n}\n\n# \u2500\u2500 Project Attack Patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_project_attack_patterns() {\n  section \"Project Attack Patterns\"\n\n  local code_dirs=(\"${CODE_DIRS[@]}\")\n  local remote_install_files=0\n  local env_secret_files=0\n  local devcontainer_unpinned=0\n  local compose_unpinned=0\n  local broad_actions_permissions=0\n  local git_submodule_files=0\n  local git_filter_files=0\n\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n\n    while IFS= read -r file; do\n      if grep -qiE '(curl|wget)[^|;&amp;]*(\\||&gt;|bash|sh)|bash\\s+&lt;\\(|sh\\s+&lt;\\(' \"$file\" 2&gt;/dev/null; then\n        ((++remote_install_files))\n        if [[ $remote_install_files -le 3 ]]; then\n          warn \"patterns\" \"Remote install script pattern in ${file#\"$HOME\"/}\" \\\n            \"Download, pin, verify checksum/signature, then execute reviewed scripts\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -name \"Makefile\" -o -name \"*.sh\" -o -name \"*.bash\" -o -name \"*.zsh\" -o -name \"Dockerfile*\" -o -name \"*.md\" \\) -not -path \"*/node_modules/*\" -not -path \"*/.git/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r envfile; do\n      if grep -qiE '(API_KEY|TOKEN|SECRET|PASSWORD|PRIVATE_KEY|ACCESS_KEY)\\s*=' \"$envfile\" 2&gt;/dev/null; then\n        ((++env_secret_files))\n        if [[ $env_secret_files -le 3 ]]; then\n          warn \"patterns\" \"Secret-like values in ${envfile#\"$HOME\"/}\" \\\n            \"Keep real secrets out of repo files; use .env.example placeholders and secret managers\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -name \".env\" -o -name \".env.*\" \\) -not -name \".env.example\" -not -path \"*/node_modules/*\" -not -path \"*/.git/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r devcontainer; do\n      if grep -qiE '\"image\"\\s*:\\s*\"[^\"]+:(latest|main|master|edge|dev)\"|\"image\"\\s*:\\s*\"[^\"]+\"(,|$)' \"$devcontainer\" 2&gt;/dev/null &amp;&amp; ! grep -q '@sha256:' \"$devcontainer\" 2&gt;/dev/null; then\n        ((++devcontainer_unpinned))\n        if [[ $devcontainer_unpinned -le 3 ]]; then\n          warn \"patterns\" \"Devcontainer image is not digest-pinned in ${devcontainer#\"$HOME\"/}\" \\\n            \"Pin devcontainer images with @sha256 and review devcontainer features\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -name \"devcontainer.json\" -o -path \"*/.devcontainer/devcontainer.json\" \\) 2&gt;/dev/null || true)\n\n    while IFS= read -r compose; do\n      if grep -qiE 'image:\\s*[^@[:space:]]+:(latest|main|master|edge|dev)\\b|image:\\s*[^@[:space:]]+\\s*$' \"$compose\" 2&gt;/dev/null; then\n        ((++compose_unpinned))\n        if [[ $compose_unpinned -le 3 ]]; then\n          warn \"patterns\" \"Docker Compose image may be tag-pinned or implicit latest in ${compose#\"$HOME\"/}\" \\\n            \"Pin Compose images by digest for reproducible local services\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -name \"docker-compose.yml\" -o -name \"docker-compose.yaml\" -o -name \"compose.yml\" -o -name \"compose.yaml\" \\) -not -path \"*/node_modules/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r workflow; do\n      if grep -qiE '^\\s*permissions:\\s*(write-all|read-all)|^\\s*contents:\\s*write|^\\s*id-token:\\s*write' \"$workflow\" 2&gt;/dev/null; then\n        ((++broad_actions_permissions))\n        if [[ $broad_actions_permissions -le 3 ]]; then\n          warn \"patterns\" \"Broad GitHub Actions permissions in ${workflow#\"$HOME\"/}\" \\\n            \"Set least-privilege permissions per workflow/job and require OIDC only where needed\"\n        fi\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -path '*/.github/workflows/*.yml' -o -path '*/.github/workflows/*.yaml' \\) 2&gt;/dev/null || true)\n\n    while IFS= read -r submodule; do\n      ((++git_submodule_files))\n      warn \"patterns\" \"Git submodules found in ${submodule#\"$HOME\"/} \u2014 submodule refs and URLs need review\" \\\n        \"Review .gitmodules URLs, pin submodule commits, and prefer HTTPS/SSH over git://\"\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \".gitmodules\" 2&gt;/dev/null || true)\n\n    while IFS= read -r attrs; do\n      if grep -qiE 'filter=|textconv|diff=.*command' \"$attrs\" 2&gt;/dev/null; then\n        ((++git_filter_files))\n        warn \"patterns\" \"Git attributes can invoke filters/diff drivers in ${attrs#\"$HOME\"/}\" \\\n          \"Review filter, textconv, and diff driver config before opening untrusted repos\"\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \".gitattributes\" -not -path \"*/.git/*\" 2&gt;/dev/null || true)\n  done\n\n  [[ $remote_install_files -gt 3 ]] &amp;&amp; warn \"patterns\" \"...and $((remote_install_files - 3)) more files with remote install patterns\" \"\"\n  [[ $env_secret_files -gt 3 ]] &amp;&amp; warn \"patterns\" \"...and $((env_secret_files - 3)) more env files with secret-like values\" \"\"\n  [[ $devcontainer_unpinned -gt 3 ]] &amp;&amp; warn \"patterns\" \"...and $((devcontainer_unpinned - 3)) more unpinned devcontainer files\" \"\"\n  [[ $compose_unpinned -gt 3 ]] &amp;&amp; warn \"patterns\" \"...and $((compose_unpinned - 3)) more Compose files with unpinned images\" \"\"\n\n  local total_patterns=$((remote_install_files + env_secret_files + devcontainer_unpinned + compose_unpinned + broad_actions_permissions + git_submodule_files + git_filter_files))\n  if [[ $total_patterns -eq 0 ]]; then\n    pass \"patterns\" \"No high-risk project attack patterns found in scanned directories\"\n  fi\n}\n\n# \u2500\u2500 Additional Ecosystems \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_additional_ecosystems() {\n  section \"Additional Ecosystems\"\n\n  local code_dirs=(\"${CODE_DIRS[@]}\")\n  local composer_projects=0\n  local composer_no_lock=0\n  local gradle_projects=0\n  local gradle_no_lock=0\n  local nuget_projects=0\n  local terraform_files=0\n  local terraform_loose=0\n  local swift_projects=0\n  local swift_no_resolved=0\n  local dart_projects=0\n  local dart_no_lock=0\n\n  for base in \"${code_dirs[@]}\"; do\n    [[ -d \"$base\" ]] || continue\n\n    while IFS= read -r composer; do\n      ((++composer_projects))\n      local dir\n      dir=$(dirname \"$composer\")\n      if [[ ! -f \"$dir/composer.lock\" ]]; then\n        ((++composer_no_lock))\n        warn \"deps-php\" \"composer.json without composer.lock in ${dir#\"$HOME\"/}\" \\\n          \"Run composer update intentionally, commit composer.lock, and deploy with composer install\"\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"composer.json\" -not -path \"*/vendor/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r gradle; do\n      ((++gradle_projects))\n      local dir\n      dir=$(dirname \"$gradle\")\n      if [[ ! -f \"$dir/gradle.lockfile\" &amp;&amp; ! -d \"$dir/gradle/dependency-locks\" ]]; then\n        ((++gradle_no_lock))\n        info \"deps-jvm\" \"Gradle project without dependency locking in ${dir#\"$HOME\"/}\" \\\n          \"Enable dependencyLocking and commit generated lockfiles\"\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -name \"build.gradle\" -o -name \"build.gradle.kts\" \\) -not -path \"*/.gradle/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r nuget; do\n      ((++nuget_projects))\n      info \"deps-dotnet\" \"NuGet project detected in ${nuget#\"$HOME\"/}\" \\\n        \"Use packages.lock.json with RestoreLockedMode=true and trusted package sources\"\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" \\( -name \"*.csproj\" -o -name \"packages.config\" -o -name \"Directory.Packages.props\" \\) -not -path \"*/bin/*\" -not -path \"*/obj/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r tf; do\n      ((++terraform_files))\n      if grep -qE 'version\\s*=\\s*\"(&gt;=|&gt;|~&gt;|.*\\*)' \"$tf\" 2&gt;/dev/null; then\n        ((++terraform_loose))\n        warn \"deps-iac\" \"Loose Terraform/OpenTofu provider constraint in ${tf#\"$HOME\"/}\" \\\n          \"Pin providers tightly and commit .terraform.lock.hcl\"\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"*.tf\" -not -path \"*/.terraform/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r swift; do\n      ((++swift_projects))\n      local dir\n      dir=$(dirname \"$swift\")\n      if [[ ! -f \"$dir/Package.resolved\" ]]; then\n        ((++swift_no_resolved))\n        warn \"deps-swift\" \"Swift Package.swift without Package.resolved in ${dir#\"$HOME\"/}\" \\\n          \"Run swift package resolve and commit Package.resolved for apps/tools\"\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"Package.swift\" -not -path \"*/.build/*\" 2&gt;/dev/null || true)\n\n    while IFS= read -r pubspec; do\n      ((++dart_projects))\n      local dir\n      dir=$(dirname \"$pubspec\")\n      if [[ ! -f \"$dir/pubspec.lock\" ]]; then\n        ((++dart_no_lock))\n        warn \"deps-dart\" \"pubspec.yaml without pubspec.lock in ${dir#\"$HOME\"/}\" \\\n          \"Commit pubspec.lock for apps; use locked installs in CI\"\n      fi\n    done &lt; &lt;(find \"$base\" -maxdepth \"$SCAN_DEPTH\" -name \"pubspec.yaml\" -not -path \"*/.dart_tool/*\" 2&gt;/dev/null || true)\n  done\n\n  [[ $composer_projects -gt 0 &amp;&amp; $composer_no_lock -eq 0 ]] &amp;&amp; pass \"deps-php\" \"All $composer_projects Composer project(s) have composer.lock\"\n  [[ $gradle_projects -gt 0 &amp;&amp; $gradle_no_lock -eq 0 ]] &amp;&amp; pass \"deps-jvm\" \"All $gradle_projects Gradle project(s) appear to use dependency locking\"\n  [[ $terraform_files -gt 0 &amp;&amp; $terraform_loose -eq 0 ]] &amp;&amp; pass \"deps-iac\" \"No loose Terraform provider constraints detected\"\n  [[ $swift_projects -gt 0 &amp;&amp; $swift_no_resolved -eq 0 ]] &amp;&amp; pass \"deps-swift\" \"All $swift_projects Swift package(s) have Package.resolved\"\n  [[ $dart_projects -gt 0 &amp;&amp; $dart_no_lock -eq 0 ]] &amp;&amp; pass \"deps-dart\" \"All $dart_projects Dart/Flutter project(s) have pubspec.lock\"\n\n  if [[ $composer_projects -eq 0 &amp;&amp; $gradle_projects -eq 0 &amp;&amp; $nuget_projects -eq 0 &amp;&amp; $terraform_files -eq 0 &amp;&amp; $swift_projects -eq 0 &amp;&amp; $dart_projects -eq 0 ]]; then\n    info \"ecosystems\" \"No PHP, JVM, .NET, Terraform, Swift, or Dart manifests found in scanned directories\" \"\"\n  fi\n}\n\n# \u2500\u2500 macOS Security Baseline \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_macos() {\n  section \"macOS Security Baseline\"\n\n  # System Integrity Protection\n  local sip_status\n  sip_status=$(csrutil status 2&gt;/dev/null || echo \"unknown\")\n  if [[ \"$sip_status\" == *\"enabled\"* ]]; then\n    pass \"macos\" \"System Integrity Protection (SIP) is enabled\"\n  else\n    crit \"macos\" \"SIP is disabled \u2014 system files are unprotected\" \\\n      \"Reboot to Recovery Mode \u2192 csrutil enable\"\n  fi\n\n  # Gatekeeper\n  local gk_status\n  gk_status=$(spctl --status 2&gt;/dev/null || echo \"unknown\")\n  if [[ \"$gk_status\" == *\"assessments enabled\"* ]]; then\n    pass \"macos\" \"Gatekeeper is enabled\"\n  else\n    crit \"macos\" \"Gatekeeper is disabled \u2014 unsigned apps can run freely\" \\\n      \"sudo spctl --master-enable\"\n  fi\n\n  # FileVault\n  local fv_status\n  fv_status=$(fdesetup status 2&gt;/dev/null || echo \"unknown\")\n  if [[ \"$fv_status\" == *\"On\"* ]]; then\n    pass \"macos\" \"FileVault disk encryption is enabled\"\n  else\n    warn \"macos\" \"FileVault is not enabled \u2014 disk is not encrypted\" \\\n      \"System Settings \u2192 Privacy &amp; Security \u2192 FileVault \u2192 Turn On\"\n  fi\n\n  # Firewall\n  local fw_status\n  fw_status=$(/usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate 2&gt;/dev/null || echo \"unknown\")\n  if [[ \"$fw_status\" == *\"enabled\"* ]]; then\n    pass \"macos\" \"Application firewall is enabled\"\n  else\n    warn \"macos\" \"Application firewall is disabled\" \\\n      \"sudo /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on\"\n  fi\n}\n\n# \u2500\u2500 Vulnerability Scanners \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_scanners() {\n  section \"Vulnerability Scanners (meta-tools)\"\n\n  local has_scanner=false\n\n  if installed osv-scanner; then\n    pass \"scanners\" \"osv-scanner is installed \u2014 multi-ecosystem CVE scanner\"\n    has_scanner=true\n  else\n    info \"scanners\" \"osv-scanner not installed (Google, free, widest coverage)\" \\\n      \"brew install osv-scanner  # or: go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest\"\n  fi\n\n  if installed trivy; then\n    pass \"scanners\" \"trivy is installed \u2014 vulns + secrets + IaC scanning\"\n    has_scanner=true\n  else\n    info \"scanners\" \"trivy not installed (Aqua, free, vuln + secrets + misconfig)\" \\\n      \"brew install trivy\"\n  fi\n\n  if installed grype; then\n    pass \"scanners\" \"grype is installed \u2014 vulnerability scanner\"\n    has_scanner=true\n  else\n    info \"scanners\" \"grype not installed (Anchore, free, image + filesystem)\" \"\"\n  fi\n\n\t  if installed syft; then\n\t    pass \"scanners\" \"syft is installed \u2014 SBOM generation\"\n\t  else\n\t    info \"scanners\" \"syft not installed \u2014 generates SBOMs for compliance/auditing\" \\\n\t      \"brew install syft\"\n\t  fi\n\n  if installed cosign; then\n    pass \"scanners\" \"cosign is installed \u2014 container/artifact signature verification\"\n  else\n    info \"scanners\" \"cosign not installed \u2014 recommended for verifying signed containers and artifacts\" \\\n      \"brew install cosign\"\n  fi\n\n  if installed gitleaks; then\n    pass \"scanners\" \"gitleaks is installed \u2014 repository secret scanning\"\n  else\n    info \"scanners\" \"gitleaks not installed \u2014 catches committed secrets before push\" \\\n      \"brew install gitleaks\"\n  fi\n\n  if installed pre-commit; then\n    pass \"scanners\" \"pre-commit is installed \u2014 useful for local policy gates\"\n  else\n    info \"scanners\" \"pre-commit not installed \u2014 useful for secret and lint hooks before commit\" \\\n      \"brew install pre-commit\"\n  fi\n\n  if ! $has_scanner; then\n    warn \"scanners\" \"No multi-ecosystem vulnerability scanner installed\" \\\n      \"brew install osv-scanner trivy\"\n  fi\n}\n\n# \u2500\u2500 MCP Servers (Claude/Cursor) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# AI coding tools accept MCP server definitions that can run arbitrary code or\n# call arbitrary HTTP endpoints. A single rogue MCP added to ~/.claude.json gives\n# the attacker tool-call access to your sessions.\n\naudit_mcp_servers() {\n  section \"MCP Servers (~/.claude.json)\" \"mcp_servers\"\n\n  local claude_json=\"$HOME/.claude.json\"\n  if [[ ! -f \"$claude_json\" ]]; then\n    info \"mcp\" \"No ~/.claude.json \u2014 Claude Code not configured here\"\n    return\n  fi\n\n  local servers=\"\"\n  if installed jq; then\n    servers=\"$(jq -r '\n      .mcpServers // {} | to_entries[]\n      | \"\\(.key)\\t\\(.value.type // \"stdio\")\\t\\(.value.url // .value.command // \"?\")\"\n    ' \"$claude_json\" 2&gt;/dev/null || true)\"\n  fi\n\n  if [[ -z \"$servers\" ]]; then\n    pass \"mcp\" \"No MCP servers configured\"\n    return\n  fi\n\n  # Known-safe URLs/commands. Anything else gets flagged for manual review.\n  local safe_pattern='^(https://(api\\.anthropic\\.com|dash\\.brain-ai\\.dev|.*\\.githubusercontent\\.com|mcp\\.openai\\.com)|/nix/store/|/usr/local/|/opt/homebrew/|npx |node |uvx |python )'\n\n  while IFS=$'\\t' read -r name typ target; do\n    [[ -z \"$name\" ]] &amp;&amp; continue\n    if printf '%s' \"$target\" | grep -qE \"$safe_pattern\"; then\n      pass \"mcp\" \"MCP $name [$typ] \u2192 $target\"\n    else\n      crit \"mcp\" \"Unrecognized MCP server: $name [$typ] \u2192 $target\" \\\n        \"Inspect ~/.claude.json; remove with: jq 'del(.mcpServers.\\\"$name\\\")' ~/.claude.json | sponge ~/.claude.json\"\n    fi\n  done &lt;&lt;&lt; \"$servers\"\n}\n\n# \u2500\u2500 Known-bad packages (offline lockfile match) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Curated list of compromised packages from 2024\u20132026. Matches against\n# package-lock.json / pnpm-lock.yaml / uv.lock without hitting the network.\n# Update the lists below as new supply-chain incidents are disclosed.\n\naudit_known_bad_pkgs() {\n  section \"Known-bad package signatures\" \"known_bad_pkgs\"\n\n  local KNOWN_BAD_NPM\n  KNOWN_BAD_NPM=$(cat &lt;&lt;'NPMBAD'\n@ctrl/tinycolor|*|GHSA-7vfx-9hwp-c2x4|critical\n@nx/devkit|17.3.0|GHSA-cxm3-wv7p-998c|critical\n@nx/devkit|20.5.0|GHSA-cxm3-wv7p-998c|critical\n@nx/devkit|21.5.0|GHSA-cxm3-wv7p-998c|critical\nnx|17.3.0|GHSA-cxm3-wv7p-998c|critical\nnx|20.5.0|GHSA-cxm3-wv7p-998c|critical\nnx|21.5.0|GHSA-cxm3-wv7p-998c|critical\nngx-bootstrap|18.1.4|shai-hulud-2025|critical\nngx-toastr|19.0.2|shai-hulud-2025|critical\n@crowdstrike/falcon-shoelace|0.4.2|shai-hulud-2025|critical\nevent-stream|3.3.6|CVE-2018-1000620|critical\nua-parser-js|0.7.29|CVE-2021-44906|critical\nua-parser-js|0.8.0|CVE-2021-44906|critical\nua-parser-js|1.0.0|CVE-2021-44906|critical\nrc|1.2.9|GHSA-g2q5-5433-rhrf|critical\nrc|1.3.9|GHSA-g2q5-5433-rhrf|critical\nrc|2.3.9|GHSA-g2q5-5433-rhrf|critical\ncoa|2.0.3|GHSA-73qr-pfmq-6rp6|critical\ncoa|2.0.4|GHSA-73qr-pfmq-6rp6|critical\ncoa|2.1.1|GHSA-73qr-pfmq-6rp6|critical\nnode-ipc|10.1.1|CVE-2022-23812|critical\nnode-ipc|10.1.2|CVE-2022-23812|critical\nnode-ipc|11.0.0|CVE-2022-23812|critical\nnode-ipc|11.1.0|CVE-2022-23812|critical\nflatmap-stream|*|CVE-2018-16487|critical\neslint-scope|3.7.2|CVE-2018-7408|critical\nNPMBAD\n  )\n\n  local KNOWN_BAD_PY\n  KNOWN_BAD_PY=$(cat &lt;&lt;'PYBAD'\nctx|*|CVE-2022-29217|critical\nphpass|*|typosquat|critical\nPYBAD\n  )\n\n  # Match helper: glob \"*\" wildcards, else exact-equal\n  _kbp_match() {\n    local glob=\"$1\" v=\"$2\"\n    [[ \"$glob\" == \"*\" ]] &amp;&amp; return 0\n    # shellcheck disable=SC2053\n    [[ \"$v\" == $glob ]]\n  }\n\n  # Extract pkg+version pairs from a lockfile\n  _kbp_extract_npm() {\n    local lf=\"$1\"\n    if installed jq; then\n      jq -r '(.packages // {}) | to_entries[]\n        | select(.key != \"\")\n        | \"\\(.key | sub(\"^node_modules/\"; \"\") | sub(\".*/node_modules/\"; \"\"))\\t\\(.value.version // \"\")\"' \"$lf\" 2&gt;/dev/null\n    fi\n  }\n  _kbp_extract_pnpm() {\n    local lf=\"$1\"\n    grep -oE \"/[@a-zA-Z0-9_./-]+@[0-9][a-zA-Z0-9.+_-]*\" \"$lf\" 2&gt;/dev/null \\\n      | sed 's|^/||' \\\n      | awk -F'@' '{ if (NF==2) print $1 \"\\t\" $2; else if (NF==3) print \"@\" $2 \"\\t\" $3 }'\n  }\n  _kbp_extract_uv() {\n    local lf=\"$1\"\n    awk '\n      /^\\[\\[package\\]\\]/ { in_pkg=1; name=\"\"; ver=\"\"; next }\n      /^\\[/ &amp;&amp; !/^\\[\\[package\\]\\]/ { in_pkg=0 }\n      in_pkg &amp;&amp; /^name *=/ { gsub(/^name *= *|[\"[:space:]]+/, \"\", $0); name=$0 }\n      in_pkg &amp;&amp; /^version *=/ { gsub(/^version *= *|[\"[:space:]]+/, \"\", $0); ver=$0; if(name &amp;&amp; ver) print name \"\\t\" ver; name=\"\"; ver=\"\" }\n    ' \"$lf\" 2&gt;/dev/null\n  }\n\n  local hits=0\n  declare -A reported=()\n\n  _kbp_scan() {\n    local lf=\"$1\" extractor=\"$2\" bad_list=\"$3\"\n    local project; project=\"$(dirname \"$lf\")\"\n    while IFS=$'\\t' read -r name ver; do\n      [[ -z \"$name\" || -z \"$ver\" ]] &amp;&amp; continue\n      while IFS='|' read -r bn bv cve sev; do\n        [[ -z \"$bn\" ]] &amp;&amp; continue\n        if [[ \"$name\" == \"$bn\" ]] &amp;&amp; _kbp_match \"$bv\" \"$ver\"; then\n          local key=\"$project|$name@$ver\"\n          [[ -n \"${reported[$key]:-}\" ]] &amp;&amp; continue\n          reported[$key]=1\n          crit \"known_bad\" \"$name@$ver ($cve) in $project\" \\\n            \"cd $project &amp;&amp; (pnpm up $name@latest || npm i $name@latest)\"\n          hits=$((hits+1))\n        fi\n      done &lt;&lt;&lt; \"$bad_list\"\n    done &lt; &lt;(\"$extractor\" \"$lf\")\n  }\n\n  local dir\n  for dir in \"${CODE_DIRS[@]}\"; do\n    [[ -d \"$dir\" ]] || continue\n    while IFS= read -r -d '' lf; do _kbp_scan \"$lf\" _kbp_extract_npm  \"$KNOWN_BAD_NPM\"; done \\\n      &lt; &lt;(find \"$dir\" -maxdepth \"$SCAN_DEPTH\" -name package-lock.json -not -path '*/node_modules/*' -not -path '*/.git/*' -print0 2&gt;/dev/null)\n    while IFS= read -r -d '' lf; do _kbp_scan \"$lf\" _kbp_extract_pnpm \"$KNOWN_BAD_NPM\"; done \\\n      &lt; &lt;(find \"$dir\" -maxdepth \"$SCAN_DEPTH\" -name pnpm-lock.yaml  -not -path '*/node_modules/*' -not -path '*/.git/*' -print0 2&gt;/dev/null)\n    while IFS= read -r -d '' lf; do _kbp_scan \"$lf\" _kbp_extract_uv   \"$KNOWN_BAD_PY\";  done \\\n      &lt; &lt;(find \"$dir\" -maxdepth \"$SCAN_DEPTH\" -name uv.lock          -not -path '*/.venv/*'        -not -path '*/.git/*' -print0 2&gt;/dev/null)\n  done\n\n  if [[ $hits -eq 0 ]]; then\n    pass \"known_bad\" \"No known-bad packages matched in any scanned lockfile\"\n  fi\n}\n\n# \u2500\u2500 Lockfile registry-hijack check \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n# Catches tarball substitution: lockfile claims the official name+version but\n# 'resolved' points to a hostile mirror or arbitrary URL.\n\naudit_registry_hijack() {\n  section \"Lockfile registry-hijack\" \"registry_hijack\"\n  local hits=0\n  local dir\n  for dir in \"${CODE_DIRS[@]}\"; do\n    [[ -d \"$dir\" ]] || continue\n    while IFS= read -r -d '' lf; do\n      while IFS= read -r url; do\n        url=\"${url#*\\\"}\"; url=\"${url%\\\"*}\"\n        [[ -z \"$url\" ]] &amp;&amp; continue\n        if ! printf '%s' \"$url\" | grep -qE '^(https://registry\\.npmjs\\.org/|https://registry\\.yarnpkg\\.com/|git\\+ssh://|git\\+https://|github:|file:)'; then\n          crit \"registry\" \"Non-official tarball in ${lf#$dir/}: $url\" \\\n            \"Verify intent; if not, rm lockfile and regenerate from a clean install\"\n          hits=$((hits+1))\n          [[ $hits -ge 15 ]] &amp;&amp; return\n        fi\n      done &lt; &lt;(grep -E '\"resolved\":' \"$lf\" 2&gt;/dev/null | grep -oE '\"https?://[^\"]+\"')\n    done &lt; &lt;(find \"$dir\" -maxdepth \"$SCAN_DEPTH\" -name package-lock.json -not -path '*/node_modules/*' -not -path '*/.git/*' -print0 2&gt;/dev/null)\n  done\n  [[ $hits -eq 0 ]] &amp;&amp; pass \"registry\" \"All package-lock.json resolutions point to official registries\"\n}\n\n# \u2500\u2500 gh CLI scope audit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_gh_scopes() {\n  section \"GitHub CLI auth\" \"gh_scopes\"\n  if ! installed gh; then\n    info \"gh\" \"gh CLI not installed\"\n    return\n  fi\n  local stat\n  if ! stat=\"$(gh auth status 2&gt;&amp;1)\"; then\n    info \"gh\" \"gh not logged in\"\n    return\n  fi\n  local scopes\n  scopes=\"$(printf '%s' \"$stat\" | grep -i 'scopes:' | head -1 | sed 's/.*scopes://; s/ //g')\"\n  if [[ -z \"$scopes\" ]]; then\n    info \"gh\" \"Could not parse gh auth scopes\"\n    return\n  fi\n  if printf '%s' \"$scopes\" | grep -qE \"delete_repo|admin:org|admin:enterprise\"; then\n    crit \"gh\" \"gh token has dangerous scopes: $scopes\" \\\n      \"gh auth refresh --scopes repo,read:org   # downgrade\"\n  elif printf '%s' \"$scopes\" | grep -qE \"workflow\"; then\n    warn \"gh\" \"gh token has 'workflow' scope: $scopes\" \\\n      \"If you don't push workflows from CLI: gh auth refresh --scopes repo,read:org\"\n  else\n    pass \"gh\" \"gh token scopes look minimal: $scopes\"\n  fi\n}\n\n# \u2500\u2500 Shell history secret-pattern scan \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\naudit_shell_history() {\n  section \"Shell history secrets\" \"shell_history\"\n  local files=(\"$HOME/.zsh_history\" \"$HOME/.bash_history\" \"$HOME/.local/share/fish/fish_history\")\n  local patterns='AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|ghs_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9]{32,}|xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+|eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}'\n  local total=0\n  local f\n  for f in \"${files[@]}\"; do\n    [[ -f \"$f\" ]] || continue\n    local n\n    # grep -c always prints a count to stdout (0 when no match) but exits 1\n    # on zero matches. Don't add `|| echo 0` \u2014 it produces \"0\\n0\" \u2192 arithmetic error.\n    n=\"$(grep -cE \"$patterns\" \"$f\" 2&gt;/dev/null)\" || n=0\n    if [[ \"${n:-0}\" -gt 0 ]]; then\n      warn \"history\" \"$n probable-secret hit(s) in ${f/#$HOME/~}\" \\\n        \"Rotate any matching credentials; trim history: grep -vE '' $f &gt; $f.clean &amp;&amp; mv $f.clean $f\"\n      total=$((total+n))\n    fi\n  done\n  [[ $total -eq 0 ]] &amp;&amp; pass \"history\" \"No obvious secret patterns in shell history\"\n}\n\n# \u2500\u2500 Report Generation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ngenerate_html_report() {\n  local output_file=\"${1:-/tmp/supply-chain-audit-report.html}\"\n  local timestamp\n  timestamp=$(date \"+%Y-%m-%d %H:%M\")\n\n  cat &gt; \"$output_file\" &lt;&lt; 'HTMLHEAD'\n\n\n\n\n\nSupply Chain Security Audit\n\n  * { box-sizing: border-box; }\n  body { margin: 0; background: #0f172a; color: #e2e8f0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif; }\n  .min-h-screen { min-height: 100vh; }\n  .p-4 { padding: 1rem; }\n  .max-w-4xl { max-width: 56rem; }\n  .mx-auto { margin-left: auto; margin-right: auto; }\n  .mb-8 { margin-bottom: 2rem; }\n  .mb-4 { margin-bottom: 1rem; }\n  .mb-3 { margin-bottom: 0.75rem; }\n  .mb-2 { margin-bottom: 0.5rem; }\n  .mt-4 { margin-top: 1rem; }\n  .mt-1 { margin-top: 0.25rem; }\n  .flex { display: flex; }\n  .gap-4 { gap: 1rem; }\n  .p-5 { padding: 1.25rem; }\n  .pl-4 { padding-left: 1rem; }\n  .py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }\n  .py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; }\n  .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }\n  .py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }\n  .rounded { border-radius: 0.25rem; }\n  .rounded-full { border-radius: 9999px; }\n  .text-center { text-align: center; }\n  .text-3xl { font-size: 1.875rem; line-height: 2.25rem; }\n  .text-lg { font-size: 1.125rem; line-height: 1.75rem; }\n  .text-sm { font-size: 0.875rem; line-height: 1.25rem; }\n  .text-xs { font-size: 0.75rem; line-height: 1rem; }\n  .font-bold { font-weight: 700; }\n  .font-semibold { font-weight: 600; }\n  .text-white { color: #fff; }\n  .text-slate-400 { color: #94a3b8; }\n  .text-slate-600 { color: #475569; }\n  .card { background: #1e293b; border: 1px solid #334155; border-radius: 0.75rem; }\n  .crit { border-left: 3px solid #ef4444; background: #7f1d1d20; }\n  .warn { border-left: 3px solid #f59e0b; background: #78350f20; }\n  .pass { border-left: 3px solid #22c55e; }\n  .info { border-left: 3px solid #3b82f6; }\n  code { background: #0f172a; padding: 2px 6px; border-radius: 4px; font-size: 0.85em; }\n  .badge-red { background: #991b1b; color: #fca5a5; }\n  .badge-yellow { background: #854d0e; color: #fde68a; }\n  .badge-green { background: #166534; color: #86efac; }\n  .badge-blue { background: #1e3a5f; color: #93c5fd; }\n  @media (min-width: 768px) { .md\\:p-8 { padding: 2rem; } }\n\n\n\n\n\nHTMLHEAD\n\n  # Header with stats\n  cat &gt;&gt; \"$output_file\" &lt;&lt; EOF\n\n\n  \nSupply Chain Security Audit\n  \n$(hostname) &middot; $timestamp &middot; v$VERSION\n  \n\n    $CRIT_COUNT Critical\n    $WARN_COUNT Warnings\n    $PASS_COUNT Passed\n    $INFO_COUNT Info\n  \n\nEOF\n\n  # Findings\n  local current_tool=\"\"\n  for finding in \"${FINDINGS[@]}\"; do\n    IFS='|' read -r severity tool message fix &lt;&lt;&lt; \"$finding\"\n    if [[ \"$tool\" != \"$current_tool\" ]]; then\n      [[ -n \"$current_tool\" ]] &amp;&amp; echo \"\" &gt;&gt; \"$output_file\"\n      echo \"\n\" &gt;&gt; \"$output_file\"\n      echo \"\n$tool\" &gt;&gt; \"$output_file\"\n      current_tool=\"$tool\"\n    fi\n\n    local css_class=\"info\"\n    local label=\"INFO\"\n    case \"$severity\" in\n      CRIT) css_class=\"crit\"; label=\"CRITICAL\" ;;\n      WARN) css_class=\"warn\"; label=\"WARNING\" ;;\n      PASS) css_class=\"pass\"; label=\"OK\" ;;\n      INFO) css_class=\"info\"; label=\"INFO\" ;;\n    esac\n\n    # Escape HTML\n    message=$(echo \"$message\" | sed 's/&amp;/\\&amp;/g; s/&lt;/\\&lt;/g; s/&gt;/\\&gt;/g')\n    fix=$(echo \"$fix\" | sed 's/&amp;/\\&amp;/g; s/&lt;/\\&lt;/g; s/&gt;/\\&gt;/g')\n\n    echo \"\n\" &gt;&gt; \"$output_file\"\n    echo \"\n$label: $message\" &gt;&gt; \"$output_file\"\n    [[ -n \"$fix\" ]] &amp;&amp; echo \"\n$fix\" &gt;&gt; \"$output_file\"\n    echo \"\" &gt;&gt; \"$output_file\"\n  done\n  [[ -n \"$current_tool\" ]] &amp;&amp; echo \"\" &gt;&gt; \"$output_file\"\n\n  # Footer\n  cat &gt;&gt; \"$output_file\" &lt;&lt; 'HTMLFOOT'\n\n\n  supply-chain-audit.sh\n\n\nHTMLFOOT\n\n  echo \"$output_file\"\n}\n\n# \u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nmain() {\n  local do_fix=false\n  local no_prompt=0\n  local list_groups=0\n\n  # Parse arguments: paths go to SCAN_DIRS, plus flags for new features\n  local extra_dirs=()\n  local i=0\n  local args=(\"$@\")\n  while [[ $i -lt $# ]]; do\n    arg=\"${args[$i]}\"\n    case \"$arg\" in\n      --fix)           do_fix=true ;;\n      --json)          JSON_MODE=1 ;;\n      --quiet|-q)      QUIET=1 ;;\n      --no-prompt)     no_prompt=1 ;;\n      --list-groups)   list_groups=1 ;;\n      --only)          i=$((i+1)); ONLY_GROUPS=\"${args[$i]}\" ;;\n      --skip)          i=$((i+1)); SKIP_GROUPS=\"${args[$i]}\" ;;\n      --version)       echo \"supply-chain-audit.sh $VERSION\"; return 0 ;;\n      --help|-h)\n        echo \"Usage: supply-chain-audit.sh [OPTIONS] [DIRS...]\"\n        echo \"\"\n        echo \"Scans developer tools and dependency files for supply chain risks.\"\n        echo \"If no DIRS or SCAN_DIRS are provided in an interactive shell, the script\"\n        echo \"prompts before using the default scan directories.\"\n        echo \"\"\n        echo \"Arguments:\"\n        echo \"  DIRS        Directories to scan for dependency files (default: ~/code ~/projects ~/src ~/dev)\"\n        echo \"\"\n        echo \"Options:\"\n        echo \"  --fix       Generate a remediation script\"\n        echo \"  --help      Show this help\"\n        echo \"\"\n        echo \"Environment:\"\n        echo \"  SCAN_DIRS   Space-separated directories to scan (overrides defaults)\"\n        echo \"  SCAN_DEPTH  Max search depth within directories (default: 5)\"\n        echo \"\"\n        echo \"Examples:\"\n        echo \"  ./supply-chain-audit.sh                    # prompt before scanning defaults\"\n        echo \"  ./supply-chain-audit.sh ~/work ~/repos     # scan specific dirs\"\n        echo \"  SCAN_DEPTH=3 ./supply-chain-audit.sh       # shallow scan\"\n        return 0\n        ;;\n      *) extra_dirs+=(\"$arg\") ;;\n    esac\n    i=$((i+1))\n  done\n\n  if [[ $list_groups -eq 1 ]]; then\n    cat &lt; \"$fix_file\" &lt;&lt; 'FIXHEADER'\n#!/usr/bin/env bash\n# Auto-generated remediation script \u2014 review before running!\nset -euo pipefail\necho \"Supply Chain Security Remediation\"\necho \"Review each command before uncommenting and running.\"\necho \"\"\nFIXHEADER\n\n  for finding in \"${FINDINGS[@]}\"; do\n    IFS='|' read -r severity tool message fix &lt;&lt;&lt; \"$finding\"\n    [[ -z \"$fix\" ]] &amp;&amp; continue\n    [[ \"$severity\" != \"CRIT\" &amp;&amp; \"$severity\" != \"WARN\" ]] &amp;&amp; continue\n    {\n      echo \"# [$severity] $tool: $message\"\n      echo \"# $fix\"\n      echo \"\"\n    } &gt;&gt; \"$fix_file\"\n  done\n\n  chmod +x \"$fix_file\"\n  printf \"\\n  %sFix script: %s%s\\n\" \"$YELLOW\" \"$fix_file\" \"$NC\"\n  printf \"  %sReview and uncomment commands before running.%s\\n\" \"$DIM\" \"$NC\"\n}\n\nmain \"$@\"\n", "creation_timestamp": "2026-05-20T15:46:13.000000Z"}]}