stix
Threat intelligence in STIX 2.1: read bundles, pull out indicators and their IOCs (IPs, domains, URLs, hashes, emails) from STIX patterns, turn them into detect rules you can run on logs, write your own IOCs as a bundle, and download from TAXII 2.1 servers.
Reading and writing bundles works everywhere, including the playground. The TAXII client (stix.taxii) talks to a server, so it needs the TB app on your computer.
import stix
Example
import stix
# Write IOCs as a STIX 2.1 bundle, then read the indicators back.
let bundle = stix.bundle(["203.0.113.42", "login-verify.example.net"])
for ind in stix.indicators(bundle) {
say(f"{ind.pattern} -> {ind.iocs}")
}
# Indicators become detect rules you can run on logs.
let rules = stix.to_detect_rules(bundle)
say(
f"{len(rules)} rules, the first looks at: {rules[0].detection.ioc.map(fn(m) { return m.keys()[0] }).slice(0, 3)}",
)
say(stix.pattern("[ipv4-addr:value IN ('192.0.2.1', '192.0.2.2')]"))
Functions
parse(text)
returns mapRead STIX JSON text into a bundle map {"type": "bundle", "id", "objects"}. A bare list of objects or a single object is wrapped in a bundle; so is a TAXII envelope ({"objects": [...]}).
Visualizehow stix.parse works
stix.parse("[{\"type\": \"ipv4-addr\", \"id\": \"ipv4-addr--1\", \"value\": \"203.0.113.9\"}]") gives {"type": "bundle", "objects": [{"type": "ipv4-addr", "id": "ipv4-addr--1", "value": "203.…
Real result: worked out by running this example with TB while the site was built.
read(path)
returns mapRead a STIX JSON file into a bundle map (see stix.parse).
Visualizehow stix.read works
stix.read("apps/data/intel/sample-feed.stix.json").objects.map(o => o.type) gives ["identity", "indicator", "indicator", "indicator", "indicator", "indicator", "indicator"…
Real result: worked out by running this example with TB while the site was built.
write(path, bundle)
returns nilWrite a bundle (from stix.bundle() or stix.read()) as pretty JSON.
Visualizehow stix.write works
stix.write("feed-copy.json", feed) gives nil
Real result: worked out by running this example with TB while the site was built.
stringify(bundle)
returns textThe bundle as pretty-printed STIX JSON text.
Visualizehow stix.stringify works
stix.stringify(stix.bundle(["203.0.113.9"])) gives "{\n \"type\": \"bundle\",\n \"id\": \"bundle--b4378729-22bb-5bd3-ad6c-22d6b3f86f6e\",\…
Real result: worked out by running this example with TB while the site was built.
indicators(bundle, options?)
returns listThe indicator objects in a bundle, each as a map.
Fields of each indicator
"id", "name", "description"from the STIX object."pattern", "pattern_type", "indicator_types"from the STIX object."valid_from", "valid_until", "expired", "confidence"from the STIX object."tags"ATT&CK tags from the kill chain phases."iocs"a list of {"type", "value"} maps."skipped"comparisons that aren't exact (LIKE, MATCHES, !=).
IOCs come from pattern comparisons with = or IN. For example, [ipv4-addr:value = '198.51.100.7'] gives {"type": "ip", ...}.
IOC types
- ip, domain, url, email, filename, cidr.
- md5, sha1, sha256 (from file hashes).
Revoked indicators are left out. So are expired ones (valid_until in the past).
Options
expiredtrue keeps the expired indicators.
Visualizehow stix.indicators works
stix.indicators(feed, {"active": true}) gives [{"id": "indicator--a1d3a1e2-0c1e-4f7d-8c2b-1b7b4b0e2f01", "name": "SSH password guessing…
Real result: worked out by running this example with TB while the site was built.
iocs(bundle)
returns mapEvery IOC in a bundle (from indicator patterns and from cyber observables such as ipv4-addr objects), grouped like ioc.extract(): {"ips", "domains", "urls", "emails", "md5", "sha1", "sha256", "sha512", "cidrs", "filenames"}, each a list without repeats.
Visualizehow stix.iocs works
stix.iocs(feed) gives {"ips": ["203.0.113.42", "198.51.100.77", "198.51.100.23"], "domains": ["login-verify.exa…
Real result: worked out by running this example with TB while the site was built.
pattern(text)
returns listThe IOCs in one STIX pattern: stix.pattern("[ipv4-addr:value = '203.0.113.9' OR domain-name:value = 'bad.example']") gives [{"type": "ip", "value": "203.0.113.9", "object", "property"}, ...].
Visualizehow stix.pattern works
stix.pattern("[ipv4-addr:value = '203.0.113.9' OR domain-name:value = 'bad.example']") gives [{"type": "ip", "value": "203.0.113.9", "object": "ipv4-addr", "property": "value"}, {"ty…
Real result: worked out by running this example with TB while the site was built.
to_detect_rules(bundle, options?)
returns listDetect rules (for detect.run()) from a bundle's indicators. By default there is one rule per indicator.
A rule alerts when a record field holds one of its IOCs:
- ip, src_ip or dst_ip for IPs (CIDR ranges use |cidr).
- domain, host or query for domains.
- url, hash, email or file name fields for those types.
The level comes from the indicator's confidence (high when there is none). ATT&CK kill chain phases become tags, so attack.map() works on the alerts.
Options
levela fixed level for every rule, like "critical".group"type" makes one rule per IOC type instead of per indicator. This is better for big feeds.fieldswhich record fields to check for a type, like {"ip": ["client"]}.expiredtrue keeps expired indicators.
Visualizehow stix.to_detect_rules works
stix.to_detect_rules(feed, {"level": "high"}) gives [{"name": "Threat intel: SSH password guessing source", "level": "high", "detection": {"i…
Real result: worked out by running this example with TB while the site was built.
bundle(iocs, options?)
returns mapWrite IOCs as a STIX 2.1 bundle of indicators. Ids are stable: the same IOC always gets the same indicator id.
iocs can be:
- a list of text. The kind is worked out: IP, domain, URL, email, hash, CIDR.
- a list of {"type", "value", "name"?, "description"?} maps.
- the map from ioc.extract().
Options
indicator_typesthe STIX indicator types (default: ["malicious-activity"]).descriptiontext for every indicator.confidence0-100.valid_untilwhen the indicators expire.timethe created / valid_from timestamp (default: now).tagsattack.<tactic> tags become kill chain phases.
Example
let b = stix.bundle(["198.51.100.7", "bad.example"])
say(len(b.objects))
Visualizehow stix.bundle works
stix.bundle(["203.0.113.9", "bad.example", "http://bad.example/x"], {"confidence": 80}).objects.map(o => o.pattern) gives ["[ipv4-addr:value = '203.0.113.9']", "[domain-name:value = 'bad.example']", "[url:value …
Real result: worked out by running this example with TB while the site was built.
taxii(url, auth?, options?)
returns clientA TAXII 2.1 client. url is the server's discovery address (like https://intel.example/taxii2/) or an API root.
auth can be:
nilno login.texta bearer token.- {"user", "password"}, {"token"} or {"header", "key"}.
The auth is never printed.
Methods
discover()gives back a map about the server.collections()a list of {"id", "title", "description", "can_read", "can_write"}.objects(collection_id, added_after?)a bundle. It follows the server's paging, up to max_pages pages.
Options
timeoutseconds to wait.max_pagesthe most pages objects() reads (default: 20).
Visualizehow stix.taxii works
stix.taxii("https://intel.example/taxii2/", {"token": "…"}).collections() gives [{"id": "91a7b528", "title": "Phishing feed", "can_read": true}]
Illustration: example values, drawn from what the function does.