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/rununused-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.
cargo install --path crates/aegis-cli # from this repo
# or
cargo install --git https://github.com/qwexvf/aegis-cliNeeds 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.
| 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.
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-commitScan 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 insteadFleet-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 $TOKENaegis.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 versionenrich 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 1Some 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-evalThree 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.
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, goBlocked installs exit 1 and the package manager never runs.
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 againWraps 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.
| 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 |
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.
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.
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 functionOther knobs: AEGIS_GATE_FAIL_ON (safe/review/prompt/block, default
block), AEGIS_GATE_DEEP, and AEGIS_GATE_JOBS.
- Non-interactive shells. CI,
make,mise runandpackage.jsonscripts never source an rc file, so the wrapper is absent there by construction. Useaegis ci <lockfile>andaegis hook --installinstead. npx/pnpm dlx/bunx/uvxas separate entry points, andpython -m pip.- Non-registry specs are skipped, not verified. A
git+https://,file:,link:orworkspace: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 askippedline as "unverified", not "fine".
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.zstThree 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.dandld.so.preloaddrop-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.
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 = 30The 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.
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.
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.
- 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 anHttpClienttrait — 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
.podspecvia the ruby grammar), each an ordinary Cargo dependency — no cgo, no vendored toolchain, no build step outsidecargo build.
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).
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.
The Go v0.29 tree that preceded this rewrite is frozen on the
old branch; releases up to
v0.29.1 are Go builds.