Files
sif/pkg/scan/frameworks/detector.go
Celeste Hickenlooper 49ecfccb4a refactor: rewrite framework detection with modular detector architecture
- create detector interface and registry for extensibility
- extract detectors to separate files: backend.go, frontend.go, cms.go, meta.go
- reduce detect.go from 785 lines to 178 lines (pure orchestrator)
- export VersionMatch and ExtractVersionOptimized for detector use
- create result.go with NewFrameworkResult and WithVulnerabilities helpers
- add url validation to New() for early error detection
- add sif_test.go with main package tests
- update detect_test.go to use external test package pattern
2026-01-03 05:57:09 -08:00

134 lines
3.0 KiB
Go

/*
BSD 3-Clause License
(c) 2022-2025 vmfunc, xyzeva & contributors
*/
package frameworks
import (
"net/http"
"strings"
"sync"
)
// Signature represents a pattern to match for framework detection.
type Signature struct {
Pattern string
Weight float32
HeaderOnly bool
}
// Detector is the interface for framework detection plugins.
type Detector interface {
// Name returns the unique framework name.
Name() string
// Signatures returns patterns to search for this framework.
Signatures() []Signature
// Detect performs detection and returns confidence (0.0-1.0) and version.
// The version can be empty if not detectable.
Detect(body string, headers http.Header) (confidence float32, version string)
}
// registry holds all registered detectors.
var (
registryMu sync.RWMutex
registry = make(map[string]Detector)
)
// Register adds a detector to the registry. Should be called from init().
func Register(d Detector) {
registryMu.Lock()
defer registryMu.Unlock()
registry[d.Name()] = d
}
// GetDetectors returns all registered detectors.
func GetDetectors() map[string]Detector {
registryMu.RLock()
defer registryMu.RUnlock()
// Return a copy to prevent mutation
result := make(map[string]Detector, len(registry))
for k, v := range registry {
result[k] = v
}
return result
}
// GetDetector returns a specific detector by name.
func GetDetector(name string) (Detector, bool) {
registryMu.RLock()
defer registryMu.RUnlock()
d, ok := registry[name]
return d, ok
}
// BaseDetector provides common functionality for detector implementations.
type BaseDetector struct {
name string
signatures []Signature
}
// NewBaseDetector creates a new base detector.
func NewBaseDetector(name string, signatures []Signature) BaseDetector {
return BaseDetector{name: name, signatures: signatures}
}
// Name returns the framework name.
func (b BaseDetector) Name() string {
return b.name
}
// Signatures returns the detection signatures.
func (b BaseDetector) Signatures() []Signature {
return b.signatures
}
// MatchSignatures checks body and headers against signatures and returns a weighted score.
func (b BaseDetector) MatchSignatures(body string, headers http.Header) float32 {
var weightedScore float32
var totalWeight float32
for _, sig := range b.signatures {
totalWeight += sig.Weight
if sig.HeaderOnly {
if containsHeader(headers, sig.Pattern) {
weightedScore += sig.Weight
}
} else if strings.Contains(body, sig.Pattern) {
weightedScore += sig.Weight
}
}
if totalWeight == 0 {
return 0
}
return weightedScore / totalWeight
}
// containsHeader checks if a signature pattern exists in headers.
func containsHeader(headers http.Header, signature string) bool {
sigLower := strings.ToLower(signature)
// Check header names
for name := range headers {
if strings.Contains(strings.ToLower(name), sigLower) {
return true
}
}
// Check header values
for _, values := range headers {
for _, value := range values {
if strings.Contains(strings.ToLower(value), sigLower) {
return true
}
}
}
return false
}