Threat modeling that lives with your code.

The dragon, evolved โ€” threat models as code.

Wyrm takes OWASP Threat Dragon's mission and moves it into your codebase. A threat model is a plain Open Threat Model file in the repo โ€” versioned, diff-reviewable, and enforced in CI. Wyrm parses it, validates it, runs a STRIDE rule engine over it, and renders a data-flow diagram from it.

See it work

A real .threatmodel/shop.otm.yaml โ€” a checkout across four trust zones. Wyrm renders the data-flow diagram and finds the flaws; the flagged flows light up red (critical) and amber (medium), and the session cache is flagged for sitting in the DMZ.

Rendered from the model  ยท  wyrm diagram
flowchart LR
  subgraph tz_internet["Internet"]
    browser["Browser"]
  end
  subgraph tz_dmz["DMZ"]
    gateway["API Gateway"]
    cache["Session Cache"]
  end
  subgraph tz_private["Private Network"]
    orders["Orders Service"]
    payments["Payments Service"]
  end
  subgraph tz_data["Data Tier"]
    db["Orders DB"]
  end
  browser -->|"Submit order"| gateway
  gateway -->|"Place order"| orders
  orders -->|"Charge card"| payments
  orders -->|"Persist order"| db
  gateway -->|"Read session"| cache
  classDef flagged stroke:#ff5c6c,stroke-width:2px;
  class cache flagged
  linkStyle 0 stroke:#ff5c6c,stroke-width:3px
  linkStyle 1,3 stroke:#ffd24d,stroke-width:2px
      
STRIDE findings  ยท  wyrm analyze
CRIT InformationDisclosure Sensitive data crosses a boundary in cleartext (Submit order) WYRM-T001 โ€” PII + card data leave the internet zone for the DMZ unencrypted. โ†ณ Enforce TLS on this flow; tag it tls/https once encrypted. HIGH Tampering Unencrypted flow across a trust boundary (Submit order) HIGH ElevationOfPrivilege Datastore without an auth boundary (Session Cache) WYRM-T004 โ€” the cache sits in the DMZ; move it to a private zone. MED Spoofing External entity flow lacks authentication (Submit order) โ€ฆ + 2 more medium (Place order, Persist order) 6 finding(s). โ€” exits non-zero on HIGH+, failing the build.

In your editor

Editors run the same engine as a language server: open a *.otm.yaml and findings appear inline as you type. Same diagnostics in the editor and in CI โ€” they never disagree. Integrations for Zed, VS Code, IntelliJ, and Obsidian โ€” see the editor setup guide.

