Skip to content

Latest commit

 

History

History
292 lines (228 loc) · 14.2 KB

File metadata and controls

292 lines (228 loc) · 14.2 KB

Engine API reference

The Threat Model Forge engine API exposes a small, versioned /v1 HTTP surface over the real .NET engine, and serves the Studio single-page app from its root, so the API and UI ship as one hosted artifact.

The API contract is the single source of truth: the OpenAPI document at /openapi/v1.json is what Studio's typed client is generated from. A checked-in copy lives at src/ThreatModelForge.Api/openapi/v1.json.

Run

# From source: serves the built Studio SPA at the root.
dotnet run --project src/ThreatModelForge.Api        # http://localhost:5205/

# Container: the published engine API + Studio image (pulls on first run).
docker run --rm -p 8080:8080 ghcr.io/hacks4snacks/tmforge   # http://localhost:8080/

Any non-API path falls back to Studio's index.html (so client-side routes resolve), while /v1 and /openapi are matched first.

Endpoints

Method & path Tag Purpose
GET /v1/health System Liveness probe.
GET /v1/formats Formats List supported formats and their capabilities.
POST /v1/detect Formats Detect a file's format from its bytes.
GET /v1/stencils Catalog List element stencils.
GET /v1/stencil-packs Catalog List stencil packs.
GET /v1/rules Catalog List analysis rules.
GET /v1/rule-packs Catalog List rule packs.
GET /v1/rule-bundle Catalog Report which custom rule packs this host loaded, and any load diagnostics.
GET /v1/property-schema Catalog List the typed custom-property schema the rules read.
POST /v1/model/analyze Model Analyze a model and return findings.
POST /v1/model/analysis Model One analysis action: findings and threats from a single rule evaluation, plus the effective rule packs and diagnostics.
POST /v1/model/analysis-document Model Record the analysis as a versioned, reconcilable tmforge-analysis document.
POST /v1/model/threats Model Generate the STRIDE threat register (rule threats plus the model's author overlay).
POST /v1/model/threat-register Model Split the register by origin and standing: manual, current-generated, stale-generated, and entries whose rule was not part of the run.
POST /v1/model/read Model Parse uploaded bytes (base64) into the canonical model.
POST /v1/model/convert?to=<format> Model Convert a model to another format.
POST /v1/model/export/tm7 Model Export a model as a .tm7 file.
POST /v1/model/report?format=<html|svg> Report Render a model to an HTML or SVG report.
POST /v1/model/analysis-report?format=<sarif|html|json> Report Render the analysis findings as SARIF, HTML, or JSON.
GET /openapi/v1.json n/a The OpenAPI document.

<format> is one of tm7, tmforge-json, drawio, or vsdx. See Formats & interoperability.

Errors

Failures are answered as RFC 9457 problem documents (application/problem+json), and the status separates what you can fix from what you cannot:

Status Meaning Examples
400 The request was unusable as sent. Retrying it unchanged cannot help. Body that is not JSON; a missing ?to=; an unregistered format id; content that is not valid base64; uploaded bytes that are not the format they claim to be.
404 The path is not part of the /v1 API, or the content was not recognized. A mistyped endpoint; POST /v1/detect on bytes matching no known format.
500 The server failed. This one is worth reporting. Anything unexpected — the classification above is deliberately narrow, so a real fault is never disguised as your mistake.

The detail of a 400 names what was unusable, so the request can be corrected without guesswork:

{
  "status": 400,
  "title": "The request could not be processed as sent.",
  "detail": "No threat model format with id 'nonsense'.",
  "instance": "/v1/model/convert"
}

An unmatched path under /v1 answers 404 as the API rather than falling through to the Studio's HTML shell, so a mistyped endpoint fails as a client error instead of returning a page a JSON client cannot parse. Paths outside /v1 still reach the SPA, which is what makes client-side routing work.

One analysis action

Findings and threats are the same detection: a threat is a finding from a rule that declares a threat category, kept for its lifecycle (open → mitigated → accepted). Asking for them separately makes the engine evaluate every enabled rule twice for one user action, so a UI that shows both should call POST /v1/model/analysis, which evaluates once and projects both:

curl -s -X POST http://localhost:8080/v1/model/analysis \
  -H 'Content-Type: application/json' --data @model.tmforge.json
# { "findings": […], "threats": […], "rulePacks": […], "diagnostics": [] }

POST /v1/model/analyze and POST /v1/model/threats remain available and return exactly what the combined action returns for their half; they exist for callers that genuinely need only one projection, and they do not materialize the other.

Custom rule packs

Custom rules are deployment configuration, not request input: this host never loads rules from a request body, so a caller cannot inject detection logic. Name the packs (files or directories) with the TmForge:Rules setting and they are read once at startup, then applied to every rule-reading endpoint — catalogs, analysis, threats, reports, and .tm7 export — as one effective bundle:

# a single pack, or a ';'-separated list
TmForge__Rules='/etc/tmforge/corporate.tmrules.json' dotnet ThreatModelForge.Api.dll

Confirm what actually loaded before trusting a clean run:

curl http://localhost:8080/v1/rule-bundle
# { "rulePacks": [ { "id": "corporate", "version": "2.1", "fingerprint": "sha256:…", "ruleCount": 12 } ],
#   "diagnostics": [] }

A model may pin the packs it was reviewed with; a missing or changed pack becomes an error finding with rule id rule-pack-mismatch. See Custom rules on every surface.

Usage examples

Liveness

curl http://localhost:8080/v1/health

Discover capabilities

curl http://localhost:8080/v1/formats          # what can be read/written and how faithfully
curl http://localhost:8080/v1/rules            # the analysis rules
curl http://localhost:8080/v1/rule-packs       # the rule packs (core-hygiene, stride-completeness, ...)
curl http://localhost:8080/v1/property-schema  # typed custom properties the rules read
curl http://localhost:8080/v1/stencils         # authoring stencils

Detect a format

POST /v1/detect sniffs uploaded bytes and reports the matching format.

Analyze a model

POST /v1/model/analyze returns findings for a supplied model, the same rule engine tmforge analyze uses. This is how Studio's Analyze button overlays findings on the canvas.

Each finding's id is stable: {ruleId}:{diagram}:{target}:{occurrence}, where the diagram and target segments are the element ids from the request model (they read model when the finding is about the model or a whole diagram). The same model analyzed twice produces the same ids, and enabling or disabling an unrelated rule leaves the other ids alone — so a caller can reconcile a finding against a previous run. See finding identity for the details and the one caveat.

Record the analysis

POST /v1/model/analysis-document returns a versioned tmforge-analysis document — the analysis as evidence rather than as something to render:

{
  "schema": "tmforge-analysis",
  "version": 1,
  "model":    { "name": "Payments", "fingerprint": "sha256:b5c79b9a…" },
  "analyzer": { "name": "ThreatModelForge.Analysis", "version": "0.7.0.0", "fingerprint": "sha256:6f6d4de7…" },
  "rulePacks": [],
  "findings": [
    {
      "id": "TM1013:7e3f1d52…:f1:0",
      "ruleId": "TM1013",
      "severity": "warning",
      "message": "",
      "diagram": "7e3f1d52…",
      "elementIds": ["f1"],
      "disposition": "generated-threat",
      "threatId": "80f5b13b…:TM1013"
    }
  ],
  "diagnostics": []
}

Every finding carries exactly one disposition:

Disposition Meaning
generated-threat Threat-bearing and not yet triaged.
unresolved Threat-bearing and explicitly marked as needing investigation.
accepted Threat-bearing and accepted as a risk.
mitigated Threat-bearing and mitigated.
hygiene The rule declares no threat category, so this is a modelling-quality observation, not a risk.
suppressed A suppression silenced it; it is recorded rather than dropped.

The four threat-bearing dispositions always carry a threatId that joins to POST /v1/model/threats; hygiene and suppressed never do. That separation is the point: a reviewer should not have to accept "this diagram has no trust boundary" as a risk in order to clear a gate.

Two things make the document comparable between runs. The fingerprints let a consumer detect that a stored analysis no longer describes the model or rule selection in front of it — the model fingerprint covers the structural model only, so triaging a threat does not report the model as changed, while loading a pack or disabling a rule does move the analyzer fingerprint. And the document carries no timestamp, so two analyses of the same inputs are byte-identical and a diff shows only what actually changed. When a run happened is something CI already records; putting it here would cost the property the artifact exists for.

Generate threats

POST /v1/model/threats returns the model's STRIDE threat register the same rule findings as analyze, framed as threats and overlaid with the model's author-owned state. The request model's threats overlay carries risk acceptance, per-threat edits (state, priority, mitigation, description), and manually-authored threats (manual: true, keyed in the reserved manual: namespace, scoped to element ids or model-wide). Those edits and manual threats round-trip into the exported .tm7 register, so a threat accepted or authored in Studio opens in the Microsoft Threat Modeling Tool. This powers Studio's threat panel and the tmforge threats verb.

A manual entry whose id is not a usable identity — malformed, or already claimed by another threat — is reported in diagnostics rather than silently dropped, and the first entry to claim an id keeps it.

Split the threat register

POST /v1/model/threat-register classifies every register entry against one analysis run and returns the counts plus a per-entry state:

State Meaning
manual An author wrote it. Rules never produce or retire it.
current-generated The current rules still produce it.
stale-generated Stored, but the rule ran and no longer produces it.
indeterminate-generated Its rule was not part of this run, so its standing cannot be judged.

Applying a generation result never deletes, so triage survives every re-run — which is exactly why a left-over entry needs to be distinguishable from a live one. Staleness is only ever claimed when the rule actually ran; an entry whose rule is missing or disabled is counted in indeterminateGenerated and its rule named in unavailableRuleIds, never assumed stale.

The counts are not a partition and must not be summed. What "stored" means also depends on the format: a .tm7 carries the full generated register, while canonical tmforge-json persists only author-owned state — so on this endpoint staleGenerated is most useful as triage that no longer matches any threat the rules produce.

Convert / export

# Convert (target chosen by the `to` query parameter):
#   POST /v1/model/convert?to=drawio
# Export a .tm7 specifically:
#   POST /v1/model/export/tm7

Both .tm7 paths embed the Threat Model Forge knowledge base and write typed properties, so the exported file opens in the Microsoft Threat Modeling Tool. See Formats.

Report

POST /v1/model/report?format=html (or format=svg) renders a report from a model, the hosted equivalent of tmforge report. Multi-page models render every diagram: the HTML report has one section per page, and the SVG stacks the pages. HTML reports generate the enabled rule-backed threats on demand, overlay the model's manual threats and triage, and show rule id, STRIDE category, scope, priority, mitigation, references, and decision note. SVG output renders diagrams only.

POST /v1/model/analysis-report?format=sarif (or html / json) renders the analysis artifacts instead — the findings evidence, not the threat-model document. These are the same artifacts tmforge analyze --reportFolder writes, so a report served here and a file written in CI are the same document. An unrecognized format falls back to the readable findings HTML.

curl -s -X POST 'http://localhost:8080/v1/model/analysis-report?format=sarif' \
  -H 'Content-Type: application/json' --data @model.tmforge.json -o findings.sarif

Both report endpoints run the host's configured rule packs and honor the model's own disabled selection, so a report always matches the analysis it claims to describe.

OpenAPI & client generation

The document is produced by the ASP.NET Core OpenAPI integration and served at /openapi/v1.json. After changing the /v1 contract, refresh the checked-in copy so Studio's generated client stays in sync (Studio's npm run gen:api reads that file). See the Studio guide.

Notes for hosting

  • The container listens on port 8080; from source it listens on 5205.
  • In development the API permits CORS from the Studio dev server at http://localhost:5199.
  • The API is stateless: it operates on the model bytes you send it, so it scales horizontally. See Deployment.

See also