Scan first. Run never.
AUR recipes are small files, but they are not harmless text. PKGBUILD and .install can contain shell behavior, mutable sources, lifecycle hooks, privilege operations, and credential-bearing URLs. The safest first move is therefore not “build it in a sandbox”; it is inspect the recipe as an untrusted artifact and preserve the uncertainty you cannot resolve.
That is the product boundary of aurscan. A successful scan is not a safety certification. It is a decision-support report with explicit evidence, confidence, and coverage.
From checkout to decision
local package checkout
│
▼
bounded traversal + file hashes
│
├── PKGBUILD / .install ──► conservative Bash inspection
├── .SRCINFO ──► declarative metadata parser
└── sources + checksums ──► integrity and URL rules
│
▼
finding(rule, severity, confidence, location)
│
redaction + deterministic policy decision
│
terminal report · JSON · SARIF · stable exit code
The scanner reads PKGBUILD, .install, .SRCINFO, declared sources, and recipe commands. It bounds repository and file sizes, rejects symlinked input files, sanitizes terminal-control characters, and redacts credential-like URL values before they reach a report.
What I built
The implementation is organized as a vertical slice rather than a collection of disconnected checks.
| Layer | Repository surface | Result |
|---|---|---|
| Input boundary | Local traversal, size limits, symlink rejection | A bounded set of files to inspect. |
| Parsing | recipe.py, srcinfo.py, conservative Bash path | Structured package and command facts without shell evaluation. |
| Rules | Source, checksum, URL, lifecycle, privilege, persistence, and payload checks | Stable findings with evidence locations. |
| Policy | policy/evaluator.py | pass_with_notes, review, or block decisions. |
| Reporting | Terminal, JSON, and SARIF renderers | Human review and CI/code-scanning integration. |
| Hygiene | redaction.py | Safer evidence strings and sanitized output. |
A finding carries more than a severity label. The model includes a stable rule ID, confidence, evidence location, remediation text, and fingerprint. That makes repeated scans comparable and gives a reviewer something better than “high risk” with no path back to the source line.
The CLI is designed for a review loop
aurscan scan --local ./package --offline --format terminal
aurscan scan --local ./package --format json --output report.json
aurscan scan --local ./package --format sarif --output report.sarif
Exit codes are part of the interface: 0 for pass-with-notes, 1 for review, 2 for block, 3 for invalid input or configuration, 4 for acquisition failure, and 5 for parser or internal failure. This turns the scanner into something a developer can place before a build step without scraping human-readable output.
The exact AUR RPC client, hardened Git retrieval, OSV identity adapter, and read-only pacman inventory are service modules around the local-first core. Network acquisition is explicit; the default safety path does not need it.
Why the “never execute” invariant changes the design
The scanner cannot use the convenience of shell evaluation to understand every dynamic branch. Instead, uncertainty is a first-class output. Static Bash inspection can identify dangerous shapes—download-to-execution pipelines, dynamic evaluation, privilege operations, sensitive filesystem writes, persistence hooks, encoded payloads—but it cannot prove intent or fully predict runtime behavior.
That limitation is honest and useful. It prevents a green result from meaning “we failed to understand this script, so it must be safe.”
Evidence map
src/aurscan/scanner.pycoordinates the bounded repository scan.src/aurscan/parsing/bash_ast.pyperforms conservative command inspection.src/aurscan/parsing/recipe.pyparses sources, checksums, and package metadata.src/aurscan/parsing/srcinfo.pytreats.SRCINFOas data rather than executing it.src/aurscan/policy/evaluator.pymaps findings to policy decisions and exit codes.src/aurscan/report/renderers.pyemits terminal, JSON, and SARIF formats.src/aurscan/redaction.pysanitizes and redacts evidence values.tests/unit/covers installation, scanner behavior, and service modules.
Status and limits
Version 0.1.0 implements the first complete vertical slice: local traversal, hashing, .SRCINFO parsing, conservative Bash inspection, source and behavior rules, deterministic policy, multiple report formats, and a read-only inventory adapter. Full dependency graphs, history diffs, installed-package orchestration, and dynamic builds remain out of scope for this release.
A pinned checksum binds bytes to a recipe, not to benign intent. A pinned Git commit can still contain malicious code, and advisory databases are incomplete. aurscan reports evidence, confidence, and coverage; it does not certify a package.