zairo
Git-diff-aware security scanning. zairo scans what actually changed in
a commit or PR, not the whole codebase — so LLM-based vulnerability scanning
stays fast and cheap enough to run on every diff.
Why
Pointing an LLM at an entire repository on every commit doesn't scale: it's
slow, expensive, and buries the two lines that actually changed under
thousands that didn't. zairo instead:
- Diffs the repo to find exactly which lines changed.
- Builds a small dependency graph around those lines — the changed functions plus their direct callers/callees — using Trailmark (supports Python, TypeScript/JavaScript, Java, Go, Rust, C/C++, C#, Ruby, PHP, Swift, Kotlin, and more, auto-detected).
- Scans each changed function/method individually: the model sees that code plus real caller/callee snippets — and, when the change is at the whole-file level rather than inside one function, an outline of the file's other definitions too. All of it pulled from the subgraph only; nothing outside it is ever sent.
- Writes the result as a browsable report, and optionally as SARIF for your existing security tooling.
Contents
- Why
- Install
- Quickstart
zairo analyze— scan a single repozairo fleet— scan multiple repos at once- Output files
- CI / PR gating
- Caching
- Development
Install
pip install zairo
Requires Python 3.12+ (a core dependency, Trailmark, requires it). LLM scanning (--llm) goes through
LiteLLM, so you'll also need an
API key for whatever --model you use — set via the environment variable
your provider expects (e.g. GEMINI_API_KEY for the default
gemini/gemini-2.5-pro, ANTHROPIC_API_KEY for Claude models, OPENAI_API_KEY
for GPT models). See LiteLLM's provider docs
for the full list.
Quickstart
# Analyze uncommitted changes in a repo
zairo analyze /path/to/repo
# Diff two refs, and run an LLM scan on what changed
zairo analyze /path/to/repo --base main --target HEAD --llm
No repo handy? Generate a few small ones with known, distinct vulnerabilities to try it on:
python examples/generate_sample_repos.py
zairo analyze examples/sample-repos/cmd-injection-app --base HEAD~1 --target HEAD --llm
This creates examples/sample-repos/{cmd-injection-app,sql-injection-app,path-traversal-app}
— each a 2-commit repo with one commit introducing a specific,
LLM-findable vulnerability. (Not committed to this repo: a usable git
history can't live nested inside another repo's .git, so the generator
creates them locally on demand.)
zairo analyze
Scans a single repo. repo_path is the only required argument.
What to diff:
| Option | Default | Meaning |
|---|---|---|
--base, -b |
(none) | Ref to diff from (e.g. main, HEAD~3). Omitted entirely: diffs uncommitted changes against HEAD. |
--target, -t |
(none) | Ref to diff to (e.g. HEAD, a branch name). Requires --base. Omitted with --base set: diffs --base against the working tree. |
--depth, -d |
1 |
How many hops to traverse out from changed nodes when building the impact subgraph (callers-of-callers, etc.). |
--language, -l |
auto |
Force a specific language for Trailmark parsing instead of auto-detecting. |
LLM scanning:
| Option | Default | Meaning |
|---|---|---|
--llm |
off | Run the LLM vulnerability scan. Without it, zairo only produces the impact graph, no findings. |
--model |
gemini/gemini-2.5-pro |
Any LiteLLM model string. |
--concurrency, -c |
5 |
Parallel LLM requests. |
--max-tokens |
4096 |
Output token budget per request. Reasoning models count their internal thinking against this too — too low can produce empty responses; raise it if you see that. |
--cache / --no-cache |
cache on | Cache findings by content hash in <output>/.llm_cache.json, so re-running against unchanged code skips the LLM entirely. |
--tokens |
off | Print total prompt/completion/total token usage across real API calls (cache hits aren't counted — they made no call). |
Output & gating:
| Option | Default | Meaning |
|---|---|---|
--output, -o |
zairo_out |
Output directory for report.json / report.html / report.sarif. |
--fail-on |
(none) | Exit non-zero if any finding is at or above this severity (low/medium/high/critical). Requires --llm. See CI / PR gating. |
--verbose, -v |
off | Print diagnostic detail: git commands run, worktree setup, node matching, per-node LLM scan progress, and full (untruncated) per-node error messages. |
Run zairo analyze --help for this same list from the CLI.
zairo fleet
For a security team that owns many repos, not just one: scans several repos
with the same settings and rolls the results up into one
fleet.json/fleet.html/fleet.sarif, alongside each repo's own reports
under <output>/<repo-slug>/.
# From positional paths
zairo fleet /path/to/repo-a /path/to/repo-b --base main --target HEAD --llm
# Or from a file, one repo path per line ('#' comments allowed)
zairo fleet --repos-file repos.txt --base main --target HEAD --llm --fail-on high
It accepts almost all the same options as analyze (see the tables above —
--depth, --language, --llm, --model, --concurrency, --cache,
--max-tokens, --tokens, --fail-on, --verbose), applied identically to
every repo. That means --base/--target need to mean the same thing
across all of them — this fits best when your repos share a diffing
convention (e.g. all diffing against main). Repos with different
conventions need separate fleet invocations.
--tokens reports one combined total across every repo scanned, not a
per-repo breakdown.
A few things unique to fleet:
| Option | Default | Meaning |
|---|---|---|
--repos-file |
(none) | A text file, one repo path per line, merged with any positional paths. |
--repo-concurrency |
1 (sequential) |
How many repos to scan in parallel. Above 1, progress prints one summary line per repo on completion (in completion order) instead of the default's live per-stage detail — concurrent repos' output can't safely interleave line-by-line. --stop-on-error also becomes best-effort: it cancels repos that haven't started yet, but can't interrupt one already in flight. |
--continue-on-error / --stop-on-error |
continue | Whether one repo failing (bad path, parse error, ...) aborts the rest of the run. Either way, any failed repo still fails the overall exit code. |
--fail-on |
(none) | Same severity gate as analyze, but evaluated across all repos combined — one critical finding anywhere fails the whole run. |
Note --concurrency (shared with analyze) and --repo-concurrency control different things: --concurrency is LLM requests in parallel within one repo's scan; --repo-concurrency is how many repos run at once. Combined, total in-flight LLM requests can reach their product — mind your provider's rate limits.
Run zairo fleet --help for the full list.
Output files
| File | When | What it is |
|---|---|---|
report.json |
always | The raw impact graph (nodes, edges, and any attached findings) as data. |
report.html |
always | A self-contained, interactive dependency-graph viewer (Cytoscape.js) — click a node to see its findings. |
report.sarif |
--llm used |
Findings in SARIF 2.1.0, for GitHub code scanning or any other SARIF consumer. Written even for a clean scan (an empty-but-valid log) — that's what lets a scanning UI mark previously reported alerts as resolved. Findings are grouped into SARIF rules by CWE when the model tagged one, so recurring issues of the same category collapse into one rule instead of a new one every time the wording differs. |
fleet produces the same three files per repo, plus fleet.json / fleet.html
/ fleet.sarif (a rollup: per-repo status and severity counts, a dashboard
table linking into each repo's reports, and every repo's SARIF results
merged into one multi-run log).
CI / PR gating
--fail-on <low|medium|high|critical> (on both analyze and fleet) exits
non-zero if any finding at or above that severity is found, so a CI step can
block a merge on it. It requires --llm, and doesn't suppress the SARIF
output — it's still written even on a failed gate, so a scanning UI reflects
the current state either way.
zairo analyze . --base "$BASE_REF" --target HEAD --llm --fail-on high -o zairo_out
See examples/github-actions/zairo-pr-scan.yml
for a full PR-scan workflow: it runs zairo on the PR diff, uploads
report.sarif to GitHub's code scanning, and fails the job if the gate
fails.
Caching
LLM findings are cached by a hash of the exact prompt content (the code
sent, plus its neighbor context) — not by commit or file path. So re-running
zairo after an unrelated change elsewhere in the repo, or after reverting
a change back to something previously scanned, skips the LLM call entirely
for anything unchanged. The cache lives at <output>/.llm_cache.json;
delete it (or pass --no-cache) to force a fresh scan.
Development
python -m venv venv
source venv/bin/activate
pip install -e ".[dev]"
pytest
The package uses a src/ layout (src/zairo/), so pip install -e . is
required for import zairo to resolve correctly during development — the
package isn't importable by just being on PYTHONPATH from the repo root.
License
MIT — see LICENSE.
Metadata
Release files for zairo 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| zairo-0.6.0.tar.gz | 49.8 kB | With provenance | Details for zairo-0.6.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| zairo-0.6.0-py3-none-any.whl | Python 3 | none | any | Details for zairo-0.6.0-py3-none-any.whl |
Total release size: 92.2 kB
Release files / zairo-0.6.0.tar.gz
| Download URL | zairo-0.6.0.tar.gz |
|---|---|
| Size | 49.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dc9cc57a6bbd528ccb533eef11144144712be99c818e9f8c5d2c484b18b9e681
|
|
BLAKE2b-256 checksum How to use checksums |
1e3a8e4a6f9c0f51e9f1790178643abab78733491c8002027ae6223652bbfef3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logRelease files / zairo-0.6.0-py3-none-any.whl
| Download URL | zairo-0.6.0-py3-none-any.whl |
|---|---|
| Size | 42.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
89974c51647c93e617fda55588a1e916e646faffd878c1312d8e612750c1b10c
|
|
BLAKE2b-256 checksum How to use checksums |
cc06c315ab1dbe26bddf1e2b69f04860424e7a83160c1eb14c811428ec3c2084
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logSecurity for zairo 0.6.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for zairo-0.6.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 29, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.