Files
NetScaler_forensik/tests/test_sigma_rules.py
T

190 lines
9.0 KiB
Python

import unittest
from netscaler_triage.sigma_rules import SigmaEvaluationLimit, SigmaMatcher, UnsupportedSigma
def rule(detection: str, service: str = "") -> str:
return (
"title: Local test\nlogsource:\n product: netscaler\n"
+ service
+ "detection:\n"
+ detection
)
class SigmaTests(unittest.TestCase):
def test_lookahead_rule_on_raw_and_decoded_text(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(
" s:\n"
" raw|re: '(?im)^(?=.*(?:pitboss|NSPPE))"
"(?=.*(?:base64|PD9))(?=.*(?:2N|httpaccess))'\n"
" condition: s\n"
)
)
sample = "pitboss NSPPE base64 httpaccess"
self.assertTrue(matcher.search({"raw": sample}))
hits = matcher.search_with_details({"raw": "encoded record"}, [sample])
self.assertEqual(hits[0]["basis"], ["decoded:0"])
for benign in ("pitboss NSPPE", "base64 httpaccess", "NSPPE base64"):
self.assertFalse(matcher.search({"raw": benign}))
def test_catalog_not_repeated_in_hit(self) -> None:
text = rule(" s: [abc]\n condition: s\n")
text += "indicators:\n - type: ipv4\n value: 192.0.2.1\n status: reference_only\n"
text += "indicator_status: reference_only\n"
matcher = SigmaMatcher.from_yaml(text)
self.assertEqual(matcher.indicators[0]["value"], "192.0.2.1")
hit = matcher.search_with_details({"raw": "abc"})[0]
self.assertEqual(hit["custom_attributes"], {"indicator_status": "reference_only"})
with self.assertRaises(UnsupportedSigma):
SigmaMatcher.from_yaml(text.replace("value: 192.0.2.1", "value: 123"))
def test_evaluation_bounds(self) -> None:
matcher = SigmaMatcher.from_yaml(rule(" s: [abc]\n condition: s\n"))
with self.assertRaises(SigmaEvaluationLimit):
matcher.search({"raw": "x" * 1048577})
with self.assertRaises(SigmaEvaluationLimit):
matcher.search({"raw": "abc"}, ["abc"] * 65)
with self.assertRaises(SigmaEvaluationLimit):
matcher.search({"raw": "abc"}, ["x" * 1048577])
def test_boolean_and_keywords(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(
" selection: [alpha, beta]\n exclude: [benign]\n"
" condition: selection and not exclude\n"
)
)
self.assertTrue(matcher.search({"raw": "prefix ALPHA suffix"}))
self.assertFalse(matcher.search({"raw": "alpha benign"}))
self.assertFalse(matcher.search({"raw": "other"}))
def test_modifiers_fields_and_all(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(
" s:\n uri|startswith: /vpn/\n user_agent|contains|all: [curl, exploit]\n"
" source_ip|endswith: '.1'\n condition: s\n"
)
)
event = {"fields": {"uri": "/vpn/a", "user_agent": "CURL exploit", "src": "192.0.2.1"}}
self.assertTrue(matcher.search(event))
self.assertFalse(
matcher.search({"fields": {"uri": "/vpn/a", "user_agent": "curl", "src": "192.0.2.1"}})
)
def test_selectors_and_wildcard_escaping(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(
" sel_a: {uri: '/a*'}\n sel_b: {user_agent: 'agent?'}\n"
" condition: all of sel_*\n"
)
)
self.assertTrue(matcher.search({"uri": "/abcdef", "user_agent": "agentX"}))
self.assertFalse(matcher.search({"uri": "/abcdef", "user_agent": "agentXY"}))
matcher = SigmaMatcher.from_yaml(
rule(" sel_a: {uri: '/literal\\*'}\n sel_b: [other]\n condition: 1 of sel_*\n")
)
self.assertTrue(matcher.search({"uri": "/literal*"}))
self.assertFalse(matcher.search({"uri": "/literalanything"}))
def test_regex_service_and_decoded_basis(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(
" s:\n raw|re: '(?i)passthru\\s*\\('\n condition: s\n",
" service: http_access\n",
),
source="test.yml",
)
hits = matcher.search_with_details(
{"raw": "encoded bytes", "kind": "http_access"}, ["passthru('id')"]
)
self.assertEqual(hits[0]["basis"], ["decoded:0"])
self.assertEqual(hits[0]["source"], "test.yml")
self.assertFalse(matcher.search({"raw": "passthru('id')", "kind": "syslog"}))
def test_regex_case_is_not_string_case(self) -> None:
matcher = SigmaMatcher.from_yaml(rule(" s: {raw|re: Alpha}\n condition: s\n"))
self.assertTrue(matcher.search({"raw": "Alpha"}))
self.assertFalse(matcher.search({"raw": "alpha"}))
def test_structured_only_match_is_not_attributed_to_decoding(self) -> None:
matcher = SigmaMatcher.from_yaml(rule(" s: {SHA256: [abc, def]}\n condition: s\n"))
hits = matcher.search_with_details({"sha256": ["def"], "raw": "original"}, ["decoded"])
self.assertEqual(hits[0]["basis"], ["raw"])
def test_null_numeric_and_missing_fields(self) -> None:
matcher = SigmaMatcher.from_yaml(rule(" s: {status: 200, absent: null}\n condition: s\n"))
self.assertTrue(matcher.search({"status": 200}))
self.assertFalse(matcher.search({"status": "200"}))
self.assertFalse(matcher.search({"status": 200, "absent": "value"}))
def test_numeric_and_boolean_lists_preserve_types(self) -> None:
numeric = SigmaMatcher.from_yaml(rule(" s: {status: 403}\n condition: s\n"))
self.assertTrue(numeric.search({"status": [200, 403]}))
self.assertFalse(numeric.search({"status": ["403", True]}))
boolean = SigmaMatcher.from_yaml(rule(" s: {enabled: true}\n condition: s\n"))
self.assertTrue(boolean.search({"enabled": [False, True]}))
self.assertFalse(boolean.search({"enabled": [0, 1, "true"]}))
def test_limits_cannot_be_skipped_by_service_or_short_circuit(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(" s: {status: 200, uri: '*'}\n condition: s\n", " service: http_access\n")
)
for event in (
{"kind": "syslog", "raw": "x" * 1048577},
{"kind": "http_access", "status": 404, "uri": ["x" * 1048577]},
):
with self.subTest(kind=event["kind"]), self.assertRaises(SigmaEvaluationLimit):
matcher.search(event)
with self.assertRaises(SigmaEvaluationLimit):
matcher.search({"kind": "syslog"}, ["x" * 1048577])
def test_unused_metadata_is_not_traversed_and_used_lists_are_bounded(self) -> None:
matcher = SigmaMatcher.from_yaml(rule(" s: {status: 200}\n condition: s\n"))
self.assertTrue(matcher.search({"status": 200, "unrelated": ["x" * 1048577]}))
with self.assertRaises(SigmaEvaluationLimit):
matcher.search({"status": [200] * 10001})
def test_decoded_views_preserve_positive_and_negative_structured_context(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(
" s: {raw|contains: suspicious, uri|startswith: /vpn/}\n"
" excluded: {source_ip: '192.0.2.5'}\n"
" condition: s and not excluded\n"
)
)
event = {"raw": "encoded", "uri": "/vpn/check", "source_ip": "192.0.2.6"}
hits = matcher.search_with_details(event, ["suspicious"])
self.assertEqual(hits[0]["basis"], ["decoded:0"])
self.assertFalse(matcher.search({**event, "source_ip": "192.0.2.5"}, ["suspicious"]))
self.assertFalse(matcher.search({**event, "uri": "/health"}, ["suspicious"]))
def test_explicitly_unsupported(self) -> None:
for modifier in ("base64", "cidr", "exists", "fieldref", "cased"):
with self.subTest(modifier=modifier), self.assertRaises((UnsupportedSigma, ValueError)):
SigmaMatcher.from_yaml(rule(f" s:\n raw|{modifier}: abc\n condition: s\n"))
with self.assertRaises(UnsupportedSigma):
SigmaMatcher.from_yaml(rule(" s: [abc]\n condition: s\n", " service: windows\n"))
with self.assertRaises(UnsupportedSigma):
SigmaMatcher.from_yaml(
rule(" s: [abc]\n condition: s\n").replace("netscaler", "windows")
)
with self.assertRaises(UnsupportedSigma):
SigmaMatcher.from_yaml("title: Unsupported\ncorrelation: {type: event_count}\n")
with self.assertRaises(UnsupportedSigma):
SigmaMatcher.from_yaml(rule(" s: &selector [abc]\n condition: s\n"))
with self.assertRaises(UnsupportedSigma):
SigmaMatcher.from_yaml(rule(" s: [abc]\n s: [def]\n condition: s\n"))
def test_timeout(self) -> None:
matcher = SigmaMatcher.from_yaml(
rule(" s: {raw|re: '(a+)+$'}\n condition: s\n"), timeout=0.00001
)
with self.assertRaises(TimeoutError):
matcher.search({"raw": "a" * 50000 + "!"})
if __name__ == "__main__":
unittest.main()