Initial release: offline NetScaler triage with Sigma rules and MIT license
This commit is contained in:
commit
711c42b270
52 files changed
+61222
No files matched your search
@@ -0,0 +1,105 @@
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from netscaler_triage.engine import scan
|
||||
from netscaler_triage.matching import Matcher
|
||||
|
||||
|
||||
class RuleDetailsTests(unittest.TestCase):
|
||||
def test_shared_rule_keeps_both_source_descriptions_without_duplicate_regex(self) -> None:
|
||||
matcher = Matcher(
|
||||
[
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"id": "shared",
|
||||
"pattern": "signal",
|
||||
"description": "Source one",
|
||||
"source": "REF-1",
|
||||
},
|
||||
{
|
||||
"id": "shared",
|
||||
"pattern": "signal",
|
||||
"description": "Source two",
|
||||
"source": "REF-2",
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
defaults=False,
|
||||
)
|
||||
self.assertEqual(len(matcher.rules), 1)
|
||||
_, _, details = matcher.search_with_details("signal")
|
||||
self.assertEqual({row["source"] for row in details}, {"REF-1", "REF-2"})
|
||||
|
||||
def test_default_descriptions_and_compatible_search(self) -> None:
|
||||
matcher = Matcher([], defaults=True)
|
||||
ids, hits = matcher.search("payload ;hostname")
|
||||
detailed_ids, detailed_hits, details = matcher.search_with_details("payload ;hostname")
|
||||
self.assertEqual((ids, hits), (detailed_ids, detailed_hits))
|
||||
self.assertTrue(
|
||||
any(row.get("name") == "command_injection" and row["description"] for row in details)
|
||||
)
|
||||
|
||||
def test_same_id_does_not_attribute_unmatched_description(self) -> None:
|
||||
matcher = Matcher(
|
||||
[
|
||||
{
|
||||
"patterns": [
|
||||
{
|
||||
"id": "custom",
|
||||
"pattern": "MATCH_ME",
|
||||
"description": "Matched explanation",
|
||||
"source": "https://example.org/source",
|
||||
},
|
||||
{
|
||||
"id": "custom",
|
||||
"pattern": "NEVER_MATCH",
|
||||
"description": "Must not be attributed",
|
||||
"source": "https://example.org/other",
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
defaults=False,
|
||||
)
|
||||
ids, _, details = matcher.search_with_details("MATCH_ME")
|
||||
self.assertEqual(ids, ["custom"])
|
||||
self.assertEqual(len(details), 1)
|
||||
self.assertEqual(details[0]["description"], "Matched explanation")
|
||||
self.assertEqual(details[0]["source"], "https://example.org/source")
|
||||
|
||||
def test_decoded_rule_metadata_reaches_findings(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp).resolve()
|
||||
logs = root / "logs"
|
||||
logs.mkdir()
|
||||
(logs / "http.log").write_text(
|
||||
'192.0.2.1 - - [01/Oct/2026:12:00:00 +0000] "GET /?x=aWQ%3D HTTP/1.1" '
|
||||
'404 0 "-" "ua"\n'
|
||||
)
|
||||
config = {
|
||||
"patterns": [
|
||||
{
|
||||
"id": "decoded-id",
|
||||
"pattern": "^id$",
|
||||
"description": "The decoded value is the id command; execution is not "
|
||||
"established.",
|
||||
"source_id": "REF-1",
|
||||
"source": "https://example.org/advisory",
|
||||
}
|
||||
]
|
||||
}
|
||||
scan([("ADC", logs)], root / "out", [config], defaults=False)
|
||||
events = [
|
||||
json.loads(line) for line in (root / "out/findings.jsonl").read_text().splitlines()
|
||||
]
|
||||
self.assertEqual(len(events), 1)
|
||||
self.assertEqual(events[0]["rules"], ["decoded-id"])
|
||||
self.assertEqual(events[0]["rule_details"], config["patterns"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user