Overview / Usage
OPERATIONS
Airlock is a CLI with no runtime dependencies. It is not a GitHub Action yet, and there is no package index release. Everything on this page runs from a checkout.
Install
Python 3.11+$ git clone https://github.com/posture-sec/airlock.git $ cd airlock $ python3 -m venv .venv && . .venv/bin/activate $ pip install . $ airlock --version
The wheel carries the rule data inside the package, so an installed copy scans with the rules it shipped with. A source checkout resolves the same data from rules/.
Run without installing if you prefer:
PYTHONPATH=src python3 -m airlock scan --mode full
No network call is made during a scan. There is no telemetry and no update check.
Commands
scan / manifest| Command | What it does |
|---|---|
| airlock scan --mode full | Scans the tree and reads policy from the working tree, announced with untrusted-policy-source at INFO. |
| airlock scan --event pull_request --base <sha> | Reads policy, suppressions and the manifest from the pull request base commit. |
| airlock scan --event push --before <sha> | Reads them from github.event.before, the commit the push started at. |
| airlock scan --fail-on warn | Moves the exit-code threshold. The report still prints every finding. |
| airlock scan --format github | Emits workflow commands as annotations. console is the default. |
| airlock scan --format json --output report.json | Writes the machine-readable report instead of printing it. |
| airlock manifest add <targets> ... | Records one or more artifacts by SHA-256 in wasm-manifest.toml. Stages nothing. |
| airlock manifest check | Runs the scan pipeline and keeps the wasm-* findings, so the manifest is judged the same way twice. |
When GITHUB_STEP_SUMMARY is set, a step summary is written as well. GitHub caps annotations per level per step and a step summary at 1 MiB; both limits are measured and recorded in the repository rather than assumed.
Trust modes
Policy source| Mode | Policy, suppressions, manifest | Failure behavior |
|---|---|---|
| pr | The pull request base commit | An unresolvable base is exit 2. A policy edited inside the change is policy-changed, and the base copy runs. |
| push | github.event.before | A force push is history-rewritten at HIGH. A new branch reports no-trusted-base at WARN and uses the shipped defaults. |
| full | The working tree | Announced with untrusted-policy-source at INFO. For a local look, not a gate. |
The workflow must check out enough history for the base to be reachable. actions/checkout defaults to depth 1; set fetch-depth: 0. Airlock never fetches, so a shallow checkout fails closed rather than guessing.
Policy
airlock.tomlversion = 1 [policy] fail_on = "high" max_file_bytes = 5000000 line_cap = 20000 tail_max_len = 2000 tail_entropy = 4.5 [soroban] [soroban.require_release_profile] overflow-checks = true panic = "abort" lto = true codegen-units = 1 debug-assertions = false
The file is read from the trusted base in pr and push modes. A key this build does not read produces policy-unknown-key at WARN, so a typo cannot silently leave a setting at its default.
fail_on moves the exit-code threshold. max_file_bytes is the read limit; a larger file is a finding, not a pass. line_cap is the longest line the regexes run on; a longer line is accounted for by cfg-long-line.
The [soroban] table is reserved for the Phase 6 detectors and is not read by a rule yet. Stating it as reserved is more useful than presenting a setting that does nothing.
Suppressions
Fingerprint-keyed[[suppress]] rule = "pat-obfuscator-id" path = "vendor/legacy/bundle.js" # 64 hex characters, copied from the finding fingerprint = "PASTE_THE_FINDING_FINGERPRINT" reason = "pinned vendor bundle, reviewed 2026-10-30" expires = "2026-11-29"
A suppression names a rule, a path and the fingerprint of one finding. It cannot use a glob, and it has no inline form: there is no comment a payload can add to exempt itself.
Fingerprints are occurrence-aware. The same line repeated twice produces two occurrences and needs two entries, because suppressing one copy must not hide the other.
reason is required. An entry that does not apply is suppression-invalid at HIGH, and an entry within 30 days of its expiry is suppression-expiring at WARN. A scanner-state finding cannot be suppressed at all, even if the rule data says otherwise.
Wasm manifest
Hash-keyed / Two steps$ airlock manifest add target/*/*.wasm \ --origin built \ --source contracts/counter \ --build "cargo build --release --target wasm32-unknown-unknown" $ git add wasm-manifest.toml && git commit -m "manifest: counter" # land the manifest on its own, then add the artifacts $ airlock manifest check
The manifest is keyed by SHA-256, not by path, so a known artifact that moves is a WARN rather than a HIGH. An unlisted artifact, or one whose bytes changed, is HIGH.
Land the manifest before the pull request that adds the artifacts. The trusted base is what a pull request is judged against, so a change that adds both at once has no reviewed entry to compare with.
Archives are reported by wasm-archive-skipped because the scanner reads bytes and never unpacks them. A Wasm file inside a zip is an unexamined hole, not a pass.
The Action is planned
Task 4.1 / Not shipped- uses: posture-sec/airlock@<full commit sha> # pin the SHA, not a tag
with:
mode: pr
action.yml does not exist yet. When it does, pin the full commit SHA, read the diff before moving it, and make the check a required status check. Without that last part, a pull request that deletes the scan step simply never runs it.
Until then the honest usage is a CLI step in a workflow you control, with a full-history checkout and read-only permissions.
Exit codes
0 / 1 / 2 / 3| Code | Meaning |
|---|---|
| 0 | Nothing at or above fail_on, which defaults to high. |
| 1 | A finding at or above the threshold. |
| 2 | Usage or configuration error, including an unresolvable base and a missing git. |
| 3 | Internal error. |
A scan that could not run never reports success. That includes a detector that raised, an invalid configuration and a base this checkout cannot reach.