site-docs/features/iac-exceptions-policy.md
Rama main · réplica de lectura del repositorio original.
# Exceptions and policy for IaC
Platform and AppSec teams need two things from an IaC gate: block risky
changes, and let a specific, explained, by-design case through without
switching the rule off for everyone. agent-bom has one exception model for IaC
findings, whichever way the exception is declared, and a policy file that can
allow, warn or block by rule, resource and environment.
Every accepted exception:
- needs a **reason**;
- records **where it was declared** (`source` and `origin`), and optionally an
**owner** and an **expiry**;
- leaves the active findings and every gate (`--fail-on-severity`, policy
`block`), but **stays in the output**: the JSON `iac_findings.suppressed`
list, SARIF `suppressions` (shown as dismissed in code-scanning UIs), the HTML
report and the console summary (`N accepted exception(s)`);
- **stops applying the day after it expires**: the finding is active again and
the scan prints a warning. Within 14 days of expiry the scan prints a notice.
A malformed exception never suppresses anything. A bad ignore-file entry or
policy rule stops the scan before anything is scanned, with an error naming the
entry, whether or not the run includes IaC paths; an inline marker with a
malformed `expires=` is reported and not applied.
## Where exceptions come from
| Source | Declared in | Who controls it | Recorded as |
|---|---|---|---|
| Inline marker | the IaC file, on or above the line | anyone who can commit the file | `source: inline`, `origin: path:line` |
| Ignore file | `.agent-bom-ignore.yaml` or `--ignore-file` | repository owners | `source: ignore_file`, `origin: file#entry` |
| Policy `allow` rule | `--policy` file | whoever owns the policy | `source: policy`, `origin: policy:name#rule` |
| Tenant exception | `POST /v1/exceptions`, approved by a second admin | the platform's admins | `source: tenant`, `approved_by` |
### Inline marker
Put the marker on the offending line or the line directly above it. `expires=`
and `owner=` are optional and come before the reason:
```yaml
securityContext:
# agent-bom: ignore[K8S-001] expires=2026-12-31 owner=platform-team node debugger needs host access
privileged: true
```
Several rule ids can share a marker: `ignore[K8S-001,K8S-006]`. A marker with
no reason suppresses nothing.
### Ignore file
`agent-bom iac` and `agent-bom scan --iac` read `.agent-bom-ignore.yaml` from
the working directory, or the file given with `--ignore-file`. An entry is an
IaC entry when it has `rule`, `resource` or `type: iac`, or when its `id` is an
IaC rule id (`K8S-001`, `TF-SEC-012`, `DOCKER-003`, ...). Vulnerability entries
in the same file keep working as before.
```yaml
ignores:
# One rule, one directory, until a date
- rule: K8S-001
path: "deploy/dev/**"
reason: "Debug pods in dev need privileged mode"
owner: platform-team
expires: 2026-12-31
# One rule on one declared resource
- rule: K8S-006
resource:
kind: Deployment
name: legacy-batch
namespace: jobs
labels: {team: data}
reason: "Writes scratch files to the root filesystem; tracked in DATA-142"
expires: 2027-03-31
# The vulnerability form still works
- id: CVE-2024-1234
reason: "Not exploitable: no user input reaches the parser"
```
| Field | Required | Meaning |
|---|---|---|
| `rule` (or `id`) | yes | IaC rule id, or a glob such as `K8S-*`. A bare `*` needs a `path` or `resource`. |
| `reason` | yes | Why this is accepted. Carried into every export. |
| `path` | no | Glob on the finding's path, relative to the scanned directory or to the working directory. |
| `resource` | no | `kind`, `name`, `namespace` (globs) and `labels` (all must be present). Matches only findings whose resource the parser knows (Kubernetes manifests and rendered Helm charts); it never matches a finding without one. |
| `expires` | no | `YYYY-MM-DD`; the exception applies through that day. |
| `owner` | no | Team or person accountable for the exception. |
Unknown keys are rejected so a typo cannot quietly widen or disable an entry.
### Tenant exceptions (API)
On a self-hosted control plane, IaC exceptions use the same request and
four-eyes approval workflow as vulnerability exceptions, scoped to the caller's
tenant:
```bash
curl -X POST "$AGENT_BOM_URL/v1/exceptions" -H "X-API-Key: $REQUESTER_KEY" \
-H 'Content-Type: application/json' -d '{
"iac_rule_id": "K8S-001",
"iac_path": "deploy/ops/*",
"iac_resource": {"kind": "Pod", "name": "node-debugger"},
"reason": "Node debugger needs host access (CAB-1182)",
"expires_at": "2026-12-31T00:00:00Z"
}'
curl -X PUT "$AGENT_BOM_URL/v1/exceptions/$ID/approve" -H "X-API-Key: $SECOND_ADMIN_KEY"
```
An IaC exception needs an exact rule id, a path or resource scope, a reason and
a bounded, timezone-aware expiry; the requester cannot approve it. Only
approved, unexpired exceptions of the scan's tenant are applied when the API
scans a repository, and when the MCP `scan` tool scans a project directory
(the tenant the MCP server is bound to with `AGENT_BOM_MCP_TENANT_ID`). A revoked
or expired one stops applying at the next scan. These repository scans honour
inline markers and tenant exceptions; they do not read an ignore file from the
scanned repository. If the exception store is unavailable, no tenant exception
applies and the scan records a warning.
## Policy-as-code for IaC
A policy rule is an IaC rule when it sets `"scope": "iac"` or any `iac_*`
condition. IaC rules are never evaluated against vulnerabilities, and the
existing vulnerability rules are never evaluated against IaC findings.
| Condition | Matches |
|---|---|
| `iac_rule` | rule id or glob (`K8S-001`, `K8S-*`) |
| `severity_gte` | `CRITICAL`, `HIGH`, `MEDIUM`, `LOW` |
| `iac_path` | glob on the finding's path |
| `iac_resource_kind`, `iac_resource_name`, `iac_namespace` | globs on the declared resource |
| `iac_labels` | labels the resource must carry |
| `iac_environment` | the finding's environment (below) |
A finding's environment comes from the policy's `environments` map (path globs
per environment) first, then from an `environment` or `env` label on the
resource. A finding with no environment matches no `iac_environment` rule, so
map every deploy directory you want a rule to cover.
| Action | Effect |
|---|---|
| `block` (or `fail`) | fails the scan (exit 1) |
| `warn` | reported, does not fail |
| `allow` | accepts the finding as an exception: needs `reason`, takes optional `owner` and `expires`, and is recorded with `source: policy` |
A `block` rule with `"waivable": false` cannot be bypassed from the repository:
inline markers, ignore-file entries and policy `allow` rules that cover a
finding it matches are rejected (the scan prints `rejected exception`), and the
finding blocks. Only an approved tenant exception can waive it.
Example: block privileged pods in prod, warn in dev, and accept the dev debug
pod explicitly.
```json
{
"name": "iac-guardrails",
"environments": {
"prod": ["deploy/prod/**", "k8s/overlays/prod/**"],
"dev": ["deploy/dev/**", "k8s/overlays/dev/**"]
},
"rules": [
{
"id": "no-privileged-prod",
"description": "Privileged pods are blocked in prod",
"iac_rule": "K8S-001",
"iac_environment": "prod",
"action": "block",
"waivable": false
},
{
"id": "dev-debugger-ok",
"iac_rule": "K8S-001",
"iac_environment": "dev",
"iac_resource_name": "node-debugger",
"action": "allow",
"reason": "Dev node debugger runs privileged by design",
"owner": "platform-team",
"expires": "2027-01-31"
},
{
"id": "privileged-dev-warn",
"description": "Other privileged pods in dev are reviewed",
"iac_rule": "K8S-001",
"iac_environment": "dev",
"action": "warn"
}
]
}
```
```bash
agent-bom iac deploy/ --policy iac-guardrails.json --fail-on-severity high -f sarif -o iac.sarif
```
## Reading the result
```json
"iac_findings": {
"total": 5,
"findings": [ ... active findings, the only ones gates see ... ],
"suppressed": [
{
"rule_id": "K8S-001",
"file_path": "dev/pod.yaml",
"resource": {"kind": "Pod", "name": "node-debugger", "namespace": "dev", "labels": {}},
"suppressed": true,
"suppression": {
"state": "accepted_risk",
"reason": "Debug pods in dev need privileged mode",
"source": "ignore_file",
"origin": ".agent-bom-ignore.yaml#1",
"owner": "platform-team",
"expires": "2026-12-31",
"expiring_soon": false,
"approved_by": null,
"exception_id": "iacx-..."
}
}
],
"exceptions": {"accepted": 1, "expired": 0, "expiring_soon": 0, "invalid": 0, "rejected": 0,
"by_source": {"ignore_file": 1}, "notices": []},
"policy": {"policy_name": "iac-guardrails", "failures": [], "warnings": [], "allowed": [], "passed": true}
}
```
Use `--verbose` on the console to list each accepted exception with its reason,
owner and expiry.
## Security notes
Exceptions are a place to hide risk, so the design keeps them auditable:
- no exception without a reason, and nothing malformed is ever applied;
- every accepted finding stays in JSON, SARIF and HTML with who/where/why/until;
- expiry is enforced on every scan, not only when someone remembers;
- `waivable: false` keeps a repository author from waiving an org rule in code;
- tenant exceptions need a second admin, an exact rule and scope, and a bounded
expiry, and every read and write is scoped to the caller's tenant.
See also: [Cloud Posture and IaC Gates](cloud-posture.md),
[Policy Engine](policy.md).