Skip to content

API Reference

The Python API mirrors the CLI. All classes are importable from heptagon directly.


CIRP model

heptagon.cirp.model.CIRPModel

The executable plan. Holds teams, stakeholders, scenarios, reporting edges, evidence, and decisions.

from heptagon.cirp.model import CIRPModel

cirp = CIRPModel(name="my_cirp", version="1.0")

Methods:

Method Purpose
add_team(team) Add one of the 5 R-C-C-P-T teams
add_stakeholder(s) Add an internal or external stakeholder
add_scenario(s) Add an applicable scenario
add_reporting_edge(reporter, escalate_to) Add a DAG edge
log_evidence(ev) Append a piece of evidence
log_decision(d) Append a decision
verify_all() -> dict Run all six compliance gates
run_tabletop(name, rounds=20, seed=42) -> dict Simulate a scenario

heptagon.cirp.model.Team

from heptagon.cirp.model import Team

Team(
    role="response",                    # one of: response, cyber_mgmt,
                                        #         crisis_mgmt,
                                        #         product_owner, third_party
    members=["IR lead", "SOC analyst"],
    contact="+1-555-0101",
    backup="+1-555-0102",
    mandate="Fix the breach, rebuild systems.",
)

heptagon.cirp.model.Evidence

from heptagon.cirp.model import Evidence

ev = Evidence.from_bytes(
    source="soc-siem",
    custodian="Priya Raman",
    data=b"alert-payload",
    description="SIEM alert: mass file rename",
)
# ev.evidence_id — UUID
# ev.timestamp   — UTC datetime
# ev.sha256      — hex digest of data

heptagon.cirp.model.Decision

from heptagon.cirp.model import Decision

d = Decision.new(
    actor="cyber_mgmt",
    rationale="isolate host-12",
    evidence_refs=[ev.evidence_id],
    effect_artifacts=[ev.evidence_id],   # F6
    predecessor="response",               # F7
    successors=["crisis_mgmt"],           # F7
    break_glass=False,
    signer=signer,                        # optional Ed25519 signer
)

If signer=None, the decision carries a legacy SHA-256 content hash which is non-empty but not cryptographically verifiable. New code should pass a Signer.


Ed25519 signing

heptagon.audit.signing.Signer

from heptagon.audit.signing import Signer

signer = Signer.generate()                    # fresh keypair
signer.save("key.json")                       # writes with 0600

loaded = Signer.load("key.json")              # read back
sig = loaded.sign(b"payload")                 # 64-byte signature

ok = Signer.verify(
    loaded.public_key_bytes,
    b"payload",
    sig,
)                                             # True

Design notes:

  • The private key is stored as raw bytes, hex-encoded in JSON.
  • Permissions are set to 0600 on save.
  • The keypair is not encrypted at rest. If this is a concern, wrap the file in your own passphrase-based encryption before storing it.

Incident log

heptagon.audit.log.IncidentLog

from heptagon.audit.log import IncidentLog, save_log, load_log

log = IncidentLog()
log.append_evidence(ev)
log.append_decision(d)

save_log(log, "incidents.jsonl")              # write
reloaded = load_log("incidents.jsonl")        # read

The file format is JSONL: one JSON object per line, tagged {"type": "evidence"|"decision", "data": {...}}. Order is preserved.


Symbolic layer

The symbolic core lives in heptagon.core.symbolic. These classes are used by the CIRP model but can also be used directly.

Class Purpose
FiberBundle7D The seven-coordinate record space
MoralSheaf Flabbiness condition, injective recording
Lineage DAG of reporting structure with cycle detection
PresentationOperator Who sees what during a review
TestimonyOperator Per-component attestation
MoralJacobians Responsibility (diagonal) vs influence (non-diagonal)
UnifiedEvaluation Faithfulness and rank-deficiency metrics
IncidentResponsePipeline Orchestrator for the above

Lineage example

from heptagon.core.symbolic import Lineage

lin = Lineage(
    labels=["soc", "ir_lead", "ciso", "ceo", "legal"],
    edges=[(0, 1), (1, 2), (2, 3), (2, 4)],
)

lin.is_ancestor(0, 4)     # True — soc reaches legal via the chain
lin.ancestors_of(4)       # [0, 1, 2]
lin.successors_of(2)      # [3, 4]
lin.roots()               # [0]

Cycles raise ValueError. Self-loops raise ValueError. Out-of-bounds indices raise ValueError.


Merkle seal

heptagon.audit.merkle

Introduced in v1.1.0. Detects deletion, reordering, and truncation of records within a JSONL log — attack classes that per-record Ed25519 signatures alone cannot catch.

from heptagon.audit.merkle import (
    compute_root,
    verify_root,
    seal_log,
    load_seal,
    MerkleSeal,
    ALGORITHM_NAME,   # "sha256-merkle"
)
Function Purpose
compute_root(lines) SHA-256 Merkle root over a list of record strings or bytes
verify_root(lines, expected_hex) Recompute and compare
seal_log(log_path, out_path=None, signer=None) Write a companion .root file
load_seal(path) Read a .root file back as a MerkleSeal

The seal detects:

Attack Detected
Delete a record from the log Yes
Reorder records Yes
Truncate the log Yes
Modify one character in a record Yes

It does not detect an attacker who deletes both the log and the seal file from the same machine. That is a custody problem, not a cryptographic one. See docs/threat-model.md for the full boundary.

MerkleSeal dataclass

seal = seal_log("incidents.jsonl", signer=signer)
# seal.merkle_root      — hex string (64 chars)
# seal.record_count     — number of records sealed
# seal.algorithm        — "sha256-merkle"
# seal.signature        — optional Ed25519 signature over the seal
# seal.signer_public_key

Signing the seal with a different key than the log is recommended. If the same key signs both, compromise of that key compromises both layers at once.


YAML plan loading

from heptagon.cirp.yaml_loader import (
    load_cirp_from_yaml,
    load_cirp_from_yaml_string,
    build_cirp_from_dict,
    validate_plan_dict,
)

cirp = load_cirp_from_yaml("plan.yaml")

The schema is loaded via importlib.resources from heptagon.schemas, so it is available regardless of install method (source tree, editable install, wheel, zipped egg).


Full example

from heptagon.audit.log import IncidentLog, save_log
from heptagon.audit.signing import Signer
from heptagon.cirp.model import CIRPModel, Decision, Evidence, Team

# Set up the plan
cirp = CIRPModel(name="demo", version="1.0")
for role in ("response", "cyber_mgmt", "crisis_mgmt",
             "product_owner", "third_party"):
    cirp.add_team(Team(role, ["x"], "+1", "+2", "do the thing"))
cirp.add_reporting_edge("response", "cyber_mgmt")
cirp.add_reporting_edge("cyber_mgmt", "crisis_mgmt")

# Log evidence and a signed decision
signer = Signer.generate()
log = IncidentLog()

ev = Evidence.from_bytes("soc", "analyst", b"alert", "initial")
log.append_evidence(ev)

d = Decision.new(
    actor="cyber_mgmt",
    rationale="isolate host-12",
    evidence_refs=[ev.evidence_id],
    effect_artifacts=[ev.evidence_id],
    predecessor="response",
    successors=["crisis_mgmt"],
    signer=signer,
)
log.append_decision(d)

save_log(log, "demo.jsonl")

# Seal the log with a Merkle root (v1.1.0)
from heptagon.audit.merkle import seal_log
seal = seal_log("demo.jsonl")
print(f"Root: {seal.merkle_root[:16]}...")
print(f"Records: {seal.record_count}")