Path: blob/dev/pkg/fuzz/analyzers/time/time_delay_test.go
2070 views
// Tests ported from ZAP Java version of the algorithm12package time34import (5"math/rand"6"reflect"7"testing"8"time"9)1011// This test suite verifies the timing dependency detection algorithm by testing various scenarios:12//13// Test Categories:14// 1. Perfect Linear Cases15// - TestPerfectLinear: Basic case with slope=1, no noise16// - TestPerfectLinearSlopeOne_NoNoise: Similar to above but with different parameters17// - TestPerfectLinearSlopeTwo_NoNoise: Tests detection of slope=2 relationship18//19// 2. Noisy Cases20// - TestLinearWithNoise: Verifies detection works with moderate noise (±0.2s)21// - TestNoisyLinear: Similar but with different noise parameters22// - TestHighNoiseConcealsSlope: Verifies detection fails with extreme noise (±5s)23//24// 3. No Correlation Cases25// - TestNoCorrelation: Basic case where delay has no effect26// - TestNoCorrelationHighBaseline: High baseline (~15s) masks any delay effect27// - TestNegativeSlopeScenario: Verifies detection rejects negative correlations28//29// 4. Edge Cases30// - TestMinimalData: Tests behavior with minimal data points (2 requests)31// - TestLargeNumberOfRequests: Tests stability with many data points (20 requests)32// - TestChangingBaseline: Tests detection with shifting baseline mid-test33// - TestHighBaselineLowSlope: Tests detection of subtle correlations (slope=0.85)34//35// ZAP Test Cases:36//37// 1. Alternating Sequence Tests38// - TestAlternatingSequences: Verifies correct alternation between high and low delays39//40// 2. Non-Injectable Cases41// - TestNonInjectableQuickFail: Tests quick failure when response time < requested delay42// - TestSlowNonInjectableCase: Tests early termination with consistently high response times43// - TestRealWorldNonInjectableCase: Tests behavior with real-world response patterns44//45// 3. Error Tolerance Tests46// - TestSmallErrorDependence: Verifies detection works with small random variations47//48// Key Parameters Tested:49// - requestsLimit: Number of requests to make (2-20)50// - highSleepTimeSeconds: Maximum delay to test (typically 5s)51// - correlationErrorRange: Acceptable deviation from perfect correlation (0.05-0.3)52// - slopeErrorRange: Acceptable deviation from expected slope (0.1-1.5)53//54// The test suite uses various mock senders (perfectLinearSender, noCorrelationSender, etc.)55// to simulate different timing behaviors and verify the detection algorithm works correctly56// across a wide range of scenarios.5758// Mock request sender that simulates a perfect linear relationship:59// Observed delay = baseline + requested_delay60func perfectLinearSender(baseline float64) func(delay int) (float64, error) {61return func(delay int) (float64, error) {62// simulate some processing time63time.Sleep(10 * time.Millisecond) // just a small artificial sleep to mimic network64return baseline + float64(delay), nil65}66}6768// Mock request sender that simulates no correlation:69// The response time is random around a certain constant baseline, ignoring requested delay.70func noCorrelationSender(baseline, noiseAmplitude float64) func(int) (float64, error) {71return func(delay int) (float64, error) {72time.Sleep(10 * time.Millisecond)73noise := 0.074if noiseAmplitude > 0 {75noise = (rand.Float64()*2 - 1) * noiseAmplitude76}77return baseline + noise, nil78}79}8081// Mock request sender that simulates partial linearity but with some noise.82func noisyLinearSender(baseline float64) func(delay int) (float64, error) {83return func(delay int) (float64, error) {84time.Sleep(10 * time.Millisecond)85// Add some noise (±0.2s) to a linear relationship86noise := 0.287return baseline + float64(delay) + noise, nil88}89}9091func TestPerfectLinear(t *testing.T) {92// Expect near-perfect correlation and slope ~ 1.093requestsLimit := 6 // 3 pairs: enough data for stable regression94highSleepTimeSeconds := 595corrErrRange := 0.196slopeErrRange := 0.297baseline := 5.09899sender := perfectLinearSender(5.0) // baseline 5s, observed = 5s + requested_delay100match, reason, err := checkTimingDependency(101requestsLimit,102highSleepTimeSeconds,103corrErrRange,104slopeErrRange,105baseline,106sender,107)108if err != nil {109t.Fatalf("Unexpected error: %v", err)110}111if !match {112t.Fatalf("Expected a match but got none. Reason: %s", reason)113}114}115116func TestNoCorrelation(t *testing.T) {117// Expect no match because requested delay doesn't influence observed delay118requestsLimit := 6119highSleepTimeSeconds := 5120corrErrRange := 0.1121slopeErrRange := 0.5122baseline := 8.0123124sender := noCorrelationSender(8.0, 0.1)125match, reason, err := checkTimingDependency(126requestsLimit,127highSleepTimeSeconds,128corrErrRange,129slopeErrRange,130baseline,131sender,132)133if err != nil {134t.Fatalf("Unexpected error: %v", err)135}136if match {137t.Fatalf("Expected no match but got one. Reason: %s", reason)138}139}140141func TestNoisyLinear(t *testing.T) {142// Even with some noise, it should detect a strong positive correlation if143// we allow a slightly bigger margin for slope/correlation.144requestsLimit := 10 // More requests to average out noise145highSleepTimeSeconds := 5146corrErrRange := 0.2 // allow some lower correlation due to noise147slopeErrRange := 0.5 // slope may deviate slightly148baseline := 2.0149150sender := noisyLinearSender(2.0) // baseline 2s, observed ~ 2s + requested_delay ±0.2151match, reason, err := checkTimingDependency(152requestsLimit,153highSleepTimeSeconds,154corrErrRange,155slopeErrRange,156baseline,157sender,158)159if err != nil {160t.Fatalf("Unexpected error: %v", err)161}162163// We expect a match since it's still roughly linear. The slope should be close to 1.164if !match {165t.Fatalf("Expected a match in noisy linear test but got none. Reason: %s", reason)166}167}168169func TestMinimalData(t *testing.T) {170// With too few requests, correlation might not be stable.171// Here, we send only 2 requests (1 pair) and see if the logic handles it gracefully.172requestsLimit := 2173highSleepTimeSeconds := 5174corrErrRange := 0.3175slopeErrRange := 0.5176baseline := 5.0177178// Perfect linear sender again179sender := perfectLinearSender(5.0)180match, reason, err := checkTimingDependency(181requestsLimit,182highSleepTimeSeconds,183corrErrRange,184slopeErrRange,185baseline,186sender,187)188if err != nil {189t.Fatalf("Unexpected error: %v", err)190}191if !match {192t.Fatalf("Expected match but got none. Reason: %s", reason)193}194}195196// Utility functions to generate different behaviors197198// linearSender returns a sender that calculates observed delay as:199// observed = baseline + slope * requested_delay + noise200func linearSender(baseline, slope, noiseAmplitude float64) func(int) (float64, error) {201return func(delay int) (float64, error) {202time.Sleep(10 * time.Millisecond)203noise := 0.0204if noiseAmplitude > 0 {205noise = (rand.Float64()*2 - 1) * noiseAmplitude // random noise in [-noiseAmplitude, noiseAmplitude]206}207return baseline + slope*float64(delay) + noise, nil208}209}210211// negativeSlopeSender just for completeness - higher delay = less observed time212func negativeSlopeSender(baseline float64) func(int) (float64, error) {213return func(delay int) (float64, error) {214time.Sleep(10 * time.Millisecond)215return baseline - float64(delay)*2.0, nil216}217}218219func TestPerfectLinearSlopeOne_NoNoise(t *testing.T) {220baseline := 2.0221match, reason, err := checkTimingDependency(22210, // requestsLimit2235, // highSleepTimeSeconds2240.1, // correlationErrorRange2250.2, // slopeErrorRange (allowing slope between 0.8 and 1.2)226baseline,227linearSender(baseline, 1.0, 0.0),228)229if err != nil {230t.Fatalf("Unexpected error: %v", err)231}232if !match {233t.Fatalf("Expected a match for perfect linear slope=1. Reason: %s", reason)234}235}236237func TestPerfectLinearSlopeTwo_NoNoise(t *testing.T) {238baseline := 2.0239// slope=2 means observed = baseline + 2*requested_delay240match, reason, err := checkTimingDependency(24110,2425,2430.1, // correlation must still be good2441.5, // allow slope in range (0.5 to 2.5), we should be close to 2.0 anyway245baseline,246linearSender(baseline, 2.0, 0.0),247)248if err != nil {249t.Fatalf("Error: %v", err)250}251if !match {252t.Fatalf("Expected a match for slope=2. Reason: %s", reason)253}254}255256func TestLinearWithNoise(t *testing.T) {257baseline := 5.0258// slope=1 but with noise ±0.2 seconds259match, reason, err := checkTimingDependency(26012,2615,2620.2, // correlationErrorRange relaxed to account for noise2630.5, // slopeErrorRange also relaxed264baseline,265linearSender(baseline, 1.0, 0.2),266)267if err != nil {268t.Fatalf("Error: %v", err)269}270if !match {271t.Fatalf("Expected a match for noisy linear data. Reason: %s", reason)272}273}274275func TestNoCorrelationHighBaseline(t *testing.T) {276baseline := 15.0277// baseline ~15s, requested delays won't matter278match, reason, err := checkTimingDependency(27910,2805,2810.1, // correlation should be near zero, so no match expected2820.5,283baseline,284noCorrelationSender(baseline, 0.1),285)286if err != nil {287t.Fatalf("Error: %v", err)288}289if match {290t.Fatalf("Expected no match for no correlation scenario. Got: %s", reason)291}292}293294func TestNegativeSlopeScenario(t *testing.T) {295baseline := 10.0296// Increasing delay decreases observed time297match, reason, err := checkTimingDependency(29810,2995,3000.2,3010.5,302baseline,303negativeSlopeSender(baseline),304)305if err != nil {306t.Fatalf("Error: %v", err)307}308if match {309t.Fatalf("Expected no match in negative slope scenario. Reason: %s", reason)310}311}312313func TestLargeNumberOfRequests(t *testing.T) {314baseline := 1.0315// 20 requests, slope=1.0, no noise. Should be very stable and produce a very high correlation.316match, reason, err := checkTimingDependency(31720,3185,3190.05, // very strict correlation requirement3200.1, // very strict slope range321baseline,322linearSender(baseline, 1.0, 0.0),323)324if err != nil {325t.Fatalf("Error: %v", err)326}327if !match {328t.Fatalf("Expected a strong match with many requests and perfect linearity. Reason: %s", reason)329}330}331332func TestHighBaselineLowSlope(t *testing.T) {333baseline := 15.0334match, reason, err := checkTimingDependency(33510,3365,3370.2,3380.2, // expecting slope around 0.5, allow range ~0.4 to 0.6339baseline,340linearSender(baseline, 0.85, 0.0),341)342if err != nil {343t.Fatalf("Error: %v", err)344}345if !match {346t.Fatalf("Expected a match for slope=0.5 linear scenario. Reason: %s", reason)347}348}349350func TestHighNoiseConcealsSlope(t *testing.T) {351baseline := 5.0352// slope=1, but noise=5 seconds is huge and might conceal the correlation.353// With large noise, the test may fail to detect correlation.354match, reason, err := checkTimingDependency(35512,3565,3570.1, // still strict3580.2, // still strict359baseline,360linearSender(baseline, 1.0, 5.0),361)362if err != nil {363t.Fatalf("Error: %v", err)364}365// Expect no match because the noise level is too high to establish a reliable correlation.366if match {367t.Fatalf("Expected no match due to extreme noise. Reason: %s", reason)368}369}370371func TestAlternatingSequences(t *testing.T) {372baseline := 0.0373var generatedDelays []float64374reqSender := func(delay int) (float64, error) {375generatedDelays = append(generatedDelays, float64(delay))376return float64(delay), nil377}378match, reason, err := checkTimingDependency(3794, // requestsLimit38015, // highSleepTimeSeconds3810.1, // correlationErrorRange3820.2, // slopeErrorRange383baseline,384reqSender,385)386if err != nil {387t.Fatalf("Unexpected error: %v", err)388}389if !match {390t.Fatalf("Expected a match but got none. Reason: %s", reason)391}392// Verify alternating sequence of delays393expectedDelays := []float64{15, 3, 15, 3}394if !reflect.DeepEqual(generatedDelays, expectedDelays) {395t.Fatalf("Expected delays %v but got %v", expectedDelays, generatedDelays)396}397}398399func TestNonInjectableQuickFail(t *testing.T) {400baseline := 0.5401var timesCalled int402reqSender := func(delay int) (float64, error) {403timesCalled++404return 0.5, nil // Return value less than delay405}406match, _, err := checkTimingDependency(4074, // requestsLimit40815, // highSleepTimeSeconds4090.1, // correlationErrorRange4100.2, // slopeErrorRange411baseline,412reqSender,413)414if err != nil {415t.Fatalf("Unexpected error: %v", err)416}417if match {418t.Fatal("Expected no match for non-injectable case")419}420if timesCalled != 1 {421t.Fatalf("Expected quick fail after 1 call, got %d calls", timesCalled)422}423}424425func TestSlowNonInjectableCase(t *testing.T) {426baseline := 10.0427rng := rand.New(rand.NewSource(time.Now().UnixNano()))428var timesCalled int429reqSender := func(delay int) (float64, error) {430timesCalled++431return 10 + rng.Float64()*0.5, nil432}433match, _, err := checkTimingDependency(4344, // requestsLimit43515, // highSleepTimeSeconds4360.1, // correlationErrorRange4370.2, // slopeErrorRange438baseline,439reqSender,440)441if err != nil {442t.Fatalf("Unexpected error: %v", err)443}444if match {445t.Fatal("Expected no match for slow non-injectable case")446}447if timesCalled > 3 {448t.Fatalf("Expected early termination (≤3 calls), got %d calls", timesCalled)449}450}451452func TestRealWorldNonInjectableCase(t *testing.T) {453baseline := 0.0454var iteration int455counts := []float64{11, 21, 11, 21, 11}456reqSender := func(delay int) (float64, error) {457iteration++458return counts[iteration-1], nil459}460match, _, err := checkTimingDependency(4614, // requestsLimit46215, // highSleepTimeSeconds4630.1, // correlationErrorRange4640.2, // slopeErrorRange465baseline,466reqSender,467)468if err != nil {469t.Fatalf("Unexpected error: %v", err)470}471if match {472t.Fatal("Expected no match for real-world non-injectable case")473}474if iteration > 4 {475t.Fatalf("Expected ≤4 iterations, got %d", iteration)476}477}478479func TestSmallErrorDependence(t *testing.T) {480baseline := 0.0481rng := rand.New(rand.NewSource(time.Now().UnixNano()))482reqSender := func(delay int) (float64, error) {483return float64(delay) + rng.Float64()*0.5, nil484}485match, reason, err := checkTimingDependency(4864, // requestsLimit48715, // highSleepTimeSeconds4880.1, // correlationErrorRange4890.2, // slopeErrorRange490baseline,491reqSender,492)493if err != nil {494t.Fatalf("Unexpected error: %v", err)495}496if !match {497t.Fatalf("Expected match for small error case. Reason: %s", reason)498}499}500501502