Skip to content

aegis

Supply-chain security scanner for source packages, lockfiles, and container images. No CGO, no tokio — pure-Rust, feature-gated crates, parallel across cores via rayon.

It answers the questions a security engineer actually asks about a dependency:

  • Does this package's code do something dangerous? (analyze — AST + heuristics)
  • Does my lockfile pin a known-vulnerable version? (ci — OSV/GHSA)
  • How do I fix it? (fix — minimal safe version bumps)
  • Did this dependency's behavior change between versions? (snapshot — drift)
  • Is a flagged dependency even used? (reach / run unused-deps)
  • What's in this container image? (image — tarball or registry pull)

Pure-Rust, offline-first, CI-first. Every check runs locally with no backend and no network unless you ask for one; the online paths (advisory feeds, published-source fetch) are opt-in and cached on disk.

Install

cargo install --path crates/aegis-cli      # from this repo
# or
cargo install --git https://github.com/qwexvf/aegis-cli

Needs a working C compiler — the tree-sitter grammars are Rust crates that compile their vendored parser.c through the cc build script. That's the same cc cargo already needs to link, so a stock rustup install on Linux or macOS is enough; there is no cgo boundary and no separate grammar toolchain.

Commands

command what it does
parse <lockfile> list a lockfile's dependencies (24 parsers)
analyze <dir> AST + heuristic scan of a package's source → risk verdict
ci <lockfile> CI gate: per-dep enrich + CVE (OSV/GHSA), fail the build on findings
fix <lockfile> version-bump plan + safe upgrade commands
run <aegis.toml> parallel fleet scan (ast/heuristics/cve/deprecated/license/unused-deps)
sbom <lockfile> CycloneDX 1.5 / SPDX 2.3 SBOM
image <tarball> / image --ref <repo:tag> scan an OCI image (3 tiers)
snapshot <sub> aegis.lock lifecycle — see Snapshots
aur <sub> AUR / PKGBUILD install gate — see AUR packages
reach <dir> <pkg> is a dependency imported (reachable) in JS/TS/Py/Go/PHP source?
allowlist <sub> manage capability suppressions — see Allowlist
explain [capability|pkg@ver] the risk model, or a published package's capabilities
hook [--install|--uninstall] git pre-commit hook that scans staged lockfiles
npm/pnpm/yarn/bun/cargo/pip/go install gate — check packages before the manager fetches them, see Install gate
shell-init <shell> shell functions that route installs through the gate
actions / actions scan generate a workflow, or audit existing ones for risk
audit tail the local decision log — what was scanned, and what the verdict was
cache list / cache clear the on-disk advisory caches (CISA KEV, OSV documents)
doctor check the environment can run a scan; exits 1 on a real problem
completion <shell> shell completion script (bash, zsh, fish, powershell, elvish)

Most reporting commands take --json; ci, analyze, and run also take --sarif (SARIF 2.1.0 for GitHub Code Scanning). Exit codes: 0 clean, 1 findings ≥ threshold, 2 usage/IO error.

Security workflows

Block vulnerable dependencies in CI

aegis ci package-lock.json --fail-on block       # exit 1 → build fails
aegis ci package-lock.json --sarif > aegis.sarif # → upload to Code Scanning
aegis actions > .github/workflows/aegis.yml      # ready-made workflow

--fail-on is a verdict threshold — safe, review, prompt, or block (default block). A dep's verdict folds together its scanned capabilities, its advisories, and whether your source actually imports it: an unused dependency's advisory verdict is downgraded one level.

Catch bad dependencies before they're committed

aegis hook --install    # writes .git/hooks/pre-commit

Scan a suspicious package's source

aegis analyze ./node_modules/some-pkg --ecosystem npm
#   verdict: block (score 180)
#   [ 70] install-hook-suspicious — install hook downloads-and-executes …
#   [ 65] git-dep-in-optional — worm-propagation injection vector
aegis analyze ./pkg --online     # + npm maintainer/provenance/tarball-drift checks
aegis explain lodash@4.17.4      # fetch + scan a published package instead

