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:

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.

BandDomainRulesLives in

Why the packs are shaped this way

The layout encodes the product strategy, a barbell:

Language-based risk (injection, tool poisoning) is intentionally not in these packs. It is not a flag on a component, it is in the words, so it lives in the ML layer (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.