AIRLOCK

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.

[ 01 ]

Install

Python 3.11+
From a checkout
$ 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.

[ 02 ]

Commands

scan / manifest
Command surface
CommandWhat it does
airlock scan --mode fullScans 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 warnMoves the exit-code threshold. The report still prints every finding.
airlock scan --format githubEmits workflow commands as annotations. console is the default.
airlock scan --format json --output report.jsonWrites 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 checkRuns 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.

[ 03 ]

Trust modes

Policy source
Where policy comes from
ModePolicy, suppressions, manifestFailure behavior
prThe pull request base commitAn unresolvable base is exit 2. A policy edited inside the change is policy-changed, and the base copy runs.
pushgithub.event.beforeA force push is history-rewritten at HIGH. A new branch reports no-trusted-base at WARN and uses the shipped defaults.
fullThe working treeAnnounced 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.

[ 04 ]

Policy

airlock.toml
airlock.toml
version = 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.

[ 05 ]

Suppressions

Fingerprint-keyed
One entry
[[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.

[ 06 ]

Wasm manifest

Hash-keyed / Two steps
Record, land, then verify
$ 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.

[ 07 ]

The Action is planned

Task 4.1 / Not shipped
Intended shape, not usable
- 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.

Residual risk, and what a required check does not cover →

[ 08 ]

Exit codes

0 / 1 / 2 / 3
What a run returns
CodeMeaning
0Nothing at or above fail_on, which defaults to high.
1A finding at or above the threshold.
2Usage or configuration error, including an unresolvable base and a missing git.
3Internal 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.