Rule packs
A holistic guide to the deterministic checks: what each pack covers, how they compose into one engine, what the ID bands mean, and the full rule list, generated from the shipped packs so it matches the release exactly.
Overview
Every deterministic check ships in the open, as plain YAML in attestral/rules/, and each cites the control it enforces. A rule is data, not code: a target component type, a structured match, and the framework it maps to. There is no eval anywhere, and an unknown matcher fails closed, so it simply never fires.
This page is the conceptual map. For a searchable index with the full description and recommendation per rule, use the rule browser on the docs page or run attestral explain <ID>. The tables here are generated by scripts/render_packs_doc.py from the same YAML the engine loads.
Two ways to read the packs
The packs split along two independent axes, and it helps to keep them apart:
- By file (how they load). Five YAML files.
core_rules.yamlalways loads first; each*_pack.yamlis a provider expansion loaded after it. This split is operational, so a cloud expansion lands without touching the shared core file, the classic parallel-edit hazard. - By ID band (what they cover). Six
ATL-NNNnumber ranges, one per domain. The band is the domain, not the file. Agentic (1xx) and cross-boundary (2xx) rules live only incore_rules.yaml; each cloud band is split between a seed set incore_rules.yamland the bulk of the checks in that provider's pack.
So core_rules.yaml is deliberately a mixed pack, and the provider packs are pure cloud. The two tables below show each lens.
How the packs compose
At startup, RuleEngine builds its rule list from _builtin_packs(): the core file first, then every provider pack in sorted order, then any custom packs you pass in. All rules merge into one flat list and every rule runs against every matching component, so from the engine's point of view there is one pack; the files are just how we keep it maintainable.
_builtin_packs() = [core_rules.yaml] + sorted(*_pack.yaml)
= core → aws → azure → gcp → k8s
RuleEngine(["org_rules.yaml"]) # your packs load last, alongside the built-ins
Load a custom org pack the same way. Duplicate IDs across packs are an error the generator and the engine both reject, so every rule ID is globally unique.
The five packs
Each card links its file. The bar is the pack's severity mix.
Coverage by domain (ID band)
The ID band is the source of truth for what a rule is about. Bands map to domains, not to files: a 2xx rule is cross-boundary wherever it lives, and cloud bands are split between the core seed set and the provider pack.
| Band | Domain | Rules | Lives in |
|---|
Why the packs are shaped this way
The layout encodes the product strategy, a barbell:
- Agentic security is the identity (1xx, 2xx). MCP, prompt-injection surfaces, tool poisoning, excessive agency, agent supply chain, confused-deputy credential holders, and whole-system reachability. These reason over a system model of components, edges, and trust boundaries, not a single resource, so they cannot be copied by adding more cloud policies. They cite OWASP Agentic Security, OWASP MCP, MITRE ATLAS, and SAFE-MCP.
- Cross-boundary is the moat (2xx,
target: model). These match the whole design, not a component: a secret crossing a trust boundary, an agent reaching a cloud resource it should not, a taint flow from untrusted input to a sensitive action. They headline a scan because nobody else produces them. - Cloud is parity on what matters, not volume (0xx, 3xx, 4xx, 5xx). Roughly 150 high-signal CIS and essential checks across AWS, Azure, GCP, and Kubernetes, good enough that nobody needs a second tool, not an arms race. Every cloud rule earns its place with a real CIS or NIST control and a fixture.
ml.py, --ml) and is scored on tool and server descriptions and system-prompt files. See the ML layer page.Anatomy of a rule
Every rule is the same shape. attestral explain <ID> prints the same fields.
- id: ATL-1NN
title: <imperative problem statement>
severity: critical | high | medium | low | info
target: mcp_server # a component.type prefix, or "model"
match: { attr_missing: auth } # a named, typed matcher
description: <why it's a risk, in one sentence>
recommendation: <the concrete fix>
frameworks: ["OWASP-AgSec TOOL-1", "CIS AWS 2.3.1"]
The matchers are a fixed, typed set (attr_equals, attr_in, attr_missing, attr_starts_with, attr_contains, attr_list_contains, attr_list_any_of, attr_any_contains, and the model-level model_has_both). Each is a named check in engine.py; there is no string execution. See writing rules for the authoring workflow.
Every rule, by pack
The complete list, generated from the packs. Filter by text or severity; open the rule browser or run attestral explain <ID> for the full description and recommendation.
Generated from the packs in attestral/rules/. The GitHub-tracked version, with the same content, is docs/rule-packs.md.