132 lines
5.6 KiB
Java
132 lines
5.6 KiB
Java
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");
|
||
}
|
||
}
|