| title | Control Schema (ctrl.v1) |
|---|---|
| sidebar_label | ctrl.v1 |
| sidebar_position | 3 |
| description | Reference for the ctrl.v1 control definition schema. |
Schema ID: urn:stave:schema:control:v0.1
A control defines a safety rule that infrastructure assets must satisfy. Controls are written in YAML and evaluated against observation snapshots.
dsl_version: ctrl.v1
id: CTL.S3.PUBLIC.001
name: No Public S3 Buckets
description: >
S3 buckets must not allow public read or list access.
domain: exposure
scope_tags: [aws, s3]
type: unsafe_state
severity: critical
compliance:
cis_aws_v1.4.0: "2.1.5"
pci_dss_v3.2.1: "1.2.1"
soc2: "CC6.1"
params: {}
unsafe_predicate_alias: s3.is_public_readable
remediation:
description: Bucket has public read access enabled.
action: Enable S3 Public Access Block.| Field | Type | Required | Description |
|---|---|---|---|
dsl_version |
string | Yes | Must be "ctrl.v1" |
id |
string | Yes | Control ID matching ^CTL\.[A-Z0-9]+\.[A-Z0-9]+(\.[A-Z0-9]+)*\.[0-9]+$ |
name |
string | Yes | Human-readable name |
description |
string | Yes | What the control checks |
domain |
enum | No | exposure, identity, storage, platforms, third_party |
scope_tags |
array of strings | No | Vendor/technology tags (e.g., aws, s3) |
version |
string | No | Version identifier |
type |
string | No | Control type (e.g., unsafe_state, unsafe_duration) |
severity |
enum | No | critical, high, medium, low, info |
compliance |
object | No | Compliance framework mappings (framework name → control ID) |
params |
object | No | Configurable parameters |
unsafe_predicate |
predicate | Conditionally | Conditions that make an asset unsafe |
unsafe_predicate_alias |
string | Conditionally | Built-in semantic alias expanded during load (for example s3.is_public_readable) |
scope |
object | No | Scope rules to limit which assets are evaluated |
scope.exclude |
predicate | No | Resources matching this predicate are excluded |
remediation |
remediation | No | Remediation guidance |
Control IDs follow the pattern CTL.<VENDOR>.<CATEGORY>.<SEQ>:
CTL.S3.PUBLIC.001
│ │ │ └── Sequence number
│ │ └──────── Category (PUBLIC, ENCRYPT, ACCESS, etc.)
│ └───────────── Vendor/service (S3, IAM, etc.)
└───────────────── Prefix (always CTL)
Multi-segment categories are allowed: CTL.S3.PUBLIC.PREFIX.001, CTL.S3.ACL.ESCALATION.001.
A predicate defines unsafe conditions using boolean logic. It contains either any (OR) or all (AND), each holding an array of rules.
# OR: any one match triggers
unsafe_predicate:
any:
- field: properties.storage.access.public_read
op: eq
value: true
- field: properties.storage.access.public_list
op: eq
value: true
# AND: all must match
unsafe_predicate:
all:
- field: properties.storage.kind
op: eq
value: bucket
- field: properties.storage.access.has_external_access
op: eq
value: truePredicates can be nested — rules may contain their own any/all blocks for complex logic.
You can also use a built-in alias instead of writing predicate rules manually:
unsafe_predicate_alias: s3.is_public_writableExactly one of unsafe_predicate or unsafe_predicate_alias must be present.
The DSL supports 15 operators:
| Operator | Description | Value type |
|---|---|---|
eq |
Equals | string, bool, numeric |
ne |
Not equals (missing fields match) | string, bool, numeric |
gt |
Greater than | numeric |
lt |
Less than | numeric |
gte |
Greater than or equal | numeric |
lte |
Less than or equal | numeric |
in |
Value in list | array |
missing |
Field absent, nil, or empty | boolean (true) |
present |
Field exists and non-empty | boolean (true) |
contains |
String contains substring | string |
any_match |
Any array element matches nested predicate | nested predicate |
neq_field |
Value not equal to another field | field path |
not_in_field |
Value not in another field's list | field path |
list_empty |
List field is empty or nil | boolean (true) |
not_subset_of_field |
List has elements not in another field | field path |
Predicate semantics to note:
- Missing fields do not match
eq false— only explicitly setfalsetriggerseq false. - Missing fields do match
ne "value"— absence counts as "not equal." value_from_paramcan reference a key in the control'sparamssection instead of a literalvalue.
| Field | Type | Required | Description |
|---|---|---|---|
description |
string | Yes | What the violation means |
action |
string | Yes | How to remediate |
example |
string | No | Example safe configuration |
| Type | Behavior |
|---|---|
unsafe_state |
Violation when predicate matches in any snapshot |
unsafe_duration |
Violation when asset is unsafe longer than --max-unsafe threshold |
unsafe_recurrence |
Violation when exposure window count exceeds limit within window |
prefix_exposure |
Violation when protected prefixes are publicly readable |
Controls are organized by vendor and category:
controls/s3/
├── public/ # Public exposure checks
├── access/ # Cross-account access
├── acl/ # ACL privilege checks
├── encrypt/ # Encryption requirements
├── versioning/ # Versioning requirements
├── logging/ # Access logging
├── lifecycle/ # Lifecycle rules
├── lock/ # Object lock
├── network/ # Network conditions
├── governance/ # Tagging and governance
├── write_scope/ # Upload policy scoping
├── tenant/ # Tenant isolation
├── takeover/ # Bucket takeover
├── artifacts/ # Repository artifacts
└── misc/ # Controls and completeness
stave validate performs full JSON Schema Draft 2020-12 validation using the santhosh-tekuri/jsonschema library. Control YAML is converted to JSON internally before schema validation runs. The schema is embedded in the binary — no external files or network access needed.
Validation also runs automatically at the start of stave apply, so controls are always checked before evaluation begins.
| Constraint | Detail |
|---|---|
additionalProperties: false |
Extra fields are rejected at every level — typos in field names cause immediate failure |
| Required fields | dsl_version, id, name, description, and one of unsafe_predicate/unsafe_predicate_alias |
const version |
dsl_version must be exactly "ctrl.v1" |
| ID pattern | Must match ^CTL\.[A-Z0-9]+\.[A-Z0-9]+(\.[A-Z0-9]+)*\.[0-9]+$ |
enum for domain |
exposure, identity, storage, platforms, third_party |
enum for severity |
critical, high, medium, low, info |
enum for op |
15 allowed operators: eq, ne, gt, lt, gte, lte, in, missing, present, contains, any_match, neq_field, not_in_field, list_empty, not_subset_of_field |
| String minimums | id, name, description must be non-empty (minLength: 1) |
| Remediation required fields | If remediation is present, description and action are required |
| Predicate structure | unsafe_predicate must contain any or all arrays of predicate rules |
# Validate a single control
stave validate --in controls/s3/public/CTL.S3.PUBLIC.001.yaml
# Validate all controls in a directory
stave validate --controls controls/s3/
# JSON output for programmatic use
stave validate --controls controls/s3/ --format jsonExit codes: 0 = valid, 2 = validation errors found.
The canonical schema file is schemas/ctrl.v1.schema.json.