9 min read

Section 10

Continuous & Automated TRA

Traditional TRAs are periodic snapshots — but architectures change constantly. This section covers threat modeling as code, CI/CD pipeline integration, risk-as-code policies, and continuous risk monitoring with automated re-assessment triggers.

Related Content

See Secure SDLC Integration for embedding security into the development lifecycle. This section focuses specifically on automating the TRA process.

Threat Modeling as Code

Threat-modeling-as-code tools define system architecture in code and automatically generate threat models — making them version-controlled, reviewable, and CI/CD-integrated.

pytm (Python)

Define systems in Python, auto-generate DFDs and STRIDE threats.

  • • Pythonic DSL for system modeling
  • • Automatic STRIDE threat generation
  • • DFD diagram output (PlantUML/Graphviz)
  • • JSON/HTML report generation
  • • Open source (OWASP project)

Threagile (YAML)

YAML-based agile threat modeling with risk tracking.

  • • YAML model definition (no code required)
  • • Built-in risk rules engine
  • • PDF/Excel risk report generation
  • • Data flow and architecture diagrams
  • • Docker-based execution

MS Threat Modeling Tool

Microsoft's GUI-based tool with Azure integration.

  • • Visual DFD editor
  • • Azure/cloud-specific threat templates
  • • Built-in mitigation suggestions
  • • HTML report generation
  • • Free (Windows only)

pytm Example

payment_threat_model.py
"""
pytm Threat Model: Payment API Service
Generates STRIDE threats and DFD automatically.
"""
from pytm import TM, Server, Datastore, Dataflow, Boundary, Actor

# Define the threat model
tm = TM("Payment API - Threat Model")
tm.description = "TRA for payment processing microservice"
tm.isOrdered = True

# Trust boundaries
internet = Boundary("Internet")
dmz = Boundary("DMZ")
internal = Boundary("Internal Network")
db_zone = Boundary("Database Zone")

# External entities
customer = Actor("Customer")
customer.inBoundary = internet

payment_provider = Actor("Payment Provider (Stripe)")
payment_provider.inBoundary = internet

# Processes
api_gateway = Server("API Gateway")
api_gateway.inBoundary = dmz
api_gateway.protocol = "HTTPS"
api_gateway.isEncrypted = True
api_gateway.sanitizesInput = True

payment_service = Server("Payment Service")
payment_service.inBoundary = internal
payment_service.protocol = "gRPC"
payment_service.isEncrypted = True
payment_service.hasAccessControl = True
payment_service.authorizesSource = True

# Data stores
payment_db = Datastore("Payment Database")
payment_db.inBoundary = db_zone
payment_db.isEncrypted = True
payment_db.isSQL = True
payment_db.hasAccessControl = True

audit_log = Datastore("Audit Log (Immutable)")
audit_log.inBoundary = db_zone
audit_log.isEncrypted = True

# Data flows
customer_to_gw = Dataflow(customer, api_gateway, "Payment Request")
customer_to_gw.protocol = "HTTPS"
customer_to_gw.isEncrypted = True
customer_to_gw.sanitizesInput = True

gw_to_service = Dataflow(api_gateway, payment_service, "Validated Request")
gw_to_service.protocol = "gRPC + mTLS"
gw_to_service.isEncrypted = True

service_to_db = Dataflow(payment_service, payment_db, "Transaction Record")
service_to_db.protocol = "TLS"
service_to_db.isEncrypted = True

service_to_provider = Dataflow(
    payment_service, payment_provider, "Payment Authorization"
)
service_to_provider.protocol = "HTTPS"
service_to_provider.isEncrypted = True

service_to_audit = Dataflow(
    payment_service, audit_log, "Audit Event"
)
service_to_audit.protocol = "TLS"
service_to_audit.isEncrypted = True

# Generate threats and DFD
tm.process()
# Output: DFD diagram + STRIDE threat list + HTML report

Threagile YAML Model

threagile-model.yaml
# Threagile YAML Model: Payment Microservice
# Run: docker run --rm -v "$(pwd)":/app/work threagile/threagile -model /app/work/model.yaml

