-
Notifications
You must be signed in to change notification settings - Fork 0
Exporting
The STIX bundle is not a report you produce at the end. It is the save file. Investigations live in the IndexedDB of one browser profile on one machine, so until you have downloaded the .json, nothing on disk holds your work. The status bar says so: an investigation that has never been exported shows never exported, and one changed since its last download shows unexported changes.
Everything below happens in the browser. Nothing is uploaded. For where the data lives and what leaves, see /about.
Two ways in, and they open the same panel:
- the Export STIX button, top right of the workspace;
- the command palette (
Ctrl/Cmd+K), entry Export STIX bundle….
There is no dedicated keyboard shortcut for it.
The panel has two steps that are deliberately separate: Generate the bundle, then Download the .json. Only the download counts as an export. Opening the dialog, generating, looking at the preview and closing leaves the status bar untouched, because no file exists yet. Changing any option after generating clears the generated bundle and you have to generate again, so that you can never download a file that no longer matches the options on screen.
The file is named after the investigation, stripped of anything outside letters, digits, accented letters, space, _ and -, with .stix.json appended.
| Option | Default | What it does |
|---|---|---|
| Container | report |
Wraps every exported object in a report (finished product) or a grouping (investigation in progress). |
| TLP | TLP:AMBER |
Marking applied to the whole bundle. no marking emits none. |
| Confidence | 75 - high (analyst curation) |
Integer 0-100 written on every object that does not carry its own. none writes nothing and lets the receiving platform decide. |
| Author | empty | Creates an identity (class organization) and points created_by_ref at it on every object that can carry one. |
| Include notes and opinions | on | Whether your note and opinion objects travel with the bundle. |
TLP, author and confidence are remembered between sessions in localStorage under dmas.export-prefs. They are analyst settings, not investigation settings: you set your CERT name and your house TLP once. The container choice and the notes checkbox are not remembered and go back to report and on every time.
A report carries published (the investigation's creation date) and report_types: ["threat-report"]. A grouping carries context: "suspicious-activity" instead. The distinction is about what you are claiming: a report asserts a finished analysis, a grouping asserts "these things belong together, I am still working". Platforms treat them differently in their UI, and analysts reading your bundle will read the container type as a claim about maturity.
Both identifiers are deterministic and derived from the investigation name, so re-exporting the same investigation updates the same container rather than piling up new ones. Renaming the investigation produces a different container.
Confidence is not a decoration. On a platform that honours it, it arbitrates updates: incoming data with a lower confidence than what is already stored does not overwrite it. 75 is high enough that hand-curated work wins against most automated feeds, and low enough that it does not pretend to certainty.
none is the honest choice when you do not know the receiving platform's policy: writing nothing lets it apply its own default rather than having you guess one.
Observables never carry a confidence. STIX 2.1 has no confidence property on an SCO, so the value is dropped for those objects rather than smuggled in.
Filling the author field adds one identity object to the bundle and sets created_by_ref on the SDOs, the relationships, the container and the notes. Observables get no created_by_ref, because the spec does not allow one on an SCO.
Leaving it empty is legitimate: the bundle then attributes nothing, and the receiving platform will attribute it to whatever connector ingested it.
With the checkbox on, every note becomes a note object and every opinion an opinion object. A note attached to an entity references that entity; a note attached to nothing references the container. A note whose entity was not exported (a candidate still in the triage tray) is skipped, with a warning.
Turn it off when you are shipping to a partner and the notes are your working thinking rather than intel.
TLP and confidence set on an individual object, in the inspector or through bulk edit, take precedence over the export values for that object alone. That is how you ship a mostly TLP:GREEN bundle with two TLP:RED observables in it. Every marking definition actually used is embedded in the bundle, in a fixed order, so the file does not change shape depending on the order you created things in.
The inspector only offers TLP and confidence on SDOs. To set a TLP on an observable, select it together with at least one other object: the bulk edit panel replaces the inspector as soon as two things are selected, and it applies to everything in the selection whatever the type. The builder then reads that TLP on the observable and honours it.
The lint runs on the local state when you open the dialog and prints a list titled what would make the bundle cleaner. Nothing in it stops the export by itself. Three of its findings are advance warning that the builder will refuse anyway, and two of them say so in their own text.
The status bar also shows a lint counter, which counts only the warn level findings.
| Finding | Level | What it means |
|---|---|---|
| N candidate(s) still in the triage tray | info | Only confirmed entities are exported. The tray is not part of the bundle. |
| N relationship(s) touch a candidate entity | warn | Those relationships are dropped: an edge cannot point at an object that is not in the file. |
| N imported entity/entities already confirmed without triage | warn | They arrived from a bundle carrying our layout extension, so they bypassed the tray. Check their provenance before pushing them to a platform: a third-party bundle can forge that extension. |
| likely duplicate: two X named "Y" | warn | Same type, same name (case and edge whitespace ignored). They will very likely collapse onto one identifier at build time. |
| X "Y": isolated, no relationship | info | The container will still reference it, but nothing in the graph says why it is there. |
| location: no country, region or coordinates | warn | The export will be blocked. |
| indicator: missing pattern | warn | The export will be blocked. |
| indicator: no based-on towards the source observable | info | The pattern exists but nothing links it to the observable it came from, so the provenance chain is broken on the consumer side. |
| file: no hash | warn | A file identified by name alone deduplicates badly everywhere downstream. |
| attack-pattern: no MITRE ID | info | Without x_mitre_id, the receiving platform can only deduplicate on the name. |
| observable: no indicator (based-on) covers it | info | A fact wired into the graph with no detection built on it. Fine, if that is what you meant. It only fires on an observable that already has a relationship, an isolated one gets the isolated finding instead, and a consists-of from an infrastructure counts as covering it. |
| relationship: activity end before its start | warn |
stop_time earlier than start_time. Nothing downstream will catch this, which is why it is caught here. |
| last_seen earlier than first_seen | warn | Same reasoning. |
| valid_until earlier than valid_from | warn | On an indicator this blocks the export. Note that an empty valid_from is not absent: the builder falls back on the object's creation date, and the check compares against that effective value. The message says so when it applies. |
These come back as Export impossible with a list, and no bundle is produced.
-
A
locationwith nocountry, noregionand not both coordinates. The spec requires one of the three. The tool used to fall back on copying the name intoregion, which is a normalised vocabulary: "Brive-la-Gaillarde" came out as a region of the world and polluted the consumer's filters. Refusing is the lesser harm. -
An
indicatorwith nopattern. There is nothing to detect with. -
An
indicatorwhosevalid_untilis not strictly aftervalid_from. The object would be invalid and the detection would never fire on the consumer side. -
An
autonomous-systemwhose name contains no digits, so no AS number can be read from it. - An empty investigation, meaning nothing confirmed to reference.
- A failure against the OASIS STIX 2.1 JSON schemas. Every generated bundle is validated against the official schemas before you are offered the download, including a check that a schema exists for each type present. This is the belt-and-braces pass: if it fires, the message is the schema's own and can be terse.
Distinct from the lint: these describe what the builder actually did, and they appear next to the fingerprint once the bundle exists. The download is still offered.
- An object collapses onto one already exported. Identifiers are computed from properties, so two canvas nodes can legitimately land on the same identifier (two techniques with the same MITRE ID, two relationships of the same type between the same ends and the same time window). Only the first is written, so only its description travels. Merge the two nodes on the canvas if you want both descriptions kept.
- A relationship skipped because one end is not exported.
- A note skipped because its entity is not exported.
-
Properties of an imported observable not re-exported. Only custom
x_…properties survive a round trip on an observable. A modelled-elsewhere STIX property is often a reference (resolves_to_refs,belongs_to_refs) pointing at identifiers that are not in your bundle, and re-emitting it would manufacture dangling references. The loss is announced rather than silent, because an observable enriched elsewhere used to grow quietly poorer at every round trip.
Under the generated bundle sits a line like sha256:…. It is the SHA-256 of the canonical (JCS) serialisation of the bundle's objects, sorted by identifier, with two things removed first: the canvas layout extension on each object, and the tooling objects (the extension definition and the tool identity).
What that buys you:
- Two analysts on the same state get the same fingerprint. It is a content identity, quotable in a ticket or a chat message to establish that you are both looking at the same thing.
- Moving nodes around does not change it. A visual rearrangement is not a new version of the intelligence.
- Two exports of the same state produce the same file, byte for byte. Even the bundle identifier is derived from the fingerprint rather than being a fresh random UUID. That is what makes the unexported changes indicator trustworthy rather than decorative.
The status bar compares the investigation's updated_at against the state the downloaded file actually contains, not against the moment you clicked download. A canvas changed between generating and clicking would otherwise have shown green over a stale file.
One deliberate looseness: dragging a node changes the bundle bytes (positions are embedded) but does not change the fingerprint and does not mark the investigation as modified. The indicator stays green while the file on disk no longer quite matches. Rearranging a layout has no business demanding a new export.
| Carried | Left behind |
|---|---|
| Confirmed entities and their properties | Candidates in the triage tray |
| Relationships between confirmed entities, with descriptions and time windows | Relationships touching a candidate |
| Notes and opinions, if the checkbox is on | The scratchpad (work notes), which is why editing it never moves the fingerprint |
The container (report or grouping) with its object_refs
|
Screenshots pasted on the canvas |
| Marking definitions actually used, and the author identity if you set one | Pinned-note positions on the canvas |
| Node positions, in a per-object extension | Undo history |
The layout extension is worth understanding, because it decides what happens on the way back in. It records position_x, position_y, the local id and the source, and it travels with its own extension-definition and a tool identity so a consumer is never left holding an unresolvable reference. Its schema is plain text rather than a URL, on purpose: a URL would bake a hosting address into every bundle an analyst exports, and that address may stop answering long after the bundle is filed.
Its practical consequence: a bundle exported from Draw Me A STIX and imported back comes in already confirmed, with its layout; a third-party bundle lands in the triage tray. The extension is what marks objects as having been curated here. That is also why the lint flags imported-and-confirmed entities, since the marker is only as trustworthy as the file it came in.
For the identifier scheme itself, see docs/identifiers.md.
Under Share in the toolbar, or Export image / PDF / Markdown… in the command palette. These are for humans, not for machines. They carry no identifiers, they cannot be imported anywhere, and they are not a backup. Downloading one does not touch the export status.
| Format | What you get |
|---|---|
| PNG / JPG | A capture of the whole graph, not only the visible part, at up to 4x resolution capped at 7000px on the longest side. Your layout is preserved exactly. |
| The graph on a page of its own, painted in the canvas's own background so the picture is not a dark stamp on a white sheet, and the narrative as selectable text on portrait pages after it, one section per page. The capture is embedded as JPEG to keep the file light. | |
| Markdown | The graph redrawn as a mermaid diagram, not captured. Renders as a real diagram on GitHub, GitLab, Obsidian and VS Code. |
Include the narrative appends the graph read back as prose: for PNG and JPG it composes a report image, for PDF it adds text pages, for Markdown it adds a Narrative section. A report carries one thing the panel does not, the same chronology again per subject, because a report is read in one go and a 300px panel is read while working.
Draw the chronology as a timeline is offered once something on the canvas carries a date, and only for the global chronology: a drawing per subject would be several pictures saying what the by-subject lists already say in less room. In Markdown it is a mermaid timeline, generated rather than drawn, since whoever opens the file renders it; in the image and in the PDF it is a rail with a dot per moment, drawn here because nothing downstream will do it for them. It shortens and says so: a URL loses its scheme and a label is cut past seventy characters, because a box of ninety characters is a paragraph in a frame. The list underneath is the same chronology in full.
Include my notes and opinions is a separate choice from the one the bundle offers, because a platform and a colleague do not want the same candour.
All of it is deterministic, built from the graph. No language model is involved anywhere in this tool.
Markdown is the odd one out and the most useful of the three for a ticket or a wiki page: it stays diffable and searchable, where an image does not.
- Triage-walkthrough - getting entities out of the tray, which is what makes them exportable in the first place.
- Importing-into-OpenCTI - what the receiving side does with the confidence, the TLP and the author you just chose.
- Canvas-reference - the inspector and bulk edit, where per-object TLP and confidence are set.
- Troubleshooting - when an export refuses and the message is not enough.
Using it
Running it
Changing it
When it breaks
In the application:
In the repository: