Introduction
stix-rust is a Rust toolkit — with Python, Java, and TypeScript bindings — for working with STIX 2.1, the OASIS standard for representing cyber threat intelligence. It does three things:
- Parse STIX patterning-language patterns into a typed, serializable AST.
- Import STIX objects (SDOs, SCOs) and bundles into a flexible object model.
- Match patterns against sets of observed objects and tell you what bound.
What is STIX?
STIX (Structured Threat Information eXpression) is a standardized JSON language for describing threat intelligence — indicators, malware, observed data, and the relationships between them. Two pieces matter most here:
-
STIX objects — JSON documents like an
ipv4-addr, afile, or anobserved-dataSDO, usually delivered together in a bundle. -
The patterning language — a query-like syntax used inside
indicatorobjects to describe what to look for:[ipv4-addr:value = '198.51.100.1' OR domain-name:value = 'evil.example']
stix-rust parses those patterns and evaluates them against observed objects to answer: “does this threat intelligence match what we saw?”
Shape of the toolkit
The core is five Rust crates with clean dependency edges; four language bindings wrap a shared FFI facade:
| Layer | Pieces |
|---|---|
| Core crates | stix-pattern (parser), stix-model (objects), stix-matcher (engine), stix (umbrella) |
| FFI facade | stix-ffi — opaque handles + JSON deep structure, wrapped by every binding |
| Bindings | Python · Java · TypeScript Node · TypeScript wasm |
Every language gets the same conceptual API: an Engine that parses patterns and bundles into handles, matches them, and accepts custom object type registrations with validation/computed-property hooks.
Status, honestly
The parser handles the complete STIX 2.1 patterning grammar. The matcher
implements all comparison operators, boolean logic, object-path resolution
(including reference traversal), and observation-level AND/OR.
Two grammar features parse but do not yet match: FOLLOWEDBY sequencing and
the temporal qualifiers (WITHIN, REPEATS, START..STOP). Reaching them at match
time returns an explicit unsupported error rather than silently passing. See
Limitations & Caveats for the full list of sharp edges — reading
that page before production use is strongly recommended.
Getting Started
Install
Rust
cargo add stix-rust
Or from source:
[dependencies]
stix = { git = "https://github.com/benjamin-small/stix-rust", package = "stix-rust" }
The package is stix-rust; the library name is stix, so code reads use stix::…
either way.
Python
pip install stix-rust
Or from source:
pip install "maturin>=1.5,<2.0"
cd bindings/python && maturin develop
TypeScript
npm install @stix-rust/node # native Node addon
npm install @stix-rust/wasm # portable WebAssembly (Node + browser)
Or from source: cd bindings/typescript-node && npm install && npm run build
(same for typescript-wasm).
Java
Maven Central: not yet published — source-only for now; the coordinate below is the planned artifact.
// Gradle (Kotlin DSL)
implementation("io.github.benjaminsmall:stix:0.1.0")
Or from source: cd bindings/java && gradle test builds the native library and
runs the suite; see the Java page for library-loading details.
Quick start (Rust)
#![allow(unused)]
fn main() {
use stix::{parse, matcher::match_bundle, model::Bundle};
let pattern = parse("[ipv4-addr:value = '198.51.100.5']")?;
let bundle = Bundle::from_json_str(r#"{
"type": "bundle",
"objects": [
{ "type": "ipv4-addr", "id": "ipv4-addr--a1", "value": "198.51.100.5" },
{ "type": "observed-data", "id": "observed-data--a1",
"first_observed": "2020-03-01T12:00:00Z",
"last_observed": "2020-03-01T12:10:00Z",
"number_observed": 1,
"object_refs": ["ipv4-addr--a1"] }
]
}"#)?;
let result = match_bundle(&pattern, &bundle)?;
assert!(result.is_match());
}
That’s the whole loop: parse a pattern, import a bundle, match. The same three steps exist in every binding — see the language pages for the identical example in Python, Java, and TypeScript.
Next stops:
- Patterns — what the pattern language can express.
- Matching — how evaluation actually works.
- Custom Object Types — extending the model without forking.
Patterns
stix-pattern parses the complete STIX 2.1 patterning grammar into a typed AST.
This page covers what the language expresses and how the parser structures it.
Anatomy of a pattern
[file:name LIKE '%.exe' AND file:size > 1024] OR [ipv4-addr:value ISSUBSET '10.0.0.0/8']
A pattern is a tree of observation expressions — each [ … ] block is one
observation test — combined with AND, OR, and FOLLOWEDBY, optionally wrapped
in qualifiers. Inside the brackets live comparison expressions: property tests
combined with their own AND/OR.
Supported constructs
| Construct | Examples |
|---|---|
| Comparison operators | = != < <= > >= IN LIKE MATCHES ISSUBSET ISSUPERSET EXISTS |
| Negation | file:name NOT = 'x' |
Boolean (inside [ ]) | [a = 1 AND b = 2], grouping with ( ) |
| Object paths | file:hashes.'SHA-256', network-traffic:protocols[0], x:list[*] |
| Reference traversal | network-traffic:src_ref.value |
| Typed literals | t'2020-01-01T00:00:00Z' (timestamp), b'aGk=' (base64), h'cafe' (hex) |
| Observation operators | [a] AND [b], [a] OR [b], [a] FOLLOWEDBY [b] |
| Qualifiers | WITHIN 60 SECONDS, REPEATS 5 TIMES, START t'…' STOP t'…' |
FOLLOWEDBYand the qualifiers parse but are not yet matched — see Limitations.
Precedence
Observation level, loosest to tightest:
FOLLOWEDBY < OR < AND < qualifiers (postfix) < [ … ] / ( … )
Comparison level (inside [ ]): OR < AND < individual test. So
[a = 1 OR b = 2 AND c = 3] parses as a = 1 OR (b = 2 AND c = 3) — use
parentheses when in doubt.
Object paths
A path starts with the object type, then walks properties:
.key— property access (quote keys with special characters:hashes.'SHA-256')[0]— list index[*]— any list element (the test passes if any element satisfies it)- A step through a
_refproperty dereferences to the referenced object (requires an object store at match time; see Matching).
The AST is data
parse() returns a Pattern that is fully serde-serializable — useful for
tooling, caching, and the language bindings (every binding exposes the AST as a
native object/dict/Map). A small example:
#![allow(unused)]
fn main() {
let pattern = stix::parse("[file:size > 1024]").unwrap();
println!("{}", serde_json::to_string_pretty(&pattern).unwrap());
}
{
"expression": {
"Observation": {
"Test": {
"path": { "object_type": "file", "steps": [ { "Key": "size" } ] },
"operator": "GreaterThan",
"negated": false,
"value": { "Literal": { "Integer": 1024 } }
}
}
}
}
Parse errors carry a byte-offset span into the source string:
parse error at bytes 19..20: expected a literal value
Objects & Bundles
stix-model imports STIX objects and bundles into a model that is typed where it
helps and flexible everywhere else.
The trichotomy: typed, generic, custom
Every imported object is one of three shapes:
| Shape | When | What you get |
|---|---|---|
| Typed | Recognized types (currently observed-data) | A real Rust struct (ObservedData) with typed fields |
| Generic | Every other type — including custom x-* types | A value-backed property map preserving all properties |
| Custom | Types you register yourself | Your struct (see Custom Object Types) |
The crucial design point: all three implement the ObjectView trait —
#![allow(unused)]
fn main() {
pub trait ObjectView {
fn id(&self) -> Option<&str>;
fn type_(&self) -> Option<&str>;
fn property(&self, name: &str) -> Option<StixValue>;
}
}
— and the matcher consumes only ObjectView. That’s why unknown and custom types
match out of the box, and why typed objects can synthesize properties on demand
(a property need not literally exist in the JSON to be matchable).
StixValue
property() returns a StixValue — a JSON-shaped dynamic value: null, bool,
integer, float, string, list, or object. Two things to know:
- Integers and floats are distinct (numeric comparisons promote, but the values are stored as parsed).
- Timestamps, hex, and binary are carried as strings at this layer; the matcher compares them against pattern literals as strings.
Bundles and the object store
#![allow(unused)]
fn main() {
use stix::model::{Bundle, ObjectStore, ObjectView};
let bundle = Bundle::from_json_str(json)?; // validates type == "bundle"
let store = ObjectStore::from_bundle(&bundle); // id → object index
let obj = store.get("ipv4-addr--a1").unwrap();
assert_eq!(obj.property("value").unwrap().as_str(), Some("198.51.100.5"));
}
The ObjectStore is how reference properties resolve: when a pattern path walks
through src_ref, the matcher looks the id up in the store. No store → reference
paths resolve to nothing (not an error — an empty result).
ObservedData, the typed SDO
observed-data is the one built-in typed struct because the matcher needs its
fields: first_observed / last_observed / number_observed (temporal metadata)
and object_refs (which SCOs were seen together). It tolerates STIX 2.0’s inline
objects map as well, and retains unknown properties via a flattened map so nothing
is lost on round-trip.
Matching
stix-matcher evaluates a parsed pattern against observations and reports
whether — and where — it matched.
Observations
An observation is a set of objects seen together, plus temporal metadata
(first_observed, last_observed, number_observed). This mirrors the STIX
semantics and the MITRE reference implementation: each observed-data SDO is one
observation, its object_refs naming the objects in it.
Four entry points
All normalize to observations internally; pick by what you have:
| Entry point | Input | Use when |
|---|---|---|
match_bundle(&pattern, &bundle) | a whole Bundle | you have bundle JSON — the common case; derives observations from its observed-data SDOs |
match_observed_data(&pattern, &sdos, &store) | observed-data SDOs + an ObjectStore | MITRE-compatible; you manage the store |
match_observations(&pattern, &observations) | pre-built Observations | you construct observations yourself |
match_scos(&pattern, &scos) | a flat object list | quick tests; the list is treated as one observation |
The result is a MatchResult: is_match() plus observations() — the indices of
the observations that participated in the match.
How a [ … ] block evaluates: binding enumeration
Within one observation, if a comparison expression references multiple constraints on the same object type, they must be satisfied by one object, not spread across several. The matcher implements this by binding enumeration: for each distinct object type in the expression, it tries each candidate object of that type, and the expression matches if some assignment makes the boolean tree true.
Concretely, the pattern [file:name = 'evil.exe' AND file:size = 10]:
// MATCHES — one file satisfies both constraints
{ "objects": [ { "type": "file", "name": "evil.exe", "size": 10 } ] }
// DOES NOT MATCH — constraints hold only across two different files
{ "objects": [
{ "type": "file", "name": "evil.exe", "size": 99 },
{ "type": "file", "name": "ok.txt", "size": 10 } ] }
Constraints on different types ([ipv4-addr:value = … AND domain-name:value = …])
bind independently — one object per type, all within the same observation.
Observation-level logic
- A
[ … ]block matches if any observation in the input satisfies it. [a] AND [b]requires both blocks to be satisfied — possibly by different observations.[a] OR [b]requires either.[a] FOLLOWEDBY [b]and theWITHIN/REPEATS/START..STOPqualifiers parse but return an explicitUnsupportederror at match time — they never silently pass. See Limitations.
Operator semantics worth knowing
LIKEuses SQL wildcards (%,_) and is anchored (whole-value).MATCHESis a regular expression, unanchored. An invalid regex never matches.ISSUBSET/ISSUPERSEToperate on IP addresses and CIDR ranges (IPv4/IPv6, never mixed families).EXISTStests that a path resolves to any value at all.NOTbefore an operator negates that single test.- Numeric comparisons promote integers to floats; strings (including timestamps) compare lexicographically.
Custom Object Types
Custom and unknown STIX types (x-* or anything else) already parse and match
with zero registration — they become generic value-backed objects that preserve
every property. Registration is for the three things the generic path can’t give
you: typed access, validation, and computed properties.
Rust: typed structs
Register your own struct for a type; parsed bundles then carry your type, and you can downcast back to it:
#![allow(unused)]
fn main() {
use serde::{Deserialize, Serialize};
use stix::model::{ModelRegistry, ObjectView, StixValue};
#[derive(Debug, Serialize, Deserialize)]
struct AcmeWidget { #[serde(rename = "type")] type_: String, id: String, risk_score: i64 }
impl ObjectView for AcmeWidget {
fn id(&self) -> Option<&str> { Some(&self.id) }
fn type_(&self) -> Option<&str> { Some(&self.type_) }
fn property(&self, name: &str) -> Option<StixValue> {
match name {
"risk_score" => Some(StixValue::Integer(self.risk_score)),
// a computed property — synthesized, not stored in the JSON:
"risk_band" => Some(StixValue::String(
if self.risk_score > 80 { "high" } else { "low" }.into())),
_ => None,
}
}
}
let mut registry = ModelRegistry::new();
registry.register::<AcmeWidget>("x-acme-widget");
let bundle = registry.parse_bundle(json)?;
// typed access after parsing:
if let Some(w) = bundle.objects[0].downcast_ref::<AcmeWidget>() { /* w.risk_score */ }
}
Because risk_band is exposed through ObjectView, the pattern
[x-acme-widget:risk_band = 'high'] matches a property that never existed in the
JSON. A runnable version lives in the repo:
cargo run -p stix-rust --example custom_model.
Rust: data-level hooks
No struct needed — register a Value → Result<Value> hook for validation and
enrichment. It runs once per object at parse time; the result is stored as
data, so matching stays callback-free:
#![allow(unused)]
fn main() {
registry.register_handler("x-acme-widget", |mut obj| {
if obj.get("risk_score").is_none() {
return Err(stix::model::ModelError::InvalidObject("missing risk_score".into()));
}
let score = obj["risk_score"].as_i64().unwrap_or(0);
obj["risk_band"] = serde_json::json!(if score > 80 { "high" } else { "low" });
Ok(obj)
});
}
A hook rejection surfaces from parse_bundle as a validation error.
The same idea in every binding
Each binding exposes the identical import-time hook — a host function taking and
returning the object; throwing rejects it (a ValidationError /
ValidationException):
# Python
engine.register_type("x-acme-widget", lambda obj: {**obj,
"risk_band": "high" if obj.get("risk_score", 0) > 80 else "low"})
// TypeScript (Node and wasm)
engine.registerType("x-acme-widget", (obj) => ({
...obj, risk_band: obj.risk_score > 80 ? "high" : "low" }));
// Java
engine.registerType("x-acme-widget", obj -> {
long score = ((Number) obj.getOrDefault("risk_score", 0)).longValue();
obj.put("risk_band", score > 80 ? "high" : "low");
return obj;
});
Hooks always run at parse/import time, synchronously — never during matching — so there is no cross-language callback overhead on the hot path.
Python
The stix module (PyO3 + maturin). Deep structure — the pattern AST and bundle
objects — arrives as native dict/list. Ships type stubs (py.typed).
Install
pip install stix-rust
From source instead:
pip install "maturin>=1.5,<2.0" && cd bindings/python && maturin develop
Worked example
import stix
engine = stix.Engine()
# 1. parse a pattern; the AST is a dict
pattern = engine.parse_pattern("[ipv4-addr:value = '198.51.100.5']")
print(pattern.ast["expression"])
# 2. import a bundle; iterate its objects
bundle = engine.parse_bundle(open("bundle.json").read())
print(len(bundle), [o["type"] for o in bundle])
print(bundle.object(0)) # dict, or None if out of range
# 3. match — hit and miss
result = engine.match_bundle(pattern, bundle)
print(result.matched, result.observations)
miss = engine.parse_pattern("[ipv4-addr:value = '203.0.113.9']")
assert not engine.match_bundle(miss, bundle).matched
# 4. custom type with a computed property
def normalize(obj):
obj["risk_band"] = "high" if obj.get("risk_score", 0) > 80 else "low"
return obj
engine.register_type("x-acme-widget", normalize)
banded = engine.parse_bundle(widget_bundle_json)
hit = engine.parse_pattern("[x-acme-widget:risk_band = 'high']")
assert engine.match_bundle(hit, banded).matched
Errors
| Exception | Raised by |
|---|---|
stix.ParseError | invalid pattern syntax |
stix.ModelError | invalid JSON / not a bundle |
stix.MatchError | matching failure (e.g. unsupported feature reached) |
stix.ValidationError | a register_type hook raised |
All subclass stix.StixError. Hooks run at parse_bundle time; raising any
exception inside one rejects the object.
Notes
- Type stubs +
py.typedare included — IDEs and mypy see precise signatures. - Requires Python ≥ 3.8 (abi3 wheel).
Java
Package io.github.benjaminsmall.stix (JNI via jni-rs). Deep structure arrives as
Jackson Map<String, Object>.
Install
Maven Central: not yet published — the Java binding is currently source-only. The coordinate below is the planned artifact.
// Gradle (Kotlin DSL)
implementation("io.github.benjaminsmall:stix:0.1.0")
From source instead:
cd bindings/java && gradle test builds the
native library (cargo) and runs JUnit. Tests load it from rust/target/release
via java.library.path.
Worked example
import io.github.benjaminsmall.stix.*;
import java.util.Map;
try (Engine engine = new Engine()) {
// 1. parse a pattern; the AST is a Map
try (Pattern pattern = engine.parsePattern("[ipv4-addr:value = '198.51.100.5']")) {
Map<String, Object> ast = pattern.ast();
// 2. import a bundle; iterate its objects
try (Bundle bundle = engine.parseBundle(json)) {
System.out.println(bundle.objectCount());
for (Map<String, Object> obj : bundle) System.out.println(obj.get("type"));
bundle.object(99); // Optional.empty() when out of range
// 3. match — hit and miss
MatchResult result = engine.matchBundle(pattern, bundle);
System.out.println(result.matched() + " " + result.observations());
}
}
// 4. custom type with a computed property
engine.registerType("x-acme-widget", obj -> {
long score = ((Number) obj.getOrDefault("risk_score", 0)).longValue();
obj.put("risk_band", score > 80 ? "high" : "low");
return obj;
});
// [x-acme-widget:risk_band = 'high'] now matches enriched bundles
}
Errors
| Exception | Thrown by |
|---|---|
ParseException | invalid pattern syntax |
ModelException | invalid JSON / not a bundle |
MatchException | matching failure |
ValidationException | a registerType hook threw |
All extend StixException (unchecked, carries a code()).
Notes
Engine,Pattern, andBundlehold native handles: use try-with-resources; ajava.lang.ref.Cleanerfrees anything not explicitly closed.- Hooks run at
parseBundletime, applied Java-side (Jackson) — no JNI callbacks. - Bundling the native library into the jar per-platform is a publish-time follow-up.
TypeScript (Node)
@stix-rust/node — a native Node addon (napi-rs). Deep structure arrives as plain
JS objects. If you need the browser, use the wasm package;
the API is identical.
Install
npm install @stix-rust/node
From source instead:
cd bindings/typescript-node && npm install && npm run build
Worked example
import { Engine, ParseError, ValidationError } from "@stix-rust/node";
const engine = new Engine();
// 1. parse a pattern; the AST is a plain object
const pattern = engine.parsePattern("[ipv4-addr:value = '198.51.100.5']");
console.log(pattern.ast.expression);
// 2. import a bundle; iterate its objects
const bundle = engine.parseBundle(json);
console.log(bundle.objectCount(), [...bundle].map((o) => o.type));
bundle.object(99); // undefined when out of range
// 3. match — hit and miss
const result = engine.matchBundle(pattern, bundle);
console.log(result.matched, result.observations);
const miss = engine.parsePattern("[ipv4-addr:value = '203.0.113.9']");
console.assert(!engine.matchBundle(miss, bundle).matched);
// 4. custom type with a computed property
engine.registerType("x-acme-widget", (obj) => ({
...obj,
risk_band: obj.risk_score > 80 ? "high" : "low",
}));
const banded = engine.parseBundle(widgetBundleJson);
const hit = engine.parsePattern("[x-acme-widget:risk_band = 'high']");
console.assert(engine.matchBundle(hit, banded).matched);
Errors
| Error class | Thrown by |
|---|---|
ParseError | invalid pattern syntax |
ModelError | invalid JSON / not a bundle |
MatchError | matching failure |
ValidationError | a registerType hook threw |
All extend StixError (which carries a .code). Hooks run at parseBundle time;
throwing inside one rejects the object.
Notes
- Prebuilt binaries per platform ship with the published package; from source, the build compiles the addon for your machine.
- Full TypeScript types included; Node ≥ 18.
TypeScript (wasm)
@stix-rust/wasm — a portable WebAssembly build (wasm-bindgen) that runs in
Node and the browser. The API is identical to
@stix-rust/node; on the web target the module is
initialized asynchronously first.
Install
npm install @stix-rust/wasm
From source instead:
cd bindings/typescript-wasm && npm install && npm run build
(Node target; npm run build:web produces the browser build.)
Worked example
import { Engine, ParseError, ValidationError } from "@stix-rust/wasm";
const engine = new Engine();
// 1. parse a pattern; the AST is a plain object
const pattern = engine.parsePattern("[ipv4-addr:value = '198.51.100.5']");
console.log(pattern.ast.expression);
// 2. import a bundle; iterate its objects
const bundle = engine.parseBundle(json);
console.log(bundle.objectCount(), [...bundle].map((o) => o.type));
bundle.object(99); // undefined when out of range
// 3. match — hit and miss
const result = engine.matchBundle(pattern, bundle);
console.log(result.matched, result.observations);
const miss = engine.parsePattern("[ipv4-addr:value = '203.0.113.9']");
console.assert(!engine.matchBundle(miss, bundle).matched);
// 4. custom type with a computed property
engine.registerType("x-acme-widget", (obj) => ({
...obj,
risk_band: obj.risk_score > 80 ? "high" : "low",
}));
const banded = engine.parseBundle(widgetBundleJson);
const hit = engine.parsePattern("[x-acme-widget:risk_band = 'high']");
console.assert(engine.matchBundle(hit, banded).matched);
Errors
| Error class | Thrown by |
|---|---|
ParseError | invalid pattern syntax |
ModelError | invalid JSON / not a bundle |
MatchError | matching failure |
ValidationError | a registerType hook threw |
All extend StixError (which carries a .code). Hooks run at parseBundle time;
throwing inside one rejects the object.
Notes
- Browser use: build with the web target and initialize the module before use
(per wasm-pack’s
--target webconventions); after init the API above is the same. - Everything runs synchronously and single-threaded inside the wasm module — fine for the parse/import/match workload.
Architecture
Crate graph
Five focused crates with acyclic edges; each lower crate is usable standalone. Four bindings wrap one shared FFI facade:
graph TD
subgraph core [Rust core]
P[stix-pattern<br/>lexer + parser] --> U[stix<br/>umbrella]
M[stix-model<br/>objects, bundles, registry] --> U
P --> X[stix-matcher<br/>engine]
M --> X
X --> U
U --> F[stix-ffi<br/>facade]
end
subgraph bindings [Language bindings]
F --> PY[python<br/>PyO3]
F --> JV[java<br/>jni-rs]
F --> TN[ts-node<br/>napi-rs]
F --> TW[ts-wasm<br/>wasm-bindgen]
end
Data flow
flowchart LR
PS[pattern string] -->|stix-pattern::parse| AST[Pattern AST]
BJ[bundle JSON] -->|ModelRegistry::parse_bundle| B[Bundle]
B --> OS[ObjectStore<br/>id → object]
AST --> MX{{stix-matcher}}
B --> MX
OS -->|deref _ref paths| MX
MX --> MR[MatchResult<br/>matched + observation indices]
Custom-type hooks run inside parse_bundle (once per object, synchronously); their
output is stored as data, so nothing re-enters host-language code during matching.
One match, step by step
Pattern: [network-traffic:src_ref.value = '198.51.100.5'] against a bundle
containing an ipv4-addr, a network-traffic whose src_ref points at it, and an
observed-data referencing both.
- Observations.
match_bundlefinds theobserved-dataSDO → one observation containing the two SCOs, and builds anObjectStoreover the bundle. - Candidates. The expression references one type,
network-traffic; the observation has one candidate → one binding to try. - Path resolution.
src_refresolves on the bound object to the string"ipv4-addr--a1"; because the path continues (.value), the matcher treats it as an id, dereferences it through the store, and readsvalueoff theipv4-addr→"198.51.100.5". - Operator.
=compares the resolved value with the literal → true. - Result. The observation satisfies the block →
MatchResultwithmatched = trueand that observation’s index.
The FFI facade
stix-ffi is a pure-Rust crate (no FFI macros) that every binding wraps: an
Engine handle owning the registry, opaque Pattern/Bundle handles, a plain
MatchOutcome, and a flat FfiError { code, message } each language maps onto its
own exception hierarchy. Deep structure crosses as JSON; each binding converts it to
native objects at its edge. This keeps all four bindings thin and behaviorally
identical.
How the repo is run
The repository is organized into agent-owned areas (core, one per binding) with an
ownership map and issue workflow in
AGENTS.md;
design specs and implementation plans live under docs/superpowers/.
Limitations & Caveats
The sharp edges, in one place. Read this before production use.
-
FOLLOWEDBYis not matched. It parses, but evaluation returns an explicitUnsupportederror. Status: planned. -
Temporal qualifiers are not matched.
WITHIN,REPEATS, andSTART..STOPparse but returnUnsupportedat match time. Status: planned. -
Timestamps compare as strings. RFC3339 values are not parsed into instants:
2020-01-01T00:00:00Z≠2020-01-01T00:00:00.000Zeven though they denote the same moment. Workaround: normalize timestamp formats on ingest (a custom-type hook is a good place). -
Binding enumeration, not full constraint search. Within an observation the matcher binds one object per referenced type. This gives correct “same object” semantics for the overwhelming majority of patterns, but exotic patterns needing several objects of the same type simultaneously may differ from the MITRE reference. Status: full binding-set semantics are future work.
-
Reference paths need an
ObjectStore. A path throughsrc_ref(or any_ref) resolves via the store; without one it resolves to nothing — a non-match, not an error.match_bundle/match_scosbuild the store for you. -
Typed objects synthesize properties. What
property()returns is not necessarily what the JSON contained (that’s a feature — see Custom Object Types) — but don’t assume a 1:1 mapping. -
[*]expands, per-element.x:list[*] = 'v'passes if any element matches; there is no cross-step backtracking beyond that expansion. -
Integers and floats are distinct in storage, and comparisons promote numerically — but a pattern’s
5and data’s"5"(string) never match. -
LIKEis anchored;MATCHESis not.LIKEmust cover the whole value;MATCHESsearches anywhere in it. An invalid regex never matches (no error). -
ISSUBSET/ISSUPERSETare IP/CIDR-only. IPv4 or IPv6, never mixed families; unparseable input never matches. There is no generic string-set containment.
Also worth knowing: custom-type hooks run only at parse/import time — if you mutate an engine’s registrations, previously parsed bundles are unaffected.
API Reference
The full rustdoc API reference is published alongside this guide:
It is rebuilt from main on every merge. Every public item carries documentation —
enforced by the missing_docs lint across all five crates.
Contributing
Issues and pull requests are welcome. The project keeps a disciplined loop: design
specs and implementation plans live under docs/superpowers/, and the repository is
divided into agent-owned areas described in
AGENTS.md —
file issues with an area:* label and keep PRs within one area.
Dev commands
Rust core (crates/):
cargo test # all suites
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all
cargo doc --workspace --no-deps # API docs
Bindings (each is excluded from the root workspace and builds standalone):
cd bindings/python && maturin develop && python -m pytest -q
cd bindings/typescript-node && npm install && npm test
cd bindings/typescript-wasm && npm install && npm test
cd bindings/java && gradle test
This site:
cargo install mdbook mdbook-mermaid
mdbook serve docs/book # live-reload preview at localhost:3000
Ground rules
- Keep
cargo testgreen and clippy clean (-D warnings, includingmissing_docs— every public item is documented). - Changes confined to one area per PR; cross-area work is split.
- Interface changes update the area README and this book in the same PR.