import hashlib import io import json import tempfile import unittest from contextlib import redirect_stderr, redirect_stdout from importlib.resources import files from pathlib import Path from unittest.mock import patch from netscaler_triage.cli import main from netscaler_triage.engine import scan from netscaler_triage.matching import Matcher, load_config, public_config from netscaler_triage.sigma_rules import SigmaEvaluationLimit, SigmaMatcher RULE = """title: Decoded command with request context id: 283224da-7d4b-4436-a564-bf16e14b2536 logsource: product: netscaler service: http_access detection: selection: uri|startswith: /vpn/ raw|re: '^id$' excluded: uri|endswith: /health?x=aWQ%3D condition: selection and not excluded falsepositives: - Authorized diagnostic requests require analyst review. level: medium """ def write_requests(logs: Path) -> None: logs.mkdir() lines = [ "192.0.2.1 - - [01/Oct/2026:12:00:00 +0000] " f'"GET {uri} HTTP/1.1" 403 0 "-" "synthetic-agent"\n' for uri in ("/vpn/test?x=aWQ%3D", "/vpn/health?x=aWQ%3D", "/other?x=aWQ%3D") ] (logs / "httpaccess.log").write_text("".join(lines), encoding="utf-8") def findings(output: Path) -> list[dict[str, object]]: return [json.loads(line) for line in (output / "findings.jsonl").read_text().splitlines()] class SigmaIntegrationTests(unittest.TestCase): def test_cli_yaml_structured_context_decoded_and_negation(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary).resolve() logs, output, rules = root / "logs", root / "results", root / "rules.yml" write_requests(logs) rules.write_text(RULE, encoding="utf-8") with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()): code = main( [ "scan", "--input", f"ADC={logs}", "--output", str(output), "--config", str(rules), "--no-default-patterns", ] ) self.assertEqual(code, 0) rows = findings(output / "details") matches = [row for row in rows if row.get("rules")] self.assertEqual(len(matches), 1) self.assertEqual(matches[0]["line"], 1) details = matches[0]["rule_details"] self.assertIsInstance(details, list) assert isinstance(details, list) self.assertTrue(any("decoded:" in str(detail["basis"]) for detail in details)) self.assertIn("sigma_decoded_views", matches[0]) self.assertTrue(all(detail["source"] == "rules.yml" for detail in details)) views, layers = matches[0]["sigma_decoded_views"], matches[0]["decoding"] assert isinstance(views, list) and isinstance(layers, list) for detail in details: for basis in detail["basis"]: if basis.startswith("decoded:"): index = int(basis.split(":", 1)[1]) self.assertEqual(layers[views[index]]["output"], "id") def test_decoded_sha256_is_a_structured_sigma_field(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary).resolve() logs, output, rules = root / "logs", root / "results", root / "hash.yml" write_requests(logs) digest = hashlib.sha256(b"id").hexdigest() rules.write_text( "title: Synthetic decoded hash\nlogsource: {product: netscaler}\n" f"detection:\n selection:\n SHA256: '{digest}'\n condition: selection\n", encoding="utf-8", ) scan([("ADC", logs)], output, [load_config(rules)], defaults=False) matched = [row for row in findings(output) if row.get("rules")] self.assertEqual(len(matched), 3) self.assertTrue(all("Synthetic decoded hash" in str(row["rules"]) for row in matched)) def test_public_pack_parses_and_loads_by_default(self) -> None: content = files("netscaler_triage").joinpath("data", "public_sigma.yml").read_text() pack = SigmaMatcher.from_yaml(content, source="public_sigma.yml") self.assertGreaterEqual(len(pack.rules), 22) self.assertEqual(pack.indicators, public_config()["indicators"]) matcher = Matcher([]) hits = matcher.search_sigma({"raw": " None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary).resolve() logs, output = root / "logs", root / "results" logs.mkdir() (logs / "messages").write_text("first\nsecond\n", encoding="utf-8") with patch.object( Matcher, "search_sigma", side_effect=[SigmaEvaluationLimit("limit"), []] ): result = scan([("ADC", logs)], output, [], defaults=False, extract_all=True) self.assertEqual(result["status"], "partial") self.assertEqual(result["counts"]["records"], 2) self.assertEqual(result["counts"]["matching_errors"], 1) rows = findings(output) self.assertTrue(rows[0]["analysis_errors"]) self.assertEqual(rows[1]["raw"], "second") if __name__ == "__main__": unittest.main()