Python API¶
The engines are plain functions over a typed Catalog and an OrgPosture.
from vantage.catalog import load_catalog
from vantage.coverage import compute_coverage
from vantage.io import load_org
from vantage.selectors import expand_org
cat = load_catalog("real") # or "seed", "nist", or a catalog JSON path
org = expand_org(cat, load_org("vantage/postures/acme-real.yaml"))
cov = compute_coverage(cat, org)
print(cov.summary())
vantage.catalog
¶
Catalog loading: the offline toy seed catalog, or the real-data catalog built from ATT&CK STIX + CIS v8 official mapping + SigmaHQ (see vantage.ingest.build).
vantage.io
¶
YAML (de)serialisation of org posture files.
Field types are checked here, so a malformed posture file gives one ValidationError naming the
file and the field instead of a TypeError deep inside an engine.
org_from_dict(d)
¶
Build an OrgPosture from parsed YAML, checking every field's type.
org_to_dict(org)
¶
Inverse of org_from_dict (sorted, so the YAML is stable).
load_org(path)
¶
Read a posture YAML file. Syntax and type errors raise ValidationError naming the file.
save_org(org, path)
¶
Write a posture YAML file.
vantage.models
¶
Typed contracts. Everything the engines consume is validated here.
TECHNIQUE_RE = re.compile('^T\\d{4}(\\.\\d{3})?$')
module-attribute
¶
ValidationError
¶
Bases: ValueError
Invalid catalog, posture file or selector (the CLI prints it and exits 2).
CoverageStatus
¶
Technique
dataclass
¶
An ATT&CK (sub-)technique.
id
instance-attribute
¶
name
instance-attribute
¶
tactic
instance-attribute
¶
description = ''
class-attribute
instance-attribute
¶
tactics = ()
class-attribute
instance-attribute
¶
all_tactics
property
¶
Every tactic the technique belongs to.
__init__(id, name, tactic, description='', tactics=())
¶
__post_init__()
¶
Control
dataclass
¶
A framework control and the techniques its official mapping assigns to it.
id
instance-attribute
¶
framework
instance-attribute
¶
title
instance-attribute
¶
mitigates = frozenset()
class-attribute
instance-attribute
¶
text = ''
class-attribute
instance-attribute
¶
ig = None
class-attribute
instance-attribute
¶
function = ''
class-attribute
instance-attribute
¶
baselines = frozenset()
class-attribute
instance-attribute
¶
__init__(id, framework, title, mitigates=frozenset(), text='', ig=None, function='', baselines=frozenset())
¶
LogSource
dataclass
¶
Detection
dataclass
¶
A detection rule, its techniques and the log sources it needs.
id
instance-attribute
¶
title
instance-attribute
¶
techniques
instance-attribute
¶
requires
instance-attribute
¶
cost = 1.0
class-attribute
instance-attribute
¶
level = ''
class-attribute
instance-attribute
¶
status = ''
class-attribute
instance-attribute
¶
__init__(id, title, techniques, requires, cost=1.0, level='', status='')
¶
__post_init__()
¶
Segment
dataclass
¶
ZeroTrustFacts
dataclass
¶
Declared segmentation and identity facts for the Zero-Trust score.
segments = field(default_factory=list)
class-attribute
instance-attribute
¶
open_flows = field(default_factory=set)
class-attribute
instance-attribute
¶
mfa_coverage = 0.0
class-attribute
instance-attribute
¶
privileged_accounts = 0
class-attribute
instance-attribute
¶
pam_vaulted = 0
class-attribute
instance-attribute
¶
device_posture_checks = False
class-attribute
instance-attribute
¶
stale_accounts = 0
class-attribute
instance-attribute
¶
total_accounts = 1
class-attribute
instance-attribute
¶
__init__(segments=list(), open_flows=set(), mfa_coverage=0.0, privileged_accounts=0, pam_vaulted=0, device_posture_checks=False, stale_accounts=0, total_accounts=1)
¶
validate()
¶
Raise ValidationError for unknown segments or out-of-range values.
OrgPosture
dataclass
¶
What the org claims and what is actually live.
name
instance-attribute
¶
claimed_controls
instance-attribute
¶
ingested_log_sources
instance-attribute
¶
deployed_detections
instance-attribute
¶
zero_trust = field(default_factory=ZeroTrustFacts)
class-attribute
instance-attribute
¶
__init__(name, claimed_controls, ingested_log_sources, deployed_detections, zero_trust=ZeroTrustFacts())
¶
Mitigation
dataclass
¶
Catalog
dataclass
¶
Techniques, controls, log sources, detections and mitigations, with provenance.
techniques
instance-attribute
¶
controls
instance-attribute
¶
log_sources
instance-attribute
¶
detections
instance-attribute
¶
mitigations = field(default_factory=dict)
class-attribute
instance-attribute
¶
meta = field(default_factory=dict)
class-attribute
instance-attribute
¶
__init__(techniques, controls, log_sources, detections, mitigations=dict(), meta=dict())
¶
validate()
¶
Raise ValidationError if any edge points at an unknown id.
validate_org(org)
¶
Raise ValidationError if the posture names ids the catalog does not have.
vantage.coverage
¶
Coverage engine: defended / paper_only / detected_only / blind per technique.
LEVEL_WEIGHT = {'critical': 1.0, 'high': 0.9, 'medium': 0.7, 'low': 0.5, 'informational': 0.3}
module-attribute
¶
STATUS_WEIGHT = {'stable': 1.0, 'test': 0.8, 'experimental': 0.6}
module-attribute
¶
DEFAULT_LEVEL_WEIGHT = 0.7
module-attribute
¶
DEFAULT_STATUS_WEIGHT = 0.8
module-attribute
¶
CoverageResult
dataclass
¶
Per-technique status plus the claims and live rules behind it.
status
instance-attribute
¶
claimed
instance-attribute
¶
detected_by
instance-attribute
¶
dead_detections = field(default_factory=set)
class-attribute
instance-attribute
¶
catalog = field(default=None, repr=False, compare=False)
class-attribute
instance-attribute
¶
confidence
property
¶
technique -> 1 - prod(1 - quality(rule)) over its live rules (computed lazily).
total
property
¶
Number of techniques scored.
claimed_pct
property
¶
Coverage the org would report on paper (any control claims the technique).
true_pct
property
¶
Defended (claimed AND detectable) share, in percent.
weighted_true_pct
property
¶
Defended coverage where each claimed technique counts by its detection confidence (noisy-OR of live rule qualities) instead of 0/1.
__init__(status, claimed, detected_by, dead_detections=set(), catalog=None, _confidence=None)
¶
by_status(s)
¶
Technique ids with status s, sorted.
pct(s)
¶
Share of techniques with status s, in percent.
detected_set()
¶
Techniques with at least one live rule, whatever is claimed.
summary()
¶
Rounded headline numbers, status counts and dead rules (CLI and API output).
rule_quality(d)
¶
Confidence in (0, 1] that a live rule really detects its tagged techniques.
live_detections(cat, deployed, ingested)
¶
A detection fires only if deployed AND every required log source is ingested.
compute_coverage(cat, org, *, ingested=None, deployed=None, controls=None)
¶
Score every technique; keyword overrides replace the org's ingested, deployed or claimed sets.
vantage.failure
¶
Failure propagation: knock out one node, recompute, rank single points of failure.
Three node kinds:
log_source/detection- the technique loses detection (defended -> paper-only, detected-only -> blind).techniques_gone_darklists those techniques.control- the claimed control fails (lapses, is mis-configured, or the claim is false): the technique loses its claimed mitigation (defended -> detected-only, paper-only -> blind).techniques_gone_darkthen lists the techniques left with no claimed control.
FailureImpact
dataclass
¶
simulate_failure(cat, org, kind, node)
¶
Recompute coverage with one node removed (reference implementation for the rankers).
rank_spofs(cat, org, top=None)
¶
All single-node failures in one pass (O(live rules x techniques/rule)).
A technique goes dark when node X fails iff every live detection covering it depends on X. Equivalent to calling simulate_failure for each node (tested), but fast on the full SigmaHQ catalog where there are thousands of deployed rules.
rank_control_spofs(cat, org, top=None)
¶
Claimed controls whose failure leaves techniques with no claimed mitigation, in one pass.
A technique loses its claim when control X fails iff X is the only claimed control mapped to it.
Equivalent to simulate_failure(kind="control") for every claimed control (tested).
vantage.recommend
¶
Greedy weighted set-cover: cheapest actions for maximum new technique coverage.
Actions
- deploy_detection - an undeployed rule whose log sources are already ingested (or will be)
- onboard_log_source - ingest a new source AND deploy every catalog rule it unlocks
Greedy ratio (new techniques / cost) gives the classic ln(n)-approximation for set cover.
ACTIONS = ('deploy_detection', 'onboard_log_source')
module-attribute
¶
Recommendation
dataclass
¶
One recommended action, its cost and the techniques it makes detectable.
recommend(cat, org, *, budget=None, max_steps=10, actions=ACTIONS)
¶
Greedy plan. actions restricts the action types (e.g. only log-source onboarding).
vantage.zerotrust
¶
Rule-based Zero-Trust posture score (0-100) from segmentation + IAM facts.
WEIGHTS = {'segmentation': 0.35, 'mfa': 0.25, 'privileged_access': 0.2, 'device_posture': 0.1, 'account_hygiene': 0.1}
module-attribute
¶
LATERAL_TACTICS = {'lateral-movement', 'discovery'}
module-attribute
¶
ZTScore
dataclass
¶
segmentation_score(zt)
¶
1 minus the criticality-weighted share of segment pairs with open flows.
score_zero_trust(zt, cat=None)
¶
Score segmentation, MFA, PAM, device posture and account hygiene.
vantage.selectors
¶
Posture-file selectors so real-catalog org files stay short.
"@ig1" / "@ig2" / "@ig3" (all CIS safeguards up to that Implementation
Group), "@family:AC|IA" (NIST 800-53 families), "@baseline:LOW|MODERATE|HIGH" (NIST SP 800-53B baselines), "@all", or globs such as "CIS-8." / "NIST-AC-".
ingested_log_sources: "@all" or globs such as "windows/*", "aws/cloudtrail". deployed_detections: "@all", "@status:stable", "@min-level:high", or globs over rule ids. Several "@..." terms in one entry are AND-ed with "&": "@status:stable&@min-level:medium". Plain ids pass through unchanged (and are validated by Catalog.validate_org).
A selector or glob that matches nothing in the catalog is an error (a typo such as "@status:stabel" would otherwise silently give 0% coverage), and so is an unknown Sigma level, Sigma status or SP 800-53B baseline name.
LEVELS = ['informational', 'low', 'medium', 'high', 'critical']
module-attribute
¶
STATUSES = {'stable', 'test', 'experimental', 'deprecated', 'unsupported'}
module-attribute
¶
BASELINES = {'LOW', 'MODERATE', 'HIGH', 'PRIVACY'}
module-attribute
¶
expand_org(cat, org)
¶
Resolve selectors in-place against the catalog and return the org.
vantage.automap
¶
Auto-map free-text controls to ATT&CK techniques.
Mappers (all expose rank(text, k) -> [(technique_id, score), ...]):
TfidfMapper- dependency-free TF-IDF + cosine against technique name/description.EmbeddingMapper- sentence-transformers embeddings + cosine (optional dependency).MitigationBridgeMapper- match the control text to ATT&CK mitigations (Mxxxx), then rank the techniques those mitigations address (STIXmitigatesrelationships). Uses only public ATT&CK data - never the CIS labels - but exploits the fact that controls are preventive/organisational text, which is much closer to mitigation prose than to adversary-behaviour prose.PopularityBaseline/RandomBaseline- sanity baselines for the benchmark.
evaluate (micro P/R@k, used by the offline seed demo) and evaluate_mapper (P@k, R@k,
MAP, per-control macro averages, used by benchmarks/bench_automap.py against the official
CIS v8 -> ATT&CK mapping).
TfidfMapper
¶
TF-IDF cosine between the control text and each technique's name and description.
name = 'tfidf-direct'
class-attribute
instance-attribute
¶
ids = list(cat.techniques)
instance-attribute
¶
model = _Tfidf([technique_doc(t) for t in cat.techniques.values()])
instance-attribute
¶
__init__(cat)
¶
scores(text)
¶
Similarity to every technique, aligned with self.ids.
rank(text, k=5)
¶
Top-k techniques for free text.
EmbeddingMapper
¶
Sentence-embedding cosine between the control text and each technique (ml extra).
name = f"embed-direct[{model.split('/')[-1]}]"
instance-attribute
¶
enc = _Encoder(model)
instance-attribute
¶
ids = list(cat.techniques)
instance-attribute
¶
mat = self.enc.encode([technique_doc(t) for t in cat.techniques.values()])
instance-attribute
¶
__init__(cat, model=DEFAULT_EMBED_MODEL)
¶
scores(text)
¶
Similarity to every technique, aligned with self.ids.
rank(text, k=5)
¶
Top-k techniques for free text.
MitigationBridgeMapper
¶
text -> ATT&CK mitigations -> techniques they mitigate.
score(t) = max_{m mitigates t} sim(text, m) + alpha * sim(text, t) The direct term only breaks ties inside a mitigation's technique group.
name = f"mitigation-bridge[{encoder if encoder == 'tfidf' else model.split('/')[-1]}]"
instance-attribute
¶
ids = list(cat.techniques)
instance-attribute
¶
idx = {t: i for i, t in enumerate(self.ids)}
instance-attribute
¶
mits = list(cat.mitigations.values())
instance-attribute
¶
alpha = alpha
instance-attribute
¶
direct = TfidfMapper(cat)
instance-attribute
¶
__init__(cat, encoder='tfidf', alpha=0.1, model=DEFAULT_EMBED_MODEL)
¶
scores(text)
¶
Bridge score per technique (aligned with self.ids); memoised per text.
rank(text, k=5)
¶
Top-k techniques for free text.
RandomBaseline
¶
PopularityBaseline
¶
Always predicts the techniques most often mapped by other controls (leave-one-out).
name = 'baseline-popularity'
class-attribute
instance-attribute
¶
cat = cat
instance-attribute
¶
counts = Counter(t for c in cat.controls.values() for t in c.mitigates)
instance-attribute
¶
__init__(cat)
¶
rank_for(control, k)
¶
Top-k techniques for a control from the other controls' labels (leave-one-out).
rank(text, k=5)
¶
Top-k techniques for free text.
evaluate_mapper(mapper, cat, ks=(5, 10, 20, 50), depth=200, ci=False)
¶
Macro-averaged P@k, R@k and MAP@depth. With ci=True also adds <metric>_ci = 95%
bootstrap interval over controls.
per_control_scores(mapper, cat, ks=(5, 10, 20, 50), depth=200)
¶
Per-control P@k, R@k and AP@depth against each control's mitigates set (controls
with no mapped techniques are skipped). Lists are aligned by control.
bootstrap_ci(values, n=1000, seed=0, alpha=0.05)
¶
Percentile bootstrap CI of the mean (resampling controls with replacement).
paired_bootstrap_ci(a, b, n=2000, seed=0, alpha=0.05)
¶
Mean of a-b and its percentile bootstrap CI, resampling the same controls for both.
vantage.ingest.nist
¶
Parser for the CTID Mappings Explorer NIST SP 800-53 rev5 -> ATT&CK Enterprise mapping (JSON).
Source: https://github.com/center-for-threat-informed-defense/mappings-explorer (mappings/nist_800_53/attack-16.1/nist_800_53-rev5/enterprise/...) - Apache-2.0. Each mapping object is one (control, technique) pair with mapping_type "mitigates"; objects with status "non_mappable" (technique has no mitigation in ATT&CK) carry no control and are skipped.
vantage.frameworks
¶
All control frameworks with a public ATT&CK mapping, as Control objects in one ATT&CK release.
- CIS Controls v8 (official CIS mapping, ATT&CK v8.2) - from the real catalog
- NIST SP 800-53 rev5 (CTID, ATT&CK v16.1) - control statement prose from the NIST OSCAL catalog, and SP 800-53B LOW / MODERATE / HIGH / PRIVACY baseline membership
- CRI Profile v2.1, CSA CCM 4.1, AWS, Azure, GCP, M365 (CTID Mappings Explorer)
Every technique id is carried forward to the catalog's ATT&CK release through revoked-by links
(AttackData.resolve); deprecated/unknown ids are dropped and counted.
TEXT_RICH = ('CIS v8', 'NIST 800-53', 'CSA-CCM-4.1', 'CRI-2.1')
module-attribute
¶
Framework
dataclass
¶
One control framework's controls, carried forward to the current ATT&CK release.
name
instance-attribute
¶
controls
instance-attribute
¶
stats = field(default_factory=dict)
class-attribute
instance-attribute
¶
source_attack = ''
class-attribute
instance-attribute
¶
mapped
property
¶
Controls with at least one mapped technique.
techniques
property
¶
Every technique some control of the framework maps to.
__init__(name, controls, stats=dict(), source_attack='')
¶
load_oscal()
¶
NIST SP 800-53 rev5 OSCAL catalog with SP 800-53B baseline membership attached.
nist_framework(attack, oscal=None, text='statement')
¶
text: 'statement' (OSCAL control statement), 'title' (name only, as in the CTID file).
ctid_framework(short, attack)
¶
One CTID Mappings Explorer framework (short is a key of paths.CTID_FRAMEWORKS).
cis_framework(catalog_controls, stats=None)
¶
CIS v8 safeguards from the built catalog (already carried forward at build time; pass the
catalog's meta['cis'] as stats to report those build-time counts).
all_frameworks(attack, catalog_controls, include_title_only_nist=False, cis_stats=None)
¶
Every framework keyed by display name (CIS first, then NIST, then the CTID files).
baseline_techniques(nist, oscal)
¶
Techniques mitigated by the CTID-mapped controls in each SP 800-53B baseline.
source_universe(release_file, attack)
¶
Current-release technique ids that existed (directly or via revoked-by) in an older ATT&CK release: the fair candidate set for scoring a mapping authored against that release.
vantage.transfer
¶
Supervised control -> ATT&CK mapping that is trained on one framework and applied to another.
TransferMapper combines three signals, all computed over the ATT&CK technique universe:
bridge- the zero-shot mitigation-bridge score (control text -> ATT&CK mitigations -> techniques; seevantage.automap.MitigationBridgeMapper). Uses no framework labels.knn- label transfer: the query control's k most similar training controls vote for their mapped techniques, weighted by text similarity (normalised to max 1).-
prior- how often each technique is mapped in the training framework (normalised to max 1).score(t) = bridge(t) + beta * knn(t) + gamma * prior(t)
beta and gamma are chosen on the training framework only, by leave-one-control-out
MAP@200 over a small grid; the test framework's labels are never seen. When the test control is
itself part of the training set (the in-framework diagonal), rank_for excludes it from the
neighbours and from the prior. Because (beta, gamma) were tuned on those same leave-one-out folds,
that diagonal is optimistic; nested_loo_aps gives the honest in-framework estimate, choosing
(beta, gamma) without the held-out control. (The TF-IDF vocabulary/IDF of the training texts still
includes the held-out control: a small transductive effect on the diagonal only.)
BETAS = (0.0, 0.25, 0.5, 1.0, 2.0, 4.0)
module-attribute
¶
GAMMAS = (0.0, 0.1, 0.25, 0.5, 1.0)
module-attribute
¶
TransferMapper
¶
Mitigation bridge + kNN label transfer + technique prior, trained on one framework's labels.
bridge = bridge or MitigationBridgeMapper(cat, encoder, model=model)
instance-attribute
¶
ids = [self.bridge.ids[i] for i in self._keep]
instance-attribute
¶
idx = {t: i for i, t in enumerate(self.ids)}
instance-attribute
¶
train = [c for c in train if c.mitigates & set(self.idx)]
instance-attribute
¶
k_nn = k_nn
instance-attribute
¶
counts = Counter(t for c in self.train for t in c.mitigates if t in self.idx)
instance-attribute
¶
name = f"transfer[{self.bridge.name.split('[')[1].rstrip(']')}, train={train_name}]"
instance-attribute
¶
__init__(cat, train, encoder='tfidf', model=DEFAULT_EMBED_MODEL, k_nn=10, beta=None, gamma=None, train_name='train', bridge=None, universe=None)
¶
universe restricts the candidate techniques (e.g. those that existed in the ATT&CK
release a framework was mapped against); bridge lets callers share one encoder.
fit()
¶
Grid-search (beta, gamma) by leave-one-out MAP@200 on the training framework.
nested_loo_aps()
¶
Honest in-framework estimate: for each training control, (beta, gamma) are re-chosen on the other n-1 controls' leave-one-out APs, then that control's AP is reported.
Returns {control id: AP@200}. Requires that fit ran (beta/gamma not passed in).
scores(text, exclude=None)
¶
Combined score per technique (aligned with self.ids); exclude = leave-one-out.
rank(text, k=5)
¶
Top-k techniques for free text.
rank_for(control, k)
¶
Top-k techniques for a control, leaving it out of the training labels.
PriorTransferBaseline
¶
Labels-only transfer: rank techniques by how often the training framework maps them.
train = list(train)
instance-attribute
¶
name = f'prior-only[train={train_name}]'
instance-attribute
¶
__init__(train, train_name='train')
¶
rank_for(control, k)
¶
Most frequent training techniques, excluding the query control's own labels.
rank(text, k=5)
¶
Most frequent training techniques (the text is ignored).
average_precision(ranked, gold, depth=200)
¶
AP@depth of one ranking against a gold set (normalised by min(|gold|, depth)).
vantage.ingest.ctid
¶
Generic parser for CTID Mappings Explorer files (any framework -> ATT&CK Enterprise).
Source: https://github.com/center-for-threat-informed-defense/mappings-explorer (Apache-2.0). Handles both "mitigates" mappings (NIST 800-53, CRI Profile, CSA CCM) and the "technique_scores" security-stack mappings (AWS, Azure, GCP, M365), where every capability->technique pair carries a protect/detect/respond category and a minimal/partial/significant score.
Only the capability id, description (its name) and group are kept as control text. The per-pair
comments field is deliberately dropped: it is written about the technique and would leak the
label into any auto-mapper that reads it.
POSITIVE = {'mitigates', 'technique_scores', 'technique_score'}
module-attribute
¶
CtidCapability
dataclass
¶
One CTID Mappings Explorer capability (control) and its techniques.
id
instance-attribute
¶
name
instance-attribute
¶
group
instance-attribute
¶
group_name
instance-attribute
¶
techniques = field(default_factory=set)
class-attribute
instance-attribute
¶
scores = field(default_factory=dict)
class-attribute
instance-attribute
¶
categories = field(default_factory=set)
class-attribute
instance-attribute
¶
__init__(id, name, group, group_name, techniques=set(), scores=dict(), categories=set())
¶
parse_ctid(doc)
¶
Capabilities and metadata from a Mappings Explorer JSON document (comments dropped).
load_ctid(path)
¶
Read a Mappings Explorer JSON file.
vantage.ingest.oscal
¶
NIST SP 800-53 rev5 OSCAL catalog + SP 800-53B baseline profiles.
Source: https://github.com/usnistgov/oscal-content (nist.gov/SP800-53/rev5/json) - public domain (US Government work). Gives the full control statement prose (which the CTID mapping file does not carry) and the LOW / MODERATE / HIGH / PRIVACY baseline memberships.
Control ids are normalised to the CTID zero-padded form: ac-2 -> AC-02,
ac-2.1 -> AC-02(01).
OscalControl
dataclass
¶
One OSCAL rev5 control: title, statement text and SP 800-53B baselines.
id
instance-attribute
¶
title
instance-attribute
¶
family
instance-attribute
¶
family_name
instance-attribute
¶
statement
instance-attribute
¶
guidance = ''
class-attribute
instance-attribute
¶
withdrawn = False
class-attribute
instance-attribute
¶
baselines = field(default_factory=set)
class-attribute
instance-attribute
¶
__init__(id, title, family, family_name, statement, guidance='', withdrawn=False, baselines=set())
¶
norm_id(oscal_id)
¶
OSCAL control id (ac-2.1) in CTID form (AC-02(01)).
parse_catalog(doc)
¶
Controls and enhancements from the OSCAL rev5 catalog, parameters substituted.
parse_profile(doc)
¶
Control ids included by an OSCAL baseline profile.
load_catalog_with_baselines(catalog, profiles)
¶
OSCAL catalog with each control's SP 800-53B baseline membership.