threagile_version: 1.0.0

title: Payment Service TRA
date: 2025-01-15

business_criticality: critical

technical_assets:
  api-gateway:
    type: process
    usage: business
    technology: kong
    internet: true
    encryption: transparent
    communication_links:
      payment-service-link:
        target: payment-service
        protocol: grpc
        authentication: certificate
        encryption: transparent

  payment-service:
    type: process
    usage: business
    technology: go
    internet: false
    encryption: transparent
    data_assets_processed:
      - cardholder-data
      - transaction-records
    communication_links:
      db-link:
        target: payment-db
        protocol: postgresql
        authentication: credentials
        encryption: transparent
      stripe-link:
        target: stripe-api
        protocol: https
        authentication: token
        encryption: transparent

  payment-db:
    type: datastore
    usage: business
    technology: postgresql
    internet: false
    encryption: data-with-symmetric-shared-key
    data_assets_stored:
      - cardholder-data
      - transaction-records

data_assets:
  cardholder-data:
    usage: business
    quantity: many
    confidentiality: strictly-confidential
    integrity: critical
    availability: critical

  transaction-records:
    usage: business
    quantity: many
    confidentiality: confidential
    integrity: critical
    availability: critical

trust_boundaries:
  dmz:
    type: network-cloud-security-group
    technical_assets_inside:
      - api-gateway

  internal:
    type: network-cloud-security-group
    technical_assets_inside:
      - payment-service

  data-zone:
    type: network-cloud-security-group
    technical_assets_inside:
      - payment-db

CI/CD Integration

Integrate threat modeling into your pipeline to catch architectural risk regressions before they reach production.

.github/workflows/threat-model.yml
# GitHub Actions Workflow: Automated Threat Model Check
name: Threat Model Validation
on:
  pull_request:
    paths:
      - 'architecture/**'
      - 'threat-model/**'
      - 'infrastructure/**'

jobs:
  threat-model:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write

    steps:
      - uses: actions/checkout@v4

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

      - name: Check for High/Critical Risks
        run: |
          # Parse Threagile JSON output for risk severity
          HIGH_RISKS=$(jq '[.identified_risks[] |
            select(.severity == "high" or
                   .severity == "critical")] |
            length' threat-model/output/risks.json)

          if [ "$HIGH_RISKS" -gt 0 ]; then
            echo "::error::Found $HIGH_RISKS high/critical risks"
            echo "## Threat Model: $HIGH_RISKS Risks Found" >> $GITHUB_STEP_SUMMARY
            jq -r '.identified_risks[] |
              select(.severity == "high" or
                     .severity == "critical") |
              "- **(.severity)**: (.title)"' \
              threat-model/output/risks.json >> $GITHUB_STEP_SUMMARY
            exit 1
          fi

      - name: Upload Threat Report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: threat-model-report
          path: threat-model/output/

Risk-as-Code Policies

Codify risk policies so they are version-controlled, testable, and automatically enforced. Use Open Policy Agent (OPA) or custom scripts to evaluate risk conditions.

risk_policies.rego
# Risk-as-Code Policy (Rego / Open Policy Agent)
# Evaluates architecture decisions against risk policies

package tra.risk_policies

# Deny unencrypted data stores containing sensitive data
deny[msg] {
    some store in input.data_stores
    store.classification in ["confidential", "restricted"]
    not store.encryption_at_rest
    msg := sprintf(
        "RISK: Data store '%s' contains %s data without encryption at rest",
        [store.name, store.classification]
    )
}

# Deny internet-facing services without authentication
deny[msg] {
    some service in input.services
    service.internet_facing
    not service.authentication_required
    msg := sprintf(
        "RISK: Internet-facing service '%s' has no authentication",
        [service.name]
    )
}

# Warn on single-tenant database without backup encryption
warn[msg] {
    some store in input.data_stores
    store.business_criticality == "critical"
    not store.backup_encryption
    msg := sprintf(
        "WARN: Critical data store '%s' backups are not encrypted",
        [store.name]
    )
}

