Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ open as-is; see [Formats & interoperability](docs/formats.md).
- **Version like code**: semantic `diff`, three-way `merge`, and `git-setup` to wire both into
your repo, plus declarative `apply`/`export` manifests for reproducible models.
- **Convert** between `.tm7`, `tmforge-json`, draw.io, and Visio.
- **Import OWASP Threat Dragon v2 JSON** with stable identities and authored threats. The initial
[bounded subset](docs/formats.md#threat-dragon-owasp-threat-dragon-v2-import) supports rectangular
boundaries and directed flows; native Threat Dragon export is not supported.
- **Report** to self-contained HTML (with inline SVG diagrams), or `render` the diagram right
in your terminal.
- **Analyze in CI** with the `tmforge` CLI (`tmforge analyze`), gating builds on SARIF-reported
Expand Down
5 changes: 5 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,11 @@ tmforge report payments.tm7 --rules ./corporate.tmrules.json --out payments.html

Convert between formats. The target is chosen by `--to` or inferred from the `--out` extension.

OWASP Threat Dragon v2 JSON is an additional **input-only** format, detected from its content.
Use `tmforge convert dragon.json --to tmforge-json --out imported.tmforge.json` or `--to tm7`.
The initial [supported subset and refusal rules](formats.md#threat-dragon-owasp-threat-dragon-v2-import)
are deliberate: unsupported trust-boundary geometry and threat treatments are not silently changed.

```text
tmforge convert [--to <format>] [--out <path>] [--knowledge-base <file.tb7>] [--json] <input>
```
Expand Down
69 changes: 63 additions & 6 deletions docs/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ consume them, and each provider declares how faithfully it round-trips.
| `tmforge-json` | `.tmforge.json` | Threat Model Forge JSON (canvas wire model) | Yes | Yes | Structural |
| `drawio` | `.drawio` | draw.io / diagrams.net | Yes | Yes | Structural |
| `vsdx` | `.vsdx` | Microsoft Visio | Yes | Yes | Structural |
| `threat-dragon` | `.json` (content-detected) | OWASP Threat Dragon v2 | Yes, bounded subset | No | Import only |

List these at runtime with `tmforge` (via conversion targets) or the API's `GET /v1/formats`, which
returns each provider's capabilities and fidelity note.
Expand Down Expand Up @@ -57,8 +58,11 @@ validated the same as one authored with custom attributes.
The shape Studio and the API speak: elements, flows, trust boundaries, names, and geometry, plus an
optional analysis selection (disabled packs/rules) and an author-owned threat overlay, risk
acceptance, and per-threat edits (state, priority, mitigation, description), plus manually-authored
threats. Multi-page models carry a `diagrams` array (one entry per page, with its name); single-page
models keep the flat `elements`/`flows` shape, so older readers keep working. Knowledge-base
threats. An optional `metadata` object retains the model name, owner, description, contributors, and
reviewer. Imported manual threats carry informational provenance in their optional `source` object.
Multi-page models carry a `diagrams` array (one entry per page, with its name); a named or explicitly
identified single page also retains that array. The flat `elements`/`flows` mirror the first page for
older readers, and legacy flat documents remain readable. Knowledge-base
attributes and the full generated-threat register are not represented, the register is regenerated
from the rules on demand, and only the author-owned overlay round-trips so it's structural rather
than lossless. Use it to bridge Studio and the CLI.
Expand All @@ -78,6 +82,58 @@ per-shape **Visio Shape Data** (visible in Visio's Shape Data pane) and re-impor
properties. The rich threat model itself is not reconstructed, so the mapping is structural. Import
recognizes packages this provider wrote and the documented master/shape convention.

### `threat-dragon` (OWASP Threat Dragon v2 import)

This is a **read-only, bounded import**, not native Threat Dragon editing or a lossless conversion.
The reader recognizes the `summary` / `detail.diagrams` JSON envelope and accepts major version 2.
It does not claim the generic `.json` extension, execute foreign rules, or fetch external resources.
An unsupported construct refuses the entire import rather than returning a partial diagram.

**Preserved:** named pages and stable identities; actors, processes, stores and rectangular trust
boundaries; directed, page-local flows; model title/owner/description/contributors/reviewer; and
authored threats with title, description, category, mitigation, priority, supported state and scope.
Both `data.threats` and the cell-level `threats` schema variant are accepted, but two nonempty threat
arrays on the same cell are ambiguous and refused.

Imported threats use `manual:threat-dragon.<source-id>` keys and remain independent of generated
tmforge threats. Source format/version, threat/cell/diagram ids, methodology, original status,
severity and custom score are retained as provenance. Non-STRIDE category text is preserved, not
coerced into STRIDE; importing a LINDDUN category does not install a LINDDUN evaluator. Source ids
must fit the manual-id vocabulary; malformed or duplicate identities are refused.

**Control mapping:** explicit store booleans map `storesCredentials` to `StoresCredentials`, `isALog`
to `StoresLogData`, `isSigned` to `Signed`, and `isEncrypted` to `Encrypted=At-rest` or `No`.
Actor `providesAuthentication` maps to `AuthenticatesItself`; flow `protocol` is preserved verbatim.
Missing values remain missing. Flow encryption alone does not invent a protocol such as TLS.
Other cell data is preserved under `ThreatDragon.data.*`, alongside source identity properties.
**Out-of-scope flags are retained as information, not applied as tmforge suppressions.** The imported
model is analyzed under the selected tmforge rules, not Threat Dragon's detection semantics.

**Refused in this first delivery:** curved/line trust boundaries, unknown cell kinds, bidirectional
or detached flows, cross-page endpoints, fractional/out-of-range rectangles, and unmappable threat
statuses or priorities. Supported statuses are `Open`, `Mitigated`, and `Accepted`; supported
priorities are `Critical`, `High`, `Medium`, and `Low`. For example, `NA`, `Transferred`, `Avoided`
and `Eliminated` are not silently rewritten to `Accepted`. Geometry must use integer coordinates
within +/-1,000,000 and dimensions from 1 to 100,000; rounding could alter a trust claim.

Limits are 8 MiB of UTF-8 JSON, depth 64, 128 diagrams, 10,000 cells, and 20,000 authored threats.
Style, thumbnails, free-form routing/vertices and unlisted document fields are not retained.
Keep the original file. In particular, the upstream v2 demo includes curved boundaries and is
deliberately refused by this first delivery; replacing those boundaries requires a human review,
not automatic conversion to rectangles.

```bash
tmforge open dragon.json
tmforge convert dragon.json --to tmforge-json --out imported.tmforge.json
tmforge convert dragon.json --to tm7 --out imported.tm7
```

Studio **Open File**, the HTTP read endpoint, WASM and MCP use the same reader. Studio detaches the
source file handle and proposes a new `.tmforge.json` name for Save. Native `--to threat-dragon`
output is refused without overwriting the destination. Schema/sample grounding uses
[Threat Dragon v2.6.2](https://github.com/OWASP/threat-dragon/tree/v2.6.2/ThreatDragonModels); tests use
a synthetic fixture covering the supported subset, not a claim that every v2 model is importable.

## Converting

### CLI
Expand All @@ -99,7 +155,7 @@ See the [CLI reference](cli-reference.md#convert).

### API

```
```http
POST /v1/model/convert?to=<format> # convert to any format
POST /v1/model/export/tm7 # export a .tm7 specifically
POST /v1/detect # sniff a file's format from its bytes
Expand All @@ -109,9 +165,10 @@ See the [API reference](api-reference.md).

### Studio

Studio round-trips through `tmforge-json` (**Export tmforge-json** / **Import JSON**). For `.tm7`,
`.drawio`, or `.vsdx`, convert with the CLI or the API. The browser canvas itself doesn't parse file
formats. See the [Studio guide](studio-guide.md#importing-and-exporting).
Studio round-trips through `tmforge-json`. **Open File** uses the active API or in-browser WASM
engine to read `.tm7`, `.drawio`, `.vsdx`, and supported Threat Dragon JSON. Read-only inputs save
as new tmforge files, not back to their original format. The browser canvas itself does not parse
foreign file formats. See the [Studio guide](studio-guide.md#importing-and-exporting).

## Choosing a format

Expand Down
17 changes: 17 additions & 0 deletions docs/studio-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,23 @@ it. To change the model, edit the manifest and re-apply, or save the model as it
> `tmforge apply`, but Studio cannot recognize it: every manifest field is optional, so a recognizer
> that accepted an absent envelope would claim any JSON file. Add the envelope to open it here.

### Opening Threat Dragon v2 JSON

**Open File** recognizes supported OWASP Threat Dragon v2 JSON through the active engine. Imported
threats appear as manual entries alongside tmforge-generated threats after **Analyze**, retaining
their original category, text, treatment and scope. Model metadata and threat provenance survive
saving, reopening and exporting to `.tm7`.

This is import-only: Studio does not bind the original file for overwriting. **Save** offers a new
`.tmforge.json` filename, and Threat Dragon is not offered as an export target. Keep the original
for Threat Dragon-specific content such as styling and routing.

The first delivery supports rectangular boundaries and directed flows. Curved boundaries,
bidirectional flows, fractional rectangles and unsupported treatment states are refused, with no
partial replacement of the current workspace. Out-of-scope flags remain source information and do
not disable tmforge rules. See the [full import contract](formats.md#threat-dragon-owasp-threat-dragon-v2-import)
before migrating a model.

## Merging edits from two branches

When two people edit the same model on different branches, click **Merge** in the toolbar to
Expand Down
2 changes: 2 additions & 0 deletions src/ThreatModelForge.Api/InputErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ThreatModelForge.Api
{
using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -81,6 +82,7 @@ private static bool IsCallerInput(Exception exception)

// Uploaded bytes that are not the model document they claim to be.
case JsonException:
case InvalidDataException:
return true;
default:
return false;
Expand Down
83 changes: 83 additions & 0 deletions src/ThreatModelForge.Api/openapi/v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,61 @@
}
}
},
"MetaInformation": {
"type": "object",
"properties": {
"assumptions": {
"type": [
"null",
"string"
],
"description": "Gets or sets the recorded assumptions."
},
"contributors": {
"type": [
"null",
"string"
],
"description": "Gets or sets the contributors."
},
"externalDependencies": {
"type": [
"null",
"string"
],
"description": "Gets or sets the external dependencies."
},
"highLevelSystemDescription": {
"type": [
"null",
"string"
],
"description": "Gets or sets the high-level system description."
},
"owner": {
"type": [
"null",
"string"
],
"description": "Gets or sets the model owner."
},
"reviewer": {
"type": [
"null",
"string"
],
"description": "Gets or sets the reviewer."
},
"threatModelName": {
"type": [
"null",
"string"
],
"description": "Gets or sets the threat model name."
}
},
"description": "Free-text metadata describing the modeled system (owner, reviewer, assumptions, and so on)."
},
"PackDto": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -1511,6 +1566,15 @@
"string"
]
},
"source": {
"type": [
"null",
"object"
],
"additionalProperties": {
"type": "string"
}
},
"manual": {
"type": "boolean"
}
Expand Down Expand Up @@ -1656,6 +1720,15 @@
"string"
]
},
"source": {
"type": [
"null",
"object"
],
"additionalProperties": {
"type": "string"
}
},
"priority": {
"type": [
"null",
Expand Down Expand Up @@ -1832,6 +1905,16 @@
"string"
]
},
"metadata": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/MetaInformation"
}
]
},
"elements": {
"type": [
"null",
Expand Down
2 changes: 2 additions & 0 deletions src/ThreatModelForge.Engine/Authoring/AuthoringService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ public static AuthoringResultDto EditThreat(TmForgeModelDto? model, EditThreatRe
Priority = priority ?? existing.Priority,
Description = request.Description ?? existing.Description,
Mitigation = request.Mitigation ?? existing.Mitigation,
Source = existing.Source,
Category = request.Category ?? existing.Category,
Title = Resolve(request.Title, existing.Title),
ElementIds = existing.ElementIds,
Expand Down Expand Up @@ -393,6 +394,7 @@ private static TmForgeModelDto WithThreats(TmForgeModelDto source, List<ThreatSt
{
Schema = source.Schema,
Version = source.Version,
Metadata = source.Metadata,
Elements = source.Elements,
Flows = source.Flows,
Diagrams = source.Diagrams,
Expand Down
1 change: 1 addition & 0 deletions src/ThreatModelForge.Engine/EngineService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,7 @@ private static void AppendManualThreats(
Interaction = DescribeScope(ids, idToName),
State = NormalizeState(entry.State),
Justification = entry.Justification,
Source = entry.Source,
Manual = true,
});
}
Expand Down
3 changes: 3 additions & 0 deletions src/ThreatModelForge.Engine/ThreatDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public sealed class ThreatDto
/// <summary>Gets the author-set description, when present.</summary>
public string? Description { get; init; }

/// <summary>Gets informational source provenance when this is an imported threat.</summary>
public IReadOnlyDictionary<string, string>? Source { get; init; }

/// <summary>Gets a value indicating whether this threat was authored manually (not projected from a rule).</summary>
public bool Manual { get; init; }
}
Expand Down
3 changes: 3 additions & 0 deletions src/ThreatModelForge.Engine/ThreatStateDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public sealed class ThreatStateDto
/// <summary>Gets the author-set mitigation override.</summary>
public string? Mitigation { get; init; }

/// <summary>Gets informational source provenance for an imported manual threat, never rule input.</summary>
public IReadOnlyDictionary<string, string>? Source { get; init; }

/// <summary>Gets the author-set priority (<c>High</c> / <c>Medium</c> / <c>Low</c>).</summary>
public string? Priority { get; init; }

Expand Down
4 changes: 4 additions & 0 deletions src/ThreatModelForge.Engine/TmForgeModelDto.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace ThreatModelForge.Engine
{
using System.Collections.Generic;
using ThreatModelForge.Model;

/// <summary>
/// The canonical tmforge-json model posted by the editor.
Expand All @@ -13,6 +14,9 @@ public sealed class TmForgeModelDto
/// <summary>Gets the schema version.</summary>
public string? Version { get; init; }

/// <summary>Gets the author-owned model description, owner and review metadata.</summary>
public MetaInformation? Metadata { get; init; }

/// <summary>Gets the DFD elements.</summary>
public IReadOnlyList<TmForgeElementDto>? Elements { get; init; }

Expand Down
Loading
Loading