This document defines the intended extension contracts for MarlinSpike.
It exists to make three boundaries explicit:
- Rust engines
- Python plugins
- YAML rule packs
These are not interchangeable. Each surface has a different job, different ownership boundary, and different stability expectations.
Some of this is already implemented today. Some of it is the target contract for the repo family the project is moving toward.
| Surface | Status | Current example | Primary input | Primary output |
|---|---|---|---|---|
| Rust engine | Implemented today / expanding | marlinspike-dpi, marlinspike-malware |
pcap / pcapng / packet events / Bronze-adjacent observables |
Versioned machine-readable artifacts |
| Python plugin | Target contract | marlinspike-mitre, marlinspike-iec62443, marlinspike-pera |
Finished MarlinSpike report JSON | Versioned sidecar JSON artifact |
| YAML rule pack | Target contract | MITRE, IEC 62443, and PERA mapping packs | Loaded by a Python plugin | Declarative rules and local policy |
MarlinSpike is moving to a repo family with one suite repo and vendored component subrepos:
marlinspikeSuite repo and integration homemarlinspike-msengineCore engine repo, internal package namemsenginemarlinspike-workbenchWeb UI and collaboration surfacemarlinspike-pluginsPython plugin monorepomarlinspike-enginesRust engine workspace
The suite repo vendors component repos using git subtree, not git submodules. The contract rules in this document are meant to keep those component repos interoperable.
The component repos are intended to be authoritative. The suite repo pins and vendors compatible revisions for teams that want one clone.
All three surfaces should follow these rules:
- The portable MarlinSpike report artifact remains the primary handoff between packet analysis and downstream analyst review.
- Optional extensions must run headlessly. They must not require the Flask UI, a browser, or a database connection.
- All machine-readable outputs must be deterministic and versioned.
- Packet-facing code should not make site-policy decisions.
- Report-facing code should not parse raw packet captures directly.
- YAML must stay declarative. It is configuration and mapping data, not a general programming language.
Status: implemented today in the Stage 2 DPI path.
Rust engines own packet-facing and event-heavy work:
- reading
pcaporpcapng - protocol parsing
- transaction normalization
- extracted artifacts and structured observations
- parser-safe, high-throughput handling of raw traffic
Rust engines do not own:
- final topology scoring
- responder-facing finding text
- ATT&CK mapping
- site-specific policy
- HTML or UI behavior
The current marlinspike-dpi integration expects a CLI surface equivalent to:
marlinspike-dpi --input <pcap> --capture-id <id> --output <json> --prettyAt minimum, a Rust engine intended for MarlinSpike integration must:
- accept an input capture path
- accept a stable capture identifier
- write JSON output to a caller-specified path
- return a non-zero exit code on failure
- emit useful stderr or stdout on failure
marlinspike-malware scan --rules-dir <rules> --events <json> --output <json>The malware engine accepts a JSON array of ObservedEvent objects (extracted from Bronze conversations) and writes a JSON array of MalwareFinding objects. It returns exit code 0 on success, non-zero on failure.
The current Stage 2 adapter in _ms_engine.py expects a JSON envelope with these top-level concepts:
{
"version": "engine-version",
"input": {},
"output": {
"checkpoint": {},
"events": []
}
}The event stream may contain multiple families, but MarlinSpike currently consumes these Bronze-style families:
protocol_transactionasset_observationtopology_observation
The engine may also emit families such as:
parse_anomalyextracted_artifact
Those additional families are useful because they can feed downstream Python plugins such as malware matching without forcing DPI logic into the main app.
marlinspike-malware emits a different output shape — a flat JSON array of MalwareFinding objects rather than a Bronze event envelope. Each finding includes rule_id, rule_name, family, severity, confidence, summary, observable_field, observable_value, event_id, references, and tags. The orchestrating engine (_ms_engine.py) converts these into c2_indicators and risk_findings for the report.
- The JSON envelope must be versioned.
- Event family names must be stable within a contract version.
- Fields required by the MarlinSpike adapter must not silently disappear in a minor release.
- Breaking schema changes require a contract version bump and an adapter update.
- A failed engine run must return a non-zero exit code.
- A failed engine run must not pretend success with an incomplete or corrupt JSON file.
- If an engine is optional, MarlinSpike may fall back to a Python path when configured to do so.
Status: target contract for report-facing extensions such as marlinspike-mitre, marlinspike-iec62443, and marlinspike-pera.
Current suite implementation:
marlinspike-mitreis now authored in the standalone sibling repo atmarlinspike-mitre.- This suite keeps a vendored runtime copy at
plugins/marlinspike_mitre/. - Its default vendored rule pack lives at
rules/mitre/base.yaml. - The workbench can auto-run it after a successful scan and load the resulting
-mitre.jsonsidecar underextensions.marlinspike-mitre.
Python plugins own report-facing logic:
- enrichment
- correlation
- responder-facing classification
- ATT&CK mapping
- IOC and malware matching
- standards crosswalks
- site-specific post-processing
Python plugins consume the finished MarlinSpike report artifact rather than raw packets. That keeps the primary app approachable for the wider OT/ICS community and makes field changes realistic during live remediation work.
A Python plugin should be runnable as a headless CLI. The preferred shape is:
python -m marlinspike_plugin_name \
--input-report <report.json> \
--output <artifact.json>Optional flags may include:
--rules <yaml>--site-rules <yaml>--merge-report <enriched-report.json>--strict
The plugin must not mutate the input report in place.
Current marlinspike-mitre CLI example:
python3 -m plugins.marlinspike_mitre \
--input-report report.json \
--output report-mitre.json \
--rules rules/mitre/base.yamlThe required input is a finished MarlinSpike report JSON. Plugins may read:
capture_infoconversationsprotocol_summaryport_summarynodesedgesrisk_findingsc2_indicatorsmac_table
The report artifact is the primary input contract. A plugin may also read sidecar artifacts, but that should be optional rather than required for baseline operation.
A Python plugin should emit a sidecar JSON artifact with a stable envelope:
{
"artifact_type": "plugin_output",
"plugin_id": "marlinspike-plugin-name",
"plugin_version": "0.1.0",
"contract_version": 1,
"generated_at": "2026-03-26T00:00:00Z",
"input_report": "report.json",
"summary": {},
"data": {},
"warnings": []
}Plugin-specific content belongs inside data.
If a merged report is requested, the preferred location is:
{
"extensions": {
"marlinspike-plugin-name": {
"artifact_type": "plugin_output"
}
}
}The sidecar artifact remains authoritative. A merged report is a convenience copy for downstream review.
Examples:
marlinspike-mitrewould emit ATT&CK classifications, confidence, and evidence references.marlinspike-iec62443would emit standards mappings, control families, and evidence references.marlinspike-perawould emit PERA model classifications and zone-context overlays.
Plugins should reference concrete evidence rather than producing ungrounded assertions.
Preferred evidence references:
- stable finding IDs
- stable indicator IDs
- node IPs or MACs
- edge identifiers
- artifact hashes
If the base report does not yet carry stable finding or indicator IDs, a plugin may emit deterministic derived IDs temporarily, but stable report-native IDs are preferred for long-term compatibility.
Plugins may optionally contribute analyst-facing views to the workbench, but only through a declarative schema.
This is the key boundary:
- the workbench owns the shell, navigation, layout, styling, and rendering behavior
- a plugin may contribute structured view data for one or more workbench locations
- a plugin must not inject arbitrary HTML, CSS, or JavaScript into the viewer
That keeps the operator experience coherent while still letting repo-family modules expose richer context.
Preferred merged-report shape:
{
"extensions": {
"marlinspike-plugin-name": {
"artifact_type": "plugin_output",
"summary": {},
"data": {},
"workbench_views": []
}
}
}Each workbench_views entry should follow this shape:
{
"view_id": "technique-coverage",
"title": "Technique Coverage",
"nav_label": "Coverage",
"location": "intel",
"badge": "6",
"summary": "Observed and inferred ATT&CK classifications for this report.",
"order": 20,
"blocks": []
}Supported location values:
dashboardmapfindingsevidenceassetsintelriskreports
Supported block types:
metric_stripkey_valuechip_listtablerecordsmarkdown
Block shapes:
{
"type": "metric_strip",
"title": "Coverage Summary",
"items": [
{ "label": "Observed", "value": "4", "tone": "positive" },
{ "label": "Inferred", "value": "2", "tone": "warn" }
]
}{
"type": "key_value",
"title": "Pack Metadata",
"items": [
{ "label": "Pack", "value": "ATT&CK OT Base" },
{ "label": "Version", "value": "2026.03" }
]
}{
"type": "chip_list",
"title": "Mapped Categories",
"items": ["C2_DNS_EXFIL", "CROSS_PURDUE"]
}{
"type": "table",
"title": "Top Techniques",
"columns": ["Technique", "Basis", "Confidence"],
"rows": [
["T1048", "observed", "0.95"],
["T1132", "observed", "0.82"]
]
}{
"type": "records",
"title": "Responder Notes",
"items": [
{
"title": "T1048 Exfiltration Over Alternative Protocol",
"subtitle": "Observed",
"body": "Mapped from DNS exfil and high-entropy queries.",
"chips": ["T1048", "DNS", "Exfiltration"]
}
]
}{
"type": "markdown",
"title": "Analyst Guidance",
"text": "Review the affected engineering workstation and validate expected DNS destinations."
}Compatibility rules:
view_id,title, and at least one valid block are required for a view to render- unknown
locationvalues fall back tointel - unknown block types must be ignored rather than breaking the viewer
- plugins should keep view payloads concise and evidence-oriented
- workbench views are optional enrichment, not the authoritative plugin artifact
Recommended usage:
- use
dashboardfor lightweight plugin KPIs or quick-read summaries - use
evidence,intel, orriskfor analyst-facing deep dives - use
reportsfor portable artifact supplements that should remain visible in exported review flows - avoid duplicating large sections of the base MarlinSpike report unless the plugin adds new interpretation or pivots
- A plugin must return a non-zero exit code for real execution failures.
- No-match results are not failures. They should produce a valid artifact with empty results.
- Optional plugins should not cause the core scan to fail unless strict mode is explicitly requested.
Python plugins should not:
- parse raw packet captures as their primary interface
- depend on Flask routes or Jinja templates
- require online lookups for baseline operation
- embed arbitrary rule logic inside YAML templates
Illustrative target shape:
{
"artifact_type": "plugin_output",
"plugin_id": "marlinspike-iec62443",
"plugin_version": "0.1.0",
"contract_version": 1,
"generated_at": "2026-03-26T00:00:00Z",
"input_report": "example-report.json",
"summary": {
"mapping_total": 2,
"sr_family_total": 2,
"high_priority_total": 1,
"unmapped_finding_total": 1
},
"data": {
"mappings": [
{
"control_family": "SR 3.1",
"title": "Communication Integrity",
"basis": "observed",
"confidence": 0.88,
"mapped_from": ["CROSS_PURDUE"],
"affected_nodes": ["10.10.20.14"],
"evidence_refs": ["finding:cross-purdue:1"],
"rationale": "Cross-level communication indicates conduit and communication-control review."
}
],
"coverage": {
"mapped_findings": ["CROSS_PURDUE"],
"unmapped_findings": ["NO_AUTH_OBSERVED"]
}
},
"warnings": []
}Expected meanings:
mappingscaptures standards-facing control context.coverageshows what the plugin did not map so analysts can see the edge of current support.summaryis lightweight and safe for dashboards or report viewers.
Illustrative target shape:
{
"artifact_type": "plugin_output",
"plugin_id": "marlinspike-mitre",
"plugin_version": "0.1.0",
"contract_version": 1,
"generated_at": "2026-03-26T00:00:00Z",
"input_report": "example-report.json",
"summary": {
"classification_total": 3,
"observed_total": 2,
"inferred_total": 1,
"unmapped_category_total": 1
},
"data": {
"classifications": [
{
"technique_id": "T1048",
"title": "Exfiltration Over Alternative Protocol",
"family": "Exfiltration",
"publication": "Published on report findings",
"basis": "observed",
"confidence": 0.95,
"mapped_from": ["C2_DNS_EXFIL"],
"affected_nodes": ["10.10.20.14", "8.8.8.8"],
"evidence_refs": ["finding:c2-dns-exfil:1"],
"rationale": "DNS exfil finding matched the ATT&CK exfiltration mapping."
},
{
"technique_id": "T1132",
"title": "Data Encoding",
"family": "Exfiltration",
"publication": "Published on report findings",
"basis": "observed",
"confidence": 0.82,
"mapped_from": ["C2_DNS_EXFIL", "C2_DNS_HIGH_ENTROPY"],
"affected_nodes": ["10.10.20.14"],
"evidence_refs": ["finding:c2-dns-exfil:1", "finding:c2-dns-high-entropy:1"],
"rationale": "High-entropy DNS labels suggested encoded payload transfer."
}
],
"coverage": {
"mapped_categories": ["C2_DNS_EXFIL", "C2_DNS_HIGH_ENTROPY"],
"unmapped_categories": ["NO_AUTH_OBSERVED"]
}
},
"warnings": []
}Expected meanings:
classificationscaptures analyst-facing ATT&CK context.basisdistinguishes observed mappings from weaker inferred ones.coverageshows what the plugin did not map so analysts can see the edge of current support.
Status: target contract for declarative content consumed by Python plugins.
YAML rule packs own declarative content:
- mapping tables
- IOC lists
- ATT&CK technique mappings
- confidence defaults
- enable or disable flags
- local suppressions
- site overrides
YAML rule packs do not own:
- packet parsing
- graph traversal
- arbitrary code execution
- network access
- UI rendering
The preferred top-level shape is:
schema_version: 1
pack_id: marlinspike-pack-id
pack_version: "2026.03"
plugin_id: marlinspike-plugin-name
description: Short description
rules: []Required top-level fields:
schema_versionpack_idpack_versionplugin_idrules
Optional fields:
descriptionreferencesauthordefault_enabled
The preferred rule shape is:
- id: example-rule
enabled: true
title: Example Rule
when:
finding_categories: ["C2_DNS_EXFIL"]
severity: ["HIGH", "CRITICAL"]
emit:
techniques:
- code: T1048
confidence: 0.95
references:
- https://example.invalid/referenceRule semantics should remain intentionally limited:
- equality and list membership checks
- explicit field matches
- bounded numeric thresholds
- enable or disable toggles
- metadata emission
Rule semantics should not include:
- embedded Python
- templated shell execution
- unbounded expression evaluation
- inline JavaScript
Rule packs should be applied in this order:
- base pack
- vendor or domain pack
- site override pack
Later packs may disable or override earlier rules by ID, but they should not silently redefine unrelated IDs.
- Invalid YAML must fail closed.
- Unknown top-level schema versions must fail closed.
- Unknown rule keys should be rejected in strict mode and warned in permissive mode.
- Plugins must validate packs before use.
Use these names consistently in docs and future implementation:
marlinspike-msengine: core engine repo, internal package and CLI namemsenginemarlinspike-workbench: web UI repomarlinspike-plugins: Python plugin monorepomarlinspike-engines: Rust engine workspacemarlinspike-dpi: Rust enginemarlinspike-malware: Rust engine / event matchermarlinspike-mitre: Python plugin backed by YAML rule packsmarlinspike-iec62443: Python plugin backed by YAML rule packsmarlinspike-pera: Python plugin backed by YAML rule packs
If new work is being proposed, place it by the input it consumes:
- raw packets or high-volume protocol events: Rust engine
- finished MarlinSpike report artifact: Python plugin
- analyst-tunable matching or mapping content: YAML rule pack