Files
NetScaler_forensik/tests/test_supplemental_checks.py
T

107 lines
4.8 KiB
Python

"""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=<bad\ufffd^M>, for login",
"acs=<https://example.invalid/>, 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=<abc\xf0^M>, 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)