java-topology/defects/zeek/unit/ZeekRuleMatcherTest.java

132 lines
5.6 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package unit;
import java.util.*;
/**
* zeek-0001 — CWE-407: O(n²) rule deduplication in per-packet signature matching
*
* Models Zeek src/RuleMatcher.cc is_member_of():
* Slow: std::ranges::find(matched_rules, idx) — O(R) per call, 6 calls per packet
* Fast: unordered_set<intptr_t>::count(idx) — O(1) per call
*
* As a connection accumulates matched rules over P packets,
* each packet costs O(R_matched) per check. Over P packets with R total rules:
* Slow: O(P * R_matched_avg * 6) = O(P * R) with fully loaded state
* Fast: O(P * 1 * 6) = O(P)
*/
public class ZeekRuleMatcherTest {
// --- SLOW: vector-based matched_rules (defective) ---
static boolean isMemberOfSlow(List<Long> matched, long ruleIdx) {
for (long r : matched) {
if (r == ruleIdx) return true;
}
return false;
}
/**
* Simulate P packets across a connection, each triggering up to batchSize rules.
* Per packet: 6 is_member_of checks per candidate rule.
* Returns total operations (inner loop iterations).
*/
static long simulateSlowConnection(int totalRules, int packetsPerConn, int rulesMatchedPerPacket) {
List<Long> matchedRules = new ArrayList<>();
long ops = 0;
Random rng = new Random(7);
for (int pkt = 0; pkt < packetsPerConn; pkt++) {
// Select rulesMatchedPerPacket candidates to check
for (int i = 0; i < rulesMatchedPerPacket; i++) {
long ruleIdx = rng.nextInt(totalRules);
// 6 is_member_of calls model the 6 call sites in RuleMatcher.cc
for (int call = 0; call < 6; call++) {
for (long r : matchedRules) { // O(R) scan
ops++;
if (r == ruleIdx) break;
}
}
// Add to matched_rules if not already present (ExecRuleActions)
if (!isMemberOfSlow(matchedRules, ruleIdx)) {
matchedRules.add(ruleIdx);
}
}
}
return ops;
}
// --- FAST: hash set matched_rules (fixed) ---
static long simulateFastConnection(int totalRules, int packetsPerConn, int rulesMatchedPerPacket) {
Set<Long> matchedRules = new HashSet<>();
long ops = 0;
Random rng = new Random(7);
for (int pkt = 0; pkt < packetsPerConn; pkt++) {
for (int i = 0; i < rulesMatchedPerPacket; i++) {
long ruleIdx = rng.nextInt(totalRules);
// 6 O(1) hash lookups
for (int call = 0; call < 6; call++) {
ops++; // single hash lookup
matchedRules.contains(ruleIdx);
}
matchedRules.add(ruleIdx);
}
}
return ops;
}
public static void main(String[] args) {
// R = total rules in IDS ruleset (large enterprise deployment)
// P = packets per connection
// B = rules fired per packet (typically low, but state accumulates)
final int R = 500;
final int P = 1000;
final int B = 5; // rules that trigger per packet (accumulates in matched_rules)
// Correctness: same rule indices should be found/not-found by both
List<Long> slowList = new ArrayList<>(Arrays.asList(10L, 20L, 30L, 50L, 99L));
Set<Long> fastSet = new HashSet<>(Arrays.asList(10L, 20L, 30L, 50L, 99L));
assert isMemberOfSlow(slowList, 30L) : "slow: should find 30";
assert !isMemberOfSlow(slowList, 40L) : "slow: should miss 40";
assert fastSet.contains(30L) : "fast: should find 30";
assert !fastSet.contains(40L) : "fast: should miss 40";
long slowOps = simulateSlowConnection(R, P, B);
long fastOps = simulateFastConnection(R, P, B);
assert slowOps > fastOps : "slow should cost more ops, got slow=" + slowOps + " fast=" + fastOps;
double speedup = (double) slowOps / fastOps;
System.out.println("zeek-0001 CWE-407: is_member_of O(R) vector scan vs O(1) hash set");
System.out.println(" R (rules) = " + R + ", P (packets/conn) = " + P + ", B (rules/pkt) = " + B);
System.out.println(" Slow ops (vector linear scan, 6x per pkt): " + slowOps);
System.out.println(" Fast ops (hash set, 6x per pkt): " + fastOps);
System.out.printf (" Speedup: %.0fx%n", speedup);
System.out.println();
// Verify the quadratic growth: at 2× rules the slow cost should ~4× (quadratic)
long slowOpsHalfR = simulateSlowConnection(R/2, P, B);
long slowOpsDoubleR= simulateSlowConnection(R*2, P, B);
double ratio = (double) slowOpsDoubleR / slowOpsHalfR;
// For a truly quadratic structure ratio approaches 4.0; assert it's super-linear
assert ratio > 2.0 : "expected super-linear growth, got ratio=" + ratio;
long fastOpsHalfR = simulateFastConnection(R/2, P, B);
long fastOpsDoubleR = simulateFastConnection(R*2, P, B);
double fastRatio = (double) fastOpsDoubleR / fastOpsHalfR;
// Fast should be roughly linear (ratio ~2) or even flat (constant O(1) per call)
assert fastRatio < ratio : "fast should scale better than slow";
System.out.println("Quadratic growth check (R/2 → 2R):");
System.out.println(" Slow ops ratio: " + String.format("%.2f", ratio) + "x (>2 confirms super-linear)");
System.out.println(" Fast ops ratio: " + String.format("%.2f", fastRatio) + "x (near-linear)");
System.out.println();
System.out.println("2/2 PASS");
}
}