argus
September 2, 2026 · View on GitHub
"100-eyed guardian." Static install-time scanner for eight package ecosystems, with opt-in Sigstore verification plumbing for npm provenance.
argus is a Rust CLI that inspects package artifacts from npm,
PyPI, crates.io, Go modules, NuGet, Maven, RubyGems, and Composer/Packagist
before package build or install hooks run. It combines artifact-integrity checks
with ecosystem-specific static rules; neither a matching digest nor a clean
static scan proves that an artifact is safe. See the matrix below and the
"Status" section for the implemented capability snapshot.
v0.3.0 is the current
immutable binary release, and the workspace release contract targets v0.3.0.
GitHub Release metadata is authoritative for publication status, dates, and
assets. By product decision, the protected
v1 branch intentionally remains on v0.2.1 until Argus reaches an explicitly
approved v1.0 stability boundary. Action consumers should pin
majiayu000/argus@v0.3.0; high-assurance environments may instead pin the full
release commit. The operator sequence and verification boundary are documented
in docs/releasing.md.
Ten-minute CI quickstart
The primary product path is a pull-request admission gate over a supported lockfile. The Action downloads and verifies an immutable Argus release before scanning the resolved dependency tree; it never runs package lifecycle, build, or install hooks.
name: Argus dependency admission
on:
pull_request:
permissions:
contents: read
jobs:
argus:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: majiayu000/argus@v0.3.0
with:
scanType: lockfile
path: package-lock.json
githubToken: ${{ github.token }}
format: json
failOn: approval
Replace package-lock.json with another supported lockfile. failOn: approval
fails the job for both block and allow-with-approval; operational errors
always fail. Use the optional base input when the workflow has materialized
the base revision's lockfile and only changed dependencies should be fetched.
The three report decisions mean:
allow: assessment completed and no active rule requires review; this is not proof that the artifact is safe.allow-with-approval: stop and review the reported evidence before granting an exact, digest-bound approval.block: reject the change. Fetch, scan, integrity, intelligence, or required comparison failures never become a clean report.
For local investigation, download the asset for the exact platform, verify its GitHub attestation, and extract the archive:
gh release download v0.3.0 --repo majiayu000/argus \
--pattern 'argus-v0.3.0-aarch64-apple-darwin.tar.gz'
gh release verify-asset v0.3.0 \
argus-v0.3.0-aarch64-apple-darwin.tar.gz --repo majiayu000/argus
tar -xzf argus-v0.3.0-aarch64-apple-darwin.tar.gz
./argus lockfile-scan package-lock.json --format json
Ecosystem capability matrix
All rows describe current code on main. Each immutable release tag and its
release manifest commit define that release's binary contents. The
[0.3.0] section describes the current
published release. GitHub Release metadata is authoritative for whether a
version has been published.
| Ecosystem | CLI command | Integrity source | Artifact and inspected surfaces | Explicit limitations |
|---|---|---|---|---|
| npm | fetch | Registry dist.integrity SRI digest | Tarball; lifecycle scripts, package metadata, text/binary content rules, structural obfuscation signatures, opt-in bounded metadata-anomaly checks, and opt-in full Sigstore verification | Obfuscation detection covers structural signatures (_0x identifier mangling, nested decoder chains, direct decode-to-eval) only — entropy and minification are reported as evidence, not scored, so a novel obfuscator can still pass; npm search supplies candidates rather than complete publisher history; Sigstore verification remains opt-in |
| PyPI | pypi-fetch | PyPI JSON digests.sha256 | sdist/wheel; setup.py, import-time Python surfaces, and package content | Does not execute Python or prove runtime behavior |
| crates.io | crates-fetch | crates.io API SHA-256 checksum | .crate; build.rs, Rust source, and proc-macro structure | Does not compile code or execute procedural macros |
| Go modules | go-fetch | GOPROXY .ziphash h1: directory hash when available | Module ZIP; init, package initializers, process and network calls | Missing/unusable .ziphash is reported as go-integrity-unverified Info and can still allow; source detection is regex-based and sum.golang.org transparency is not verified |
| NuGet | nuget-fetch | Catalog SHA-512 packageHash when available | .nupkg; PowerShell install hooks and MSBuild .targets/.props | Does not verify .signature.p7s or inspect DLL bytecode; unavailable catalog hashes are reported explicitly |
| Maven | maven-fetch | .jar.sha256, falling back to weaker .jar.sha1 | JAR; pom.xml, manifests, resources, and embedded build scripts | Does not inspect .class bytecode; SHA-1 fallback detects corruption but is not collision-resistant |
| RubyGems | gems-fetch | Registry SHA-256 sha | .gem; gemspec, extconf.rb, and Ruby source | Static rules do not execute Ruby; internal archive checksums are not an independent trust anchor |
| Composer / Packagist | composer-fetch | Packagist dist.shasum SHA-1 | Dist ZIP; lifecycle hooks, autoload.files, and PHP source | SHA-1 is weak, missing hashes are high-risk, VCS-only packages are unsupported, and dynamic PHP can evade regex rules |
Decisions
- block — at least one high-risk rule fired.
- allow-with-approval — only approval-scoped evidence such as a known native-build pattern, bounded npm metadata anomaly, or weak-only lockfile integrity fired; require explicit approval.
- allow — no rule fired.
Developer and advanced CLI reference
The examples below run the source workspace. End users should start with the verified Action or immutable release quickstart above.
# Scan one local package directory
cargo run -p argus-cli -- scan corpus/fixtures/lifecycle-curl-sh
# Fetch a real npm package: packument -> tarball -> SHA-512 verify -> safe
# extract -> scan. No lifecycle script ever runs.
cargo run -p argus-cli -- fetch chalk@5.3.0
cargo run -p argus-cli -- fetch '@types/node@20.10.0' --format json
# Opt in to bounded npm metadata-anomaly checks. The separate cache stores
# npm search responses for at most 15 minutes.
cargo run -p argus-cli -- fetch chalk@5.3.0 \
--metadata-anomaly \
--metadata-cache-dir ~/.cache/argus/npm-metadata
# Fetch a real PyPI package: JSON API -> sdist/wheel -> SHA-256 verify -> safe
# extract -> scan. setup.py never runs.
cargo run -p argus-cli -- pypi-fetch requests@2.31.0 --prefer wheel
cargo run -p argus-cli -- pypi-fetch django@5.0.0 --prefer both --format json
# Fetch a real crates.io crate: JSON API -> .crate -> SHA-256 verify -> safe
# extract -> scan. build.rs never runs.
cargo run -p argus-cli -- crates-fetch serde@1.0.228
cargo run -p argus-cli -- crates-fetch tokio --format json
# Fetch a Go module: GOPROXY zip -> h1 verify when .ziphash exists -> static scan
cargo run -p argus-cli -- go-fetch golang.org/x/text@v0.16.0
# Fetch a NuGet package: .nupkg -> catalog hash (when available) -> hook scan
cargo run -p argus-cli -- nuget-fetch Newtonsoft.Json@13.0.3
# Fetch a Maven artifact: JAR checksum -> POM/resource/build-script scan
cargo run -p argus-cli -- maven-fetch org.apache.commons:commons-lang3:3.14.0
# Fetch a RubyGem: registry SHA-256 -> nested .gem extraction -> Ruby scan
cargo run -p argus-cli -- gems-fetch rake@13.2.1
# Fetch a Composer package: Packagist dist ZIP -> lifecycle/PHP scan
cargo run -p argus-cli -- composer-fetch monolog/monolog@3.7.0
# Custom registry that serves tarballs from a separate CDN/host:
cargo run -p argus-cli -- fetch internal-tool@1.2.3 \
--registry https://npm.corp.example \
--allow-tarball-host cdn.corp.example \
--allow-tarball-host objects.corp.example
# Run the full regression corpus (6 agent + 11 package + 1 lockfile cases)
cargo run -p argus-cli -- corpus test
# Machine-readable output
cargo run -p argus-cli -- scan path/to/pkg --format json
# SARIF 2.1.0 for code-scanning integrations
cargo run -p argus-cli -- scan path/to/pkg --format sarif > argus.sarif
# Lockfiles use basename + a closed structure/version signature.
cargo run -p argus-cli -- scan path/to/project/pnpm-lock.yaml --format json
# An explicit parser is validated together with the basename and signature.
# Extra source hosts are exact DNS names, not patterns.
cargo run -p argus-cli -- scan package-lock.json \
--lockfile-format package-lock \
--allow-registry-host packages.corp.example
# Query OSV for one exact package version. The cache directory is always
# explicit; online mode sends this coordinate to api.osv.dev.
cargo run -p argus-cli -- vulns package \
--ecosystem npm --name lodash --version 4.17.20 \
--cache-dir ~/.cache/argus/osv
# Query every complete external coordinate in one supported lockfile without
# network access. Offline mode requires a complete fresh cache snapshot.
cargo run -p argus-cli -- vulns lockfile Cargo.lock \
--cache-dir ~/.cache/argus/osv --offline --format json
# Scan agent and repository automation surfaces: MCP configs, skills, hooks,
# AGENTS.md / CLAUDE.md, .github/workflows/*.{yml,yaml}, and recursively
# discovered local action.yml/action.yaml metadata.
# Detects injection/override language (AGT-01), dangerous script
# capabilities like curl|sh or secret-read + network-egress (AGT-03),
# high-risk config flags such as alwaysLoad: true (AGT-05), and GitHub
# Actions supply-chain hazards (AGT-06). Scan the repository root, the
# .github/workflows directory, one workflow file, or one Action metadata file.
cargo run -p argus-cli -- agent scan ~/.claude
cargo run -p argus-cli -- agent scan . --format sarif
cargo run -p argus-cli -- agent scan path/to/skill .mcp.json --format json
# Recompute the explicitly synthetic GH-58 fixture metrics.
cargo run -p argus-cli -- corpus eval --corpus corpus/agent --format json
The compiled binary is named argus and exits non-zero on block.
Scanning commands accept --jobs N with N in 1..=64. Omitted jobs resolve
once per invocation to the machine's available parallelism, capped at 16.
Argus creates one private worker pool, keeps discovery, archive extraction,
metadata selection, global resource accounting, and report emission ordered,
and parallelizes only independent bounded file matching. --jobs 1 is the
deterministic compatibility/debug path. Invalid values are rejected by CLI
preflight before filesystem scanning or network access. The flag is available
on scan, all fetch routes, agent scan, vulns package|lockfile, and
corpus test|eval; unrelated commands do not accept it.
Registry, artifact, OSV advisory-detail, and intelligence archive GETs use a fixed fail-closed retry policy: at most three total attempts, 10 seconds per attempt, and 30 seconds including backoff. Only typed transient connection failures and HTTP 408, 425, 429, 500, 502, 503, or 504 are retried. Other statuses, TLS or redirect-policy failures, oversized/malformed bodies, parsing, and integrity failures stop immediately. OSV querybatch POST requests and other non-idempotent operations are never retried. This policy is intentionally not user-configurable.
External rules and overrides
scan, all eight ecosystem fetch commands, and agent scan accept an explicit
trusted --rules-dir. Every lowercase .yaml/.yml file below that directory
is loaded atomically before artifact or network work begins. A malformed,
unreadable, duplicate, colliding, oversized, or escaping rule file rejects the
whole invocation; Argus never activates a partial directory.
--rules-dir is supported on Unix in v1. Non-Unix builds reject it during
preflight, before scanning or network access, until handle-relative Windows
directory traversal is implemented. Typed --rule-override values remain
available on every supported platform.
schema_version: 1
rules:
- id: corp-forbidden-installer
description: corporate installer marker
policy_class: blocking
default_severity: high
help_uri: https://security.example.com/rules/corp-forbidden-installer
languages: [bash, javascript, text]
matcher: { kind: literal, pattern: CORP_FORBIDDEN_INSTALLER }
External rules require a fixed severity and a literal or regex matcher.
The closed language set is bash, python, javascript, typescript,
rust, go, ruby, php, powershell, csharp, xml, json, yaml,
toml, markdown, and text. Matching is bounded, uses valid UTF-8 only, and
emits at most one finding per rule and logical file. npm lifecycle bodies are
also matched as Bash surfaces.
argus scan path/to/pkg --rules-dir ./rules
argus agent scan path/to/skill --rules-dir ./rules --format sarif
argus scan Cargo.lock \
--rules-dir ./rules \
--rule-override corp-forbidden-installer=severity:medium
argus fetch react@latest \
--metadata-anomaly \
--rule-override version-shape-anomaly=param:minimum_history_days=60
--rule-override ID=off|severity:LEVEL|param:KEY=VALUE is repeatable.
off and severity work for registered built-in or external IDs. Parameter
overrides are accepted only for the closed built-in typosquat and npm anomaly
schemas; unknown keys, wrong types, duplicates, out-of-range values, security
caps, and cross-field-invalid combinations are rejected during preflight.
Text, JSON, and SARIF retain the effective ruleset digest, loaded files,
disabled IDs, parameter overrides, and versioned data digests even when no
findings remain. With neither rules flag, reports keep the existing output
shape and behavior. Standalone vulns commands accept action overrides for
registered vulnerability findings but do not accept detector parameters or a
rules directory because they do not scan artifact text.
Offline typosquat data
Typosquat matching uses ten embedded, versioned assets: one popular-name
dataset for each supported ecosystem, a US QWERTY adjacency graph, and a
pinned Unicode confusables table. Scanning never downloads reputation data.
The initial 382-name migration preserves the exact frozen Rust lists from
commit 6aef7b1; its legacy-priority records are compatibility provenance,
not claims about current registry download rank. Dataset age, normalization,
namespace semantics, source references, and raw SHA-256 digests live in the
checked-in manifest under crates/argus-rules/data/typosquat/v1/.
The migration source of truth is separate from runtime output:
sources/migration-v1.json records the frozen base commit, source files,
constant names, source-blob hashes, source-order hashes, and original
priorities. sources/qwerty-us-v1.json and the pinned Unicode 17 UTS39 source
are the other generator inputs. The generator never reads v1/ to discover
names. From the repository root, maintainers can verify a clean, byte-identical
rebuild—including shuffled migration input—without deleting worktree files:
node crates/argus-rules/data/typosquat/verify-v1-generation.mjs
Direct generation requires all inputs and an explicit output directory:
node crates/argus-rules/data/typosquat/generate-v1-migration.mjs \
--migration-source crates/argus-rules/data/typosquat/sources/migration-v1.json \
--qwerty-source crates/argus-rules/data/typosquat/sources/qwerty-us-v1.json \
--confusables-source crates/argus-rules/data/typosquat/sources/unicode-17.0.0-confusables.txt.gz \
--output-dir /tmp/argus-typosquat-v1
Updates are an offline maintainer operation and must provide reviewable source
evidence. Generated files are sorted deterministically; a data change should
show counts, added/removed canonical identities, source evidence, and manifest
digest changes in the pull request. Runtime scans only validate and consume
the committed v1/ assets; the frozen source snapshot is not compiled into a
second runtime table.
Lockfile source and integrity policy
argus scan statically normalizes nine lockfile families without starting a
package manager, VCS command, shell, or network request:
| Lockfile | Accepted closed versions/signature | Integrity interpretation |
|---|---|---|
package-lock.json | npm lockfile 2, 3 | registry/URL entries require valid SRI; root/link/workspace records are unavailable by format |
yarn.lock | Classic 1; Berry metadata 4, 6, 8 | Classic uses SRI or the resolved SHA-1 fragment; Berry npm/archive records require checksum |
pnpm-lock.yaml | canonical 5.4, 6.0, 9.0 | registry/tarball records require SRI; link/workspace/file records are unavailable |
poetry.lock | 1.1, 2.0, 2.1 | every listed registry artifact is retained and requires a valid hash |
uv.lock | 1 | every listed registry/URL distribution is retained and requires a valid hash |
Cargo.lock | 3, 4 | registry packages require SHA-256; path and git records do not treat a VCS revision as an artifact hash |
go.sum | strict three-field grammar | every line requires a valid Go h1: digest; source host is unavailable by format |
Gemfile.lock | Bundler major 2, 3, 4; CHECKSUMS only where supported | exact lock-name checksum association; an absent checksum section is unavailable, not verified |
composer.lock | schema-v1 structure | non-empty dist SHA-1 is weak evidence; missing dist shasum is optional-absent |
Every normalized source is evaluated independently. Plain HTTP is always
Critical/block. HTTPS, SSH, and scp-like git hosts must exact-match the
format's documented public hosts or a repeated --allow-registry-host; user
entries accept one IDNA-normalized DNS host and reject schemes, ports, paths,
userinfo, wildcards, suffix patterns, and IP literals. Git refs are immutable
only when they are a 40- or 64-character lowercase commit digest.
Strong SHA-256/384/512, SRI, and Go h1: evidence can allow; SHA-1/MD5-only
evidence requires approval. Required missing evidence blocks at High, and
unknown algorithms, malformed encodings/lengths, or conflicting values block
at Critical. Legitimate unavailable-by-format records produce one
format-scoped Info finding with a count and at most 20 stable locators; they do
not change the decision.
Detection is fail-closed: unknown/ambiguous basenames or signatures, new
versions, unsupported entries, coverage mismatch, parse failure, or any bound
failure exits operationally with code 2, stderr, and empty stdout—no text,
JSON, or SARIF report is emitted. Bounds are 64 MiB input, 100,000 records,
64 nesting levels, 1 MiB per scalar, 1,000,000 total scalars, and 64 MiB of
RFC 8785 canonical finding/evidence JSON. Equality is accepted; plus one is
rejected. This scan evaluates source and integrity metadata only: it does not
claim vulnerability status, malicious-package status, or artifact safety.
Whole-lockfile dependency scanning
argus scan <lockfile> evaluates the lockfile's own source and integrity
metadata. argus lockfile-scan <lockfile> answers the other CI question — is
anything inside the resolved dependency tree unsafe — by fetching and
statically scanning every dependency the lockfile resolves.
argus lockfile-scan package-lock.json --cache-dir .argus-cache
argus lockfile-scan package-lock.json --base origin-main-lock.json # delta only
argus lockfile-scan package-lock.json --malicious-db malicious-packages.json
argus lockfile-scan package-lock.json --approval-ledger approvals.json
argus lockfile-scan package-lock.json --export-observation observation.json
argus lockfile-scan package-lock.json --format sarif > argus.sarif
Each registry-fetchable coordinate is dispatched to its ecosystem's fetch
pipeline, so every package gets the same integrity verification and static
rules as the single-package commands. --jobs bounds concurrency, --cache-dir
is reused across every fetch, and --base restricts the active sweep to
dependencies this change added or altered. For an altered coordinate, Argus
also scans the base artifact and reports introduced and resolved findings. A
base artifact that cannot be assessed blocks the aggregate result; it is never
presented as an empty or clean comparison. Base findings remain comparison
evidence and never replace or downgrade the current report.
For npm, PyPI, and crates.io lockfiles, Argus also verifies the downloaded artifact against the digest retained by the lockfile, independently of the registry response. A mismatch is an explicit unassessed failure and blocks the aggregate result.
--malicious-db applies the same pinned, verified OpenSSF snapshot used by the
single-package fetch commands to every current package report. The snapshot is
loaded once per lockfile scan. Missing or corrupt intelligence remains an
operational error, including when the lockfile has no registry-fetchable
targets.
--approval-ledger accepts only explicit approvals bound to the exact purl,
lockfile digest, capability, reason, and future expiry. A complete binding can
turn an allow-with-approval aggregate into allow; it can never downgrade a
blocking finding or an incomplete assessment. --export-observation writes a
digest-bound manifest for approval-bearing or blocking artifacts together with
suggested deny-network, no-secret, read-only filesystem, and isolated-process
controls. Argus does not execute that observation or embed a sandbox.
The repository-root Action uses this same admission path for
scanType: lockfile. Its optional base, baseLockfileFormat, maliciousDb,
and approvalLedger inputs are workspace-relative regular files; SARIF and the
native decision/exit-code contract remain available through the existing
outputs. Every invocation must pass githubToken: ${{ github.token }} for the
authenticated release download and attestation verification path; Argus does
not fall back to anonymous GitHub API requests. Pin
uses: majiayu000/argus@v0.3.0, or use
majiayu000/argus@0c991479d909c1246329c9410f39f7291dd7cece when an immutable
Action source commit is required. The protected v1 branch is deliberately not
the current release channel yet.
The aggregate decision is the worst package decision, and coverage is stated before findings. Two categories are reported rather than dropped:
- not scanned — local/path dependencies, unsupported source shapes, conflicting resolutions, or incomplete coordinates.
- unassessed — dependencies whose fetch or scan failed. These escalate the
aggregate decision to
block. A dependency argus could not read is missing evidence, not evidence of safety, so an unreachable registry never yields a clean tree.
JSON and text include base/current version changes. SARIF emits one run per current package so historical base findings are not reported as active alerts.
Weighted risk scoring (opt-in)
The default decision is policy-driven: a rule either participates in the
decision or it does not, so a Low finding and a Critical one are
interchangeable. --risk-scoring adds a weighted score in which severity
actually participates.
argus scan path/to/pkg --risk-scoring # report the score
argus scan path/to/pkg --risk-scoring --risk-decides # let it decide
argus scan path/to/pkg --risk-scoring --risk-decides \
--risk-approval-threshold 2000 --risk-block-threshold 5000
Weights are derived from the severity each detector already assigns (Critical 10000, High 6000, Medium 3000, Low 1000, Info 0 basis points). At the default thresholds a single finding produces exactly the decision the severity profile already produced, so enabling scoring is not a surprise. The difference appears with several findings: two independent Medium-risk behaviours accumulate past the block threshold, where set algebra saw one "medium bucket" and stopped. Repeated observations of the same rule count once, so a noisy detector cannot block on volume.
Reports carry the score and every rule's contribution — text, JSON risk, and
SARIF properties.argusRisk — so a scored decision can be checked rather than
taken on faith.
Scoring is off by default and --risk-scoring alone does not change any exit
code; --risk-decides is required for that. The completed 1,438-sample
benchmark does not justify a per-rule override: only eight agent rule ids were
observed, and AGT-01-injection-language grouped all 15 of its block labels
with 229 non-block labels under the same id. Six other ids had only one or two
observations. A rule-id weight cannot separate those outcomes, and extending
that sparse agent-only result to the cross-ecosystem catalog would overfit.
The CI quality report therefore publishes per-rule support and 95% Wilson
intervals while the runtime retains the severity-derived profile. Confidence
remains full for an emitted detector observation; benchmark block frequency
is a policy outcome, not a probability that the observation itself occurred.
Explicit OSV vulnerability queries
The secure OSV cache and verified malicious-package snapshot implementations currently require Unix filesystem primitives. On Windows, those explicit intelligence commands fail closed as unsupported; package, lockfile, and agent static scans remain available.
argus vulns is an opt-in known-vulnerability query. It accepts either one
exact package coordinate or the normalized external coordinates from any of
the nine lockfile families above:
argus vulns package \
--ecosystem <npm|pypi|crates.io|go|nuget|maven|rubygems|packagist> \
--name <name> --version <exact> --cache-dir <dir>
argus vulns lockfile <path> \
[--lockfile-format <package-lock|yarn|pnpm|poetry|uv|cargo|go-sum|bundler|composer>] \
--cache-dir <dir>
Both modes support --format text|json|sarif (default text),
--max-age-seconds from 0 through 2592000 (default 86400), and optional
--fail-on-severity low|medium|high|critical. Active advisories normally
produce allow-with-approval and exit 2; a finding meeting the configured
threshold produces block and exit 1; a complete no-match produces allow
and exit 0.
--cache-dir is required in online and offline modes. Online queries use only
the fixed https://api.osv.dev service and disclose the exact package
coordinates being checked. --offline prohibits all OSV network access and
requires every coordinate to have a complete fresh cache entry.
--offline --allow-stale explicitly authorizes only a complete stale snapshot
and emits visible vulnerability-data-stale approval evidence. Missing,
corrupt, partial, future-dated, or unauthorized stale cache data is an
operational error: exit 2, stderr, and empty stdout/no SARIF. Reports expose
only the stable <argus-osv-cache> label, never the cache path.
These results are deliberately separate:
vulnsreports known vulnerabilities for exact versions from OSV.intelmatches an explicitly imported offline known-malicious package snapshot.- provenance and lockfile integrity verify origin/digest evidence.
- static package heuristics report suspicious install/runtime behavior.
One result family does not rewrite another. A no-match is not proof that a
package is benign, correctly sourced, or safe. argus vulns never installs,
upgrades, edits a manifest/lockfile, starts a package manager, or executes
package code.
Opt-in npm metadata anomalies
fetch --metadata-anomaly enables policy npm-anomaly-v1; without this flag
Argus makes no npm search request and emits no inferred metadata status. The
policy produces two approval-only Medium findings:
version-shape-anomaly: the target has at least six earlier stable SemVer releases spanning at least 30 days, lands within 72 hours of its direct predecessor, jumps by at least two major versions or ten minor versions within the same major, and that jump class did not occur in the preceding five transitions.rapid-publish-window: the target version's exact_npmUser.nameappears on at least five distinct package names in the bounded npm search candidates published during the preceding 24 hours.
Insufficient valid history becomes the Info findings
npm-version-shape-unassessed or npm-rapid-publish-unassessed; these do not
change an otherwise-allow decision. Missing required target metadata,
malformed/truncated responses, more than 250 search objects, bodies over 2 MiB,
cache corruption, redirect-policy failures, and transport failures are
operational errors: Argus exits 2 before emitting any report.
npm search is used only for candidate discovery and exposes current package
versions, not a complete publisher activity ledger. Argus therefore exact
matches publisher.username, never treats fewer than five observed packages
as clean, and makes at most one search request per publisher per scan. The
optional --metadata-cache-dir is keyed by the normalized full registry base
URL (including base path), publisher, target publication time, and policy. A
cache entry is reusable for 15 minutes only when it was fetched no earlier than
the target publication time.
Offline known-malicious package intelligence
Argus can explicitly import a fixed revision of the OpenSSF
malicious-packages OSV data set and use the verified local snapshot while
scanning any of the eight supported registries:
REVISION="$(git -C /path/to/malicious-packages rev-parse HEAD)"
cargo run -p argus-cli -- intel import \
--source https://github.com/ossf/malicious-packages \
--revision "$REVISION" \
--output ~/.cache/argus/malicious-packages.json
cargo run -p argus-cli -- intel status \
--db ~/.cache/argus/malicious-packages.json
cargo run -p argus-cli -- fetch suspicious-package@1.2.3 \
--malicious-db ~/.cache/argus/malicious-packages.json \
--format json
Only intel import uses the network. It accepts the canonical GitHub source,
a full pinned commit SHA, and the bounded GitHub-to-codeload archive redirect.
Normal scans load and verify the local snapshot without making an intelligence
request. Missing, corrupt, incompatible, or future-dated data is an operational
error when --malicious-db is enabled; Argus does not silently continue as if
there were no match.
A match emits known-malicious-package at Critical severity and blocks the
package. A non-match means only that the exact coordinate was absent from the
pinned snapshot—it is not evidence that the package is safe. Text, JSON, and
SARIF output retain the snapshot source, revision, import time, age, and
archive/records/snapshot digests even when there is no match.
This data set is malicious-package intelligence. It is deliberately separate from general CVE/advisory lookup, which remains tracked by GH-94.
SARIF and GitHub Code Scanning
--format sarif is available on package/lockfile scans, every ecosystem fetch
command, and agent scan. The output preserves Argus rule IDs, severity,
file/line evidence when present, package coordinates, and stable partial
fingerprints. A finding without a line uses an artifact-level location; Argus
does not invent line 1.
Generic SARIF consumers can read the generated file directly. A GitHub Actions
job can upload it with the official action (currently v4):
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v7
- run: argus scan path/to/pkg --format sarif > argus.sarif
- uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: argus.sarif
Argus writes normal SARIF only after a complete scan report exists. Invalid
input, parser failures, network failures, and agent-snapshot failures before
inventory comparison write an error to stderr, exit 2, and leave stdout
empty instead of emitting a clean run. An agent-snapshot failure after
comparison retains its completed results in a partial SARIF run whose
invocation has executionSuccessful=false and a sanitized error notification.
A successful SARIF report retains the normal decision exit codes:
allow = 0, block = 1, and allow-with-approval = 2.
Rule coverage (Milestone 0)
| Family | Rules |
|---|---|
| lifecycle | lifecycle-script, pre-scan-execution-marker |
| content | remote-download, shell-pipe-execution, credential-access, network-exfiltration, credential-exfiltration-chain, download-execution-chain, binary-execution, runtime-hook, wallet-interception, token-harvest, github-write-api, npm-publish |
| binary | binary-file |
| name | typosquatting, low-reputation, dependency-confusion, public-registry-internal-name, known-native-build-pattern |
| lockfile | lockfile-http-resolved, untrusted-registry-host, lockfile-mutable-vcs-ref, lockfile-integrity-missing, lockfile-integrity-invalid, lockfile-integrity-weak, lockfile-integrity-unavailable |
| provenance | missing-provenance (info), provenance-verified-subject (info), provenance-subject-mismatch (block), provenance-fetch-blocked / provenance-fetch-failed / provenance-parse-failed (operational errors) |
| npm metadata | version-shape-anomaly, rapid-publish-window (approval); npm-version-shape-unassessed, npm-rapid-publish-unassessed (info) |
| ai-context | ai-context-poisoning — writes to .cursorrules, CLAUDE.md, .claude/*, AGENTS.md, .aider.conf.yml, .continuerules, .codexrules, .windsurfrules. Pioneered at scale by the TrapDoor campaign (Socket.dev 2026-05-24). |
runtime-hook recognizes direct, computed-property, and
Object.defineProperty rewrites of globalThis, window, or global from
parsed JavaScript/TypeScript facts. Comments and string examples do not count
as executable rewrites.
Agent-surface rule coverage (GH-57)
argus agent scan statically scans agent and repository automation surfaces —
MCP configs, skill definitions, hook scripts, instruction files, immediate
.github/workflows/*.{yml,yaml} files, and recursively discovered local
action.yml / action.yaml metadata — without executing anything. For local
Action metadata, only runs.using: composite steps receive AGT-06 dependency
and inline-script checks. Invalid or duplicate-key protected YAML is an
operational error. AGT-06 is included in the immutable v0.3.0 release.
| Rule | Severity | Detects |
|---|---|---|
AGT-01-injection-language | critical → block | authority-claim / instruction-override / concealment language (English + Chinese) in AGENTS.md, CLAUDE.md, SKILL.md, .claude/**/*.md, and MCP tool description fields |
capability-manifest | medium → approval | declarative capability entries in JSON (capability, evidence, optional resolved_host) for network egress, unresolved hosts, sensitive reads, agent config writes, exec/eval, obfuscation, and persistence |
AGT-03-remote-exec | high → block | remote download piped to a shell (curl … | sh, iwr … | iex) in hook/skill scripts |
AGT-03-secret-exfil | high → block | high-sensitivity credential access combined with network egress in the same script |
capability-misfit | high → block | declared skill intent does not justify high-risk capability combinations such as credential exfiltration or agent config/hook writes |
agent-config-write | medium → approval or high → block | script writes .claude/settings*.json or hook paths; matching agent-config intent is declarative, mismatched intent blocks |
hook-persistence | high → block | script persists or auto-approves an agent hook |
credential-access / network-exfiltration | high → block | manifest-backed evidence for credential reads and off-box network exfiltration |
agent-native-executable | medium → approval | a skill or hook ships an ELF, Mach-O, or PE/DOS executable whose binary semantics cannot be inspected by the text rules |
AGT-05-mcp-always-load | medium → approval | mcpServers.<name>.alwaysLoad: true (permanent full trust) |
AGT-05-enable-all-project-mcp | medium → approval | enableAllProjectMcpServers: true |
AGT-05-enabled-mcpjson-servers | medium → approval | non-empty enabledMcpjsonServers allowlist |
AGT-05-posttooluse-output-rewrite | medium → approval | PostToolUse hook rewriting updatedToolOutput for non-MCP tools |
AGT-05-config-unparseable | info | agent config file is not valid JSON |
AGT-06-workflow-mutable-action | medium → approval | a workflow or local composite Action uses a remote Action, reusable workflow, or Docker action that is not pinned to a full commit SHA or image digest |
AGT-06-workflow-context-injection | critical → block | a workflow or local composite Action interpolates attacker-controlled GitHub event data directly into an inline run script instead of crossing an environment-variable boundary |
AGT-06-workflow-untrusted-checkout | critical → block | pull_request_target or workflow_run checks out an attacker-controlled pull-request/workflow-run ref |
AGT-06-workflow-write-all | high → block | workflow-level or job-level permissions: write-all grants every available GITHUB_TOKEN permission write access |
AGT-06-workflow-privileged-write | medium → approval | pull_request_target or workflow_run explicitly grants a scoped GITHUB_TOKEN permission write access |
AGT-02 | medium → approval | an already-approved MCP/skill description drifted from its recorded baseline hash (rug-pull detection; see below) |
AGT-02-baseline-entry-missing | info | a baselined description is no longer present on the scanned surface |
AGT-02-baseline-unreadable | info | --baseline file could not be read/parsed (scan continues; not treated as "no drift") |
AGT-04-entry-added | medium → approval | a non-symlink high-context file or directory was added after approval |
AGT-04-entry-removed | medium → approval | a non-symlink high-context file or directory was removed after approval |
AGT-04-entry-type-changed | medium → approval | a high-context path changed between file and directory |
AGT-04-content-modified | medium → approval | the complete bytes of a high-context file changed |
AGT-04-symlink-changed | medium → approval | a symlink was added, removed, retargeted, or changed to/from another entry type |
AGT-04 install-time high-context snapshot
AGT-04 provides an explicit before/after approval workflow for agent installations. Keep the snapshot outside the tree an installer can modify, preferably in separately protected version control:
# 1. Before installation: approve the complete high-context inventory.
argus agent scan path/to/agent \
--update-snapshot /protected/argus/agent.snapshot.json
# 2. Run the installer using your normal, separately reviewed process.
your-installer path/to/agent
# 3. After installation: compare without approving the changes.
argus agent scan path/to/agent \
--check-snapshot /protected/argus/agent.snapshot.json
# 4. Review every finding; approve a new state only with an explicit update.
argus agent scan path/to/agent \
--update-snapshot /protected/argus/agent.snapshot.json
All five AGT-04 changes are Medium and therefore
allow-with-approval unless another rule blocks. Check mode never records
approval. Update mode records approval only after discovery, semantic rules,
optional judge, and atomic persistence all succeed; it does not erase existing
AGT-01/02/03/05/06/judge findings or force their exit code to zero. The normal
report and snapshot written: N entries message are emitted only after the
snapshot has been persisted.
Snapshot mode performs a complete, non-following walk without pruning
.git or node_modules. Its canonical membership includes the existing
instruction, MCP, hook, and skill-script surfaces; all .claude/** entries;
and .cursorrules, .aider.conf.yml, .continuerules, .codexrules, and
.windsurfrules. It inventories classified files, directories, and symlinks.
New inventory-only shapes are hashed but skipped before text, UTF-8, size,
binary, and semantic symlink validation; existing semantic surfaces retain
their fail-closed validation.
Root-aware classification exists only in snapshot check/update mode. For
example, scanning ~/.claude, ~/.claude/rules, or
~/.claude/settings.json preserves the .claude/ classification context
while report and snapshot keys remain scan-root-relative. With no snapshot
flag—including AGT-02-only check/update—the legacy root-relative classifier
and .git/node_modules pruning remain unchanged; settings.json at a
~/.claude scan root is not silently promoted into a new semantic surface.
The snapshot target cannot hide a protected entry. If a target inside the scan root classifies as any supported surface, check and update reject it before exclusion, loading, rendering, or writing, even when the future update target does not exist. An unclassified target can be inside the root, but an external protected location is recommended.
The persisted schema is strict version 1 with sorted relative UTF-8 paths. Files store SHA-256 of all bytes; directories store no digest; symlinks store only SHA-256 of the raw link-target representation (Unix bytes, or Windows UTF-16 code units encoded little-endian), never target plaintext. Unknown fields, duplicate decoded JSON keys, invalid paths, invalid digests, traversal errors, and entries that change while being captured fail closed. Because symlink representation is platform-specific, approve and check such snapshots on the same platform.
Updates use a same-directory temporary file, write, flush, file sync, then atomic replace. A failed stage preserves the previous destination and cleans the temporary file. Argus does not coordinate concurrent snapshot writers; serialize approval operations and treat the snapshot as a trust artifact.
The persistence combinations are deliberately narrow:
| Flags | Allowed |
|---|---|
| no baseline/snapshot flag | yes; legacy multi-path behavior |
--check-snapshot | yes; exactly one scan path |
--update-snapshot | yes; exactly one scan path |
--baseline + --check-snapshot | yes; two read-only checks on one path |
| any update flag plus another baseline/snapshot flag | no |
Failures before inventory comparison produce the ordinary operational
contract: stderr, exit 2, and empty stdout. Once inventory comparison is
complete, any later collection/projection, semantic rule, AGT-02, judge, or
snapshot-persistence failure retains completed findings in a blocked partial
report: text starts with execution: incomplete, JSON uses the
agent_scan_incomplete envelope, SARIF sets
executionSuccessful=false, stderr is sanitized, and the exit code is 2.
Optional external semantic judge
The deterministic scanner remains the default and never starts a process or
uses the network. To add an explicitly configured semantic layer, pass both
--llm-judge and the path to an executable bridge:
argus agent scan path/to/skill \
--llm-judge \
--llm-judge-command ./my-llm-judge-bridge \
--format json
Argus starts that exact executable without a shell or interpolated arguments,
writes a versioned JSON request to stdin, and requires a strict JSON response
containing schema_version, decision, and a non-empty rationale. The bridge
can recommend allow, allow-with-approval, or block; its result becomes an
additional llm-intent-judge finding, so it can escalate but never erase or
downgrade deterministic findings.
The opt-in process is fail-closed: 30-second timeout, 4 MiB request limit, 1 MiB limits for stdout and stderr, and a 4096-byte rationale limit. Timeouts, non-zero exits, output overflow, invalid UTF-8/JSON, unknown response fields, or unsupported decisions make the scan return an operational error. The bridge owns any network/API configuration; Argus contains no provider URL or credential handling.
AGT-02 description-drift baseline (GH-64)
AGT-01/03/05 catch malicious agent surfaces at first sight, but they cannot
catch a rug-pull: an MCP tool/server description or SKILL.md
frontmatter that a human already approved and that is later silently mutated.
AGT-02 closes that gap with an explicit, file-based baseline.
# 1. Approve the current descriptions — writes the baseline file.
cargo run -p argus-cli -- agent scan --update-baseline agt02.baseline.json ~/.claude
# → prints "baseline written: N entries" to stderr, exits 0.
# 2. Later scans compare against the approved baseline.
cargo run -p argus-cli -- agent scan --baseline agt02.baseline.json ~/.claude
# → any drifted description emits an AGT-02 finding (medium → allow-with-approval).
What is baselined: every MCP mcpServers.<name>.description and
tools[].description field, plus SKILL.md frontmatter name / description.
Each entry is keyed by "<relative-path>#<locator>" and stored as a SHA-256
hex hash of the description's UTF-8 bytes. Findings show only the first 12 hex
chars of the old/new hashes — never the description plaintext, which may itself
carry injection language. --baseline and --update-baseline are mutually
exclusive.
Behavior: a changed hash → AGT-02 medium (re-approval, not a hard block —
legitimate edits and rug-pulls are lexically indistinguishable; if the new text
also trips AGT-01, the existing critical → block derivation still escalates). A
baselined entry that disappeared → info. A brand-new description not in the
baseline → no AGT-02 finding (AGT-01/03/05 cover first-time surface). With no
--baseline/--update-baseline, AGT-02 is inert and behavior is identical to
GH-57 (no baseline = no drift check, stated explicitly rather than faked).
Trust boundary
--update-baseline is the approval action: whoever runs it declares the
current descriptions trusted. argus does not custody that trust — it only
records and compares hashes. Treat the baseline file as a security artifact:
commit it to your own version control and review its diffs, exactly as you
would review the descriptions themselves. AGT-02 answers only "did an approved
description change?"; whether the new content is malicious is still AGT-01's
(lexical) and GH-59's (intent-misfit) job.
PyPI rule coverage (Milestone 1)
| Family | Rules |
|---|---|
| sdist install-time | setup-py-execution, setup-subprocess, setup-remote-download, setup-eval |
| wheel + sdist | import-time-hook (rewriting sys.modules / __builtins__ at module load) |
| structural | pypi-sdist-no-manifest (info) |
| ported from npm rules (file-content scan) | credential-access, ai-context-poisoning, runtime-hook, wallet-interception |
| name | typosquatting against 60+ Python package names |
crates.io rule coverage (Milestone 1)
| Family | Rules |
|---|---|
| build.rs compile-time | build-rs-subprocess (shells / curl / wget / scripting interpreters only — plain Command::new("rustc") is allow-listed), build-rs-network, build-rs-include-bytes (binary blob + XOR loop), xor-decryption-loop |
| structural | build-rs-execution (info), proc-macro-crate (info), embedded-binary-blob (info) |
| ported from npm rules (file-content scan) | credential-access, ai-context-poisoning, runtime-hook |
| name | typosquatting against 70+ crate names |
Layout
crates/argus-core— data types (Decision,Finding,ScanReport).crates/argus-rules— static detection rules.crates/argus-fetch— npm registry client.crates/argus-pypi— PyPI registry client (sdist + wheel).crates/argus-crates— crates.io registry client (.crate + build.rs).crates/argus-go— Go module proxy client (ZIP +h1:dirhash).crates/argus-nuget— NuGet v3 client (.nupkg+ MSBuild/PowerShell surfaces).crates/argus-maven— Maven Central client (JAR + POM/build resources).crates/argus-rubygems— RubyGems client (nested.gemarchive + Ruby surfaces).crates/argus-composer— Packagist/Composer client (dist ZIP + Composer/PHP surfaces).crates/argus-lockfile— bounded nine-format lockfile normalization and source/integrity policy; no transport or process dependency.crates/argus-cli— theargusbinary.
Development
Enable the git hooks once per clone so cargo fmt drift can't reach CI:
uv tool install pre-commit # or: pipx install pre-commit
pre-commit install # pre-commit stage: cargo fmt + file hygiene
pre-commit install -t pre-push # pre-push stage: cargo clippy -D warnings
CI is the authoritative gate (cargo fmt --check, clippy, cargo test,
argus corpus test); the hooks just give faster local feedback. Run the full
local set anytime with pre-commit run --all-files.
Status
Argus v0.3.0 was published on 2026-09-02 as an immutable GitHub Release with
21 assets: raw binaries and archives for five native targets, checksums,
documentation, and Sigstore attestations. Pin the exact release and select the
asset for your target; for example:
gh release download v0.3.0 --repo majiayu000/argus \
--pattern 'argus-v0.3.0-aarch64-apple-darwin.tar.gz'
gh release verify-asset v0.3.0 \
argus-v0.3.0-aarch64-apple-darwin.tar.gz --repo majiayu000/argus
There is no package-registry distribution. The protected v1 Action branch
intentionally remains on the verified v0.2.1 release commit and is not the
recommended channel for v0.3.0. Build from source against main only when you
intentionally want the source-tree state recorded in the CHANGELOG.
Baseline capability snapshot (as of 2026-07-18; see [0.2.0] for subsequent
work):
- M0 — rule engine + regression corpus + CI (#4, #5).
- M1 — npm tarball fetch + safe extraction + scan (#6); PyPI sdist/wheel (#23); crates.io
.crate+build.rsanalysis (#24); and the completed #22 long-tail umbrella: NuGet (#49), Maven (#50), RubyGems (#51), Composer/Packagist (#52), and Go modules (#53). - M2 verification — the DSSE, Fulcio-chain, SCT, Rekor proof/checkpoint/SET, artifact-subject, and OIDC identity-policy path is opt-in behind the
sigstoreCargo feature (#14). Real npm v0.2intoto/0.0.2SLSA bundles are supported through the audited verifier compatibility patch documented indocs/design/sigstore-verification.md§10. Invalid cryptographic material or policy mismatches remain Critical and block; npm-keyring bundles remain Unsupported. Builds without thesigstorefeature hard-error when--verify-sigstoreis requested.
These entries are implemented and covered by repository tests in the published
v0.3.0 source snapshot. Later main changes are not part of that immutable
release until a new version is published.
Detection coverage is intentionally not claimed in headline numbers without
benchmark evidence — see corpus/ for the regression set the
project gates on and docs/supply-chain-attacks.md
for the attack catalog argus is designed against.
The Agent Infra Stack
This project is one layer of an open-source stack for running coding agents (Claude Code, Codex) as serious infrastructure. Every piece works standalone; together they close the loop:
argus is the Trust layer at install time — scan what you pull from package registries before it ever runs. Its runtime counterpart is vibeguard.
| Layer | Project | What it does |
|---|---|---|
| Extend | claude-skill-registry | Discover and search community Claude Code skills |
| Extend | spellbook | Cross-runtime skills for Claude Code, Codex, and multi-agent workflows |
| Trust | argus ◀ you are here | Static install-time scanner for eight package ecosystems (npm, PyPI, crates.io, Go, NuGet, Maven, RubyGems, Composer) |
| Trust | vibeguard | Rules, hooks, and guards against hallucinated or unverified agent changes |
| Remember | remem | Local-first persistent memory for Claude Code and Codex sessions |
| Orchestrate | harness | Rust agent orchestration platform — rules, skills, GC, observability |
| Route | litellm-rs | High-performance Rust AI gateway — 100+ LLM APIs via OpenAI format |
| Keep | keepline | Session command center — monitor, recover, never lose agent work |