Fleet-scan a monorepo of vendored packages

aegis run aegis.toml    # per-task verdicts, overall fail if any task blocks
[[task]]
name = "vendored-lib"
path = "./vendor/lib"
ecosystem = "npm"
checks = ["ast", "heuristics", "cve", "deprecated", "license", "unused-deps"]
deny_licenses = ["GPL-3.0"]

Scan a container image

aegis image ./image.tar          # docker save / OCI layout
aegis image --ref alpine:latest  # pull from a registry, then scan
aegis image --ref ghcr.io/org/private:1.2 --username u --password $TOKEN

Snapshots

aegis.lock records what each dependency is and what it does, so the next scan can tell you what changed. This is the maintainer-takeover signal: a patch release that suddenly gains shell-spawn is worth a human look even when no CVE exists for it.

aegis snapshot save          # scan the lockfile → aegis.lock (offline, fast)
aegis snapshot enrich        # fetch + scan each dep, add advisories (network)
aegis snapshot show --all    # render it; --used-only hides unused deps
aegis snapshot diff          # saved lock vs a fresh re-scan of the lockfile
aegis snapshot rescan        # re-query advisories; exit 1 on a NEW one (cron)
aegis snapshot verify        # lint the lock for loadability + schema version

enrich is idempotent — re-running only processes deps that don't have a fingerprint yet, so a partial run is safe to resume. diff also accepts two explicit snapshot paths (aegis snapshot diff . a.json b.json).

Single-package drift, without a project lockfile:

aegis snapshot capture ./pkg-v1 --out baseline.json
aegis snapshot capture ./pkg-v2 --baseline baseline.json
#   + shell-spawn  (NEW capability — possible takeover)   → exit 1

Allowlist

Some capabilities are legitimate for a given package: a build tool really does spawn a shell, a template engine really does call Function(). An allowlist records that decision so it stops showing up as a finding.

aegis allowlist list                     # builtin + user + project
aegis allowlist add lodash --capability dynamic-eval \
    --reason "template compiler, reviewed 2026-08"
aegis allowlist test npm/lodash@4.17.21  # which rules match, and why
aegis allowlist verify                   # both files parse and compile
aegis allowlist remove lodash --capability dynamic-eval

Three layers, applied in order:

layer where scope
builtin compiled in curated, ships with the binary
user $XDG_CONFIG_HOME/aegis/allowlist.toml every project on the machine
project aegis.toml committed, shared with the team

--reason is required. A suppression nobody explained is indistinguishable from a mistake six months later, and the point of an allowlist is that somebody decided the risk was acceptable.

Rules are validated before they are written, so an unknown capability or a malformed semver range cannot land on disk and quietly break every later scan. Editing aegis.toml preserves the rest of the file — your [[task]] entries survive.

Install gate

ci and analyze tell you about a dependency you already have. The install gate runs before the package manager fetches anything, which is the only point at which a malicious postinstall has not yet executed.

aegis npm install lodash          # check, then hand off to the real npm
aegis pnpm add left-pad@1.3.0
aegis cargo add serde             # also pnpm, yarn, bun, pip, go

Blocked installs exit 1 and the package manager never runs.

Routing a bare pnpm install through it

Nobody types aegis pnpm install, so generate shell functions that do:

eval "$(aegis shell-init zsh)"              # try it in this shell
aegis shell-init zsh --install              # persist it to your rc file
aegis shell-init --uninstall                # remove it again

Wraps npm, pnpm, yarn and bun by default; --pm cargo,pip,go or --all adds the rest. cargo and go are opt-in because cargo check and go build run constantly and install nothing.

What it checks

Command shape Checked
npm install <pkg> the named packages
npm install / npm ci / pnpm install --frozen-lockfile every dependency the lockfile pins, advisories only (see below)
pip install -r requirements.txt every requirement listed
npm run build, cargo check nothing — not an install, passed straight through
npm install ./local, git+https://…, workspace:* skipped and reported: there is no registry entry to check

