IOC Federation¶
An IOC (Indicator of Compromise) list is a versioned inventory of known-bad
artifacts: confirmed-malicious domains, file hashes, and package names. It sits
outside the heuristic score. When a PKGBUILD matches an IOC entry,
TrustSight emits a definitive, attributed finding ("this source domain is on
emiliano-go's known-bad list, incident atomic-arch-2026-06") with no score,
no band, and no probability language. This is detection, not inference.
The design rests on four principles:
- State, not rules. IOCs are observations about the world, so they live in the baseline layer next to the corpus baseline (A13/A13b), never in the rule config. A rule override or weight cannot reach them.
- Attribution, not aggregation. Every match names the curator that flagged the artifact. The user sees who called it bad, and can follow the incident and evidence.
- Federation, not centralization. Independent curators publish signed baselines; TrustSight verifies, imports, and reports with source labels.
- Expiration, not permanence. IOCs carry a time-to-live. An expired indicator is reported as expired, never silently ignored and never permanently flagged.
The baseline format¶
A baseline is a directory:
manifest.json¶
{
"version": 1,
"source": "emiliano-go",
"created_at": "2026-08-09T00:00:00Z",
"expires_at": "",
"signature": "<hex ed25519 signature, or empty>",
"public_key": "<hex ed25519 public key, or empty>"
}
iocs.jsonl¶
One JSON object per line:
{"type": "domain", "value": "evil-cdn.xyz", "source": "emiliano-go", "confidence": "high", "provenance": "ASA-2026-06", "campaign": "atomic-arch-2026-06", "expires_at": "2026-09-15T00:00:00Z"}
{"type": "hash", "value": "deadbeef...", "source": "emiliano-go", "confidence": "high", "provenance": "vendor report"}
{"type": "package", "value": "malicious-aur-pkg", "source": "emiliano-go", "provenance": "ASA-2026-06"}
{"type": "file_path", "value": "src/hooks/deps", "source": "emiliano-go", "confidence": "high", "provenance": "Sonatype-2026-003775"}
{"type": "pkgbuild_pattern", "value": "\\bnpm\\s+install\\b", "source": "emiliano-go", "confidence": "high", "provenance": "aur-general"}
Any entry with an unknown type, an unusable value, or no source is
dropped with a warning; the rest import.
Indicator types and normalization¶
| Type | Match target | Normalization |
|---|---|---|
domain |
Host of a source= URL (or any referenced host) |
Lowercased, IDNA/punycode-folded, reduced to the registered domain, so a subdomain and the xn-- and unicode spellings all collapse to one value. |
hash |
Digests inside sha256sums / sha512sums / md5sums (and any other hex digest in the visible text) |
Lowercased; must be a hex digest of a known length (32/40/56/64/96/128). |
package |
pkgname / pkgbase and declared dependency names |
Lowercased, exact match. |
file_path |
A file the diff added or modified, and a file the recipe names (install=, local source=() entries) |
Separators folded to /, a leading ./ and trailing / removed. Case-sensitive. Removals are not a compromise signal and do not match. |
pkgbuild_pattern |
Added diff lines, matched as a regular expression | The pattern text, compiled once. Invalid or catastrophic-backtracking patterns are refused at load. |
file_path and pkgbuild_pattern are the two non-equality types. file_path
is still an exact comparison on a normalized path. pkgbuild_pattern is the
one type that matches a shape rather than an artefact, so it is treated as a
weaker claim: a pattern finding is never FATAL - a confirmed tier is
capped at CRITICAL - and an unsafe pattern is refused rather than run. The
corpus pivot understands domain, hash and package only; the other two
describe a file or a line, which a stored corpus row does not reference.
Pattern safety and caps¶
A signed baseline is third-party text, so pkgbuild_pattern entries are
checked statically at load - never by running the pattern on adversarial
input, which is exactly what hangs on the shapes being rejected:
- A nested quantifier (
(a+)+) or a quantified alternation ((a|aa)+) is refused; both are the classic catastrophic-backtracking shapes, and neither is anything a curator needs. - An invalid regex, an empty pattern, or one longer than 2,000 characters is refused.
- At most 500 pattern entries load; the rest are ignored, so an unbounded set cannot become a per-line denial of service.
- A refused pattern is dropped with a warning, and the analysis marks itself degraded rather than matching a smaller list than the operator believes is loaded.
Signature¶
signature is a detached Ed25519 signature over the canonical
manifest.json (with the signature field emptied) concatenated with
iocs.jsonl byte-for-byte. Verification uses the public_key carried in the
manifest; IOC curator keys are not pinned in configuration. An unsigned
baseline imports only with --allow-unsigned, which is for a baseline you
built and trust locally.
How matching works¶
A dedicated IOC Match stage runs after rule matching and before scoring:
It reads added diff lines and the current package text used for package-level
checks, then queries the stored indicators for every source-URL host, every checksum
and hex digest, and the package/pkgbase/dependency names. Hits are attached to
PackageFact.ioc_matches; see the report schema.
Each IocMatch carries type, value, source, confidence, provenance,
campaign, the surface it was found on (source_host, checksum_array,
package_name, a dependency field, ...), the line, and expired.
What an IOC match does not do¶
- It never changes the score. Matches live on
ioc_matches, never inscore_breakdown; the same PKGBUILD scores identically with or without a hit. IOCs are attribution the reviewer can verify, not a number. - It is not downgraded by a coverage gap, positive evidence, or an override. If an indicator matched in the analysed text, it is reported.
- It does not disappear when it expires. An expired indicator is still
reported, labelled expired, so a lapsed indicator never reads as clean. Only
trustsight ioc listhides expired entries by default (pass--include-expired). - A cleaned-up package name is not an indicator. A
packageentry flags the name forever, a false positive the moment the malicious commit is reverted. Compromised-but-remediated names live in a separate point-in-time exposure reference underdata/aur-compromised/, which H056 never matches.
These properties are enforced by the gates an IOC match carries its source,
IOC matches never contribute to the score, an expired IOC is never silent,
IOCs are not in the rule config layer, and a pattern IOC that risks ReDoS
is refused (see the
enforcement map).
Configuration and commands¶
The [baselines.ioc] section and its feeds are documented in
configuration. The
trustsight ioc {sources,import,update,list,export} commands are documented
in the CLI reference.
Storage is the ioc_entries table, keyed by (type, value, source).
Importing a baseline for a source replaces that source's rows and leaves other
sources (and expired rows) untouched, so imports are idempotent and
attributable.
Harvesting¶
Turning advisories and community reports into signed baselines is the job of a separate project, not TrustSight's codebase: a harvester proposes candidate entries, human curators review and sign, and TrustSight only ever consumes the signed output. Keeping the harvester out of the tool is what keeps the IOC gate non-evadable by construction: a hash either matches or it does not, and no code in TrustSight decides what belongs on the list.
Federation¶
The indicator tier (I-series) formalizes what this page describes: signed external data as a first-class tier, matched outside the heuristic score and reported at maximum alert priority. The Indicators reference documents the tier and H056.
A federated source is a publisher's signed indicator set, fetched with
ioc update --federation <url> --publisher-key KEY.pem. The set is verified
against the pinned publisher key through the same signature-verification path
as the release channel before any entry is imported, so an unsigned or
mis-signed set is refused, never merged. Publisher trust is per key and
revocable; only hashed indicators travel (domains, hashes, names - no raw
maintainer data). The report already exposes the promoted indicators array
(see the report schema), so a consumer needs no change.
Cadence. ioc update refreshes the release-channel baseline and
ioc update --federation refreshes a publisher set, both on the operator's
cadence - typically the same schedule as the corpus cycle. A stale or
unsigned baseline is refused rather than merged; the tool never falls back to
an older signed set silently. A fresh install ships an empty list, so a
miss is uninformative until the operator imports a signed set.