github.com/chainreactors/fingers@v1.2.1/fingers/optimization_bench_test.go (about)

     1  package fingers
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"regexp"
    12  	"runtime"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/chainreactors/utils/ahocorasick"
    18  	re2 "github.com/wasilibs/go-re2"
    19  )
    20  
    21  // ──────────────────────────────────────────────────────────────
    22  // 1. Pattern Statistics — understand what we're optimizing
    23  // ──────────────────────────────────────────────────────────────
    24  
    25  func TestFingerPatternStatistics(t *testing.T) {
    26  	engine := newPerfEngine(t)
    27  
    28  	var totalBody, totalHeader, totalRegexp, totalVuln, totalVersion, totalMD5, totalMMH3, totalCert int
    29  	var totalFingers, totalRules int
    30  	var fingersWithBody, fingersWithHeader, fingersWithRegexp int
    31  
    32  	for _, finger := range engine.HTTPFingers {
    33  		totalFingers++
    34  		hasBody, hasHeader, hasRegexp := false, false, false
    35  		for _, rule := range finger.Rules {
    36  			totalRules++
    37  			if rule.Regexps == nil {
    38  				continue
    39  			}
    40  			totalBody += len(rule.Regexps.Body)
    41  			totalHeader += len(rule.Regexps.Header)
    42  			totalRegexp += len(rule.Regexps.Regexp)
    43  			totalVuln += len(rule.Regexps.Vuln)
    44  			totalVersion += len(rule.Regexps.Version)
    45  			totalMD5 += len(rule.Regexps.MD5)
    46  			totalMMH3 += len(rule.Regexps.MMH3)
    47  			totalCert += len(rule.Regexps.Cert)
    48  
    49  			if len(rule.Regexps.Body) > 0 {
    50  				hasBody = true
    51  			}
    52  			if len(rule.Regexps.Header) > 0 {
    53  				hasHeader = true
    54  			}
    55  			if len(rule.Regexps.Regexp) > 0 || len(rule.Regexps.Vuln) > 0 {
    56  				hasRegexp = true
    57  			}
    58  		}
    59  		if hasBody {
    60  			fingersWithBody++
    61  		}
    62  		if hasHeader {
    63  			fingersWithHeader++
    64  		}
    65  		if hasRegexp {
    66  			fingersWithRegexp++
    67  		}
    68  	}
    69  
    70  	t.Logf("=== HTTP Fingerprint Pattern Statistics ===")
    71  	t.Logf("Total HTTP fingers:    %d", totalFingers)
    72  	t.Logf("Total rules:           %d", totalRules)
    73  	t.Logf("")
    74  	t.Logf("Body keywords:         %d  (fingers with body: %d)", totalBody, fingersWithBody)
    75  	t.Logf("Header keywords:       %d  (fingers with header: %d)", totalHeader, fingersWithHeader)
    76  	t.Logf("Regexp patterns:       %d  (fingers with regexp: %d)", totalRegexp, fingersWithRegexp)
    77  	t.Logf("Vuln regexp patterns:  %d", totalVuln)
    78  	t.Logf("Version patterns:      %d", totalVersion)
    79  	t.Logf("MD5 hashes:            %d", totalMD5)
    80  	t.Logf("MMH3 hashes:           %d", totalMMH3)
    81  	t.Logf("Cert patterns:         %d", totalCert)
    82  	t.Logf("")
    83  	t.Logf("AC candidate keywords (body+header): %d", totalBody+totalHeader)
    84  	t.Logf("RE2 candidate patterns (regexp+vuln+version): %d", totalRegexp+totalVuln+totalVersion)
    85  }
    86  
    87  // ──────────────────────────────────────────────────────────────
    88  // 2. Real website data collection & helpers
    89  // ──────────────────────────────────────────────────────────────
    90  
    91  type realSiteData struct {
    92  	Name    string
    93  	Content []byte
    94  }
    95  
    96  func fetchRealSite(url string) ([]byte, error) {
    97  	client := &http.Client{Timeout: 10 * time.Second}
    98  	resp, err := client.Get(url)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	defer resp.Body.Close()
   103  
   104  	var buf bytes.Buffer
   105  	fmt.Fprintf(&buf, "%s %s\r\n", resp.Proto, resp.Status)
   106  	for key, values := range resp.Header {
   107  		for _, v := range values {
   108  			fmt.Fprintf(&buf, "%s: %s\r\n", key, v)
   109  		}
   110  	}
   111  	buf.WriteString("\r\n")
   112  	io.Copy(&buf, resp.Body)
   113  	return buf.Bytes(), nil
   114  }
   115  
   116  var testSites = []struct {
   117  	Name string
   118  	URL  string
   119  }{
   120  	{"nginx-default", "http://nginx.org"},
   121  	{"apache-httpd", "http://httpd.apache.org"},
   122  	{"github", "https://github.com"},
   123  	{"cloudflare", "https://www.cloudflare.com"},
   124  	{"wordpress", "https://wordpress.org"},
   125  	{"jenkins", "https://www.jenkins.io"},
   126  	{"grafana", "https://grafana.com"},
   127  	{"elastic", "https://www.elastic.co"},
   128  }
   129  
   130  func loadTestSites(t *testing.T) []realSiteData {
   131  	t.Helper()
   132  	cacheFile := "/tmp/fingers_bench_sites.json.gz"
   133  
   134  	if data, err := os.ReadFile(cacheFile); err == nil {
   135  		gr, err := gzip.NewReader(bytes.NewReader(data))
   136  		if err == nil {
   137  			defer gr.Close()
   138  			var sites []realSiteData
   139  			if err := json.NewDecoder(gr).Decode(&sites); err == nil && len(sites) > 0 {
   140  				t.Logf("loaded %d cached sites from %s", len(sites), cacheFile)
   141  				return sites
   142  			}
   143  		}
   144  	}
   145  
   146  	var sites []realSiteData
   147  	for _, s := range testSites {
   148  		content, err := fetchRealSite(s.URL)
   149  		if err != nil {
   150  			t.Logf("SKIP %s: %v", s.Name, err)
   151  			continue
   152  		}
   153  		sites = append(sites, realSiteData{Name: s.Name, Content: content})
   154  		t.Logf("fetched %s: %d bytes", s.Name, len(content))
   155  	}
   156  
   157  	if len(sites) > 0 {
   158  		var buf bytes.Buffer
   159  		gw := gzip.NewWriter(&buf)
   160  		json.NewEncoder(gw).Encode(sites)
   161  		gw.Close()
   162  		os.WriteFile(cacheFile, buf.Bytes(), 0644)
   163  	}
   164  
   165  	return sites
   166  }
   167  
   168  func generateSyntheticResponses() []realSiteData {
   169  	return []realSiteData{
   170  		{
   171  			Name: "typical-nginx",
   172  			Content: []byte("HTTP/1.1 200 OK\r\nServer: nginx/1.18.0\r\nContent-Type: text/html\r\nX-Powered-By: PHP/7.4.3\r\n\r\n<!DOCTYPE html><html><head><title>Welcome to nginx!</title></head><body><h1>Welcome to nginx!</h1><p>If you see this page, the nginx web server is successfully installed.</p></body></html>"),
   173  		},
   174  		{
   175  			Name: "wordpress-site",
   176  			Content: []byte("HTTP/1.1 200 OK\r\nServer: Apache/2.4.41\r\nX-Powered-By: PHP/7.4.3\r\nLink: <https://example.com/wp-json/>; rel=\"https://api.w.org/\"\r\n\r\n<!DOCTYPE html><html lang=\"en-US\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link rel=\"stylesheet\" href=\"/wp-content/themes/flavor/style.css\"><script src=\"/wp-includes/js/jquery/jquery.min.js\"></script></head><body class=\"home blog wp-custom-logo\"><div id=\"page\"><header><h1 class=\"site-title\"><a href=\"/\">Example Blog</a></h1></header><main><article class=\"post\"><h2>Hello World</h2><p>Welcome to WordPress.</p></article></main><footer><p>Powered by WordPress</p></footer></div></body></html>"),
   177  		},
   178  		{
   179  			Name: "spring-boot-app",
   180  			Content: []byte("HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nContent-Type: application/json\r\nX-Application-Context: application:8080\r\n\r\n{\"status\":\"UP\",\"components\":{\"db\":{\"status\":\"UP\",\"details\":{\"database\":\"MySQL\",\"validationQuery\":\"isValid()\"}},\"diskSpace\":{\"status\":\"UP\",\"details\":{\"total\":107374182400,\"free\":53687091200,\"threshold\":10485760,\"path\":\"/opt/app/.\",\"exists\":true}}},\"groups\":[\"liveness\",\"readiness\"]}"),
   181  		},
   182  		{
   183  			Name: "tomcat-default",
   184  			Content: []byte("HTTP/1.1 200 OK\r\nServer: Apache-Coyote/1.1\r\nContent-Type: text/html;charset=UTF-8\r\n\r\n<!DOCTYPE html><html lang=\"en\"><head><title>Apache Tomcat/9.0.50</title></head><body><h1>Apache Tomcat/9.0.50</h1><p>If you're seeing this, you've successfully installed Tomcat.</p><ul><li><a href=\"/manager/html\">Server Status</a></li><li><a href=\"/host-manager/html\">Host Manager</a></li></ul></body></html>"),
   185  		},
   186  		{
   187  			Name: "iis-aspnet",
   188  			Content: []byte("HTTP/1.1 200 OK\r\nServer: Microsoft-IIS/10.0\r\nX-Powered-By: ASP.NET\r\nX-AspNet-Version: 4.0.30319\r\n\r\n<!DOCTYPE html><html><head><title>IIS Windows Server</title></head><body><img src=\"iisstart.png\" alt=\"IIS\"><h1>Internet Information Services</h1></body></html>"),
   189  		},
   190  		{
   191  			Name: "large-react-app",
   192  			Content: generateLargeResponse(64 * 1024),
   193  		},
   194  		{
   195  			Name: "very-large-response",
   196  			Content: generateLargeResponse(256 * 1024),
   197  		},
   198  		{
   199  			Name: "empty-body",
   200  			Content: []byte("HTTP/1.1 302 Found\r\nServer: cloudflare\r\nLocation: /login\r\nCF-RAY: abc123\r\n\r\n"),
   201  		},
   202  	}
   203  }
   204  
   205  func generateLargeResponse(bodySize int) []byte {
   206  	var buf bytes.Buffer
   207  	buf.WriteString("HTTP/1.1 200 OK\r\nServer: nginx/1.21.0\r\nContent-Type: text/html\r\nX-Powered-By: Express\r\nSet-Cookie: session=abc123; Path=/; HttpOnly\r\n\r\n")
   208  	buf.WriteString("<!DOCTYPE html><html><head><title>React App</title>")
   209  	buf.WriteString("<script src=\"/static/js/main.chunk.js\"></script>")
   210  	buf.WriteString("<link rel=\"stylesheet\" href=\"/static/css/main.chunk.css\">")
   211  	buf.WriteString("</head><body><div id=\"root\">")
   212  
   213  	filler := "<div class=\"component\"><span>Content block with various text including keywords that may partially match fingerprints like powered-by, server-info, and x-generator headers embedded in HTML.</span></div>\n"
   214  	for buf.Len() < bodySize {
   215  		buf.WriteString(filler)
   216  	}
   217  	buf.WriteString("</div></body></html>")
   218  	return buf.Bytes()
   219  }
   220  
   221  // ──────────────────────────────────────────────────────────────
   222  // 3. Aho-Corasick Optimization Prototype
   223  // ──────────────────────────────────────────────────────────────
   224  
   225  type acIndex struct {
   226  	bodyAC         *ahocorasick.Automaton
   227  	headerAC       *ahocorasick.Automaton
   228  	bodyPatterns   []acPatternRef
   229  	headerPatterns []acPatternRef
   230  }
   231  
   232  type acPatternRef struct {
   233  	FingerIdx  int
   234  	RuleIdx    int
   235  	PatternIdx int
   236  	Pattern    string
   237  }
   238  
   239  func buildACIndex(fingers Fingers) *acIndex {
   240  	idx := &acIndex{}
   241  
   242  	var bodyKeywords []string
   243  	var headerKeywords []string
   244  
   245  	for fi, finger := range fingers {
   246  		for ri, rule := range finger.Rules {
   247  			if rule.Regexps == nil {
   248  				continue
   249  			}
   250  			for pi, body := range rule.Regexps.Body {
   251  				bodyKeywords = append(bodyKeywords, body)
   252  				idx.bodyPatterns = append(idx.bodyPatterns, acPatternRef{
   253  					FingerIdx:  fi,
   254  					RuleIdx:    ri,
   255  					PatternIdx: pi,
   256  					Pattern:    body,
   257  				})
   258  			}
   259  			for pi, header := range rule.Regexps.Header {
   260  				headerKeywords = append(headerKeywords, header)
   261  				idx.headerPatterns = append(idx.headerPatterns, acPatternRef{
   262  					FingerIdx:  fi,
   263  					RuleIdx:    ri,
   264  					PatternIdx: pi,
   265  					Pattern:    header,
   266  				})
   267  			}
   268  		}
   269  	}
   270  
   271  	if len(bodyKeywords) > 0 {
   272  		ac, err := ahocorasick.NewBuilder().
   273  			AddStrings(bodyKeywords).
   274  			Build()
   275  		if err == nil {
   276  			idx.bodyAC = ac
   277  		}
   278  	}
   279  
   280  	if len(headerKeywords) > 0 {
   281  		ac, err := ahocorasick.NewBuilder().
   282  			AddStrings(headerKeywords).
   283  			Build()
   284  		if err == nil {
   285  			idx.headerAC = ac
   286  		}
   287  	}
   288  
   289  	return idx
   290  }
   291  
   292  type acMatchResult struct {
   293  	FingerIdx  int
   294  	RuleIdx    int
   295  	PatternIdx int
   296  	MatchType  string
   297  }
   298  
   299  func (idx *acIndex) matchKeywords(header, body []byte) []acMatchResult {
   300  	var results []acMatchResult
   301  
   302  	if idx.headerAC != nil && len(header) > 0 {
   303  		matches := idx.headerAC.FindAll(header, -1)
   304  		for _, m := range matches {
   305  			ref := idx.headerPatterns[m.PatternID]
   306  			results = append(results, acMatchResult{
   307  				FingerIdx:  ref.FingerIdx,
   308  				RuleIdx:    ref.RuleIdx,
   309  				PatternIdx: ref.PatternIdx,
   310  				MatchType:  "header",
   311  			})
   312  		}
   313  	}
   314  
   315  	if idx.bodyAC != nil && len(body) > 0 {
   316  		matches := idx.bodyAC.FindAll(body, -1)
   317  		for _, m := range matches {
   318  			ref := idx.bodyPatterns[m.PatternID]
   319  			results = append(results, acMatchResult{
   320  				FingerIdx:  ref.FingerIdx,
   321  				RuleIdx:    ref.RuleIdx,
   322  				PatternIdx: ref.PatternIdx,
   323  				MatchType:  "body",
   324  			})
   325  		}
   326  	}
   327  
   328  	return results
   329  }
   330  
   331  // ──────────────────────────────────────────────────────────────
   332  // 4. Baseline vs AC keyword matching benchmark
   333  // ──────────────────────────────────────────────────────────────
   334  
   335  func TestBaselineVsAC_Keywords(t *testing.T) {
   336  	if testing.Short() {
   337  		t.Skip("skipping heavy performance comparison in short mode")
   338  	}
   339  	engine := newPerfEngine(t)
   340  
   341  	sites := generateSyntheticResponses()
   342  	realSites := loadTestSites(t)
   343  	sites = append(sites, realSites...)
   344  
   345  	if len(sites) == 0 {
   346  		t.Fatal("no test data available")
   347  	}
   348  
   349  	acIdx := buildACIndex(engine.HTTPFingers)
   350  	t.Logf("AC index built: %d body patterns, %d header patterns",
   351  		len(acIdx.bodyPatterns), len(acIdx.headerPatterns))
   352  
   353  	for _, site := range sites {
   354  		content := NewContent(site.Content, "", true)
   355  
   356  		// Baseline: current bytes.Contains approach
   357  		runtime.GC()
   358  		baseStart := time.Now()
   359  		baselineHits := 0
   360  		for _, finger := range engine.HTTPFingers {
   361  			for _, rule := range finger.Rules {
   362  				if rule.Regexps == nil {
   363  					continue
   364  				}
   365  				for _, h := range rule.Regexps.Header {
   366  					if content.Header != nil && bytes.Contains(content.Header, []byte(h)) {
   367  						baselineHits++
   368  					}
   369  				}
   370  				for _, b := range rule.Regexps.Body {
   371  					body := content.Body
   372  					if body == nil {
   373  						body = content.Content
   374  					}
   375  					if bytes.Contains(body, []byte(b)) {
   376  						baselineHits++
   377  					}
   378  				}
   379  			}
   380  		}
   381  		baseElapsed := time.Since(baseStart)
   382  
   383  		// AC approach
   384  		runtime.GC()
   385  		acStart := time.Now()
   386  		acResults := acIdx.matchKeywords(content.Header, content.Body)
   387  		acElapsed := time.Since(acStart)
   388  
   389  		speedup := 0.0
   390  		if acElapsed > 0 {
   391  			speedup = float64(baseElapsed) / float64(acElapsed)
   392  		}
   393  
   394  		t.Logf("[%s] content=%dB baseline=%s(%d hits) AC=%s(%d hits) speedup=%.2fx",
   395  			site.Name, len(site.Content), baseElapsed, baselineHits, acElapsed, len(acResults), speedup)
   396  	}
   397  }
   398  
   399  // ──────────────────────────────────────────────────────────────
   400  // 5. Baseline vs RE2 regex matching benchmark
   401  // ──────────────────────────────────────────────────────────────
   402  
   403  func TestBaselineVsRE2_Regex(t *testing.T) {
   404  	if testing.Short() {
   405  		t.Skip("skipping heavy performance comparison in short mode")
   406  	}
   407  	engine := newPerfEngine(t)
   408  
   409  	// Collect all compiled regexps from HTTP fingers
   410  	type regInfo struct {
   411  		Pattern  string
   412  		Compiled CompiledRegexp
   413  	}
   414  	var allRegexps []regInfo
   415  	for _, finger := range engine.HTTPFingers {
   416  		for _, rule := range finger.Rules {
   417  			if rule.Regexps == nil {
   418  				continue
   419  			}
   420  			for _, r := range rule.Regexps.CompliedRegexp {
   421  				allRegexps = append(allRegexps, regInfo{Pattern: r.String(), Compiled: r})
   422  			}
   423  			for _, r := range rule.Regexps.CompiledVulnRegexp {
   424  				allRegexps = append(allRegexps, regInfo{Pattern: r.String(), Compiled: r})
   425  			}
   426  		}
   427  	}
   428  	t.Logf("Total regex patterns to test: %d", len(allRegexps))
   429  
   430  	sites := generateSyntheticResponses()
   431  	realSites := loadTestSites(t)
   432  	sites = append(sites, realSites...)
   433  
   434  	for _, site := range sites {
   435  		content := NewContent(site.Content, "", true)
   436  
   437  		// Go stdlib regexp (current approach)
   438  		runtime.GC()
   439  		goStart := time.Now()
   440  		goHits := 0
   441  		for _, ri := range allRegexps {
   442  			if ri.Compiled.Match(content.Content) {
   443  				goHits++
   444  			}
   445  		}
   446  		goElapsed := time.Since(goStart)
   447  
   448  		t.Logf("[%s] content=%dB go_regexp=%s(%d hits) patterns=%d",
   449  			site.Name, len(site.Content), goElapsed, goHits, len(allRegexps))
   450  	}
   451  }
   452  
   453  // ──────────────────────────────────────────────────────────────
   454  // 6. Full engine match: Baseline vs AC-accelerated
   455  // ──────────────────────────────────────────────────────────────
   456  
   457  func TestFullEngineMatch_BaselineVsACPrefilter(t *testing.T) {
   458  	if testing.Short() {
   459  		t.Skip("skipping heavy performance comparison in short mode")
   460  	}
   461  	engine := newPerfEngine(t)
   462  	acIdx := buildACIndex(engine.HTTPFingers)
   463  
   464  	sites := generateSyntheticResponses()
   465  	realSites := loadTestSites(t)
   466  	sites = append(sites, realSites...)
   467  
   468  	const iterations = 100
   469  
   470  	for _, site := range sites {
   471  		// Baseline: full engine match
   472  		runtime.GC()
   473  		baseStart := time.Now()
   474  		var baseFrames int
   475  		for i := 0; i < iterations; i++ {
   476  			fs, _ := engine.HTTPMatch(site.Content, "")
   477  			baseFrames = len(fs)
   478  		}
   479  		baseElapsed := time.Since(baseStart)
   480  
   481  		// AC prefilter: identify candidate fingers, then only run those
   482  		content := NewContent(site.Content, "", true)
   483  		runtime.GC()
   484  		acStart := time.Now()
   485  		var acFrames int
   486  		for i := 0; i < iterations; i++ {
   487  			acResults := acIdx.matchKeywords(content.Header, content.Body)
   488  
   489  			candidateFingers := make(map[int]bool)
   490  			for _, r := range acResults {
   491  				candidateFingers[r.FingerIdx] = true
   492  			}
   493  
   494  			// Still need to run regex fingers that AC can't cover
   495  			for fi, finger := range engine.HTTPFingers {
   496  				if candidateFingers[fi] {
   497  					continue
   498  				}
   499  				for _, rule := range finger.Rules {
   500  					if rule.Regexps == nil {
   501  						continue
   502  					}
   503  					if len(rule.Regexps.CompliedRegexp) > 0 || len(rule.Regexps.CompiledVulnRegexp) > 0 ||
   504  						len(rule.Regexps.MD5) > 0 || len(rule.Regexps.MMH3) > 0 || len(rule.Regexps.Cert) > 0 {
   505  						candidateFingers[fi] = true
   506  						break
   507  					}
   508  				}
   509  			}
   510  
   511  			count := 0
   512  			for fi := range candidateFingers {
   513  				finger := engine.HTTPFingers[fi]
   514  				_, _, ok := finger.PassiveMatch(content)
   515  				if ok {
   516  					count++
   517  				}
   518  			}
   519  			acFrames = count
   520  		}
   521  		acElapsed := time.Since(acStart)
   522  
   523  		speedup := 0.0
   524  		if acElapsed > 0 {
   525  			speedup = float64(baseElapsed) / float64(acElapsed)
   526  		}
   527  
   528  		t.Logf("[%s] %dx: baseline=%s(%d frameworks) AC_prefilter=%s(%d frameworks) speedup=%.2fx",
   529  			site.Name, iterations, baseElapsed, baseFrames, acElapsed, acFrames, speedup)
   530  	}
   531  }
   532  
   533  // ──────────────────────────────────────────────────────────────
   534  // 7. Formal Benchmarks
   535  // ──────────────────────────────────────────────────────────────
   536  
   537  func BenchmarkKeywords_Baseline(b *testing.B) {
   538  	engine := newPerfEngine(b)
   539  	content := NewContent(generateLargeResponse(32*1024), "", true)
   540  
   541  	b.ReportAllocs()
   542  	b.ResetTimer()
   543  	for i := 0; i < b.N; i++ {
   544  		for _, finger := range engine.HTTPFingers {
   545  			for _, rule := range finger.Rules {
   546  				if rule.Regexps == nil {
   547  					continue
   548  				}
   549  				for _, h := range rule.Regexps.Header {
   550  					if content.Header != nil {
   551  						bytes.Contains(content.Header, []byte(h))
   552  					}
   553  				}
   554  				for _, bo := range rule.Regexps.Body {
   555  					body := content.Body
   556  					if body == nil {
   557  						body = content.Content
   558  					}
   559  					bytes.Contains(body, []byte(bo))
   560  				}
   561  			}
   562  		}
   563  	}
   564  }
   565  
   566  func BenchmarkKeywords_AhoCorasick(b *testing.B) {
   567  	engine := newPerfEngine(b)
   568  	acIdx := buildACIndex(engine.HTTPFingers)
   569  	content := NewContent(generateLargeResponse(32*1024), "", true)
   570  
   571  	b.ReportAllocs()
   572  	b.ResetTimer()
   573  	for i := 0; i < b.N; i++ {
   574  		acIdx.matchKeywords(content.Header, content.Body)
   575  	}
   576  }
   577  
   578  func BenchmarkRegexp_GoStdlib(b *testing.B) {
   579  	engine := newPerfEngine(b)
   580  	var allRegexps []CompiledRegexp
   581  	for _, finger := range engine.HTTPFingers {
   582  		for _, rule := range finger.Rules {
   583  			if rule.Regexps == nil {
   584  				continue
   585  			}
   586  			allRegexps = append(allRegexps, rule.Regexps.CompliedRegexp...)
   587  			allRegexps = append(allRegexps, rule.Regexps.CompiledVulnRegexp...)
   588  		}
   589  	}
   590  
   591  	content := NewContent(generateLargeResponse(32*1024), "", true)
   592  
   593  	b.ReportAllocs()
   594  	b.ResetTimer()
   595  	for i := 0; i < b.N; i++ {
   596  		for _, r := range allRegexps {
   597  			r.Match(content.Content)
   598  		}
   599  	}
   600  }
   601  
   602  func BenchmarkFullMatch_NoAC(b *testing.B) {
   603  	engine := newPerfEngine(b)
   604  	content := NewContent(generateLargeResponse(32*1024), "", true)
   605  
   606  	b.ReportAllocs()
   607  	b.ResetTimer()
   608  	for i := 0; i < b.N; i++ {
   609  		engine.HTTPFingers.PassiveMatch(content, false)
   610  	}
   611  }
   612  
   613  func BenchmarkFullMatch_WithAC(b *testing.B) {
   614  	engine := newPerfEngine(b)
   615  	content := generateLargeResponse(32 * 1024)
   616  
   617  	b.ReportAllocs()
   618  	b.ResetTimer()
   619  	for i := 0; i < b.N; i++ {
   620  		engine.HTTPMatch(content, "")
   621  	}
   622  }
   623  
   624  func BenchmarkFullMatch_ACPrefilter(b *testing.B) {
   625  	engine := newPerfEngine(b)
   626  	acIdx := buildACIndex(engine.HTTPFingers)
   627  	raw := generateLargeResponse(32 * 1024)
   628  	content := NewContent(raw, "", true)
   629  
   630  	b.ReportAllocs()
   631  	b.ResetTimer()
   632  	for i := 0; i < b.N; i++ {
   633  		acResults := acIdx.matchKeywords(content.Header, content.Body)
   634  		candidateFingers := make(map[int]bool)
   635  		for _, r := range acResults {
   636  			candidateFingers[r.FingerIdx] = true
   637  		}
   638  		for fi, finger := range engine.HTTPFingers {
   639  			if candidateFingers[fi] {
   640  				continue
   641  			}
   642  			for _, rule := range finger.Rules {
   643  				if rule.Regexps == nil {
   644  					continue
   645  				}
   646  				if len(rule.Regexps.CompliedRegexp) > 0 || len(rule.Regexps.CompiledVulnRegexp) > 0 ||
   647  					len(rule.Regexps.MD5) > 0 || len(rule.Regexps.MMH3) > 0 || len(rule.Regexps.Cert) > 0 {
   648  					candidateFingers[fi] = true
   649  					break
   650  				}
   651  			}
   652  		}
   653  		for fi := range candidateFingers {
   654  			engine.HTTPFingers[fi].PassiveMatch(content)
   655  		}
   656  	}
   657  }
   658  
   659  // ──────────────────────────────────────────────────────────────
   660  // 8. Content size scaling analysis
   661  // ──────────────────────────────────────────────────────────────
   662  
   663  func TestContentSizeScaling(t *testing.T) {
   664  	if testing.Short() {
   665  		t.Skip("skipping heavy performance comparison in short mode")
   666  	}
   667  	engine := newPerfEngine(t)
   668  	acIdx := buildACIndex(engine.HTTPFingers)
   669  
   670  	sizes := []int{1024, 4096, 16384, 65536, 262144}
   671  	const iterations = 50
   672  
   673  	t.Logf("%-10s %15s %15s %10s", "Size", "Baseline", "AC", "Speedup")
   674  	t.Logf(strings.Repeat("-", 55))
   675  
   676  	for _, size := range sizes {
   677  		raw := generateLargeResponse(size)
   678  		content := NewContent(raw, "", true)
   679  
   680  		// baseline
   681  		runtime.GC()
   682  		baseStart := time.Now()
   683  		for i := 0; i < iterations; i++ {
   684  			engine.HTTPMatch(raw, "")
   685  		}
   686  		baseElapsed := time.Since(baseStart) / time.Duration(iterations)
   687  
   688  		// AC
   689  		runtime.GC()
   690  		acStart := time.Now()
   691  		for i := 0; i < iterations; i++ {
   692  			acResults := acIdx.matchKeywords(content.Header, content.Body)
   693  			candidateFingers := make(map[int]bool)
   694  			for _, r := range acResults {
   695  				candidateFingers[r.FingerIdx] = true
   696  			}
   697  			for fi, finger := range engine.HTTPFingers {
   698  				if candidateFingers[fi] {
   699  					continue
   700  				}
   701  				for _, rule := range finger.Rules {
   702  					if rule.Regexps == nil {
   703  						continue
   704  					}
   705  					if len(rule.Regexps.CompliedRegexp) > 0 || len(rule.Regexps.CompiledVulnRegexp) > 0 ||
   706  						len(rule.Regexps.MD5) > 0 || len(rule.Regexps.MMH3) > 0 {
   707  						candidateFingers[fi] = true
   708  						break
   709  					}
   710  				}
   711  			}
   712  			for fi := range candidateFingers {
   713  				engine.HTTPFingers[fi].PassiveMatch(content)
   714  			}
   715  		}
   716  		acElapsed := time.Since(acStart) / time.Duration(iterations)
   717  
   718  		_ = acStart
   719  		speedup := float64(baseElapsed) / float64(acElapsed)
   720  		t.Logf("%-10d %15s %15s %9.2fx", size, baseElapsed, acElapsed, speedup)
   721  	}
   722  }
   723  
   724  // ──────────────────────────────────────────────────────────────
   725  // 9. AC build cost analysis
   726  // ──────────────────────────────────────────────────────────────
   727  
   728  func TestACBuildCost(t *testing.T) {
   729  	engine := newPerfEngine(t)
   730  
   731  	start := time.Now()
   732  	acIdx := buildACIndex(engine.HTTPFingers)
   733  	buildTime := time.Since(start)
   734  
   735  	t.Logf("AC build time: %s", buildTime)
   736  	t.Logf("Body patterns: %d, Header patterns: %d", len(acIdx.bodyPatterns), len(acIdx.headerPatterns))
   737  
   738  	// Single match warmup + timing
   739  	raw := generateLargeResponse(16 * 1024)
   740  	content := NewContent(raw, "", true)
   741  
   742  	matchStart := time.Now()
   743  	results := acIdx.matchKeywords(content.Header, content.Body)
   744  	matchTime := time.Since(matchStart)
   745  
   746  	t.Logf("Single AC match time: %s, hits: %d", matchTime, len(results))
   747  	t.Logf("Build amortized over 1000 matches: %s per match", buildTime/1000)
   748  }
   749  
   750  // ──────────────────────────────────────────────────────────────
   751  // 10. Go stdlib regexp vs go-re2 (C wrapper) comparison
   752  // ──────────────────────────────────────────────────────────────
   753  
   754  func TestGoRegexpVsRE2(t *testing.T) {
   755  	if testing.Short() {
   756  		t.Skip("skipping heavy performance comparison in short mode")
   757  	}
   758  	engine := newPerfEngine(t)
   759  
   760  	type patternPair struct {
   761  		goRe  *regexp.Regexp
   762  		re2Re *re2.Regexp
   763  		src   string
   764  	}
   765  
   766  	var patterns []patternPair
   767  	for _, finger := range engine.HTTPFingers {
   768  		for _, rule := range finger.Rules {
   769  			if rule.Regexps == nil {
   770  				continue
   771  			}
   772  			for _, r := range rule.Regexps.CompliedRegexp {
   773  				src := r.String()
   774  				goRe, err := regexp.Compile(src)
   775  				if err != nil {
   776  					continue
   777  				}
   778  				re2Re, err := re2.Compile(src)
   779  				if err != nil {
   780  					t.Logf("SKIP re2 compile %q: %v", src, err)
   781  					continue
   782  				}
   783  				patterns = append(patterns, patternPair{goRe: goRe, re2Re: re2Re, src: src})
   784  			}
   785  			for _, r := range rule.Regexps.CompiledVulnRegexp {
   786  				src := r.String()
   787  				goRe, err := regexp.Compile(src)
   788  				if err != nil {
   789  					continue
   790  				}
   791  				re2Re, err := re2.Compile(src)
   792  				if err != nil {
   793  					t.Logf("SKIP re2 compile %q: %v", src, err)
   794  					continue
   795  				}
   796  				patterns = append(patterns, patternPair{goRe: goRe, re2Re: re2Re, src: src})
   797  			}
   798  		}
   799  	}
   800  	t.Logf("Compiled %d patterns for both engines", len(patterns))
   801  
   802  	sizes := []int{1024, 8192, 32768, 131072}
   803  	const iterations = 20
   804  
   805  	for _, size := range sizes {
   806  		content := NewContent(generateLargeResponse(size), "", true)
   807  
   808  		// Go stdlib
   809  		runtime.GC()
   810  		goStart := time.Now()
   811  		goHits := 0
   812  		for i := 0; i < iterations; i++ {
   813  			for _, p := range patterns {
   814  				if p.goRe.Match(content.Content) {
   815  					goHits++
   816  				}
   817  			}
   818  		}
   819  		goElapsed := time.Since(goStart) / time.Duration(iterations)
   820  
   821  		// go-re2
   822  		runtime.GC()
   823  		re2Start := time.Now()
   824  		re2Hits := 0
   825  		for i := 0; i < iterations; i++ {
   826  			for _, p := range patterns {
   827  				if p.re2Re.Match(content.Content) {
   828  					re2Hits++
   829  				}
   830  			}
   831  		}
   832  		re2Elapsed := time.Since(re2Start) / time.Duration(iterations)
   833  
   834  		speedup := float64(goElapsed) / float64(re2Elapsed)
   835  		t.Logf("[%dB] go_regexp=%s(%d) re2=%s(%d) speedup=%.2fx",
   836  			size, goElapsed, goHits/iterations, re2Elapsed, re2Hits/iterations, speedup)
   837  	}
   838  }
   839  
   840  func BenchmarkRegexp_RE2(b *testing.B) {
   841  	engine := newPerfEngine(b)
   842  	var re2Regexps []*re2.Regexp
   843  	for _, finger := range engine.HTTPFingers {
   844  		for _, rule := range finger.Rules {
   845  			if rule.Regexps == nil {
   846  				continue
   847  			}
   848  			for _, r := range rule.Regexps.CompliedRegexp {
   849  				re2r, err := re2.Compile(r.String())
   850  				if err != nil {
   851  					continue
   852  				}
   853  				re2Regexps = append(re2Regexps, re2r)
   854  			}
   855  			for _, r := range rule.Regexps.CompiledVulnRegexp {
   856  				re2r, err := re2.Compile(r.String())
   857  				if err != nil {
   858  					continue
   859  				}
   860  				re2Regexps = append(re2Regexps, re2r)
   861  			}
   862  		}
   863  	}
   864  
   865  	content := NewContent(generateLargeResponse(32*1024), "", true)
   866  
   867  	b.ReportAllocs()
   868  	b.ResetTimer()
   869  	for i := 0; i < b.N; i++ {
   870  		for _, r := range re2Regexps {
   871  			r.Match(content.Content)
   872  		}
   873  	}
   874  }
   875  
   876  // ──────────────────────────────────────────────────────────────
   877  // 11. Combined AC + RE2 full optimization
   878  // ──────────────────────────────────────────────────────────────
   879  
   880  func TestCombinedOptimization(t *testing.T) {
   881  	if testing.Short() {
   882  		t.Skip("skipping heavy performance comparison in short mode")
   883  	}
   884  	engine := newPerfEngine(t)
   885  	acIdx := buildACIndex(engine.HTTPFingers)
   886  
   887  	// Pre-compile re2 versions of all regex patterns
   888  	type re2RulePatterns struct {
   889  		regexps     []*re2.Regexp
   890  		vulnRegexps []*re2.Regexp
   891  	}
   892  	re2Cache := make(map[*Rule]*re2RulePatterns)
   893  	for _, finger := range engine.HTTPFingers {
   894  		for _, rule := range finger.Rules {
   895  			if rule.Regexps == nil {
   896  				continue
   897  			}
   898  			rp := &re2RulePatterns{}
   899  			for _, r := range rule.Regexps.CompliedRegexp {
   900  				re2r, err := re2.Compile(r.String())
   901  				if err == nil {
   902  					rp.regexps = append(rp.regexps, re2r)
   903  				}
   904  			}
   905  			for _, r := range rule.Regexps.CompiledVulnRegexp {
   906  				re2r, err := re2.Compile(r.String())
   907  				if err == nil {
   908  					rp.vulnRegexps = append(rp.vulnRegexps, re2r)
   909  				}
   910  			}
   911  			re2Cache[rule] = rp
   912  		}
   913  	}
   914  
   915  	sites := generateSyntheticResponses()
   916  	realSites := loadTestSites(t)
   917  	sites = append(sites, realSites...)
   918  
   919  	iterations := 50
   920  	for _, site := range sites {
   921  		if len(site.Content) > 300000 {
   922  			iterations = 10
   923  		}
   924  
   925  		content := NewContent(site.Content, "", true)
   926  
   927  		// Baseline
   928  		runtime.GC()
   929  		baseStart := time.Now()
   930  		var baseFrames int
   931  		for i := 0; i < iterations; i++ {
   932  			fs, _ := engine.HTTPMatch(site.Content, "")
   933  			baseFrames = len(fs)
   934  		}
   935  		baseElapsed := time.Since(baseStart) / time.Duration(iterations)
   936  
   937  		// AC + RE2 combined
   938  		runtime.GC()
   939  		optStart := time.Now()
   940  		var optFrames int
   941  		for i := 0; i < iterations; i++ {
   942  			// Phase 1: AC keyword scan
   943  			acResults := acIdx.matchKeywords(content.Header, content.Body)
   944  			matched := make(map[int]bool)
   945  			for _, r := range acResults {
   946  				matched[r.FingerIdx] = true
   947  			}
   948  
   949  			// Phase 2: RE2 regex scan for fingerprints with regex-only rules
   950  			for fi, finger := range engine.HTTPFingers {
   951  				if matched[fi] {
   952  					continue
   953  				}
   954  				for _, rule := range finger.Rules {
   955  					if rule.Regexps == nil {
   956  						continue
   957  					}
   958  					rp := re2Cache[rule]
   959  					if rp == nil {
   960  						continue
   961  					}
   962  					hit := false
   963  					for _, r := range rp.vulnRegexps {
   964  						if r.Match(content.Content) {
   965  							hit = true
   966  							break
   967  						}
   968  					}
   969  					if !hit {
   970  						for _, r := range rp.regexps {
   971  							if r.Match(content.Content) {
   972  								hit = true
   973  								break
   974  							}
   975  						}
   976  					}
   977  					if hit {
   978  						matched[fi] = true
   979  						break
   980  					}
   981  				}
   982  			}
   983  
   984  			count := 0
   985  			for fi := range matched {
   986  				finger := engine.HTTPFingers[fi]
   987  				_, _, ok := finger.PassiveMatch(content)
   988  				if ok {
   989  					count++
   990  				}
   991  			}
   992  			optFrames = count
   993  		}
   994  		optElapsed := time.Since(optStart) / time.Duration(iterations)
   995  
   996  		speedup := float64(baseElapsed) / float64(optElapsed)
   997  		t.Logf("[%s] %dB: baseline=%s(%d) AC+RE2=%s(%d) speedup=%.2fx",
   998  			site.Name, len(site.Content), baseElapsed, baseFrames, optElapsed, optFrames, speedup)
   999  	}
  1000  }