Why a lockfile install is advisory-only

A named install (npm install lodash) gets the full treatment: source fetch, AST capability scan, and advisories.

A lockfile install means the whole tree — transitives included, commonly many hundreds of packages. Capability-scanning all of them costs a tarball fetch and an AST parse each: measured at over 90 seconds for a real 922-dependency lockfile, in front of a command you are waiting on. So lockfile mode checks advisories only, which is one batched OSV query for the whole tree (32s cold, 3s warm on that same lockfile) and catches what matters most there — a known-vulnerable version already pinned in your tree.

AEGIS_GATE_DEEP=1 opts a lockfile install into the full capability scan.

Private registries

The gate reads .npmrc — registry=, @scope:registry= and _authToken — from the project directory and $HOME, plus NPM_CONFIG_REGISTRY, and looks each package up on the registry it actually comes from.

A failure against a private registry warns rather than blocks: a private host cannot distinguish "this package does not exist" from "you are not authorised", and blocking on that made every install fail for anyone on Verdaccio, Artifactory or GitHub Packages. A 404 on the public registry still blocks — there it is a real signal that the name is unclaimed.

Only npm-family registries are configurable today; cargo, PyPI and Go always use their public registries.

Failing closed

Anything the gate cannot verify — registry unreachable, package not found, a scan that errored — blocks. Making the scanner unreachable would otherwise be a complete bypass for exactly the adversary this tool models.

Two escape hatches, both named in every blocking message:

AEGIS_NO_GATE=1 pnpm install …            # skip the gate entirely
AEGIS_GATE_ALLOW_UNCHECKED=1 pnpm add …   # accept packages that could not be verified
command pnpm install …                    # bypass the shell function

Other knobs: AEGIS_GATE_FAIL_ON (safe/review/prompt/block, default block), AEGIS_GATE_DEEP, and AEGIS_GATE_JOBS.

What it does not cover

  • Non-interactive shells. CI, make, mise run and package.json scripts never source an rc file, so the wrapper is absent there by construction. Use aegis ci <lockfile> and aegis hook --install instead.
  • npx / pnpm dlx / bunx / uvx as separate entry points, and python -m pip.
  • Non-registry specs are skipped, not verified. A git+https://, file:, link: or workspace: dependency has no registry entry to check, so the gate reports it and lets it through. A git dependency is arguably a higher-risk install shape than a registry one, so treat a skipped line as "unverified", not "fine".

AUR packages

Arch's AUR ships build recipes, not binaries: installing a package runs a PKGBUILD as your user and a .install hook as root. aegis aur scans both, plus the package the build produces.

aegis aur scan ~/.cache/paru/clone/some-pkg     # one package, human output
aegis aur scan <dir> --json                     # machine-readable
aegis aur gate < request.json                   # a whole transaction, one process
aegis aur inspect ./some-pkg-1.0-1-x86_64.pkg.tar.zst

Three layers, because each sees what the others cannot:

  • PKGBUILD text — privilege escalation in a build function, a committed binary in source=() (matched on magic bytes, not file extension), source and checksum arrays that disagree, paste/shortener hosts, download-and-exec.
  • git history — works on a first install with no stored state, by checking the attacker-writable history against the AUR's server-side FirstSubmitted: a force-pushed history, forged commit dates, spliced roots.
  • the built package — pacman hooks (root code on every future transaction), setuid bits, sudoers.d and ld.so.preload drop-ins, PAM modules, files landing in a home directory. A build can fetch a payload from a perfectly legitimate host; whatever it produces still has to appear here.

Calibrated against real data rather than intuition: 1200 packages from /var/cache/pacman/pkg (96.8% clean, no CRITICAL rule fires on any of them), the 41 .INSTALL scripts those ship, and 116 freshly cloned AUR packages.

Using it with an AUR helper

There is a paru fork wired to call this before anything from a PKGBUILD runs — after the sources are fetched, before the first makepkg stage — and again on the built package before pacman -U: github.com/qwexvf/paru.

