Python API¶
occam.ach
¶
Analysis of Competing Hypotheses (Heuer) with confidence grading.
Design rules (deliberately conservative):
- Hypotheses are ranked by least weighted inconsistency, never by most support. Consistent evidence is reported but does not drive the ranking.
- Every assessment includes an
unknown actorhypothesis and onefalse flaghypothesis per actor that planted-able markers point at. - Spoofable evidence (code overlap, language artifacts, metadata, claims) is down-weighted and a conclusion leaning mostly on it is capped at LOW.
- Non-diagnostic evidence (rated the same for every hypothesis) gets zero weight, as in Heuer's method.
- The rule-based rater only proposes cells; analysts can override any cell and the result is recomputed deterministically.
ACHEngine
dataclass
¶
actors
instance-attribute
¶
confidence_caps = True
class-attribute
instance-attribute
¶
diagnosticity = True
class-attribute
instance-attribute
¶
evidence
instance-attribute
¶
false_flag_hypotheses = True
class-attribute
instance-attribute
¶
kb = field(default_factory=load_bundled)
class-attribute
instance-attribute
¶
overrides = field(default_factory=dict)
class-attribute
instance-attribute
¶
question = 'Who conducted the activity?'
class-attribute
instance-attribute
¶
ranking_rule = 'heuer'
class-attribute
instance-attribute
¶
sensitivity = True
class-attribute
instance-attribute
¶
spoofable_discount = SPOOFABLE_DISCOUNT
class-attribute
instance-attribute
¶
support_weight = 0.5
class-attribute
instance-attribute
¶
unknown_hypothesis = True
class-attribute
instance-attribute
¶
__init__(actors, evidence, question='Who conducted the activity?', kb=load_bundled(), overrides=dict(), sensitivity=True, ranking_rule='heuer', support_weight=0.5, false_flag_hypotheses=True, unknown_hypothesis=True, spoofable_discount=SPOOFABLE_DISCOUNT, confidence_caps=True, diagnosticity=True)
¶
__post_init__()
¶
assess()
¶
matrix(evidence=None)
¶
override(evidence_id, hypothesis_id, rating)
¶
build_hypotheses(actors, evidence, false_flags=True, unknown=True)
¶
Actor hypotheses plus (by default) the mandatory alternatives.
false_flags / unknown exist only for ablation studies; turning them
off removes the deception and untracked-actor hypotheses.
rate(e, h, actors, kb)
¶
rate_actor(e, a, kb)
¶
Rate one evidence item against one actor profile.
TTPs: common techniques are non-diagnostic (N); a match is C, or CC when the technique is rare across ATT&CK groups; a technique only matched at parent level (T1059.001 vs T1059.003) is N; absence from the profile is I. Absence is only weakly inconsistent because public profiles are incomplete - this is what keeps large-profile bias in check.
render_matrix(a, evidence)
¶
occam.attribution
¶
Attribution over real ATT&CK group profiles: OCCAM's ACH vs a naive baseline.
Two attributors share one interface -- attribute(evidence) -> Attribution:
SimilarityAttributor-- the naive TTP-similarity baseline the research question is framed against: IDF-weighted cosine similarity between the observed techniques/software and every group profile, confidence = softmax over similarities. Spoofable markers are (naively) counted as matches for the actor they point at.ACHAttributor-- the Heuer ACH engine fromoccam.achwith the mandatory unknown-actor and false-flag hypotheses and capped confidence.
Both return the probability that the leading named answer is correct, so calibration (Brier, ECE) can be compared on equal terms.
GRADE_PROB = {'high': 0.875, 'moderate': 0.675, 'low': 0.5}
module-attribute
¶
ACHAttributor
¶
OCCAM's ACH engine applied to ATT&CK profiles.
engine_kw = engine_kw
instance-attribute
¶
grade_prob = dict(grade_prob or GRADE_PROB)
instance-attribute
¶
kb = kb
instance-attribute
¶
name = 'OCCAM ACH (least-inconsistency + confidence caps)'
class-attribute
instance-attribute
¶
profiles = profiles
instance-attribute
¶
ranking_rule = ranking_rule
instance-attribute
¶
shortlist = shortlist
instance-attribute
¶
__init__(profiles, kb, shortlist=None, ranking_rule='heuer', grade_prob=None, **engine_kw)
¶
assess(evidence)
¶
attribute(evidence)
¶
candidates(evidence)
¶
All profiles, or -- with shortlist -- the top-k by TTP similarity
plus every actor that a spoofable marker points at (so the matching
false-flag hypothesis is always on the table).
Attribution
dataclass
¶
confidence
instance-attribute
¶
flagged = None
class-attribute
instance-attribute
¶
hard_score = None
class-attribute
instance-attribute
¶
leading
instance-attribute
¶
leading_kind
instance-attribute
¶
probability
instance-attribute
¶
ranked_actors = field(default_factory=list)
class-attribute
instance-attribute
¶
__init__(leading, leading_kind, probability, confidence, ranked_actors=list(), flagged=None, hard_score=None)
¶
IDFCoverageAttributor
¶
Bases: SimilarityAttributor
Plain coverage baseline: the share of the observed IDF mass that a group's profile covers (no length normalisation of the profile, so large profiles are not penalised). Spoofable markers are ignored.
SimilarityAttributor
¶
IDF-weighted cosine nearest-profile matching with softmax confidence.
marker_boost is how much cosine each spoofable marker adds to the actor
it points at (0 = the baseline ignores spoofable evidence entirely) and
temperature is the softmax temperature. The defaults (0.15, 0.05) are
the marker-trusting baseline; results depend strongly on marker_boost
(see results/falseflag.md for a sweep).
kb = kb
instance-attribute
¶
marker_boost = marker_boost
instance-attribute
¶
name = 'baseline: TTP-similarity (IDF cosine + softmax)'
class-attribute
instance-attribute
¶
profiles = profiles
instance-attribute
¶
temperature = temperature
instance-attribute
¶
__init__(profiles, kb, temperature=0.05, marker_boost=0.15)
¶
attribute(evidence)
¶
hard_scores(evidence)
¶
Cosine on non-spoofable evidence only (no marker boost).
scores(evidence)
¶
evidence_from_items(items, kb, prefix='E')
¶
Turn observed technique / software ids into ACH evidence rows.
planted_markers(framed_actor, n=3, prefix='FF')
¶
Spoofable markers (code overlap, metadata, language) pointing at one actor.
occam.calibration
¶
Learned confidence-grade -> probability map (a calibrated grader).
ACH states its confidence as a grade (high / moderate / low). By default the
grade maps to fixed ICD-203 band midpoints (occam.attribution.GRADE_PROB).
GradeCalibrator learns that map from labelled outcomes instead: each
grade's probability is the Beta-smoothed empirical accuracy of answers given
at that grade, optionally monotone (a higher grade never gets a lower
probability, via pooling adjacent violators).
The benchmark fits it on the leave-one-report-out results with a group-wise 2-fold cross-fit, so reported calibrated scores never use their own labels.
GRADES = ('low', 'moderate', 'high')
module-attribute
¶
GradeCalibrator
dataclass
¶
Learned map from an ACH confidence grade to a stated probability.
fit estimates each grade's accuracy (Beta prior of strength
prior_strength around prior_mean), calibrator[grade] returns
it, and save / load round-trip it as JSON.
counts = field(default_factory=dict)
class-attribute
instance-attribute
¶
monotone = True
class-attribute
instance-attribute
¶
prior_mean = 0.5
class-attribute
instance-attribute
¶
prior_strength = 2.0
class-attribute
instance-attribute
¶
probs = field(default_factory=dict)
class-attribute
instance-attribute
¶
__getitem__(grade)
¶
__init__(prior_strength=2.0, prior_mean=0.5, monotone=True, probs=dict(), counts=dict())
¶
fit(grades, correct)
¶
load(path)
classmethod
¶
mapping()
¶
save(path)
¶
to_dict()
¶
occam.evaluation
¶
Attribution benchmark on real MITRE ATT&CK group usage.
Protocol -- leave-one-report-out:
Every ATT&CK uses relationship of a group cites the public report(s) it was
taken from. For each (group, cited report) pair with at least min_items
techniques/software, the items cited by that report form one held-out
incident. The group's profile is rebuilt without any item supported only
by that report (and group-usage counts are decremented accordingly), so the
attributor never sees the incident it is asked to attribute.
Three settings, each asking a different question of the attributor:
closed-- the true group is in the candidate set; correct = name it.open-- the true group is removed from the candidate set (an untracked actor); correct = decline to name anyone (unknown / false-flag).false_flag-- asclosedplusn_markersspoofable markers (code overlap, metadata, language, claim) planted to frame a different random group. The stated conclusion is correct if it names the true group or concludes "false flag:was framed" (which is true). Naming the framed group is a framed error; named_truecounts only conclusions that name the real group.
Extra settings (opt-in via settings=), added after the round-3 audit:
authentic-- control forfalse_flag: the same spoofable markers point at the TRUE group (genuine code/language overlap). Correct = name the true group; concluding "the true group was framed" is a false alarm (detected_framethen counts false alarms).mimicry-- the adversary copiesn_markersof the framed group's rarest techniques/software as ordinary hard (non-spoofable) evidence, the harder false flag in which nothing is pre-typed as spoofable. Scored asfalse_flag.
Stated probabilities are compared with outcomes via Brier score and ECE.
crossfit_calibrate additionally re-maps stated probabilities with
histogram binning fitted on the other half of the groups (2-fold, split by
group so no actor is in both halves), for a calibrated comparison.
ALL_SETTINGS = ('closed', 'open', 'false_flag', 'authentic', 'mimicry')
module-attribute
¶
Factory = Callable[[list[ActorProfile], AttackKB], object]
module-attribute
¶
SETTINGS = ('closed', 'open', 'false_flag')
module-attribute
¶
HoldoutWorld
dataclass
¶
Incident
dataclass
¶
Outcome
dataclass
¶
attribution
instance-attribute
¶
correct
instance-attribute
¶
detected_frame = False
class-attribute
instance-attribute
¶
framed = False
class-attribute
instance-attribute
¶
incident
instance-attribute
¶
marker_target = None
class-attribute
instance-attribute
¶
named_true = False
class-attribute
instance-attribute
¶
setting
instance-attribute
¶
top5 = False
class-attribute
instance-attribute
¶
__init__(incident, setting, attribution, correct, framed=False, top5=False, named_true=False, detected_frame=False, marker_target=None)
¶
StaticWorld
dataclass
¶
campaign_incidents(data, min_items=4, min_profile=8)
¶
One incident per ATT&CK campaign attributed to a group.
The incident is everything ATT&CK records the campaign as using. When it is held out, every item of the attributed group that is cited only by the campaign's own references is removed from the group's profile, so the campaign cannot be matched against a copy of itself.
crossfit_calibrate(outcomes, bins=10, prior=1.0)
¶
Histogram-binning recalibration, 2-fold cross-fitted by group.
Each outcome's stated probability is replaced by the (Laplace-smoothed) empirical accuracy of its probability bin in the other fold, so no incident is calibrated with its own label or its own group's labels.
crossfit_grade_calibrate(outcomes)
¶
Replace each stated probability with the learned probability of its ACH
grade, fitted on the other group fold (see occam.calibration.GradeCalibrator).
evaluate(data, factories, settings=SETTINGS, min_items=4, max_per_group=3, n_markers=3, seed=7, limit=None, progress=None, incs=None, hw=None)
¶
Run every attributor on every incident in every setting.
By default the incidents are incidents of data in a
HoldoutWorld; pass incs / hw for campaign or temporal
protocols.
incidents(data, min_items=4, max_per_group=None, min_profile=8)
¶
One incident per (group, cited report) with >= min_items items.
Groups whose remaining profile would have fewer than min_profile items
are skipped (nothing meaningful to attribute against).
mimicry_items(profiles, kb, framed, have, n)
¶
The n rarest (fewest ATT&CK groups) items of framed that the
incident does not already contain -- what a careful imitator would copy.
summarize(outcomes, probs=None)
¶
temporal_incidents(old, new, min_items=4, max_per_group=3, min_profile=8)
¶
Incidents that only exist in a later ATT&CK release.
Profiles come from old; incidents are (a) campaigns of new that are
absent from old and (b) (group, report) pairs of new whose report
old never cited for that group. Only groups already tracked in old
(with a profile of >= min_profile items) are kept: the question is
whether yesterday's knowledge attributes tomorrow's activity.
occam.extract
¶
Prose -> IOCs + ATT&CK techniques, every hit carrying a source span.
Baseline is deterministic keyword/regex matching. A trained sentence-level
text->technique classifier (occam.classifier) can be layered on top;
like any future (e.g. LLM) extractor it must emit span-anchored
TechniqueHit objects so that hallucinated facts (no span) can be rejected.
extract(text, source_id='doc', kb=None, classifier=None)
¶
Extract IOCs, techniques and software from prose.
classifier (optional, see occam.classifier) adds sentence-level
technique predictions; each still carries the sentence span it came from.
navigator_layer(result, name='OCCAM extraction')
¶
Minimal ATT&CK Navigator layer (v4.5 format) for the extracted techniques.
occam.neo4j
¶
Neo4j export of the Diamond-model graph as a Cypher script (stdlib only).
OCCAM keeps its graph in memory. This adapter writes the same Diamond schema
(Event, Capability, Infrastructure, Victim, Adversary
nodes; USES / OBSERVED / TARGETS / ATTRIBUTED_TO edges and an
optional campaign property on events) as idempotent MERGE statements,
so it can be loaded with cypher-shell -f graph.cypher or the Neo4j Browser.
No driver and no network connection are needed to produce the file.
LABELS = {'capability': ('Capability', 'USES'), 'infrastructure': ('Infrastructure', 'OBSERVED'), 'victim': ('Victim', 'TARGETS'), 'adversary': ('Adversary', 'ATTRIBUTED_TO')}
module-attribute
¶
to_cypher(events, campaigns=None)
¶
Render {event_id: {facet: [values]}} (the occam.graph input) as Cypher.