Skip to content

Commit 5fdd097

Browse files
committed
Optimize full default CRS rule dispatch
1 parent 99775a2 commit 5fdd097

9 files changed

Lines changed: 4053 additions & 204 deletions

File tree

rules/engine.rss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,16 @@ pub fn record_rule_match(state: map<string>, matched: [string]) -> map<string> {
155155
s
156156
}
157157

158+
pub fn apply_detection_paranoia_skip(state: map<string>, id: int, threshold: int, marker: string) -> map<string> {
159+
let mut s = state;
160+
if engine_context::ctx_get(&s, "blocked") != "1"
161+
&& engine_context::ctx_get(&s, "skip") == ""
162+
&& engine_text::number(engine_context::ctx_get(&s, "tx.detection_paranoia_level"), 1) < threshold {
163+
s = record_rule_match(s, [id + "", "0", "0", "403", marker, ""]);
164+
}
165+
s
166+
}
167+
158168
pub fn apply_rule(state: map<string>, id: int, chain_index: int, has_chain: bool, text: [string], target_count: int, transform_plan: int, score: int, disruptive: bool, status: int) -> map<string> {
159169
let mut s = state;
160170
let operator: int = (target_count % 16384) / 64;

rules/engine_bundle.rss

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,10 @@ pub fn new_state(method: string, path: string, query: string, protocol: string,
229229
}
230230
}
231231
232+
pub fn apply_action(state: map<string>, phase: int) -> map<string> {
233+
state
234+
}
235+
232236
pub fn apply_marker(state: map<string>, marker: string) -> map<string> {
233237
let mut s = state;
234238
if ctx_get(&s, "skip") == marker {
@@ -237,37 +241,6 @@ pub fn apply_marker(state: map<string>, marker: string) -> map<string> {
237241
s
238242
}
239243
240-
pub fn apply_rule_blob(state: map<string>, blob: string) -> map<string> {
241-
let mut s = state;
242-
if blob != "" {
243-
let rows: [string] = string_split_literal(blob, "\r");
244-
for row_index in 0..rows.length {
245-
let fields: [string] = string_split_literal((&rows)[row_index], "\t");
246-
if fields.length >= 13 && (&fields)[0] == "R" && category_enabled(&s, (&fields)[1]) {
247-
let mut text: [string] = [(&fields)[10], (&fields)[11], (&fields)[12]];
248-
for field_index in 13..fields.length {
249-
text[text.length] = (&fields)[field_index];
250-
}
251-
s = apply_rule(
252-
s,
253-
number((&fields)[2], -1),
254-
number((&fields)[3], 0),
255-
(&fields)[4] == "1",
256-
text,
257-
number((&fields)[5], 0),
258-
number((&fields)[6], 0),
259-
number((&fields)[7], 0),
260-
(&fields)[8] == "1",
261-
number((&fields)[9], 403)
262-
);
263-
} else if fields.length >= 3 && (&fields)[0] == "M" && category_enabled(&s, (&fields)[1]) {
264-
s = apply_marker(s, (&fields)[2]);
265-
}
266-
}
267-
}
268-
s
269-
}
270-
271244
pub fn update_target(state: map<string>, id: int, base: string, selector: string) -> map<string> {
272245
ctx_update(state, id, base, selector)
273246
}
@@ -305,6 +278,16 @@ pub fn record_rule_match(state: map<string>, matched: [string]) -> map<string> {
305278
s
306279
}
307280
281+
pub fn apply_detection_paranoia_skip(state: map<string>, id: int, threshold: int, marker: string) -> map<string> {
282+
let mut s = state;
283+
if ctx_get(&s, "blocked") != "1"
284+
&& ctx_get(&s, "skip") == ""
285+
&& number(ctx_get(&s, "tx.detection_paranoia_level"), 1) < threshold {
286+
s = record_rule_match(s, [id + "", "0", "0", "403", marker, ""]);
287+
}
288+
s
289+
}
290+
308291
pub fn apply_rule(state: map<string>, id: int, chain_index: int, has_chain: bool, text: [string], target_count: int, transform_plan: int, score: int, disruptive: bool, status: int) -> map<string> {
309292
let mut s = state;
310293
let operator: int = (target_count % 16384) / 64;

rules/pd_edge_waf.rss

Lines changed: 1302 additions & 37 deletions
Large diffs are not rendered by default.

rules/ruleset.rss

Lines changed: 1288 additions & 6 deletions
Large diffs are not rendered by default.

rules/ruleset_bundle.rss

Lines changed: 1302 additions & 37 deletions
Large diffs are not rendered by default.

tests/smoke.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,11 @@ fn default_ruleset_uses_generated_rules_without_synthetic_attack_probes() {
201201
.expect("ruleset source should be readable");
202202
let engine = std::fs::read_to_string(root.join("rules/engine_bundle.rss"))
203203
.expect("engine bundle should be readable");
204-
assert!(ruleset.contains("apply_rule_blob"));
205-
assert!(ruleset.contains("\\t911100\\t"));
206-
assert!(ruleset.contains("\\t942100\\t"));
207-
assert!(ruleset.contains("\\t949110\\t"));
204+
assert!(!ruleset.contains("apply_rule_blob"));
205+
assert!(!engine.contains("apply_rule_blob"));
206+
assert!(ruleset.contains("apply_rule(next, 911100, 0, false"));
207+
assert!(ruleset.contains("apply_rule(next, 942100, 0, false"));
208+
assert!(ruleset.contains("apply_rule(next, 949110, 0, false"));
208209
assert!(!ruleset.contains("sqli_category_prefilter"));
209210
assert!(!ruleset.contains("sqli_query_rule_match"));
210211
assert!(!engine.contains("sqli_category_prefilter"));
@@ -248,8 +249,8 @@ fn enabled_ruleset_folds_common_exception_updates_into_rule_payloads() {
248249
assert!(!source.contains("fn evaluate_request_999_common_exceptions_after"));
249250
assert!(!source.contains("engine_bundle::update_target(next,"));
250251
assert!(!source.contains("update_target(next, 941100"));
251-
assert!(source.contains("\\t942290\\t"));
252-
assert!(source.contains("\\t409674\\t619\\t"));
253-
assert!(source.contains("\\tREQUEST_COOKIES\\t__gads"));
254-
assert!(!source.contains("\\t!REQUEST_COOKIES\\t"));
252+
assert!(source.contains("apply_rule(next, 942290, 0, false"));
253+
assert!(source.contains("], 409674, 619, 5, false, 403);"));
254+
assert!(source.contains("\"REQUEST_COOKIES\", \"__gads\""));
255+
assert!(!source.contains("\"!REQUEST_COOKIES\""));
255256
}

tools/bundle_engine.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,12 @@ def remove_function(source: str, name: str) -> str:
5252
ruleset = (rules / "ruleset.rss").read_text(encoding="utf-8")
5353
ruleset_lines = [line for line in ruleset.splitlines() if not line.startswith("use ")]
5454
ruleset_body = "\n".join(ruleset_lines).replace("engine_bundle::", "")
55-
for helper in ("set_phase", "apply_action", "component_signature"):
55+
for helper in (
56+
"set_phase",
57+
"apply_action",
58+
"component_signature",
59+
"apply_rule_blob",
60+
):
5661
if re.search(rf"\b{helper}\(", ruleset_body) is None:
5762
engine_bundle = remove_function(engine_bundle, helper)
5863
(rules / "engine_bundle.rss").write_text(engine_bundle, encoding="utf-8")

tools/convert_crs.py

Lines changed: 71 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,19 @@ def render_directive_call(
481481
return f"next = engine_bundle::component_signature(next, {rss_string(directive.value)});"
482482

483483

484-
def guard_rule_call(call: str) -> str:
484+
def is_detection_paranoia_skip(directive: Directive) -> bool:
485485
return (
486-
'if engine_bundle::ctx_get(&next, "blocked") != "1" '
487-
'&& engine_bundle::ctx_get(&next, "skip") == "" { '
488-
f"{call} }}"
486+
directive.kind == "SecRule"
487+
and directive.chain_index == 0
488+
and directive.targets.upper() == "TX:DETECTION_PARANOIA_LEVEL"
489+
and directive.operator == "@lt"
490+
and directive.pattern in {"1", "2", "3", "4"}
491+
and not action_values(directive.actions, "t")
492+
and anomaly_score(directive.actions) == 0
493+
and not has_action(directive.actions, "chain")
494+
and not has_action(directive.actions, "deny")
495+
and directive.message == ""
496+
and action_value(directive.actions, "skipAfter") != ""
489497
)
490498

491499

@@ -494,8 +502,49 @@ def render_entry_directive_call(
494502
data_contents: dict[str, str],
495503
target_updates: dict[int, list[str]],
496504
) -> str:
497-
call = render_directive_call(directive, data_contents, target_updates)
498-
return guard_rule_call(call) if directive.kind == "SecRule" else call
505+
if is_detection_paranoia_skip(directive):
506+
return (
507+
"next = engine_bundle::apply_detection_paranoia_skip("
508+
f"next, {directive.rule_id}, {directive.pattern}, "
509+
f"{rss_string(action_value(directive.actions, 'skipAfter'))});"
510+
)
511+
# apply_rule owns the blocked/skip checks. Repeating them around every
512+
# generated call only adds map lookups and branches to the hot path.
513+
return render_directive_call(directive, data_contents, target_updates)
514+
515+
516+
def render_entry_phase_calls(
517+
directives: list[tuple[Directive, int]],
518+
markers: list[Directive],
519+
data_contents: dict[str, str],
520+
target_updates: dict[int, list[str]],
521+
) -> list[str]:
522+
"""Render a phase while collapsing skipAfter no-op call tails."""
523+
lines: list[str] = []
524+
open_skip_guards = 0
525+
for directive, _ in directives:
526+
lines.append(
527+
" " * open_skip_guards
528+
+ render_entry_directive_call(
529+
directive, data_contents, target_updates
530+
)
531+
)
532+
if directive.kind == "SecRule" and action_value(
533+
directive.actions, "skipAfter"
534+
):
535+
lines.append(
536+
" " * open_skip_guards
537+
+ 'if engine_bundle::ctx_get(&next, "skip") == "" {'
538+
)
539+
open_skip_guards += 1
540+
while open_skip_guards > 0:
541+
open_skip_guards -= 1
542+
lines.append(" " * open_skip_guards + "}")
543+
lines.extend(
544+
render_directive_call(marker, data_contents, target_updates)
545+
for marker in markers
546+
)
547+
return lines
499548

500549

501550
def render_module(
@@ -578,10 +627,10 @@ def render_entry(
578627
if module_name(directive.source) in enabled_categories:
579628
grouped.setdefault(directive.source, []).append(directive)
580629

581-
phase_records: dict[int, list[str]] = {}
630+
phase_sections: dict[int, list[tuple[str, list[str]]]] = {}
582631
lines = [
583632
f"// Executable OWASP CRS {version} ruleset.",
584-
"// Default ModSecurity and CRS rules execute from phase rule blobs.",
633+
"// Default ModSecurity and CRS rules execute as generated phase-specific calls.",
585634
"use engine_bundle;",
586635
"",
587636
]
@@ -611,79 +660,31 @@ def render_entry(
611660
phased_directives.setdefault(effective_phase, []).append(
612661
(directive, effective_paranoia)
613662
)
614-
def encoded_rule_records(
615-
body_directives: list[tuple[Directive, int]], body_markers: list[Directive]
616-
) -> list[str]:
617-
field_separator = "\t"
618-
record_separator = "\r"
619-
rows: list[str] = []
620-
for directive, _ in body_directives:
621-
if directive.kind == "SecRule":
622-
arguments = rule_arguments(
623-
directive, data_contents, target_updates
624-
)
625-
text = json.loads(arguments[3])
626-
fields = [
627-
"R",
628-
category,
629-
arguments[0],
630-
arguments[1],
631-
"1" if arguments[2] == "true" else "0",
632-
arguments[4],
633-
arguments[5],
634-
arguments[6],
635-
"1" if arguments[7] == "true" else "0",
636-
arguments[8],
637-
*text,
638-
]
639-
if any(
640-
field_separator in field or record_separator in field
641-
for field in fields
642-
):
643-
raise ValueError(
644-
f"rule {directive.rule_id} contains reserved blob separator"
645-
)
646-
rows.append(field_separator.join(fields))
647-
for marker in body_markers:
648-
fields = ("M", category, marker.marker)
649-
if any(
650-
field_separator in field or record_separator in field
651-
for field in fields
652-
):
653-
raise ValueError(
654-
f"marker {marker.marker} contains reserved blob separator"
655-
)
656-
rows.append(field_separator.join(fields))
657-
return rows
658663

659664
for phase, phase_directives in phased_directives.items():
660-
phase_records.setdefault(phase, []).extend(
661-
encoded_rule_records(phase_directives, markers)
665+
calls = render_entry_phase_calls(
666+
phase_directives, markers, data_contents, target_updates
662667
)
668+
phase_sections.setdefault(phase, []).append((category, calls))
663669

664-
def phase_blob(phase: int) -> str:
665-
return "\r".join(phase_records.get(phase, []))
666-
667-
lines.append("pub fn inspect_request(next: map<string>) -> map<string> {")
668-
for phase in (1, 2):
670+
def append_phase(phase: int) -> None:
669671
lines.append(f" next = engine_bundle::ctx_set_phase(next, {phase});")
670-
blob = phase_blob(phase)
671-
if blob:
672+
for category, calls in phase_sections.get(phase, []):
672673
lines.append(
673-
" next = engine_bundle::apply_rule_blob("
674-
f"next, {rss_string(blob)});"
674+
" if engine_bundle::category_enabled("
675+
f'&next, "{category}") {{'
675676
)
677+
lines.extend(f" {call}" for call in calls)
678+
lines.append(" }")
679+
680+
lines.append("pub fn inspect_request(next: map<string>) -> map<string> {")
681+
for phase in (1, 2):
682+
append_phase(phase)
676683
lines.extend([" next", "}", ""])
677684

678685
lines.append("pub fn inspect_response(next: map<string>) -> map<string> {")
679686
for phase in (3, 4, 5):
680-
lines.append(f" next = engine_bundle::ctx_set_phase(next, {phase});")
681-
blob = phase_blob(phase)
682-
if blob:
683-
lines.append(
684-
" next = engine_bundle::apply_rule_blob("
685-
f"next, {rss_string(blob)});"
686-
)
687+
append_phase(phase)
687688
lines.extend(
688689
[
689690
" next",

0 commit comments

Comments
 (0)