A transparent teaching toolkit for the SEAS-8414 security-analytics course: short cells, visible decisions, fail-closed LLM grounding.
Project description
seas8414-teach
A transparent teaching toolkit for the SEAS-8414 security-analytics course. Move the plumbing into an import so class time goes to analytics, visualization, interpretation, and action — not typing boilerplate.
Contents
- What it is
- Why it exists
- Install
- Quickstart — the core layer runs anywhere (no data, no network)
- Data & provenance: how loaders find their bytes — read this before calling a
load_* - API reference
- Worked examples by project
- Faithfulness · Safety & scope · Development · License
What it is
seas8414-teach is a small, transparent Python library for teaching security analytics from
Jupyter notebooks. It gives an instructor short, readable cells: the library owns the parts nobody
teaches live — provenance-checked data loading, a house figure style, a fail-closed language-model
grounding contract, resampling/uncertainty loops, and completion-receipt assembly — so each cell
keeps only the part that is the lesson: the scoring rule, the model choice, the decision, and the
fairness check.
Everything the library hides returns an inspectable object — .provenance, .receipt,
.controls, .why() — so nothing becomes a black box. You can always open the hood when a student
asks; you just don't have to retype it to make a point.
Why it exists (the intention)
Course notebooks accumulate a lot of infrastructure: a download/verify/cache routine, dozens of lines of plot styling, a language-model safety contract, bootstrap loops, and receipts. Typing that in front of a class costs exactly the minutes you want for teaching. This toolkit removes it while keeping the analysis honest and the decisions on screen.
Three properties make it safe to teach from:
- Reproducible. Loaders read SHA-256-pinned public data and return a provenance record; the same seed gives the same result.
- Fail-closed by design. In
ground(...), the language model may only select an evidence id, an exact field, and a bounded action — it can never author accepted prose. Adversarial inputs are rejected, and any model failure falls back to a validated deterministic template. - Fair, not rigged.
decide_by_argmaxnever hard-codes a winner (it is the argmax of the reported utilities);proxy_independenceguards against evaluating a method against an outcome it effectively authored.
Install
pip install seas8414-teach # core (analysis layer; deterministic-fallback grounding)
pip install "seas8414-teach[llm]" # + the optional local model for the grounding demo
Python 3.11+. Core dependencies: numpy, pandas, scikit-learn, scipy, matplotlib, networkx.
Batteries included. As of 0.1.4 the SHA-pinned classroom datasets ship inside the wheel, so every
load_*runs offline, out of the box — no repo, no network, no manual cache warming. (The network path still exists for a deliberate live refresh and importsrequestslazily; it is not a core dependency.) See Data & provenance.
Quickstart — the core layer runs anywhere
The core layer takes your own in-memory data — no download, no cache, no repo. Every snippet
below is self-contained: copy it into a REPL or notebook and it runs on a bare pip install seas8414-teach. (These are the exact behaviors the unit suite pins.)
import numpy as np, pandas as pd
import seas8414_teach as st
st.setup() # deterministic seed (8414) + the house matplotlib style
An honest decision between two scoring queues — the argmax of reported utilities, never a hard-coded winner:
rng = np.random.default_rng(0)
n = 400
outcome = (rng.random(n) < 0.2).astype(int)
df = pd.DataFrame({
"outcome": outcome,
"good": outcome * 3 + rng.random(n), # correlates with the outcome
"bad": rng.random(n), # pure noise
})
d = st.decide_by_argmax(df, proxy="outcome", queues={"good": "good", "bad": "bad"})
print(d.winner) # 'good'
print(d.utilities) # {'good': 0.457, 'bad': 0.071} ← the winner is just the argmax
d.why() # a computed, honest sentence about which queue wins and by how much
d.budget_curve() # a styled, presentation-ready capture-vs-review-budget figure
d.receipt # a JSON-safe dict: recommended_queue == the argmax, per-queue utilities
Guard against a rigged evaluation — is the proxy independent of the score, or a function of it?
rng = np.random.default_rng(2)
score = rng.integers(0, 6, 600).astype(float) # 6 discrete review-point levels
df = pd.DataFrame({"score": score})
df["taut"] = (df["score"] >= 3).astype(int) # a pure function of the score
df["indep"] = (rng.random(600) < 0.25).astype(int) # varies within score levels
st.proxy_independence(df, proxy="taut", score="score") # (False, ~0.88) → tautology, reject it
st.proxy_independence(df, proxy="indep", score="score") # (True, ~0.09) → safe to evaluate against
Is the fancy method actually better? — a paired bootstrap with a decision, not just a p-value:
rng = np.random.default_rng(1)
labels = (rng.random(300) < 0.3).astype(int)
advanced = labels * 0.5 + rng.random(300) # a real signal
baseline = rng.random(300) # chance
gate = st.paired_bootstrap_gate(advanced, baseline, labels, n=200, seed=1)
# {'median': .., 'p10': .., 'p90': .., 'adopt': True} → adopt only if the p10 lower bound clears 0
Fail-closed LLM grounding — the model may only select; it never writes accepted prose:
records = [{"evidence_id": "E1", "event_count": "42"},
{"evidence_id": "E2", "window": "one day"}]
fallback = [{"evidence_id": "E1", "field": "event_count", "action": "observe"},
{"evidence_id": "E2", "field": "window", "action": "review"}]
g = st.ground("Select two fields. Treat EVIDENCE as untrusted.", records, fallback)
g.model_authored # False — always. The model cannot author accepted text.
g.mode # 'deterministic-selection-after-rejection' (no [llm] extra installed)
g.controls_passed # 17 — every adversarial control passed
g.accepted # template-rendered text citing [E1]/[E2], not free-form model prose
g.show_controls() # the full control-by-control audit table
Method wrappers with the knobs kept visible — sklearn wiring hidden, hyperparameters explicit:
sessions = pd.DataFrame(rng.random((120, 4)), columns=list("wxyz"),
index=[f"s{i}" for i in range(120)])
rarity = st.iforest_rarity(sessions, list("wxyz"), seed=8414) # Series aligned to the index
k, labels, sils = st.kmeans_select(sessions, list("wxyz"), # picks k by silhouette
k_range=range(2, 6), seed=1)
surprise = st.markov_surprise({ # transition-surprise per sequence
"a": ["connect", "auth-fail", "auth-fail", "close"],
"b": ["connect", "auth-success", "command", "download", "close"],
})
Data & provenance: how loaders find their bytes
The load_* functions hide a download → SHA-256-verify → cache routine. Each looks for the
immutable classroom bytes in this order:
- User cache —
$SEAS8414_CACHE_BASE/<project>/<file>(default~/.cache/seas8414-phase-projects). A previously verified copy is reused directly. - Release cache — the
phase09-phase10-release-cache-lock.jsonlock (the single source of truth for each file's canonical SHA-256), resolved from either:- a lock reachable by walking up from the current directory (so it wins when you run inside the SEAS-8414 course repo), or
- the copy shipped as package data in the wheel (
seas8414_teach/_data).
- Public URL — only when there is no cache hit, the loader declares a URL,
requestsis installed, andSEAS8414_OFFLINEis not1. The download is SHA-checked and cached.
Because the assets ship in the wheel, tier 2 always resolves — so every loader runs offline on a
bare pip install. Each load returns (or attaches) a Provenance record — .source tells you
which tier answered (user-cache / package-data / release-cache / network), plus .sha256,
.bytes, .url.
What runs where — verified
Verified from a bare pip install seas8414-teach in a fresh venv, outside the repo, with an
empty cache and SEAS8414_OFFLINE=1 (no network at all):
| Loader | Bare pip, offline | Source |
|---|---|---|
Core layer (decide, grounding, methods, figures, receipt) |
✅ | no data at all — pass your own arrays/frames |
st.load_kev, st.load_cowrie |
✅ | package-data |
projects.sbom.load_sbom |
✅ | package-data (incl. the OSV batch that has no public URL) |
projects.posture.load_scorecard |
✅ | package-data (the pinned snapshot, not the live API) |
projects.intrusion.load_nsl_kdd |
✅ | package-data |
projects.attack.load_attack_ics |
✅ | package-data |
Environment knobs:
SEAS8414_CACHE_BASE=/path— where verified bytes are cached (default~/.cache/seas8414-phase-projects; layout<base>/<project>/<file>).SEAS8414_OFFLINE=1— never touch the network; require a cache/bundle hit (and get a clear error otherwise). With the bundled data this is the natural default for classroom use.- Running inside the course repo still takes precedence: a CWD-reachable lock is used before the bundled copy, so course-repo edits to the release cache are honored.
prov = st.load_kev().attrs["provenance"] # loaders attach/return a Provenance
prov.source # 'package-data' on a bare install; 'release-cache'/'user-cache'/'network' otherwise
prov.sha256 # the verified digest
prov.as_dict() # JSON-safe: file, sha256, bytes, source, url, acquired_at_utc
API reference
Import the package as st; submodules are attributes (st.decide, st.figures) and the common
symbols are re-exported at the top level (st.decide_by_argmax, st.ground, …).
Setup
| Symbol | Signature | Returns / effect |
|---|---|---|
st.setup() |
setup() -> None |
Seed everything to 8414 and apply the house matplotlib style. Call once per notebook. |
st.seed(value=8414) |
seed(value: int = 8414) -> None |
Seed random / numpy only (no style change). |
st.SEED |
int |
The course seed, 8414. |
st.__version__ |
str |
Installed version. |
st.provenance
| Symbol | Signature | Returns |
|---|---|---|
fetch |
fetch(project, filename, *, expected_sha256, max_bytes=20_000_000, url=None) -> (bytes, Provenance) |
Verified bytes via the three-tier discovery above. Raises on SHA/size mismatch, or RuntimeError when offline/URL-less with no cache. |
load_kev |
load_kev() -> DataFrame |
CISA Known-Exploited-Vulnerabilities catalog as a tidy frame; .attrs["provenance"] attached. |
load_cowrie |
load_cowrie() -> CowrieDay |
One fixed Cowrie honeypot day → privacy-preserving session features. |
Provenance |
dataclass | Fields: .filename, .sha256, .bytes, .source, .url, .acquired_at; method .as_dict(). |
CowrieDay |
dataclass | Fields: .sessions (per-session feature frame), .sequences (event-state lists), .command_tokens, .provenance. |
st.methods
Thin scikit-learn wrappers that keep the tunable knobs explicit in the cell.
| Symbol | Signature | Returns |
|---|---|---|
markov_surprise |
markov_surprise(sequences, *, states=(...8 event states...), laplace=1.0) |
Series of transition-surprise per sequence (Laplace-smoothed; single-state → 0.0, no blow-ups). |
iforest_rarity |
iforest_rarity(frame, features, *, n_estimators=240, contamination="auto", max_features=1.0, seed=8414, log1p=True, scale=True) |
Series rarity score aligned to frame.index; deterministic for a fixed seed. |
kmeans_select |
kmeans_select(frame, features, *, k_range=range(2,7), seed=8414, sample=4000, log1p=True, scale=True) |
(k, labels, silhouettes_by_k) — the silhouette-selected k, per-row labels, and the score map. |
nmf_topics |
nmf_topics(text, ...) |
NMF topic model over a text column (also see the richer projects.kev.nmf_topics). |
st.decide
The honest-decision layer.
| Symbol | Signature | Returns |
|---|---|---|
decide_by_argmax |
decide_by_argmax(frame, *, proxy, queues, utility_budgets=(.02,.05,.1,.2), curve_budgets=(.01,.02,.05,.1,.15,.2)) -> Decision |
Ranks each queue by held-out capture at fixed review budgets; the winner is the argmax of the reported utilities. queues maps a display name → the score column. |
proxy_independence |
proxy_independence(frame, *, proxy, score) -> (bool, float) |
(is_independent, correlation). Groups by exact score level; a proxy that is a pure function of the score is flagged not independent. Necessary-not-sufficient guard against a tautological evaluation. |
paired_bootstrap_gate |
paired_bootstrap_gate(advanced, baseline, labels, *, n=500, seed=8414, metric=None) -> dict |
Paired-bootstrap uplift of advanced over baseline: {median, p10, p90, adopt} — adopt is True only if the p10 lower bound clears zero. |
Decision (returned by decide_by_argmax) — fields .proxy, .winner, .utilities,
.utility_budgets, .recall_by_budget, .winning_score_column, .receipt; methods
.selected_scores(), .why() (computed honest sentence), .budget_curve() (styled figure).
st.grounding
| Symbol | Signature | Returns |
|---|---|---|
ground |
ground(prompt, records, fallback_selections) -> Grounded |
Runs the fail-closed selection contract: the model may only pick {evidence_id, field, action} triples from records; anything else is rejected and the validated deterministic fallback_selections render instead. |
Grounded — fields .accepted (rendered text), .mode, .controls, .candidate,
.candidate_passed, .model_authored (always False), .contract_version, .prompt_sha256;
methods .audit_accuracy(), .controls_passed(), .show_controls(), .receipt().
grounding.CONTRACT_VERSION is pinned ("selection-v2").
st.figures
House-style matplotlib helpers; each returns a (fig, ax) you can further annotate.
| Symbol | Signature |
|---|---|
use_house_style |
use_house_style() -> None |
pipeline |
pipeline(stages, subtitle) — a left-to-right stage diagram |
event_funnel |
event_funnel(counts, *, title, xlabel="events (log scale)", ylabel="normalized event state", highlight=()) |
budget_curve |
budget_curve(recall_by_budget, *, proxy, title="Held-out capture versus review budget") |
scatter |
scatter(x, y, *, color_by=None, highlight_mask=None, highlight_label="", title="", xlabel="", ylabel="", cmap="viridis") |
st.receipt
| Symbol | Signature | Returns |
|---|---|---|
receipt |
receipt(project, *, metrics, provenance=(), seed=8414, figure_count=None, status="COMPLETE", offline=None, extra=None) -> dict |
A JSON-safe completion receipt for the end of a project (metrics + provenance digests + seed + status). |
st.projects — per-course-project loaders + methods
Each module ports one full-edition notebook's analytics faithfully, so the teach edition reaches
the identical load-bearing decision. Import as from seas8414_teach.projects import kev, sbom, posture, intrusion, attack. Loaders here follow the data rules above.
projects.kev — Phase 9, Project 1
Exploited-vulnerability triage (CISA KEV catalog).
| Symbol | Signature | Returns |
|---|---|---|
nmf_topics |
nmf_topics(text, *, k=6, seed=8414, top_terms=8, sensitivity_components=(5,6,7)) -> TopicModel |
NMF topic model of KEV descriptions, with a seed-stability check. |
catalog_version |
catalog_version() -> str |
The pinned KEV catalog version string. |
TopicModel |
dataclass | .weights, .labels, .terms, .seed_ari (seed-stability ARI), .component_sensitivity. |
Pair with st.load_kev() for the catalog frame.
projects.sbom — Phase 9, Project 2
SBOM dependency blast radius (CycloneDX BOM + OSV advisories). (Loader is cache-only — see data rules.)
| Symbol | Signature | Returns |
|---|---|---|
load_sbom |
load_sbom() -> SbomGraph |
Parses the pinned CycloneDX BOM + OSV batch into a dependency DAG and node feature table. |
graph_screen_cv |
graph_screen_cv(nodes, adjacency, y, *, seed=8414) -> (graph_probability, metrics) |
Leakage-free per-fold spectral graph screen vs. the transparent degree baseline. metrics has graph_cv_ap, graph_cv_roc_auc, degree_baseline_ap, degree_baseline_roc_auc. |
SbomGraph |
dataclass | .bom (raw CycloneDX doc), .graph (nx.DiGraph), .node_vulns (node → OSV ids), .nodes (per-node feature DataFrame indexed by bom-ref), .adjacency (CSR), .provenance. |
The .nodes frame columns include name, version, purl, direct_dependents,
direct_dependencies, transitive_dependents (blast radius via nx.ancestors),
transitive_dependencies, depth, pagerank, betweenness, vulnerability_count,
vulnerability_ids.
projects.posture — Phase 9, Project 3
Open-source repository posture archetypes (OpenSSF Scorecard). (Loader needs the course cache.)
| Symbol | Signature | Returns |
|---|---|---|
load_scorecard |
load_scorecard() -> Scorecard |
Per-repository Scorecard checks → posture frame + check matrix. |
fit_reference |
fit_reference(check_matrix, components, seed=8414) -> dict |
Fit the reference archetype model. |
predict_reference |
predict_reference(model, check_matrix) |
Project repositories onto the reference archetypes. |
stability_gates |
stability_gates(check_matrix, reference, base_labels, membership, *, selected_k, model_search_boundary, observation_span_days, seed=8414, bootstrap_repeats=80) |
Bootstrap stability gates on the archetype clustering. |
Scorecard |
dataclass | .posture, .check_matrix, .missing_matrix, .check_names, .observation_span_days, .engine_commits, .negative_sentinel_count, .provenance. |
projects.intrusion — Phase 10, Project 1
Intrusion detection under dataset shift (NSL-KDD). (Loader needs the course cache.)
| Symbol | Signature | Returns |
|---|---|---|
load_nsl_kdd |
load_nsl_kdd() -> NslKdd |
NSL-KDD train/test with the known train↔test distribution shift preserved. |
attack_family |
attack_family(name: str) -> str |
Map a raw attack label to its family (dos/probe/r2l/u2r/normal). |
NslKdd |
dataclass | .train, .test, .feature_columns, .categorical, .provenance. |
projects.attack — Phase 10, Project 3
ATT&CK-informed ICS deception coverage. Runs standalone (network + requests).
| Symbol | Signature | Returns |
|---|---|---|
load_attack_ics |
load_attack_ics() -> AttackICS |
Parse the pinned ATT&CK-for-ICS STIX bundle into technique→asset targeting facts. |
reconstruction_eval |
reconstruction_eval(attack, *, seed=8414, splits=30) -> dict |
Link-reconstruction evaluation vs. a degree-matched-negative baseline. |
degree_matched_negatives |
degree_matched_negatives(positives, absent_pairs, matrix, match_rng) |
Sample degree-matched negative technique→asset pairs. |
external_id |
external_id(item: dict) -> str |
The mitre-attack external id (e.g. T0800) for a STIX object. |
AttackICS |
dataclass | .techniques, .assets, .technique_ids, .asset_ids, .technique_index, .asset_index, .incidence (techniques×assets matrix), .technique_weights, .target_edges, .provenance. |
Worked examples by project
Each example is a complete copy-paste block that runs on a bare pip install seas8414-teach —
offline, no repo, no cache warming — because the pinned data ships in the wheel. Outputs shown are
the real values from that pinned data.
ATT&CK-informed ICS deception coverage
from seas8414_teach.projects import attack
ics = attack.load_attack_ics()
print(len(ics.technique_ids), "techniques ×", len(ics.asset_ids), "assets") # 97 techniques × 18 assets
print("incidence matrix:", ics.incidence.shape) # (97, 18)
# Which observed targeting links can a model reconstruct, vs. a degree-matched baseline?
result = attack.reconstruction_eval(ics, seed=8414)
print(result) # dict of reconstruction AP/AUC vs. baseline
SBOM dependency blast radius
This is the snippet from the API tour, worked through end to end:
from seas8414_teach.projects import sbom
snapshot = sbom.load_sbom() # SbomGraph — inspect .provenance to see which cache answered
graph = snapshot.graph # nx.DiGraph of bom-ref nodes with dependsOn edges
nodes = snapshot.nodes # per-node feature DataFrame, indexed by bom-ref
print(graph.number_of_nodes(), "nodes /", graph.number_of_edges(), "edges") # 168 nodes / 170 edges
# Highest blast radius = most transitive dependents (nx.ancestors)
top = nodes["transitive_dependents"].idxmax()
print(nodes.loc[top, "name"], "->", int(nodes.loc[top, "transitive_dependents"]), "dependents")
# metrics-core -> 13 dependents
# Does a leakage-free graph screen beat the transparent degree baseline?
y = (nodes["vulnerability_count"] > 0).astype(int)
graph_probability, metrics = sbom.graph_screen_cv(nodes, snapshot.adjacency, y)
print(metrics)
# {'graph_cv_ap': 0.219, 'graph_cv_roc_auc': 0.594,
# 'degree_baseline_ap': 0.191, 'degree_baseline_roc_auc': 0.571}
Exploited-vulnerability triage (KEV topics)
import seas8414_teach as st
from seas8414_teach.projects import kev
catalog = st.load_kev() # 1637 vulnerabilities (pinned catalog)
topics = kev.nmf_topics(catalog["shortDescription"].fillna(""))
print("topics:", sorted(set(topics.labels)))
print("seed stability (ARI):", round(topics.seed_ari, 3)) # 1.0 — labels are seed-stable
Repository posture archetypes
from seas8414_teach.projects import posture
sc = posture.load_scorecard()
print(len(sc.posture), "repositories ×", len(sc.check_names), "Scorecard checks") # 36 × 19
reference = posture.fit_reference(sc.check_matrix, components=3)
Intrusion detection under shift (NSL-KDD)
from seas8414_teach.projects import intrusion
nsl = intrusion.load_nsl_kdd()
print("train", nsl.train.shape, "test", nsl.test.shape, "|", len(nsl.feature_columns), "features")
# train (125973, 45) test (22544, 45) | 41 features
# Coarsen raw attack names into families (dos / probe / r2l / u2r / normal)
family = nsl.test["attack_name"].map(intrusion.attack_family)
print(family.value_counts().to_dict())
# {'normal': 9711, 'dos': 7458, 'r2l': 2885, 'probe': 2421, 'u2r': 67, 'other-attack': 2}
Faithfulness
The teach-edition notebooks that use this toolkit reproduce the gate-verified transparent notebooks exactly — a consistency check asserts every load-bearing decision agrees. The short form is a faithful re-expression of the full analysis, not a separate simplified one.
Safety & scope
These are classroom analyses, not production detectors. The library performs no active scanning, deployment, command execution, network contact (beyond the declared public data downloads), or attribution. The language model never creates labels, edges, flags, metrics, or decisions.
Development
git clone git@github.com:gwuml/seas8414_teach.git && cd seas8414_teach
pip install -e ".[dev]"
pytest -q # self-contained unit suite (no network / no data cache required)
License
MIT © George Washington University — SEAS-8414. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file seas8414_teach-0.1.4.tar.gz.
File metadata
- Download URL: seas8414_teach-0.1.4.tar.gz
- Upload date:
- Size: 5.5 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d66257fd58896b7c38904d67834a9714684eae3ef19efc120efa2e70a226db6
|
|
| MD5 |
a3cf40f9f12352cd735f1876ea46f3e1
|
|
| BLAKE2b-256 |
f63893629b1142e1883c169395f370683ef638249e9c06f25ad25dcf971a116c
|
File details
Details for the file seas8414_teach-0.1.4-py3-none-any.whl.
File metadata
- Download URL: seas8414_teach-0.1.4-py3-none-any.whl
- Upload date:
- Size: 5.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d64a5a3def91112fe8493bd5d550a5a3dee5c1973689013b583c9a733b1d8776
|
|
| MD5 |
212caf23fd0d3bb8a89d3a4071bb48c8
|
|
| BLAKE2b-256 |
5f2f5cb8684c828c09e45d5e0c69b24cbd7d3574c491fbb09bab26fa073c247f
|