bokuweb/sakimori

GitHub: bokuweb/sakimori

sakimori 是一个跨平台供应链防护工具,通过代理和监控机制拦截恶意包管理器依赖,保护开发环境安全。

Stars: 73 | Forks: 0

# sakimori [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/69da996e04143910.svg)](https://github.com/bokuweb/sakimori/actions/workflows/ci.yml) [![release](https://img.shields.io/github/v/release/bokuweb/sakimori?sort=semver)](https://github.com/bokuweb/sakimori/releases/latest) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 🚧Work In Progress🚧 **Cross-platform supply-chain guard for every package manager on your machine.** Silently blocks too-young versions, known-malicious packages and unsigned publishes — across **npm, cargo, pypi, nuget** — without touching your build tools. # Three commands, once. $ sakimori proxy install-ca # trust the proxy's root CA $ sakimori proxy install-daemon # auto-run in the background $ sakimori install-gate install # route your shell through it # Business as usual, permanently safer. $ npm install react # → proxy silently drops versions < 7d old # → npm picks the newest older version # → no error, no broken build, just a measurably safer dependency - [Why this exists](#why-this-exists) - [How it works](#how-it-works) — proxy architecture, 4 ecosystems - [Install](#install) - [Desktop quick start](#desktop-quick-start) - [Feature reference](#feature-reference) — every subcommand with examples - [CI usage (GitHub Actions)](#ci-usage-github-actions) - [Docker image](#docker-image) - [Configuration reference](#configuration-reference) - [Troubleshooting](#troubleshooting) - [Known limitations](#known-limitations) — what this honestly can't do - [Development](#development) ## Why this exists Supply-chain attacks follow a predictable timeline: 1. Attacker publishes a malicious version at `T+0` 2. Community notices, yanks it at `T+12–72h` Most victims install between hours 0–12. **pnpm 10.x** introduced [`minimumReleaseAge`](https://pnpm.io/next/settings#minimumreleaseage) to solve this for npm only — versions younger than the threshold become invisible to the resolver, which silently falls back to the newest older one. ## How it works ┌───────────────────┐ ┌────────────────────┐ │ npm / cargo / │ │ │ user ───► │ pip / uv / │ ─────►│ sakimori proxy │ ──► real registry │ dotnet / poetry │ HTTPS│ (localhost:8910) │ (metadata + tarball) └───────────────────┘ └─────────┬──────────┘ │ ▼ rewrites metadata: - drop versions < --min-age - drop unsigned versions (--require-provenance) - retarget npm dist-tags.latest - returns 403 for pinned tarball fetches to too-young versions The proxy's root CA is installed into the system trust store once; from then on, every HTTPS request your package managers make through `HTTPS_PROXY=http://127.0.0.1:8910` gets transparently filtered. ### Ecosystem coverage | ecosystem | silent auto-fallback | hard deny on pinned fetch | |---|---|---| | **crates.io** | ✅ sparse-index JSONL rewrite (drops too-young lines from `//`) | ✅ `403` on `.crate` download to a denied version | | **npm** | ✅ packument rewrite (drops versions + retargets `dist-tags.latest`) | ✅ `403` on `.tgz` download | | **pypi** | ✅ Warehouse JSON API (`/pypi//json`) + PEP 691 Simple JSON + PEP 503 Simple HTML via JSON-API lookup | ✅ `403` on `files.pythonhosted.org` tarball download | | **nuget** | ✅ registration-page rewrite (`/v3/registration*/...`) + flat-container index via registration lookup | ✅ `403` on `.nupkg` download | | **vscode-marketplace** | ✅ `extensionquery` JSON rewrite (drops `versions[].lastUpdated` younger than `--min-age`) on `marketplace.visualstudio.com` + `open-vsx.org` | ✅ `.vsix` lifecycle gate: `403` on startup-autorun (`activationEvents: ["*" | "onStartupFinished"]`), on any bundled `node_modules/*` package listed by OSV / GHSA as malicious, and on High-severity IOC content hits inside the archive ([roadmap #21 / #25 / #26](CLAUDE.md)) | All five ecosystems' metadata paths now rewrite silently — pnpm-style `minimumReleaseAge` across the board, no fail-hard in the common case. ### OS support matrix sakimori has two layers, and they have **different** OS coverage. Read this carefully before assuming "macOS isn't supported" or "Linux gets everything": | capability | Linux | macOS | Windows | |---|---|---|---| | **Fetch-layer** (proxy + install-gate) | | | | | ↳ `sakimori proxy start` (MITM + age filter + auto-fallback) | ✅ | ✅ | ✅ | | ↳ `sakimori install-gate install` (shell wiring) | ✅ zsh / bash / fish | ✅ zsh / bash / fish | ✅ PowerShell | | ↳ `~/.sakimori/installs.jsonl` recording — *who installed what, when* | ✅ | ✅ | ✅ | | ↳ `sakimori advisories scan` (OSV JOIN over the install log) | ✅ | ✅ | ✅ | | ↳ Lifecycle-script inspection (`--lifecycle-policy audit|block`) | ✅ | ✅ | ✅ | | ↳ `sakimori deps check` / `verify-cache` / `watch` | ✅ | ✅ | ✅ | | **Supervisor-layer** (`sakimori run` / `daemon`) | | | | | ↳ exec / open / connect events | ✅ eBPF | ❌ planned ([roadmap 5b](CLAUDE.md)) | ✅ ETW | | ↳ PPid attribution → package-manager origin | ✅ | ❌ | partial | | ↳ `--snapshot-workspace` (drift + known-IOC scan at shutdown) | ✅ | ❌ | partial | | ↳ Live network block | ✅ eBPF cgroup hooks | ❌ planned (#5) | ✅ Defender Firewall | | ↳ Live file/exec block | tripwire (SIGKILL); pre-syscall in progress (#4) | ❌ | audit-only | **Headline**: *if you only care about "tell me which packages I installed and warn me when one of them gets a CVE next week", macOS is a first-class platform.* That's the part most users want. The supervisor (live blocking, exec attribution, workspace drift) is where the Mac gap is — tracked as roadmap item 5b in CLAUDE.md (Apple's Endpoint Security framework, requires Apple-issued entitlement + SystemExtension signing). CI coverage matches: the [`macos-smoke` workflow](.github/workflows/macos-smoke.yml) exercises proxy start → pinned-tarball fetch → `installs.jsonl` → `advisories scan` → `install-gate shellenv` end-to-end on every PR that touches the relevant crates, so the cells marked ✅ for macOS above don't silently regress. ### Editor-extension coverage (VSCode / Cursor / Windsurf / OpenVSX) The 2026-05 GitHub-internal-repo compromise (poisoned VS Code extension on an employee device) made it concrete: editor and browser extensions are a parallel distribution channel that almost nothing on the supply-chain market actually catches end-to-end. sakimori covers it across four layers, each addressing a failure mode the others can't: | layer | what catches | where in sakimori | |---|---|---| | **Fetch (proxy)** | Marketplace install of a young / freshly-published extension | `extensionquery` JSON rewriter on `marketplace.visualstudio.com` + `open-vsx.org` — silent `minimumReleaseAge`-style fallback per ecosystem #20 | | **Runtime attribution** | Extension subtree opens `~/.ssh/id_ed25519` / hits IMDS / executes a downloaded payload | PPid walker recognises `code` / `cursor` / `windsurf` / `code-server` / `Code Helper (Plugin)` and stamps `source: vscode` on every Connect / Open / Exec event the extension subtree produces — persistence-write, cloud-secret-egress, IOC scanner all fire #19 | | **Workspace poisoning** | A repo with `.vscode/tasks.json` auto-running on `folderOpen` | IOC catalog v2026.05.21+ ships `vscode.tasks-folderopen-autorun` as a basename-scoped content needle (High severity, family `editor-extension`) #23 | | **Sideload tamper** | An extension installed bypassing the Marketplace fetch (e.g. dragged-in `.vsix`, manual `git clone` into `~/.vscode/extensions/`) | `sakimori extensions snapshot` / `extensions diff` auto-discovers `~/.vscode`, `~/.vscode-insiders`, `~/.cursor`, `~/.windsurf`, plus the platform `globalStorage` tree; the diff runs the IOC catalog against every added / modified path #24 | | **Bundled-dep poisoning** | An extension whose top-level manifest is clean but that ships a malicious transitive dep inside its `extension/node_modules/` tree | `.vsix` lifecycle gate walks the bundled tree, emits one `InstallEvent { ecosystem: npm }` per nested `package.json` (so they show up in `installs.jsonl` + OSV scan + OTLP / hub fan-out), and `403`s the install under `--lifecycle-policy block` when any bundled `(name, version)` is in the OSV known-bad set #25 | | **In-`.vsix` payload IOC** | Bundled JS / JSON that embeds a known exfil destination (`webhook.site`, `discord.com/api/webhooks/`, …) — including in transitive deps the publisher never audited | `.vsix` lifecycle gate runs the `sakimori-core::iocs` content-needle catalog over every text-shaped archive entry on fetch; High-severity hits `403` the install under `--lifecycle-policy block` and surface in the audit log regardless of mode #26 | #### Why this is different from "be a marketplace mirror" A few existing tools in this space try to be a **registry mirror** for the VS Code Marketplace — they stand up a server, scrape Microsoft's gallery, and ask users to repoint VS Code at the mirror via `product.json` edits or `extensions.gallery.serviceUrl` overrides. That approach has real friction: 1. **VS Code has no first-class "alternate registry" config.** The marketplace URL is hardcoded in `product.json`; switching it requires modifying Microsoft's binary, which the EULA forbids redistributing. (VSCodium, Cursor, Code-OSS, Windsurf — forks — *do* expose a configurable gallery setting; the EULA issue is specific to upstream VS Code.) 2. **Marketplace ToS treats redistribution carefully.** §3 of the VS Code Marketplace terms allows access to the gallery for downloading extensions; standing up an independent mirror that *re-serves* the gallery to other users sits in genuinely murky territory. Several mirror-style projects have hit takedown notices or quietly become enterprise-only because of this. **sakimori takes a different shape — an endpoint MITM proxy, not a mirror.** That sidesteps both problems: - **No editor binary modification.** The user sets `HTTPS_PROXY=http://127.0.0.1:8080` (via `sakimori install-gate install`, the same one-liner `npm install` already uses) and trusts sakimori's local CA. The marketplace request the editor makes is unchanged; only the *response* the editor receives is filtered to drop too-young versions. `product.json` stays byte-for-byte identical to whatever Microsoft shipped. - **No re-serving.** sakimori never stands up an authoritative gallery. It MITMs the user's *own* request to Microsoft (or Eclipse for OpenVSX), filters the JSON in transit, and hands it back. The bytes leave sakimori the moment the editor reads them; no per-tenant caching or republication. Architecturally this is the same posture Bitdefender, Kaspersky, Cisco Umbrella, and corporate SSL-inspection appliances have run for a decade — well-understood, both legally and operationally. - **Same proxy, every editor.** One running instance covers VS Code, Cursor, VSCodium, Code-OSS, code-server, and any other editor that speaks the Marketplace / OpenVSX `extensionquery` API. No per-editor configuration knob. #### Defence in depth, not just the proxy The proxy alone doesn't solve the problem — a determined attacker ships an extension *with* a deliberate publication delay so it clears `--min-age`, or sideloads via `.vsix` to bypass the proxy entirely. That's why the four-layer table above matters: - A sideloaded extension never hits the proxy → `extensions diff` still catches it (new files under `~/.vscode/extensions/`). - An aged-into-marketplace extension passes the rewriter → attribution + persistence-write rule pack catches the actual malicious behaviour (writing to `~/.ssh/`, hitting IMDS, etc.) at runtime. - A workspace `.vscode/tasks.json` autorun never touches the marketplace at all → the IOC catalog catches the dropper primitive directly. Registry-firewall tools (Sonatype Nexus Firewall, JFrog Xray) sit at the right layer for #1 but only for the proxy path. EDR / SCA tools cover none of the four directly. sakimori is the only endpoint agent we're aware of that covers all four with a shared attribution + IOC backbone. #### Roadmap items pending in this area `.vsix` / `.crx` lifecycle gate (block on `activationEvents: ["*"]`-style autorun primitives at fetch time), `Ecosystem:: VscodeExtension` propagation into the install log and OSV-JOIN advisory scan, and Chrome Web Store coverage are all tracked in [CLAUDE.md](CLAUDE.md) roadmap entries #20–#24. Pull requests welcome. ## Install Pick whichever fits your setup. ### Homebrew (macOS / Linux) brew install bokuweb/sakimori/sakimori # ↑ the repo-is-its-own-tap convention; no separate `brew tap` needed. Auto-updated on every release via the `homebrew-formula.yml` workflow — the formula lives at [`HomebrewFormula/sakimori.rb`](HomebrewFormula/sakimori.rb) in this repo. ### Pre-built binary (macOS / Linux / Windows) # macOS (Apple Silicon) curl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-aarch64-apple-darwin.tar.gz \ | sudo tar -xz -C /usr/local/bin # macOS (Intel) curl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-apple-darwin.tar.gz \ | sudo tar -xz -C /usr/local/bin # Linux (x86_64 musl static) curl -fsSL https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-unknown-linux-musl.tar.gz \ | sudo tar -xz -C /usr/local/bin # Windows (PowerShell) Invoke-WebRequest -Uri https://github.com/bokuweb/sakimori/releases/latest/download/sakimori-x86_64-pc-windows-msvc.tar.gz -OutFile c.tgz tar -xzf c.tgz -C "$env:USERPROFILE\.local\bin" Every release also ships a `.sha256` sidecar. The archive contains the `sakimori` binary (Linux also ships `sakimori.bpf.o` for the supervised-run mode). ### Docker / OCI docker run --rm -p 8910:8910 \ -v sakimori-conf:/etc/sakimori-xdg \ ghcr.io/bokuweb/sakimori-proxy:v0 \ --listen 0.0.0.0:8910 --min-age 7d Mount `/etc/sakimori-xdg` as a volume to persist the generated root CA across container restarts. See [Docker image](#docker-image). ### From source cargo install --git https://github.com/bokuweb/sakimori sakimori The Linux eBPF supervised-run mode additionally needs `rustup toolchain install nightly --component rust-src` + `cargo install bpf-linker`. Not required for proxy / deps / install-gate. ## Desktop quick start Three commands, once per machine. Each is idempotent. # 1. Generate the proxy's root CA and install it into the system # trust store. macOS uses `security`, Linux uses # `update-ca-certificates`, Windows uses elevated # `Import-Certificate` (triggers one UAC prompt). sakimori proxy install-ca # 2. Register the proxy as a background service so it's always up. # macOS: ~/Library/LaunchAgents/com.sakimori.proxy.plist # Linux: ~/.config/systemd/user/sakimori-proxy.service # Windows: Task Scheduler /sakimori-proxy sakimori proxy install-daemon # Follow the printed `launchctl bootstrap …` / `systemctl --user enable --now` # / `schtasks.exe /Create …` line. # 3. Append HTTPS_PROXY + CA bundle env vars to your shell rc. # Detects zsh / bash / fish / PowerShell from $SHELL (or your OS). sakimori install-gate install Open a new shell — everything's wired: $ env | grep -E 'HTTPS_PROXY|CARGO_HTTP_CAINFO' HTTPS_PROXY=http://127.0.0.1:8910 CARGO_HTTP_CAINFO=/Users/you/.config/sakimori/ca.pem $ sakimori doctor sakimori doctor ──────────────────────────────────────────────────────────── ✓ CA certificate /Users/you/.config/sakimori/ca.pem (644 bytes) ✓ CA private key /Users/you/.config/sakimori/ca.key ✓ Proxy reachable accepted TCP on 127.0.0.1:8910 ✓ $HTTPS_PROXY http://127.0.0.1:8910 ✓ install-gate rc /Users/you/.zshrc ✓ Daemon unit /Users/you/Library/LaunchAgents/com.sakimori.proxy.plist ──────────────────────────────────────────────────────────── 6 check(s): 0 fail, 0 warn From here, `npm install` / `pnpm add` / `yarn add` / `cargo add` / `cargo build` / `pip install` / `uv add` / `poetry add` / `dotnet add package` / `dotnet restore` all flow through the proxy. ### Observable proof that it works $ curl -s https://index.crates.io/se/rd/serde | wc -l # direct 315 $ curl -sx http://127.0.0.1:8910 https://index.crates.io/se/rd/serde | wc -l 306 # the 9 most recent versions are invisible to cargo's resolver cargo picks the newest remaining in-range version — no error, just safer. Same shape on the other three ecosystems. ### Uninstall Reverse each step (same flags): sakimori install-gate uninstall # strip block from shell rc sakimori proxy uninstall-daemon # remove launchd / systemd / Task Scheduler unit sakimori proxy uninstall-ca # remove CA from system trust store rm -rf ~/.config/sakimori # delete CA + key (optional) ## Feature reference ### `proxy start` Start the MITM HTTPS proxy in the foreground. `install-daemon` wraps this for background use; run it directly when you want logs on stdout or you're running the proxy yourself in Docker. Run `sakimori proxy start --help` for the canonical, always-up-to-date flag list. The grouping below summarises the surface so you know what knobs exist; defaults are tuned for "drop into `~/.zshrc` and forget" desktop use — CI workflows usually want to layer on the lifecycle gate and provenance check. sakimori proxy start [OPTIONS] | group | flags | what it does | |---|---|---| | **Networking** | `--listen ` (default `127.0.0.1:8910`), `--config-dir ` | Where the proxy listens; where its CA / config files live. | | **Release-age gate** | `--min-age ` (default `7d`), `--fail-on-missing` | Versions younger than `--min-age` are silently dropped from the metadata response the client sees. `--fail-on-missing` treats unknown publish dates as deny (default: fail-open). | | **Provenance gate** (npm only) | `--require-provenance` | Drop every npm version that doesn't carry a Sigstore provenance claim. Closes the "stolen publish token" hole `--min-age` alone can't cover — a thief can publish immediately, but without an OIDC-authenticated CI run can't attach valid provenance. | | **Known-malicious gate** | `--osv`, `--osv-mirror`, `--osv-mirror-url ` | Consult OSV.dev (live) and/or the sakimori-hosted pre-filtered mirror; hard-deny versions tagged MAL-* / known-malicious regardless of `--min-age`. | | **Typosquat detection** | `--typosquat {warn,block}`, `--typosquat-mirror`, `--typosquat-mirror-url ` | Compare incoming package names against a top-N-per-ecosystem list (lodash, requests, tokio, Newtonsoft.Json, …) and warn or block close-distance candidates. | | **Lifecycle-script gate** (Shai-Hulud-class defence) | `--lifecycle-policy {audit,block,strip}`, `--lifecycle-allow ` (repeatable), `--lifecycle-strip-on-failure {block,passthrough}`, `--lifecycle-strip-cache-dir `, `--lifecycle-no-strip-cache` | `audit` logs install-time scripts; `block` 403s tarballs that ship them; `strip` rewrites the tarball in place to drop the script keys + recompute the SRI hash + amend the packument so npm's integrity verifier agrees. Same policy also drives the `.vsix` gate: startup-autorun denials carry `x-sakimori-deny: lifecycle-vsix`, bundled-dep known-bad denials carry `lifecycle-vsix-bundled-known-bad`, and in-archive IOC denials carry `lifecycle-vsix-ioc`. See CLAUDE.md Roadmap #15 / #21 / #25 / #26 for the threat models. | | **Egress allow-list** | `--network-allow ` (repeatable), `--network-allow-file ` | Default-deny hostname filter. Patterns: `host.example.com` (exact) or `*.example.com` (any subdomain, excludes apex). Off by default. | | **Install log + advisories** | `--no-install-log`, `--install-log ` | The local-first append-only audit log feeding `sakimori advisories scan`. On by default at `~/.sakimori/installs.jsonl`. | | **OTLP fan-out** | `--otlp-endpoint `, `--otlp-header ` (repeatable) | Mirror every allowed install as an OTLP/HTTP `LogRecord` to Datadog / Honeycomb / Loki / a self-run otel-collector. The **wire envelope** is spec-compliant OTLP/HTTP JSON (any collector parses it); the **`package.*` attribute keys** are sakimori-specific, not OpenTelemetry semantic conventions — OTel has no "package install" semconv yet. See [OTLP semantic conventions](#otlp-semantic-conventions) below. | | **Custom registries** | `--npm-registry`, `--pypi-registry`, `--pypi-files-host`, `--cargo-registry-host`, `--cargo-sparse-host`, `--nuget-registry` (all repeatable), `--registries-config `, `--upstream-ca-file ` (repeatable) | Teach the proxy about internal mirrors / replacement registries so the rewriters + lifecycle gate fire on their traffic too. `--upstream-ca-file` adds a PEM CA to the upstream rustls trust store for mirrors behind a private CA. See the [Custom registries](#custom--internal-registries) subsection below. | **First-run side effect**: generates a self-signed root CA at the config dir and prints the OS-specific trust command. Subsequent runs reuse the existing CA. **Egress allow-list** closes the eBPF-by-IP gap: when you also run `sakimori run` with a network policy, the kernel layer enforces by resolved IP and loses against CDN rotation. The proxy's hostname filter sees the SNI / `Host:` value the client actually asked for, so an entry like `*.githubusercontent.com` matches every rotating CDN IP automatically — the same convention `step-security/harden-runner` users are used to: sakimori proxy start \ --network-allow api.github.com \ --network-allow '*.githubusercontent.com' \ --network-allow registry.npmjs.org #### Custom / internal registries The rewriters + lifecycle gate dispatch by **hostname**. By default only the canonical public hosts are watched — traffic to `registry.npmjs.org` runs the npm packument rewriter, traffic to `pypi.org` runs the PyPI rewriters, etc. Internal mirrors / replacement registries (Verdaccio, GitHub Packages, Artifactory, Takumi Guard, JFrog, Nexus, …) are passed through opaquely unless you teach the proxy about them. Three layered sources, applied in order with case-insensitive dedupe: built-in defaults → optional TOML config file (`--registries-config `) → per-ecosystem CLI flags. The canonical public hosts remain watched alongside your additions. # CLI flags (repeatable). Each accepts a bare hostname or a URL — # the host part is extracted (https://npm.flatt.tech:8443/path → # npm.flatt.tech). sakimori proxy start \ --npm-registry npm.flatt.tech \ --npm-registry 'https://npm.corp.internal:8443/' \ --pypi-registry pypi.corp.internal \ --nuget-registry nuget.corp.internal | flag | feeds | canonical default | |---|---|---| | `--npm-registry` | npm packument + tarball | `registry.npmjs.org` | | `--pypi-registry` | PyPI Warehouse JSON + Simple index | `pypi.org` | | `--pypi-files-host` | PyPI sdist + wheel downloads | `files.pythonhosted.org` | | `--cargo-registry-host` | crates.io API `/api/v1/crates/…` | `crates.io` | | `--cargo-sparse-host` | crates.io sparse index | `index.crates.io` | | `--nuget-registry` | NuGet registration + flat-container | `api.nuget.org` | Or pin the same lists in a config file (so a team can ship one canonical config and not paste the same flags into every invocation): # ~/.config/sakimori/registries.toml [registries] npm = ["registry.npmjs.org", "npm.flatt.tech"] pypi_index = ["pypi.org"] pypi_files = ["files.pythonhosted.org"] crates = ["crates.io"] crates_sparse = ["index.crates.io"] nuget = ["api.nuget.org"] sakimori proxy start --registries-config ~/.config/sakimori/registries.toml To lock the proxy to *only* the internal mirrors and reject the canonical public hosts, combine with `--network-allow`: sakimori proxy start \ --registries-config /etc/sakimori/registries.toml \ --network-allow npm.corp.internal \ --network-allow pypi.corp.internal If the internal mirror's TLS chain is signed by a **private CA** not in the `webpki-roots`-shipped trust store, pass each CA PEM file with `--upstream-ca-file` (repeatable). Without this the upstream handshake fails with `UnknownIssuer` even when the hostname is on `--registries-config`: sakimori proxy start \ --registries-config /etc/sakimori/registries.toml \ --upstream-ca-file /etc/ssl/corp-root-ca.pem \ --upstream-ca-file /etc/ssl/intermediate.pem **Non-goals** (intentionally not done): - **Path-shape rewriting.** The custom host must serve the canonical registry's URL shape (npm packument + `//-/-.tgz`; PyPI Warehouse JSON / PEP 503/691 Simple; NuGet v3 registration + flat-container; cargo sparse). A mirror that exposes a different layout (e.g. Artifactory at `/artifactory/api/npm//`) needs a path-prefix-aware parser variant — not implemented. - **`dist.tarball` URL rewriting.** The npm rewriter preserves the upstream's own tarball URL byte-for-byte — mirrors that serve their own tarball URLs keep doing so transparently. #### OTLP semantic conventions `--otlp-endpoint` emits one OTLP/HTTP **JSON** `LogRecord` per allowed install. Two layers, with different compliance stories: - **Envelope — OTLP-wire compliant.** The `resourceLogs[].scopeLogs[].logRecords[]` shape, the proto3→JSON name mapping (camelCase wire names; `timeUnixNano` as a decimal string for 64-bit ints), the `AnyValue` variant keys (`stringValue`, `intValue`, …), and the resource attributes `service.name` / `service.version` all follow the OTLP spec. Any spec-compliant collector (otel-collector-contrib, Datadog Agent's OTLP receiver, Honeycomb's OTel endpoint, Loki, …) parses the payload. This is enforced by [`crates/sakimori-proxy/tests/otlp_proto_roundtrip.rs`][otlp-rt], which deserializes every emitted payload through `opentelemetry-proto`'s generated `ExportLogsServiceRequest` type — same strict shape gate a real collector applies. - **Attribute keys — sakimori-specific, NOT semconv.** The per-install fields use a `package.*` namespace (`package.ecosystem`, `package.name`, `package.version`, `package.resolved_at`, `package.execution_mode`, `package.project_path`, `package.user_agent`, `package.git.*`). OpenTelemetry has no registered "package install event" attribute set yet, so we use our own namespace rather than shoehorn the data into `code.*` or `vcs.*`. If/when semconv ships an official equivalent (e.g. `software.package.*`), sakimori will add it alongside the existing keys rather than rename — existing dashboards keep working. If you grep your collector config for "semconv-compliant attributes": **no, these aren't.** If you grep for "OTLP-wire compatible": **yes, they are.** ### `proxy install-ca` / `uninstall-ca` Add / remove the root CA from the OS trust store. Cross-platform: | OS | Mechanism | Privilege prompt | |---|---|---| | macOS | `security add-trusted-cert -k /Library/Keychains/System.keychain` | `sudo` | | Linux | copy to `/usr/local/share/ca-certificates/` + `update-ca-certificates` | `sudo` | | Windows | `Import-Certificate -CertStoreLocation Cert:\LocalMachine\Root` | UAC via `Start-Process -Verb RunAs` | If you're not elevated, sakimori prints the exact shell command and exits — no silent reruns with privileges. sakimori proxy install-ca [--config-dir ] sakimori proxy uninstall-ca [--config-dir ] ### `proxy install-daemon` / `uninstall-daemon` Write a user-level service unit so the proxy runs in the background at login and restarts on failure. | OS | Unit | Location | |---|---|---| | macOS | launchd plist (`KeepAlive`, `RunAtLoad`, `Background` ProcessType) | `~/Library/LaunchAgents/com.sakimori.proxy.plist` | | Linux | systemd `--user` unit (`Restart=on-failure`, `WantedBy=default.target`) | `~/.config/systemd/user/sakimori-proxy.service` | | Windows | Task Scheduler v1.4 XML (`LogonTrigger`, `RestartOnFailure 99×1m`, `Hidden`) | `%LOCALAPPDATA%\sakimori\sakimori-proxy.task.xml` | sakimori proxy install-daemon [OPTIONS] Options: --listen [default: 127.0.0.1:8910] --min-age [default: 7d] --binary Override the sakimori binary path baked into the unit. Defaults to the canonical path of the currently-running executable. The command prints the exact activation line (`launchctl bootstrap` / `systemctl --user enable --now` / `schtasks.exe /Create`) — run that to start the service. ### `install-gate` Edit the user's shell rc file so every new shell exports `HTTPS_PROXY` + CA-bundle env vars pointing at the proxy. Idempotent via `# >>> sakimori install-gate >>>` sentinels. sakimori install-gate shellenv [--listen ] [--shell {bash,zsh,fish,powershell}] sakimori install-gate install [--rc ] [--shell ...] sakimori install-gate uninstall [--rc ] [--shell ...] Environment variables set (per shell): | var | who uses it | |---|---| | `HTTPS_PROXY` / `HTTP_PROXY` (+ lowercase variants) | curl, npm, pip, cargo, dotnet, git | | `CARGO_HTTP_CAINFO` | cargo (uses libcurl; doesn't honour system trust store on Linux) | | `PIP_CERT` | pip | | `NODE_EXTRA_CA_CERTS` | npm, yarn, pnpm | | `REQUESTS_CA_BUNDLE` | Python `requests`, poetry, uv | | `SSL_CERT_FILE` | generic OpenSSL-using tools | Default rc file per shell: | shell | path | |---|---| | bash | `~/.bashrc` | | zsh | `~/.zshrc` | | fish | `~/.config/fish/config.fish` | | powershell | `$PROFILE` = `~/Documents/PowerShell/Microsoft.PowerShell_profile.ps1` | ### `doctor` One-command diagnostic. Checks: 1. CA certificate exists + non-empty 2. CA private key exists + `chmod 600` (Unix) 3. Proxy is accepting TCP on `--listen` 4. `$HTTPS_PROXY` in the current shell matches the proxy address 5. Shell rc file contains the install-gate sentinel 6. Daemon unit file exists at the expected location Exits `0` on no failures (warnings are informational), `1` otherwise. sakimori doctor [--listen ] [--config-dir ] [--rc ] Sample output when the proxy is down: ✓ CA certificate /Users/you/.config/sakimori/ca.pem (644 bytes) ✓ CA private key /Users/you/.config/sakimori/ca.key ✗ Proxy reachable no listener on 127.0.0.1:8910: Connection refused ↳ start it: `sakimori proxy start` (or, for background: `sakimori proxy install-daemon`) ! $HTTPS_PROXY unset in this shell ↳ run `sakimori install-gate install` and open a new shell ### `deps check` Lockfile-level age gate, usable standalone (no proxy required). Good for a **pre-install CI step** that fails the build before the malicious package is even fetched. sakimori deps check --min-age 7d Cargo.lock package-lock.json # Different thresholds per ecosystem? Run twice. sakimori deps check --min-age 14d Cargo.lock sakimori deps check --min-age 3d package-lock.json # Ignore first-party packages. sakimori deps check --min-age 7d --ignore '@my-org/*' package-lock.json # Machine-readable output for CI gating. sakimori deps check --min-age 7d --format json Cargo.lock Supported lockfile formats: | ecosystem | lockfile | registry endpoint consulted | |---|---|---| | cargo | `Cargo.lock` | `crates.io/api/v1/crates/` | | npm | `package-lock.json` (lockfileVersion ≥ 2) | `registry.npmjs.org` | | pypi | `uv.lock`, `poetry.lock`, `requirements.txt` (exact `==` pins only) | `pypi.org/pypi///json` | | nuget | `packages.lock.json` (central-package-management) | `api.nuget.org/v3/registration5-{semver1,gz-semver2}/…` | Exit codes: | code | meaning | |---|---| | 0 | all packages meet the threshold | | 1 | at least one violation | | 2 | parse or I/O error | Cache location: `$XDG_CACHE_HOME/sakimori/deps-cache.json` (`%LOCALAPPDATA%\sakimori\…` on Windows). Publish dates are immutable, so there's no TTL. ### `deps verify-cache` Re-hash the package manager's local cache against the lockfile's `integrity:` fields and fail if any byte doesn't match what the lockfile pinned. This catches the *content* half of the **TanStack 2025 npm supply-chain attack**: a tarball restored from `actions/ cache` whose bytes have been swapped, while the lockfile entry itself looks untouched. Run it right after install, in the brief moment when the store is fully populated but nothing has built against it yet: # npm cacache (uses ~/.npm/_cacache by default) sakimori deps verify-cache --lockfile package-lock.json # pnpm store v3 (auto-picks ~/.local/share/pnpm/store/v3 on Linux, # ~/Library/pnpm/store/v3 on macOS) sakimori deps verify-cache --lockfile pnpm-lock.yaml # cargo registry cache (walks $CARGO_HOME/registry/cache/*/) sakimori deps verify-cache --lockfile Cargo.lock # Override the store path (monorepos with isolated stores, corporate # runners with non-standard layouts). Windows defaults are auto- # detected (`%LOCALAPPDATA%\npm-cache\_cacache`, `%LOCALAPPDATA%\pnpm\store\v3`). sakimori deps verify-cache --lockfile pnpm-lock.yaml --cache /opt/pnpm-store/v3 # Machine-readable for CI gating sakimori deps verify-cache --lockfile package-lock.json --format json Supported stores: | ecosystem | lockfile | store walked | |---|---|---| | npm | `package-lock.json` (v2/v3) | `~/.npm/_cacache/content-v2////` | | pnpm | `pnpm-lock.yaml` (v6–v9) | `/v3/files//[-exec]` + per-tarball `-index.json` | | cargo | `Cargo.lock` | `$CARGO_HOME/registry/cache//-.crate` | Exit codes: | code | meaning | |---|---| | 0 | every lockfile entry verifies cleanly against the store | | 1 | at least one mismatch or missing-from-store entry | | 2 | parse / I/O error | The same check is wrapped as a one-line GitHub Actions step — see [CI usage](#ci-usage-github-actions) below. ### `deps watch` Long-running FS-event watcher for lockfile changes. Designed for launchd at login. # One-off (Ctrl-C to quit) sakimori deps watch ~/code --min-age 7d # With modal prompts (Keep / Revert via osascript) sakimori deps watch ~/code --min-age 7d --action prompt # Stdout logging, e.g. for tmux / screen sakimori deps watch ~/code --min-age 7d --notifier stdout `--action` controls what happens on violation: | value | behaviour | |---|---| | `notify` (default) | Desktop notification. Lockfile untouched, nothing blocked. | | `prompt` (macOS only) | Keep / Revert modal via osascript. Revert runs `git checkout HEAD -- `. | | `revert` | Silently restore the lockfile to `HEAD` via git. Destructive; file must be tracked. | See [packaging/macos/README.md](packaging/macos/README.md) for the launchd plist. ### `workspace snapshot` / `workspace diff` Detect unexpected file edits made during a build — the supply-chain analogue of "did this `npm install` rewrite my source files / `.git/config` / CI configuration?". Pure offline; no network. # Before the build coronarium workspace snapshot $GITHUB_WORKSPACE -o /tmp/before.json cargo build # …or whatever you actually want to audit # After the build — exits non-zero on any drift coronarium workspace diff /tmp/before.json $GITHUB_WORKSPACE What the diff reports: files **added**, **modified** (size or SHA-256 changed), or **removed** between the two snapshots. Always-skipped directory basenames (anywhere in the tree): `.git`, `node_modules`, `target`, `dist`, `build`, `vendor`, `__pycache__`, `.venv`, `venv`, `.next`, `.turbo`, `.cache`. The list is hardcoded — `.gitignore` is **not** honoured because an attacker can write into it. Pass `--skip ` (repeatable) to extend the list for your own build artefacts. Symlinks are recorded by target string; the link target is not dereferenced. Files larger than 64 MiB default to a size-only entry (no SHA), so two oversized files with identical sizes but different contents will read as unchanged — bump `--max-file-bytes` if that matters for your repo. `--format json` for machine-readable output. `--allow-drift` suppresses the non-zero exit when you only want the report. ### `extensions snapshot` / `extensions diff` The editor-extension counterpart of `workspace snapshot`. Auto- discovers every editor extension root that exists on the host — `~/.vscode/extensions/`, `~/.vscode-insiders/extensions/`, `~/.cursor/extensions/`, `~/.windsurf/extensions/`, plus the platform-appropriate VS Code `User/globalStorage/` — and produces one merged snapshot. Each file's relative path is prefixed with the root's label (`vscode-extensions/foo.bar-1.0.0/package.json`) so the same extension id installed under two editors doesn't collide. # Take a baseline (run when you trust the current state) sakimori extensions snapshot -o ~/.sakimori/extensions-baseline.json # Some time later — perhaps after `git pull`, perhaps daily via cron sakimori extensions diff ~/.sakimori/extensions-baseline.json The diff reports added / modified / removed entries and runs the known-IOC catalog against every implicated path: a sideloaded `.vsix` whose `package.json` references `discord.com/api/ webhooks/`, or a workspace's `.vscode/tasks.json` configured to auto-run on `folderOpen`, surfaces as both a structural drift entry and a High-severity IOC hit. High-severity IOC hits force exit 1 unconditionally; structural drift exits 1 unless `--allow-drift`. A user without Cursor installed sees no `cursor-extensions/` entries — the walker filters to roots that actually exist at call time. `--home ` overrides `$HOME` for tests / CI. This is the **sideload backstop**: even if an attacker bypasses the marketplace fetch entirely (drag-and-drop `.vsix`, `git clone` directly into the extensions dir, vendored install), the diff catches the new files. Pair with the proxy's `extensionquery` rewriter for the fetch path and you cover both the marketplace-bound and out-of-band install routes. ### `actions audit` Static analysis for `.github/workflows/*.yml`. Walks every `uses:` in the workflow and flags any reference that isn't pinned to a 40-char commit SHA — the supply-chain analogue of an unpinned dependency. Offline by default; opt into the GitHub API with `--resolve` when you want the suggested replacement SHA inline. sakimori actions audit .github/workflows/*.yml # Machine-readable. sakimori actions audit --format json .github/workflows/ci.yml # Treat first-party (actions/*, github/*) mutable refs as blocking # too — useful once you've already pinned all your third-party deps. sakimori actions audit --strict .github/workflows/*.yml # Look up the current SHA each mutable @ resolves to via the # GitHub REST API. Reads $GITHUB_TOKEN from the env to lift the # rate limit from 60/hour to 5000/hour. The output gets a # `→ resolved: ` line per finding (text) or a `resolved_sha` # field (JSON) so you can copy-paste the right pinned form. sakimori actions audit --resolve .github/workflows/*.yml Severity: | | when | |---|---| | **error** | third-party action with mutable tag/branch (`foo/bar@v1`, `foo/bar@main`) | | **warn** | first-party (`actions/*`, `github/*`) mutable tag, or docker image without `@sha256:` digest | | **ok** | 40-char SHA pin, local action (`./...`), docker image with digest | Exit code: `1` when at least one error is present (or any warn, under `--strict`); `0` otherwise. Composite-action `action.yml` files are ignored — only workflow files (those with a top-level `jobs:` block) are walked. Resolution failures (rate-limit, removed action) appear as `→ resolve failed: …` per finding without aborting the audit. **Workflow-level lint** (in addition to per-`uses:` SHA pinning): the auditor also flags the `pull_request_target` + writable Actions cache pattern — the TanStack 2025 cache-poisoning vector. If a workflow runs on `pull_request_target` (or `workflow_run`) **and** any job step writes to the GitHub Actions cache, that's an Error. sakimori actions audit .github/workflows/bundle-size.yml # .github/workflows/bundle-size.yml (1 ok, 0 warn, 0 error) # ERROR [pull_request_target_with_cache_write] workflow runs on # `pull_request_target` and writes to the Actions cache — # an untrusted fork PR can poison the cache that a later # trusted workflow restores (TanStack-style npm supply-chain # compromise). … # · size (actions/cache@v4): actions/cache writes via post-step on cache miss Detected cache writers: `actions/cache@*`, `actions/cache/save@*`, `actions/setup-{node,python,java,dotnet,ruby}` with `with.cache:`, `actions/setup-go` (caches by default), `Swatinem/rust-cache`, `mozilla-actions/sccache-action`, `astral-sh/setup-uv` with `enable-cache: true`. Cache writes use a runner-internal token, not the workflow `GITHUB_TOKEN`, so `permissions: contents: read` does **not** block them. Split cache-writing steps into a separate workflow that doesn't run on fork PRs, or gate the offending job behind `if: github.event.pull_request.head.repo.full_name == github.repository`. JSON output puts these under a top-level `workflow_findings` array alongside the per-`uses:` `findings`. ### `run` Wraps a command under eBPF (Linux) / ETW (Windows) supervision and observes — optionally denies — its syscalls: - `connect(2)` on IPv4 / IPv6 - `openat(2)` - `execve(2)` sakimori run \ --policy .github/sakimori.yml \ --mode audit \ --log sakimori.log.json \ --html sakimori-report.html \ -- cargo test Flags: | flag | env | default | description | |---|---|---|---| | `--policy` / `-p` | `SAKIMORI_POLICY` | — | policy file (YAML or JSON) | | `--mode` | — | from policy | `audit` or `block` — overrides the policy's `mode:` | | `--log` | — | `-` (stdout) | JSON audit log destination | | `--summary` | `GITHUB_STEP_SUMMARY` | — | markdown summary | | `--html` | — | — | self-contained HTML report (dark-mode aware, filterable) | | `--snapshot-workspace` | — | — | dir to hash before/after the run; drift goes into the JSON log + step summary, and (in block mode) makes the run fail | | `--snapshot-skip` | — | — | extra dir basenames to skip during the snapshot (repeatable) | | `--snapshot-extensions` | — | — | snapshot every editor-extension dir under `$HOME` before + after the run; drift + pre-existing IOC + drift-time IOC sections land in the JSON log under `extension_drift` / `extension_iocs` / `extension_iocs_baseline`. High-severity IOC fails the run unconditionally; structural drift fails the run only in block mode | Exit code: child's exit code, unless `mode=block` and **either**: - at least one event was denied, **or** - a `--snapshot-workspace` baseline was taken and the post-run diff is non-empty → exits `1` either way. Policy format: # .github/sakimori.yml mode: block # audit | block network: # default is `deny`, so only listed destinations can be reached. allow: - target: api.github.com # A+AAAA resolved at startup ports: [443] - target: 140.82.112.0/20 # CIDR expanded (up to /16 for v4) ports: [22, 443] - target: 2606:4700::/48 # IPv6 CIDRs work too ports: [443] file: default: allow # most builds open hundreds of files deny: - /etc/shadow - /root/.ssh process: deny_exec: - /usr/bin/nc env: # Scrub the env block before the child execs. Real prevention, # not a tripwire — `Command::env_clear()` happens before # `execve`, so the child (and its postinstall grandchildren) # literally cannot read what's been stripped. default: pass # `pass` keeps everything not on `deny`; # `clear` flips it to allowlist mode allow: [PATH, HOME, "GITHUB_*"] deny: ["AWS_*", "*_TOKEN", "*_SECRET", NPM_TOKEN] **First-time setup pattern** — run in `mode: audit` once, then let `policy suggest` turn the log into a starter policy, prune by hand, and flip to `mode: block`: coronarium run --mode audit --log audit.json -- cargo test coronarium policy suggest audit.json -o .github/coronarium.yml $EDITOR .github/coronarium.yml # remove anything you don't want allowed coronarium run -p .github/coronarium.yml --mode block -- cargo test `suggest` populates `network.allow` (one entry per host:port observed, hostnames preferred over raw IPs) and `file.allow` (one entry per parent directory observed). Exec targets are surfaced as a commented `# observed_exec:` block — `process.deny_exec` is deliberately left empty because the suggester can't know which of the binaries the build actually wanted. **Curated rule packs (`policy preset`):** ready-to-merge YAML blocks for known supply-chain attack patterns. Currently shipped: - `sakimori policy preset persistence` — `file.deny` tripwire for OS-level persistence writes (launchd / systemd / cron / shell rc / `~/.ssh`). Per-user paths expand from `$HOME` (override with `--home /path`); system paths always included. - `sakimori policy preset cloud-secret-egress` — `network.deny` tripwire for AWS / GCP / Azure IMDS and STS-style secret endpoints. Pairs with `sakimori proxy start --network-allow ...` for SNI-level enforcement. Both presets print to stdout (or `-o policy.yml`) with explanatory comment headers so the operator can pick the entries that fit their threat model and merge into an existing policy. The persistence preset ships in `mode: audit` because its full list exceeds the Linux 8-entry kernel cap on `file.deny` under `mode: block`; to enforce, prune to your 8 most critical paths and flip the `mode:` field to `block`. The cloud-secret-egress preset ships in `mode: block` (no cap on `network.deny`). **Known-IOC workspace scan (`workspace scan-iocs`):** walk a workspace and flag files whose path / basename / content matches a known supply-chain compromise fingerprint (e.g. `.claude/ setup.mjs` dropped by the Shai-Hulud npm worm; basename `.npmrc` for token-exfil; content needles for `webhook.site`, `discord.com/api/webhooks/`, `requestbin.com`). Distinct from `workspace diff` — diff catches "something changed during the build," scan-iocs catches "this file exists at all, which it shouldn't." The catalog is bundled in the binary (versioned; `CATALOG_VERSION` in `sakimori-core::iocs`). Exits non-zero on any High-severity hit; `--strict` escalates Medium-severity hits to exit 1 too. Same skip list as `workspace snapshot` (`.git`, `node_modules`, `target`, …); extend with `--skip `. sakimori workspace scan-iocs $GITHUB_WORKSPACE sakimori workspace scan-iocs . --format json sakimori workspace scan-iocs . --strict --skip my-build-artefact `scan-iocs` is also wired into `workspace diff`, `sakimori run --snapshot-workspace`, and `sakimori daemon start --workspace-baseline …` automatically — every added / modified path in the drift report is scanned against the same catalog and the findings land in the JSON log under `workspace_iocs`. A High-severity hit forces exit 1 in any mode (Audit too); `--allow- drift` does not suppress it. The bundled catalog is the only source today. A signed-YAML refresh path (`sakimori iocs update`) is a roadmap item — see CLAUDE.md Roadmap #18 for the planned surface. The HTML report includes: - verdict (ALLOW / DENY), kind, pid, comm - **host column** (PTR-resolved reverse DNS for connect events) - detail (IP:port / filename / exec argv) - filter box matching across all fields - dark-mode aware, self-contained (no external CSS/JS) **Per-event source attribution (Linux):** the supervisor walks `/proc//{status,cmdline}` PPid chains at event time and tags each event with the originating package manager (npm, pnpm, yarn, cargo, pip, uv, poetry, dotnet, go, maven, gradle, bundler, composer). That shows up as a `source: { package_manager, root_argv, chain }` field on every JSON-log event and as a "Sources" top-N table in the step summary, so a connect to `evil.example` reads as "came from `npm install foo@1.2.3`" rather than just "from pid 12345 (sh)". Best-effort — pids that have already exited by the time the userspace drain reads the ringbuf get `source: null` and fall into the `(unattributed)` row. Windows ETW supervisor doesn't attach attribution yet. ## CI usage (GitHub Actions) ### Minimal: run every install through the proxy Works on **Linux, macOS, and Windows** GitHub-hosted runners (Windows requires sakimori v0.34.3 or newer — earlier Windows release tarballs ship only `sakimori-win.exe`, the ETW supervisor, which has no proxy subcommand). The proxy starts in the background as the action's main step, exports `HTTPS_PROXY` + the CA bundle for every common HTTPS client via `$GITHUB_ENV`, and survives across `run:` step boundaries until the post-step kills it at end-of-job. jobs: build: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 # Spawns `sakimori proxy start` detached and appends # HTTPS_PROXY / CARGO_HTTP_CAINFO / NODE_EXTRA_CA_CERTS / # PIP_CERT / REQUESTS_CA_BUNDLE / SSL_CERT_FILE to $GITHUB_ENV # for every step after this one. - uses: bokuweb/sakimori/proxy@v0 with: min-age: 7d - run: npm ci # routed through the proxy - run: cargo test # routed through the proxy - run: pip install -r requirements.txt # routed through the proxy Inputs: | input | default | description | |---|---|---| | `min-age` | `7d` | Minimum package age. Same grammar as `--min-age`. | | `listen` | `127.0.0.1:8910` | Proxy listen address. | | `fail-on-missing` | `false` | Treat unknown publish dates as deny. | | `version` | `v0` | sakimori release tag to download. | | `token` | `${{ github.token }}` | Used by `gh release download`. | Outputs: | output | description | |---|---| | `ca-cert` | Absolute path to the proxy's root CA PEM. Also exported via `$GITHUB_ENV` as `CARGO_HTTP_CAINFO`, `PIP_CERT`, `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, and `SSL_CERT_FILE`. | ### Alternative: lockfile-only pre-flight check Cheaper (no proxy), but fails loudly on any too-young dep instead of silently falling back. - uses: bokuweb/sakimori@v0 - run: $SAKIMORI_BIN deps check --min-age 7d Cargo.lock package-lock.json - run: cargo test # only reached if the check passed ### Cache-poisoning guard: `bokuweb/sakimori/verify-cache@v0` The proxy filters at **fetch** time — it can't see bytes restored from `actions/cache`. If your workflow uses `actions/cache` (or `actions/setup-node` with `cache:`, `Swatinem/rust-cache`, etc.) a poisoned restore happens between cache-restore and install, behind the proxy's back. Drop this step in **right after install** to re-hash every blob in the local store against the lockfile's `integrity:` fields: - uses: bokuweb/sakimori/proxy@v0 with: { min-age: 7d } - uses: actions/cache@v4 with: { path: ~/.local/share/pnpm/store, key: ... } - run: pnpm install # populates / hits the cache # ↓ catches TanStack-style cache poisoning: cache restored a # tarball whose bytes don't match what the lockfile pinned. - uses: bokuweb/sakimori/verify-cache@v0 with: lockfile: pnpm-lock.yaml Supports `package-lock.json`, `pnpm-lock.yaml`, and `Cargo.lock`; auto-picks the cache root for the runner OS. Inputs: | input | default | description | |---|---|---| | `lockfile` | (required) | Path to `package-lock.json`, `pnpm-lock.yaml`, or `Cargo.lock`. | | `cache` | (auto) | Override the store root. Auto-detected from the runner OS — `~/.npm/_cacache` (Linux/macOS) or `%LOCALAPPDATA%\npm-cache\_cacache` (Windows) for npm; `~/.local/share/pnpm/store/v3` / `~/Library/pnpm/store/v3` / `%LOCALAPPDATA%\pnpm\store\v3` for pnpm; `$CARGO_HOME` (default `~/.cargo` or `%USERPROFILE%\.cargo`) for cargo. | | `format` | `text` | `text` or `json`. | | `version` | `v0` | sakimori release tag. | | `token` | `${{ github.token }}` | Used by `gh release download`. | Exit codes match the CLI: `0` clean, `1` on any mismatch / missing entry. **pnpm v11+ SQLite stores are not yet supported** — the action exits with a clear `Unsupported` error rather than passing silently. (v10 still uses the JSON layout and works fine.) ### eBPF-supervised test run — job-scoped form (Linux only) Use `bokuweb/sakimori/job@v0` when you want a single audit log covering **every step in the job** instead of just one wrapped command. The action's pre-hook spawns a background eBPF supervisor attached to the runner-worker's cgroup; cgroup v2 inheritance means every step the runner forks afterwards (`actions/checkout`, your `run:` blocks, `actions/upload-artifact`, ...) is observed by the same supervisor. The post-hook flushes the JSON log / step summary / HTML report and fails the job if `mode: block` denied anything. runs-on: ubuntu-latest steps: - uses: bokuweb/sakimori/job@v0 # MUST come before checkout so the with: # supervisor is up first policy: .github/sakimori.yml mode: block html: sakimori-report.html - uses: actions/checkout@v4 - run: corepack enable - run: pnpm install --frozen-lockfile - run: pnpm build - run: pnpm test # post-hook of bokuweb/sakimori/job runs here automatically Limitations: Linux runners only (Windows needs a different kernel hook), and **container jobs** (`jobs..container:`) are unsupported because the host-side cgroup attach can't reach steps that run inside the container. Matrix shards and reusable-workflow callers are each their own job and need their own `bokuweb/sakimori/job@v0`. **Uploading the audit log from the same job**: the daemon writes its JSON / HTML / step-summary at end-of-job (the post-hook), which is too late for an `actions/upload-artifact` step inside the same job. Drop in `bokuweb/sakimori/job/stop@v0` right before the upload to flush the daemon early: - uses: bokuweb/sakimori/job@v0 with: { policy: .github/sakimori.yml, mode: block } - uses: actions/checkout@v4 - run: pnpm test - uses: bokuweb/sakimori/job/stop@v0 # flush + stop - uses: actions/upload-artifact@v4 with: name: sakimori-report path: | sakimori.log.json sakimori-report.html It's idempotent — the daemon's own post-hook turns into a no-op on the missing pid-file. On non-Linux matrix entries the sub-action no-ops silently, so it's safe to drop into a cross-OS workflow. **Tamper detection**: pass `snapshot-workspace: ` to also catch on-disk tampering. The daemon can't take the baseline itself (it starts before checkout), so add a tiny step right after checkout that records the baseline — the action exports the paths for you: - uses: bokuweb/sakimori/job@v0 with: policy: .github/sakimori.yml mode: block snapshot-workspace: . - uses: actions/checkout@v4 - run: sudo -E "$SAKIMORI_BIN" workspace snapshot "$SAKIMORI_WORKSPACE_DIR" -o "$SAKIMORI_BASELINE_PATH" - run: pnpm install --frozen-lockfile - run: pnpm build The daemon re-snapshots `$SAKIMORI_WORKSPACE_DIR` at post-time, diffs against the baseline, and surfaces drift in the JSON log + step summary. Forgetting the snapshot step is non-fatal (the daemon logs a warning and the drift section is omitted). ### eBPF-supervised test run — one-step form (Linux + Windows) The simplest form: pass the command you want supervised via the `run:` input. The action installs sakimori AND wraps the command with `sakimori run` for you — no separate `sudo -E env "PATH=$PATH" "$SAKIMORI_BIN" run …` step required. strategy: matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: bokuweb/sakimori@v0 with: policy: .github/sakimori.yml mode: audit html: sakimori-report.html run: | corepack enable cargo test pnpm install --frozen-lockfile pnpm test On Linux the script runs under `sudo -E env "PATH=$PATH" "$SAKIMORI_BIN" run … -- bash -euxo pipefail -c ''`; on Windows under `& $env:SAKIMORI_BIN … -- pwsh -NoProfile -Command ""`. `--summary` defaults to `$GITHUB_STEP_SUMMARY` and `--log` defaults to the `log:` input (`sakimori.log.json`). Add `snapshot-workspace: ` to also catch on-disk tampering. ### eBPF-supervised test run — explicit form (Linux + Windows) If you need more control over the wrapper invocation, omit `run:` and write the `sakimori run` step yourself. The action exports `$SAKIMORI_BIN`, `$SAKIMORI_POLICY`, `$SAKIMORI_MODE`, and `$SAKIMORI_LOG` for you. strategy: matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: bokuweb/sakimori@v0 with: policy: .github/sakimori.yml mode: audit - if: runner.os == 'Linux' run: | # `sudo -E` preserves env *except* PATH (sudo always replaces # it with secure_path). `env "PATH=$PATH"` re-injects the # runner user's PATH so the supervised child can find tools # installed outside /usr/bin (pnpm, cargo, rustup toolchains). sudo -E env "PATH=$PATH" "$SAKIMORI_BIN" run \ --policy "$SAKIMORI_POLICY" \ --mode "$SAKIMORI_MODE" \ --log "$SAKIMORI_LOG" \ --html sakimori-report.html \ --summary "$GITHUB_STEP_SUMMARY" \ -- cargo test - if: runner.os == 'Windows' shell: pwsh run: | & $env:SAKIMORI_BIN ` --policy $env:SAKIMORI_POLICY ` --log sakimori.log.json ` --html sakimori-report.html ` -- cargo test - uses: actions/upload-artifact@v4 if: always() with: name: sakimori-report-${{ runner.os }} path: | sakimori-report.html sakimori.log.json ### PR comment with the HTML report `bokuweb/sakimori/comment@v0` reads the JSON log and upserts a single PR comment (keyed by an HTML marker, re-runs edit in place). Embeds a `gh run download` one-liner to view the full HTML on your machine. - uses: bokuweb/sakimori/comment@v0 if: github.event_name == 'pull_request' with: log: sakimori.log.json artifact-name: sakimori-report html-filename: sakimori-report.html # fail-on-denied: "true" # optional ### Runner support matrix | runner | proxy | supervised run | notes | |---|---|---|---| | `ubuntu-latest`, `ubuntu-22.04`, `ubuntu-24.04` | ✅ | ✅ | canonical Linux target, eBPF + tracepoints | | `ubuntu-24.04-arm` | ✅ | ✅ | aarch64 binary ships in each release | | `windows-latest` | ✅ | ✅ | ETW public providers; elevated by default | | `windows-2022`, `windows-2019` | ✅ | ⚠️ | probably works but not smoke-tested | | `macos-latest` | ✅ | ❌ | supervised mode is Linux/Windows only | | container jobs (`container:` on Linux) | ✅ | ⚠️ | needs `--privileged` + host cgroup mount | | self-hosted Linux | ✅ | ⚠️ | needs passwordless sudo, kernel ≥ 5.13 | | self-hosted Windows | ✅ | ⚠️ | needs Administrator for ETW | ## Docker image Prebuilt multi-arch image on GHCR: docker pull ghcr.io/bokuweb/sakimori-proxy:v0 Tags: `v0` (floating), `v0.N`, `v0.N.M`, `latest`. Available archs: `linux/amd64`, `linux/arm64`. Run with a named volume so the CA persists across restarts: docker run --rm -p 8910:8910 \ -v sakimori-conf:/etc/sakimori-xdg \ ghcr.io/bokuweb/sakimori-proxy:v0 \ --listen 0.0.0.0:8910 --min-age 7d # One-shot: grab the generated CA so hosts can trust it. docker run --rm -v sakimori-conf:/etc/sakimori-xdg \ --entrypoint cat ghcr.io/bokuweb/sakimori-proxy:v0 \ /etc/sakimori-xdg/sakimori/ca.pem > /tmp/sakimori-ca.pem Then on each host: export HTTPS_PROXY=http://:8910 export CARGO_HTTP_CAINFO=/tmp/sakimori-ca.pem # (or install-ca into your OS trust store with the CA you just copied) ## Configuration reference ### Duration grammar (`--min-age`, policy `age`) Integer + unit. Bare numbers default to days. | suffix | unit | |---|---| | `d` | days | | `h` | hours | | `m` | minutes | | `s` | seconds | Examples: `7d`, `72h`, `30m`, `3600s`, `7` (= 7 days). ### File locations | OS | CA + key | Cache | Daemon unit | |---|---|---|---| | macOS | `~/.config/sakimori/ca.{pem,key}` (or `$XDG_CONFIG_HOME`) | `~/Library/Caches/sakimori/deps-cache.json` | `~/Library/LaunchAgents/com.sakimori.proxy.plist` | | Linux | `$XDG_CONFIG_HOME/sakimori/ca.{pem,key}` | `$XDG_CACHE_HOME/sakimori/deps-cache.json` | `~/.config/systemd/user/sakimori-proxy.service` | | Windows | `%LOCALAPPDATA%\sakimori\ca.{pem,key}` | `%LOCALAPPDATA%\sakimori\deps-cache.json` | `%LOCALAPPDATA%\sakimori\sakimori-proxy.task.xml` | ### Environment variables read | var | purpose | |---|---| | `SAKIMORI_POLICY` | Default policy file for `run` / `check-policy` | | `SAKIMORI_MODE` | Override policy `mode` in `run` | | `SAKIMORI_LOG` | Default log destination in `run` | | `SAKIMORI_BIN` | Set by the GH Action install step | | `SAKIMORI_BPF_OBJ` | Path to `sakimori.bpf.o` (Linux only) | | `GITHUB_STEP_SUMMARY` | Default `--summary` target | | `XDG_CONFIG_HOME` / `XDG_CACHE_HOME` | Override default config/cache dir | ## Troubleshooting ### `sakimori doctor` says the proxy is unreachable - Check it's actually running: `pgrep -f 'sakimori proxy'` - On macOS: `launchctl list | grep sakimori` - On Linux: `systemctl --user status sakimori-proxy` - On Windows: `schtasks /Query /TN sakimori-proxy` - Try `sakimori proxy start` in the foreground — see the log. ### TLS errors from cargo / npm / pip Cargo on Linux uses libcurl which doesn't read the system trust store — `CARGO_HTTP_CAINFO` must point at the sakimori CA. Likewise `PIP_CERT` for pip and `NODE_EXTRA_CA_CERTS` for npm. `install-gate install` sets all of these. If you skipped that, either install-gate now or set them manually. ### `install-ca` on macOS says "needs privilege" macOS keychain writes need `sudo`. Re-run with sudo, or copy the printed `security add-trusted-cert …` line and run it yourself. ### `npm install` still pulls a too-young version 1. Is the proxy running? `sakimori doctor` 2. Is `HTTPS_PROXY` set in **this** shell? (install-gate only applies to new shells.) `echo $HTTPS_PROXY` 3. Is the package being downloaded from a host sakimori intercepts? By default only the canonical public hosts (`registry.npmjs.org`, `pypi.org`, `files.pythonhosted.org`, `crates.io`, `index.crates.io`, `api.nuget.org`) are watched. Internal mirrors / replacement registries need to be added — see [Custom / internal registries](#custom--internal-registries) for the `--npm-registry` / `--registries-config` flags. ### Container / remote Docker usage Run the proxy on a separate host and point client env at it: export HTTPS_PROXY=http://proxy.corp.internal:8910 export CARGO_HTTP_CAINFO=/etc/sakimori/ca.pem # copy from the proxy container ## Known limitations Honest assessment. Full details in [CLAUDE.md](CLAUDE.md). ### Proxy