{"record":{"id":"507f50f1b1256fc0","repo":"stamparm/maltrail","slug":"wildcard-alert","errorCode":null,"errorMessage":"wildcard alert","messagePattern":"wildcard alert","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sensor/src/heuristics/nxdomain.rs","lineNumber":180,"sourceCode":"            if let Some(NxAlert::Exact { trail }) = nx.observe(\"a.com\", \"a.com\", 10) {\n                assert_eq!(trail, \"a.com\");\n                alerts += 1;\n            }\n        }\n        assert_eq!(alerts, 1, \"the key is dropped after alerting, so it fires once\");\n    }\n\n    #[test]\n    fn wildcard_key_builds_the_python_trail() {\n        let mut nx = NxCounters::default();\n        let key = \"*.evil.com\";\n        let mut trail = None;\n        for i in 0..(settings::NO_SUCH_NAME_PER_HOUR_THRESHOLD + 2) {\n            if let Some(NxAlert::Wildcard { trail: t, .. }) = nx.observe(key, &format!(\"h{i}.evil.com\"), 10) {\n                trail = Some(t);\n            }\n        }\n        let trail = trail.expect(\"wildcard alert\");\n        assert!(trail.starts_with('('), \"{trail}\");\n        assert!(trail.ends_with(\").evil.com\"), \"{trail}\");\n        assert!(trail.contains(\"h1,\"), \"{trail}\");\n    }\n\n    #[test]\n    fn entropy_and_consonants() {\n        // sanity: a high-entropy DGA-looking label beats the 3.5 threshold\n        assert!(label_entropy(\"xkqwzlvbnmfghjd\") > settings::SUSPICIOUS_DOMAIN_ENTROPY_THRESHOLD);\n        assert!(label_entropy(\"aaaa\") < 0.001);\n        assert_eq!(label_entropy(\"\"), 0.0);\n        assert_eq!(consonant_count(\"google\"), 3);\n        assert!(consonant_count(\"xkqwzlvbnmf\") > settings::SUSPICIOUS_DOMAIN_CONSONANT_THRESHOLD);\n    }\n}\n","sourceCodeStart":162,"sourceCodeEnd":196,"githubUrl":"https://github.com/stamparm/maltrail/blob/77cfb06d7606506d101bbcec0786c77166c4255e/sensor/src/heuristics/nxdomain.rs#L162-L196","documentation":"Test assertion in wildcard_key_builds_the_python_trail: after pumping > NO_SUCH_NAME_PER_HOUR_THRESHOLD queries under one parent domain, observe() must eventually emit NxAlert::Wildcard carrying a Python-style trail string; the expect fails if no wildcard alert was produced. It encodes the contract that sustained NXDOMAIN traffic under one suffix is classified as a wildcard.","triggerScenarios":"The loop runs NO_SUCH_NAME_PER_HOUR_THRESHOLD+2 times yet no NxAlert::Wildcard variant is returned — the wildcard detector never fires (threshold logic, key building, or counting regressed) or observe now returns a different NxAlert variant.","commonSituations":"Changing the wildcard detection threshold, renaming/alert-variant refactors, or a bug where repeated subdomains are deduplicated so the counter never crosses the threshold.","solutions":["Confirm observe() still returns NxAlert::Wildcard once the hourly threshold is exceeded","Check the wildcard key builder still aggregates the parent domain instead of per-subdomain keys","Verify the test's key/parent-domain setup matches the detector's expectations"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"let mut got_wildcard = false;\nfor i in 0..(settings::NO_SUCH_NAME_PER_HOUR_THRESHOLD + 2) {\n    if matches!(nx.observe(key, &format!(\"h{i}.evil.com\"), 10), Some(NxAlert::Wildcard { .. })) { got_wildcard = true; }\n}\nassert!(got_wildcard, \"wildcard alert never fired\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["When changing alert variants, grep tests for exhaustive matches on NxAlert","Keep threshold constants in settings so tests scale with them"],"tags":["rust","tests","heuristics"],"backgroundTag":"internal-invariant-violation","analyzedSha":"77cfb06d7606506d101bbcec0786c77166c4255e","analyzedAt":"2026-09-13T03:50:16.010Z","contentChangedAt":"2026-09-13T03:50:16.010Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}