# Module S07 — Threat Modeling Harnesses

**Course**: 2A — Building AI Harnesses for Cybersecurity
**Module**: S07 — Threat Modeling Harnesses
**Duration**: 60 minutes
**Level**: Senior Engineer and above
**Prerequisites**: S00–S06 complete

> *A harness that ingests architecture as-written — draw.io, Terraform, OpenAPI — converts it to a data flow diagram, runs STRIDE per element with LLM-generated threats, and emits prioritized mitigations as tracked issues. Threat modeling that happens at the speed of architecture change, not the speed of a quarterly meeting.*

---

## Learning Objectives

1. Build an architecture-to-DFD ingestion pipeline that parses draw.io XML, Terraform, CloudFormation, and OpenAPI into a structured data flow diagram with trust boundaries.
2. Implement a STRIDE analysis engine that applies the relevant threat categories per DFD element, generates candidate threats with LLM-driven attack narratives, and deduplicates across the model.
3. Generate per-threat mitigations mapped to OWASP controls, CWE remediations, and cloud best practices, and auto-create issues in GitHub/Linear/Jira.
4. Version threat models and diff two architecture versions to identify what changed and what new threats the change introduced.

---

# S07.1 — Architecture Ingestion

Threat modeling fails in most organizations because it is manual. An architect draws a diagram in a quarterly meeting, a security engineer writes a spreadsheet of threats, the diagram goes stale the day someone merges a Terraform change, and nobody re-runs the model. A threat modeling harness fixes the root cause: the model must be generated from the architecture as-written, and regenerated whenever the architecture changes.

## Input sources

The harness ingests architecture from the artifacts the engineering team already maintains:

| Source | What it gives you | Parser |
| --- | --- | --- |
| **draw.io / diagrams.net XML** | Boxes-and-lines architecture diagrams | XML parser; shape metadata → components, arrows → data flows |
| **Mermaid diagrams** | Architecture-as-code diagrams | Mermaid AST parser |
| **Terraform / CloudFormation** | Infrastructure as declared | Resource graph; resources → components, references → data flows |
| **OpenAPI / Swagger specs** | API endpoints, request/response schemas | Spec parser; endpoints → external entry points, schemas → data structures |
| **Source structure** | Service boundaries, module graph | Directory + import analysis |

The strongest ingestion path is IaC (Terraform/CloudFormation) plus OpenAPI, because those are machine-verifiable declarations of what is actually deployed. Draw.io diagrams are useful but they are documentation, not source of truth — they drift. The harness treats IaC as the authoritative input and diagrams as supplementary context.

## The architecture-to-DFD pipeline

Every input source normalizes to a structured Data Flow Diagram representation:

```typescript
interface DFD {
  elements: DFDElement[];
  flows: Flow[];
  trust_boundaries: TrustBoundary[];
  metadata: { source: string; version: string; generated_at: string };
}

interface DFDElement {
  id: string;
  name: string;
  type: "external_actor" | "process" | "data_store" | "service";
  trust_boundary_id: string;
  technologies: string[];   // e.g. ["S3", "lambda", "postgres"]
  exposes: string[];        // entry points (API endpoints, ports)
}

interface Flow {
  id: string;
  source: string;           // element id
  target: string;           // element id
  data: string[];           // what flows: ["PII", "credentials", "files"]
  protocol: string;         // "HTTPS", "TCP", "event"
  crosses_boundary: boolean;
}
```

The DFD is the canonical intermediate representation. Every downstream stage — STRIDE, mitigation, versioning — operates on the DFD, not on the raw input. This decouples ingestion from analysis: a new input source (e.g. a CDK construct library) only needs a new parser that emits DFD elements and flows.

## Component extraction

The parser's first job is to identify components — the nodes in the DFD. From Terraform, every resource is a component: an `aws_s3_bucket` is a data store, an `aws_lambda_function` is a process, an `aws_api_gateway_rest_api` is a service with external entry points. From OpenAPI, every endpoint group maps to a service; the request schemas describe the data that flows in. The extraction is mechanical — a resource-to-element-type mapping table — but the flow extraction requires resolving Terraform references, because a `aws_lambda_permission` block that grants API Gateway invocation is what establishes the data flow between the API and the Lambda.

```python
# Resource type → DFD element type mapping (the mechanical part)
RESOURCE_TYPE_MAP = {
    "aws_s3_bucket": ("data_store", ["S3"]),
    "aws_lambda_function": ("process", ["lambda"]),
    "aws_api_gateway_rest_api": ("service", ["api_gateway"]),
    "aws_rds_instance": ("data_store", ["postgres", "rds"]),
    "aws_dynamodb_table": ("data_store", ["dynamodb"]),
    "aws_sqs_queue": ("data_store", ["sqs"]),
    "aws_cloudfront_distribution": ("service", ["cloudfront"]),
}

def terraform_to_dfd(tf_plan: dict) -> DFD:
    elements, flows = [], []

    for res in tf_plan["resource_changes"]:
        rtype = res["type"]
        if rtype not in RESOURCE_TYPE_MAP:
            continue  # unsupported resources are skipped, not guessed
        elem_type, tech = RESOURCE_TYPE_MAP[rtype]
        attrs = res["change"]["after"]
        elements.append(DFDElement(
            id=res["address"],
            name=attrs.get("name", res["address"]),
            type=elem_type,
            trust_boundary_id=detect_boundary(rtype, attrs),  # see below
            technologies=tech,
            exposes=detect_entry_points(rtype, attrs),
        ))

    # Flow extraction: resolve references between resources.
    # aws_lambda_permission.source_arn → flow from API Gateway to Lambda
    # aws_lambda_function.environment  → marks credentials flowing into the process
    for res in tf_plan["resource_changes"]:
        flows.extend(extract_flows_from_references(res, elements))

    return DFD(elements=elements, flows=flows,
               trust_boundaries=derive_boundaries(elements),
               metadata={"source": "terraform", "version": tf_plan.get("version", "?")})
```

The `extract_flows_from_references` function is where the real work lives. It walks each resource's attribute references — `source_arn`, `role_arn`, `queue_arn`, `event_source_arn` — and emits a `Flow` between the referencing element and the referenced element. A `data` field annotates what flows: `credentials` when the flow is a role assumption, `events` when it is a queue subscription, `PII` when the source schema is tagged as such (inferred from the bucket name or a tagging convention). The reference graph *is* the data flow graph; Terraform already encodes it, the harness just reads it.

## Trust boundary detection

A trust boundary is a line across which the level of trust changes — the edge between the public internet and your VPC, between a low-trust tenant and a high-trust admin service, between a third-party SaaS and your internal network. Trust boundaries are where the most dangerous threats live, because they are where an adversary can cross from a low-trust zone into a high-trust one. Shostack's *Threat Modeling: Designing for Security* makes the case explicitly: a threat that crosses a trust boundary is a higher-priority finding than the same threat contained within one, because the boundary crossing is the precondition for privilege escalation.

The harness detects trust boundaries from the infrastructure declaration:

- **Internet-facing resources** (resources with a public IP, an internet gateway, or a public-load-balancer listener) establish a boundary between the public internet and the VPC.
- **IAM role boundaries** (a Lambda assuming a cross-account role) establish a boundary between accounts.
- **Tenant isolation constructs** (a per-tenant database schema, a tenant-scoped IAM policy) establish boundaries between tenants.

Each DFD element is assigned to a trust boundary. Each flow is marked `crosses_boundary` if its source and target are in different boundaries. The STRIDE engine uses these crossings as its highest-priority analysis targets.

### Boundary detection is harder than it looks

The three rules above are the easy cases. The hard cases are where boundaries are *implicit* — present in the architecture's security model but not declared in a single resource attribute. The harness must infer them:

- **VPC subnet tiers.** A public subnet, a private subnet, and an isolated subnet are three trust zones, but Terraform marks them only as `aws_subnet` resources with different route tables. The harness infers the tier from the route table: a subnet whose route table has a `0.0.0.0/0` via an internet gateway is public; via a NAT gateway is private; with no default route is isolated. This is a three-zone boundary, not one, and collapsing it into "VPC vs internet" misses the private-to-isolated escalation path.

