Initial release: offline NetScaler triage with Sigma rules and MIT license

This commit is contained in:
StasonJatham committed 2026-10-01 23:08:50 +02:00
commit 711c42b270
52 files changed
+61222

No files matched your search

+171
View File
@@ -0,0 +1,171 @@
"""Public intelligence provenance and representative matching regressions."""
import base64
import json
import tempfile
import unittest
from pathlib import Path
from typing import Any, ClassVar
from urllib.parse import quote
from netscaler_triage.decoding import decode_layers, has_encoding_candidate
from netscaler_triage.engine import scan
from netscaler_triage.matching import Matcher, public_config
class IntelInventoryTests(unittest.TestCase):
catalog: ClassVar[dict[str, Any]]
matcher: ClassVar[Matcher]
@classmethod
def setUpClass(cls) -> None:
cls.catalog = public_config()
cls.matcher = Matcher([])
def test_unique_catalog_and_resolvable_public_provenance(self) -> None:
sources = {row["id"]: row for row in self.catalog["sources"]}
self.assertGreaterEqual(len(sources), 37)
rows = self.catalog["indicators"]
self.assertEqual(len(rows), len({(row["type"], row["value"]) for row in rows}))
self.assertEqual(len(rows), len({row["id"] for row in rows}))
for row in rows:
with self.subTest(indicator=row["id"]):
self.assertTrue(row["roles"])
self.assertTrue(row["observations"])
self.assertTrue(row["source_ids"])
for source_id in row["source_ids"]:
self.assertIn(source_id, sources)
self.assertIn(sources[source_id]["url"], row["source_urls"])
for observation in row["observations"]:
self.assertTrue(observation["source_dates"])
self.assertIn("original_status", observation)
for date in observation["source_dates"]:
self.assertIn(date["id"], sources)
self.assertIn("published", date)
self.assertTrue(date["retrieved"])
urls = {source["url"] for source in sources.values()}
self.assertIn(
"https://misp.ifin.network/feed/63670851-a19e-4fc4-8fe5-c89bebc8e921.json", urls
)
self.assertTrue(any("elastic" in url for url in urls))
self.assertTrue(any("sygnia.co" in url for url in urls))
def test_secondary_direction_conflicts_and_shared_proxy_context_survive(self) -> None:
rows = self.catalog["indicators"]
observations = [ob for row in rows for ob in row["observations"]]
conflicts = [
ob
for ob in observations
if ob.get("misp_type") == "ip-dst" and "direction conflict" in ob.get("description", "")
]
self.assertTrue(conflicts)
shared = next(row for row in rows if row["value"] == "104.28.215.136")
context = [ob for ob in shared["observations"] if ob.get("misp_to_ids") is False]
self.assertTrue(context)
self.assertTrue(any(ob["original_status"] == "context_only" for ob in context))
self.assertIn("shared", json.dumps(shared).lower())
def test_every_public_indicator_matches_raw_and_decoded_views(self) -> None:
for row in self.catalog["indicators"]:
value = row["value"]
with self.subTest(indicator=row["id"]):
_, hits, _ = self.matcher.search_with_details(value)
self.assertTrue(any(hit["id"] == row["id"] for hit in hits))
raw = self.matcher.search_sigma({"raw": value})
if value == "/var/python/bin/customsnmpd":
self.assertFalse(
raw
) # Legitimate binary: standalone Sigma intentionally omitted.
else:
self.assertTrue(any("raw" in hit["basis"] for hit in raw))
encoded = base64.b64encode(value.encode()).decode()
layers = decode_layers("base64:" + encoded)
decoded = [
layer["output"]
for layer in layers
if layer.get("output") and layer.get("output_encoding") != "hex"
]
self.assertIn(value, decoded)
sigma = self.matcher.search_sigma({"raw": "encoded event"}, decoded)
if value == "/var/python/bin/customsnmpd":
self.assertFalse(sigma)
else:
self.assertTrue(
any(any(b.startswith("decoded:") for b in hit["basis"]) for hit in sigma)
)
escaped = quote(value, safe="")
if escaped != value:
url_layers = decode_layers(escaped)
self.assertTrue(any(layer.get("output") == value for layer in url_layers))
def test_encoding_gate_accepts_canonical_public_indicator_fields(self) -> None:
for row in self.catalog["indicators"]:
encoded = base64.b64encode(row["value"].encode()).decode()
for value in (encoded, encoded.rstrip("=")):
with self.subTest(indicator=row["id"], encoded=value):
self.assertTrue(has_encoding_candidate(value))
self.assertTrue(has_encoding_candidate("payload=" + value))
for payload in ("HTTP_X_UX", "HTTP_X_UX" * 8):
encoded = base64.b64encode(payload.encode()).decode().rstrip("=")
self.assertTrue(has_encoding_candidate(encoded))
for ordinary in ("heartbeat", "INFO routine system heartbeat", "GET / HTTP/1.1"):
self.assertFalse(has_encoding_candidate(ordinary))
# A valid but noncanonical spelling must not pass the whole-field gate.
self.assertFalse(has_encoding_candidate("SFRUUF9YX1VZ=="))
def test_ipv4_literals_do_not_match_address_prefixes(self) -> None:
for row in self.catalog["indicators"]:
if row["type"] != "ipv4":
continue
for text in ("9" + row["value"], row["value"] + "9", row["value"] + ".1"):
with self.subTest(value=row["value"], text=text):
_, hits, _ = self.matcher.search_with_details(text)
self.assertFalse(any(hit["id"] == row["id"] for hit in hits))
self.assertFalse(self.matcher.search_sigma({"raw": text}))
def test_behavior_patterns_match_real_syntax_not_escaped_backslashes(self) -> None:
samples = [
"SSL_HANDSHAKE_FAILURE DTLSv1.0 Handshake failure-Internal Error",
"NSPPE-42 exit with orphan rings",
"pitboss PPE unexpectedly died NSPPE;id;#",
"HTTP_X_UX_0",
"*/5 * * * * root perl /var/tmp/.nsmon/nsmon.pl",
]
for sample in samples:
with self.subTest(sample=sample):
self.assertTrue(self.matcher.search_sigma({"raw": sample}))
benign = self.matcher.search_sigma({"raw": "pitboss PPE missed too many heartbeats"})
self.assertFalse(any(hit.get("level") in {"high", "critical"} for hit in benign))
def test_http_user_agent_encodings_keep_original_and_source_metadata(self) -> None:
payload = "/var/tmp/.nsmon/nsmon.pl 149.104.78.208"
b64 = base64.b64encode(payload.encode()).decode()
variants = [
quote(payload, safe=""),
b64,
b64.rstrip("="),
quote(base64.b64encode(quote(payload, safe="").encode()).decode(), safe=""),
]
originals = [
f'192.0.2.10 - - [01/Oct/2026:12:00:00 +0000] "GET / HTTP/1.1" 200 0 "-" "{variant}"'
for variant in variants
]
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
logs = root / "logs"
logs.mkdir()
(logs / "httpaccess.log").write_text("\n".join(originals) + "\n")
output = root / "output"
scan([("ADC", logs)], output, [])
events = [
json.loads(line) for line in (output / "findings.jsonl").read_text().splitlines()
]
self.assertEqual(len(events), len(originals))
for event, original in zip(events, originals, strict=True):
self.assertEqual(event["raw"], original)
hits = [
hit for hit in event["indicator_matches"] if hit["value"] == "149.104.78.208"
]
self.assertTrue(hits)
self.assertTrue(all(hit["source_urls"] and hit["source_ids"] for hit in hits))
self.assertTrue(any(layer.get("output") == payload for layer in event["decoding"]))