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
24 changes: 24 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ and `/openapi` are matched first.
| `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/preflight?to=<format>` | Model | Check source bytes and optionally preview conversion losses without writing or analyzing. |
| `POST /v1/model/manifest` | Model | Materialize a declarative authoring manifest into a model (the `tmforge apply` build). |
| `POST /v1/model/layout` | Model | Return geometry-only updates after preserving every boundary membership and actual flow crossing; unsafe candidates are refused atomically. |
| `POST /v1/model/convert?to=<format>` | Model | Convert a model to another format. |
Expand Down Expand Up @@ -77,6 +78,29 @@ An unmatched path under `/v1` answers `404` as the API rather than falling throu
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.

## Document preflight

Send the same `{ "contentBase64": "...", "formatId": "tmforge-json" }` payload used by the read
endpoint to `POST /v1/model/preflight`. `formatId` is optional; it can also be `tmforge-manifest` for
explicitly selected legacy manifests. The optional `to` query parameter selects a writable conversion
target. No rule evaluation, file write, or remote content resolution occurs.

The response is `{success,format,targetFormat,diagnostics}`. Each diagnostic contains a stable
`code`, `severity` (`error`, `warning`, `info`), source `path`, and actionable `message`. JSON
diagnostics use JSONPath locations; foreign-reader failures may include a provider-specific location
in their message. An assessment that finds input errors still returns HTTP **200** with
`success: false`. Invalid request JSON or base64 remains an ordinary **400** problem response.

Warnings indicate known omissions or changes, not security findings. Inspect them before calling
`/v1/model/read` or `/v1/model/convert`; those existing response shapes are unchanged. For an import
into Studio's canonical model, select `to=tmforge-json`. The limit is 8 MiB of decoded input and
100 diagnostics; JSON readers also cap nesting at 64. The final diagnostic is an error when the
diagnostic budget is exhausted, so a truncated result cannot look successful.

The WASM `Preflight(contentBase64, formatId, targetFormat)` export returns the same result; empty
strings omit the two format selections. MCP exposes `preflight(path, format?, to?)` with the existing
workspace sandbox and archive limits. Neither operation accepts rule content or changes models.

## One analysis action

Findings and threats are the same detection: a threat is a finding from a rule that declares a threat
Expand Down
47 changes: 47 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ tmforge <command> [options] <file>
| Command | Kind | Purpose |
| --- | --- | --- |
| [`open`](#open) | Inspect | Summarize a model (element / flow / threat counts). |
| [`preflight`](#preflight) | Inspect | Check document integrity and preview conversion losses without writing. |
| [`list`](#list) | Inspect | List components, flows, boundaries, threats, or diagrams. |
| [`show`](#show) | Inspect | Show one element/flow's name, type, and properties. |
| [`stencils`](#stencils) | Inspect | List the built-in authoring stencils. |
Expand Down Expand Up @@ -99,6 +100,43 @@ would invite deleting real findings after a mistyped `--rules` path or a disable
The full register lives in `.tm7`. `tmforge-json` deliberately persists only author-owned state
(triage and manual threats), so `persistedGenerated` is zero for a model held in that format.

### `preflight`

Check whether a model or authoring manifest can be interpreted, and optionally preview known losses
for a conversion target. This operation does **not** evaluate security rules, write files, repair
the source, or mark a threat mitigated.

```text
tmforge preflight <file> [--format <id>] [--to <id>] [--json]
```

```bash
tmforge preflight examples/webshop.tm7 --to tmforge-json
tmforge preflight examples/webshop.manifest.json --format tmforge-manifest --json
```

`--format` selects a registered reader or `tmforge-manifest`. Explicit selection is required for a
legacy manifest without a schema; ambiguous JSON is not guessed into an empty model.
Exit codes are **0** for no blocking diagnostics (warnings may remain), **2** for structural errors,
and **1** for usage or file-access errors. The JSON envelope's `data` contains `success`, `format`,
`targetFormat`, and `diagnostics`, each with `code`, `severity`, `path`, and `message`.

Example diagnostic:

```json
{
"code": "model.unresolved-endpoint",
"severity": "error",
"path": "$.flows[0].target",
"message": "Flow 'request' refers to 'missing', which is not an element on this page. Correct the target reference; the flow will not be dropped."
}
```

Preflight accepts at most 8 MiB of input and returns at most 100 diagnostics. JSON nesting is limited
to 64 levels. A diagnostic-limit error means the assessment is incomplete; correct the reported
problems and rerun it. See [preflight and fidelity](formats.md#preflight-and-import-diagnostics) for
what the checks cover and what remains outside their scope.

### `list`

List entities of a chosen kind.
Expand Down Expand Up @@ -887,6 +925,15 @@ 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.

Conversion now performs preflight before opening the destination. Errors refuse the conversion;
warnings are printed to stderr and included in `data.diagnostics` with `--json`. Add
`--fail-on-loss` to refuse warnings too (exit **2**), leaving any existing destination untouched.
Use `preflight --to <format>` to inspect the same diagnostics without producing output.

```bash
tmforge convert model.tm7 --to drawio --out model.drawio --fail-on-loss
```

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)
Expand Down
48 changes: 48 additions & 0 deletions docs/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,49 @@ output is refused without overwriting the destination. Schema/sample grounding u
[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.

## Preflight and import diagnostics

Run `tmforge preflight <file> [--to <format>]` before migrating a model. It inspects raw input before
deserialization can hide misspelled fields, duplicate identities, or unresolved flows, then reports
known losses for the selected destination. Preflight is **document validation, not security
analysis**. A passing result does not claim that the design is secure or every foreign extension
can round-trip.

- Canonical JSON rejects duplicate JSON fields, empty or colliding page/element/flow identities,
unknown element kinds, invalid field types, incomplete component sizes, and dangling/cross-page
flows. References are checked before any connector can disappear during reconstruction.
- Manifest JSON rejects unknown fields, including `properties` where `props` is required. Custom
keys inside `props` remain governed by existing property policy. `--force` does not bypass field
spelling or JSON integrity checks. Legacy unversioned manifests remain supported when explicitly
selected.
- Canonical extension fields remain readable for compatibility, but receive path-specific warnings
that they are not represented by the engine and may be lost on conversion. Studio's existing
flow handles and label offsets, custom property bags, and expected rule-pack fingerprints are
recognized fields, not spelling errors.
- Draw.io input with missing/compressed graph content is refused rather than imported as an empty
diagram. Use an uncompressed XML export. Duplicate cells and broken attached-flow endpoints are
refused; preflight names free-standing lines omitted by the reader and unfamiliar shapes whose
kinds are inferred.
- Visio preflight names shapes treated as annotations rather than model objects. Invalid/duplicate
shape ids and broken connector attachments are refused; the existing bounded page-catalog and
archive checks remain in force.

Conversion diagnostics identify known losses such as line boundaries and embedded knowledge bases
when projecting into canonical JSON; threat-register, property, identity and metadata loss when
exporting diagrams; rule settings not carried by a conversion; and TM7 coordinate translation.
These are conservative checks of the supported mappings, not a complete semantic diff or an
openability guarantee from another product. Retain the source document when warnings apply.

The command, API, WASM and MCP return the same diagnostic codes, severities, paths and messages.
Studio reviews warnings before **Open File**, **Save** with an engine, or **Export** continues;
blocking errors cannot be accepted. Rejection and cancellation leave the current workspace
unchanged. Native JSON saves retain the Studio wire model rather than performing a format
conversion, so they do not warn about losing their own analysis settings.

Preflight and CLI conversion accept at most 8 MiB of source content. Canonical JSON reads are strict
UTF-8 with an optional BOM and a nesting limit of 64. At most 100 diagnostics are returned, with an
explicit error if the diagnostic budget is exhausted. Correct the reported problems and rerun.

## Converting

### CLI
Expand All @@ -144,6 +187,11 @@ tmforge convert <input> --to <format> --out <path>

The target is chosen by `--to`, or inferred from the `--out` extension.

Errors found by preflight block conversion before the output file is opened. Warnings are emitted
before writing and included in `--json` output. Use `--fail-on-loss` to refuse warnings as well, or
the read-only `preflight --to` command to review them first. Existing source and output files are
not changed by preflight or by a refused conversion.

```bash
tmforge convert model.tm7 --to drawio --out model.drawio
tmforge convert model.tm7 --to vsdx --out model.vsdx
Expand Down
13 changes: 13 additions & 0 deletions docs/studio-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,19 @@ then `tmforge analyze` / `tmforge report` / `tmforge convert` in a pipeline, or
> or `.drawio` itself. Use the API's `convert` / `read` endpoints or the CLI for those. Studio
> speaks `tmforge-json`; the engine handles every other format behind `/v1`.

### Preflight review

Before replacing the canvas, **Open File** runs preflight through the active engine. Structural
errors appear with their source paths and must be corrected in the input. Known import losses appear
in a review dialog with **Continue** and **Cancel**. Closing or cancelling leaves the current model
and undo history unchanged; a delayed import is discarded if the workspace changes while it runs.

**Save** and **Export** also review known conversion losses before writing. Native JSON saves do not
perform the engine's structural conversion, so they retain the existing wire state. Importing a new
file requires the API or WASM engine to be ready; offline authoring and saving the current JSON
workspace remain available. These diagnostics are separate from **Analyze** and do not accept or
mitigate threats. See [preflight coverage and limits](formats.md#preflight-and-import-diagnostics).

### Opening an authoring manifest

**Open File** also accepts a declarative [authoring manifest](cli-reference.md#apply) — the
Expand Down
6 changes: 6 additions & 0 deletions src/ThreatModelForge.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ public static void Main(string[] args)
EngineService.ReadModel(Convert.FromBase64String(file.ContentBase64), file.FormatId)))
.WithName("ReadModel")
.WithTags("Model");
app.MapPost(
"/v1/model/preflight",
(FileContentDto file, string? to) => TypedResults.Ok(
DocumentPreflight.Inspect(Convert.FromBase64String(file.ContentBase64), file.FormatId, to)))
.WithName("PreflightModel")
.WithTags("Model");

// A declarative authoring manifest is a threat model's reviewable source, not one of the
// registered model formats, so /v1/detect cannot claim it and /v1/model/read cannot parse
Expand Down
87 changes: 87 additions & 0 deletions src/ThreatModelForge.Api/openapi/v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,45 @@
}
}
},
"/v1/model/preflight": {
"post": {
"tags": [
"Model"
],
"operationId": "PreflightModel",
"parameters": [
{
"name": "to",
"in": "query",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileContentDto"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PreflightResultDto"
}
}
}
}
}
}
},
"/v1/model/manifest": {
"post": {
"tags": [
Expand Down Expand Up @@ -795,6 +834,28 @@
}
}
},
"DocumentDiagnostic": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Gets the stable diagnostic code."
},
"severity": {
"type": "string",
"description": "Gets the severity: error, warning or info."
},
"path": {
"type": "string",
"description": "Gets the location in the source document, using JSONPath for JSON input."
},
"message": {
"type": "string",
"description": "Gets the actionable diagnostic text."
}
},
"description": "A structural input or conversion diagnostic, separate from security findings."
},
"ExpectedRulePackDto": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -1322,6 +1383,32 @@
},
"description": "Describes a stencil pack: a named, togglable group of related stencils (for example, the\nAzure pack). The palette uses packs so the user can show or hide whole families at once."
},
"PreflightResultDto": {
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"format": {
"type": [
"null",
"string"
]
},
"targetFormat": {
"type": [
"null",
"string"
]
},
"diagnostics": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DocumentDiagnostic"
}
}
}
},
"PropertyDescriptor": {
"type": "object",
"properties": {
Expand Down
3 changes: 2 additions & 1 deletion src/ThreatModelForge.Cli/CommandCatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal static class CommandCatalog
private static readonly IReadOnlyList<CommandInfo> All = new List<CommandInfo>
{
new CommandInfo("open", "Summarize a threat model (counts of elements, flows, and threats).", "name, owner, source, format{id,name}, diagramCount, componentCount, connectorCount, trustBoundaryCount, threatCount, diagrams[]", OpenCommand.Run),
new CommandInfo("preflight", "Check model integrity and preview conversion losses without writing files.", "success, format, targetFormat, diagnostics[]{code,severity,path,message}", PreflightCommand.Run),
new CommandInfo("list", "List components, flows, boundaries, threats, or diagrams.", "kind, count, items[]", ListCommand.Run),
new CommandInfo("show", "Show an element/flow's name, type, and custom properties.", "id, kind, name, stencil, stencilLabel, properties{}", ShowCommand.Run),
new CommandInfo("diff", "Show a structural diff between two models (added/removed/modified).", "summary, added[], removed[], modified[]", DiffCommand.Run),
Expand All @@ -36,7 +37,7 @@ internal static class CommandCatalog
new CommandInfo("threats", "Report threats: the persisted, triaged view of the analysis findings (--write to persist).", "summary{count,written}, threats[]{id,ruleId,category,categoryId,categoryName,stride,title,mitigation,severity,priority,references[],scope,interaction}", ThreatsCommand.Run),
new CommandInfo("accept", "Accept a generated threat's risk (marks it not-applicable with a reason).", "threat, state, reason", AcceptCommand.Run),
new CommandInfo("report", "Generate an HTML report from a threat model.", "output, format, bytes", ReportCommand.Run),
new CommandInfo("convert", "Convert a threat model between file formats.", "input, output, format", ConvertCommand.Run),
new CommandInfo("convert", "Convert a threat model between file formats.", "input, output, format, diagnostics[]{code,severity,path,message}", ConvertCommand.Run),
new CommandInfo("apply", "Build a model from a declarative JSON manifest (all-or-nothing).", "output, format, dryRun, boundaries, elements, flows", ApplyCommand.Run),
new CommandInfo("export", "Export a model as a declarative JSON manifest.", "output, boundaries, elements, flows", ExportCommand.Run),
new CommandInfo("git-setup", "Wire git to use tmforge for .tm7 diff/merge (or --print the commands).", null, GitSetupCommand.Run),
Expand Down
Loading
Loading