106 lines
4.8 KiB
Python
106 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)
|