# Deny services processing PII without audit logging
deny[msg] {
    some service in input.services
    service.processes_pii
    not service.audit_logging_enabled
    msg := sprintf(
        "RISK: Service '%s' processes PII without audit logging",
        [service.name]
    )
}

Continuous Risk Monitoring

Define Key Risk Indicators (KRIs) that trigger automated re-assessment when thresholds are exceeded.

KRI Source Threshold Action
Architecture changes Git commits to architecture/ folder Any merge to main Trigger threat model diff
New critical CVEs Dependency scanning (Dependabot/Snyk) CVSS ≥ 9.0 on in-scope component Trigger vulnerability reassessment
Failed security scans SAST/DAST/IaC scanning pipeline > 5 high findings in sprint Flag for risk review
Cloud posture drift CSPM (Prowler, Prisma, Defender) Critical finding on production Escalate to risk owner
Threat intelligence CTI feeds, ISAC alerts Active exploitation of relevant TTP Trigger threat landscape update
Vendor incidents Vendor security advisories Tier 1 vendor security incident Trigger supply chain reassessment

Continuous TRA Feedback Loop

Section Summary

Key Takeaways

  • • pytm and Threagile enable version-controlled threat models
  • • CI/CD integration catches architectural risk regressions automatically
  • • Risk-as-code with OPA/Rego policies enables automated enforcement
  • • KRIs trigger re-assessment when risk conditions change
  • • Continuous TRA creates a feedback loop, not a one-time document

Evidence to Capture

An automated finding is only evidence if it can be tied to a commit and a pipeline run. A generated report with no provenance cannot be defended or reproduced.

Model source and commit: The model file, its path and the commit evaluated, so the result matches a specific architecture state rather than an approximate one.

Pipeline run record: Run identifier, branch, timestamp, tool versions and the archived report artefact.

Rule and policy version: The policy or rule set version applied. The same model produces different findings under different rules.

The generated finding itself: The threat or policy violation with the element it attaches to, not a count in a summary.

Human triage decision: Who reviewed the output, what was accepted, suppressed or escalated, and the reason given.

Indicator breach record: The indicator, threshold, observed value, source and the assessment action it triggered.

Automation False Positives

Automated threat modelling is a diff engine. It reports change and rule violations, and most of both are not risk.

  • Renamed element reported as new threat: A renamed component is treated as a new element with new findings. Check whether any flow actually changed.
  • Rules firing on out-of-scope elements: Generated entries for a component that receives no traffic drown the ones that matter. Suppress at the element, with a recorded reason.
  • Non-production branch: An indicator breach computed from a feature branch describes a risk position that will never be deployed.
  • Tool default assumptions: The modelling tools apply default trust and authentication assumptions that may not match the deployed configuration.
  • Failed policy check as a risk: A policy violation is a control gap. The risk is the event it enables, quantified separately.
  • Stale model: A model not updated with the last architecture change generates confident findings about a system that no longer exists.

Automation Trust Checklist

Before trusting a run

  • ☐ Model commit matches the deployed architecture
  • ☐ Tool and rule versions pinned in the pipeline image
  • ☐ Report artefact archived against the run identifier
  • ☐ Findings triaged by a named reviewer
  • ☐ Suppressions recorded with a rationale

Automation boundaries

  • ☐ Model covers the stack the tool actually inspects
  • ☐ Policy checks gate merges to main, not feature branches
  • ☐ Thresholds defined per indicator, with an owner
  • ☐ No risk closed by automation without human acceptance

Make the Automation Trustworthy

Treatment is tuning and gating: keep the signal high, tie every run to an artefact, and move the review trigger into the pipeline rather than a calendar.

Make runs reproducible

  • • Fail the build when the architecture model changes without regenerating the threat model
  • • Pin tool and rule versions in the pipeline image
  • • Archive the generated report as a build artefact against the run

Keep the signal high

  • • Suppress with a recorded rationale, never by deleting the element
  • • Tune rules against the last three months of triage decisions
  • • Route findings to the owning team with element context attached

Review trigger

  • • Threshold breached on a production indicator
  • • Architecture change merged to main
  • • Model or rule engine upgrade