Threat-model generator
pipeline_check --output threatmodel emits a self-contained Markdown threat-model document populated from the same scan output the JSON / HTML / SARIF reporters consume: findings, the component inventory, and any matched attack chains. Selecting the format auto-enables the inventory pass so a one-flag invocation produces a populated document.
Why STRIDE?
The OWASP CICD Top 10 mapping every rule already carries is the right vocabulary for a CI/CD audience but not the one auditors and threat modelers prefer. STRIDE has been the lingua franca of threat-modeling docs since Microsoft introduced it in 1999, and most compliance frameworks (SOC 2 CC, PCI 6.5, NIST SSDF PW.1) speak it natively.
The mapping is mechanical. There's nothing the reporter knows that isn't already in the rule registry. STRIDE classification is derived per-finding at report time, so re-policing is one table swap.
Quick start
# Single provider
pipeline_check --pipeline gitlab --gitlab-path .gitlab-ci.yml \
--output threatmodel --output-file threatmodel.md
# Multi-provider (cross-provider chains land in the document too)
pipeline_check --pipelines github,oci --output threatmodel
Document layout
# Threat Model
## Scope providers in scope, scorer summary
## Trust boundaries heuristics keyed off provider mix
## Assets the inventory, grouped by (provider, type)
## STRIDE analysis failing findings bucketed
### S — Spoofing
### T — Tampering
### R — Repudiation
### I — Information Disclosure
### D — Denial of Service
### E — Elevation of Privilege
## Attack chains optional, only when chains matched
## Implemented controls passing-check counts per STRIDE bucket
## Risk register top 25 failing findings, flat table
## Methodology short footer pointing at the policy
Sample output
Running against the GitLab insecure fixture in this repo:
# Threat Model
_Generated by pipeline-check v1.0.4 on 2026-05-12 14:30 UTC._
## Scope
**Providers in scope:** gitlab (1)
**Region:** `us-east-1`
**Grade:** D
**Score:** 0/100
**Failed checks:** 35
**Passing controls:** 0
Severity breakdown:
- **CRITICAL:** 6 failed, 0 passing
- **HIGH:** 15 failed, 0 passing
- **MEDIUM:** 13 failed, 0 passing
- **LOW:** 1 failed, 0 passing
## Trust boundaries
- Pull-request author -> CI runner. Untrusted source-tree contents
(PR-controlled YAML, scripts, dependency manifests) cross into a
runner that holds CI secrets and (in privileged trigger modes)
write-scope tokens.
- CI runner -> registry. Built artifacts (container images,
packages, OCI manifests) cross from the runner into a registry
whose downstream consumers trust the produced bytes.
## STRIDE analysis
### T -- Tampering
_Integrity of input, code, dependencies, or artifacts. Attacker
modifies what flows through the pipeline._
| Threat | Severity | Affected | Mitigation |
|-------------------------------------------------|----------|----------|-----------------------------------------|
| `GL-010` Multi-project pipeline ingests upstream artifact unverified | CRITICAL | 1 | Add a verification step before... |
| `GL-002` Script injection via untrusted commit/MR context | HIGH | 1 | Read these values into intermediate... |
| ... | | | |
(The complete document is ~250 lines; see the sample output above for one rendered against the GitLab fixture.)
Mapping policy
The OWASP CICD Top 10 to STRIDE primary table:
| OWASP | STRIDE primary | STRIDE secondary |
|---|---|---|
CICD-SEC-1 Insufficient Flow Control |
T | E |
CICD-SEC-2 Inadequate IAM |
S | E |
CICD-SEC-3 Dependency Chain Abuse |
T | |
CICD-SEC-4 Poisoned Pipeline Execution |
T | E |
CICD-SEC-5 Insufficient PBAC |
E | |
CICD-SEC-6 Credential Hygiene |
I | S |
CICD-SEC-7 Insecure System Config |
E | D |
CICD-SEC-8 Ungoverned 3rd-Party |
T | E |
CICD-SEC-9 Improper Artifact Integrity |
T | |
CICD-SEC-10 Insufficient Logging |
R |
CWE refinements that prepend to the head:
| CWE | Promotes to | Why |
|---|---|---|
CWE-200 |
I | generic info exposure |
CWE-522 |
I | insufficiently protected creds |
CWE-552 |
I | files / dirs accessible externally |
CWE-798 |
I | hardcoded credentials |
CWE-287 |
S | improper authentication |
CWE-290 |
S | auth bypass by spoofing |
CWE-345 |
T | integrity check missing |
CWE-78 |
T | OS command injection |
CWE-77 |
T | generic command injection |
CWE-494 |
T | download of code without integrity |
CWE-829 |
T | functionality from untrusted sphere |
CWE-1357 |
T | reliance on uncontrolled component |
CWE-400 |
D | uncontrolled resource consumption |
CWE-770 |
D | alloc without limits |
CWE-269 |
E | improper privilege management |
CWE-250 |
E | execution with unnecessary privilege |
CWE-778 |
R | insufficient logging |
Findings with no OWASP and no CWE tags default to Tampering, the most common CI/CD failure mode.
Use cases
- SOC 2 / PCI evidence package: attach
threatmodel.mdnext to the scan JSON. Auditors get a STRIDE-shaped narrative they can read directly; engineers get the JSON for tooling. - Architecture review: paste into a Confluence / Notion page as a starting draft. The Assets and trust-boundary sections give reviewers a concrete map of what's in scope.
- Quarterly posture review: regenerate against the latest scan, diff against the prior quarter to see which STRIDE buckets gained / lost open risks.
Why no rule changes
The reporter never mutates the rule registry. STRIDE classification is a pure function of each finding's existing OWASP and CWE tags, evaluated at report time. The two policy tables (_OWASP_TO_STRIDE and _CWE_PREPEND in pipeline_check/core/threatmodel_reporter.py) are the single point where the policy lives, so re-policing for a different STRIDE flavor (LINDDUN, PASTA categories) is one table swap, not a rule-by-rule re-tag.