argus

September 2, 2026 · View on GitHub

CI License: Apache-2.0

"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.

EcosystemCLI commandIntegrity sourceArtifact and inspected surfacesExplicit limitations
npmfetchRegistry dist.integrity SRI digestTarball; lifecycle scripts, package metadata, text/binary content rules, structural obfuscation signatures, opt-in bounded metadata-anomaly checks, and opt-in full Sigstore verificationObfuscation 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
PyPIpypi-fetchPyPI JSON digests.sha256sdist/wheel; setup.py, import-time Python surfaces, and package contentDoes not execute Python or prove runtime behavior
crates.iocrates-fetchcrates.io API SHA-256 checksum.crate; build.rs, Rust source, and proc-macro structureDoes not compile code or execute procedural macros
Go modulesgo-fetchGOPROXY .ziphash h1: directory hash when availableModule ZIP; init, package initializers, process and network callsMissing/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
NuGetnuget-fetchCatalog SHA-512 packageHash when available.nupkg; PowerShell install hooks and MSBuild .targets/.propsDoes not verify .signature.p7s or inspect DLL bytecode; unavailable catalog hashes are reported explicitly
Mavenmaven-fetch.jar.sha256, falling back to weaker .jar.sha1JAR; pom.xml, manifests, resources, and embedded build scriptsDoes not inspect .class bytecode; SHA-1 fallback detects corruption but is not collision-resistant
RubyGemsgems-fetchRegistry SHA-256 sha.gem; gemspec, extconf.rb, and Ruby sourceStatic rules do not execute Ruby; internal archive checksums are not an independent trust anchor
Composer / Packagistcomposer-fetchPackagist dist.shasum SHA-1Dist ZIP; lifecycle hooks, autoload.files, and PHP sourceSHA-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:

LockfileAccepted closed versions/signatureIntegrity interpretation
package-lock.jsonnpm lockfile 2, 3registry/URL entries require valid SRI; root/link/workspace records are unavailable by format
yarn.lockClassic 1; Berry metadata 4, 6, 8Classic uses SRI or the resolved SHA-1 fragment; Berry npm/archive records require checksum
pnpm-lock.yamlcanonical 5.4, 6.0, 9.0registry/tarball records require SRI; link/workspace/file records are unavailable
poetry.lock1.1, 2.0, 2.1every listed registry artifact is retained and requires a valid hash
uv.lock1every listed registry/URL distribution is retained and requires a valid hash
Cargo.lock3, 4registry packages require SHA-256; path and git records do not treat a VCS revision as an artifact hash
go.sumstrict three-field grammarevery line requires a valid Go h1: digest; source host is unavailable by format
Gemfile.lockBundler major 2, 3, 4; CHECKSUMS only where supportedexact lock-name checksum association; an absent checksum section is unavailable, not verified
composer.lockschema-v1 structurenon-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:

  • vulns reports known vulnerabilities for exact versions from OSV.
  • intel matches 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.name appears 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)

FamilyRules
lifecyclelifecycle-script, pre-scan-execution-marker
contentremote-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
binarybinary-file
nametyposquatting, low-reputation, dependency-confusion, public-registry-internal-name, known-native-build-pattern
lockfilelockfile-http-resolved, untrusted-registry-host, lockfile-mutable-vcs-ref, lockfile-integrity-missing, lockfile-integrity-invalid, lockfile-integrity-weak, lockfile-integrity-unavailable
provenancemissing-provenance (info), provenance-verified-subject (info), provenance-subject-mismatch (block), provenance-fetch-blocked / provenance-fetch-failed / provenance-parse-failed (operational errors)
npm metadataversion-shape-anomaly, rapid-publish-window (approval); npm-version-shape-unassessed, npm-rapid-publish-unassessed (info)
ai-contextai-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.

