-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
What goes wrong, what the message actually says, and what to do about it.
Everything below is the behaviour of the code in this repository. If a message you see is not on this page, it is worth an issue: the project's rule is that an abnormal state gets named, not swallowed.
Three surfaces, and the difference matters when you are trying to work out whether something got through.
| Surface | Lifetime | Used for |
|---|---|---|
| Info toast | 8 seconds | "3 candidate(s) to the triage tray", operation succeeded |
| Error toast | 6 seconds | An operation failed and the failure is over |
| Persistent bar at the top | Until you dismiss it | A state that lasts: browser full, another tab wrote |
Do not confuse it with the status bar, which is the counts strip: objects, relationships, notes, entities awaiting triage, lint, export freshness, "local only" and the version.
The persistent bar exists because a toast was the wrong shape for "your browser
is full". That condition does not end when the message fades, and an analyst who
missed the toast kept working while every later write silently failed. Two tones
only, danger and warn, and a danger bar is never painted over by a warn
one.
Dialogs (export, import, restore, enrichment) report inside themselves and stay open, so you can read the list and act on it.
Message. In the persistent bar, in the danger tone:
This browser profile is full: nothing can be written any more. Export your investigations or back everything up before continuing, then free some space.
Cause. IndexedDB raised QuotaExceededError. The store singles that one
error out from every other transaction failure and maps it to status 507,
precisely so it can be shown this way rather than as a fleeting technical
string.
What it means for your data. Nothing was half-written. Every transaction in the store aborts on error before propagating, so a write either landed whole or did not land at all. What you already had is intact; what you do next will not be saved until there is room.
What to do.
- Stop editing. Further changes will be lost.
- Get the work out: Back up everything on the home screen writes a full
dump (
dmas-backup-<date>.json) including the things STIX cannot carry, the triage tray, canvas positions, pasted screenshots and the scratchpad. The STIX export of a single investigation is the other way out, and the portable one. - Free space in the browser profile. Pasted screenshots are usually the bulk of it: they are stored as WebP blobs in the same database. Deleting an investigation you have already exported removes its objects, relationships, notes and captures together.
- Reload and check a small edit saves before resuming.
The application calls navigator.storage.persist() the first time you create an
investigation, import a bundle, restore a backup or paste a capture. That asks
the browser not to evict the database under disk pressure. It is best effort and
it does not raise the quota.
Message. Thrown when the database cannot open:
Another tab is still using an older version of this application. Close the other tabs, then reload this page.
Cause. A schema upgrade (the store is at version 2, which added the
captures object store) is blocked by an older tab holding the database open.
Without this message the open would never settle, the home screen would stay
empty forever, and you would reasonably conclude your investigations were gone.
They are not.
What to do. Close the other Draw Me A STIX tabs, reload.
Message. In the persistent bar, warn tone, with a Reload button:
Another tab changed this data. What you see here may be out of date.
Cause. Two tabs on the same investigation. The tool does not merge concurrent edits, deliberately: it is single-user, and merging would be a subject of its own. It says so instead, which is enough that nobody works from a stale picture. Take the Reload button before doing any bulk edit in this tab, because a bulk edit here writes back properties read before the other tab's change.
What you see. Either a page explaining that the browser withholds the
cryptographic functions the tool needs, or, on a build older than this check,
crypto.randomUUID is not a function at the first thing you try to create.
Both have the same cause.
Cause. You are reaching the application over plain HTTP under a hostname
that is not localhost, typically an IP address on a LAN. Browsers restrict
crypto.randomUUID and crypto.subtle to secure contexts, and the
application uses the first for every identifier it mints and the second for
the fingerprint on every export. Neither is optional, so it refuses to start
rather than let you build an investigation that cannot be saved or exported.
Fix. Put TLS in front of it, or reach it through localhost. A tunnel does the job for a quick look:
ssh -L 8000:localhost:8000 you@the-host
# then open http://localhost:8000What will not work. Anything you try on the server side. Your reverse proxy forwards plain HTTP to the container even when the browser is on HTTPS, so the container cannot tell a working deployment from a broken one. An HSTS header is ignored when it arrives over plain HTTP, and a redirect to HTTPS without a certificate to land on only turns the error into a refused connection.
Self-hosting has the longer version.
What you see. A presentation card ("The CTI analyst's STIX scratchpad"), with See what it does and Open it anyway.
Cause. The screen matched one of two conditions:
| Condition | Threshold | Why |
|---|---|---|
| Coarse pointer and narrow | max-width: 820px |
That is a phone or a portrait tablet. Width alone would wrongly block a laptop with a shrunken window. |
| Very narrow, any pointer | max-width: 560px |
Below this the rail and the panels leave nothing for the canvas, mouse or no mouse. |
It is a card and not a wall because the common case is not an analyst trying to work from a phone, it is someone who followed a shared link and is deciding whether to come back to it from a desk.
How to get past it. Press Open it anyway. The choice is written to
localStorage under dmas.smallscreen and is durable, not session-scoped:
someone deliberately working on a tablet should not walk through that door every
day. Clearing site data resets it.
Two things worth knowing:
- The detection is live. Widening the window or rotating the device changes the screen without a reload.
- The STIX guide is exempt.
#/guiderenders on any screen, because prose reads fine on a phone.
The export dialog has two lists and they are not the same thing.
Lint, at the top, headed "Lint - what would make the bundle cleaner". It is a diagnostic, it never blocks, and it runs as soon as the dialog opens. Two of its warnings tell you in advance that generation will fail: "the export will be blocked".
Problems, after you press Generate the bundle, headed "Export impossible :". No file is produced. Every problem is a line you have to fix on the canvas.
The lines come from two passes.
Pass one, the bundle builder. It refuses to invent data. Each entity that cannot be built adds a line, and all of them are collected before the export is abandoned, so one generate shows you every offender rather than the first.
| Line | Fix |
|---|---|
empty investigation: nothing to export |
Everything is still a candidate in the triage tray, or the investigation is empty. Only confirmed entities are exported. |
indicator "X": ``pattern`` property required for export |
Give the indicator a STIX pattern in the inspector. |
indicator "X": valid_until (…) is not after valid_from (…) |
The spec requires a strictly later end. When you never set valid_from, it falls back to the entity's creation date, and the message says so. An inverted window makes an object the consumer will silently never fire on. |
location "X": give a country code, a region, or both coordinates - the spec requires one of the three |
city and administrative_area refine a place, they do not satisfy the requirement. The tool used to copy the name into region, a normalised vocabulary, so "Brive-la-Gaillarde" arrived at the consumer as a region of the world. It refuses now instead. |
autonomous-system "X": AS number not found |
Set number, or put the digits in the name (AS15169). |
Pass two, OASIS schema validation. The bundle is checked object by object against the official STIX 2.1 JSON schemas before it is handed to you. Lines look like:
indicator "Beacon C2" /valid_from: must match format "date-time"
that is: type, the object's name (or its id when it has no name), the JSON path inside the object when there is one, then the schema's own message. Two lines that come from this pass and read oddly:
-
<type> <id>: no vendored OASIS schema for this type. The bundle contains a type with no schema shipped in the app. Reported as a problem like any other rather than passed through unchecked. - A field you never touched. Properties imported from a third-party bundle are kept as they came; a malformed one surfaces here. Open the entity's inspector and remove it.
Validators are precompiled at build time, because the production CSP forbids
unsafe-eval and ajv cannot compile at runtime.
These appear beside the fingerprint, with the bundle already built. They are not failures, they are things the file does not say.
-
relationship <type> skipped: one end is not exported (candidate entity?). Confirm the entity out of the triage tray and generate again, or accept the loss. -
<type> "X" collapses onto an object already exported (<id>): identical STIX identity, so only the first one is written. Merge the two on the canvas to keep both descriptions.Two canvas nodes computed to the same deterministic identifier: two techniques carrying the samex_mitre_id, or the same observable entered twice. Neither the lint nor schema validation can see this, and on the consumer's side the second assertion would simply vanish on ingestion, hence the warning. See docs/identifiers.md for what feeds an identifier. Take the last sentence of the message with a pinch of salt: in the current build there is no manual merge action. Accepting a triage candidate that matches an existing node merges them automatically, but two nodes you created by hand have to be reconciled by deleting one, which also deletes its relationships, and drawing them again. -
<type> "X": <props> not re-exported (property not modelled here; a reference would point outside this bundle). An observable enriched elsewhere carries properties the builder does not emit. Said out loud because, unmentioned, such an observable grew poorer at every round trip with nobody noticing.
The fingerprint above the download button is the version fingerprint: identical for two analysts looking at the same state, and unaffected by canvas positions. The download, not opening the dialog, is what counts as an export.
The importer is tolerant on purpose: unknown types, relationships outside the matrix, exotic extensions. It imports everything it can represent, counts the rest, and does not strand you on an error. So the report is where the truth is, not the absence of a red banner.
Two failures happen before any of that and abandon the import outright:
This file is not valid JSONthis file is not a STIX bundle (type=bundle, objects=[...])
Otherwise you get the Bundle imported dialog: counts, then a "Skipped:" line
grouping what was dropped by type (<type> ×<n>), then the warnings.
| Entry | Meaning |
|---|---|
| A STIX type name | Not in the tool's SDO or SCO lists, so it has no canvas representation. |
identity (auteur) |
An identity referenced by some object's created_by_ref. It is the bundle's author, metadata, not something an analyst placed on a canvas. |
Containers (report, grouping), marking-definition, extension-definition,
language-content, and the tool's own identity are not counted at all: they are
plumbing or handled elsewhere. TLP markings are not lost, they are mapped back
onto each object as a tlp property, so a round trip preserves the marking
object by object.
| Warning | What happened |
|---|---|
object without a usable type or id skipped |
type or id was missing, or was not a string. A bundle is third-party JSON and these fields get rendered, so they are filtered on the way in rather than at display time. |
<type> <id> without a usable value, skipped |
An observable with nothing to name it: no value, no file name or hash, no AS number. |
relationship <type> skipped: one end was not imported |
One end was among the objects above. The relationship has nowhere to attach. |
<note|opinion> <id> without content, skipped |
Empty note or opinion. |
no importable entity in this bundle |
The file parsed but held nothing the canvas can show. |
<n> entity/entities placed in the triage tray (third-party bundle, validate before exporting) |
Not an error, see below. |
An object arrives confirmed only if it carries this tool's layout extension, which means it was curated here. Everything else arrives as a candidate and waits in the tray: nothing reaches the canvas from a third-party bundle without you validating it. Relationship types are not checked against the matrix on import, deliberately, since dropping a third-party assertion would be worse than keeping an unusual one.
Entities that were confirmed on arrival are flagged for the lint, which will say "imported entity/entities already confirmed without triage - check their provenance before exporting".
For the ingestion side of the round trip, see Importing into OpenCTI.
All of these appear inside the enrichment dialog as Failed: <message>, or
beside the endpoint in Settings as ✕ <message> when you press Test. The
Test button is the faster way to tell an endpoint problem from an enricher
problem.
| Message | Cause | What to do |
|---|---|---|
token rejected (401) |
The sidecar refused the bearer token. | Retype it in Settings. If your token was not remembered it is simply gone, see below. On a self-hosted sidecar, check STIXIT_ENRICHER_TOKEN really is set: when it is absent or empty the service generates a random one at startup and logs it as a warning, so the token you need is in the sidecar's own startup log. The shipped docker-compose produces exactly that when you never set ENRICHER_TOKEN. |
endpoint unreachable - check the URL, HTTPS (an HTTPS app cannot call an HTTP endpoint) and the sidecar CORS list |
The fetch itself failed. The browser deliberately gives no detail, so the message names the three real causes. |
See the three cases below. |
timed out - the queried source is not answering, try again in a few minutes |
150 seconds passed with no response at all. That ceiling is browser-side and deliberately wider than the sidecar's own budget, so it only fires when the connection is hung rather than when a source is merely slow. | Retry later. |
<source> did not answer after <n> attempt(s) in 90s (<reason>). This source is regularly saturated: try again in a few minutes. |
The sidecar exhausted its retries. | Wait. Nothing on your side is broken. |
<source> refused with a rate limit, and caches that refusal for about 30 minutes on this exact request. Retrying the same item now will not help; another one will most likely work. |
HTTP 429 from the remote source. | Enrich a different item. Measured on CIRCL: the refusal is cached by their front end on that exact URL, so hammering the same CVE achieves nothing even once the quota is free. |
<source> refused the request (HTTP <code>). |
A non-retryable status from the remote source. | Check the selector. |
enrichment unavailable |
A local binary failed on the sidecar (not found, timed out, non-zero exit). | The detail is in the sidecar's logs on purpose: its stderr would leak host paths, versions, and the selector itself. |
"<enricher>" does not accept type <type> (expected: …) |
Wrong observable type for that enricher. | Should not normally be reachable: the dialog only lists enrichers whose accepts matches the node's type. |
Mixed content. The app is served over HTTPS and your endpoint URL is
http://. The browser blocks the request before it leaves, and reports nothing
useful. This is the most common cause on the hosted app. A sidecar reachable
from an HTTPS page has to be on HTTPS behind a real hostname; there is no
setting that gets around it.
CORS. The sidecar answers only origins in its allow list. Default:
http://localhost:5173,http://localhost:4173,http://localhost:8000, overridden
by STIXIT_ENRICHER_ORIGINS. Serving the app from anywhere else, including a
different port, means the browser discards a perfectly good response. See
Self-hosting and Enrichment sidecar.
The URL. Trailing slashes are trimmed, but the path is not: the app appends
/enrichers and /enrich, so the configured URL must be the sidecar's root.
No enricher applies to "", or the endpoint is unreachable (check the URL, the token, HTTPS).
The dialog fetches every configured endpoint's catalogue when it opens and treats a failure as an empty catalogue, so an unreachable endpoint and a type no enricher handles look the same here. Use Test in Settings to tell them apart: there the error is shown in full.
Not a bug. The token is treated as a credential, not a preference. By default it
goes to sessionStorage and dies with the tab, while the endpoint URL and label
stay in localStorage. This is not encryption, both are read just as easily; it
bounds the token's life to a working session instead of leaving it on disk
indefinitely.
You will see the password field reappear on that endpoint's row, with the placeholder "Token for this session". That is on purpose: a field asking to be filled explains itself, where an unexplained 401 on the first enrichment does not.
To change it, tick Keep the token on this machine on the endpoint's row. On the Add form the same checkbox is spelled out in full, "Keep the token on this machine, so it survives closing the tab". Endpoint by endpoint, never globally. When the token is held for the session only, the row says so.
Two consequences worth knowing:
- A session token is never picked up by a backup file. Nor is a remembered one: the app writes no settings into a backup at all.
- Restoring a backup never applies enrichment endpoint settings, even if the file contains them. It says so afterwards: "this file also carried enrichment endpoint settings (a URL and its token), which were NOT applied". A backup is a file that can come from someone else, and silently repointing enrichment at their server would ship the IOC you are investigating over there on the next click.
- A locked-down private browsing mode can make
sessionStorageunavailable. The endpoint still saves, and the token holds for the tab but has to be retyped after a reload. Losing a token is repairable; failing to save the endpoint would not be.
The work is gone. There is no server-side copy to recover from and no support channel that can get it back: that is the direct cost of the design, and it is the honest answer. /about sets out where the data lives.
What survives a cleared profile is only what was written to a file:
| File | Written by | Carries |
|---|---|---|
<name>.stix.json |
Export dialog, Download the .json | The intel: confirmed objects, relationships, notes and opinions if included, the container, markings. Not the triage tray, not canvas positions of anything but the objects, not screenshots, not the scratchpad. |
dmas-backup-<date>.json |
Home screen, Back up everything | Everything in IndexedDB: all investigations, the triage tray, positions, pasted screenshots, scratchpad notes. No settings at all: not your enrichment token, and not your export preferences either. |
Both restore. A bundle re-imported gives back the same version fingerprint, so a round trip is lossless for what STIX can carry. A backup restores the local state as well. Your export preferences, the default TLP, author and confidence, are not in either file: expect to set them again on a fresh profile.
So that this does not happen twice. The two files answer different needs and you want both habits:
- Export the bundle when an investigation reaches a state worth keeping. It is the portable artefact, the one another tool can read.
- Back everything up on a rhythm that matches how much you would mind losing a day. A backup is one JSON file; put it wherever your other work files go.
Restoring a backup replaces, by identifier, any investigation the file carries and leaves every other one untouched. The confirmation dialog lists what would be overwritten with its object, relationship and note counts and its last modification date, precisely so that restoring yesterday's backup over today's work is a decision rather than an accident. Rows in the file that belong to no restored investigation are dropped and counted in the report rather than written into an investigation the dialog never named.
A handful of user-visible strings are still in French, left over from before the repository moved to English. They are cosmetic and not a sign that something is broken:
- The export problem list is headed "Export impossible :".
- A few store errors read
investigation inconnue,relation inconnue,note inconnue,capture inconnue,une opinion doit avoir une opinion_value, andsauvegarde illisible : liste d'investigations absente. A store error with no message of its own falls back toErreur (<status>). - The import report can show
identity (auteur)in its skipped line. - The author field in the export dialog has
Mon CERTfor a placeholder.
- Self-hosting, for CSP, origins and serving the app yourself
- Enrichment sidecar, for running and configuring it
- Exporting, for what each option puts in the bundle
- Triage walkthrough, for the candidate workflow
- Canvas reference, for shortcuts and canvas behaviour
Using it
Running it
Changing it
When it breaks
In the application:
In the repository: