Playbook: Investigate a Failing Gate
Scenario
Your CI check just went red on sicario verify. This playbook walks the
whole investigation: read the human output, map each finding code to its
meaning, switch to machine output, open the evidence file
(generated/sicario/gate-summary.json), read scan_coverage to learn what
was and was not scanned — including the difference between a file the policy
excluded and a file the scanner could not read, and what a
SICARIO-FINDINGS-TRUNCATED overflow means — then fix each cause and re-run
to green.
Intended reader: an engineer with a red check who wants to fix the cause, not silence the finding.
Output conventions
Every quoted output block carries a machine-readable marker declaring it
verified or illustrative. Verified blocks come from the reference run in
this page's front matter and are re-executable as-is against the staged
repository below. Illustrative blocks are visibly labeled; they are used
where counts depend on repository state (for example, file totals that
include .git internals).
Every step is a terminal interaction, captured as text per the terminal-text capture class; the CI check that turned red is only this playbook's premise, and no step directs you to a rendered page or run view, so this playbook carries no screenshots (stated per the visual-asset policy). For wiring and reading the check in CI itself, see the CI playbook.
Prerequisites and starting state
- Python 3.9+ and SicarioSpec 0.6.0. Commands use the module form
python3 -m sicario_cli.cli; ifsicariois on your PATH it is equivalent.gitand a POSIX shell (bash or zsh) on macOS or Linux; on Windows use WSL.
The starting state is a repository staged to fail with several distinct
codes. To reconstruct it exactly (this is also how the reference run built
it): initialize a git repository, run
python3 -m sicario_cli.cli init . --profile public-core, add generated/
to .gitignore, and then:
-
Create a first spec from the shipped template:
mkdir -p specs/001-payment-webhooks && cp .specify/templates/spec-template.md specs/001-payment-webhooks/spec.md, plus a trivialsrc/webhooks.py. -
Add a project rule
.sicario/rules/210-todo-security.rule.jsonthat forbids unresolved security TODO markers (deliberately small caps so this playbook can show truncation):{"id": "ACME-TODO-SECURITY","severity": "medium","kind": "regex-forbidden","path": "src/**/*.py","params": {"pattern": "TODO\\(security\\)","max_findings_per_file": 5,"max_findings_per_rule": 50},"message": "Unresolved security TODO; file a tracked issue or fix before merge","enabled": true} -
Add a small binary file
assets/diagram.png(any non-UTF-8 bytes) and abuild/directory with one file in it. -
Commit this state —
sicario verify .passes on it — then stage three defects as working-tree changes, as if a bad branch arrived: delete the## Misuse / Abuse Casessection fromspecs/001-payment-webhooks/spec.md; deletedocs/security/threat-model.md; and writesrc/jobs.pycontaining a function with nine# TODO(security): validate signer for step Ncomment lines (N from 0 to 8).
Reconstructed exactly as a script (this is also what the docs verification
runner does — see FR-051); src/webhooks.py is trivial filler and
src/jobs.py's nine TODO markers are laid out so the first five the scanner
meets are on lines 2–6, matching the truncation shown in step 5:
git init -q
sicario init . --profile public-core
printf 'generated/\n' >> .gitignore
mkdir -p specs/001-payment-webhooks
cp .specify/templates/spec-template.md specs/001-payment-webhooks/spec.md
mkdir -p src
printf 'def handle_webhook(payload):\n return {"ok": True}\n' > src/webhooks.py
mkdir -p assets
python3 -c "open('assets/diagram.png', 'wb').write(bytes([0x89, 0x50, 0x4E, 0x47, 0x00, 0xFF, 0xFE, 0x01]))"
mkdir -p build
printf 'built\n' > build/output.txt
git add -A
git commit -q -m "chore: staged baseline"
python3 - <<'PY'
import re
from pathlib import Path
p = Path("specs/001-payment-webhooks/spec.md")
text = p.read_text()
text = re.sub(r"\n## Misuse / Abuse Cases\n.*?(?=\n## )", "\n", text, flags=re.S)
p.write_text(text)
PY
rm docs/security/threat-model.md
printf 'def process_jobs():\n' > src/jobs.py
for n in 0 1 2 3 4 5 6 7 8; do
printf ' # TODO(security): validate signer for step %s\n' "$n" >> src/jobs.py
done
printf ' return True\n' >> src/jobs.py
Steps
1. Run the gate locally and read the human output
python3 -m sicario_cli.cli verify .
HIGH SICARIO-MISSING-THREAT-MODEL docs/security/threat-model.md: Missing docs/security/threat-model.md
HIGH SICARIO-SPEC-SECTION specs/001-payment-webhooks/spec.md: spec.md missing required section: Abuse Cases
MEDIUM ACME-TODO-SECURITY src/jobs.py:2: Unresolved security TODO; file a tracked issue or fix before merge
MEDIUM ACME-TODO-SECURITY src/jobs.py:3: Unresolved security TODO; file a tracked issue or fix before merge
MEDIUM ACME-TODO-SECURITY src/jobs.py:4: Unresolved security TODO; file a tracked issue or fix before merge
MEDIUM ACME-TODO-SECURITY src/jobs.py:5: Unresolved security TODO; file a tracked issue or fix before merge
MEDIUM ACME-TODO-SECURITY src/jobs.py:6: Unresolved security TODO; file a tracked issue or fix before merge
MEDIUM SICARIO-FINDINGS-TRUNCATED: Findings truncated for ACME-TODO-SECURITY: reported 5 of 9 occurrence(s) across 1 file(s); 4 suppressed (truncation-scope: per-file; caps: per-file=5, per-rule=50)
sicario verify failed with 8 finding(s)
Exit code 1. Eight findings across four distinct codes. Each finding line
has one anatomy:
SEVERITY CODE path:line: message
SEVERITY(CRITICAL/HIGH/MEDIUM/LOW) ranks the finding for readers; any finding fails the gate regardless of severity.CODEis the stable identifier you map in step 2.path:linelocates the cause; findings with no line concept (a missing file, a missing section) carry just the path, and the truncation finding carries no path at all.- The message is the rule's static text — for
regex-forbiddenrules it never echoes the matched content, so output and evidence cannot leak what was matched.
Four distinct codes are in play: two shipped (SICARIO-MISSING-THREAT-MODEL,
SICARIO-SPEC-SECTION), one project-defined (ACME-TODO-SECURITY), and one
engine-emitted overflow (SICARIO-FINDINGS-TRUNCATED).
2. Map each code to its meaning
-
Shipped
SICARIO-*codes are documented in the finding-code tables ofUSAGE.md(repository root of SicarioSpec) and in the rule engine reference. Here:SICARIO-MISSING-THREAT-MODELmeansdocs/security/threat-model.mdis missing;SICARIO-SPEC-SECTIONmeans a spec lacks a required section — and the message names which (Abuse Cases). -
Project codes (anything not
SICARIO-*, hereACME-TODO-SECURITY) are defined by your own repository. Find the defining rule file — the code is the rule'sid:grep -rl "ACME-TODO-SECURITY" .sicario/rules/Verified output.sicario/rules/210-todo-security.rule.jsonThe rule file gives you the pattern, the target glob, the severity, and the caps — everything the finding meant.
-
SICARIO-FINDINGS-TRUNCATEDis not a defect of its own: it is the engine telling you a findings cap suppressed output. Step 5 reads it properly.
3. Switch to machine output when you need it
python3 -m sicario_cli.cli verify . --format json | head -20
[
{
"severity": "high",
"code": "SICARIO-MISSING-THREAT-MODEL",
"message": "Missing docs/security/threat-model.md",
"path": "docs/security/threat-model.md"
},
{
"severity": "high",
"code": "SICARIO-SPEC-SECTION",
"message": "spec.md missing required section: Abuse Cases",
"path": "specs/001-payment-webhooks/spec.md"
},
{
"severity": "medium",
"code": "ACME-TODO-SECURITY",
"message": "Unresolved security TODO; file a tracked issue or fix before merge",
"path": "src/jobs.py",
"line": 2
},
(head -20 shows the first three of the eight entries; drop it to see the
full document.) Three things to know about the machine formats
(--format json, --format sarif):
-
stdout carries only the document, so you can pipe it straight into
jqor an uploader; the human summary line goes to stderr:Verified outputsicario verify failed with 8 finding(s) -
the exit code (
1failing,0passing) is the authoritative verdict in every format — CI should gate on it, not on parsing text; -
pathandlineare separate fields, so paths stay resolvable file references for SARIF consumers.
4. Open the evidence file: findings versus scan coverage
Every run (any format) writes generated/sicario/gate-summary.json:
python3 -c "
import json
d = json.load(open('generated/sicario/gate-summary.json'))
print('status:', d['status']); print('finding_count:', d['finding_count'])
print('keys:', list(d.keys()))
print('scan_coverage keys:', list(d['scan_coverage'].keys()))
"
status: fail
finding_count: 8
keys: ['generated_at_utc', 'status', 'finding_count', 'findings', 'scan_coverage']
scan_coverage keys: ['skipped_path_set', 'rules', 'disabled_rules', 'overrides', 'asset_root']
Two different kinds of content live here, and keeping them straight is the core reading skill:
findingsis what the verdict is computed from — the same entries the CLI printed, as data.statusandfinding_countsummarize it.scan_coverageis evidence about how the run scanned: per-rule coverage records, the fixed skipped-path policy, rules that were disabled, overrides of shipped rules, and where the shipped rules were loaded from (asset_root). Nothing inscan_coverageever changes the verdict — it exists so a reviewer can see what a green or red verdict actually covered.
5. Read the truncation record: what a cap suppressed
The overflow finding said reported 5 of 9. The coverage record for the
truncated rule carries the exact accounting:
python3 -c "
import json
d = json.load(open('generated/sicario/gate-summary.json'))
recs = {r['rule_id']: r for r in d['scan_coverage']['rules']}
print(json.dumps(recs['ACME-TODO-SECURITY'], indent=2))
"
{
"rule_id": "ACME-TODO-SECURITY",
"kind": "regex-forbidden",
"target": "src/**/*.py",
"files_scanned": 2,
"files_skipped": 0,
"skipped_files": [],
"files_excluded": 0,
"excluded_dirs": [],
"excluded_dirs_total": 0,
"excluded_dirs_truncated": false,
"max_excluded_dirs_recorded": 50,
"files_matched": 1,
"total_occurrences": 9,
"total_matches": 9,
"findings_reported": 5,
"occurrences_suppressed": 4,
"truncated": true,
"truncation_scopes": [
"per-file"
],
"max_findings_per_file": 5,
"max_findings_per_rule": 50
}
How to read a SICARIO-FINDINGS-TRUNCATED situation:
- The caps (
max_findings_per_file,max_findings_per_rule— defaults 20 and 200; this rule sets 5 and 50) bound how many findings are printed, never how many are counted:total_occurrences: 9andoccurrences_suppressed: 4are exact, because counting continues after emission stops. - The overflow finding is mandatory and unsuppressible whenever anything was suppressed, and it inherits the truncated rule's severity — a cap can shorten output, but it cannot make suppression invisible or the run greener.
- Practically: do not fix five TODOs and expect green. The record says nine exist; fix the file, not the printed subset.
6. Excluded is a decision; skipped is a limitation
Now the coverage question a red (or green) gate always raises: what was not scanned? Look at the record for the shipped secret-scan rule, whose target is the whole tree:
python3 -c "
import json
d = json.load(open('generated/sicario/gate-summary.json'))
recs = {r['rule_id']: r for r in d['scan_coverage']['rules']}
r = recs['SICARIO-HARDCODED-SECRET']
keep = ('rule_id','target','files_scanned','files_skipped','skipped_files',
'files_excluded','excluded_dirs','excluded_dirs_total')
print(json.dumps({k: r[k] for k in keep}, indent=2))
"
Illustrative output — the shape and the named paths are exact for the staged
repository; file counts vary with repository state (the .git total in
particular).
{
"rule_id": "SICARIO-HARDCODED-SECRET",
"target": "**/*",
"files_scanned": 118,
"files_skipped": 1,
"skipped_files": [
"assets/diagram.png"
],
"files_excluded": 161,
"excluded_dirs": [
{
"path": ".git",
"files": 158
},
{
"path": "build",
"files": 1
},
{
"path": "generated",
"files": 2
}
],
"excluded_dirs_total": 3
}
These are two different facts, kept deliberately separate:
files_excluded/excluded_dirs— a policy decision. The fixed skipped-path set (recorded once per run asscan_coverage.skipped_path_set:.git,.venv,node_modules,build,dist,generated, and similar) excludes these directories before any read is attempted. Attribution is per excluded directory root with a file count, so you can review the decision: herebuild/andgenerated/are build products, exactly what the policy is for.files_skipped/skipped_files— a scanner limitation. These files were targeted and could not be read or decoded — hereassets/diagram.png, a binary. Each one is listed by name so you can investigate it: a binary image is expected; your.env-style file appearing here would mean the scan you are relying on did not actually read it.
A file that was not scanned is never reported as if it had been cleared — if either number is nonzero, you know precisely which files a green verdict would say nothing about.
7. Fix each cause and watch the count fall
Fix in any order; re-run after each to see the finding disappear. The reference run went:
Restore the deleted threat model (it is tracked, so restore from git — or recreate it):
git restore docs/security/threat-model.md
python3 -m sicario_cli.cli verify . | tail -2
MEDIUM SICARIO-FINDINGS-TRUNCATED: Findings truncated for ACME-TODO-SECURITY: reported 5 of 9 occurrence(s) across 1 file(s); 4 suppressed (truncation-scope: per-file; caps: per-file=5, per-rule=50)
sicario verify failed with 7 finding(s)
Restore the spec's deleted section (again from git; when writing a spec for real, put the section back with real content):
git restore specs/001-payment-webhooks/spec.md
python3 -m sicario_cli.cli verify . | tail -2
MEDIUM SICARIO-FINDINGS-TRUNCATED: Findings truncated for ACME-TODO-SECURITY: reported 5 of 9 occurrence(s) across 1 file(s); 4 suppressed (truncation-scope: per-file; caps: per-file=5, per-rule=50)
sicario verify failed with 6 finding(s)
Resolve the TODO markers — actually resolve them (do the work or file a
tracked issue and reference it), don't reword them into a spelling the
pattern misses. The reference run replaced src/jobs.py with a version
whose docstring references the tracking issue and contains no
TODO(security) markers. Reproducible as:
cat > src/jobs.py <<'PY'
"""Job processing; signer validation tracked in JIRA-1234."""
def process_jobs():
return True
PY
Then:
python3 -m sicario_cli.cli verify .
sicario verify passed
Exit code 0, and the evidence agrees:
python3 -c "
import json
d = json.load(open('generated/sicario/gate-summary.json'))
print('status:', d['status'], '| finding_count:', d['finding_count'])
"
status: pass | finding_count: 0
8. If you cannot fix it now: the legitimate paths
Tempted to silence a finding instead? There are exactly two legitimate paths, and "quietly disable the rule" is not one of them:
- Fix the cause — everything above.
- Record an exception — a row in
docs/risk/security-exceptions.mdwith an owner, an expiry, an approval, and a compensating control, plus the rule change that implements it. Rule overrides and disables are visible by construction: every one is recorded inscan_coverage.overridesin the evidence file, disabled critical rules carry adisables-critical-severity-ruleimpact string reviewers grep for, and none of it changes the verdict in your favor invisibly. The override playbook walks that evidence trail end to end — read it before narrowing or disabling anything.
Success check
You have completed this playbook when:
- You can name all four staged finding codes and where each was defined (shipped rule, project rule, or engine overflow).
python3 -m sicario_cli.cli verify .exits0and printssicario verify passed, andgenerated/sicario/gate-summary.jsonshowsstatus: pass,finding_count: 0.- From the evidence file alone you can state: how many occurrences the
truncated rule really found (9), which file the scanner could not read
(
assets/diagram.png), and which directories policy excluded (.git,build,generated).
Further reading
- Declarative rule engine — finding-code semantics,
caps and truncation, and the full coverage-record schema; the shipped
finding-code tables are in
USAGE.mdat the SicarioSpec repository root. - Playbook: override a shipped rule — the evidence trail for narrowing or disabling a rule.
- Playbook: add a custom project rule — where project
codes like
ACME-TODO-SECURITYcome from.