~/home ~/blog ~/projects ~/about ~/resume

Threat Modeling for the Rest of Us: Part 2 - Tools, Automation, and Your First Pipeline

Introduction

In Part 1, you put a threat model on a whiteboard. That was the right first step. Now let’s talk about why it’s going to die there if you don’t do something about it.

I’ve seen this play out dozens of times. A team runs a solid threat modeling session – the right people in the room, real threats identified, concrete mitigations assigned. Everyone walks out feeling productive. Someone snaps a photo of the whiteboard. That photo ends up in a Confluence page with the title “Threat Model - Checkout Service - Q1 2026.” And that’s the last time anyone looks at it.

Three sprints later, the team adds a new payment provider integration. Six weeks after that, they migrate the session store from Redis to a managed service with a completely different trust model. The architecture has diverged so far from that whiteboard photo that the threat model is now a historical artifact – a snapshot of how things used to be, not something you’d use to make security decisions today.

This is the default outcome. Not because teams are lazy or don’t care about security, but because static documents can’t keep pace with dynamic systems. A PDF threat model emailed to the security team the week it was created has a half-life measured in deploys, not months. The moment your architecture changes and the model doesn’t, you’re making security decisions based on a fiction.

The thesis of this article is simple: threat models should live in your repo, run in your pipeline, and feed your security toolchain – or they rot. That means treating your threat model the same way you treat your infrastructure: as code, version-controlled, reviewed in pull requests, and validated on every build. When the architecture changes, the threat model changes in the same commit. When the threat model identifies a risk, it generates a finding that lands in the same dashboard your engineers already check every morning.

I realize not every team is ready to wire threat models into CI/CD (Continuous Integration / Continuous Deployment) on day one. If you just ran your first whiteboard session last week, jumping straight to pipeline integration would be like teaching someone to drive by putting them on a highway. This article covers the full journey – from GUI tools that give you a visual on-ramp, through threat-model-as-code with Threagile and similar frameworks, into pipeline integration and ecosystem connectivity. You don’t have to adopt everything at once. But you should know where the practice is heading, because the teams that get threat modeling to stick are the ones that automate early enough to survive the second quarter.

Here’s what we’ll cover: GUI-based tools for teams that want a visual starting point and aren’t ready to write YAML on day one. Threat-model-as-code as the recommended long-term approach – what it looks like, why it wins, and how to get started with Threagile. Pipeline integration that runs your threat model on every pull request the same way you run your linter and your test suite. Connecting your threat model outputs to the tools your security team already uses – SIEM (Security Information and Event Management), ticketing, vulnerability management. And finally, keeping your threat models current as your architecture evolves, because a living threat model that’s 90% accurate beats a dead one that was perfect the day it was drawn.

Let’s get into it.

Where Most Teams Start (And Why It’s Not Enough)

If your team is doing threat modeling with a GUI tool today, you’re ahead of most. Seriously. The majority of organizations I’ve worked with over the past fifteen years have no threat modeling practice at all – just a vague intention to “do it eventually” that never survives contact with the sprint backlog. So if you’re using one of the tools below, you’ve already cleared the hardest hurdle: getting people to sit down and think about threats before they ship code.

That said, there’s a ceiling. And it’s worth understanding where it is before you hit it.

OWASP Threat Dragon

Threat Dragon is the tool I point most teams toward when they’re getting started. It’s free, open-source, and runs in a browser – no procurement cycle, no license negotiations, no waiting six weeks for IT to approve a Windows installer. You draw your data flow diagrams, and Threat Dragon generates threats against the components you’ve modeled. It applies STRIDE by default, which gives you a structured way to think about spoofing, tampering, repudiation, information disclosure, denial of service, and elevation of privilege without having to memorize the framework.

For a team running its first or second threat modeling session, this is genuinely useful. It gets the conversation started and produces an artifact you can point to.

The limitation shows up around the third month. Your threat model lives in Threat Dragon’s storage – not in your Git repo, not next to the Pulumi code that defines your infrastructure, not in a place your CI pipeline can reach. When a developer adds a new API endpoint or swaps out a message broker, nobody opens Threat Dragon to update the diagram. The model drifts. Within a quarter, you’re back to that Confluence page with the whiteboard photo – just with better graphics.

Microsoft Threat Modeling Tool

Microsoft’s Threat Modeling Tool takes a more structured approach. It’s template-driven, with pre-built stencils for Azure services, generic web applications, and other common architectures. You build a data flow diagram, and the tool auto-generates a list of threats based on the interaction patterns it detects. The output is detailed, well-categorized, and backed by years of Microsoft’s internal threat modeling methodology.

If your stack is heavily Azure-based, this tool gives you a head start that’s hard to beat. The templates understand Azure’s trust boundaries, and the generated threats are specific enough to be actionable rather than generic.

The trade-offs are real, though. It’s Windows-only, which immediately excludes a chunk of engineering teams. The models are stored as standalone .tm7 files that live outside your development workflow. And the tool is deeply tied to the Microsoft ecosystem – if you’re running on AWS or GCP, you’re fighting the templates instead of leveraging them. Like Threat Dragon, the fundamental problem is disconnection: the model exists in a parallel universe from your codebase, and keeping those universes in sync requires manual discipline that rarely survives past the first reorg.

The Pattern Worth Noticing

Both of these tools produce useful output. Neither of them is a mistake. But they share a structural limitation that no amount of discipline can fully overcome: they create artifacts that live outside the development workflow. The threat model is a document you maintain in addition to your code, not as part of your code. That separation is what kills threat modeling programs. Not lack of training, not lack of tooling, not lack of executive buy-in – just the quiet, predictable drift between a model that doesn’t auto-update and a codebase that never stops changing.

The question isn’t whether these tools are good. They are. The question is how to take what they give you – the habit of thinking about threats, the vocabulary, the structured approach – and move it into a form that survives the pace of modern development. That’s where threat-model-as-code comes in.

After fifteen years of watching threat models gather dust in SharePoint folders and die in Confluence pages, I’m going to give you the most opinionated take in this series: define your threat models in structured, machine-readable formats that live in version control alongside your source code. YAML, Python, JSON – the format matters less than the principle. Your threat model is code. Treat it like code.

This isn’t a novel idea. We figured out decades ago that infrastructure definitions belong in version control. We moved runbooks into Ansible playbooks, network diagrams into Pulumi programs, and monitoring rules into Prometheus configs. Threat models are the last major security artifact still trapped in the document era, and it’s time to drag them into the repo where they belong.

Why threat-model-as-code wins:

  • Version controlled – every change to your threat model produces a diff, a commit hash, and an entry in git log. You can see who changed the threat model, when, and why. When an auditor asks “how has your threat posture evolved over the past year,” you don’t dig through email threads – you run git log --oneline threagile.yaml and hand them the history.

  • Reviewable – threat model changes go through code review like any other pull request. When someone adds a new service or modifies a trust boundary, the security team sees the diff in the same PR that contains the code change. No more “hey, did anyone update the threat model?” in Slack three weeks after the deploy.

  • Automatable – if your threat model is a file a tool can parse, your CI/CD pipeline can validate it on every push. Did someone add a new internet-facing service without encryption? The pipeline catches it before it merges. Did a trust boundary change in a way that introduces new attack surface? Automated analysis flags it in the PR, not in a quarterly review.

  • Living – when the threat model lives in the same repo as the code it describes, it changes when the system changes. Not eventually, not when someone remembers, not at the next security review – in the same commit. The model and the system stay in sync because they’re literally in the same place.

This is the approach I recommend to every team I work with. If you take one thing from this article, make it this: get your threat model into your repo.

The Worked Example: A Microservices API Platform

To make this concrete, let’s build a threat model for a system you’d actually encounter in the wild – a microservices API platform. Nothing exotic, just the kind of architecture you’d find behind most SaaS products shipping today.

Here’s the architecture:

                          TRUST BOUNDARY: External Network
                          ================================
                                      |
                              HTTPS   |   API requests,
                            (TLS 1.3) |   auth tokens
                                      |
                                      v
                          +-----------+-----------+
                          |                       |
                          |     API Gateway       |
                          |  (routing, rate        |
                          |   limiting, TLS term)  |
                          |                       |
                          +-----------+-----------+
                                      |
          TRUST BOUNDARY: Internal Network
          =================================
                  |                   |                   |
          REST/mTLS           REST/mTLS           REST/mTLS
          auth tokens         user queries        notifications
                  |                   |                   |
                  v                   v                   v
        +---------+------+  +--------+-------+  +--------+--------+
        |                |  |                |  |                  |
        |  Auth Service  |  |  User Service  |  |  Notification   |
        |  (authn,       |  |  (profiles,    |  |  Service        |
        |   tokens)      |  |   preferences) |  |  (email/SMS)    |
        |                |  |                |  |                  |
        +-------+--------+  +-------+--------+  +--------+--------+
                |                    |                     |
                |            SQL/TLS |              HTTPS  |
                |            user data              API calls
                |                    |                     |
                |                    v                     v
                |           +--------+--------+   +-------+--------+
                |           |                 |   |                |
                |           |   PostgreSQL    |   |  Third-Party   |
                +---------->|   Data Store    |   |  Provider      |
                 SQL/TLS    |                 |   |  (Twilio/      |
                 auth data  |                 |   |   SendGrid)    |
                            +-----------------+   +----------------+
                            TRUST BOUNDARY:
                            Data Store Tier         External SaaS

Five components, three trust boundaries, and enough data flows to produce meaningful threats. The API Gateway is the single entry point from the outside world. Auth Service and User Service handle authentication and user data respectively. Notification Service talks to a third-party provider for email and SMS delivery – that’s an outbound trust boundary crossing that a lot of teams forget to model. And PostgreSQL sits in its own data tier, because your database should never share a trust boundary with your application services.

Defining the System in Threagile

Threagile is an open-source threat modeling tool built around a simple idea: you define your architecture in YAML, and Threagile analyzes it to identify threats automatically. No GUI, no drag-and-drop, no proprietary file format – just a YAML file that describes your system and a CLI that tells you what’s risky about it.

Here’s what our API platform looks like as a Threagile model. Threagile uses YAML to define your system’s architecture, trust boundaries, and data flows – everything the tool needs to identify threats automatically.

# threagile.yaml -- Threat model for the API platform
# Lives in repo root, reviewed in PRs, validated in CI

threagile_version: 1.0.0

title: "API Platform Threat Model"
description: "Microservices API platform serving external clients"

# Data assets: what are we protecting?
data_assets:
  auth-credentials:
    description: "User credentials and session tokens"
    usage: business
    sensitivity: strictly-confidential
    integrity: critical
    availability: critical
  user-profile-data:
    description: "User profiles and preferences"
    usage: business
    sensitivity: confidential
    integrity: important
    availability: important
  notification-content:
    description: "Email and SMS message content"
    usage: business
    sensitivity: confidential
    integrity: important
    availability: operational

# Technical assets: the components in our architecture
technical_assets:
  api-gateway:
    description: "Entry point -- routing, rate limiting, TLS termination"
    type: process
    usage: business
    technologies:
      - web-service-rest
    internet: true               # faces the outside world
    encryption: transparent      # TLS termination happens here
    data_assets_processed:
      - auth-credentials
    communication_links:
      auth-service-link:
        target: auth-service
        protocol: https
        authentication: token
        data_sensitivity: strictly-confidential
      user-service-link:
        target: user-service
        protocol: https
        authentication: token
        data_sensitivity: confidential
      notification-service-link:
        target: notification-service
        protocol: https
        authentication: token
        data_sensitivity: confidential

  auth-service:
    description: "Handles authentication and token management"
    type: process
    usage: business
    technologies:
      - web-service-rest
    internet: false              # internal only
    encryption: transparent
    data_assets_processed:
      - auth-credentials
    communication_links:
      db-auth-link:
        target: postgresql-store
        protocol: https
        authentication: credentials
        data_sensitivity: strictly-confidential

  user-service:
    description: "Manages user profiles and preferences"
    type: process
    usage: business
    technologies:
      - web-service-rest
    internet: false
    encryption: transparent
    data_assets_processed:
      - user-profile-data
    communication_links:
      db-user-link:
        target: postgresql-store
        protocol: https
        authentication: credentials
        data_sensitivity: confidential

  notification-service:
    description: "Sends email/SMS via third-party providers"
    type: process
    usage: business
    technologies:
      - web-service-rest
    internet: true               # calls out to Twilio/SendGrid
    encryption: transparent
    data_assets_processed:
      - notification-content
    communication_links:
      third-party-link:
        target: third-party-provider
        protocol: https
        authentication: token    # API key to external provider
        data_sensitivity: confidential

  postgresql-store:
    description: "Primary data store -- user data and auth records"
    type: datastore
    usage: business
    technologies:
      - database
    internet: false
    encryption: data-with-symmetric-shared-key
    data_assets_processed:
      - auth-credentials
      - user-profile-data

  third-party-provider:
    description: "External SaaS (Twilio/SendGrid) for delivery"
    type: external-entity
    usage: business
    technologies:
      - web-service-rest
    internet: true
    encryption: transparent

# Trust boundaries: who trusts whom?
trust_boundaries:
  external-network:
    description: "Untrusted -- public internet"
    type: network-cloud-provider
    technical_assets_inside:
      - api-gateway
  internal-network:
    description: "Internal service mesh -- mTLS between services"
    type: network-cloud-provider
    technical_assets_inside:
      - auth-service
      - user-service
      - notification-service
  data-tier:
    description: "Database tier -- restricted network access"
    type: network-cloud-provider
    technical_assets_inside:
      - postgresql-store

That’s your entire system architecture in a file that lives in your repo, gets reviewed in PRs, and runs through CI. Every component, every trust boundary, every data flow – version controlled and automatable.

When you run threagile analyze against this file, it walks the model and identifies threats based on the relationships between components, the trust boundaries they cross, and the sensitivity of the data flowing between them. The API Gateway is internet-facing and processes strictly-confidential credentials? Threagile flags it. The Notification Service crosses a trust boundary to reach an external provider with confidential content? Flagged. The PostgreSQL store sits in a separate trust boundary from the services that query it, but the communication links need to be validated for proper authentication? Flagged and categorized.

You don’t get a vague “consider encryption” recommendation. You get specific, STRIDE-categorized threats tied to the exact components and data flows in your model. And because the model is YAML, updating it when your architecture changes is a three-line diff in the same PR that adds the new service.

The Python Alternative: pytm

If your team is more comfortable in Python than YAML, pytm offers the same threat-model-as-code approach with a different syntax. You define your system using Python objects – Server, Datastore, ExternalEntity, Dataflow – and pytm generates data flow diagrams and a threat list programmatically. It’s the same core idea: your threat model is code, it lives in your repo, and it produces machine-readable output that your pipeline can consume. I’m using Threagile as the worked example here because YAML is more readable for a mixed audience of developers, security engineers, and architects who may not all be fluent in Python. But if your team already thinks in Python, pytm is a strong choice – evaluate both and pick the one your team will actually maintain.

Running Automated Analysis

You have a YAML file describing your architecture. Now let’s run it through Threagile and see what comes back.

Threagile ships as a Docker image, so there’s nothing to install locally – no Go toolchain, no dependency hell, no version conflicts. You mount your working directory into the container, point it at your model file, and it writes everything to an output directory. One command:

docker run --rm -v $(pwd):/app/work threagile/threagile -model /app/work/threagile.yaml -output /app/work/output

That pulls the Threagile image (if you don’t have it cached), reads your threagile.yaml, runs its full rule set against the model, and writes results into an output/ directory. When it finishes, you’ll find three things in there: a risks report documenting every threat the tool identified along with severity ratings and suggested mitigations, generated data flow diagrams built from the relationships in your YAML, and a risk tracking file you can use to manage risk acceptance decisions over time.

Walking Through the Output

The risks report is where you’ll spend most of your time. Threagile walks every component, every communication link, and every trust boundary crossing in your model, then applies its built-in rule set to flag structural weaknesses. For our API platform model, here’s a representative sample of what it finds:

RISK ID:    unencrypted-communication@api-gateway>auth-service-link
Severity:   Elevated
Category:   Information Disclosure
Asset:      api-gateway -> auth-service communication link
Description: Communication link between API Gateway and Auth Service
             transmits strictly-confidential data (auth-credentials)
             without verified end-to-end encryption across trust boundary.
Mitigation: Enforce mTLS on the api-gateway to auth-service link and
            verify certificate validation is not disabled in service config.

RISK ID:    missing-authentication@user-service>notification-service
Severity:   High
Category:   Spoofing / Elevation of Privilege
Asset:      user-service -> notification-service internal link
Description: Internal communication between User Service and Notification
             Service lacks explicit authentication. Any service on the
             internal network could impersonate User Service and trigger
             arbitrary notifications.
Mitigation: Implement mutual authentication (mTLS or signed JWTs) on all
            internal service-to-service communication links.

RISK ID:    third-party-data-transfer@notification-service>third-party-link
Severity:   Elevated
Category:   Information Disclosure
Asset:      notification-service -> third-party-provider communication link
Description: Confidential notification content is sent to an external
             third-party provider (Twilio/SendGrid) without verification
             that the provider enforces encryption at rest and in transit
             for received data.
Mitigation: Verify third-party provider's encryption posture, enforce TLS
            1.2+ on the outbound connection, and add contractual data
            protection requirements to the vendor agreement.

The first two are the kinds of findings that make automated analysis worth the effort – they’re structural issues that a human reviewer might miss because they look correct at a glance. The API Gateway talks to Auth Service over what appears to be HTTPS, but Threagile flags that the trust boundary crossing with strictly-confidential data needs verified end-to-end encryption, not just transport-layer security that terminates at the gateway. The missing authentication between User Service and Notification Service is even more insidious – internal services tend to trust each other implicitly, and that implicit trust is exactly what an attacker exploits after gaining a foothold on the internal network.

The third finding – the third-party data transfer – is the kind of thing that shows up in every architecture that integrates with external SaaS providers and almost never gets caught until a compliance audit forces the question. You’re sending user-facing content to a provider you don’t control. Threagile can’t tell you whether Twilio’s encryption posture meets your requirements, but it can tell you that you’re crossing a trust boundary with confidential data and haven’t modeled the controls on the other side.

The generated data flow diagrams give you a visual representation of the same architecture you defined in YAML – useful for presentations, architecture reviews, and onboarding new team members who think better in pictures than in markup. And the risk tracking file gives you a structured way to record decisions: this risk is mitigated, this one is accepted with justification, this one is deferred to next quarter. That tracking file goes into version control alongside the model, so your risk acceptance decisions have the same audit trail as your code.

What Automation Catches and What It Misses

I want to be direct about the boundaries here. Automated threat analysis is good at catching common structural patterns – unencrypted communication channels, missing authentication on internal links, exposed assets without proper access controls, trust boundary crossings with sensitive data. These are the kinds of findings that follow repeatable rules, and a tool that applies those rules consistently across every component in your model will catch things that a tired human in a two-hour whiteboard session will overlook.

But automated analysis won’t catch business logic threats. It won’t find the flaw where an attacker can manipulate the notification service to enumerate valid user accounts by observing delivery timing. It won’t identify novel attack paths that chain three low-severity findings into a critical exploit. It won’t understand that your specific regulatory environment means a data flow that’s technically fine is actually a compliance violation. Those require a human who understands your business, your threat actors, and the context that no YAML file can fully capture.

The whiteboard session from Part 1 and the automated analysis here are complementary – one catches what the other misses. The humans in the room bring business context, adversarial creativity, and institutional knowledge. The tool brings consistency, exhaustiveness, and the ability to re-run the same analysis on every commit without getting bored or distracted. You need both. Run the tool to establish a baseline, then layer human judgment on top to catch the threats that no rule set can encode.