Wyrm diagnostics inline in the Zed editor
Inline STRIDE diagnostics in Zed. (screenshot pending โ€” see the repo's SCREENSHOTS.md)

Getting started

  1. Install the CLI

    Grab a prebuilt binary from the releases โ€” no toolchain needed:

    # Linux x86_64
    curl -sSL https://github.com/1ARdotNO/wyrm/releases/latest/download/wyrm-x86_64-unknown-linux-gnu.tar.gz | tar -xz
    # macOS: wyrm-aarch64-apple-darwin.tar.gz (Apple Silicon) ยท wyrm-x86_64-apple-darwin.tar.gz (Intel)

    Or build from source with Cargo:

    cargo install --git https://github.com/1ARdotNO/wyrm wyrm-cli
  2. Add a model โ€” or generate one

    Drop a model in .threatmodel/ at your repo root so it lives with the code:

    mkdir -p .threatmodel
    $EDITOR .threatmodel/system.otm.yaml

    Already have a docker-compose.yml? Generate a baseline from your topology, then annotate it:

    wyrm init            # โ†’ .threatmodel/<project>.otm.yaml (services, zones, flows)
  3. Analyze & diagram

    wyrm validate     # structural checks
    wyrm analyze      # STRIDE findings (exits non-zero on HIGH+)
    wyrm diagram      # Mermaid data-flow diagram
  4. Gate it in CI

    - run: cargo install --git https://github.com/1ARdotNO/wyrm wyrm-cli
    - run: wyrm analyze            # fails the build on a real design flaw

Writing a model

A model is OTM โ€” trust zones, assets, components, and dataflows. Tag a flow tls and the encryption findings clear.

otmVersion: 0.2.0
project: { id: shop, name: Shop }

trustZones:
  - { id: tz-internet, name: Internet,  risk: { trustRating: 10 } }
  - { id: tz-private,  name: Private,   risk: { trustRating: 80 } }

assets:
  - id: pii
    name: Customer PII
    risk: { confidentiality: 100, integrity: 80, availability: 50 }

components:
  - { id: api, name: API, type: web-service, parent: { trustZone: tz-internet },
      assets: { processed: [pii] } }
  - { id: db,  name: DB,  type: database,    parent: { trustZone: tz-private } }

dataflows:
  - { id: save, name: Save profile, source: api, destination: db,
      assets: [pii], tags: [tls] }

Run it in CI

Pull a prebuilt binary from a release and wire wyrm into your PRs. Each push regenerates the model from your infra, merges in any mitigations a reviewer added (they're never clobbered), commits the refresh back, and fails the check if a high/critical finding is unresolved.

name: Threat model
on: pull_request

permissions:
  contents: write        # to commit the refreshed model back to the PR

jobs:
  wyrm:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5

      - name: Install wyrm
        run: |
          curl -sSL https://github.com/1ARdotNO/wyrm/releases/latest/download/wyrm-x86_64-unknown-linux-gnu.tar.gz | tar -xz
          mkdir -p "$HOME/.local/bin" && mv wyrm "$HOME/.local/bin/"
          echo "$HOME/.local/bin" >> "$GITHUB_PATH"

      - name: Refresh the threat model (keeps your mitigations)
        run: wyrm init --from k8s/ --output .threatmodel/app.otm.yaml

      - name: Commit model updates
        run: |
          git config user.name  "wyrm[bot]"
          git config user.email "wyrm@users.noreply.github.com"
          git add .threatmodel/
          git diff --cached --quiet || { git commit -m "chore: refresh threat model"; git push; }

      - name: Gate on findings
        run: wyrm analyze .threatmodel/app.otm.yaml --fail-on high

Make Gate on findings a required status check. The severity is configurable (--fail-on medium|high|critical). When a finding blocks, a reviewer pulls the branch, adds the mitigation (a tls tag, an auth control, a top-level mitigation), and pushes โ€” the next run merges their edit, the finding clears, and the check goes green. No drawing, no separate tool: the threat model rides along with the change.

Extend the rules

The STRIDE catalogue is data, not code โ€” add your own rules without recompiling. A rule is a target, a set of predicates (AND-ed), and a finding:

# .threatmodel/rules.yaml
rules:
  - id: ACME-001
    name: Databases must not sit in an internet-facing zone
    stride: ElevationOfPrivilege
    severity: critical
    target: component            # or: dataflow
    when:
      - kind: componentKindIn
        kinds: [database]
      - kind: lowTrustZone
        maxTrust: 60
    description: Company policy ACME-001.
    mitigation: Move the database to the data tier (trustRating >= 70).

Predicates include crossesTrustBoundary, notEncrypted, carriesSensitiveData, componentKindIn, and lowTrustZone.

CLI: wyrm analyze --rules rules.yaml โ€” or just drop the file at .threatmodel/rules.yaml and it's picked up automatically.

CI (declarative): commit .threatmodel/rules.yaml and the same wyrm analyze in your pipeline loads it โ€” org-wide policies gate every PR with no extra flags.

Why not just useโ€ฆ?

EditingStorage formatModel sourceIn-repo / CI gateEngine
Threat DragonGUI canvastool-specific JSONhand-drawnnomanual
pytmPython programimperative .pyhand-codedpartialPython threat lib
Threagiletext (YAML)own YAML schemahand-writtenyesGo rules
wyrmyour editor (text)OTM (standard)auto-detected + reconciledyesRust, data-driven

OTM is a published, platform-independent standard, so a wyrm model isn't locked to wyrm. The STRIDE catalogue is data โ€” grow it without recompiling.

Roadmap

Attribution

Wyrm is MIT licensed.