Assess the repo's code and IaC against ITSG-33 PBMM controls, producing evidence cards and a rolled-up compliance report. Every run is incremental — re-assess only controls whose relevant files changed; reuse cached findings for the rest.
Branch: Init
Trigger: security/itsg33.yaml is absent.
Prompt for system name and system boundary description.
Confirm security profile — default PBMM; accept override.
Detect tracker mode from
git remote -v:Remote contains trackergithub.comgithubdev.azure.comazure-devopsneither localWrite
security/itsg33.yaml:profile: PBMM system_name: <value> system_boundary: <value> tracker: <github | azure-devops | local>For
tracker: azure-devopsonly, also writeado_org,ado_project,ado_repo, parsed from the remote URL:- HTTPS form:
https://[<user>@]dev.azure.com/<org>/<project>/_git/<repo> - SSH form:
git@ssh.dev.azure.com:v3/<org>/<project>/<repo>
ado_org: https://dev.azure.com/<org> ado_project: <project> ado_repo: <repo>Completion: file exists, all four base fields are populated, and (for
tracker: azure-devops)ado_org/ado_project/ado_repoare also populated.- HTTPS form:
Create folder structure:
security/evidence/— one.mdper assessed controlsecurity/gaps/— local gap files (used whentracker: local) Completion: both directories exist.
Continue directly into the Assess branch — no separate invocation needed.
Branch: Assess
Trigger: Every run (including immediately after Init).
Step 1 — Read config
Read security/itsg33.yaml. Accept --profile <value> argument as a run-time override
of profile. Completion: active profile and tracker mode are known.
Step 2 — Fingerprint tech stack
Detect which file patterns are present in the repo:
| Signal | Pattern |
|---|---|
| Terraform | **/*.tf |
| Kubernetes manifests | **/*.yaml, **/*.yml (k8s heuristic: contains apiVersion: and kind:) |
| Helm values | **/values*.yaml |
| Dockerfile | **/Dockerfile* |
| GitHub Actions | .github/workflows/*.yaml |
| Go module | go.mod |
| Node | package.json |
| Python | requirements*.txt, pyproject.toml |
Completion: list of detected signal families recorded (e.g., [terraform, kubernetes, github-actions]).
Step 3 — Load control catalogue
Load controls.md via this context pointer. Completion: all control entries are loaded and available for Step 4.
Step 4 — Dispatch family subagents
Partition controls.md by control family prefix: AC, AU, IA, SC, CM, SI, SA, CP, RA. In a
single message, dispatch one subagent per family (Task/Agent tool, parallel calls) — every
family gets a subagent every run, even if none of its controls changed since the last run.
Each subagent's dispatch prompt must include:
- The active
profile,system_name,system_boundary, and tracker mode (from Step 1) - The detected signal list from Step 2
- A pointer to
controls.md, with an instruction to process only entries whose control ID starts with its assigned family prefix - A pointer to
evidence-card.md(the template to use when writing evidence cards) - A pointer to
finding-rules.md(the rules for weighing Pass/Fail/Not Assessable signals in Step 4c) - The subagent contract below (steps 4a-4e)
Subagent contract (per family)
For each control in the assigned family, in order:
4a. Cache check
Read security/assessment-state.yaml. For each file listed under this control's
files_read, hash the current content and compare to the stored hash. If all hashes match,
this control is cached: set "cached": true on its fragment entry (Step 4's write-up below),
and do not rewrite its evidence card. merge-state.py (Step 5) is the sole authority for a
cached control's finding, confidence, and files_read in assessment-state.yaml — it
always uses the values already stored there, discarding whatever you write for these fields
on a cached control, so you do not need to reproduce them precisely; carrying forward your
best-available copy is enough. The remaining fragment fields (risk_summary,
implementation_approach, evidence_artefacts, client_responsibility) are never read for a
cached control either, since its evidence card is not rewritten — but write-fragment.py still
requires every field to be present, so do not omit them. Use a short placeholder string such as
"(cached — see evidence card)" for the three string fields, and a placeholder list such as
["(cached)"] for evidence_artefacts (any list is accepted; an empty list [] also works).
Skip to next control.
4b. Read relevant files
Glob the control's File patterns against the repo. If no files match any pattern →
finding is Not Assessable; record reason: no matching files; skip to 4d.
4c. Reason
Read the matched files. Apply the control's Pass signals and Fail signals from
controls.md, weighing them per the rules in finding-rules.md (load via
this context pointer).
Derive:
finding: Pass / Fail / Not Assessableseverity: P1 / P2 / P3, written only to the evidence card'sSeverityfield (not part of write-fragment.py's schema); Steps 6–8 read it back from thereconfidence: plain-English note explaining what was found or not foundrisk_summary: one-to-two sentence attacker-perspective statement (from controls.md risk context)implementation_approach: narrative of how the system implements (or fails to implement) the control, citing specific files and config constructsevidence_artefacts: bulleted list of relative file paths with a note on what each demonstratesclient_responsibility: what application teams must do to maintain their side of this controlfiles_read: map of<relative path>→ SHA-256 of file content (used for cache check in 4a and stored in the fragment file)
4d. Record finding Completion criterion: every control in the assigned family has a finding (Pass / Fail / Not Assessable / cached) and a confidence note.
4e. Write evidence card
For each control with a finding (including cached, per 4a's no-rewrite rule), write/update
security/evidence/<control-id>.md using the template at evidence-card.md
(load via this context pointer). Populate all fields from the finding recorded in 4c/4d.
Completion: one .md file per control in the assigned family exists in security/evidence/.
(End of per-control loop.)
Write the family's results as JSON to a scratch input file
security/.assessment-fragments/<family>.input.json:
{
"family": "<family>",
"controls": {
"<control-id>": {
"finding": "<Pass | Fail | Not Assessable>",
"confidence": "<string>",
"risk_summary": "<string>",
"implementation_approach": "<string>",
"evidence_artefacts": ["<relative path>", "..."],
"client_responsibility": "<string>",
"files_read": {"<relative path>": "<sha256>"},
"cached": true
}
}
}
(Omit "cached" entirely for a control that was freshly assessed this run — only set it to
true for a cache hit per Step 4a.)
Then run:
python3 skills/itsg-33-assess/scripts/write-fragment.py <family> \
security/.assessment-fragments/<family>.input.json \
security/.assessment-fragments/<family>.json
If this exits non-zero, its stderr names exactly what is wrong (missing field, invalid finding
value, malformed JSON, invalid files_read hash). Fix the input file and re-run the script — up
to 2 attempts. If it still fails after 2 attempts, this counts as the "malformed output" case in
the failure handling below.
Completion: every control in the assigned family appears exactly once in
security/.assessment-fragments/<family>.json, and the script exited 0.
Failure handling: If a subagent errors, exhausts its 2 write-fragment.py
retry attempts without a clean exit, or produces a fragment missing/duplicating a control, the
orchestrator retries that one family's subagent once. If the
retry also fails, abort the entire run: report which family failed and why, leave
security/assessment-state.yaml and security/evidence/ untouched, and leave
security/.assessment-fragments/ in place for debugging. Do not proceed to Step 5.
Step 5 — Merge state
Once all 9 fragments exist in security/.assessment-fragments/:
Run:
python3 skills/itsg-33-assess/scripts/merge-state.py \ security/.assessment-fragments \ security/assessment-state.yaml \ security/assessment-state.yaml \ <profile>(
<profile>is the profile from Step 1, e.g.PBMM.) This script is the sole writer ofsecurity/assessment-state.yaml, including the PBMM plausibility check (a syntheticPLAUSIBILITY-WARNINGcontrol appended when every SC and every IA control comes back Not Assessable) — nothing is hand-edited into this file afterward. For any control a fragment marks"cached": true, it uses thefinding/confidence/files_readalready stored in the pre-runsecurity/assessment-state.yamlverbatim, discarding whatever the fragment wrote for those fields. For every other control, it takesfinding/confidence/files_readdirectly from the fragment.If this exits non-zero, its stderr names exactly what is wrong: no fragment files found, a control present in more than one fragment, a control (cached or not) missing a required field, a control marked cached with no prior entry to carry forward, or a fragment/existing- state file that isn't valid JSON. Treat this as a "malformed output" case per Step 4's failure handling — if the cause clearly traces to one family's fragment, retry that family's subagent once and re-run this step; otherwise abort the entire run, report why, and leave
security/.assessment-fragments/in place for debugging.Confirm every control in
controls.mdappears in the mergedsecurity/assessment-state.yamlexactly once. (merge-state.pyonly guards against a control appearing in more than one fragment; a control missing from every fragment is not something the script can detect, since it doesn't know the full control catalogue — check this againstcontrols.mddirectly.) A control missing from all fragments is treated as a subagent failure per Step 4's failure handling: retry that family once, then re-run this step.Delete
security/.assessment-fragments/.
Completion: security/assessment-state.yaml contains an entry for every control in
controls.md; the fragment directory no longer exists.
Step 6 — Create gap issues
For each Fail finding, create a gap issue only if no open gap already exists for
that control. Get <P1|P2|P3> for each control from its evidence card's **Severity:**
line (see Step 4c) — it is not present in security/assessment-state.yaml.
GitHub mode (tracker: github):
bash skills/itsg-33-assess/scripts/gh-list-tagged-issues.sh itsg-33:gap
- Skip creation if a returned issue's title matches
[itsg-33:gap] <Control ID> — <Control Name>. - Otherwise, write the issue body (control ID, finding, confidence note, recommended action, link to evidence card) to a scratch file, then:
bash skills/itsg-33-assess/scripts/gh-create-issue.sh \
"[itsg-33:gap] <Control ID> — <Control Name>" \
"itsg-33:gap,<P1|P2|P3>" \
<body-file>
Azure DevOps mode (tracker: azure-devops):
bash skills/itsg-33-assess/scripts/ado-list-tagged-items.sh "<ado_org>" "<ado_project>" itsg-33:gap
- Skip creation if a returned item's title matches
[itsg-33:gap] <Control ID> — <Control Name>. - Otherwise, write the description as simple HTML (
<p>,<ul>,<code>—System.Descriptionis a rich-text HTML field, not Markdown) with the same fields, then:
bash skills/itsg-33-assess/scripts/ado-create-work-item.sh \
"<ado_org>" "<ado_project>" Issue \
"[itsg-33:gap] <Control ID> — <Control Name>" \
"itsg-33:gap; <P1|P2|P3>" \
<description-file>
Local mode (tracker: local):
- Write
security/gaps/<control-id>.mdwith the same fields. - Skip if the file already exists.
If either script exits non-zero, its stderr names exactly what went wrong; report this to the user rather than retrying silently.
Completion: every Fail finding has a gap issue, work item, or gap file; no duplicates created.
Step 7 — Regenerate assessment report (Markdown)
Write/overwrite security/assessment-report.md following the structure at
report-template.md (load via this context pointer). The POA&M's
Severity column comes from each Fail control's evidence card **Severity:** line (Step 4c),
same source as Step 6's gap-issue tags.
Completion: file written; POA&M includes one row per Fail finding; evidence cards index
links to every file in security/evidence/.
Step 8 — Regenerate assessment report (HTML)
Write/overwrite security/assessment-report.html as a self-contained file (inline CSS,
no external resources), using the same structure as report-template.md.
Include:
- Summary dashboard as stat tiles (Pass count in green, Fail in red, Not Assessable in grey)
- Control family breakdown as a simple table
- Top 3 gaps highlighted
- POA&M table with sortable columns (use plain
<table>with<th>— no JS required) - Evidence cards index as a linked list (links to the
.mdfiles)
Completion: security/assessment-report.html exists; opening it in a browser renders
correctly with no external requests.