# ~/.config/paru/paru.conf, under [options]
AegisGate    = warn      # off | warn | block
AegisBin     = aegis
AegisTimeout = 30

The gate fails open: a missing binary, a timeout, or unparseable output lets the install continue and reports how many packages went unscanned. A package the scanner did not report on counts as unscanned, never as clean.

What it will not catch

Line-based text rules lose to obfuscation split across lines, and a build that exfiltrates without leaving anything in the package is invisible to all three layers. This is a filter that raises the cost of a careless attack and informs the review you were going to do anyway — not a guarantee. See the open issues.

What it detects

Behavioral capabilities (AST + heuristics): shell-spawn, dynamic-eval, net-egress, base64-decode, env-cred-read, obfuscated-payload (incl. String.fromCharCode / split-string de-obfuscation via a taint pass), install-hook-suspicious, binary-dropper, hardcoded-secret, typosquat, tarball-source-drift, git-dep-in-optional, unlisted-large-file, known-malware-ioc, maintainer-hijack / version-unpublished / maintainer-changed, provenance-missing, and more — aegis explain lists them all with weights.

Known vulnerabilities: OSV.dev + GitHub GHSA (with GITHUB_TOKEN), enriched with EPSS exploit-probability and CISA KEV; feeds cached on disk (24h/7d TTL).

PKGBUILD-specific rules are listed under AUR packages — they run against build recipes rather than published package source.

Ecosystems: npm (package-lock/yarn/pnpm/bun), PyPI (poetry/uv/pipfile/ requirements), crates, Go, RubyGems, Composer, NuGet, Maven (pom+gradle), Hex (mix+gleam), Pub, CocoaPods, Swift, CRAN, CPAN, Hackage — 24 lockfile parsers. Five of them (npm/PyPI/crates/RubyGems/Go) also fetch and scan each dependency's published source during ci and snapshot enrich.

Design

  • Dependency-free domain core (aegis-domain) — risk scoring, verdicts, capabilities, fix planning, snapshot diffing, reachability suppression, allowlist. Std only.
  • Feature-gated crates — every lockfile ecosystem, tree-sitter grammar, and heuristic behind a Cargo feature; lean builds strip what they don't need:
    cargo build --no-default-features --features npm,pypi,rust
  • Blocking HTTP (ureq) behind an HttpClient trait — mock-tested offline, no async coloring, small binary. Record/replay cassettes make network-shaped tests deterministic.
  • 13 tree-sitter grammars (js/ts/py/ruby/rust/go/php/csharp/java/haskell/ lua/gleam/dart, plus cocoapods .podspec via the ruby grammar), each an ordinary Cargo dependency — no cgo, no vendored toolchain, no build step outside cargo build.

Development

cargo test --workspace                    # 657 tests
cargo clippy --workspace --all-targets -- -D warnings
cargo run -q -p xtask -- analyze-parity   # 27/27 vs Go goldens (offline)
cargo run -q -p xtask -- sbom-parity      # 4/4   vs Go goldens (offline)
cargo run -q -p xtask -- ci-parity        # 6/6   cassette replay (offline)

CI runs three blocking jobs: build-test (fmt/clippy/test), lean-build (feature-gating guard — a leaked dependency fails here), and parity.

The parity harnesses check the Rust scanner against goldens captured from Go v0.29 for analyze, sbom, and ci. They run offline; ci-parity replays a committed HTTP cassette rather than hitting the network. Re-capture with --record (needs the Go binary) or --record-cassettes (needs network).

Security

Report vulnerabilities per SECURITY.md.

aegis is for verifying, protecting, and monitoring software you are authorized to assess. It is not a replacement for authorization.

License

Apache-2.0


The Go v0.29 tree that preceded this rewrite is frozen on the old branch; releases up to v0.29.1 are Go builds.

About

Supply-chain security scanner in Rust — offline-first, CI-first. AST risk scoring, CVE/OSV gating, SBOM, behavioral drift across 24 lockfile ecosystems.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages