Expand offline review checks and fix relative output finalization
This commit is contained in:
1 parent
83f53adb29
commit
b4fd5fb6ff
12 files changed
+571
-6
No files matched your search
@@ -95,6 +95,18 @@ class EasyCliTests(unittest.TestCase):
|
||||
self.invoke(["scan", str(folder)])
|
||||
self.assertEqual(len(self.events()), 1)
|
||||
|
||||
def test_relative_output_preserves_preparation_manifest(self) -> None:
|
||||
folder = self.case / "logs"
|
||||
folder.mkdir()
|
||||
(folder / "httpaccess.log").write_text(LOG, encoding="utf-8")
|
||||
self.invoke(["scan", "logs", "--output", "results/new"])
|
||||
output = self.case / "results/new"
|
||||
manifest = json.loads((output / "details/preparation_manifest.json").read_text())
|
||||
self.assertTrue((output / "SUMMARY.txt").is_file())
|
||||
for root in manifest["roots"]:
|
||||
self.assertTrue((output / root["path"]).is_dir())
|
||||
self.assertEqual(len(self.events()), 1)
|
||||
|
||||
def test_missing_common_drive_uses_current_output_parent(self) -> None:
|
||||
folder = self.case / "logs"
|
||||
folder.mkdir()
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user