Skip to content

Contributing

pak edited this page Aug 13, 2026 · 5 revisions

Contributing

CONTRIBUTING.md is the short form, and it is enough to get a change written and checked: what you need, the commands, the conventions.

This page is the longer version, meant for the moment you actually have the tree open and a check has just gone red. It says what each suite proves rather than how to run it, and it carries the traps that have already cost somebody an afternoon.

What you need, and only when you need it

Most changes touch frontend/ alone. That is the application: the STIX logic, the canvas, the storage, all of it runs in the browser. If your change stays there, Node is the only tool you need.

You are touching You need Why exactly
frontend/ Node 22 CI pins node-version: "22" and nothing else is tested
backend/ uv, Python 3.12 or later, plus libmagic1 on the machine pyproject.toml sets requires-python = ">=3.12"; CI installs libmagic1 before syncing, with the comment "required by pycti"
enricher/ uv, Python 3.13 or later requires-python = ">=3.13"

backend/ is not a service and never runs in production. It is the reference STIX implementation that generates the golden vectors, and the README explains why that trade was made.

On Debian or Ubuntu the pycti prerequisite is sudo apt-get install libmagic1. Without it, uv run pytest in backend/ fails at import time, before a single test runs, which reads as a broken checkout rather than a missing system library.

Both lockfiles are committed, so uv sync gives you the exact ruff, pycti and stix2 that CI uses (ruff 0.16.0, pycti 7.260722.0, stix2 3.0.2 at the time of writing).

Two generated files you will trip over

frontend/src/stix/generated/validators.mjs is not committed. It is compiled from the vendored OASIS schemas by scripts/build-validators.mjs, which runs from the predev, prebuild and pretest hooks in package.json. The reason it is precompiled rather than built at runtime: the production CSP forbids unsafe-eval, and ajv compiles schemas through new Function.

The practical consequence is that npx vitest does not work on a fresh clone. It skips the pretest hook, and the validation tests fail on a module that was never written. Run npm test, which is the same suite with the hook.

golden-vectors.json and golden-bundle.json are committed but generated. Never edit them by hand. See the golden vectors below.

Running the checks

Three test suites and two lint passes. There is no linter for the frontend, no ESLint anywhere in the tree: the type-check is what stands in its place, with strict, noUnusedLocals, noUnusedParameters and noFallthroughCasesInSwitch on in tsconfig.json.

Directory Command What it actually proves
frontend npm test 29 test files under vitest: the golden identifiers replayed case by case, the golden bundles rebuilt object for object, export/import roundtrips, the store against fake-indexeddb, OASIS validation, the investigation lint, and the English-interface guard
frontend npx tsc -b the type-check, exactly as CI runs it
frontend npm run build tsc -b, then vite over three entry points (index, guide, about), then prerender.mjs, which turns dist/guide.html and dist/about.html into routed pages with their content already in the HTML
backend uv run ruff check . rules E, F, I, UP, B, SIM at 100 columns
backend uv run pytest -q every identifier recipe against pycti, roundtrip on the golden fixture, the relationship matrix, export and import
enricher uv run ruff check . ruff's default rule set at 100 columns, since the config sets nothing but the line length
enricher uv run pytest -q the API surface, the hardening, and one file per enricher

npm run build type-checks again, so the separate tsc -b step covers no extra ground. It buys an unambiguous failure early instead of a build that dies at the end for a reason you have to read backwards.

The enricher suite patches the tool runner and the HTTP client everywhere, so it needs neither the network nor dig, whois or subfinder on your machine. If a test of yours reaches out for real, it will pass on your laptop and hang in CI.

The enricher pins only line-length. That is worth knowing before you refresh uv.lock: ruff's defaults are not frozen, and a version bump has already turned the job red on import ordering without a line of code changing.

What CI checks, job by job

.github/workflows/ci.yml runs on every pull request and on pushes to main. Three independent jobs, so a red one tells you where the damage is.

Job Steps A failure means
frontend npm ci, npx tsc -b, npm test, npm run build the application itself: types, behaviour, or the build
stix-core ruff, pytest, then regenerate both golden files and demand an empty git diff either the Python oracle broke, or the committed vectors no longer match what the oracle produces
enricher ruff, pytest the sidecar, which nothing else depends on

Read the annotations, not the logs. GitHub exposes annotations publicly but gates the logs behind repository rights, so a contributor on a fork sees a red cross and nothing else. The workflow works around this deliberately: both lint steps echo the ruff version and its rule statistics as a ::notice::, and the golden-vector check carries the first lines of the real diff into its ::error::. A check that says "out of sync" without saying how forces you to re-run CI blind, and that is the failure mode being prevented.

The golden vectors

Two implementations of the identifiers exist and have to agree: the Python one in backend/app/stix_core/ids.py, which is the oracle and is itself pinned to pycti by its own tests, and the TypeScript one in frontend/src/stix/ids.ts, which is the one that runs in your browser. The recipes themselves, property by property, are specified in docs/identifiers.md; this section is only about the workflow.

Two scripts, and you rarely need both:

You changed Run, from backend/ It rewrites
an identifier recipe (app/stix_core/ids.py) uv run python scripts/generate_golden_vectors.py frontend/src/stix/golden-vectors.json (41 object cases, 12 observable cases)
the bundle builder or the importer uv run python scripts/generate_golden_bundle.py frontend/src/stix/golden-bundle.json (one fixture investigation, exported three ways, with each fingerprint and each warning)

Then run npm test in frontend/, because those two files are what stix/ids.test.ts and stix/bundle.test.ts replay. Commit the regenerated JSON in the same pull request as the change that moved it, or the stix-core job fails on the diff.

Yes, it is awkward: a Python script in backend/ owns two source files under frontend/src/. It is that way round because the Python side is the oracle and the TypeScript side is what has to catch up.

Two things worth being careful about.

A failing pycti comparison is not a vector to regenerate. test_stix_ids.py says it in its own header: if one of those assertions breaks, OpenCTI changed its algorithm, and adjusting the expected value without realigning ids.py on the new pycti is how bundles start creating duplicates on import.

Regenerating makes the two sides agree, not correct. If a vector moves, an identifier moved with it, and an identifier that moves turns somebody's next re-import into a duplicate rather than an update. Say in the pull request which recipe changed and why. golden-bundle.json moving on its own is milder: it usually means the builder now emits a different field, and the fingerprint followed.

Conventions

Branch names, Conventional Commits and one issue per pull request are in the README's Conventions section, and are not restated here.

Nothing enforces any of it, no hook and no CI job. git log is the reference if you are unsure what a message is supposed to look like.

One issue per pull request is worth more than tidiness: commit bodies here say what was wrong and what was rejected as a fix, and a pull request carrying two unrelated changes cannot do that for either of them.

The language rule

The rule is the README's Language bullet: English everywhere, no i18n layer. What that bullet does not say is where the tree still differs from it, and what enforces the rest.

The rule widened in steps rather than at once. The interface went first, then the code comments with 1.1.0, then the commit messages and test descriptions. Nothing is being rewritten backwards, so the older history and some describe labels are still French. Read git log for what a message should look like and write yours in English.

Golden-vector fixtures are the one standing exception and keep their non-ASCII names (Opération Héron, Δοκιμή Unicode, ПлохойСофт). They exist to exercise JCS canonicalisation on non-ASCII, and translating them would delete the thing under test.

The rule is executable, up to a point. frontend/src/i18n.test.ts strips comments from every source that is not a test, then looks for accented Latin strings in JSX text and string literals. A French comment has nothing to fear, and that is the intent.

It keys on accents, and that is the blind spot that matters. French written without them goes straight through. Crossing /usr/share/dict/french with american-english over 3000 strings turned up 25 at once: error messages mostly, but also one sitting in the content of an enrichment note, which meant it travelled into exported STIX bundles. That audit cannot become a test, it flags a British spelling or a CSS class name two times out of three. Read the interface now and then.

The corpus used to be .tsx only, on the assumption that a string reaching the screen comes from a component. It was false, and the lint messages, the narrative and the markdown export went unwatched for months. It now covers the .ts modules and the build scripts too.

Reporting a vulnerability

Privately, and not as a public issue: the process is in SECURITY.md, which also draws the line between a security report and a normal bug.

Where to look next

Clone this wiki locally