Initial release: offline NetScaler triage with Sigma rules and MIT license

This commit is contained in:
StasonJatham committed 2026-10-01 23:08:50 +02:00
commit 711c42b270
52 files changed
+61222

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
"""CLI presentation must not corrupt evidence output or generate assessments."""
import contextlib
import io
import json
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from rich.console import Console
from netscaler_triage.cli import display_result, main
from netscaler_triage.engine import scan
class CliTests(unittest.TestCase):
def test_help_and_argument_errors(self) -> None:
output = io.StringIO()
with contextlib.redirect_stdout(output), self.assertRaises(SystemExit) as raised:
main(["scan", "--help"])
self.assertEqual(raised.exception.code, 0)
self.assertIn("Log folders or archives", output.getvalue())
self.assertIn("Example:", output.getvalue())
self.assertNotIn("--regex-timeout", output.getvalue())
advanced = io.StringIO()
with contextlib.redirect_stdout(advanced), self.assertRaises(SystemExit):
main(["scan", "--help-all"])
self.assertIn("--regex-timeout", advanced.getvalue())
error = io.StringIO()
with contextlib.redirect_stderr(error), self.assertRaises(SystemExit) as raised:
main(["scan", "--not-a-valid-option"])
self.assertEqual(raised.exception.code, 2)
self.assertIn("Invalid arguments", error.getvalue())
self.assertIn("--help", error.getvalue())
def test_json_and_interactive_output(self) -> None:
result = {"status": "complete", "counts": {"records": 7}}
output = io.StringIO()
with contextlib.redirect_stdout(output):
display_result(result, json_output=True)
self.assertEqual(json.loads(output.getvalue()), result)
self.assertNotIn("\x1b", output.getvalue())
terminal = io.StringIO()
with patch(
"netscaler_triage.cli.Console",
return_value=Console(
file=terminal,
force_terminal=True,
color_system=None,
),
):
display_result(result)
self.assertIn("NetScaler triage results", terminal.getvalue())
def test_missing_file_is_friendly_and_has_no_stdout(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
output, error = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(error):
code = main(["decode", str(Path(tmp) / "missing.txt")])
self.assertEqual(code, 2)
self.assertEqual(output.getvalue(), "")
self.assertIn("Check that the input file or directory exists", error.getvalue())
def test_scan_does_not_write_an_assessment(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
source = root / "logs"
source.mkdir()
(source / "sample.log").write_text("plain text\n", encoding="utf-8")
scan([("sample", source)], root / "output", [], defaults=False)
self.assertTrue((root / "output/summary.json").is_file())
self.assertFalse((root / "output/BERICHT.txt").exists())
def test_scan_requires_only_a_folder_and_never_overwrites(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
logs = root / "appliance"
logs.mkdir()
original = "ordinary text\n"
(logs / "sample.log").write_text(original, encoding="utf-8")
for _ in range(2):
output, error = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(error):
self.assertEqual(main(["scan", str(logs)]), 0)
self.assertEqual(json.loads(output.getvalue())["files"], 1)
self.assertIn("Results saved to:", error.getvalue())
destinations = list(root.glob("triage-results-*"))
self.assertEqual(len(destinations), 2)
self.assertEqual((logs / "sample.log").read_text(), original)
for destination in destinations:
config = json.loads((destination / "configuration.json").read_text())
self.assertIsNone(config["year"])
self.assertEqual(config["timezones"], {})
def test_named_inputs_remain_supported(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp).resolve()
logs = root / "logs"
logs.mkdir()
with (
contextlib.redirect_stdout(io.StringIO()),
contextlib.redirect_stderr(io.StringIO()),
):
self.assertEqual(
main(["scan", "--input", f"ADC={logs}", "--output", str(root / "out")]), 0
)
config = json.loads((root / "out/configuration.json").read_text())
self.assertEqual(config["appliances"], ["ADC"])
if __name__ == "__main__":
unittest.main()