"""Review heuristics must retain context and never turn a candidate into proof.""" import json import tempfile import unittest from pathlib import Path from typing import ClassVar from netscaler_triage.filesystem import scan_filesystem from netscaler_triage.matching import Matcher class SupplementalChecksTests(unittest.TestCase): matcher: ClassVar[Matcher] @classmethod def setUpClass(cls) -> None: cls.matcher = Matcher([]) def test_review_rules_raw_decoded_and_negative_controls(self) -> None: cases = { "httpd_commented_security_directive": ("# Require all denied", "Require all denied"), "httpd_php_handler_review": ( "AddHandler application/x-httpd-php .php", "# AddHandler application/x-httpd-php .php", ), "startup_python_reference": ( "/flash/nsconfig/rc.netscaler: python3 script.py", "python3 script.py", ), "core_file_removal": ("rm -f /var/core/*", "ls /var/core/*"), "wsfed_request_id_anomaly": ("WSFed: request_id bad^M^M", "WSFed: request_id abc-123"), "acs_control_marker": ( "acs=, for login", "acs=, for login", ), "web_artifact_response_review": ( 'GET /vpn/abcdef.ico HTTP/1.1" 404 1234', 'GET /vpn/abcdef.ico HTTP/1.1" 404 123', ), "web_artifact_error_review": ( "File does not exist: /vpn/abcdef/.sig", "File does not exist: /favicon.ico", ), "packet_engine_open_file_review": ( "NSPPE 42 root 3r /var/netscaler/sample", "other 42 root 3r /var/netscaler/sample", ), "web_user_process_review": ( "nobody 42 0.0 0.0 /bin/helper", "nobody 42 0.0 0.0 /bin/httpd -k start", ), } for name, (positive, negative) in cases.items(): with self.subTest(name=name): hits = self.matcher.search_sigma({"raw": positive}) self.assertTrue(any(h["name"] == name and h["level"] == "low" for h in hits)) decoded = self.matcher.search_sigma({"raw": "opaque"}, [positive]) self.assertTrue( any(h["name"] == name and "decoded:0" in h["basis"] for h in decoded) ) self.assertFalse( any(h["name"] == name for h in self.matcher.search_sigma({"raw": negative})) ) def test_snapshot_paths_original_bytes_and_config_scope(self) -> None: with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) source = root / "export" entries = { "etc/blx/nshttpd.conf": ( b"# Require all denied\nAddHandler application/x-httpd-php .php\n" ), "flash/nsconfig/rc.netscaler": b"python3 /opt/example.py\n", "var/log/ns.log": b"WSFed: request_id \x81\nacs=, for login\n", "var/vpn/test.PHP": b"inert", "var/netscaler/gui/package.bin": b"not found\x00/opt/Citrix/NSGClient/a", "var/netscaler/gui/test.sig": b"inert", "var/netscaler/gui/admin_ui/vendor.php": b"inert", "var/log/notes.log": b"# Require all denied\n", } for name, content in entries.items(): path = source / name path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(content) result = scan_filesystem(source, root / "results") self.assertTrue(result["complete_scan"]) rows = [ json.loads(line) for line in (root / "results/findings.jsonl").read_text().splitlines() ] found = {(r["path"], r["rule"]) for r in rows} for source_path, rule in [ ("etc/blx/nshttpd.conf", "httpd_commented_security_directive"), ("flash/nsconfig/rc.netscaler", "startup_python_content"), ("var/log/ns.log", "wsfed_request_id_anomaly"), ("var/log/ns.log", "acs_control_marker"), ("var/vpn/test.PHP", "web_tree_file_review"), ("var/netscaler/gui/test.sig", "web_tree_file_review"), ]: self.assertIn((source_path, rule), found) self.assertNotIn( ("var/netscaler/gui/admin_ui/vendor.php", "web_tree_file_review"), found ) self.assertNotIn(("var/log/notes.log", "httpd_commented_security_directive"), found) self.assertNotIn(("var/netscaler/gui/package.bin", "web_artifact_error_review"), found)