Skip to content

fix(report): one canonical Severity, one severity vocabulary, issen-owned anomaly codes - #13

Draft
h4x0r wants to merge 2 commits into
mainfrom
fix/report-layer-consolidation
Draft

fix(report): one canonical Severity, one severity vocabulary, issen-owned anomaly codes#13
h4x0r wants to merge 2 commits into
mainfrom
fix/report-layer-consolidation

Conversation

@h4x0r

@h4x0r h4x0r commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Three report-layer DRY defects, fixed under strict TDD: RED commit 76a0109 (failing tests only), GREEN commit 499b055 (implementation).

Defect 1 — two Severity clones

Both were checked against the possibility of being a genuinely different scale (as srum-analysis's Clean/Informational/Suspicious/Critical legitimately is, normalizing at its boundary per ADR-0007). Neither is — both are the canonical five-tier scale, so both migrate to forensicnomicon::report::Severity rather than converting at a boundary.

Clone Verdict
issen-signatures::matching::results::Severity Pure duplicate. Informational is the canonical Info renamed; plus a private from_str_lossy and a private lowercase Display.
forensic-pivot::rule::Severity Pure duplicate. Identical variant set, declared highest-first, deriving only PartialEq — so pivot findings could not be ranked at all. Now gains Ord and Display.

The // RED: stub — types declared but no real logic header on rule.rs was stale; that crate has real logic and is wired into issen pivot.

#[non_exhaustive] forces _ arms in issen-navigator (tree marker, severity colour), its alert mapper, and issen-cli's pivot table. Each escalates rather than silently downgrading an unknown future tier.

Defect 2 — severity_rank / severity_token triplicated

Interim fix, issen-side only — as instructed, no competing forensicnomicon PR was opened. New issen_core::severity holds the one definition: SeverityExt::{rank, token}, parse, parse_lossy, TOKENS. It is deliberately a trait, so when forensicnomicon grows inherent Severity::rank()/token() those shadow it and this module becomes a pure deletion with zero call-site churn.

Five copies collapse onto it — the two named in the task, plus three more found while tracing: severity_from_finding_str, navigator_output::severity_of, and Severity::from_str_lossy.

Two drifts the copies had already shipped, now fixed:

  • render_html emitted class="severity-info" while the stylesheet only defined .severity-informational — the Info tier rendered unstyled.
  • Correlation::severity_from_str parsed case-sensitively while issen-report's copy was case-insensitive, and issen-disk writes a capitalised "High" into event metadata.

The bottom-tier token moves "informational""info". "informational" stays accepted on every read path (--min-severity, the scan_findings query filter, the report parser), so pre-existing case DBs and invocations keep working. issen_timeline::query_findings no longer hardcodes its own level ladder.

Defect 3 — NTFS- encroachment

The two timestomp codes do not describe the same detection. Both implementations were read:

ntfs-forensic NTFS-TIMESTOMP issen NTFS-TIMESTOMP-SI-FN-MISMATCH
Input parsed $STANDARD_INFORMATION + $FILE_NAME attributes TimelineEvent metadata
Ordering signal strict $SI.created < $FN.created same, plus $SI.modified, plus a clock-skew tolerance
Sub-second tell any whole-second $SI stamp, unconditional requires the contrast: $SI zeroed while $FN retains 100 ns precision
Benign modifiers none copy / volume-move / high-FP path, each downgrading
Grade always High InfoMedium, capped at Medium by design

So consolidating onto NTFS-TIMESTOMP would make an Info-tier lead indistinguishable from a High-tier filesystem anomaly — losing the distinction issen's precision-first redesign exists to make. They stay distinct codes.

The real defect is the namespace. ntfs-forensic owns NTFS- and already ships six codes there (NTFS-TIMESTOMP, NTFS-ADS, NTFS-DELETED-RECORD, NTFS-SLACK-RESIDUE, NTFS-MFTMIRR-MISMATCH, NTFS-LOGFILE-CLEARED). issen consumes those codes, which makes it exactly the crate that must not also mint into the namespace — and the old string was a strict prefix-extension of a shipped code, so prefix grouping conflated the two.

NTFS-TIMESTOMP-SI-FN-MISMATCH  ->  HEUR-TIMESTOMP-SI-FN
NTFS-BOOT-BACKUP-MISMATCH      ->  HEUR-BOOT-BACKUP-MISMATCH

HEUR- is issen's own detection-layer prefix — 18 existing codes spanning timestomping, location, entropy, size, magic and USN. Both are now named constants (TIMESTOMP_CODE, issen_disk::BOOT_BACKUP_CODE), asserted through the constant rather than a literal, so they cannot drift again.

These are consumer-visible contract changes. The Case-001 Szechuan answers doc is updated in the same commit.

Gate

Check Result
cargo build --workspace clean
cargo test (9 affected crates) 1887 passed, 0 failed, 8 ignored
cargo clippy --workspace --all-targets -- -D warnings clean
cargo fmt --check clean

One workspace test fails both on this branch and on origin/main: issen-mem dispatch::tests::dispatch_windows_netstat_returns_ok. Verified against main directly; unrelated to this change and untouched by it.

Follow-ups (cross-repo, not done here)

  1. forensicnomicon — add inherent Severity::rank() and Severity::token(). issen_core::severity::SeverityExt is shaped so those shadow it and the interim module deletes cleanly. Deferred because forensicnomicon PR #24 is open this session.
  2. ntfs-forensic — the $Boot-vs-backup integrity check belongs in its ArtifactAnomaly family beside the $MFTMirr one (ADR-0016 puts filesystem-structure checks in the FILESYSTEM layer). Once there, the crate legitimately owns an NTFS--prefixed code for it. Noted in BOOT_BACKUP_CODE's doc comment.
  3. issen, separate changeissen-signatures' HEUR-TS-002 ($SI/$FN divergence > 24 h) and HEUR-TS-003 ($SI sub-seconds zeroed while $FN retains precision) overlap the correlation-layer detector's S1/S3 signals. A third timestomp implementation, out of scope here.

🤖 Generated with Claude Code

h4x0r and others added 2 commits August 2, 2026 02:53
… squatting

Failing tests for three report-layer DRY defects. No implementation.

Defect 1 - two Severity clones duplicating forensicnomicon's:
  - crates/issen-signatures/tests/severity_is_canonical.rs
  - crates/forensic-pivot/tests/severity_is_canonical.rs
  - crates/forensic-pivot/tests/severity_gains_canonical_ordering.rs
  Both clones are the canonical five-tier scale, not a distinct native scale
  (contrast srum-analysis's Clean/Informational/Suspicious/Critical, which
  correctly normalizes at its boundary per ADR-0007), so both migrate onto
  forensicnomicon::report::Severity rather than converting at a boundary.

Defect 2 - severity rank/token triplicated and already drifted:
  - crates/issen-core/tests/severity_vocabulary.rs (the shared vocabulary)
  - crates/issen-correlation/tests/severity_vocabulary_is_shared.rs
  - crates/issen-report/tests/severity_vocabulary_is_shared.rs
  The drift shipped: render_html emits class="severity-info" while the
  stylesheet only defines .severity-informational, so the Info tier renders
  unstyled; and Correlation::severity_from_str parses case-sensitively while
  issen-report's copy is case-insensitive.

Defect 3 - issen mints codes inside ntfs-forensic's NTFS- namespace:
  - crates/issen-correlation/tests/anomaly_code_namespace.rs
  - crates/issen-disk/src/lib.rs (inline test)

RED output:

  forensic-pivot severity_is_canonical (runtime)
    thread 'severity_is_the_canonical_forensicnomicon_type' panicked at
    crates/forensic-pivot/tests/severity_is_canonical.rs:17:5:
    forensic-pivot must re-export forensicnomicon's Severity, not clone it
    (got `forensic_pivot::rule::Severity`)
    test result: FAILED. 1 passed; 1 failed

  forensic-pivot severity_gains_canonical_ordering (compile)
    error[E0369]: binary operation `>` cannot be applied to type `Severity`
    note: `Severity` does not implement `PartialOrd`

  issen-signatures severity_is_canonical (runtime)
    thread 'severity_is_the_canonical_forensicnomicon_type' panicked at
    crates/issen-signatures/tests/severity_is_canonical.rs:19:5:
    assertion `left == right` failed: issen-signatures must re-export
    forensicnomicon's Severity, not clone it
      left: "issen_signatures::matching::results::Severity"
     right: "forensicnomicon_core::report::Severity"
    test result: FAILED. 1 passed; 1 failed

  issen-core severity_vocabulary (compile)
    error[E0432]: unresolved import `issen_core::severity`
    error[E0599]: no method named `rank` found for enum `Severity`
    error[E0599]: no method named `token` found for enum `Severity`

  issen-correlation severity_vocabulary_is_shared (compile)
    error[E0432]: unresolved import `issen_core::severity`
    error[E0599]: no method named `token` found for enum `Severity`

  issen-report severity_vocabulary_is_shared (compile)
    error[E0432]: unresolved import `issen_core::severity`
      --> crates/issen-report/tests/severity_vocabulary_is_shared.rs:10:17
       | use issen_core::severity::{self, SeverityExt};
       |                 ^^^^^^^^ could not find `severity` in `issen_core`

  issen-correlation anomaly_code_namespace (runtime)
    thread 'timestomp_code_is_not_in_the_ntfs_forensic_namespace' panicked:
    `NTFS-` is ntfs-forensic's scheme; issen must mint under a prefix it owns
    (got `NTFS-TIMESTOMP-SI-FN-MISMATCH`)
    thread 'timestomp_code_does_not_shadow_ntfs_forensics_shipped_code' panicked:
    a prefix-extension of a shipped code makes one detection look like two under
    prefix grouping (got `NTFS-TIMESTOMP-SI-FN-MISMATCH`)
    thread 'timestomp_code_is_minted_under_an_issen_owned_prefix' panicked:
    expected an issen-owned prefix, got `NTFS-TIMESTOMP-SI-FN-MISMATCH`
    test result: FAILED. 0 passed; 3 failed

  issen-disk (runtime)
    thread 'tests::boot_backup_code_is_not_in_the_ntfs_forensic_namespace'
    panicked at crates/issen-disk/src/lib.rs:2128:9:
    `NTFS-` is ntfs-forensic's scheme; issen must mint under a prefix it owns
    (got `NTFS-BOOT-BACKUP-MISMATCH`)
    test result: FAILED. 0 passed; 1 failed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… issen-owned codes

Defect 1 - both Severity clones were pure duplicates, both migrated.

  issen-signatures `Informational/Low/Medium/High/Critical` and forensic-pivot
  `Critical/High/Medium/Low/Info` are the canonical five-tier scale under
  different spellings/orderings, not distinct native scales, so neither warrants
  an ADR-0007 boundary conversion. Both are now
  `pub use forensicnomicon::report::Severity`.

  - issen-signatures loses its private `from_str_lossy` and lowercase `Display`;
    the bottom tier is `Info`, and the persisted token comes from the shared
    vocabulary (`format!("{}", sev).to_lowercase()` in issen-cli::scanning still
    yields the same lowercase string, now "info" rather than "informational").
  - forensic-pivot gains `Ord` and `Display` it never had - pivot findings could
    not be ranked at all before. It takes a `forensicnomicon` dependency with the
    `serde` feature; the five variant names are unchanged so bundled rule YAML
    keeps parsing.
  - `#[non_exhaustive]` forces `_` arms in issen-navigator (tree marker, severity
    colour), its alert mapper, and issen-cli's pivot table. Each escalates rather
    than silently downgrading an unknown future tier.

Defect 2 - the rank/token/parse vocabulary now has one definition.

  New `issen_core::severity`: `SeverityExt::{rank, token}`, `parse`,
  `parse_lossy`, `TOKENS`. Deliberately a trait, so when forensicnomicon grows
  inherent `Severity::rank()`/`token()` those shadow it and this module is a pure
  deletion - no call site changes. That upstream move is a follow-up; a competing
  forensicnomicon PR is open this session, so nothing is changed there.

  Five copies collapse onto it: `severity_rank`/`severity_token` in issen-report
  (kept as one-line call-site aliases, not a second definition),
  `Correlation::severity_str`/`severity_from_str`,
  `severity_from_finding_str`, `navigator_output::severity_of`, and
  `Severity::from_str_lossy`.

  Two drifts the copies had already shipped are fixed:

  - `render_html` emitted `class="severity-info"` while the stylesheet only
    defined `.severity-informational`, so the Info tier rendered unstyled.
  - `Correlation::severity_from_str` was case-sensitive while issen-report's copy
    was case-insensitive, and issen-disk writes a capitalised "High" into event
    metadata. Both now parse case-insensitively.

  The bottom-tier token moves from "informational" to "info". `"informational"`
  stays accepted on every read path - `--min-severity`, the `scan_findings`
  query filter (which also still matches rows persisted with the old token), and
  the report parser - so pre-existing case DBs and invocations keep working.
  `issen_timeline::query_findings` no longer hardcodes its own level ladder.

Defect 3 - the two codes describe related but genuinely different detections,
so they stay distinct; issen's moves out of ntfs-forensic's namespace.

  ntfs-forensic owns `NTFS-` and already ships `NTFS-TIMESTOMP`, `NTFS-ADS`,
  `NTFS-DELETED-RECORD`, `NTFS-SLACK-RESIDUE`, `NTFS-MFTMIRR-MISMATCH` and
  `NTFS-LOGFILE-CLEARED`. issen consumes those codes, which makes it exactly the
  crate that must not also mint into the namespace.

  Reading both implementations, they are not one detection under two names:
  ntfs-forensic works from parsed `$STANDARD_INFORMATION`/`$FILE_NAME`
  attributes and fires on strict `$SI.created < $FN.created` OR any whole-second
  `$SI` stamp, always grading High; issen's works from TimelineEvent metadata,
  adds the `$SI.modified` ordering signal and a clock-skew tolerance, requires
  the CONTRAST form of the sub-second tell (`$SI` zeroed while `$FN` retains
  100 ns precision), and downgrades on benign-context modifiers - capping at
  Medium by design as a low-confidence lead. Collapsing them onto one code would
  make an Info lead indistinguishable from a High filesystem anomaly.

  The real defect is that the old string was a prefix-extension of a shipped
  code, so prefix grouping conflated them:

    NTFS-TIMESTOMP-SI-FN-MISMATCH -> HEUR-TIMESTOMP-SI-FN
    NTFS-BOOT-BACKUP-MISMATCH     -> HEUR-BOOT-BACKUP-MISMATCH

  `HEUR-` is issen's own detection-layer prefix (18 existing codes spanning
  timestomping, location, entropy, size, magic and USN). Both codes are now
  named constants (`TIMESTOMP_CODE`, `issen_disk::BOOT_BACKUP_CODE`) and asserted
  through the constant rather than a literal, so they cannot drift again.

  These are consumer-visible contract changes; the Case-001 Szechuan answers doc
  is updated in the same commit. The `$Boot`-vs-backup check itself belongs
  upstream in ntfs-forensic's `ArtifactAnomaly` family beside the `$MFTMirr` one
  (ADR-0016 puts filesystem-structure checks in the FILESYSTEM layer) - a
  cross-repo follow-up, noted in the constant's doc comment.

Gate: cargo build --workspace clean; cargo test for the nine affected crates
1887 passed / 0 failed; cargo clippy --workspace --all-targets -D warnings
clean; cargo fmt --check clean. One workspace test fails both here and on
origin/main - issen-mem `dispatch::tests::dispatch_windows_netstat_returns_ok`,
unrelated to this change and untouched by it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant