Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
588102b
test(security): reject malformed MISP to_ids evidence
seonghobae Sep 4, 2026
f2b648f
fix(security): fail closed on malformed MISP to_ids
seonghobae Sep 4, 2026
bfa6fd4
test(security): cover malformed MISP to_ids arrays
seonghobae Sep 4, 2026
8f9c854
test(security): require explicit MISP IDS admission
seonghobae Sep 4, 2026
966e8cc
fix(security): require affirmative MISP IDS evidence
seonghobae Sep 4, 2026
b1df88c
docs(security): trace MISP IDS admission policy
seonghobae Sep 4, 2026
03da951
docs(test): link MISP admission evidence
seonghobae Sep 4, 2026
bdd5f5b
test(security): reject deleted MISP attributes
seonghobae Sep 5, 2026
33a8d54
fix(security): reject deleted MISP attributes
seonghobae Sep 5, 2026
5b99ee9
docs(security): trace deleted MISP admission
seonghobae Sep 5, 2026
84c16a2
test(misp): reject deleted parent objects
seonghobae Sep 5, 2026
d67d4cc
fix(misp): honor parent object deletion state
seonghobae Sep 5, 2026
92898fa
docs(misp): trace parent object lifecycle admission
seonghobae Sep 5, 2026
a639e62
test(feed): prove withdrawn DNSBL ownership leaks
seonghobae Sep 5, 2026
d3e09bb
docs(feed): define DNSBL snapshot ownership invariant
seonghobae Sep 5, 2026
5071b51
fix(feed): persist DNSBL snapshot ownership
seonghobae Sep 5, 2026
a81f3a1
revert: restore minimal Wardnet feed core baseline
seonghobae Sep 5, 2026
0950ad4
test(feed): pin operator DNSBL precedence and write count
seonghobae Sep 5, 2026
ba5dd62
test(security): isolate DNSBL withdrawal reconciliation
seonghobae Sep 5, 2026
b6581d4
style: expose DNSBL ownership RED through rustfmt
seonghobae Sep 5, 2026
46615df
test(security): execute DNSBL ownership RED assertions
seonghobae Sep 5, 2026
6248be0
test(security): cover DNSBL ownership persistence
seonghobae Sep 5, 2026
6b3b0b0
fix(security): persist DNSBL ownership keys
seonghobae Sep 5, 2026
28d0ac1
test(security): keep DNSBL persistence RED compile-clean
seonghobae Sep 5, 2026
340f20b
test(security): align DNSBL ownership predecessor schema
seonghobae Sep 5, 2026
4e35e9f
chore: stage exact-head DNSBL ownership repair
seonghobae Sep 5, 2026
dac89ce
chore: make exact-head DNSBL repair workflow parseable
seonghobae Sep 5, 2026
218d3b9
chore: fail over DNSBL repair to macOS runner
seonghobae Sep 6, 2026
6727224
chore: fix macOS DNSBL repair payload extraction
seonghobae Sep 6, 2026
f8410ca
chore: expose DNSBL repair script syntax defect
seonghobae Sep 6, 2026
3d4f018
chore: repair DNSBL patch-script quote delimiters
seonghobae Sep 6, 2026
7042aa1
fix(security): reconcile DNSBL feed ownership
github-actions[bot] Sep 6, 2026
913a965
chore: stage protected-main restack
seonghobae Sep 6, 2026
d8b452c
chore: adopt protected main a52ccd0
github-actions[bot] Sep 6, 2026
0c83cd5
docs(security): record DNSBL ownership GREEN
seonghobae Sep 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions crates/waf-ids-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ pub struct AppData {
pub threats: Vec<ThreatIndicator>,
#[serde(default)]
pub operator_threat_keys: Vec<ThreatIndicatorKey>,
/// DNSBL identities independently managed through the operator API.
#[serde(default)]
pub operator_dnsbl_keys: Vec<DnsblEntryKey>,
pub dnsbl: Vec<DnsblEntry>,
pub events: Vec<SecurityEvent>,
pub next_event_id: u64,
Expand Down Expand Up @@ -47,6 +50,7 @@ impl AppData {
ttl_seconds: 86_400,
}],
operator_threat_keys: Vec::new(),
operator_dnsbl_keys: Vec::new(),
dnsbl: vec![DnsblEntry {
address: "203.0.113.10".parse().expect("seed IP address is valid"),
code: "127.0.0.2".to_string(),
Expand Down Expand Up @@ -138,6 +142,17 @@ pub struct DnsblEntry {
pub prefix_len: Option<u8>,
}

/// Stable DNSBL ownership identity. It deliberately matches [`upsert_dnsbl`],
/// whose physical row identity is the IP address rather than payload metadata.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(transparent)]
pub struct DnsblEntryKey(pub IpAddr);

/// Returns the durable ownership identity for a DNSBL entry.
pub fn dnsbl_entry_key(entry: &DnsblEntry) -> DnsblEntryKey {
DnsblEntryKey(entry.address)
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommercialProfile {
pub tenant_id: String,
Expand Down Expand Up @@ -189,6 +204,9 @@ pub struct ThreatFeedStatus {
pub struct ThreatFeedOwnership {
pub feed_id: String,
pub threat_keys: Vec<ThreatIndicatorKey>,
/// DNSBL rows currently claimed by this feed snapshot.
#[serde(default)]
pub dnsbl_keys: Vec<DnsblEntryKey>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
Expand Down Expand Up @@ -573,6 +591,27 @@ pub fn replace_threat_feed_ownership(
ownership.push(ThreatFeedOwnership {
feed_id,
threat_keys,
dnsbl_keys: Vec::new(),
});
Vec::new()
}
}

/// Replaces one feed's DNSBL snapshot ownership and returns its prior keys.
pub fn replace_threat_feed_dnsbl_ownership(
ownership: &mut Vec<ThreatFeedOwnership>,
feed_id: String,
dnsbl_keys: Vec<DnsblEntryKey>,
) -> Vec<DnsblEntryKey> {
if let Some(existing) = ownership.iter_mut().find(|item| item.feed_id == feed_id) {
let previous = existing.dnsbl_keys.clone();
existing.dnsbl_keys = dnsbl_keys;
previous
} else {
ownership.push(ThreatFeedOwnership {
feed_id,
threat_keys: Vec::new(),
dnsbl_keys,
});
Vec::new()
}
Expand Down Expand Up @@ -1776,6 +1815,55 @@ mod tests {
assert!(shannon_entropy(b"abcdefgh") > 2.9);
}

#[test]
fn replaces_threat_feed_dnsbl_ownership_and_returns_previous_keys() {
let first = DnsblEntryKey("203.0.113.1".parse().unwrap());
let second = DnsblEntryKey("203.0.113.2".parse().unwrap());
let mut ownership = Vec::new();

assert!(
replace_threat_feed_dnsbl_ownership(&mut ownership, "feed-a".to_string(), vec![first],)
.is_empty()
);
assert_eq!(ownership[0].dnsbl_keys, vec![first]);

let previous =
replace_threat_feed_dnsbl_ownership(&mut ownership, "feed-a".to_string(), vec![second]);
assert_eq!(previous, vec![first]);
assert_eq!(ownership[0].dnsbl_keys, vec![second]);

assert!(
replace_threat_feed_dnsbl_ownership(&mut ownership, "feed-b".to_string(), vec![first],)
.is_empty()
);
assert_eq!(ownership.len(), 2);
assert!(ownership[1].threat_keys.is_empty());
}

#[test]
fn dnsbl_ownership_fields_default_when_deserializing_predecessor_state() {
let key = DnsblEntryKey("203.0.113.3".parse().unwrap());
let mut data = AppData::seeded();
data.operator_dnsbl_keys.push(key);
data.threat_feed_ownership.push(ThreatFeedOwnership {
feed_id: "legacy-feed".to_string(),
threat_keys: Vec::new(),
dnsbl_keys: vec![key],
});

let mut legacy = serde_json::to_value(data).unwrap();
let object = legacy.as_object_mut().unwrap();
object.remove("operator_dnsbl_keys");
object["threat_feed_ownership"].as_array_mut().unwrap()[0]
.as_object_mut()
.unwrap()
.remove("dnsbl_keys");

let loaded: AppData = serde_json::from_value(legacy).unwrap();
assert!(loaded.operator_dnsbl_keys.is_empty());
assert!(loaded.threat_feed_ownership[0].dnsbl_keys.is_empty());
}

#[test]
fn records_audit_logs_with_monotonic_ids() {
let mut data = AppData::seeded();
Expand Down
39 changes: 39 additions & 0 deletions docs/doctoring/misp-to-ids-admission.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# MISP `to_ids` admission policy

## Decision boundary

Wardnet converts selected MISP attributes into gateway enforcement material (`ThreatIndicator` and, for IPs, `DnsblEntry`). The conversion is therefore an admission decision, not a neutral JSON import. `to_ids` is treated as affirmative evidence that an attribute is intended for detection, and an attribute is admitted only when Wardnet can positively recognize that signal as true and can establish that the attribute has not been withdrawn.

The MISP object schema defines `to_ids` as a boolean, and the MISP-STIX converter documents that `to_ids=True` is the detection-ready case that becomes an Indicator. MISP REST search can also return attributes regardless of their `to_ids` setting unless the caller filters them. Wardnet consequently cannot infer IDS authorization from mere presence in a MISP document. Missing, `false`, `null`, object, array, and otherwise unrecognized `to_ids` values fail closed and contribute to `skipped_attributes`; they do not produce enforcement rows. The previously supported explicit scalar compatibility spellings (`"1"`, case-insensitive `"true"`, numeric `1`) remain accepted so the repair does not broaden unrelated parser incompatibility.

MISP's ZeroMQ contract exposes attribute deletion independently of `to_ids`: examples use `deleted: false`/`"0"` for active material and set `deleted` to `1` when an attribute is deleted, with subscribers instructed to inspect the deletion marker. MISP also gives an enclosing Object its own deletion lifecycle; the current event-object rendering path explicitly reads `object['deleted']` and marks the whole object deleted. A positive nested attribute `to_ids` value therefore cannot resurrect either a withdrawn attribute or a withdrawn parent Object. Wardnet treats recognized `deleted=true`/`1` states at either scope as withdrawn; recognized `false`/`0` states and an omitted `deleted` member remain active for compatibility with ordinary exports. A present but structurally unrecognized deletion state at either scope fails closed rather than becoming an implicit active signal.

This is the security-design application of fail-safe defaults: permission to create enforcement material is established by positive evidence rather than by the absence of a denial. Saltzer and Schroeder (1975) describe that protection principle as defaulting to lack of permission unless conditions for access are established. For the parser boundary here, the analogous safe state is “do not create an enforcement indicator” when the upstream authorization signal is absent, withdrawn, or structurally invalid.

The hostile regressions intentionally supply valid JSON whose admission members are semantically malformed, absent, or contradictory with lifecycle state. That test style is consistent with the fuzzing literature's treatment of syntactically and semantically malformed inputs as a primary way to expose security-relevant parser behavior (Manès et al., 2021). Wardnet already archives the permitted arXiv version of that survey at [`../papers/fuzzing-art-science-engineering-survey-arxiv-1812.00140.pdf`](../papers/fuzzing-art-science-engineering-survey-arxiv-1812.00140.pdf). The Saltzer–Schroeder paper is linked to the authors' MIT-hosted web rendering rather than copied into the repository because the hosted metadata retains the authors' 1975 copyright; this document does not manufacture a redistribution right.

## Alternatives considered

Keeping `unwrap_or(true)` for `to_ids` was rejected because an omitted field would silently become stronger evidence than MISP supplied. Treating arbitrary JSON values as true was rejected because a producer/schema defect would widen enforcement. Treating an unknown or malformed `deleted` value as active was rejected for the same reason: lifecycle ambiguity must not revive enforcement material. Ignoring an enclosing MISP Object's deletion marker was rejected because it allows a nested attribute to outlive the producer's parent-object withdrawal decision. Rejecting the entire MISP document was also rejected for this bounded repair: `skipped_attributes` already provides a per-attribute fail-closed path and allows valid sibling indicators to remain usable while preserving input-quality evidence. A future contract version may remove the legacy scalar compatibility spellings, but that is a separate compatibility decision and is not required to close this defect.

## Verification contract

`tests/misp_to_ids_admission.rs` is the focused security regression. It requires object, array, `null`, and missing `to_ids` values to be skipped while an explicit boolean `true` control remains admitted. It also requires attribute-level `deleted=true`, `deleted="1"`, and a structurally malformed deletion marker to be skipped even when `to_ids=true`, while `deleted=false`, `deleted="0"`, and omitted deletion state remain active. The parent-object hostile case requires nested otherwise-actionable attributes to be skipped when the enclosing MISP Object is deleted or has a structurally ambiguous deletion marker; recognized active and omitted parent deletion state remain compatible. Existing unit coverage retains boolean `to_ids=false` rejection and the supported explicit scalar compatibility cases. Merge evidence must come from the exact current PR head and then-live Wardnet CI/fuzz/security/review gates; source reasoning or predecessor runs are not a substitute.

## Traceability and references

MISP Project. (n.d.). *misp-objects: Definition, description and relationship types of MISP objects* (`schema_objects.json`). GitHub. https://github.com/MISP/misp-objects/blob/main/schema_objects.json

MISP Project. (n.d.). *MISP-STIX converter*. GitHub. https://github.com/MISP/misp-stix

MISP Project. (n.d.). *MISP ZeroMQ documentation*. GitHub. https://github.com/MISP/misp-book/blob/main/misp-zmq/README.md

MISP Project. (2026). *Event object row rendering* (`app/View/Elements/Events/View/row_object.ctp`, commit `9294667a5b40e59ea42314c2aafa99086ce1d8e6`). GitHub. https://github.com/MISP/MISP/blob/9294667a5b40e59ea42314c2aafa99086ce1d8e6/app/View/Elements/Events/View/row_object.ctp

Saltzer, J. H., & Schroeder, M. D. (1975). The protection of information in computer systems. *Proceedings of the IEEE, 63*(9), 1278–1308. https://doi.org/10.1109/PROC.1975.9939

Author-hosted rendering: https://web.mit.edu/saltzer/www/publications/protection/index.html

Manès, V. J. M., Han, H., Han, C., Cha, S. K., Egele, M., Schwartz, E. J., & Woo, M. (2021). The art, science, and engineering of fuzzing: A survey. *IEEE Transactions on Software Engineering, 47*(11), 2312–2331. https://doi.org/10.1109/TSE.2019.2946563

Repository archive: [`../papers/fuzzing-art-science-engineering-survey-arxiv-1812.00140.pdf`](../papers/fuzzing-art-science-engineering-survey-arxiv-1812.00140.pdf)
Loading
Loading