Embedding in CI/CD

Running Threagile locally is a good start. But if the analysis only happens when someone remembers to run it, it won’t happen. I’ve watched teams adopt threat-model-as-code with genuine enthusiasm, run it manually for two or three sprints, and then quietly stop because nobody built it into the pipeline. The same way you don’t rely on developers remembering to run the linter – you wire it into CI and make it automatic.

There are two patterns I’ve deployed repeatedly, and they serve different purposes. The first catches new risks at PR time, before they merge. The second gates deployments, so nothing with unmitigated high-severity risks reaches production. Most mature teams end up running both.

Pattern 1: PR-Time Validation

The idea is straightforward: when a pull request touches files that could change your system’s architecture, run Threagile against the updated model and flag any new risks before the PR merges. This gives the author and the reviewer immediate visibility into the security implications of the change – not three weeks later at a security review, but right there in the PR where the context is fresh.

The key is scoping the trigger correctly. You don’t want this running on every PR – a copy change in a README or a CSS tweak has zero threat model implications, and burning CI minutes on it just trains people to ignore the check. Instead, you filter to architecture-relevant files: the threat model itself, infrastructure definitions, Docker configurations, and anything else that could alter trust boundaries or data flows.

Here’s a GitHub Actions workflow that does this:

name: Threat Model Analysis
on:
  pull_request:
    paths:
      - 'threagile.yaml'
      - 'docs/threat-model/**'
      - 'docker-compose*.yml'
      - '**/Dockerfile'
      - 'infra/**'

jobs:
  threat-model-check:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Run Threagile analysis
        run: |
          docker run --rm \
            -v "${{ github.workspace }}:/app/work" \
            threagile/threagile \
            -model /app/work/threagile.yaml \
            -output /app/work/threagile-output          

      - name: Check for high-severity unmitigated risks
        run: |
          HIGH_RISKS=$(cat threagile-output/risks.json \
            | jq '[.[] | select(.severity == "high" or .severity == "critical") | select(.risk_status != "mitigated" and .risk_status != "accepted")] | length')
          echo "Found $HIGH_RISKS high/critical unmitigated risks"
          if [ "$HIGH_RISKS" -gt 0 ]; then
            echo "::error::Threat model analysis found $HIGH_RISKS high/critical unmitigated risk(s). Review threagile-output/risks.json for details."
            exit 1
          fi          

      - name: Comment on PR if risks found
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const risks = JSON.parse(fs.readFileSync('threagile-output/risks.json', 'utf8'));
            const high = risks.filter(r => (r.severity === 'high' || r.severity === 'critical') && r.risk_status !== 'mitigated' && r.risk_status !== 'accepted');
            let body = '## Threat Model Alert\n\nThis PR introduces architecture changes with **' + high.length + '** high/critical unmitigated risk(s):\n\n';
            high.forEach(r => { body += '- **' + r.severity.toUpperCase() + '**: ' + r.title + '\n'; });
            body += '\nPlease review and either mitigate these risks or document acceptance in `risk-tracking.yaml`.';
            github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body });            

The paths filter is the most important line in this workflow, and it’s the one you’ll customize for your repo. If your team stores infrastructure definitions in a k8s/ directory instead of infra/, update the filter. If you have Helm charts in charts/, add that path. The goal is to capture any file change that could affect your system’s architecture without casting the net so wide that the check runs on every PR. Start with the obvious paths and widen as you learn which changes actually affect the threat model.

Pattern 2: Build-Time Gate

PR-time validation catches risks early. But some teams need a harder gate – one that prevents deployment if the threat model shows unresolved high-severity risks. This is the “nothing ships to production with known critical vulnerabilities” pattern, applied to threat modeling.

The mechanism is simple. Your deployment pipeline includes a stage that runs Threagile, parses the output for unmitigated risks above a severity threshold, and fails the build if any exist. Accepted risks – the ones your team has reviewed, documented, and explicitly decided to carry – don’t trigger the gate. Only unmitigated, undocumented risks block the deploy.

Threagile supports this workflow through its risk-tracking.yaml file. When your team reviews a risk and decides to accept it, you record that decision in the tracking file with a justification, an owner, and a review date. The tracking file lives in version control alongside the model, so every acceptance decision has an audit trail. Here’s what the pipeline stage looks like:

# Run Threagile analysis
docker run --rm -v "$(pwd):/app/work" threagile/threagile \
  -model /app/work/threagile.yaml \
  -output /app/work/threagile-output

# Parse output for unmitigated high/critical risks
UNMITIGATED=$(jq '[.[] | select(
  (.severity == "high" or .severity == "critical") and
  (.risk_status != "mitigated") and
  (.risk_status != "accepted")
)] | length' threagile-output/risks.json)

echo "Unmitigated high/critical risks: $UNMITIGATED"
if [ "$UNMITIGATED" -gt 0 ]; then
  echo "BUILD FAILED: $UNMITIGATED unmitigated high/critical risk(s) found."
  echo "Review risks in threagile-output/risks.json."
  echo "To accept a risk, add it to risk-tracking.yaml with justification."
  exit 1
