Shreyas Sane
Explore

Coding

ThreatForge

Threat models that live in the repository they describe, written as YAML you can review in a pull request.

Role
Sole author: product direction, Rust and TypeScript implementation, release engineering
Years
Present
Built with
Tauri v2, Rust, React 19, TypeScript, Zustand, React Flow, Tailwind CSS, serde_yaml, quick-xml, aes-gcm, Model Context Protocol, Vitest, Playwright, GitHub Actions

The incumbent saves to a binary

Threat modeling has a tooling gap that is easy to describe and awkward to live with. Microsoft's Threat Modeling Tool is free, competent, and Windows-only, and it saves to .tm7, a binary format. A binary model cannot be diffed. It cannot be reviewed in a pull request. Two engineers cannot resolve a conflict in it. In practice that means the threat model stops being a living description of the system and becomes an artifact someone produces once for an audit.

The other option is an enterprise platform with a sales cycle attached. For a team that wants to start threat modeling on a Tuesday, that is not an option at all.

ThreatForge exists in the space between those two. It is a desktop application for building data flow diagrams and running STRIDE analysis over them, and it saves what you build as YAML.

The file is the product

Deciding that the saved model would be human-readable YAML determined most of the rest of the design. A threat model stored as text lives in the repository it describes. It shows up in a pull request next to the change that motivated it, where a reviewer can see that a new trust boundary was added and argue about it in a comment before anything merges. None of that needed a collaboration feature. It needed a file format that does not resist collaboration.

KeyLinesEntries
version1
metadata2–10
elements — shown below11–515
data_flows52–682
trust_boundaries69–922
threats93–1223
diagrams123–1291
Whole file129

tmt-4.3-sample.expected.yaml

Everything the importer is expected to produce from a Microsoft TMT file, in one document a person can read end to end.

version: '1.0'
metadata:
  title: Payments & Billing Platform
  author: Ada Lovelace
  created: 2000-01-01
  modified: 2000-01-01
  description: |-
    Checkout for the "Payments & Billing" platform.
    Cardholder data never leaves the <Corporate Network> boundary.
    Batch reconciliation is the finance team's responsibility.
elements:
- id: customer
  type: external_entity
  name: Customer
  trust_zone: ''
  description: ''
  position:
    x: 40.0
    y: 120.0
- id: payments-billing-api
  type: process
  name: Payments & Billing API
  trust_zone: ''
  description: ''
  position:
    x: 340.0
    y: 120.0
# …

tmt-4.3-sample.expected.yamlexit-zero-labs/threat-forge · src-tauri/src/importers/fixtures/

The importer's expected output, checked in as a fixture. Two of five elements are shown; data flows, trust boundaries, threats, and diagrams follow the same shape below them.

The schema is deserialized into typed Rust structures with explicit version checks, and the format version stays at 1.0 while the schema grows additively, so a model saved by an older build keeps opening. A format people are asked to commit to version control has to be a format that does not break them later.

Moving in from the incumbent had to work too, or the format argument is academic for anyone with existing models. A Rust importer reads .tm7 XML and converts it in one pass, so an existing Microsoft threat model arrives as a YAML file and nobody retypes a diagram.

Where the rules stop and the model starts

STRIDE analysis is rule work. Given a diagram, a deterministic engine in Rust can enumerate the categories that apply to each element and each flow, and it does. That part does not need a language model and should not have one, because the answers must be the same every time you open the file.

The model is useful after that: describing a specific threat in the context of this system, drafting a mitigation, arguing with a stated assumption. So it works on top of what the rule engine already produced.

It runs on the user's own API key, against Anthropic or OpenAI, called directly from the application over HTTPS. There is no server in the middle. That was chosen for cost, since a free tool cannot absorb inference bills, but it has a better property: a security tool that asks you to route your architecture diagrams through an unknown third party is asking for something it has not earned. Keys are stored in an AES-256-GCM encrypted file on desktop. The browser build cannot offer that guarantee, so it says so plainly instead of pretending.

Model output is treated as untrusted data. Responses are escaped, never rendered as markup, and the application runs under a strict content security policy with no inline scripts and no remote code loading. In a tool whose entire subject is untrusted input, the language model is just another untrusted input.

What the release actually ships

The premise of the project is that the tool it replaces only runs on Windows. That makes cross-platform support the whole point, not a stretch goal, and it decided the stack. Tauri v2 was chosen over Electron for two reasons: a much smaller binary, and a Rust backend that gives file access, schema validation, and key storage a home outside the webview.

v0.2.0 ships as eight artifacts from one GitHub Actions matrix: dmg for Apple Silicon and Intel, an NSIS installer and an MSI for Windows, and AppImage, deb, and rpm for Linux, with the Tauri updater pointed at GitHub Releases.

One gap is worth naming. Windows builds are signed through Azure Trusted Signing. macOS signing is wired into the release workflow and not yet enabled, so macOS users still see the unsigned-application warning. The roadmap carries it as an unmet exit gate.

One release in

ThreatForge is open source under Apache-2.0 and early. One tagged release exists, the issue list is long, and the STRIDE rule set is the part most likely to keep changing as it meets systems I did not design.

What is settled is the shape: the file format, the split between the Rust core and the webview, and the position that the analysis engine is deterministic and the model is an assistant to it.

What runs where

The webview draws and edits. Rust owns everything that touches the filesystem, the schema, and secrets. Nothing sits between the user and their AI provider.

React webview
Diagram canvas, threat analysis views, and the AI chat pane. Holds no keys and never writes to disk directly.
Rust backend
YAML read and write, typed schema validation, the STRIDE rule engine, TM7 import, and encrypted key storage.
The user's AI provider
Analysis requests, called directly over HTTPS with the user's own key. The project operates no inference endpoint.
GitHub Releases
Distribution and the update channel the Tauri updater checks.