Trigger
The MITRE ATT&CK mapping spec (`docs/superpowers/specs/2026-04-13-mitre-attack-mapping-design.md`) explicitly defers JSON schema additions for `mitre_tactic` and `mitre_name`. Today the JSON output emits only `mitre_technique` (the ID); downstream consumers must perform their own ID→tactic/name lookup.
Why deferred (already validated)
Brainstorming Question 2 (Perplexity-validated): "tactic derived at render time" was chosen over "tactic stored on `Finding`" because:
- STIX 2.1 uses normalized relationship objects (derived).
- Suricata EVE denormalizes (stores `mitre_tactic_id` alongside `mitre_technique_id`) for SIEM grep-friendliness — the alternative pattern.
- Perplexity recommended the DTO-with-`From<&Original>` pattern as "most maintainable" for adding derived fields without polluting the source struct.
Today there is no SIEM-ingestion consumer of wirerust's JSON output. Adding the derived fields preemptively is YAGNI.
Trigger condition for this issue
When the first SIEM-ingestion or downstream-tooling consumer of `wirerust` JSON output asks for `mitre_tactic` and/or `mitre_name` fields.
Suggested approach if undertaken
Per the brainstorming validation:
```rust
// New file: src/reporter/json_dto.rs (or inline in src/reporter/json.rs)
#[derive(Serialize)]
pub(crate) struct FindingJsonDto<'a> {
#[serde(flatten)]
inner: &'a Finding,
#[serde(skip_serializing_if = "Option::is_none")]
mitre_tactic: Option,
#[serde(skip_serializing_if = "Option::is_none")]
mitre_name: Option<&'static str>,
}
impl<'a> From<&'a Finding> for FindingJsonDto<'a> {
fn from(f: &'a Finding) -> Self {
let id = f.mitre_technique.as_deref();
Self {
inner: f,
mitre_tactic: id
.and_then(crate::mitre::technique_tactic)
.map(|t| t.to_string()),
mitre_name: id.and_then(crate::mitre::technique_name),
}
}
}
```
JSON reporter renders `Vec` instead of `Vec<&Finding>`. `Finding` itself is untouched.
Acceptance criteria
- New fields appear in JSON output only when `mitre_technique` is set AND it resolves in the lookup.
- Existing JSON consumers (none today) are unbroken — `mitre_technique` retains its current emission.
- Snapshot or substring tests covering the JSON output for at least one finding with each field combination.
- Document the addition in any consumer-facing JSON schema notes.
Trigger
The MITRE ATT&CK mapping spec (`docs/superpowers/specs/2026-04-13-mitre-attack-mapping-design.md`) explicitly defers JSON schema additions for `mitre_tactic` and `mitre_name`. Today the JSON output emits only `mitre_technique` (the ID); downstream consumers must perform their own ID→tactic/name lookup.
Why deferred (already validated)
Brainstorming Question 2 (Perplexity-validated): "tactic derived at render time" was chosen over "tactic stored on `Finding`" because:
Today there is no SIEM-ingestion consumer of wirerust's JSON output. Adding the derived fields preemptively is YAGNI.
Trigger condition for this issue
When the first SIEM-ingestion or downstream-tooling consumer of `wirerust` JSON output asks for `mitre_tactic` and/or `mitre_name` fields.
Suggested approach if undertaken
Per the brainstorming validation:
```rust
// New file: src/reporter/json_dto.rs (or inline in src/reporter/json.rs)
#[derive(Serialize)]
pub(crate) struct FindingJsonDto<'a> {
#[serde(flatten)]
inner: &'a Finding,
#[serde(skip_serializing_if = "Option::is_none")]
mitre_tactic: Option,
#[serde(skip_serializing_if = "Option::is_none")]
mitre_name: Option<&'static str>,
}
impl<'a> From<&'a Finding> for FindingJsonDto<'a> {
fn from(f: &'a Finding) -> Self {
let id = f.mitre_technique.as_deref();
Self {
inner: f,
mitre_tactic: id
.and_then(crate::mitre::technique_tactic)
.map(|t| t.to_string()),
mitre_name: id.and_then(crate::mitre::technique_name),
}
}
}
```
JSON reporter renders `Vec` instead of `Vec<&Finding>`. `Finding` itself is untouched.
Acceptance criteria