java-topology/defects/woodpecker-0001/test/woodpecker_step_builder_test.go
russell@unturf.com 1e10775b90 jitsi-videobridge: 3 CWE-407 defects, MOADs 0002-0005 CLEAN; woodpecker: 1 CWE-407 + 1 CWE-312, MOADs 0002/0003/0005 CLEAN
jitsi-videobridge (Kotlin/Java video conferencing bridge):
- 0001: Prioritize.kt selectedSourceNames.contains()+indexOf() inside forEach over conferenceSources, O(C*S)
- 0002: BandwidthAllocator.kt selectedSources getter List.contains() dedup inside forEach, O(S^2)
- 0003: ConferenceSpeechActivity.java endpointsChanged() ArrayList.contains() in removeIf+for loop, O(E^2)
  Fix: HashSet for O(1) membership; pre-built index map for indexOf
  Unit test: 4/4 PASS, 19-35x op-count reduction at N=200

woodpecker-0001 (Go CI/CD pipeline step builder):
- filterItemsWithMissingDependencies() calls containsItemWithName() (O(N) linear scan) inside
  two nested loops over items and deps: O(N*D*N) = O(N^2)
  Fix: pre-build name-set map for O(1) lookup, O(N) total
  Unit test: 3/3 PASS, 20x op-count reduction at N=100

woodpecker-0002 (CWE-312 credential logging):
- shared/token/token.go ParseRequest() logs raw Authorization header value at Trace level:
  log.Trace().Msgf("token.ParseRequest: found token in header: %s", token)
  Exposes full Bearer JWT token in application logs
  Fix: log only that header was found, not its value
  Unit test: 3/3 PASS
2026-03-31 20:18:44 -04:00

165 lines
4.5 KiB
Go

// CWE-407 benchmark: woodpecker-0001
// filterItemsWithMissingDependencies uses containsItemWithName (O(N) linear scan)
// inside two nested loops: O(N * D * N) = O(N²) where D = average deps per item.
// Fix: pre-build a name-set map for O(1) membership tests, reducing to O(N).
//
// Run: go test -v -run TestFilterItems ./...
package woodpecker_test
import (
"fmt"
"testing"
)
// Item mirrors the step_builder Item struct for benchmarking purposes.
type Item struct {
Name string
DependsOn []string
}
// slowContainsItemWithName is the original O(N) linear scan.
func slowContainsItemWithName(name string, items []*Item) bool {
for _, item := range items {
if name == item.Name {
return true
}
}
return false
}
// slowFilterItems is the original O(N²) implementation.
func slowFilterItems(items []*Item) ([]*Item, int64) {
var ops int64
itemsToRemove := make([]*Item, 0)
for _, item := range items {
for _, dep := range item.DependsOn {
ops++ // containsItemWithName call
if !slowContainsItemWithName(dep, items) {
ops += int64(len(items)) // O(N) scan
itemsToRemove = append(itemsToRemove, item)
} else {
ops += int64(len(items)) // worst-case scan
}
}
}
if len(itemsToRemove) > 0 {
filtered := make([]*Item, 0)
for _, item := range items {
ops += int64(len(itemsToRemove)) // O(N) scan per item
if !slowContainsItemWithName(item.Name, itemsToRemove) {
filtered = append(filtered, item)
}
}
sub, subOps := slowFilterItems(filtered)
ops += subOps
return sub, ops
}
return items, ops
}
// fastFilterItems is the O(N) fixed implementation using a name map.
func fastFilterItems(items []*Item) ([]*Item, int64) {
var ops int64
// Build O(1) lookup set - O(N) once.
nameSet := make(map[string]struct{}, len(items))
for _, item := range items {
nameSet[item.Name] = struct{}{}
ops++
}
toRemoveNames := make(map[string]struct{})
for _, item := range items {
for _, dep := range item.DependsOn {
ops++ // O(1) map lookup
if _, exists := nameSet[dep]; !exists {
toRemoveNames[item.Name] = struct{}{}
break
}
}
}
if len(toRemoveNames) > 0 {
filtered := make([]*Item, 0, len(items))
for _, item := range items {
ops++ // O(1) map lookup
if _, remove := toRemoveNames[item.Name]; !remove {
filtered = append(filtered, item)
}
}
sub, subOps := fastFilterItems(filtered)
ops += subOps
return sub, ops
}
return items, ops
}
// buildItems creates N items where items[N/4..N/2] have a missing dep ("ghost")
// to trigger the filter path, and the rest have valid deps on previous items.
func buildItems(n int) []*Item {
items := make([]*Item, n)
for i := 0; i < n; i++ {
deps := []string{}
if i > 0 {
deps = append(deps, fmt.Sprintf("item-%d", i-1))
}
items[i] = &Item{Name: fmt.Sprintf("item-%d", i), DependsOn: deps}
}
// Inject missing deps in the middle quarter to trigger filtering.
for i := n / 4; i < n/2; i++ {
items[i].DependsOn = append(items[i].DependsOn, "ghost-missing-dep")
}
return items
}
func TestFilterItemsCorrectness(t *testing.T) {
items := buildItems(20)
slow, _ := slowFilterItems(items)
fast, _ := fastFilterItems(items)
if len(slow) != len(fast) {
t.Fatalf("result length mismatch: slow=%d fast=%d", len(slow), len(fast))
}
slowNames := make(map[string]bool)
for _, it := range slow {
slowNames[it.Name] = true
}
for _, it := range fast {
if !slowNames[it.Name] {
t.Errorf("fast result contains item not in slow result: %s", it.Name)
}
}
t.Logf("correctness: slow=%d items, fast=%d items — match", len(slow), len(fast))
}
func TestFilterItemsOpCount(t *testing.T) {
sizes := []int{10, 30, 50, 100}
for _, n := range sizes {
items := buildItems(n)
_, slowOps := slowFilterItems(items)
_, fastOps := fastFilterItems(items)
ratio := float64(slowOps) / float64(fastOps)
t.Logf("N=%3d slow=%6d ops fast=%6d ops ratio=%.1fx", n, slowOps, fastOps, ratio)
if n >= 50 && ratio < 3.0 {
t.Errorf("N=%d: expected ratio >= 3x, got %.1fx (slow=%d, fast=%d)", n, ratio, slowOps, fastOps)
}
}
}
func TestFilterItemsPass(t *testing.T) {
n := 100
items := buildItems(n)
_, slowOps := slowFilterItems(items)
_, fastOps := fastFilterItems(items)
ratio := float64(slowOps) / float64(fastOps)
t.Logf("woodpecker-0001 N=%d: slow=%d ops, fast=%d ops, ratio=%.1fx", n, slowOps, fastOps, ratio)
if ratio < 5.0 {
t.Fatalf("FAIL: expected at least 5x op-count reduction, got %.1fx", ratio)
}
t.Logf("PASS: %.0fx op-count reduction at N=%d", ratio, n)
}