Stave detects infrastructure assets that have remained unsafe for too long, using only configuration snapshots — no cloud credentials required.
For MVP, Stave assumes you are capturing snapshots from production environments to fix critical issues.
Design implications:
stave snapshot upcomingis optimized for action-oriented, chronological next snapshotsstave snapshot prunedefaults to bounded retention so observation directories do not grow indefinitelystave.yamlcentralizes lifecycle defaults (max_unsafe,snapshot_retention,capture_cadence,snapshot_filename_template) so command behavior stays consistent in local and CI/CD workflows
git clone https://github.com/sufield/stave.git
cd stave
make buildThe binary will be created as ./stave.
make installThis installs stave to your $GOPATH/bin.
# Check capabilities
stave capabilities
# Validate inputs first
stave validate \
--controls controls/s3 \
--observations examples/public-bucket/observations/
# Run evaluation
stave apply \
--controls controls/s3 \
--observations examples/public-bucket/observations/ \
--max-unsafe 168h
# Diagnose unexpected results
stave diagnose \
--controls controls/s3 \
--observations examples/public-bucket/observations/Stave can automatically find your controls/ and observations/ directories so you don't need to type --controls and --observations every time. This works with apply, validate, and diagnose.
When you omit --controls or --observations, Stave resolves in this order:
- Check active context defaults (if set via
stave context use) - Check the project root for
controls/orobservations/directly - If not found, search up to 3 levels deep for a uniquely named directory
- If exactly one match is found, use it
- If multiple or no matches are found, report an inference error with searched paths and fix flags
The project root is determined by:
STAVE_PROJECT_ROOTenvironment variable (if set and valid)- Otherwise, the current working directory
# From a project root with conventional layout:
# my-project/
# controls/
# observations/
cd my-project
stave apply # finds both dirs automatically
stave validate # same inference
stave diagnose # same inference
# Explicit flags always win:
stave apply --controls ./custom-controls # no inference for controls
# Using STAVE_PROJECT_ROOT:
STAVE_PROJECT_ROOT=/path/to/project stave apply
# Set context defaults once for this project
stave context use prod --controls ./controls --observations ./observations --config ./stave.yaml
# If inference fails, Stave prints searched paths, candidates, and exact fix flags- Explicit flags always take precedence over inference
- Only directories with the exact name are matched (no substring matching)
- Search depth is limited to 3 levels to keep inference fast and predictable
- Inference failures include what was missing, what was searched, candidates, and exact fix flags
- Inference is deterministic, offline, and non-interactive
Use this table when you know your goal but want the fastest path to the right command and docs.
| I want to... | Run this command | Read this doc |
|---|---|---|
| Get my first finding in 60 seconds | stave apply --observations examples/public-bucket/observations/ --max-unsafe 168h --now 2026-01-11T00:00:00Z |
time-to-first-finding.md |
| Evaluate my own snapshots instantly | stave init && stave validate && stave apply |
time-to-first-finding.md |
| See where I am and what to do next | stave status |
README.md |
| Start a new project with sane defaults | stave init --profile aws-s3 |
README.md |
| Validate controls and observations before evaluating | stave validate --controls ./controls --observations ./observations |
README.md |
| Evaluate current risk status | stave apply --controls ./controls --observations ./observations --format json > output/evaluation.json |
README.md |
| See what snapshot actions are due next | stave snapshot upcoming --controls ./controls --observations ./observations --out output/upcoming.md |
README.md |
| Inspect effective project defaults and override sources | stave config show --format json |
README.md |
| Query/update project config from terminal | stave config get max_unsafe / stave config set max_unsafe 72h |
README.md |
| Check if snapshots are stale/sparse before evaluation | stave snapshot quality --observations ./observations --strict |
README.md |
| Compare drift between latest snapshots | stave snapshot diff --observations ./observations --format text |
README.md |
| Keep observations folder bounded | stave snapshot prune --observations ./observations --dry-run |
README.md |
| Keep auditability while reducing active set | stave snapshot archive --observations ./observations --archive-dir ./observations/archive --dry-run |
README.md |
| Fail CI only for policy-relevant findings | stave ci gate --in output/evaluation.json --baseline output/baseline.json |
README.md |
| Run the full remediation verification loop | stave ci fix-loop --before ./obs-before --after ./obs-after --controls ./controls --out output |
README.md |
| Search docs without leaving terminal | stave docs search "snapshot upcoming" |
README.md |
| Open the best-matching docs page path + summary | stave docs open "snapshot upcoming" |
README.md |
| Resume from where you stopped | stave status then stave status |
README.md |
| Visualize which controls cover which assets | stave graph coverage --controls ./controls --observations ./observations |
README.md |
| Debug why a specific control matched or didn't match an asset | stave trace --control CTL.S3.PUBLIC.001 --observation obs/snap.json --asset-id my-bucket |
README.md |
| Generate a human-readable report from evaluation output | stave report --in output/evaluation.json |
README.md |
| Analyze a bucket policy directly | stave inspect policy --file policy.json |
Command Reference |
| Extract specific fields from validation output | stave validate --template '{{json .Summary}}' |
README.md |
| Create a shortcut for a frequently used command | stave alias set ev "apply --controls controls/s3 --observations observations --max-unsafe 24h" |
README.md |
Need something not listed in this table?
- Suggest a missing intent or docs improvement:
https://github.com/sufield/stave/issues/new?template=docs_feedback.yml&title=docs%3A%20missing%20intent%20-%20
# Validate first
stave validate --controls ./controls --observations ./observations
# Evaluate and save JSON output for downstream tooling
stave apply --controls ./controls --observations ./observations --format json > output/evaluation.json
# Diagnose unexpected outcomes from the same artifacts
stave diagnose --controls ./controls --observations ./observations --previous-output output/evaluation.json
# Trace a single control against a specific asset
stave trace --control CTL.S3.PUBLIC.001 --observation observations/2026-01-15T000000Z.json --asset-id my-bucket
# Continue from last successful workflow step
stave statusWhen you come back later, restart from the last stable artifact instead of redoing all steps.
# 1) See where to continue
stave status
# 2) Print the next recommended command
stave statusIf you want explicit rerun patterns:
# Re-run validation from controls + normalized observations
stave validate --controls ./controls --observations ./observations
# Re-run evaluation and refresh output artifact
stave apply --controls ./controls --observations ./observations --format json > output/evaluation.json
# Re-run diagnose from existing evaluation output artifact
stave diagnose --controls ./controls --observations ./observations --previous-output output/evaluation.jsonCLI usage docs are generated by sibling ../publisher tooling via make docs-gen.
For command/flag-level reference, prefer generated CLI docs over ad-hoc hand-edited pages.
Stave provides these commands:
Getting started (run these first):
| Command | Purpose | When to Use |
|---|---|---|
status |
Project state | See where you are and what command to run next |
Core workflow:
| Command | Purpose | When to Use |
|---|---|---|
validate |
Input correctness | Before evaluation, verify inputs are sound |
apply |
Enforcement | Detect violations, produce findings |
diagnose |
Explanation | Understand unexpected results |
trace |
Predicate debugging | Step-by-step PASS/FAIL trace of a single control against a single asset |
inspect |
Domain analysis | Low-level policy, ACL, exposure, risk, and compliance analysis |
doctor |
Environment readiness | Check prerequisites before first run |
init |
Project scaffolding | Create project structure with --profile, --dir, --capture-cadence |
plan |
Readiness gate | Confirm prerequisites and input readiness before apply |
explain |
Control field requirements | Show what fields a control needs from observations |
fmt |
Deterministic formatting | Canonicalize control YAML and observation JSON |
lint |
Control quality | Validate control design quality rules |
verify |
Before/after comparison | Confirm a fix resolved violations |
For snapshot operations, use the lifecycle command set:
| Command | Purpose | When to Use |
|---|---|---|
snapshot upcoming |
Chronological next actions | Generate due-now/due-soon/overdue items from current unsafe assets |
snapshot prune |
Retention enforcement | Remove stale snapshots so observations/ remains bounded |
snapshot archive |
Audit-preserving retention | Move stale snapshots to archive directory instead of deleting |
snapshot diff |
Snapshot drift comparison | Focus remediation on what changed between latest two snapshots |
snapshot quality |
Snapshot quality gate | Warn/fail on sparse, stale, or missing-key-asset snapshots |
snapshot status |
Snapshot health summary | Generate markdown with snapshot totals, retention posture, and trend vs last week |
snapshot risk |
Snapshot risk report | Generate markdown with violations, upcoming items, and risk signals |
ci baseline save/check |
Fail-on-new CI policy | Preserve accepted findings and fail only on newly introduced findings |
ci gate |
CI policy enforcement | Apply configurable fail modes (any, new, overdue) |
ci fix-loop |
Fix verification loop | Apply before/after snapshots, verify changes, and generate remediation report |
config show |
Effective config inspection | Show resolved defaults and value sources (env/project/user/default) |
config explain |
Config resolution trace | Print effective values and where each value came from |
config get/set |
Config key management | Read or update stave.yaml keys from terminal and CI scripts |
context use/show |
Context defaults | Set/show named project defaults for controls/observations/config paths |
fmt |
Deterministic formatting | Canonicalize control YAML and observation JSON files |
generate |
Starter artifact generation | Create minimal control or observation templates quickly |
graph coverage |
Coverage visualization | Show which controls cover which assets (DOT or JSON output) |
report |
Evaluation report | Generate plain-text markdown report from evaluation output, with TSV findings for unix pipes |
alias ... |
Command aliases | `alias set |
enforce |
Remediation artifacts | Generate PAB/SCP templates from evaluation output |
controls list|explain|aliases |
Control discovery | Browse, explain, and manage control aliases |
| — | Extractor development | Use an extractor (any language) to produce obs.v0.1 JSON. See Building an Extractor |
packs list|show |
Pack discovery | Browse available control packs |
fix |
Remediation guidance | Show fix guidance for a specific finding |
bug-report |
Diagnostic bundle | Collect environment info for bug reports |
prompt from-finding |
LLM prompt generation | Generate LLM prompt from findings |
env list |
Environment variables | List supported STAVE_* variables |
schemas |
Schema listing | List wire-format contract schemas |
version |
Version info | Print version (also --version flag) |
validate → apply → diagnose
↓ ↓ ↓
Inputs Findings Insights
OK? Found? Why?
- validate - Run first to catch input errors early (malformed YAML, missing fields, timestamp issues)
- apply - Run with
--dry-runto check readiness, then without it to detect violations - diagnose - Run when evaluation output differs from what you expected from your controls, snapshots, or prior runs
- trace - Run for clause-level detail on why a specific control matched or didn't match a single asset
Keep lifecycle defaults in one place per project:
max_unsafe: 168h
snapshot_retention: 30d
default_retention_tier: critical
snapshot_retention_tiers:
critical: 30d
non_critical: 14d
ci_failure_policy: fail_on_any_violation
capture_cadence: daily
snapshot_filename_template: YYYY-MM-DDT000000Z.jsonOptional user-level CLI defaults:
# ~/.config/stave/config.yaml
cli_defaults:
output: json
quiet: false
sanitize: false
path_mode: base
allow_unknown_input: falsestave init creates cli.yaml with commented keys you can uncomment.
This is useful for frequently used flags such as --output, --quiet, --sanitize,
--path-mode, and --allow-unknown-input.
Default resolution order:
- Explicit flags
- Environment variables
- Project config (
stave.yaml) - User config (
~/.config/stave/config.yaml, orSTAVE_USER_CONFIG) - Built-in defaults
max_unsafedrives default thresholds for commands likeapplyandsnapshot upcoming.snapshot_retentionis global fallback retention when no tier-specific value is set.default_retention_tier+snapshot_retention_tiersdrive defaults forsnapshot pruneandsnapshot archive.ci_failure_policydrivesstave ci gatebehavior in CI.capture_cadenceandsnapshot_filename_templatedocument/standardize how snapshots are captured and named.
Manage these keys from terminal:
stave config get max_unsafe
stave config set max_unsafe 72h
stave config set snapshot_retention_tiers.non_critical 14dSupported stave config get/set keys:
max_unsafesnapshot_retentiondefault_retention_tierci_failure_policycapture_cadencesnapshot_filename_templatesnapshot_retention_tiers.<tier>
stave init --capture-cadence sets scaffold defaults to avoid ad-hoc snapshot timing:
daily: lower cost and lower noise, good default for most teams.hourly: tighter feedback loops for critical production incidents and fast-changing environments.
Without a cadence convention, teams capture snapshots irregularly, which makes duration windows less reliable and causes inconsistent CI behavior.
Both snapshot prune (deletes files) and snapshot archive (moves files) share the same safety model:
- Safe by default: When neither
--dry-runnor--forceis specified, both commands default to a dry run — previewing operations without applying them. - Explicit opt-in: Use
--forceto apply the operation. - Minimum retention: Both keep at least
--keep-minsnapshots (default: 2), regardless of age filters.
# Generate action items and CI summary
stave snapshot upcoming \
--controls ./controls \
--observations ./observations \
--due-soon 24h \
--status OVERDUE \
--control-id CTL.S3.PUBLIC.001 \
--format json \
--out output/upcoming.md \
--summary-out "$GITHUB_STEP_SUMMARY"
# Prune old snapshots (preview first)
stave snapshot prune --observations ./observations --older-than 30d --dry-run
stave snapshot prune --observations ./observations --older-than 30d --force
stave snapshot prune --observations ./observations --older-than 30d --dry-run --format json
# Tier-based retention (reads snapshot_retention_tiers from stave.yaml)
stave snapshot prune --observations ./observations --retention-tier non_critical --dry-run
# Archive old snapshots instead of deleting
stave snapshot archive --observations ./observations --archive-dir ./observations/archive --older-than 30d --dry-run
stave snapshot archive --observations ./observations --archive-dir ./observations/archive --older-than 30d --force
stave snapshot archive --observations ./observations --archive-dir ./observations/archive --retention-tier critical --dry-run
stave snapshot archive --observations ./observations --archive-dir ./observations/archive --older-than 30d --dry-run --format json
# Diff latest two snapshots
stave snapshot diff --observations ./observations --format json --out output/diff.json
# Diff filters for focused triage
stave snapshot diff --observations ./observations --change-type modified --asset-type res:aws:s3:bucket --asset-id prod-
# Quality gate before evaluation
stave snapshot quality --observations ./observations --strict
# Weekly status report (markdown)
stave snapshot status \
--controls ./controls \
--observations ./observations \
--archive-dir ./observations/archive \
--out output/weekly-status.md
# Weekly risk report (json)
stave snapshot risk \
--controls ./controls \
--observations ./observations \
--format json \
--out output/weekly-risk.json
# Filter risk upcoming metrics
stave snapshot risk \
--controls ./controls \
--observations ./observations \
--status OVERDUE \
--control-id CTL.S3.PUBLIC.001
# Baseline for fail-on-new CI policy
stave ci baseline save --in output/evaluation.json --out output/baseline.json
stave ci baseline check --in output/evaluation.json --baseline output/baseline.json --fail-on-new
# Policy-driven CI gate from stave.yaml defaults
stave ci gate --in output/evaluation.json --baseline output/baseline.json
# Run full fix verification loop and generate remediation artifacts
stave ci fix-loop \
--before ./obs-before \
--after ./obs-after \
--controls ./controls \
--out outputstave ci gate --policy ... supports:
fail_on_any_violation: fail when current evaluation has any findings.fail_on_new_violation: fail only when findings are new compared to baseline.fail_on_overdue_upcoming: fail when snapshot action items are already overdue.
You can set project default in stave.yaml and override per-run via:
- config:
ci_failure_policy: fail_on_new_violation - env override:
STAVE_CI_FAILURE_POLICY=fail_on_overdue_upcoming
Stave commands produce structured output (JSON to stdout) and accept structured input (via --in, --previous-output, or - for stdin). This lets you chain commands with Unix pipes.
- means "read from stdin" on flags that accept file paths:
stave validate --in -— validate from stdinstave diagnose --previous-output -— read prior apply output from stdin
The default CI pattern saves intermediate results to files:
stave apply ... --format json > output/evaluation.json
stave ci gate --in output/evaluation.json
stave report --in output/evaluation.json
stave fix --input output/evaluation.json --finding CTL.S3.PUBLIC.001@my-bucket
stave enforce --in output/evaluation.json --mode pab# Pipe apply output into diagnose
stave apply --controls controls/s3 --observations observations/ --max-unsafe 168h \
| stave diagnose --previous-output - --controls controls/s3 --observations observations/
# Extract control IDs from findings
stave apply --controls controls/s3 --observations observations/ --max-unsafe 168h \
| jq '.findings[].control_id'
# Render coverage graph as PNG
stave graph coverage --controls controls/s3 --observations observations/ \
| dot -Tpng > coverage.png
# Validate a control from stdin
cat controls/s3/CTL.S3.PUBLIC.001.yaml | stave validate --in -| Command | Produces | Consumes | Input Flag |
|---|---|---|---|
apply |
out.v0.1 JSON |
controls + observations | --controls, --observations |
diagnose |
diagnostic JSON/text | controls + observations + prior output | --previous-output (accepts -) |
validate |
validation JSON/text | single file or dirs | --in (accepts -) |
report |
markdown/text | evaluation JSON | --in |
enforce |
Terraform/SCP artifacts | evaluation JSON | --in |
fix |
remediation text | evaluation JSON | --input |
ci gate |
pass/fail | evaluation JSON | --in |
graph coverage |
DOT/JSON | controls + observations | --controls, --observations |
Checks that inputs are well-formed and consistent as a pre-evaluation validation step.
stave validate [flags]Purpose: Verify inputs are sound before evaluation.
Flags:
| Flag | Default | Description |
|---|---|---|
--controls |
controls/s3 |
Path to control definitions directory |
--observations |
observations |
Path to observation snapshots directory |
--max-unsafe |
168h |
Maximum allowed unsafe duration |
--now |
(current time) | Override evaluation time (RFC3339 format) |
--format |
text |
Output format: text or json |
--strict |
false |
Treat warnings as errors (exit 2) |
--fix-hints |
false |
Print command-level remediation hints |
--quiet |
false |
Suppress output (exit code only) |
--in |
(none) | Validate a single file path (or - for stdin) |
--template |
(none) | Go-style template string for custom output (bypasses --format) |
What it checks:
| Category | Checks |
|---|---|
| Controls | Schema validation, required fields (id, name, description), ID format |
| Observations | Schema validation, timestamps, asset IDs |
| Time sanity | Snapshots sorted, unique timestamps, --now >= latest snapshot |
| Consistency | Predicate references valid params, duration feasibility |
Exit Codes:
| Code | Meaning |
|---|---|
| 0 | All inputs valid (no errors, no warnings) |
| 2 | Validation errors or warnings found |
Examples:
# Basic validation
stave validate
# Custom directories
stave validate \
--controls ./my-controls \
--observations ./snapshots
# JSON output (for CI parsing)
stave validate --format json
# Validate a single file
stave validate --in ./observations/2026-01-11T000000Z.jsonOutput Format (text):
Validation passed (2 warnings)
WARNING: SPAN_LESS_THAN_MAX_UNSAFE
span=24h0m0s
max_unsafe=168h0m0s
Fix: Add older snapshots or reduce --max-unsafe
WARNING: ASSET_SINGLE_APPEARANCE
asset_id=res-123
Fix: Duration tracking requires asset to appear in multiple snapshots
---
Checked: 2 controls, 2 snapshots, 3 assets
Output Format (JSON):
{
"valid": true,
"warnings": [
{
"code": "SPAN_LESS_THAN_MAX_UNSAFE",
"signal": "warning",
"evidence": {"span": "24h0m0s", "sla_threshold": "168h0m0s"},
"action": "Add older snapshots or reduce --max-unsafe"
}
],
"summary": {
"controls_checked": 2,
"snapshots_checked": 2,
"resources_checked": 3
}
}Validation Codes:
| Code | Signal | Meaning |
|---|---|---|
CONTROL_MISSING_ID |
error | Control missing required id field |
CONTROL_MISSING_NAME |
error | Control missing required name field |
NOW_BEFORE_SNAPSHOTS |
error | --now must be at or after the latest snapshot |
SINGLE_SNAPSHOT |
warning | Only 1 snapshot (need 2+ for duration tracking) |
SPAN_LESS_THAN_MAX_UNSAFE |
warning | Snapshot span shorter than threshold |
CONTROL_NEVER_MATCHES |
warning | No assets match unsafe_predicate |
Evaluates configuration snapshots against safety controls.
stave apply [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--controls |
controls/s3 |
Path to control definitions directory |
--observations |
observations |
Path to observation snapshots directory |
--max-unsafe |
168h |
Maximum allowed unsafe duration |
--now |
(current time) | Override evaluation time (RFC3339 format) |
--allow-unknown-input |
false |
Allow observations with unknown source types |
--integrity-manifest |
(none) | Verify loaded observation files against expected SHA-256 hashes in a manifest JSON |
--integrity-public-key |
(none) | Verify signed manifest with Ed25519 public key (requires --integrity-manifest) |
--min-severity |
(none) | Only evaluate controls at or above this severity level |
--control-id |
(none) | Evaluate only this specific control |
--exclude-control-id |
(none) | Exclude specific controls (repeatable) |
--compliance |
(none) | Only evaluate controls mapped to this compliance framework |
Duration Format:
- Hours:
24h,168h,720h - Days:
1d,7d,30d - Combined:
1h30m
Exit Codes:
| Code | Meaning |
|---|---|
| 0 | Success, no violations found |
| 2 | Error (invalid input, missing files, schema invalid) |
| 3 | Success, violations found |
Examples:
# Basic evaluation
stave apply
# Custom directories
stave apply \
--controls ./my-controls \
--observations ./snapshots
# 7-day threshold
stave apply --max-unsafe 7d
# Deterministic evaluation (for CI/testing)
stave apply --now 2026-01-15T00:00:00Z
# Allow unknown source types
stave apply --allow-unknown-input
# Integrity-checked evaluation (unsigned manifest)
stave apply \
--controls ./my-controls \
--observations ./snapshots \
--integrity-manifest ./observations.manifest.json
# Integrity-checked evaluation (signed manifest)
stave apply \
--controls ./my-controls \
--observations ./snapshots \
--integrity-manifest ./observations.signed-manifest.json \
--integrity-public-key ./observations.pubManifest format
{
"files": {
"2026-01-01T000000Z.json": "<sha256-hex>"
},
"overall": "<sha256-hex>"
}Notes:
--integrity-public-keycan only be used with--integrity-manifest.- Integrity verification is not supported with
--observations -(stdin mode). - Any mismatch (missing/extra file, wrong hash, invalid signature) fails evaluation before control execution.
Displays supported versions and input types.
stave capabilitiesOutput:
{
"version": "0.1.0",
"offline": true,
"observations": {
"schema_versions": ["obs.v0.1"]
},
"controls": {
"dsl_versions": ["ctrl.v1"]
},
"inputs": {
"source_types": [
{
"type": "aws.config_api",
"description": "AWS Config API snapshot",
"tool_min_version": "1.0.0",
"plan_format": "terraform show -json"
},
{
"type": "aws-s3-snapshot",
"description": "S3 snapshot JSON observations"
}
]
},
"packs": [
{
"name": "s3",
"path": "controls/s3",
"version": "0.1.0"
}
],
"security_audit": {
"enabled": true,
"formats": ["json", "markdown", "sarif"],
"sbom_formats": ["spdx", "cyclonedx"],
"vuln_sources": ["hybrid", "local", "ci"],
"fail_on_levels": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"],
"compliance_frameworks": ["nist_800_53", "cis_aws_v1.4.0", "soc2", "pci_dss_v3.2.1"]
}
}Packs: The packs field lists available control packs. Each pack includes:
name: Pack identifierpath: Directory containing pack controlsversion: Pack version
Low-level domain analysis primitives. Each subcommand reads JSON from --file or stdin and outputs JSON. These are building blocks for custom tooling.
stave inspect <subcommand> [flags]Subcommands:
| Subcommand | Purpose | Input |
|---|---|---|
policy |
S3 bucket policy analysis | Raw bucket policy JSON |
acl |
S3 ACL grant analysis | JSON array of grants |
exposure |
Exposure classification | Normalized resource inputs |
risk |
Risk scoring | Statement context JSON |
compliance |
Compliance framework crosswalk | Crosswalk YAML (--file, required) |
aliases |
Predicate alias listing | None (optional --category) |
Examples:
# Analyze a bucket policy
stave inspect policy --file policy.json
# Pipe ACL grants from stdin
cat grants.json | stave inspect acl
# Resolve compliance crosswalk for NIST
stave inspect compliance --file crosswalk.yaml --framework nist_800_53
# List all predicate aliases
stave inspect aliasesAnalyzes evaluation inputs and results to identify likely causes when results don't match expectations.
stave diagnose [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--controls |
controls/s3 |
Path to control definitions directory |
--observations |
observations |
Path to observation snapshots directory |
--previous-output |
(none) | Path to existing apply output JSON |
--max-unsafe |
168h |
Maximum allowed unsafe duration |
--now |
(current time) | Override evaluation time (RFC3339 format) |
--format |
text |
Output format: text or json |
--quiet |
false |
Suppress output (exit code only) |
--case |
(none) | Filter diagnostics to one or more case values |
--signal-contains |
(none) | Filter diagnostics by signal substring (case-insensitive) |
--template |
(none) | Go-style template string for custom output (bypasses --format) |
What it checks:
| Scenario | Checks |
|---|---|
| Expected violations but got none | Threshold mismatch, time span too short, predicate mismatch |
| Unexpected violations | Clock skew, streak evidence, reset detection |
| Empty findings array | No predicate matches, under threshold, became safe |
Examples:
# Basic diagnosis
stave diagnose \
--controls controls/s3 \
--observations examples/public-bucket/observations/
# Diagnose with specific threshold
stave diagnose --max-unsafe 7d
# Diagnose existing output file
stave diagnose --previous-output previous-run.json
# Deterministic diagnosis (for CI)
stave diagnose --now 2026-01-15T00:00:00Z
# JSON output for scripting
stave diagnose --format jsonOutput format:
=== Diagnostic Summary ===
Snapshots: 3
Resources: 2
Controls: 2
Time span: 10d
Threshold: 7d
Violations: 1
Attack surface: 1
=== Diagnostics (1) ===
--- [1] expected_violations_none ---
Signal: Threshold exceeds observed unsafe duration
Evidence: Max unsafe streak: 48h; threshold: 168h
Action: Lower --max-unsafe to 48h or shorter
Command: stave apply --max-unsafe 48h
Common diagnostic signals:
| Signal | Meaning | Action |
|---|---|---|
| Threshold exceeds observed unsafe duration | Resources are unsafe but not long enough | Lower --max-unsafe |
| Time span shorter than threshold | Snapshot coverage window is shorter than the configured threshold | Collect more snapshots |
| No assets matched unsafe_predicate | Predicate doesn't match any assets | Check extractor or predicate |
| Evaluation time before latest snapshot | --now is set incorrectly |
Fix --now timestamp |
| Streak reset detected | Resource became safe briefly | Expected behavior |
Shows which controls cover which assets by testing each control's unsafe_predicate against assets from the latest observation snapshot.
stave graph coverage [flags]Purpose: Visualize policy coverage — find uncovered assets, see control scope, and understand protection density.
Flags:
| Flag | Default | Description |
|---|---|---|
--controls |
controls/s3 |
Path to control definitions directory |
--observations |
observations |
Path to observation snapshots directory |
--format |
dot |
Output format: dot or json |
--allow-unknown-input |
false |
Allow observations with unknown source types |
--sanitize |
false |
Sanitize asset identifiers (global flag) |
Examples:
# Output DOT graph to stdout
stave graph coverage --controls ./controls --observations ./obs
# Render as PNG (requires graphviz)
stave graph coverage --controls ./controls --observations ./obs | dot -Tpng > coverage.png
# JSON output for scripting
stave graph coverage --controls ./controls --observations ./obs --format json | jq .
# Sanitize asset identifiers for sharing
stave graph coverage --controls ./controls --observations ./obs --sanitizeDOT output includes:
- Control nodes (lightblue) in a cluster
- Resource nodes in a cluster (uncovered assets highlighted in lightyellow)
- Directed edges from controls to matching assets
JSON output structure:
{
"controls": ["CTL.S3.PUBLIC.001", "..."],
"assets": ["res:aws:s3:bucket:prod-data", "..."],
"edges": [
{"control_id": "CTL.S3.PUBLIC.001", "asset_id": "res:aws:s3:bucket:prod-data"}
],
"uncovered_assets": ["res:aws:s3:bucket:staging-logs"]
}Generates a plain-text markdown report from evaluation JSON output. The findings section uses TSV (tab-separated values) so that grep, sort, awk, and head work naturally.
stave report [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--in / -i |
(required) | Path to evaluation JSON file |
--out / -o |
(none) | Write report to file |
--format / -f |
text |
Output format: text or json |
Examples:
# Generate report from evaluation output
stave report --in evaluation.json
# Write report to file
stave report --in evaluation.json --out report.md
# Filter findings by control pattern
stave report --in evaluation.json | grep '^CTL.S3.PUBLIC'
# Sort findings by duration (longest first)
stave report --in evaluation.json | awk '/^CTL\./' | sort -t$'\t' -k5 -nr
# Top 5 longest-running violations
stave report --in evaluation.json | awk '/^CTL\./' | sort -t$'\t' -k5 -nr | head -5
# Count violations per control
stave report --in evaluation.json | awk -F'\t' '/^CTL\./{print $1}' | sort | uniq -c | sort -rn
# JSON output for programmatic consumption
stave report --in evaluation.json --format jsonTSV columns:
| Column | Description |
|---|---|
CONTROL_ID |
Control identifier |
RESOURCE_ID |
Resource identifier |
TYPE |
Resource type |
VENDOR |
Cloud vendor |
SEVERITY |
Control severity level |
DURATION_H |
Unsafe duration in hours |
THRESHOLD_H |
Threshold in hours |
FIRST_UNSAFE |
First unsafe timestamp (RFC3339) |
LAST_UNSAFE |
Last unsafe timestamp (RFC3339) |
Data lines start with CTL., making awk '/^CTL\./' a reliable filter for extracting data rows.
Manage command aliases stored in user config (~/.config/stave/config.yaml).
stave alias <subcommand>Subcommands:
| Subcommand | Usage | Description |
|---|---|---|
set |
stave alias set <name> "<command>" |
Create or update an alias |
list |
stave alias list |
List all defined aliases |
delete |
stave alias delete <name> |
Delete an alias |
Alias names must match [a-zA-Z0-9_-]+ and must not collide with existing command names.
Examples:
# Create an alias for a common evaluation command
stave alias set ev "apply --controls controls/s3 --observations observations --max-unsafe 24h"
# Use the alias (appends extra flags)
stave ev --now 2026-01-15T00:00:00Z
# List all aliases
stave alias list
# JSON output
stave alias list --format json
# Delete an alias
stave alias delete evShows your current project state and recommends the next command to run. Use this when resuming work or when you're unsure what step comes next.
stave status [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--format |
text |
Output format: text or json |
Examples:
# See where you are and what to do next
stave status
# JSON output for scripting
stave status --format jsonOutput (text):
Summary
-------
Project: /path/to/project
Last command: apply (2026-01-15T00:00:00Z)
Artifacts:
- controls: 35
- snapshots/raw: 2
- observations: 2
- output/evaluation.json: true
[INFO] Next: stave diagnose --controls ./controls --observations ./observations
Checks environment readiness for running Stave.
stave doctorExit Codes:
| Code | Meaning |
|---|---|
| 0 | All checks pass |
| 2 | One or more checks failed |
Examples:
stave doctorOutput shows [PASS], [WARN], or [FAIL] for each check (Go version, required tools, project structure).
Scaffolds a new Stave project directory with controls, observations, and config.
stave init [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--profile |
(none) | Project profile (e.g., aws-s3) |
--dir |
. |
Target directory |
--dry-run |
false |
Preview without creating files |
--with-github-actions |
false |
Include GitHub Actions workflow |
--capture-cadence |
daily |
Snapshot capture cadence (daily or hourly) |
Exit Codes:
| Code | Meaning |
|---|---|
| 0 | Project created |
| 2 | Invalid flags or target exists |
Examples:
stave init --profile aws-s3 --dir my-project
stave init --profile aws-s3 --with-github-actions
stave init --dry-runGenerates starter control or observation templates.
stave generate <subcommand>Subcommands:
| Subcommand | Usage | Description |
|---|---|---|
control |
stave generate control |
Generate a minimal control YAML template |
observation |
stave generate observation |
Generate a minimal observation JSON template |
Examples:
stave generate control > controls/my-new-control.yaml
stave generate observation > observations/template.jsonShows what fields a control needs from observations, helping you understand predicate requirements.
stave explain <control-id> [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--controls |
controls/s3 |
Path to control definitions directory |
--format |
text |
Output format: text or json |
Examples:
stave explain CTL.S3.PUBLIC.001
stave explain CTL.S3.PUBLIC.001 --controls ./my-controls
stave explain CTL.S3.PUBLIC.001 --format jsonDeterministic formatting for control YAML and observation JSON files.
stave fmt [path] [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--check |
false |
Check formatting without modifying (exit 1 if changes needed) |
Exit Codes:
| Code | Meaning |
|---|---|
| 0 | Files formatted (or already formatted with --check) |
| 1 | Files need formatting (--check mode) |
Examples:
stave fmt controls/s3/
stave fmt controls/s3/CTL.S3.PUBLIC.001.yaml
stave fmt --check controls/s3/Validates control design quality rules.
stave lint [path]Exit Codes:
| Code | Meaning |
|---|---|
| 0 | All quality checks pass |
| 2 | Quality issues found |
Examples:
stave lint controls/s3/
stave lint controls/s3/CTL.S3.PUBLIC.001.yamlStep-by-step PASS/FAIL trace of a single control against a single asset. Use for debugging why a control matches or doesn't match.
stave trace [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--control |
(required) | Control ID to trace |
--observation |
(required) | Path to a single observation file |
--asset-id |
(required) | Resource/asset ID to trace against |
--controls |
controls/s3 |
Path to control definitions directory |
--format |
text |
Output format: text or json |
Examples:
stave trace \
--control CTL.S3.PUBLIC.001 \
--observation observations/2026-01-15T000000Z.json \
--asset-id my-bucket
stave trace \
--control CTL.S3.PUBLIC.001 \
--observation observations/2026-01-15T000000Z.json \
--asset-id my-bucket \
--format jsonCompares before/after observations to confirm a remediation resolved violations.
stave verify [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--before |
(required) | Path to before-state observations directory |
--after |
(required) | Path to after-state observations directory |
--controls |
controls/s3 |
Path to control definitions directory |
--now |
(current time) | Override evaluation time |
--max-unsafe |
168h |
Maximum allowed unsafe duration |
Exit Codes:
| Code | Meaning |
|---|---|
| 0 | All violations resolved, none introduced |
| 3 | Remaining or new violations |
Examples:
stave verify \
--before ./obs-before \
--after ./obs-after \
--controls controls/s3 \
--now 2026-01-15T00:00:00ZGenerates remediation templates from evaluation output.
stave enforce [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--in |
(required) | Path to evaluation JSON file |
--mode |
pab |
Enforcement mode: pab (put-account-block) or scp (service control policy) |
--out |
(none) | Output directory |
--dry-run |
false |
Preview without creating files |
Exit Codes:
| Code | Meaning |
|---|---|
| 0 | Artifacts generated |
| 2 | Invalid input |
Examples:
stave enforce --in output/evaluation.json --mode pab --out output/enforcement
stave enforce --in output/evaluation.json --mode scp --out output/enforcement
stave enforce --in output/evaluation.json --dry-runBrowse and manage controls.
stave controls <subcommand>Subcommands:
| Subcommand | Usage | Description |
|---|---|---|
list |
stave controls list |
List all available controls |
explain |
stave controls explain <id> |
Explain a specific control |
aliases |
stave controls aliases |
List control ID aliases |
alias-explain |
stave controls alias-explain <alias> |
Explain what an alias resolves to |
Examples:
stave controls list
stave controls list --format json
stave controls explain CTL.S3.PUBLIC.001
stave controls aliasesBrowse available control packs.
stave packs <subcommand>Subcommands:
| Subcommand | Usage | Description |
|---|---|---|
list |
stave packs list |
List available control packs |
show |
stave packs show <name> |
Show details of a pack |
Examples:
stave packs list
stave packs show s3Shows remediation guidance for a specific finding from evaluation output.
stave fix [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--input |
(required) | Path to evaluation JSON file |
--finding |
(required) | Finding identifier (<control-id>@<asset-id>) |
Examples:
stave fix --input output/evaluation.json --finding CTL.S3.PUBLIC.001@my-bucketCollects diagnostic information for filing bug reports.
stave bug-report [flags]Flags:
| Flag | Default | Description |
|---|---|---|
--out |
bug-report.zip |
Output file path |
--include-config |
false |
Include project config in bundle |
--tail-lines |
100 |
Number of recent log lines to include |
Examples:
stave bug-report
stave bug-report --out my-bug.zip --include-configGenerates an LLM prompt from evaluation findings.
stave prompt from-finding [flags]Examples:
stave prompt from-finding --input output/evaluation.jsonLists supported STAVE_* environment variables.
stave env listLists wire-format contract schemas.
stave schemasPrints version information.
stave versionNote: Also available as the --version global flag (stave --version).
The diagnose and validate commands accept a --template flag for custom output formatting. Templates bypass --format and render directly against the command's output struct.
Supported syntax:
| Syntax | Description |
|---|---|
{{.FieldName}} |
Access a top-level field |
{{.Nested.FieldName}} |
Access nested fields |
{{range .Slice}}...{{end}} |
Iterate over slices |
{{json .Field}} |
JSON-encode a field value |
{{"\n"}} |
Literal newline |
Fields resolve by struct field name or JSON tag name.
Examples:
# Diagnose summary line
stave diagnose --controls ./controls --observations ./obs \
--template '{{.Report.Summary.Snapshots}} snapshots, {{.Report.Summary.Diagnostics}} diagnostics'
# Validate summary as JSON
stave validate --controls ./controls --observations ./obs \
--template '{{json .Summary}}'Observations capture the state of your infrastructure at a point in time.
Location: examples/public-bucket/observations/ directory (or custom path via --observations)
File naming: Use RFC3339 timestamps for deterministic ordering:
2026-01-01T000000Z.json2026-01-15T123000Z.json
Schema:
{
"schema_version": "obs.v0.1",
"generated_by": {
"source_type": "aws.config_api",
"tool": "stave-extract",
"tool_version": "1.0.0",
"provider": "hashicorp/aws",
"provider_version": "5.31.0"
},
"captured_at": "2026-01-01T00:00:00Z",
"assets": [
{
"id": "res:aws:s3:bucket:my-bucket",
"type": "storage_bucket",
"vendor": "aws",
"properties": {
"public": true,
"acl": "public-read"
},
"source": {
"file": "infra/main.tf",
"line": 42
}
}
]
}Required Fields:
| Field | Description |
|---|---|
schema_version |
Must be obs.v0.1 |
captured_at |
RFC3339 timestamp of when snapshot was taken |
assets[].id |
Unique asset identifier |
assets[].type |
Asset type (e.g., storage_bucket) |
generated_by.source_type |
Required unless --allow-unknown-input is set |
Optional Fields:
| Field | Description |
|---|---|
generated_by.tool |
Tool that generated the snapshot |
generated_by.tool_version |
Version of the tool |
assets[].vendor |
Cloud provider (e.g., aws, gcp) |
assets[].properties |
Asset configuration properties |
assets[].source.file |
Source file path |
assets[].source.line |
Line number in source file |
Controls define safety rules that assets must satisfy.
Location: controls/s3/ directory (or custom path via --controls)
Schema:
dsl_version: ctrl.v1
id: CTL.EXP.DURATION.001
name: Unsafe Duration Bound
description: An asset must not remain unsafe beyond the configured time window.
type: unsafe_duration
params:
max_unsafe_duration: "168h"
unsafe_predicate:
any:
- field: "properties.public"
op: "eq"
value: trueRequired Fields:
| Field | Description |
|---|---|
dsl_version |
Must be ctrl.v1 |
id |
Unique control identifier |
name |
Human-readable name |
unsafe_predicate.any |
List of conditions (OR logic) |
Predicate Rules:
Each rule in unsafe_predicate.any checks an asset property:
unsafe_predicate:
any:
- field: "properties.public" # Dot-notation path
op: "eq" # Operator
value: true # Expected valueSupported Operators:
| Operator | Description | Example |
|---|---|---|
eq |
Equals (string, bool, numeric) | {op: "eq", value: true} |
ne |
Not equals | {op: "ne", value: "COMPLIANCE"} |
gt |
Greater than (numeric) | {op: "gt", value: 1} |
lt |
Less than (numeric) | {op: "lt", value: 2190} |
gte |
Greater than or equal (numeric) | {op: "gte", value: 365} |
lte |
Less than or equal (numeric) | {op: "lte", value: 90} |
missing |
Field absent or empty | {op: "missing", value: true} |
present |
Field exists and non-empty | {op: "present", value: true} |
in |
Value in list | {op: "in", value: ["PII", "PHI"]} |
list_empty |
List field is empty or missing | {op: "list_empty", value: true} |
Field Paths:
Use dot notation to access nested properties:
properties.publicproperties.encryption.enabledproperties.tags.environment
{
"run": {
"now": "2026-01-11T00:00:00Z",
"sla_threshold": "168h0m0s",
"snapshots": 3
},
"summary": {
"assets_evaluated": 2,
"attack_surface": 1,
"violations": 1
},
"findings": [
{
"control_id": "CTL.EXP.DURATION.001",
"control_name": "Unsafe Duration Bound",
"control_description": "An asset must not remain unsafe beyond the configured time window.",
"asset_id": "res:aws:s3:bucket:public-bucket",
"asset_type": "storage_bucket",
"asset_vendor": "aws",
"source": {
"file": "infra/main.tf",
"line": 42
},
"evidence": {
"first_unsafe_at": "2026-01-01T00:00:00Z",
"last_seen_unsafe_at": "2026-01-11T00:00:00Z",
"unsafe_duration_hours": 240,
"threshold_hours": 168
},
"remediation": {
"description": "Resource has been unsafe beyond the allowed duration threshold.",
"action": "Review and remediate the unsafe configuration, then verify in a new snapshot."
}
}
]
}run: Evaluation context
now: Evaluation timestampsla_threshold: Configured thresholdsnapshots: Number of snapshots processed
summary: Aggregate statistics
assets_evaluated: Total unique assets seenattack_surface: Resources unsafe in latest snapshotviolations: Resources exceeding threshold
findings[]: Violation details
evidence.first_unsafe_at: When asset first became unsafeevidence.last_seen_unsafe_at: Most recent unsafe observationevidence.unsafe_duration_hours: How long asset has been unsafeevidence.threshold_hours: Configured maximum
Stave tracks how long each asset has been continuously unsafe:
- Load snapshots ordered by
captured_at - Build exposure lifecycle for each asset across snapshots
- Track unsafe windows:
- When asset matches
unsafe_predicate→ start/continue window - When asset becomes safe → reset window
- When asset matches
- Report violations where
unsafe_duration > max_unsafe
If an asset becomes safe and then unsafe again, the timer resets:
Snapshot 1 (Jan 1): public=true → unsafe window starts
Snapshot 2 (Jan 10): public=false → window RESETS (asset is safe)
Snapshot 3 (Jan 11): public=true → NEW unsafe window starts (only 1 day)
This prevents false positives when issues are temporarily fixed.
#!/bin/bash
set -e
# Build
make build
# Run evaluation
./stave apply \
--controls controls/s3 \
--observations examples/public-bucket/observations/ \
--max-unsafe 7d \
--now "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Exit code 3 = violations found (fail the build)name: Security Check
on: [push, pull_request]
jobs:
stave:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.2'
- name: Build Stave
run: make build
- name: Run Stave
run: |
./stave apply \
--controls controls/s3 \
--observations examples/public-bucket/observations/ \
--max-unsafe 168hCreate a script to generate snapshots from Terraform:
#!/bin/bash
# generate-snapshot.sh
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
OUTPUT="observations/${TIMESTAMP}.json"
terraform show -json > terraform-output.json
# Transform to Stave format (implement your transformer)
./transform-terraform.sh terraform-output.json > "$OUTPUT"
echo "Generated: $OUTPUT"-
Use deterministic timestamps for CI: Always pass
--nowin automated pipelines for reproducible results. -
Name snapshots with timestamps: Use RFC3339 format (
2026-01-01T000000Z.json) for automatic ordering. -
Keep multiple snapshots: Stave needs historical data to calculate durations. Keep at least 2-3 weeks of snapshots.
-
Start with longer thresholds: Begin with
30dand tighten to7das your remediation process matures. -
Version your controls: Store control definitions in version control alongside your infrastructure code.
-
Automate snapshot generation: Integrate snapshot generation into your CI/CD pipeline after Terraform plans.
- Check
--max-unsafethreshold—is it longer than the actual unsafe duration? - Verify
captured_attimestamps span enough time - Confirm
unsafe_predicatematches your asset properties
- Check if asset was briefly safe (resets the window)
- Verify
--nowtime if using deterministic mode - Review
evidence.first_unsafe_atin output
This is normal when:
- No assets match the
unsafe_predicate - Matching assets haven't exceeded
max_unsafe - Resources became safe before the threshold
Stave includes a dedicated S3 healthcare evaluation profile for HIPAA compliance. This profile provides two specialized commands and 20 controls covering public exposure, encryption, versioning, logging, access control, network scoping, lifecycle retention, and object lock (WORM).
The most common workflow evaluates S3 buckets from Terraform plan JSON:
# Generate Terraform plan JSON
terraform plan -out=tfplan
terraform show -json tfplan > terraform-plan.json
# Evaluate against all S3 controls
stave apply \
--controls controls/s3 \
--observations ./observations \
--max-unsafe 168hEvaluates S3 observations against the built-in PHI control profile (controls/storage/object_storage/s3/).
stave apply --profile aws-s3 --input observations.jsonFlags:
| Flag | Default | Description |
|---|---|---|
--input |
(required) | Path to observations JSON file |
--bucket-allowlist |
(none) | Bucket names/ARNs to include |
--include-all |
false |
Disable health scope filtering |
--format |
json |
Output format: json or text |
--now |
(current time) | Override current time (RFC3339) |
--quiet |
false |
Suppress output (exit code only) |
Public Exposure:
| ID | Name |
|---|---|
CTL.S3.PUBLIC.001 |
No Public Read Access to PHI S3 Data |
CTL.S3.PUBLIC.002 |
No Public List Access to PHI S3 Buckets |
CTL.S3.PUBLIC.003 |
No Public Write Access |
CTL.S3.PUBLIC.004 |
No Public ACL for PHI S3 Buckets |
CTL.S3.PUBLIC.PREFIX.001 |
Protected Prefixes Must Not Be Publicly Readable |
CTL.S3.INCOMPLETE.001 |
Complete Data Required for Safety Assessment |
Encryption:
| ID | Name |
|---|---|
CTL.S3.ENCRYPT.001 |
Encryption at Rest Required |
CTL.S3.ENCRYPT.002 |
Transport Encryption Required |
CTL.S3.ENCRYPT.003 |
PHI Buckets Must Use SSE-KMS with Customer-Managed Key |
Versioning:
| ID | Name |
|---|---|
CTL.S3.VERSION.001 |
Versioning Required |
CTL.S3.VERSION.002 |
Backup Buckets Must Have MFA Delete Enabled |
Access Logging:
| ID | Name |
|---|---|
CTL.S3.LOG.001 |
Access Logging Required |
Access Control:
| ID | Name |
|---|---|
CTL.S3.ACCESS.001 |
No Unauthorized Cross-Account Access |
CTL.S3.ACCESS.002 |
No Wildcard Action Policies |
Network Scoping:
| ID | Name |
|---|---|
CTL.S3.NETWORK.001 |
Public-Principal Policies Must Have Network Conditions |
Lifecycle Rules (HIPAA Data Retention):
| ID | Name |
|---|---|
CTL.S3.LIFECYCLE.001 |
Retention-Tagged Buckets Must Have Lifecycle Rules |
CTL.S3.LIFECYCLE.002 |
PHI Buckets Must Not Expire Data Before Minimum Retention (2190 days) |
Object Lock / WORM (HIPAA Immutable Storage):
| ID | Name |
|---|---|
CTL.S3.LOCK.001 |
Compliance-Tagged Buckets Must Have Object Lock Enabled |
CTL.S3.LOCK.002 |
PHI Buckets Must Use COMPLIANCE Mode Object Lock |
CTL.S3.LOCK.003 |
PHI Object Lock Retention Must Meet Minimum Period (2190 days) |
The S3 extractor handles these Terraform asset types:
| Terraform Resource Type | Fields Extracted |
|---|---|
aws_s3_bucket |
Bucket name, ARN, tags, object_lock_enabled |
aws_s3_bucket_policy |
Policy statements, public principal detection, network conditions |
aws_s3_bucket_acl |
ACL grants, public grantees |
aws_s3_bucket_public_access_block |
All four public access block settings |
aws_s3_bucket_account_public_access_block |
Account-level public access overrides |
aws_s3_bucket_server_side_encryption_configuration |
SSE algorithm, KMS key ID |
aws_s3_bucket_versioning |
Versioning status, MFA delete |
aws_s3_bucket_logging |
Target bucket, target prefix |
aws_s3_bucket_lifecycle_configuration |
Lifecycle rules, expiration days, transitions |
aws_s3_bucket_object_lock_configuration |
Lock mode (COMPLIANCE/GOVERNANCE), retention period |
The S3 extractor produces a vendor-agnostic canonical model at properties.storage.*. See docs/storage-canonical-model.md for the complete field reference.
Key field groups:
visibility— Public read/list/write statuscontrols— Public access block settingsencryption— At-rest algorithm, KMS key, in-transit enforcementversioning— Versioning status, MFA deletelogging— Access log target bucket and prefixaccess— External accounts, wildcard policiespolicy— Network condition analysis (IP/VPC scoping)lifecycle— Rule counts, expiration days, transition detectionobject_lock— Lock mode, retention daystags— Resource tags (used for PHI/compliance scoping)
The prefix exposure control detects when protected S3 object prefixes are publicly readable. Unlike CTL.S3.PUBLIC.001 which checks bucket-wide public access, this control operates at the prefix level — it can flag invoices/ as exposed while allowing images/ to remain intentionally public.
How it works: The evaluator inspects bucket policies, ACL grants, and public access block settings to determine effective public read access for each protected prefix. It reports the specific exposure source (policy statement, ACL grant, or missing evidence) in findings.
Getting started: The shipped control includes example prefixes that you should customize to match your bucket layout. Edit controls/s3/public/CTL.S3.PUBLIC.PREFIX.001.yaml and replace the prefix lists with your own:
# controls/s3/public/CTL.S3.PUBLIC.PREFIX.001.yaml
dsl_version: ctrl.v1
id: CTL.S3.PUBLIC.PREFIX.001
name: Protected Prefixes Must Not Be Publicly Readable
description: >
S3 bucket prefixes marked as protected must not be publicly readable.
Customize the prefix lists below to match your bucket layout.
domain: exposure
scope_tags:
- aws
- s3
type: prefix_exposure
params:
protected_prefixes: # <- prefixes that must stay private
- "invoices/"
- "secrets/"
- "internal/"
- "backups/"
allowed_public_prefixes: # <- prefixes intentionally public
- "images/"
- "static/"
- "public/"
unsafe_predicate:
any:
- field: properties.storage.kind
op: eq
value: bucketIf protected_prefixes is left empty, the control reports a violation with configuration guidance rather than silently passing — ensuring it stays visible until properly configured.
Parameters:
| Parameter | Type | Description |
|---|---|---|
protected_prefixes |
list of strings | Prefixes that must NOT be publicly readable. Trailing slashes are added automatically. |
allowed_public_prefixes |
list of strings | Prefixes that are intentionally public. Used to detect config overlaps. |
Evaluation logic:
- If
protected_prefixesis empty, the control reports anot_configuredviolation with example configuration. - If any protected prefix overlaps with an allowed prefix, a
config_overlapviolation is reported immediately. - For each protected prefix, the evaluator checks:
- Bucket policies: Does any
Allowstatement grants3:GetObjecttoPrincipal: "*"for an asset ARN that covers this prefix? - Public access block: Does
BlockPublicPolicynegate policy-based exposure? - ACL grants: Do any grants to
AllUsersorAuthenticatedUsersallowREADorFULL_CONTROL? - Missing evidence: If no policy or ACL data exists, the prefix is treated as exposed (fail-closed).
- Bucket policies: Does any
- Each violated prefix produces a separate finding with the exposure source in evidence.
Example findings:
A bucket with a public policy granting s3:GetObject on arn:aws:s3:::my-bucket/* to Principal: "*" and invoices/ as a protected prefix produces:
{
"control_id": "CTL.S3.PUBLIC.PREFIX.001",
"asset_id": "res:aws:s3:bucket:my-bucket",
"evidence": {
"misconfigurations": [
{"property": "exposure_source", "actual_value": "policy:PublicRead", "operator": "eq", "unsafe_value": "policy:PublicRead"},
{"property": "protected_prefix", "actual_value": "invoices/", "operator": "eq", "unsafe_value": "invoices/"}
],
"why_now": "Protected prefix \"invoices/\" is publicly readable via policy:PublicRead."
}
}Observation requirements: The evaluator reads these fields from properties.storage:
| Field | Source | Used for |
|---|---|---|
kind |
Resource type | Trigger predicate (eq bucket) |
policy_statements[] |
Bucket policy | Public read detection per prefix |
public_access_block |
PAB config | Negates policy/ACL exposure |
acl_grants[] |
Bucket ACL | Public grantee detection |
stave forge provides interactive tools for creating, previewing, and
testing custom security controls.
Create a new control with the interactive wizard:
# With a snapshot for live preview and path discovery
stave forge new --snapshot obs.json
# Without a snapshot (no preview, no path browsing)
stave forge newThe wizard guides you through 11 steps: asset type, control ID, name, severity, attack stage, property path selection, predicate authoring, live preview, remediation text, compliance citations, and confirmation.
List all observable properties for a given asset type:
stave forge paths --snapshot obs.json --asset-type aws_s3_bucketShows every property path with its type, presence count across resources, and distinct values for small value sets. Tag map keys are expanded individually with per-key presence counts.
Test a predicate against a snapshot without generating any files:
stave forge preview \
--snapshot obs.json \
--field properties.storage.access.public_read \
--op eq --value trueUses the identical CEL evaluation path as stave apply — if preview
says FAIL, apply will say FAIL.
For CI/CD or scripted control generation:
stave forge new --non-interactive \
--id CTL.S3.TAGS.001 \
--name "All production S3 buckets must have a team tag" \
--asset-type aws_s3_bucket \
--field properties.storage.tags.team \
--op missing \
--severity high \
--remediation "Add a team tag to all production S3 buckets"Produces the same output as the interactive wizard — control YAML and E2E test fixtures.
- Review the generated YAML in
controls/ - Update the E2E fixture with realistic test data
- Run
stave applyto verify against a real snapshot - Run
go test ./...to confirm E2E tests pass