[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-70489":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":15,"subscribersCount":15,"size":15,"stars1d":15,"stars7d":15,"stars30d":16,"stars90d":15,"forks30d":15,"starsTrendScore":15,"compositeScore":17,"rankGlobal":10,"rankLanguage":10,"license":18,"archived":19,"fork":19,"defaultBranch":20,"hasWiki":21,"hasPages":21,"topics":22,"createdAt":10,"pushedAt":10,"updatedAt":43,"readmeContent":44,"aiSummary":45,"trendingCount":15,"starSnapshotCount":15,"syncStatus":46,"lastSyncTime":47,"discoverSource":48},70489,"pwned-deps","mkbhardwas12\u002Fpwned-deps","mkbhardwas12","Lockfile-first scanner for compromised npm\u002FPyPI\u002FMaven\u002FCargo\u002FGo\u002FRubyGems packages — OSV + curated extras feed, SLSA L3, locked-container CI","https:\u002F\u002Fpypi.org\u002Fproject\u002Fpwned-deps\u002F",null,"Python",163,172,36,0,122,6.71,"Apache License 2.0",false,"main",true,[23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"cargo","cli","dependency-scanner","devsecops","go","lockfile","maven","npm","osv","pypi","python","rubygems","sarif","sca","security","sigstore","slsa","supply-chain","supply-chain-security","vulnerability-scanner","2026-06-12 02:02:34","# pwned-deps\n\n> **Drop your lockfile in. Get a red\u002Fgreen answer in 5 seconds.**\n>\n> A multi-ecosystem scanner for compromised package versions —\n> account hijacks, typosquats, dependency-confusion, retroactively\n> trojanised releases — across npm, PyPI, Maven, Cargo, Go, RubyGems.\n\n\u003C!-- TODO(logo): place a 256x256 PNG at docs\u002Flogo.png and reference it here. -->\n\n![pwned-deps demo: scanning an npm lockfile and flagging a Mini Shai-Hulud compromised package](docs\u002Fdemo.gif)\n\n> Re-render the demo any time the CLI's output changes:\n> `make demo-gif` (Docker; no host installs).\n\n[![CI](https:\u002F\u002Fgithub.com\u002Fmkbhardwas12\u002Fpwned-deps\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002Fmkbhardwas12\u002Fpwned-deps\u002Factions\u002Fworkflows\u002Fci.yml)\n[![PyPI version](https:\u002F\u002Fimg.shields.io\u002Fpypi\u002Fv\u002Fpwned-deps.svg)](https:\u002F\u002Fpypi.org\u002Fproject\u002Fpwned-deps\u002F)\n[![Python versions](https:\u002F\u002Fimg.shields.io\u002Fpypi\u002Fpyversions\u002Fpwned-deps.svg)](https:\u002F\u002Fpypi.org\u002Fproject\u002Fpwned-deps\u002F)\n[![SLSA Level 3](https:\u002F\u002Fslsa.dev\u002Fimages\u002Fgh-badge-level3.svg)](https:\u002F\u002Fslsa.dev)\n[![License: Apache 2.0](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-Apache%202.0-blue.svg)](LICENSE)\n\n## Table of contents\n\n- [At a glance](#at-a-glance)\n- [Architecture](#architecture)\n- [Why this exists](#why-this-exists)\n  - [Campaigns the bundled feed already covers](#campaigns-the-bundled-feed-already-covers)\n  - [A worked example: Mini Shai-Hulud (April 29, 2026)](#a-worked-example-mini-shai-hulud-april-29-2026)\n- [Install](#install)\n- [See it in action](#see-it-in-action)\n  - [Benchmark](#benchmark)\n- [Quick usage](#quick-usage)\n- [Watch mode (the recurring-value workflow)](#watch-mode-the-recurring-value-workflow)\n- [Supported ecosystems](#supported-ecosystems)\n- [Real-world scenarios this is built for](#real-world-scenarios-this-is-built-for)\n- [CI integration](#ci-integration)\n  - [GitHub Actions (one line)](#github-actions-one-line)\n  - [Plain workflow step (no action wrapper)](#plain-workflow-step-no-action-wrapper)\n  - [Sticky PR comment (the bot workflow)](#sticky-pr-comment-the-bot-workflow)\n  - [Static HTML dashboard (org-wide visibility)](#static-html-dashboard-org-wide-visibility)\n  - [pre-commit](#pre-commit)\n  - [GitLab CI](#gitlab-ci)\n- [Output formats](#output-formats)\n- [Threat model](#threat-model)\n  - [Verify a release with SLSA provenance](#verify-a-release-with-slsa-provenance)\n- [Comparison](#comparison)\n  - [Where each tool is the right answer](#where-each-tool-is-the-right-answer)\n- [FAQ](#faq)\n- [Contributing](#contributing)\n- [Maintenance](#maintenance)\n- [Changelog](CHANGELOG.md)\n- [Security policy](SECURITY.md)\n- [License](#license)\n- [Maintainer](#maintainer)\n\n`pwned-deps` is a Python CLI that takes one or more developer lockfiles\n(`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`, `requirements.txt`,\n`Pipfile.lock`, `poetry.lock`, `uv.lock`, `Cargo.lock`, `go.sum`,\n`pom.xml`, `Gemfile.lock`) and tells you, in seconds, whether you've\ninstalled a package version that's publicly flagged as compromised —\nsupply-chain malware, abandoned-and-hijacked packages, retroactively\npublished malicious versions.\n\n## At a glance\n\n|                       |                                                                 |\n|-----------------------|-----------------------------------------------------------------|\n| **What**              | A 5-second red\u002Fgreen answer to \"is anything in my lockfile pwned?\" |\n| **Who it's for**      | Application devs, SREs, AppSec \u002F DFIR responders during an active incident |\n| **Inputs**            | Lockfiles (npm, PyPI, Maven, Cargo, Go, RubyGems) — never source, never tarballs |\n| **Data sources**      | [OSV.dev](https:\u002F\u002Fosv.dev) public API + curated `extras.json` campaign feed (signed, sigstore + Rekor) |\n| **Outputs**           | Coloured terminal report, JSON, SARIF (GitHub Code Scanning) |\n| **Four commands**     | `pwned-deps check \u003Clockfile>` (one-shot scan) · `pwned-deps audit-repo \u003Cdir>` (forensic file-IoC scan) · `pwned-deps watch \u003Clockfile> --baseline \u003Cfile>` (daily baseline + delta alert) · `pwned-deps report \u003Cscans> -o \u003Chtml>` (org-wide HTML dashboard) |\n| **Failure mode**      | Exit `1` on confirmed compromise — wire that to your CI gate |\n| **Network footprint** | One host: `api.osv.dev`. No telemetry. Offline mode supported. |\n| **Trust model**       | Apache-2.0, SLSA L3 build provenance, OIDC-only PyPI publishing, locked container CI |\n\n## Architecture\n\nThe CLI is intentionally a thin matcher around two data sources. There\nis no service, no backend, no telemetry — your lockfile bytes never\nleave the machine running the command.\n\n```mermaid\nflowchart LR\n    subgraph User[\"Your machine \u002F CI runner\"]\n        LF[\"Lockfiles\u003Cbr\u002F>(package-lock.json,\u003Cbr\u002F>requirements.txt,\u003Cbr\u002F>Cargo.lock, ...)\"]\n        REPO[\"Repo tree\u003Cbr\u002F>(for audit-repo)\"]\n    end\n\n    subgraph CLI[\"pwned-deps CLI\"]\n        P[\"Parsers\u003Cbr\u002F>(npm \u002F pypi \u002F maven \u002F\u003Cbr\u002F>cargo \u002F go \u002F gem)\"]\n        M[\"Matcher\u003Cbr\u002F>(version_match.py)\"]\n        A[\"audit\u002Frepo.py\u003Cbr\u002F>(SHA-256 + path)\"]\n        R[\"Renderers\u003Cbr\u002F>text \u002F json \u002F sarif\"]\n    end\n\n    subgraph Data[\"Advisory data\"]\n        OSV[(\"api.osv.dev\u003Cbr\u002F>public API\")]\n        CACHE[(\"~\u002F.cache\u002Fpwned-deps\u002F\u003Cbr\u002F>osv.sqlite (24h TTL)\")]\n        EX[(\"extras.json\u003Cbr\u002F>curated feed,\u003Cbr\u002F>sigstore-signed\")]\n    end\n\n    LF --> P --> M\n    REPO --> A\n    M \u003C--> CACHE\n    CACHE \u003C-.refresh.-> OSV\n    M \u003C-- iocs\u002Ffile_iocs --> EX\n    A \u003C-- file_iocs --> EX\n    M --> R\n    A --> R\n    R --> OUT[\"Terminal · JSON · SARIF\u003Cbr\u002F>exit 0\u002F1\u002F2\u002F3\"]\n```\n\n**How a scan works (happy path):**\n\n```mermaid\nsequenceDiagram\n    autonumber\n    participant Dev as Developer \u002F CI\n    participant CLI as pwned-deps\n    participant Cache as Local SQLite cache\n    participant OSV as api.osv.dev\n    participant Feed as extras.json (bundled)\n\n    Dev->>CLI: pwned-deps check .\u002Fpackage-lock.json\n    CLI->>CLI: parse lockfile → list[(name, version, ecosystem)]\n    CLI->>Cache: lookup advisories (24h TTL)\n    alt cache miss \u002F stale\n        CLI->>OSV: POST \u002Fv1\u002Fquerybatch\n        OSV-->>CLI: advisories (CVE \u002F GHSA \u002F MAL-*)\n        CLI->>Cache: write\n    end\n    CLI->>Feed: lookup curated campaigns (EXTRA-*)\n    CLI->>CLI: match version ranges, dedupe by id\n    CLI-->>Dev: rendered report + exit code\n```\n\n**Module map (one file, one job):**\n\n| Path                                | Responsibility                                       |\n|-------------------------------------|------------------------------------------------------|\n| `src\u002Fpwned_deps\u002Fcli.py`             | Click command surface; `check` and `audit-repo`      |\n| `src\u002Fpwned_deps\u002Fparsers\u002F*.py`       | One parser per ecosystem; pure text → tuples         |\n| `src\u002Fpwned_deps\u002Fadvisory\u002Fosv_client.py` | OSV.dev HTTP client (httpx, batched)            |\n| `src\u002Fpwned_deps\u002Fadvisory\u002Fcache.py`  | SQLite cache, TTL, offline mode                      |\n| `src\u002Fpwned_deps\u002Fadvisory\u002Fmatcher.py`| Severity + ID dedup; OSV ⨯ extras.json merge         |\n| `src\u002Fpwned_deps\u002Fadvisory\u002Fversion_match.py` | OSV range semantics (introduced \u002F fixed \u002F last_affected) |\n| `src\u002Fpwned_deps\u002Fadvisory\u002Fextras.py` | Curated-feed loader; per-package ecosystem override  |\n| `src\u002Fpwned_deps\u002Faudit\u002Frepo.py`      | `audit-repo` — SHA-256 walk, file-IoC matching       |\n| `src\u002Fpwned_deps\u002Fextras_data\u002Fextras.json` | The campaign feed; sigstore-signed on `main`    |\n| `src\u002Fpwned_deps\u002Freport\u002F{text,json_out,sarif}.py` | Three renderers, identical schema input |\n\n## Why this exists\n\nSupply-chain compromises don't take a year off. Roughly every other\nmonth somebody's npm\u002FPyPI account gets hijacked, a maintainer hands\npublish rights to a stranger, or a typosquat gets coin-mined into\nproduction. The first 30 minutes of every incident is the same panic:\n\n> **\"Did *we* install one of those bad versions? Where? When? Is it\n> still in our caches and container images?\"**\n\nThe data to answer that already exists — across OSV, GHSA, vendor\nblogs, news writeups, and the affected package's GitHub issues — but\nnobody has time to assemble it under fire. `pwned-deps` does that\nassembly upfront: a curated, signed feed of named campaigns plus the\nOSV firehose, behind a single command that reads a lockfile and\nreturns red\u002Fgreen in seconds.\n\n### Campaigns the bundled feed already covers\n\nThese are the named, well-documented incidents the tool flags out of\nthe box on a fresh `pipx install` — no network required after the\nfirst cache fill, and the curated entries carry IoCs and remediation\nsteps that OSV's MAL-* records typically don't:\n\n| ID                | Year | Ecosystem | Campaign                                                      |\n|-------------------|------|-----------|---------------------------------------------------------------|\n| EXTRA-2018-0001   | 2018 | npm       | event-stream \u002F flatmap-stream (Copay wallet target)           |\n| EXTRA-2018-0002   | 2018 | npm       | eslint-scope token-stealer worm                               |\n| EXTRA-2021-0001   | 2021 | npm       | ua-parser-js account hijack (coin miner + Windows stealer)    |\n| EXTRA-2021-0002   | 2021 | npm       | coa account hijack (DanaBot family)                           |\n| EXTRA-2021-0003   | 2021 | npm       | rc account hijack (DanaBot family)                            |\n| EXTRA-2022-0001   | 2022 | PyPI      | ctx PyPI account takeover (env-var exfil)                     |\n| EXTRA-2022-0002   | 2022 | npm       | node-ipc protestware \u002F peacenotwar (CVE-2022-23812)           |\n| EXTRA-2022-0003   | 2022 | PyPI      | PyTorch nightly torchtriton dependency-confusion              |\n| EXTRA-2023-0001   | 2023 | npm       | @ledgerhq\u002Fconnect-kit Web3 wallet drainer (~$610k drained)    |\n| EXTRA-2024-0001   | 2024 | Linux     | xz-utils \u002F liblzma backdoor (CVE-2024-3094, CVSS 10.0)        |\n| EXTRA-2024-0002   | 2024 | npm       | @lottiefiles\u002Flottie-player crypto drainer                     |\n| EXTRA-2025-0001   | 2025 | GH Actions| tj-actions\u002Fchanged-files retroactive commit (CVE-2025-30066)  |\n| EXTRA-2025-0002   | 2025 | npm       | Shai-Hulud original — 180+ pkg self-replicating worm          |\n| EXTRA-2026-0001   | 2026 | npm       | Mini Shai-Hulud — SAP CAP packages                            |\n| EXTRA-2026-0002   | 2026 | npm\u002FPyPI  | Mini Shai-Hulud follow-on (intercom-client + lightning)       |\n\nThis is the curated feed only — every advisory in OSV's public\ndatabase is also queried automatically. Each entry above is sourced\nfrom at least one named research blog (full citations live in\n`extras.json`); adding a new campaign is a five-minute PR.\n\n### A worked example: Mini Shai-Hulud (April 29, 2026)\n\nUsed here because the IoC data is unusually rich (Wiz published every\nmalicious tarball SHA-256 plus the IDE-persistence files), making it\nthe cleanest demo of the audit-repo subcommand. **Four SAP-ecosystem\nnpm packages** (`@cap-js\u002Fsqlite@2.2.2`, `@cap-js\u002Fpostgres@2.2.2`,\n`@cap-js\u002Fdb-service@2.10.1`, `mbt@1.2.48`) were briefly poisoned with\na credential-stealing preinstall script. Anyone whose CI ran\n`npm install` during the ~2-4 h window pulled a payload that\nexfiltrated GitHub\u002Fnpm\u002FAWS\u002FAzure\u002FGCP\u002FK8s creds. Confirming whether\nyour pipeline ran during that window manually requires log-diving;\n`pwned-deps` is the 5-second answer.\n\nSources, all named research blogs:\n[The Hacker News](https:\u002F\u002Fthehackernews.com\u002F2026\u002F04\u002Fsap-npm-packages-compromised-by-mini.html),\n[SecurityBridge](https:\u002F\u002Fsecuritybridge.com\u002Fblog\u002Fa-mini-shai-hulud-has-appeared-when-the-npm-supply-chain-reaches-into-sap\u002F),\n[Wiz](https:\u002F\u002Fwww.wiz.io\u002Fblog\u002Fmini-shai-hulud-supply-chain-sap-npm).\n\n## Install\n\n```bash\npipx install pwned-deps          # recommended\n# or:\npip install --user pwned-deps\n```\n\nPython 3.10+ on macOS, Linux, or Windows.\n\n## See it in action\n\n**Try it now** — the [interactive lockfile simulator](https:\u002F\u002Fmkbhardwas12.github.io\u002Fpwned-deps\u002Fsimulator.html)\nruns a faithful in-browser replay of `pwned-deps check` against four\nsample lockfiles (Mini Shai-Hulud, event-stream historic, mixed,\nclean). Real campaign data, no network calls.\n\n[![lockfile simulator demo: pwned-deps check replays in-browser against a mixed npm lockfile and flags two malicious packages](docs\u002Fassets\u002Fdemo-simulator.gif)](https:\u002F\u002Fmkbhardwas12.github.io\u002Fpwned-deps\u002Fsimulator.html)\n\nBelow are the same outputs captured against bundled fixtures:\n\n> Real terminal output — captured with `tools\u002Fcapture_demos.py` against\n> the bundled fixtures, not mocked. Reproduce locally with\n> `pwned-deps check tests\u002Ffixtures\u002Fnpm\u002Fmini-shaihulud.lock.json`.\n\n| Scenario | Screenshot |\n|---|---|\n| **`check`** on a clean lockfile | ![clean scan](docs\u002Fassets\u002Fdemo-check-clean.svg) |\n| **`check`** on the historic event-stream\u002Fflatmap-stream campaign (2018) | ![event-stream scan](docs\u002Fassets\u002Fdemo-check-event-stream.svg) |\n| **`check`** on Mini Shai-Hulud (SAP CAP, April 2026) — full IoC payload | ![shai-hulud scan](docs\u002Fassets\u002Fdemo-check-shaihulud.svg) |\n| **`watch`** — Day 0 baseline, quiet day, alert day | ![watch demo](docs\u002Fassets\u002Fdemo-watch.svg) |\n| **PR comment** rendered by GitHub on a pull request | ![pr comment markdown](docs\u002Fassets\u002Fdemo-pr-comment-source.svg) |\n\n### Benchmark\n\nMatch-time on a 2024 MacBook Pro (M-series), offline mode:\n\n![benchmark](docs\u002Fassets\u002Fbenchmark.svg)\n\nMatcher work is sub-millisecond per lockfile against the bundled\nextras feed; first OSV query adds the network round-trip and is\ncached on disk for 24h. See [docs\u002Fassets\u002Fbenchmark.md](docs\u002Fassets\u002Fbenchmark.md)\nfor the raw numbers.\n\n## Quick usage\n\n```bash\n# Single file\npwned-deps check .\u002Fpackage-lock.json\n\n# Multiple files \u002F autodetect every supported lockfile in cwd\npwned-deps check .\npwned-deps check .\u002Fpyproject.toml .\u002Frequirements.lock .\u002Fpackage-lock.json\n\n# Skip network — use cached database only\npwned-deps check . --offline\n\n# Refresh the local cache\npwned-deps update\n\n# JSON for scripting\npwned-deps check . --format json\n\n# SARIF for GitHub Code Scanning\npwned-deps check . --format sarif > pwned-deps.sarif\n```\n\nExit codes:\n\n| Code | Meaning                                |\n|------|----------------------------------------|\n| `0`  | All clean                              |\n| `1`  | At least one MAL-* \u002F EXTRA-* hit (compromised package) |\n| `2`  | At least one HIGH\u002FCRITICAL CVE hit (no malicious hits) |\n| `3`  | Parse error                            |\n\n## Watch mode (the recurring-value workflow)\n\n`check` answers *\"is anything bad in my lockfile right now?\"*. **Watch\nmode** answers the question that matters every other day:\n\n> *\"Did anything I already have installed become flagged overnight?\"*\n\nThe first run records a baseline (the `(ecosystem, name, version)`\ntuples currently in your lockfile). Every run after that compares\nfresh advisory data against the baseline and exits **1** only when a\npackage that was *already* in your baseline is now publicly flagged.\nBrand-new findings on packages you don't depend on don't fire.\n\n```bash\n# Day 0 — record the baseline\npwned-deps watch .\u002Fpackage-lock.json --baseline .pwned-deps-baseline.json\n# → \"watch: baseline created at ... (47 packages)\"  (exit 0)\n\n# Day 1..N — run nightly in CI; exit 1 only if something you ship is now compromised\npwned-deps watch .\u002Fpackage-lock.json --baseline .pwned-deps-baseline.json --offline\n# → \"watch: OK — 47 baseline packages, no new findings\"   (exit 0)\n# … or:\n# → \"watch: ALERT — 1 package(s) in your baseline are now flagged:\n#     [MALICIOUS] npm:event-stream@3.3.6 (EXTRA-2018-0001) — event-stream \u002F flatmap-stream credential stealer\"\n#   (exit 1)\n\n# Re-baseline after a deliberate dependency upgrade\npwned-deps watch . --baseline .pwned-deps-baseline.json --update-baseline\n```\n\nThe baseline file is plain JSON, contains no machine-identifying data\n(only `(ecosystem, name, version)` triples), and is safe to commit\nto your repo so every contributor + CI runner shares one source of\ntruth. Pair with a nightly GitHub Actions cron — three lines of YAML\nand you have a same-day signal for every campaign that lands.\n\n## Supported ecosystems\n\n| Ecosystem | Lockfiles                                                 |\n|-----------|-----------------------------------------------------------|\n| npm       | `package-lock.json` (v1\u002Fv2\u002Fv3), `npm-shrinkwrap.json`, `pnpm-lock.yaml`, `yarn.lock` (v1 + Berry) |\n| PyPI      | `requirements*.txt` \u002F `requirements*.lock`, `Pipfile.lock`, `poetry.lock`, `uv.lock` |\n| crates.io | `Cargo.lock`                                              |\n| Go        | `go.sum`                                                  |\n| Maven     | `pom.xml` (`\u003Cdependencies>` + `\u003CdependencyManagement>`)   |\n| RubyGems  | `Gemfile.lock`                                            |\n\nLoose pins in `requirements.txt` (`>=`, `~=`, `\u003C`) and Maven property-\nvariable versions (`${spring.version}`) are scanned but reported as\n`version_unspecified` — we cannot match an advisory without an exact\nversion, so they're surfaced as a warning rather than skipped silently.\n\n## Real-world scenarios this is built for\n\nThese are the questions developers and security teams actually ask in\nthe first hour of a published supply-chain incident — and they recur\nevery few months across every ecosystem (see the campaign table\nabove). The Mini Shai-Hulud (Apr 29, 2026) example below is used\nbecause Wiz published unusually rich IoC data for it; the same\nworkflow applies to any campaign in the feed.\n\n**\"Did *we* run `npm install` during the 2-hour window?\"**\nPipe every lockfile in the org through `pwned-deps check`. Exit 1\nis the receipt that something matched. The bundled campaign feed\n(`extras.json`) covers the four SAP CAP packages the day of the\nincident — you don't have to wait for OSV.dev ingestion.\n\n**\"Where in our artifact stores are the bad tarballs?\"**\nFor campaigns where a primary source publishes the malicious\n`.tgz` SHA-256 (Wiz did for Mini Shai-Hulud), the CLI now prints\nthe hash next to every flagged version:\n\n```\n  @cap-js\u002Fsqlite@2.2.2\n    EXTRA-2026-0001  Mini Shai-Hulud (SAP CAP)\n    tarball sha256: a1da198bb4e883d077a0e13351bf2c3acdea10497152292e873d79d4f7420211\n```\n\nFeed that into `find . -name '*.tgz' -exec sha256sum {} +` against\nyour npm cache, container image layers, and artifact registries\nfor forensic confirmation — SecurityBridge's recommended approach\nrather than relying on version strings alone.\n\n**\"What else should we hunt for beyond the lockfile?\"**\nMost real campaigns leave non-lockfile traces: rogue GitHub repos\non the victim's own account, IDE-config persistence files\n(`.claude\u002Fexecution.js`, `.vscode\u002Fsetup.mjs`), known C2 domains.\nEach campaign in `extras.json` carries an `iocs` list and the CLI\nsurfaces it next to every finding:\n\n```\n  additional indicators to hunt for:\n    • GitHub repos with description 'A Mini Shai-Hulud has Appeared' …\n    • Commits whose message starts with 'OhNoWhatsGoingOnWithGitHub:' …\n    • Files dropped into other repos: .claude\u002Fexecution.js, .vscode\u002Fsetup.mjs …\n```\n\nNo more cross-referencing three vendor blogs to assemble the\nremediation list.\n\n**\"Did the second-stage payload actually land on a developer\nlaptop or build runner?\"**\nAfter the lockfile match, run the forensic file scanner:\n\n```bash\npwned-deps audit-repo .\npwned-deps audit-repo \u002Fpath\u002Fto\u002Fcheckout --format json\n```\n\nIt walks the tree (skipping `node_modules`, `.git`, `.venv`, etc.),\nhashes every file under 50 MiB, and matches against the bundled\nfile IoCs — SAP CAP `.claude\u002Fexecution.js`, `.vscode\u002Fsetup.mjs`,\nthe shared `setup.mjs` dropper, and the IDE-persistence\n`settings.json` \u002F `tasks.json` configurations. Exit codes:\n\n| Exit | Meaning                                                       |\n|-----:|---------------------------------------------------------------|\n|    0 | Clean                                                         |\n|    1 | At least one file's SHA-256 matches a known payload (CONFIRMED) |\n|    2 | A file sits at a known-persistence path but the bytes differ (SUSPECT — variant or modified) |\n\n**\"What about the follow-on packages? They were on a different\necosystem.\"**\n`extras.json` supports per-package ecosystem overrides so a single\ncampaign can span npm, PyPI, crates.io, etc. EXTRA-2026-0002\ncovers `intercom-client@7.0.5` (npm) and `lightning@2.6.2\u002F2.6.3`\n(PyPI) under one campaign — the same operator, the same shared\nC2, distinct package registries.\n\n**\"What about the first 30 minutes of an account-hijack incident,\nwhen we know the maintainer is compromised but don't yet have the\nexact bad versions?\"**\nEach campaign can declare a `compromised_maintainers` block:\n\n```json\n{\n  \"id\": \"EXTRA-YYYY-NNNN\",\n  \"ecosystem\": \"npm\",\n  \"packages\": [],\n  \"compromised_maintainers\": [\n    {\n      \"name\": \"alice\",\n      \"registry_url\": \"https:\u002F\u002Fwww.npmjs.com\u002F~alice\",\n      \"compromised_after\": \"2026-05-01T00:00:00Z\",\n      \"compromised_until\": \"2026-05-02T12:00:00Z\",\n      \"packages\": [\"alice-utils\", \"alice-cli\"]\n    }\n  ]\n}\n```\n\nAny package whose name appears in that list is reported as a\n**SUSPECT** finding (HIGH severity → exit 2), distinct from the\nCONFIRMED **MALICIOUS** hits (CRITICAL → exit 1). The summary spells\nout the compromise window so a human can decide whether their\ninstall pre-dates it. Once specific bad versions are confirmed, move\nthem into the `packages` block and the same lockfile re-scan will\nupgrade from SUSPECT to MALICIOUS automatically.\n\n**\"How do we trust the campaign feed itself?\"**\nEvery change to `extras.json` on `main` is signed with sigstore\nkeyless OIDC and logged to the public Rekor transparency log. See\n[SECURITY.md](SECURITY.md) §\"Verifying the campaign feed\" for the\nverification recipe. Force-pushes and silent removals can't escape\nthe append-only log.\n\n## CI integration\n\n### GitHub Actions (one line)\n\n```yaml\n- uses: mkbhardwas12\u002Fpwned-deps@v0.1.0\n  with:\n    path: .\n    fail-on: compromised   # also: `any` (HIGH\u002FCRITICAL too) or `never`\n    upload-sarif: true     # writes to GitHub Code Scanning\n```\n\nThe action installs `pwned-deps` from PyPI, scans every recognised\nlockfile under `path`, and uploads SARIF to Code Scanning. Step fails\nthe build on exit `1` (compromised package) by default. See\n[action.yml](action.yml) for all inputs.\n\n### Plain workflow step (no action wrapper)\n\n```yaml\n- run: pip install pwned-deps && pwned-deps check . --ci\n```\n\nExit `1` fails the build. Exit `2` is HIGH\u002FCRITICAL CVEs (no\nmalicious hits) — you decide whether that fails or warns.\n\n### Sticky PR comment (the bot workflow)\n\nFor pull requests, you usually want a *visible* signal next to the\ndiff — not just a red check. Drop\n[`examples\u002Fworkflows\u002Fpr-comment.yml`](examples\u002Fworkflows\u002Fpr-comment.yml)\ninto `.github\u002Fworkflows\u002F` and every PR that touches a lockfile gets a\nsingle sticky comment that gets *edited in place* on subsequent\npushes (no comment spam):\n\n```text\n## pwned-deps scan\n\n🚨 **1 compromised package(s)** detected\n\n| Severity   | Package                       | Advisory          | Campaign                              |\n|------------|-------------------------------|-------------------|---------------------------------------|\n| MALICIOUS  | npm:event-stream@3.3.6        | EXTRA-2018-0001 ↗ | event-stream \u002F flatmap-stream         |\n```\n\nMechanism: the workflow runs `pwned-deps check . --format json`,\npipes the JSON through [`tools\u002Fpr_comment.py`](tools\u002Fpr_comment.py)\n(stdlib-only, no extra deps), and uses `gh pr comment --edit-last`\nto find and update the prior comment by a magic marker. Comment-only\nmode (don't fail the build) is a one-line tweak documented in the\nexample.\n\n### Static HTML dashboard (org-wide visibility)\n\nFor platform\u002Fsecurity teams that need an aggregate view across\nmany repos, `pwned-deps report` consumes one or more JSON scan files\n(typically CI artifacts) and emits a single self-contained HTML\ndashboard:\n\n```bash\n# Each repo's CI uploads scan.json as an artifact; collect them, then:\npwned-deps report scans\u002F*.json -o dashboard.html --title \"ACME · supply chain\"\n```\n\n![dashboard preview](docs\u002Fassets\u002Fdemo-dashboard.png)\n\n> **Click to interact:** the same dashboard rendered for a 5-repo \"ACME Corp\" demo\n> is hosted live at \u003Chttps:\u002F\u002Fmkbhardwas12.github.io\u002Fpwned-deps\u002Fassets\u002Fdemo-dashboard.html>\n> (also committed at [`docs\u002Fassets\u002Fdemo-dashboard.html`](docs\u002Fassets\u002Fdemo-dashboard.html))\n> — try the filter chips and see the per-campaign rollup.\n\nThe HTML file is self-contained — inline CSS, no external assets,\nno telemetry, no JavaScript dependencies (one tiny vanilla-JS filter\nchip handler, no framework). Drop into S3, GitHub Pages, or `open`\nlocally. Zero infrastructure to host the org dashboard.\n\nWhat you get: top-level KPIs (scans, packages, MALICIOUS hits,\nHIGH\u002FCRITICAL CVEs), a per-source scans table, a campaign rollup\n(same advisory hitting >1 repo = high-priority cross-org incident),\nand a filterable findings table. Every campaign-supplied string is\nHTML-escaped at render time, and only `http(s):\u002F\u002F` reference URLs\nbecome clickable.\n\n### pre-commit\n\n```yaml\n# .pre-commit-config.yaml\nrepos:\n  - repo: https:\u002F\u002Fgithub.com\u002Fmkbhardwas12\u002Fpwned-deps\n    rev: v0.1.0\n    hooks:\n      - id: pwned-deps           # online (api.osv.dev)\n      # or:\n      # - id: pwned-deps-offline # cache only, no network\n```\n\nThe hook only fires when a recognised lockfile changes — unrelated\ncommits skip the network entirely.\n\n### GitLab CI\n\n```yaml\npwned-deps:\n  image: python:3.12-slim\n  script:\n    - pip install pwned-deps\n    - pwned-deps check . --ci\n  allow_failure: false\n```\n\n\n## Output formats\n\n* **`text`** (default) — colourful terminal output via `rich`,\n  MAL-*\u002FEXTRA-* findings prominently flagged.\n* **`json`** — machine-readable. Stable schema (top-level: `version`,\n  `summary`, `lockfiles[]`, each lockfile carries `findings[]` with\n  `id`, `severity`, `package`, `version`, `references`).\n* **`sarif`** — SARIF v2.1.0 for GitHub Code Scanning upload. Validates\n  against the OASIS schema; `partialFingerprints.primaryLocationLineHash`\n  is set so the same finding dedups across runs.\n\n## Threat model\n\n`pwned-deps` is itself a piece of supply-chain software. Highlights of\nthe safety contract:\n\n* **No execution of advisory or package content.** We never run\n  `npm install`, `pip install -r`, `cargo build`, `go get`, `mvn`,\n  `gem install`, or any other package-manager command on inputs.\n  Lockfile parsing is text\u002FJSON\u002FTOML\u002FXML\u002FYAML only.\n* **No `eval` \u002F `exec` \u002F `subprocess` \u002F `pickle.load` of user input.**\n  A `make verify-safety` target enforces this with a Python regex\n  scanner; the negative self-test plants `eval(\"1+1\")` and proves the\n  scanner catches it.\n* **Network allow-list.** The CLI talks only to `api.osv.dev` (and an\n  opt-in `--feed-file PATH` you explicitly hand to it). No telemetry,\n  no analytics, no crash reporting.\n* **Container-only dev** with non-root `appuser` UID 1000, network\n  denied during tests, source mounted read-only, base image pinned\n  to a SHA-256 digest.\n* **Pinned deps.** Production runtime dependencies are pinned by\n  exact version in `requirements.lock`; `--require-hashes` enforcement\n  before the first PyPI release is a TODO recorded in\n  `requirements.lock`.\n* **OIDC publishing only.** The `release.yml` workflow publishes to\n  PyPI through the Trusted Publishers OIDC flow — no long-lived\n  tokens in repository secrets.\n* **No service mode.** We never accept lockfiles via a hosted\n  backend we control. The future drag-drop web UI (V1.1) will be\n  fully client-side; lockfile contents never leave the browser.\n* **Eat your own dog food.** Every CI run executes\n  `pwned-deps check .\u002Fpyproject.toml .\u002Frequirements.lock`. If a\n  malicious version of one of our own deps appears, the release is\n  blocked.\n\nIf `pwned-deps` itself were compromised, the irony would kill the\nproject. We treat account hygiene as tier-1: hardware-key 2FA on\nGitHub, OIDC trusted publishing on PyPI, no shared maintainer\ncredentials.\n\n### Verify a release with SLSA provenance\n\nEvery published wheel and sdist ships with SLSA Level 3 build\nprovenance generated by [`slsa-github-generator`](https:\u002F\u002Fgithub.com\u002Fslsa-framework\u002Fslsa-github-generator).\nVerify before installing if you're paranoid (or in a regulated\nenvironment):\n\n```bash\npip download --no-deps pwned-deps\n# Grab the matching *.intoto.jsonl from the GitHub Release page,\n# then:\nslsa-verifier verify-artifact pwned_deps-*.whl \\\n    --provenance-path pwned_deps-*.intoto.jsonl \\\n    --source-uri github.com\u002Fmkbhardwas12\u002Fpwned-deps\n```\n\nA passing `slsa-verifier` run cryptographically proves the wheel\nwas built by [release.yml](.github\u002Fworkflows\u002Frelease.yml) on this\nrepository, by the tagged commit, with no human-in-the-middle.\n\n## Comparison\n\nHonest, hyperlink-checkable. Every claim should be verifiable from the\nlinked tool's public docs. **Submit a PR if any cell is wrong** — we'd\nrather correct than mislead.\n\n| Tool                                                         | Multi-ecosystem | Offline cache | Publisher signature check | MAL-* surfacing | Open campaign feed       | License                          |\n|--------------------------------------------------------------|-----------------|---------------|---------------------------|-----------------|--------------------------|----------------------------------|\n| [`npm audit`](https:\u002F\u002Fdocs.npmjs.com\u002Fcli\u002Fv10\u002Fcommands\u002Fnpm-audit) | npm only        | no            | yes (`--audit-signatures`, npm 9+) | partial         | no                       | open (Artistic-2.0)              |\n| [`pip-audit`](https:\u002F\u002Fgithub.com\u002Fpypa\u002Fpip-audit)             | PyPI only       | partial       | no                        | partial         | no                       | Apache-2.0                       |\n| [`osv-scanner`](https:\u002F\u002Fgithub.com\u002Fgoogle\u002Fosv-scanner)       | yes (the bar)   | yes           | no                        | partial         | no                       | Apache-2.0                       |\n| [`socket`](https:\u002F\u002Fgithub.com\u002FSocketDev\u002Fsocket-cli)          | yes             | n\u002Fa (cloud)   | yes                       | yes             | yes (free + paid tiers)  | MIT (CLI), proprietary (cloud)   |\n| **pwned-deps**                                               | yes             | yes           | no (planned V1.x)         | first-class¹    | yes (Sigstore-signed)    | Apache-2.0                       |\n\n¹ MAL-\\* and our `EXTRA-*` campaign IDs are always surfaced regardless\nof CVSS. Ships with **15 historic + recent campaigns** built in\n(event-stream 2018 → xz 2024 → tj-actions 2025 → Mini Shai-Hulud 2026).\n\n### Where each tool is the right answer\n\n- **[`osv-scanner`](https:\u002F\u002Fgithub.com\u002Fgoogle\u002Fosv-scanner)** is the\n  bar. Google-resourced, no project bias, container + filesystem\n  scanning. If you only run one tool, run that one.\n- **[`socket`](https:\u002F\u002Fsocket.dev)** has the deepest behavioural\n  analysis (it parses package source for risky API use). The free CLI\n  is enough for many teams; deeper insights are paid.\n- **[`pip-audit`](https:\u002F\u002Fgithub.com\u002Fpypa\u002Fpip-audit)** is the\n  PyPA-blessed Python-only choice; integrates cleanly with `pip\n  freeze` workflows.\n- **[`npm audit`](https:\u002F\u002Fdocs.npmjs.com\u002Fcli\u002Fv10\u002Fcommands\u002Fnpm-audit)**\n  is already on every Node developer's machine. Run it with\n  `--audit-signatures` (npm 9+) for publisher-key verification.\n\n`pwned-deps` adds: a friendlier red\u002Fgreen CLI UX, MAL-\\* as a\nfirst-class concept, the `audit-repo` forensic file scanner, and an\nopen Sigstore-signed campaign feed for incidents OSV hasn't yet\ningested. We don't pretend to replace any of the above; we're the\ntool you reach for at 2 a.m. when a fresh incident hits and you need\na yes\u002Fno answer about your pipeline before the CVE is published.\n\n## FAQ\n\n**Q. What happens if `api.osv.dev` is down?**\nThe CLI uses `~\u002F.cache\u002Fpwned-deps\u002Fosv.sqlite` (24 h TTL by default).\nRun `--offline` to skip the network entirely; whatever's cached is\nwhat you get. The exit code is identical — no network availability is\nsilently treated as \"all clean\".\n\n**Q. How do I add a new campaign before OSV ingests it?**\nSend a PR adding an entry to `src\u002Fpwned_deps\u002Fextras_data\u002Fextras.json`.\nEach campaign needs an ID, a name, a summary, ≥1 named-blog citation,\nthe affected ecosystem + (name, version) tuples, an exposure window,\nand a remediation list. Five-minute review target.\n\n**Q. Why does `pyproject.toml` print \"skipping … not a recognised\nlockfile shape\"?**\n`pwned-deps` audits *lockfiles* (resolved, exact versions). A\n`pyproject.toml` is a manifest with declared ranges — there's nothing\ndeterministic to match against an advisory. Pass it alongside your\nreal lockfile and it will be skipped with a warning rather than\ncrashing the run.\n\n**Q. Will you accept attached `.tgz`\u002F`.whl` files in issues to \"look\nat the malware\"?**\nNo. The contributing rules explicitly\nforbid attaching compromised package tarballs. PoC patterns are\nshared in text only.\n\n**Q. Can I scan Docker images \u002F SBOMs?**\nNot in V1. SBOM generation is `syft`'s job; reachability analysis is\nout of scope. We consume lockfiles, full stop.\n\n## Contributing\n\nIssues that include attack PoCs must share patterns in text only —\nnever attach malicious package tarballs to issues.\n\nAdding a new campaign is intentionally a 5-minute PR:\n\n1. Add an entry to `src\u002Fpwned_deps\u002Fextras_data\u002Fextras.json`. Cite at\n   least one named research blog (SecurityBridge, Wiz, Sophos, GHSA,\n   etc.). Do NOT fabricate version numbers; if a source doesn't pin\n   a version, use a `TODO(precise-version)` marker and document the\n   sources you checked.\n2. Add a fixture lockfile pinning one of the affected versions under\n   `tests\u002Ffixtures\u002F\u003Cecosystem>\u002F`.\n3. Run `make verify-safety && make test` (the dev container does the\n   rest).\n4. Open the PR.\n\n## Maintenance\n\nIssues are triaged within 7 days, not 24 hours. The project is\ndeliberately solo-OSS-friendly — we'd rather acknowledge slowly than\nburn out a single maintainer.\n\n## License\n\nApache License 2.0 — see [LICENSE](.\u002FLICENSE).\n\n## Maintainer\n\n`mkbhardwas12`\n\nIssues: \u003Chttps:\u002F\u002Fgithub.com\u002Fmkbhardwas12\u002Fpwned-deps\u002Fissues>\n","`pwned-deps` 是一个用于扫描 npm、PyPI、Maven、Cargo、Go 和 RubyGems 包的依赖项安全工具，能够快速检测出被劫持、拼写错误或恶意篡改的包版本。其核心功能包括通过 OSV 和自定义数据源进行多生态系统扫描，并支持多种锁定文件格式。该工具采用 Python 编写，具备 SLSA L3 安全级别和锁定容器 CI 集成，确保了高可靠性和安全性。适用于软件开发和 DevSecOps 场景，帮助开发者在持续集成流程中及时发现并修复潜在的安全漏洞。",2,"2026-06-11 03:32:32","CREATED_QUERY"]