|
19 | 19 | SigmaValidator, |
20 | 20 | build_rule_from_template, |
21 | 21 | ) |
22 | | -from app import _convert_backend_safe |
| 22 | +from app import _convert_backend_safe, _wazuh_unsupported_message |
23 | 23 |
|
24 | 24 | BACKENDS = ["splunk", "elastic", "eql", "sentinel", "wazuh", "qradar", "dac_json"] |
25 | 25 |
|
26 | 26 | # Templates whose condition is an aggregation ("selection | count(...) by field > N"). |
27 | | -# Wazuh's backend does not support aggregation conditions and raises NotImplementedError. |
| 27 | +# Wazuh's backend does not support aggregation conditions and returns a |
| 28 | +# structured {"supported": False, "reason": "aggregation_condition"} result |
| 29 | +# instead of raising (CodeQL py/stack-trace-exposure: response text must |
| 30 | +# never be derived from a caught exception object). |
28 | 31 | AGGREGATION_TEMPLATES = { |
29 | 32 | "windows_logon_brute_force", |
30 | 33 | "firewall_port_scan", |
|
41 | 44 | @pytest.mark.parametrize("key", TEMPLATE_KEYS) |
42 | 45 | def test_convert_matrix(key, backend): |
43 | 46 | rule_yaml = _RULE_YAML[key] |
44 | | - expect_not_implemented = backend == "wazuh" and key in AGGREGATION_TEMPLATES |
| 47 | + expect_unsupported = backend == "wazuh" and key in AGGREGATION_TEMPLATES |
45 | 48 |
|
46 | | - if expect_not_implemented: |
47 | | - with pytest.raises(NotImplementedError): |
48 | | - SIEMConverter.convert(rule_yaml, backend) |
| 49 | + result = SIEMConverter.convert(rule_yaml, backend) |
| 50 | + |
| 51 | + if expect_unsupported: |
| 52 | + assert isinstance(result, dict) |
| 53 | + assert result.get("supported") is False |
| 54 | + assert result.get("reason") == "aggregation_condition" |
49 | 55 | return |
50 | 56 |
|
51 | | - result = SIEMConverter.convert(rule_yaml, backend) |
| 57 | + assert not isinstance(result, dict), ( |
| 58 | + f"{key!r}/{backend!r} unexpectedly returned a structured " |
| 59 | + f"unsupported result: {result!r}" |
| 60 | + ) |
52 | 61 | assert isinstance(result, str) |
53 | 62 | assert result.strip() != "" |
54 | 63 |
|
55 | 64 |
|
56 | | -def test_convert_backend_safe_preserves_notimplementederror_text(): |
57 | | - """NotImplementedError is raised deliberately by SIEMConverter (e.g. the |
58 | | - Wazuh backend's lack of aggregation-condition support) with a message |
59 | | - written for the end user, so _convert_backend_safe() must surface it |
60 | | - verbatim rather than genericizing it.""" |
| 65 | +def test_convert_backend_safe_uses_static_message_for_unsupported_wazuh_condition(): |
| 66 | + """Known Wazuh syntax gaps (e.g. aggregation conditions) are signaled by |
| 67 | + SIEMConverter as a structured {"supported": False, "reason": ...} result, |
| 68 | + never an exception, and _convert_backend_safe() must render the |
| 69 | + corresponding STATIC message from _WAZUH_UNSUPPORTED_MESSAGES — not any |
| 70 | + text derived from the rule content or a caught exception object |
| 71 | + (CodeQL py/stack-trace-exposure).""" |
61 | 72 | rule_yaml = _RULE_YAML["brute_force_by_username"] |
62 | 73 | result = _convert_backend_safe( |
63 | 74 | rule_yaml, "wazuh", rule_id=100001, group_name="sigma_rules" |
64 | 75 | ) |
65 | | - assert result.startswith("Conversion error:") |
| 76 | + assert result == f"Conversion error: {_wazuh_unsupported_message('aggregation_condition')}" |
66 | 77 | assert "aggregation conditions" in result |
67 | | - assert "TargetUserName" in result # condition text from the real NotImplementedError message |
| 78 | + assert "TargetUserName" not in result # no rule-derived content in the message |
68 | 79 |
|
69 | 80 |
|
70 | 81 | def test_convert_backend_safe_genericizes_other_exceptions(caplog): |
71 | | - """Any exception other than NotImplementedError (here: malformed YAML |
72 | | - raising a yaml.YAMLError deep in SIEMConverter.convert()) must not leak |
73 | | - its message, parser detail, or exception class name to the client — only |
74 | | - the fixed generic message, with full detail logged server-side |
75 | | - (CodeQL py/stack-trace-exposure).""" |
| 82 | + """Any actual exception (here: malformed YAML raising a yaml.YAMLError |
| 83 | + deep in SIEMConverter.convert()) — as opposed to the structured |
| 84 | + {"supported": False, ...} result used for known Wazuh syntax gaps — must |
| 85 | + not leak its message, parser detail, or exception class name to the |
| 86 | + client. Only the fixed generic message reaches the response, with full |
| 87 | + detail logged server-side (CodeQL py/stack-trace-exposure).""" |
76 | 88 | with caplog.at_level(logging.ERROR): |
77 | 89 | result = _convert_backend_safe("not: [valid yaml structure", "splunk") |
78 | 90 |
|
@@ -119,15 +131,21 @@ def test_sentinel_aggregation_has_no_orphan_comment_or_duplicate_where(key): |
119 | 131 | ) |
120 | 132 |
|
121 | 133 |
|
122 | | -def test_validator_surfaces_yaml_parse_error_text(): |
123 | | - """Intentional behavior, not a bug: CodeQL py/stack-trace-exposure alert #24 |
124 | | - on app.py's /api/validate route was dismissed because that endpoint exists |
125 | | - specifically so a user can paste arbitrary Sigma YAML and be told why it |
126 | | - fails to parse. SigmaValidator.validate() deliberately includes the |
127 | | - yaml.YAMLError text in its errors list for exactly this reason — lock it |
128 | | - in so a future "fix" for the CodeQL alert doesn't quietly break it.""" |
| 134 | +def test_validator_yaml_parse_error_is_static_not_exception_derived(): |
| 135 | + """CodeQL py/stack-trace-exposure alerts #23/#25/#26 flagged the SUCCESS |
| 136 | + return path in app.py because SigmaValidator.validate()'s result (which |
| 137 | + is always included in those responses) embedded str(e) from the caught |
| 138 | + yaml.YAMLError. The previous "alert #24 dismissed" design (surfacing the |
| 139 | + raw yaml.YAMLError text) is superseded: the validator must now build its |
| 140 | + own static parse-failure message and never reach into the exception |
| 141 | + object, so no yaml library/module/parser detail can reach an HTTP |
| 142 | + response.""" |
129 | 143 | malformed_yaml = "title: Broken Rule\ndetection: [unclosed\n" |
130 | 144 | result = SigmaValidator.validate(malformed_yaml) |
131 | 145 |
|
132 | 146 | assert result["valid"] is False |
133 | 147 | assert any("YAML parse error" in err for err in result["errors"]) |
| 148 | + joined_errors = " ".join(result["errors"]) |
| 149 | + assert "yaml." not in joined_errors.lower() |
| 150 | + assert "line " not in joined_errors.lower() |
| 151 | + assert "column" not in joined_errors.lower() |
0 commit comments