Run web, API, code, dependency, cloud, AI, and internal-network assessments from one queue with unified findings, evidence, remediation, and audit output.
AI security
Threat models
Deterministic STRIDE and DREAD analysis with attack trees and generated mitigations.
Findings, reports, dashboards, exports, integrations, and retests all read from the same normalized record.
Pencheff favors repeatable checks, then uses AI for triage, enrichment, orchestration, and remediation where it adds signal.
From the Pencheff docs
Threat modeling (STRIDE / DREAD)
/features/threat-modelThreat modeling is the structured exercise of enumerating what could go wrong against a system before or during testing, so scans target the highest-impact paths instead of running every check generically. Pencheff implements two methods:
- STRIDE — categorise threats per asset/component as Spoofing, Tampering, Repudiation, Information Disclosure, Denial of Service, Elevation of Privilege.
- DREAD — score each threat on Damage, Reproducibility, Exploitability, Affected users, Discoverability (1–10 each, average is the priority score).
The deterministic generator lives in
apps/api/pencheff_api/services/threat_model.py (no LLM — the rubric
is a static matrix per asset type).
Threat-model resolution at scan time
Every scan that runs against a URL gets a threat model — you don't have to remember to generate one. The dispatcher follows three rules in order:
| Caller supplies… | Profile | What happens |
|---|---|---|
engagement_id with a model attached | any | The engagement's model is used as-is. summary.threat_model_source = "engagement". |
engagement_id without a model | any | A fly-by DREAD model is generated from the target URL. Not persisted — used for biasing only. summary.threat_model_source = "fly_by". |
| nothing (no engagement_id) | deep | An engagement keyed by deep-{target_id[:8]} is found-or-created in the workspace, a DREAD model is generated and persisted on it. Repeat deep scans of the same target reuse the same engagement. summary.threat_model_source = "auto_engagement". |
| nothing (no engagement_id) | quick, standard, etc. | Fly-by DREAD model from the target URL. Not persisted. summary.threat_model_source = "fly_by". |
Either way, the chosen model drives module_priority_bias, and the
result lands on Scan.summary.threat_model_bias so the worker
reorders + the dashboard explains why a particular module fired
first.
The deep-scan auto-engagement is the load-bearing path for repeatable
work: every --profile deep against https://acme.com lands in the
same engagement, accumulates findings, edits to the threat model
persist across runs, and the operator can promote it to a fully-managed
engagement at any time.
How it integrates
1. Engagement-scoped storage
A threat model is attached to an engagement, not a scan. Generate once per engagement; the model travels with every scan that runs against it.
# Generate from a target URL — the asset type is inferred (api,
# webapp, cloud) from the URL shape.
curl -X POST /engagements/$ENGAGEMENT_ID/threat-model \
-H "Authorization: Bearer $PENCHEFF_API_KEY" \
-d '{
"method": "dread",
"target_url": "https://api.example.com/graphql"
}'
# Or specify assets explicitly
curl -X POST /engagements/$ENGAGEMENT_ID/threat-model \
-d '{
"method": "stride",
"asset_types": ["webapp", "api", "cloud"],
"asset_names": ["www-frontend", "billing-api", "s3-uploads"]
}'
The dashboard's /engagements/[id]/threat-model page renders the
output as a table (or markdown, or raw JSON) and exposes a one-click
Generate / Regenerate / Clear workflow.
2. Adaptive scan profile
When a scan is created against an engagement that has a threat model, the dispatcher computes a module priority bias from the highest- scoring STRIDE categories. Modules tied to those categories run first:
| STRIDE category | Modules biased toward |
|---|---|
| Spoofing | scan_auth, scan_oauth, scan_mfa_bypass |
| Tampering | scan_injection, scan_client_side, scan_api |
| Repudiation | scan_authz, scan_infrastructure |
| Information Disclosure | scan_infrastructure, scan_api, scan_advanced, scan_subdomain_takeover |
| Denial of Service | scan_advanced, scan_infrastructure |
| Elevation of Privilege | scan_authz, scan_oauth, scan_business_logic |
The bias reorders the profile's module list — it never replaces
modules. A scan with an Information Disclosure-heavy threat model
runs scan_infrastructure before scan_injection; the same profile
on an Elevation of Privilege-heavy model runs scan_authz first.
The chosen bias is stamped onto Scan.summary.threat_model_bias at
creation time so the dashboard can display why a particular module
fired first.
3. ThreatModelAgent in the swarm
A new BreakerSpec — ThreatModelAgent — runs in parallel with the
attack breakers during the swarm's Phase 2. Its job is not to fire
scanners; it reads the recon snapshot and other breakers' findings
and produces an INFO-severity finding summarising:
- Which STRIDE categories have the most evidence in this scan.
- Which threats from the engagement's threat model are now confirmed vs. still hypothetical.
- Recommended hardening priorities specific to this target.
The agent has no exclusive scan tools — it relies on the shared
get_findings and test_endpoint tools so it stays a "lens", not a
"probe". This avoids double-firing scanners that other breakers already
own.
Output shape
{
"method": "DREAD",
"generated_at": "2026-05-08T01:34:35Z",
"method_summary": "DREAD: each threat scored on Damage, ...",
"assets": [
{"name": "https://api.example.com/graphql", "type": "api"}
],
"threats": [
{
"asset": "https://api.example.com/graphql",
"category": "Information Disclosure",
"threat": "Excessive data exposure",
"damage": 6, "reproducibility": 8, "exploitability": 7,
"affected_users": 7, "discoverability": 8,
"score": 7.2,
"priority": "high",
"mitigations": ["TLS everywhere", "Field-level encryption", ...]
}
],
"category_scores": {
"Information Disclosure": 7.2,
"Elevation of Privilege": 6.6,
"Tampering": 6.4
}
}
Viewing a scan's threat model
Every scan that resolved a persisted model surfaces a § Threat model
section on its assessment page (/scans/<id>) with a one-click link to
the full STRIDE / DREAD render at /scans/<id>/threat-model. The
scan-scoped page reads from a scan-scoped endpoint
(GET /scans/{id}/threat-model) and shows the prioritised threats,
DREAD score table, and category scores — no other state from the
underlying storage container leaks.
The link only appears when the scan has a persisted model
(summary.threat_model_source ∈ {engagement, auto_engagement}).
Fly-by models — produced by quick / standard / other non-deep
profiles — live only on summary.threat_model_bias for module
priority biasing and are not linkable since there is nothing durable
to fetch.
Endpoints
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /scans/{id}/threat-model | scans:read | Scan-scoped read. Returns the persisted model attached to this scan, or 404 if the scan only has a fly-by model. |
GET | /engagements/{id}/threat-model | engagements:read | Read current model + the computed module bias |
POST | /engagements/{id}/threat-model | engagements:write | Generate from target_url / asset_types / asset_names |
PUT | /engagements/{id}/threat-model | engagements:write | Replace the model JSONB (operator edits) |
DELETE | /engagements/{id}/threat-model | engagements:write | Clear the model — adaptive bias stops |
Report inclusion
When a scan against an engagement with a threat model produces a
markdown report, a ## Threat model section is rendered between the
executive summary and the findings table. Operators get the threat
model and the findings side-by-side in a single deliverable.
CLI parity
The local pencheff threatmodel command (pencheff threatmodel --method stride|dread) uses the same matrix as the API service — running it
locally produces a model in the same shape, so the output of one can
be loaded into the other via PUT /engagements/{id}/threat-model.
Related