RuleSeverityDetects
AGT-01-injection-languagecritical → blockauthority-claim / instruction-override / concealment language (English + Chinese) in AGENTS.md, CLAUDE.md, SKILL.md, .claude/**/*.md, and MCP tool description fields
capability-manifestmedium → approvaldeclarative 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-exechigh → blockremote download piped to a shell (curl … | sh, iwr … | iex) in hook/skill scripts
AGT-03-secret-exfilhigh → blockhigh-sensitivity credential access combined with network egress in the same script
capability-misfithigh → blockdeclared skill intent does not justify high-risk capability combinations such as credential exfiltration or agent config/hook writes
agent-config-writemedium → approval or high → blockscript writes .claude/settings*.json or hook paths; matching agent-config intent is declarative, mismatched intent blocks
hook-persistencehigh → blockscript persists or auto-approves an agent hook
credential-access / network-exfiltrationhigh → blockmanifest-backed evidence for credential reads and off-box network exfiltration
agent-native-executablemedium → approvala 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-loadmedium → approvalmcpServers.<name>.alwaysLoad: true (permanent full trust)
AGT-05-enable-all-project-mcpmedium → approvalenableAllProjectMcpServers: true
AGT-05-enabled-mcpjson-serversmedium → approvalnon-empty enabledMcpjsonServers allowlist
AGT-05-posttooluse-output-rewritemedium → approvalPostToolUse hook rewriting updatedToolOutput for non-MCP tools
AGT-05-config-unparseableinfoagent config file is not valid JSON
AGT-06-workflow-mutable-actionmedium → approvala 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-injectioncritical → blocka 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-checkoutcritical → blockpull_request_target or workflow_run checks out an attacker-controlled pull-request/workflow-run ref
AGT-06-workflow-write-allhigh → blockworkflow-level or job-level permissions: write-all grants every available GITHUB_TOKEN permission write access
AGT-06-workflow-privileged-writemedium → approvalpull_request_target or workflow_run explicitly grants a scoped GITHUB_TOKEN permission write access
AGT-02medium → approvalan already-approved MCP/skill description drifted from its recorded baseline hash (rug-pull detection; see below)
AGT-02-baseline-entry-missinginfoa baselined description is no longer present on the scanned surface
AGT-02-baseline-unreadableinfo--baseline file could not be read/parsed (scan continues; not treated as "no drift")
AGT-04-entry-addedmedium → approvala non-symlink high-context file or directory was added after approval
AGT-04-entry-removedmedium → approvala non-symlink high-context file or directory was removed after approval
AGT-04-entry-type-changedmedium → approvala high-context path changed between file and directory
AGT-04-content-modifiedmedium → approvalthe complete bytes of a high-context file changed
AGT-04-symlink-changedmedium → approvala 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:

FlagsAllowed
no baseline/snapshot flagyes; legacy multi-path behavior
--check-snapshotyes; exactly one scan path
--update-snapshotyes; exactly one scan path
--baseline + --check-snapshotyes; two read-only checks on one path
any update flag plus another baseline/snapshot flagno

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)

FamilyRules
sdist install-timesetup-py-execution, setup-subprocess, setup-remote-download, setup-eval
wheel + sdistimport-time-hook (rewriting sys.modules / __builtins__ at module load)
structuralpypi-sdist-no-manifest (info)
ported from npm rules (file-content scan)credential-access, ai-context-poisoning, runtime-hook, wallet-interception
nametyposquatting against 60+ Python package names

crates.io rule coverage (Milestone 1)

FamilyRules
build.rs compile-timebuild-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
structuralbuild-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
nametyposquatting 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 .gem archive + 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 — the argus binary.

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.rs analysis (#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 sigstore Cargo feature (#14). Real npm v0.2 intoto/0.0.2 SLSA bundles are supported through the audited verifier compatibility patch documented in docs/design/sigstore-verification.md §10. Invalid cryptographic material or policy mismatches remain Critical and block; npm-keyring bundles remain Unsupported. Builds without the sigstore feature hard-error when --verify-sigstore is 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.

LayerProjectWhat it does
Extendclaude-skill-registryDiscover and search community Claude Code skills
ExtendspellbookCross-runtime skills for Claude Code, Codex, and multi-agent workflows
Trustargus ◀ you are hereStatic install-time scanner for eight package ecosystems (npm, PyPI, crates.io, Go, NuGet, Maven, RubyGems, Composer)
TrustvibeguardRules, hooks, and guards against hallucinated or unverified agent changes
RememberrememLocal-first persistent memory for Claude Code and Codex sessions
OrchestrateharnessRust agent orchestration platform — rules, skills, GC, observability
Routelitellm-rsHigh-performance Rust AI gateway — 100+ LLM APIs via OpenAI format
KeepkeeplineSession command center — monitor, recover, never lose agent work