From 45c5c2d0fc87cf6897eabaed032231fb589185e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 13:09:18 +0900 Subject: [PATCH 1/7] test(misp): prove threat-level severity inflation --- tests/misp_threat_level_severity.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/misp_threat_level_severity.rs diff --git a/tests/misp_threat_level_severity.rs b/tests/misp_threat_level_severity.rs new file mode 100644 index 0000000..070b8f9 --- /dev/null +++ b/tests/misp_threat_level_severity.rs @@ -0,0 +1,29 @@ +#[path = "../src/misp_import.rs"] +mod misp_import; + +use waf_ids_core::Severity; + +fn admitted_event(threat_level_id: &str, value: &str) -> String { + format!( + r#"{{"Event":{{"id":"severity-contract","threat_level_id":{threat_level_id},"Attribute":[{{"type":"domain","value":"{value}","to_ids":true,"deleted":false}}]}}}}"# + ) +} + +#[test] +fn misp_defined_threat_levels_preserve_source_severity() { + // MISP threat_level_id is 1=High, 2=Medium, 3=Low. The adapter must not + // promote source severity while translating an admitted external fact. + let cases = [ + (r#""1""#, "high.example", Severity::High), + ("2", "medium.example", Severity::Medium), + (r#""3""#, "low.example", Severity::Low), + ]; + + for (threat_level_id, value, expected) in cases { + let raw = admitted_event(threat_level_id, value); + let material = misp_import::parse_misp_document(&raw, "misp:test", 60).unwrap(); + assert_eq!(material.threats.len(), 1); + assert_eq!(material.threats[0].value, value); + assert_eq!(material.threats[0].severity, expected); + } +} From 1502edf1cff801b1e4d31dfab1d4a0aad89ef489 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 13:10:11 +0900 Subject: [PATCH 2/7] fix(misp): preserve source threat-level severity --- src/misp_import.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/misp_import.rs b/src/misp_import.rs index 9226386..53a97d2 100644 --- a/src/misp_import.rs +++ b/src/misp_import.rs @@ -173,7 +173,9 @@ fn looks_like_attribute(obj: &serde_json::Value) -> bool { } fn severity_from_event(event: &serde_json::Value) -> Severity { - // MISP threat_level_id: 1=High, 2=Medium, 3=Low, 4=Undefined + // MISP threat_level_id is an external semantic code, not a zero-based Wardnet ordinal: + // 1=High, 2=Medium, 3=Low, 4=Undefined. Missing/unrecognized values retain the existing + // compatibility fallback to level 2 (Medium) rather than inventing stronger source truth. match event .get("threat_level_id") .and_then(|v| { @@ -183,9 +185,9 @@ fn severity_from_event(event: &serde_json::Value) -> Severity { }) .unwrap_or(2) { - 1 => Severity::Critical, - 2 => Severity::High, - 3 => Severity::Medium, + 1 => Severity::High, + 2 => Severity::Medium, + 3 => Severity::Low, _ => Severity::Low, } } @@ -525,4 +527,4 @@ mod tests { ) .is_err()); } -} \ No newline at end of file +} From 20394e75fc175472db3c7deaac01572750894a79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 13:10:44 +0900 Subject: [PATCH 3/7] docs(misp): trace severity translation contract --- docs/doctoring/misp-threat-level-severity.md | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/doctoring/misp-threat-level-severity.md diff --git a/docs/doctoring/misp-threat-level-severity.md b/docs/doctoring/misp-threat-level-severity.md new file mode 100644 index 0000000..fa5e353 --- /dev/null +++ b/docs/doctoring/misp-threat-level-severity.md @@ -0,0 +1,29 @@ +# MISP threat-level severity translation + +## Decision boundary + +Wardnet imports MISP events through an anti-corruption layer before selected attributes become `ThreatIndicator` or `DnsblEntry` enforcement material. `Event.threat_level_id` is therefore an external semantic code that must be translated without strengthening the producer's assertion. + +The live MISP source defines the enum as `1 = High`, `2 = Medium`, `3 = Low`, `4 = Undefined`. MISP's own CLI usage and canonical dashboard adapter encode that ordering explicitly. Wardnet previously documented the same enum but treated the values as though they were an internal ordinal: `1 -> Critical`, `2 -> High`, `3 -> Medium`. That deterministic one-tier inflation was rejected because a translation boundary must not manufacture stronger threat evidence than the source supplied. + +Wardnet now maps defined MISP values exactly: `1 -> High`, `2 -> Medium`, and `3 -> Low`. MISP `4 = Undefined` remains conservatively represented as Wardnet `Low` because the current Wardnet `Severity` enum has no `Undefined` member; this is a compatibility representation, not an assertion that MISP classified the event as Low. Missing or structurally unrecognized `threat_level_id` retains the pre-existing MISP-level-2 compatibility fallback and therefore maps to Wardnet `Medium`. A future domain-model change may introduce an explicit unknown/undefined severity, but that requires a separate aggregate/API compatibility decision rather than silently overloading this adapter repair. + +The change does not alter the independent MISP admission invariants in [`misp-to-ids-admission.md`](misp-to-ids-admission.md): `to_ids` must be affirmative and recognized, and withdrawn or structurally invalid lifecycle state must not authorize enforcement. + +## Alternatives considered + +Keeping the shifted mapping was rejected because it changes the meaning of MISP authority data and can distort SOC prioritization, policy evaluation, audit evidence, and downstream provenance. Mapping MISP `4 = Undefined` to `Critical` or `Medium` was rejected because no such source assertion exists. Rejecting every event with undefined or absent threat level was also rejected in this bounded fix because the existing import contract already accepts those events and there is no explicit Wardnet `Undefined` severity today; changing admission compatibility belongs in a separate versioned decision. + +## Verification contract + +`tests/misp_threat_level_severity.rs` is the focused contract regression. It submits otherwise admissible MISP events using both string and numeric representations and requires `threat_level_id` 1, 2, and 3 to produce exactly `Severity::High`, `Severity::Medium`, and `Severity::Low`. The test was committed before the production mapping changed, so the predecessor implementation fails by returning Critical/High/Medium. Existing `to_ids` and deletion-state tests continue to exercise the independent fail-closed admission boundary. + +Merge evidence must be produced on the exact current stacked head after the parent MISP admission delta is fixed in ancestry. Predecessor checks, review comments, or a locally inferred source mapping do not transfer as release evidence. + +## Traceability and references + +MISP Project. (n.d.). *CLI usage: Event threat level*. GitHub. https://github.com/MISP/MISP/blob/9294667a5b40e59ea42314c2aafa99086ce1d8e6/app/Console/Command/CLI_usage.md + +MISP Project. (n.d.). *CanonicalTypeAdapter: MISP threat-level filter*. GitHub. https://github.com/MISP/MISP/blob/9294667a5b40e59ea42314c2aafa99086ce1d8e6/app/Lib/Dashboard/Tools/CanonicalTypeAdapter.php + +MISP Project. (2015, November 24). *Threat level coding misleading* (Issue #729). GitHub. https://github.com/MISP/MISP/issues/729 From 2768ecf71d8527fe02a568ce4575754e71378813 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 13:11:45 +0900 Subject: [PATCH 4/7] test(misp): lock undefined and missing severity policy --- tests/misp_threat_level_severity.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/misp_threat_level_severity.rs b/tests/misp_threat_level_severity.rs index 070b8f9..08034e9 100644 --- a/tests/misp_threat_level_severity.rs +++ b/tests/misp_threat_level_severity.rs @@ -27,3 +27,15 @@ fn misp_defined_threat_levels_preserve_source_severity() { assert_eq!(material.threats[0].severity, expected); } } + +#[test] +fn undefined_and_missing_misp_levels_do_not_invent_stronger_source_truth() { + let undefined = admitted_event("4", "undefined.example"); + let undefined_material = + misp_import::parse_misp_document(&undefined, "misp:test", 60).unwrap(); + assert_eq!(undefined_material.threats[0].severity, Severity::Low); + + let missing = r#"{"Event":{"id":"severity-contract","Attribute":[{"type":"domain","value":"missing.example","to_ids":true,"deleted":false}]}}"#; + let missing_material = misp_import::parse_misp_document(missing, "misp:test", 60).unwrap(); + assert_eq!(missing_material.threats[0].severity, Severity::Medium); +} From 44b6561ee5fbd78d785635adbcac8d2ab6f5f95a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 09:20:21 +0900 Subject: [PATCH 5/7] chore: stage MISP severity parent restack --- .github/workflows/wardnet-child-restack.yml | 66 +++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/wardnet-child-restack.yml diff --git a/.github/workflows/wardnet-child-restack.yml b/.github/workflows/wardnet-child-restack.yml new file mode 100644 index 0000000..edc30cd --- /dev/null +++ b/.github/workflows/wardnet-child-restack.yml @@ -0,0 +1,66 @@ +name: Wardnet MISP severity parent restack (temporary) + +on: + push: + branches: + - fix/misp-threat-level-severity + paths: + - .github/workflows/wardnet-child-restack.yml + +permissions: + contents: write + +concurrency: + group: wardnet-child-restack-${{ github.ref }} + cancel-in-progress: false + +jobs: + restack: + runs-on: macos-15 + steps: + - name: Checkout triggering child head + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + ref: ${{ github.sha }} + fetch-depth: 0 + - name: Adopt exact stable parent without rewriting history + shell: bash + env: + EXPECTED_PARENT: 0c83cd5956f512d79c6600e823fcfa6d6f32af4e + PARENT_BRANCH: fix/misp-to-ids-fail-closed + run: | + set -euo pipefail + git fetch origin "${PARENT_BRANCH}:refs/remotes/origin/${PARENT_BRANCH}" "${GITHUB_REF_NAME}:refs/remotes/origin/${GITHUB_REF_NAME}" + test "$(git rev-parse "origin/${PARENT_BRANCH}")" = "${EXPECTED_PARENT}" + test "$(git rev-parse "origin/${GITHUB_REF_NAME}")" = "${GITHUB_SHA}" + git merge --no-ff --no-commit "${EXPECTED_PARENT}" + git rm .github/workflows/wardnet-child-restack.yml + cargo fmt --all -- --check + git diff --check --cached + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: stable + - name: Verify complete inherited parent and severity child + shell: bash + run: | + set -euo pipefail + cargo test --locked --workspace + cargo clippy --locked --workspace --all-targets -- -D warnings + - name: Commit, prove three-file child delta, and non-force push + shell: bash + env: + EXPECTED_PARENT: 0c83cd5956f512d79c6600e823fcfa6d6f32af4e + PARENT_BRANCH: fix/misp-to-ids-fail-closed + run: | + set -euo pipefail + git fetch origin "${PARENT_BRANCH}:refs/remotes/origin/${PARENT_BRANCH}" "${GITHUB_REF_NAME}:refs/remotes/origin/${GITHUB_REF_NAME}" + test "$(git rev-parse "origin/${PARENT_BRANCH}")" = "${EXPECTED_PARENT}" + test "$(git rev-parse "origin/${GITHUB_REF_NAME}")" = "${GITHUB_SHA}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git commit -m "chore: adopt MISP admission parent 0c83cd5" + actual="$(git diff --name-only "${EXPECTED_PARENT}"...HEAD | sort)" + expected=$'docs/doctoring/misp-threat-level-severity.md\nsrc/misp_import.rs\ntests/misp_threat_level_severity.rs' + test "${actual}" = "${expected}" + git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}" From 0912d5bc9490f65daa0b1a22b2bd3df26b2b5f41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 09:23:03 +0900 Subject: [PATCH 6/7] chore: format MISP severity restack with pinned toolchain --- .github/workflows/wardnet-child-restack.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/wardnet-child-restack.yml b/.github/workflows/wardnet-child-restack.yml index edc30cd..fdbcee7 100644 --- a/.github/workflows/wardnet-child-restack.yml +++ b/.github/workflows/wardnet-child-restack.yml @@ -35,12 +35,21 @@ jobs: test "$(git rev-parse "origin/${GITHUB_REF_NAME}")" = "${GITHUB_SHA}" git merge --no-ff --no-commit "${EXPECTED_PARENT}" git rm .github/workflows/wardnet-child-restack.yml - cargo fmt --all -- --check - git diff --check --cached - name: Install Rust toolchain uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable with: toolchain: stable + - name: Format and verify bounded child delta + shell: bash + run: | + set -euo pipefail + cargo fmt --all + formatted="$(git diff --name-only | sort)" + expected_formatted=$'src/misp_import.rs\ntests/misp_threat_level_severity.rs' + test "${formatted}" = "${expected_formatted}" + git diff --check + git diff --check --cached + git add src/misp_import.rs tests/misp_threat_level_severity.rs - name: Verify complete inherited parent and severity child shell: bash run: | From 2c8d499a5772b0be64d7cc3fc42ed2825ba1499e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 6 Sep 2026 09:25:13 +0900 Subject: [PATCH 7/7] docs(misp): record severity parent adoption evidence --- docs/doctoring/misp-threat-level-severity.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/misp-threat-level-severity.md b/docs/doctoring/misp-threat-level-severity.md index fa5e353..6c19832 100644 --- a/docs/doctoring/misp-threat-level-severity.md +++ b/docs/doctoring/misp-threat-level-severity.md @@ -16,9 +16,11 @@ Keeping the shifted mapping was rejected because it changes the meaning of MISP ## Verification contract -`tests/misp_threat_level_severity.rs` is the focused contract regression. It submits otherwise admissible MISP events using both string and numeric representations and requires `threat_level_id` 1, 2, and 3 to produce exactly `Severity::High`, `Severity::Medium`, and `Severity::Low`. The test was committed before the production mapping changed, so the predecessor implementation fails by returning Critical/High/Medium. Existing `to_ids` and deletion-state tests continue to exercise the independent fail-closed admission boundary. +`tests/misp_threat_level_severity.rs` is the focused contract regression. It submits otherwise admissible MISP events using both string and numeric representations and requires `threat_level_id` 1, 2, and 3 to produce exactly `Severity::High`, `Severity::Medium`, and `Severity::Low`. RED `45c5c2d0fc87cf6897eabaed032231fb589185e8` preceded production GREEN `1502edf1cff801b1e4d31dfab1d4a0aad89ef489`; the inherited implementation returned Critical/High/Medium for the three defined source levels. Existing `to_ids`, deletion-state, shared DNSBL ownership, restart and persistence regressions remain inherited from the parent rather than copied into this adapter. -Merge evidence must be produced on the exact current stacked head after the parent MISP admission delta is fixed in ancestry. Predecessor checks, review comments, or a locally inferred source mapping do not transfer as release evidence. +The child has now non-force adopted the complete stable parent lineage. Parent exact `0c83cd5956f512d79c6600e823fcfa6d6f32af4e` already contains the shared DNSBL reconciliation source GREEN and protected-main adoption. Temporary child-restack run `34001140916` pinned that parent and the triggering child ref, merged the parent without rewriting history, formatted only the two expected severity-code/test files under the pinned Rust toolchain, ran full locked workspace tests and strict workspace Clippy successfully, removed the temporary workflow, and proved the final child-versus-parent delta is exactly three files: this decision record, `src/misp_import.rs`, and `tests/misp_threat_level_severity.rs`. The resulting two-parent merge is `e0a7d9034b8810fc4284beb57f990eb2c3ab7641`. + +That restack proves source/candidate compatibility, not protected merge readiness. Standard repository/security/review workflows must be acquired on the final human-authored exact head; queued, `action_required`, predecessor-head or temporary-workflow results cannot be promoted as exact-head gate evidence. ## Traceability and references