- **Service-to-service trust.** Two microservices in the same VPC may still have different trust levels — a billing service that handles PII and a notifications service that sends emails. The boundary between them is a *logical* boundary, not a network one. The harness infers it from IAM: if the billing service's role cannot be assumed by the notifications service, there is an authorization boundary between them even though they share a network. This connects directly to S06.3's semantic codebase index — the same embedding-based retrieval that finds cross-file taint flows can retrieve the IAM policy that defines the service-to-service boundary.

- **Implicit boundaries from data classification.** A data store tagged `sensitive=true` and one tagged `internal=true` are in different trust zones even if they are both RDS instances in the same subnet. The harness respects tagging conventions as boundary markers — but only if the team's tagging is disciplined. A harness that trusts a `sensitive` tag on a bucket that also has a public-read ACL is trusting a contradiction; the harness flags the contradiction as a finding, not silently picks one.

The failure mode to watch for is the *phantom boundary*: a boundary the harness detects that does not reflect a real trust difference. A common example is a VPC peering connection — the harness sees a flow between two VPCs and marks it boundary-crossing, but if both VPCs are owned by the same team with the same trust model, the crossing is a network artifact, not a trust boundary. The harness should mark these as *candidate* boundaries and let an architect confirm or reject them. Treating every detected boundary as confirmed produces noise; treating every boundary as candidate-and-confirmable produces a reviewable model.

### Boundary detection connects downstream

The trust boundary is the hinge between ingestion and analysis. Get it wrong and every downstream stage inherits the error: STRIDE analyzes the wrong crossings, mitigations target the wrong zone, and the version diff flags the wrong deltas. The rule of thumb: a boundary model that an architect looks at and says "yes, that matches how we think about trust" is correct enough to proceed. A boundary model that requires explanation is a signal to invest in the detection rules before running STRIDE. This is the same principle as S08.1's control-plane shift — the boundary model is a creation-time artifact that must be correct before the analysis gate fires, not a review-time output that gets corrected later.

---

# S07.2 — STRIDE Analysis Engine

STRIDE is a mnemonic for six categories of threats: **S**poofing, **T**ampering, **R**epudiation, **I**nformation disclosure, **D**enial of service, **E**levation of privilege. The framework's strength is not the taxonomy itself — it is that each category maps to specific DFD element types, giving you a systematic method rather than an open-ended brainstorm.

## STRIDE per element

Each DFD element type is susceptible to a specific subset of STRIDE:

| Element type | Applicable STRIDE categories |
| --- | --- |
| **External actor** | Spoofing, Repudiation |
| **Process / service** | Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege (all six) |
| **Data store** | Tampering, Repudiation, Information disclosure, Denial of service |
| **Data flow** | Tampering, Information disclosure, Denial of service |

The engine applies the applicable categories to each element. A data store is not analyzed for spoofing (you don't impersonate a database); an external actor is not analyzed for elevation of privilege (it is already outside the trust boundary). This scoping prevents the engine from generating irrelevant threats.

### A worked STRIDE pass on one element

Consider a `payments-api` Lambda that reads from a `transactions` Postgres table and is invoked by API Gateway. It is a `process` element, so all six STRIDE categories apply. Walking the matrix by hand is what the engine does programmatically:

| Category | Candidate threat (grounded) | Why it applies here |
| --- | --- | --- |
| **Spoofing** | An attacker obtains the API Gateway's invocation ARN and invokes the Lambda directly via `lambda:Invoke`, bypassing the gateway's auth and WAF | Lambda has no intrinsic caller auth — it trusts the gateway; the boundary crossing is gateway→Lambda |
| **Tampering** | A compromised Lambda role writes a forged row to `transactions` because the role has blanket `rds:Connect` with no row-level constraint | The process writes to the data store; no integrity control on the write path |
| **Repudiation** | The Lambda mutates balances without emitting a CloudWatch event; a disputed transaction has no audit trail | Process performs a state-changing action with no non-repudiation log |
| **Information disclosure** | The Lambda logs the full request body (including card PAN) to CloudWatch; the log group is readable by a broad `ReadOnly` role | Sensitive data flows through the process and leaks via its side channel |
| **Denial of service** | An attacker calls the Lambda's public route with an expensive query that exhausts the Lambda's 15-minute timeout, driving up concurrency and exhausting the account's burst pool | Internet-facing process with no rate limit behind it |
| **Elevation of privilege** | The Lambda's execution role has `s3:*` on `*/*`; a logic flaw that lets an attacker control the S3 key lets them read arbitrary objects, not just the intended export bucket | Process holds privileges broader than its function requires |

Each row is a (element, category) pair that the LLM threat generator produces. The grounding column is the load-bearing part — without the specific technologies (Lambda, API Gateway, Postgres, CloudWatch) and the specific boundary (internet→VPC→data store), the same six categories produce six generic statements. With grounding, each threat points at a concrete failure mode an engineer can recognize and fix.

## LLM-powered threat generation

For each (element, category) pair, the engine generates candidate threats. A pure-template generator produces generic statements ("an attacker could spoof the API gateway"). The LLM-driven generator produces specific, contextual threats grounded in the element's technologies, the flows crossing its boundary, and the data involved:

```python
async def generate_threats(element: DFDElement, category: str, flows: list[Flow]) -> list[Threat]:
    prompt = f"""
    You are a threat modeling analyst applying STRIDE.

    Element: {element.name} (type: {element.type})
    Technologies: {element.technologies}
    Trust boundary: {element.trust_boundary_id}
    STRIDE category: {category}
    Flows crossing this element's boundary:
    {format_flows(flows)}

    Generate candidate threats in this category that are SPECIFIC to this element,
    its technologies, and the data that flows through it. For each threat:
    - A concrete attack narrative (how would an adversary do this?)
    - The data or asset at risk
    - A draft CVSS base vector (AV/AC/PR/UI/S/C/I/A)

    Do NOT produce generic threats. Ground each in the actual technologies and flows.
    Respond as JSON list.
    """
    result = await model.complete(prompt)
    return [Threat(**t, element_id=element.id, category=category) for t in json.loads(result)]
```

The grounding is the load-bearing instruction. Without it, the model produces "an attacker might spoof authentication" for every service — useless. With the specific technologies (S3, Lambda, Cognito) and the specific flows (PII crossing the internet-to-VPC boundary), the model produces "an attacker with stolen Cognito user pool credentials could call the Lambda directly, bypassing the API gateway's rate limiting and WAF rules — draft CVSS AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:L."

## Deduplication and prioritization

A full DFD with 20 elements and an average of 3 categories each produces 60 (element, category) pairs, each yielding 2–5 threats — potentially 200+ candidate threats. Many are duplicates (the same underlying weakness described from two angles) or near-duplicates (the same threat against two similar services).

The engine deduplicates via semantic similarity: each generated threat is embedded, and threats above a similarity threshold are merged, retaining the highest-severity phrasing. After dedup, the engine prioritizes by a draft CVSS score derived from the model's base vector. The output is a ranked list — the top threats first — not a flat dump.

## Traceability

Every threat carries `element_id` and `category`. This traceability is not decorative. When a developer asks "why did you flag this?", the harness points to the specific DFD element and the specific flow that generated the threat. When the architecture changes and the element is removed, the threats against it are automatically retired. Traceability is what makes the model maintainable across versions.

---

# S07.3 — Mitigation Generation and Issue Tracking

A threat list without mitigations is a complaint. The harness generates a per-threat mitigation, maps it to authoritative controls, tracks its status, and — critically — files it where the engineering team already works.

## Mitigation mapping

For each threat, the harness maps to three layers of remediation guidance:

```python
async def generate_mitigation(threat: Threat) -> Mitigation:
    prompt = f"""
    Threat: {threat.attack_narrative}
    Element: {threat.element_id} (technologies: {threat.technologies})
    CVSS draft: {threat.cvss_vector}

    Provide a mitigation that:
    1. Maps to the relevant OWASP ASVS control or CWE remediation.
    2. References the specific cloud provider best practice (AWS/Azure/GCP).
    3. Is implementable as a concrete change (a config, an IaC update, a code change) — not a platitude.

    Respond as JSON: {{owasp_control, cwe_mitigation, cloud_best_practice, implementation}}
    """
    result = await model.complete(prompt)
    return Mitigation(**json.loads(result), threat_id=threat.id)
```

The mapping is the value. "Use strong authentication" is a platitude. "Enable AWS IAM condition keys requiring MFA for the `sts:AssumeRole` call in the Lambda execution policy (OWASP ASVS V2.1.7, AWS IAM best practice 'require-mfa-for-role-assumption')" is a mitigation an engineer can implement in a PR.

### The deterministic CWE lookup, and where the LLM fills in

The mitigation's control-framework mapping (OWASP control, CWE ID) is *deterministic*, not model-generated. The harness maintains a lookup table from STRIDE category plus a technology fingerprint to the canonical CWE and ASVS control. The LLM's job is the target-specific implementation detail — the patch description that names the actual config line, the actual IAM statement, the actual code change. This split is the same one S03.4 uses for CVSS: the model produces the contextual prose, the deterministic table produces the verifiable reference. If the model hallucinates a CWE number, a compliance reviewer who checks it against the MITRE catalog will catch it; if the lookup table is wrong, every finding inherits the error silently. So the table is reviewed, version-controlled, and small.

```python
# Deterministic mapping — model never chooses the CWE, only the implementation text
STRIDE_CWE_MAP = {
    ("spoofing", "lambda"):        ("CWE-287", "ASVS V2.1.7"),  # Improper Authentication
    ("spoofing", "api_gateway"):   ("CWE-306", "ASVS V2.6.1"),  # Missing Authentication for Sensitive Function
    ("tampering", "postgres"):     ("CWE-862", "ASVS V4.1.3"),  # Missing Authorization
    ("info_disclosure", "s3"):     ("CWE-200", "ASVS V8.1.1"),  # Exposure of Sensitive Information
    ("elevation", "iam_role"):     ("CWE-250", "ASVS V4.3.1"),  # Execution with Unnecessary Privileges
    ("repudiation", "cloudwatch"): ("CWE-778", "ASVS V7.1.1"),  # Insufficient Logging
    ("dos", "lambda"):             ("CWE-770", "ASVS V12.1.1"), # Allocation of Resources Without Limits
}

def map_mitigation(threat: Threat) -> tuple[str, str]:
    """Deterministic CWE + ASVS lookup. Never calls the model."""
    key = (threat.category, threat.technologies[0])
    cwe, asvs = STRIDE_CWE_MAP.get(key, ("CWE-Unknown", "ASVS-Unknown"))
    if cwe == "CWE-Unknown":
        # Log the miss — unmapped (category, tech) pairs are a signal to extend the table
        log.warning(f"Unmapped mitigation: {key}")
    return cwe, asvs
```

The `CWE-Unknown` fallback is deliberate. It is better to surface a mapping gap than to let the model invent a plausible-looking CWE number that no one checks. A harness that silently lets the LLM fill in the CWE produces findings whose references are only as reliable as a one-shot generation — which is to say, unreliable. The deterministic table is the contract with the compliance team; the gap log is the backlog for improving it.

## Mitigation status tracking

Each threat-mitigation pair has a lifecycle:

| Status | Meaning |
| --- | --- |
| **Open** | Threat identified, mitigation not yet implemented |
| **In progress** | Mitigation is being implemented (PR open, IaC change in review) |
| **Mitigated** | Mitigation deployed; threat verified resolved |
| **Accepted risk** | Business decision to accept the risk; documented with approver and expiry |

The status is the link between the threat model and the engineering workflow. A threat that sits "Open" for two sprints is a signal; a threat marked "Accepted risk" by a named approver with an expiry date is a documented decision, not a gap.

## Issue tracker integration

Threats do not live in a separate security tool. They live where the engineering team works:

```python
async def create_issue(threat: Threat, mitigation: Mitigation, tracker: str):
    body = f"""
    ## Security Threat (auto-generated from threat model)

    **Element**: {threat.element_id}
    **STRIDE category**: {threat.category}
    **CVSS draft**: {threat.cvss_vector} (score: {threat.cvss_score})

    **Attack narrative**:
    {threat.attack_narrative}

    **Mitigation**:
    {mitigation.implementation}

    **References**:
    - OWASP: {mitigation.owasp_control}
    - CWE: {mitigation.cwe_mitigation}
    - Cloud best practice: {mitigation.cloud_best_practice}

    ---
    Generated by threat modeling harness. Status tracked in threat model {threat.model_id}.
    """
    if tracker == "github":
        return await gh.create_issue(title=f"[Security] {threat.title}", body=body,
                                      labels=["security", f"stride:{threat.category}"])
```

The integration is bidirectional when possible: the issue links back to the threat model, and the threat model's status updates when the issue closes. The goal is zero manual transcription between the security analysis and the engineering backlog.

## Threat model versioning

Architectures change. A threat model that is not versioned is a snapshot that goes stale. The harness diffs two model versions to identify what changed:

- **New elements** — what new components entered the architecture? Run STRIDE on them.
- **Removed elements** — what components left? Retire their threats.
- **New flows** — especially flows that newly cross a trust boundary. These are the highest-priority deltas.
- **Changed trust boundaries** — a resource that moved from private to public, or a new cross-account role.

The diff is the trigger for re-analysis. The harness does not re-run STRIDE on the entire model on every change — it runs on the delta. This is what makes continuous threat modeling tractable: the cost scales with the change, not with the size of the architecture.

## Worked scenario: threat-modeling a microservice architecture

To make the pipeline concrete, walk it end to end against a realistic shape: a three-service order-processing system deployed on AWS. The services are `orders-api` (public, behind API Gateway + Cognito), `inventory-svc` (private, invoked by `orders-api` over VPC peering), and `billing-worker` (private, consumes an SQS queue that `orders-api` writes to). Terraform declares all three; OpenAPI declares the `orders-api` endpoints.

**Ingestion.** The harness parses the Terraform plan. The resource graph yields seven elements: API Gateway (service, internet-facing), Cognito user pool (data store, identity), `orders-api` Lambda (process), `inventory-svc` ECS task (process), the VPC peering connection (data flow), the SQS queue (data store), and `billing-worker` Lambda (process). The OpenAPI spec adds entry-point detail to the API Gateway element — five `POST` endpoints, each with a request schema. Flows are extracted from references: `orders-api`→`inventory-svc` over the peering connection, `orders-api`→SQS, SQS→`billing-worker`, API Gateway→`orders-api` (invocation), `orders-api`→Cognito (token verification).

**Boundary detection.** Three boundaries emerge. The internet-to-VPC boundary is established by API Gateway's public listener. The `orders-api`-to-`inventory-svc` boundary is *inferred* from IAM: `inventory-svc`'s task role cannot be assumed by `orders-api`, but `orders-api` reaches it over the peering connection — so there is a network path without an authorization path, which the harness flags as a candidate boundary (and, separately, as a finding: cross-service calls without mutual auth). The SQS boundary is established by the queue's resource policy: `billing-worker` can read but `orders-api` can only write, so the queue is a one-way trust channel.

**STRIDE pass.** The engine runs the category matrix per element. The highest-signal findings come from the boundary crossings, not the interior:

- *Spoofing on `inventory-svc`.* The peering connection lets `orders-api` call `inventory-svc` directly, but `inventory-svc` does not verify the caller — any service that can route to it can call it. Grounded threat: "an attacker who compromises any service in the peered VPC can invoke `inventory-svc`'s decrement endpoint without authentication." Mapped to CWE-306 (Missing Authentication for Sensitive Function), ASVS V2.6.1.
- *Tampering on the SQS queue.* `orders-api` writes order events; if a message is malformed or maliciously crafted, `billing-worker` processes it. Grounded threat: "a compromised `orders-api` role enqueues a zero-amount order; `billing-worker` marks it paid without re-verifying the amount against the orders database." CWE-862 (Missing Authorization on the consumer side).
- *Information disclosure on the Cognito flow.* `orders-api` verifies tokens by calling Cognito's public JWKS endpoint over HTTPS — but if the verification result is not cached with an integrity check, a TOCTOU window lets an attacker swap the JWKS response. Grounded in the specific SDK call, not a generic "auth can be spoofed."

**Mitigation and issue creation.** Each finding generates a mitigation via the deterministic CWE table plus the LLM's implementation detail. The spoofing finding maps to ASVS V2.6.1 and produces an implementation: "add an IAM-signed request header (`x-caller-role`) that `inventory-svc` validates against the `orders-api` task role ARN; reject requests where the signature does not verify." The harness opens three GitHub issues, each labeled `stride:spoofing`, `stride:tampering`, `stride:info_disclosure` respectively, each linking back to the DFD element and the flow that generated it.

**Version diff.** Two weeks later, a developer adds a new `refunds-api` Lambda that reads from the same SQS queue. The harness re-runs ingestion, diffs the new DFD against the stored version, and flags exactly one delta: a new flow from SQS to `refunds-api`. STRIDE runs on that element and flow only. It surfaces a new finding: `refunds-api` can read *all* messages on the queue, including non-refund order events — a tenant-isolation failure because the queue is not scoped by message attribute. The finding is filed; the rest of the model is untouched. This is the cost-scales-with-change property in action, and it is what connects S07 to S08.2's scanner orchestration: the threat model is a gate input to the SDLC, and the diff is what makes the gate cheap enough to run on every change.

---

## Anti-Patterns

### The manual threat model
A diagram in a quarterly meeting, a spreadsheet of threats, stale within a week. Cure: ingest from IaC and OpenAPI; regenerate on every architecture change; the model is an output of the pipeline, not a hand-maintained artifact.

### The generic threat generator
"An attacker might spoof authentication" for every service. Useless. Cure: ground threat generation in the element's specific technologies and the specific flows crossing its boundary. Grounding is the load-bearing instruction.

### Threats in a security silo
Threats filed in a separate security tool that engineers never open. Cure: auto-create issues in the engineering tracker (GitHub/Linear/Jira) with full traceability back to the threat model. Meet engineers where they work.

### The full re-run on every change
Re-running STRIDE on the entire 50-element model on every Terraform change — expensive, slow, produces noise. Cure: diff two model versions and run analysis only on the delta. Cost scales with change, not architecture size.

---

## Key Terms

| Term | Definition |
| --- | --- |
| **DFD** | Data Flow Diagram; the canonical intermediate representation from any input source |
| **Trust boundary** | Line across which the level of trust changes; highest-priority analysis target |
| **Implicit boundary** | A trust boundary inferred from IAM, routing, or tagging rather than declared in a single resource attribute; the hard case in boundary detection |
| **Phantom boundary** | A boundary the harness detects that does not reflect a real trust difference; should be flagged as candidate, not confirmed |
| **STRIDE** | Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege |
| **Grounded threat generation** | LLM threats grounded in specific technologies + flows, not generic statements |
| **Deterministic CWE mapping** | STRIDE-category + technology fingerprint → CWE/ASVS lookup; the model never chooses the control reference, only the implementation text |
| **Traceability** | Every threat carries element_id + category; enables maintenance across versions |
| **Model diffing** | Compare two model versions; re-run analysis only on the delta |

---

## Lab Exercise

See `07-lab-spec.md`. Three labs: (1) parse Terraform + draw.io into a structured DFD; (2) run the STRIDE engine with grounded LLM threat generation; (3) generate mitigations and auto-create GitHub issues.

---

## References

1. **STRIDE** (Microsoft) — Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, Elevation of privilege. Shostack's *Threat Modeling: Designing for Security* is the canonical treatment of trust boundaries and the per-element methodology.
2. **OWASP ASVS** — Application Security Verification Standard; the control taxonomy for mitigation mapping.
3. **OWASP Threat Dragon / pytm** — open-source threat modeling tools; the DFD representation draws from these.
4. **CWE** (MITRE) — Common Weakness Enumeration; remediation references for mitigations.
5. **CVSS v3.1** (FIRST) — Common Vulnerability Scoring System; the draft scoring vector for prioritization.
6. **S06.3** — semantic codebase index and vector retrieval; this module's threat dedup uses the same vector-similarity approach, and the IAM-policy retrieval for service-to-service boundary inference reuses the same index.
7. **S08.1** — the control-plane shift to creation-time guardrails; the trust boundary model is a creation-time artifact that must be correct before the analysis gate fires, and the threat model diff is a gate input to the multi-scanner orchestration in S08.2.
8. **S08.2** — scanner orchestration and cross-scanner dedup; the threat model's findings feed the same engineering backlog as SAST/SCA findings, and the model diff is what makes the threat-model gate cheap enough to run on every change.