fi

That jq filter is doing the heavy lifting. It pulls every risk with high or critical severity, excludes any that have been mitigated or explicitly accepted, and counts what’s left. If the count is greater than zero, the build fails. You can adjust the severity threshold – some teams only gate on critical, letting high-severity risks through with a warning. Start where your team is comfortable and tighten over time.

The important distinction is between mitigated, accepted, and ignored. A mitigated risk has a control in place. An accepted risk has a documented decision – someone with authority reviewed it, weighed the cost of mitigation against the likelihood and impact, and signed off. An ignored risk is a risk nobody has looked at. The build gate blocks on ignored risks. It never blocks on risks your team has consciously decided to carry.

Practical Considerations

Which changes trigger analysis. Not every pull request needs a threat model run. A CSS change, a copy edit, a test refactor – none of these alter your system’s trust boundaries or data flows. Scope your triggers to architecture-relevant files: infrastructure configs, Docker definitions, the threat model itself, service manifests, and anything that defines how components communicate. If you trigger on everything, engineers learn to ignore the check. If you scope it tightly, the signal stays meaningful.

Handling false positives. Automated analysis will produce findings that don’t apply to your specific context. Maybe Threagile flags a communication link as unencrypted, but you know the traffic never leaves a hardware-encrypted internal bus. The right response is not to disable the check or add an exception to the pipeline. The right response is to record an accepted risk in risk-tracking.yaml with a clear justification: “Traffic between service A and service B traverses a physically isolated network segment with hardware encryption. Accepted by [name], reviewed [date].” Now you have a documented security decision instead of a suppressed finding. When an auditor asks about that communication link, you point to the tracking file instead of trying to remember a conversation from six months ago.

Where it fits in the pipeline. Run threat model analysis after your unit tests and integration tests, but before deployment. Fast feedback comes first – if your tests fail, there’s no point burning CI time on threat analysis for code that doesn’t work. But threat analysis must complete before you deploy. The pipeline order I use: lint, unit tests, integration tests, threat model analysis, deploy. If you put threat analysis first, you’ll slow down every developer’s feedback loop on every push, and they will resent it. If you put it after deploy, you’ve already shipped the risk.

Incremental rollout. This is the advice I wish someone had given me the first time I tried to wire threat modeling into a pipeline: do not start with a blocking gate. Start with warn-only mode. Configure the pipeline to run the analysis and post a PR comment with the findings, but don’t fail the build. Let your team see the output for a month. Let them get familiar with what the tool flags, learn which findings are actionable, and build confidence that the signal is worth paying attention to. After a month of warn-only – when engineers are reading the comments and occasionally filing tickets based on them – flip to blocking mode. If you force a blocking gate on day one, before the team trusts the output, you’ll get exactly two outcomes: workarounds and resentment. Neither one improves your security posture.

Feeding the Security Ecosystem

A threat model that lives in your pipeline but doesn’t talk to anything else is still an island. The real value shows up when threat model outputs feed the tools your security and engineering teams already use every day – ticketing systems, vulnerability scanners, SIEM platforms, risk dashboards. The pattern is always the same: threat model produces structured, machine-readable output; an integration layer transforms that output into actions in downstream systems. The specifics vary by toolchain, but the architecture doesn’t.

Ticketing Integration

The most immediate win is routing threat model findings directly into your team’s issue tracker. The pattern works like this: a script or webhook reads the structured output from Threagile (or whatever tool you’re running), filters for risks above a severity threshold, and creates tickets in Jira, Linear, GitHub Issues, or whatever your team tracks work in. Each ticket includes the risk severity, the affected component, a description of the threat, and the suggested mitigation from the tool’s output. The ticket gets tagged with the relevant service name and a security label so it shows up in the right team’s backlog with the right priority.

The integration doesn’t need to be complicated. A thirty-line script that runs after the Threagile analysis step in your pipeline, parses the JSON output, and calls the ticketing API is enough to start. The script checks whether a ticket already exists for each risk ID – you don’t want to create duplicates every time the pipeline runs – and only creates new tickets for findings that don’t have an existing open issue. When a risk is mitigated in the threat model (either through a code change or an explicit acceptance in the risk tracking file), the script can close or update the corresponding ticket automatically.

What matters is where those tickets land. I worked with a team that had been maintaining threat model findings in a separate spreadsheet – a risk register that the security team updated quarterly and engineering teams rarely looked at. When they switched to routing findings directly into sprint backlogs as standard tickets, their mean time to remediate threat model findings dropped from 90 days to under three weeks. The findings weren’t more urgent than before. The team wasn’t working harder. The findings were just visible in the place engineers actually look every morning, prioritized alongside feature work instead of buried in a spreadsheet nobody opens until audit season.

Vulnerability Management

Threat model output and vulnerability scanner output are two halves of a prioritization system that neither can provide alone. Your vulnerability scanner tells you what’s wrong – this library has a known CVE, that endpoint is missing a security header, this container image has an outdated base. Your threat model tells you what matters – which components handle sensitive data, which sit on trust boundaries, which face the internet.

The integration pattern connects these two signals. When a scanner finding lands on a component your threat model flagged as high-risk – say, a CVE in a dependency used by your Auth Service, which your model identifies as processing strictly-confidential credentials on a trust boundary – that finding gets escalated. Same CVE, same CVSS (Common Vulnerability Scoring System) score, but on an internal admin tool that your threat model shows handles no sensitive data and sits behind two layers of network isolation? That finding gets deprioritized. The threat model provides the context that CVSS scores can’t capture: not just how bad the vulnerability is in the abstract, but how much it matters in your specific architecture.

In practice, this means enriching your vulnerability management workflow with threat model metadata. When findings flow into your vulnerability management platform, they carry additional context from the threat model: the component’s trust boundary position, the sensitivity of data it processes, whether it’s internet-facing. Your security team uses that context to sort the backlog instead of treating every critical CVE as equally urgent. The teams I’ve seen do this well typically cut their vulnerability remediation noise by 30 to 40 percent – not by ignoring findings, but by working on the ones that actually matter first.

SIEM and Monitoring

Your threat model is a map of the data flows and trust boundaries in your system. Your SIEM is the system that watches those flows for anomalies. The connection is obvious once you see it: the threat model tells you what to monitor, and the SIEM tells you when something goes wrong.

The pattern starts with your threat model’s trust boundaries and data flows. If your model identifies a trust boundary between the API Gateway and the Auth Service – with strictly-confidential credentials crossing that boundary – then your security operations team should have detection rules for anomalous traffic on that link. Unusual request volumes, unexpected source IPs, authentication failures that spike beyond baseline, payload sizes that don’t match normal patterns. Each trust boundary crossing in your threat model is a candidate for a monitoring rule. Each high-risk data flow is a candidate for anomaly detection.

You don’t need a custom rule for every data flow in your model – that would generate alert fatigue faster than you can write runbooks. Focus monitoring investment on the trust boundaries and data flows your threat model scored as high-risk. The internal link between two low-sensitivity services in the same trust boundary probably doesn’t need its own detection rule. The link where credentials cross from the internet-facing gateway into your auth tier absolutely does. The threat model gives your SOC (Security Operations Center) team a principled way to decide where to invest monitoring effort instead of trying to watch everything equally or guessing based on gut feel.

The practical implementation is a script or integration that reads the threat model output, extracts the high-risk trust boundary crossings and data flows, and generates monitoring requirement documents or detection rule templates for your SIEM platform. Security operations engineers review those templates, tune them for your environment, and deploy them. When the threat model changes – a new trust boundary, a reclassified data flow – the monitoring requirements update in the same cycle.

Risk Dashboards

The final integration pattern is aggregation: pulling threat model outputs from across multiple teams and services into an organizational risk view. I’ll keep this brief because Part 3 covers the communication and leadership angle in depth, but the connection point matters here.

Risk dashboards become the translation layer between technical threat model outputs and executive-level risk posture reporting. Individual teams produce threat models for their services. Those models generate structured risk data – counts of unmitigated findings by severity, trends over time, components with the highest risk concentration. A dashboard aggregates that data across the organization, giving security leadership a view of where risk is concentrated, which teams are remediating effectively, and where investment is needed.

When a CISO needs to report the organization’s threat landscape to the board, aggregated threat model data is more credible than gut feel. It’s grounded in specific architectural analysis, tied to real components and real data flows, with an audit trail showing how risk posture has changed over time. That’s a fundamentally different conversation than “we think we’re doing okay” – and it’s only possible when threat models produce machine-readable output that can be aggregated and trended. Part 3 digs into how to build these communication bridges. For now, the key point is that the structured output your pipeline produces isn’t just for engineers – it’s the raw material for organizational risk visibility.

Staying Current: Automation + Process

Your threat model is accurate today. In three months, it won’t be – unless you build mechanisms to keep it current. Part 1 called out the core problem: threat models rot. The whiteboard photo becomes a historical artifact. The YAML file drifts from the architecture it describes. This section is about preventing that rot through two complementary approaches – automated triggers that catch structural changes, and process integration that catches everything automation can’t see.

Automation Triggers

The best trigger is one that fires without anyone having to remember it exists. If your threat model lives in the repo alongside your code and infrastructure definitions – and after the previous section, it should – then your CI pipeline already has the context it needs to detect when the model might be stale.

Architecture changes are the most direct trigger. When a PR adds a new service to technical_assets, modifies a communication_links block, or introduces a new trust boundary, the threat model YAML itself is changing – and that change should automatically kick off a re-analysis. This is the easiest trigger to implement because you already built it in the CI section above. The paths filter on your GitHub Actions workflow catches modifications to threagile.yaml and runs the analysis. If someone adds a new microservice and updates the threat model in the same PR, the pipeline validates the updated model before it merges. If they add the service without updating the model, the model is now stale – and that’s where the next three triggers come in.

Dependency updates should trigger a threat model review when they’re significant enough to change your security posture. I’m not talking about every patch bump in your lockfile – I’m talking about swapping your auth library from Passport to a custom OAuth implementation, or adding a new ORM that changes how you talk to the database. Detect these by watching package manifest changes (package.json, go.mod, requirements.txt, Gemfile) in CI and flagging PRs that modify them for a threat model review. Not every dependency change warrants a full re-analysis, but a human should glance at the diff and ask: does this change how we handle authentication, data access, or external communication? If yes, the threat model needs an update.

Infrastructure changes are the trigger most teams miss. A new network path, a changed trust boundary in your Pulumi stack, a modified firewall rule, a new VPC peering connection – any of these can invalidate assumptions your threat model makes about isolation and access control. If your infrastructure-as-code lives in the same repo as your threat model, the CI paths filter catches these automatically. If it lives in a separate repo, set up a cross-repo trigger or a webhook that flags the threat model as potentially stale when infra changes land. The gap between “what the threat model assumes about the network” and “what the network actually looks like” is where attackers live.

Scheduled re-analysis is your safety net. Even with all three triggers above, incremental changes slip through – a config flag that opens a new code path, a feature toggle that enables an integration nobody modeled, a DNS change that routes traffic through a new provider. Run your Threagile analysis on a schedule – weekly for high-risk systems, monthly for everything else – to catch drift that no individual trigger detected. This is the “trust but verify” layer.

Here’s a minimal example that combines path-based triggers with a scheduled run:

on:
  pull_request:
    paths:
      - 'threagile.yaml'
      - 'infra/**'
      - '**/Dockerfile'
      - 'package.json'
      - 'go.mod'
      - 'requirements.txt'
  schedule:
    - cron: '0 9 * * 1'   # Every Monday at 9 AM UTC

That covers both modes: reactive triggers when architecture-relevant files change, and a weekly scheduled run that catches everything else. Adjust the cron cadence based on how fast your architecture evolves.

Process Cadence

Automation catches file changes. It doesn’t catch context changes – a new compliance requirement, a shift in your threat landscape, an acquisition that changes your data processing obligations, a business pivot that introduces a new class of user. For those, you need process.

Quarterly threat model review is the anchor. Tie it to a security review cadence you already have – don’t create a new meeting for this. Once a quarter, pull up the threat model, compare it against the current architecture, and ask three questions: are there components in the model that no longer exist? Are there components in the system that aren’t in the model? Have the risk ratings changed based on what we’ve learned in the last 90 days? Re-run the Threagile analysis, review the output, and update anything that’s stale. This is a one-hour session, not a day-long offsite. The automation keeps the model roughly current between reviews; the quarterly session corrects whatever automation missed.

Architecture Decision Records (ADRs) are your checkpoint for significant changes. An ADR is a short document that captures a technical decision, its context, and its consequences – teams use them to record why they chose Kafka over RabbitMQ, or why they moved from a monolith to microservices. When your team writes an ADR for an architecture change, the template should include a threat model impact section: “Does this decision change trust boundaries, data flows, or external integrations? If yes, has the threat model been updated?” This forces the question at the moment the decision is made, not three months later when nobody remembers the reasoning.

Feature design reviews are where you catch smaller changes that don’t rise to the level of an ADR but still affect the threat model. Not every feature needs a full threat modeling session – that’s how you burn out your team on the practice. Instead, include a lightweight threat model impact check in your design review process. Does this feature introduce new data flows? Does it cross a trust boundary? Does it integrate with a new external service? If the answer to any of those is yes, update the model. If the answer to all three is no, move on. This takes two minutes, not two hours.

Post-incident review closes the loop. After a security incident, go back to the threat model and check whether it covered the attack path. If the model identified the risk and the issue was in mitigation execution – you knew about the risk but the control failed or wasn’t implemented – then the model is working and the gap is operational. If the model didn’t cover the attack path at all, you’ve found a blind spot. Either way, the incident directly informs the next threat model update. Every incident is a test of your model’s completeness, and the results of that test should feed back into the model itself.

Automation catches structural drift – file changes, dependency shifts, infrastructure modifications, the passage of time. Process catches context changes – new regulations, an evolving threat landscape, business pivots, acquisitions, incidents that reveal blind spots. You need both. Neither alone is sufficient.

What’s Next

You now have threat models that live in code, run in pipelines, and feed your security toolchain. The last piece: making sure the people who control the budget understand what those models are telling them.

Part 3 tackles the communication problem – translating technical threat model outputs into risk narratives that executives and board members can act on. We’ll cover dashboard design, risk aggregation across teams, and the conversations that turn threat model data into funded security initiatives. If you’ve ever struggled to explain why a trust boundary crossing matters to someone who signs purchase orders, that article is for you.

Part 3: Presenting to Leadership

Part 1: The Playbook I Wish Existed

Moose is a Chief Information Security Officer specializing in cloud security, infrastructure automation, and regulatory compliance. With 15+ years in cybersecurity and 25+ years in hacking and signal intelligence, he leads cloud migration initiatives and DevSecOps for fintech platforms.