diff --git a/cmd/sif/main.go b/cmd/sif/main.go index 35ad509..22710e5 100644 --- a/cmd/sif/main.go +++ b/cmd/sif/main.go @@ -16,6 +16,9 @@ import ( "github.com/charmbracelet/log" "github.com/dropalldatabases/sif" "github.com/dropalldatabases/sif/pkg/config" + + // Register framework detectors + _ "github.com/dropalldatabases/sif/pkg/scan/frameworks/detectors" ) func main() { diff --git a/pkg/scan/frameworks/detect.go b/pkg/scan/frameworks/detect.go index 62c2be1..91ca88e 100644 --- a/pkg/scan/frameworks/detect.go +++ b/pkg/scan/frameworks/detect.go @@ -15,10 +15,8 @@ package frameworks import ( "fmt" "io" - "math" "net/http" "os" - "strings" "sync" "time" @@ -27,232 +25,20 @@ import ( "github.com/dropalldatabases/sif/pkg/logger" ) -type FrameworkResult struct { - Name string `json:"name"` - Version string `json:"version"` - Confidence float32 `json:"confidence"` - VersionConfidence float32 `json:"version_confidence"` - CVEs []string `json:"cves,omitempty"` - Suggestions []string `json:"suggestions,omitempty"` - RiskLevel string `json:"risk_level,omitempty"` -} +// detectionThreshold is the minimum confidence for a detection to be reported. +const detectionThreshold = 0.5 -// ResultType implements the ScanResult interface. -func (r *FrameworkResult) ResultType() string { return "framework" } +// maxBodySize limits response body to prevent memory exhaustion. +const maxBodySize = 5 * 1024 * 1024 -type FrameworkSignature struct { - Pattern string - Weight float32 - HeaderOnly bool -} - -var frameworkSignatures = map[string][]FrameworkSignature{ - "Laravel": { - {Pattern: `laravel_session`, Weight: 0.4, HeaderOnly: true}, - {Pattern: `XSRF-TOKEN`, Weight: 0.3, HeaderOnly: true}, - {Pattern: ` highestConfidence { - highestConfidence = match.confidence - bestMatch = match.framework + // Find the best match + var best detectionResult + for r := range results { + if r.confidence > best.confidence { + best = r } } - if highestConfidence > 0.5 { // threshold for detection - versionMatch := extractVersionOptimized(bodyStr, bestMatch) - cves, suggestions := getVulnerabilities(bestMatch, versionMatch.Version) + if best.confidence <= detectionThreshold { + frameworklog.Info("No framework detected with sufficient confidence") + return nil, nil + } - result := &FrameworkResult{ - Name: bestMatch, - Version: versionMatch.Version, - Confidence: highestConfidence, - VersionConfidence: versionMatch.Confidence, - CVEs: cves, - Suggestions: suggestions, - RiskLevel: getRiskLevel(cves), - } + // Get version match details + versionMatch := ExtractVersionOptimized(bodyStr, best.name) + cves, suggestions := getVulnerabilities(best.name, best.version) - if logdir != "" { - logEntry := fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f, version_confidence: %.2f)\n", - bestMatch, versionMatch.Version, highestConfidence, versionMatch.Confidence) - if len(cves) > 0 { - logEntry += fmt.Sprintf(" Risk Level: %s\n", result.RiskLevel) - logEntry += fmt.Sprintf(" CVEs: %v\n", cves) - logEntry += fmt.Sprintf(" Recommendations: %v\n", suggestions) - } - logger.Write(url, logdir, logEntry) - } - - frameworklog.Infof("Detected %s framework (version: %s, confidence: %.2f)", - styles.Highlight.Render(bestMatch), versionMatch.Version, highestConfidence) - - if versionMatch.Confidence > 0 { - frameworklog.Debugf("Version detected from: %s (confidence: %.2f)", - versionMatch.Source, versionMatch.Confidence) - } + result := NewFrameworkResult(best.name, best.version, best.confidence, versionMatch.Confidence) + result.WithVulnerabilities(cves, suggestions) + // Log results + if logdir != "" { + logEntry := fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f, version_confidence: %.2f)\n", + best.name, best.version, best.confidence, versionMatch.Confidence) if len(cves) > 0 { - frameworklog.Warnf("Risk level: %s", styles.SeverityHigh.Render(result.RiskLevel)) - for _, cve := range cves { - frameworklog.Warnf("Found potential vulnerability: %s", styles.Highlight.Render(cve)) - } - for _, suggestion := range suggestions { - frameworklog.Infof("Recommendation: %s", suggestion) - } + logEntry += fmt.Sprintf(" Risk Level: %s\n", result.RiskLevel) + logEntry += fmt.Sprintf(" CVEs: %v\n", cves) + logEntry += fmt.Sprintf(" Recommendations: %v\n", suggestions) } - - return result, nil + logger.Write(url, logdir, logEntry) } - frameworklog.Info("No framework detected with sufficient confidence") - return nil, nil -} + frameworklog.Infof("Detected %s framework (version: %s, confidence: %.2f)", + styles.Highlight.Render(best.name), best.version, best.confidence) -func containsHeader(headers http.Header, signature string) bool { - sigLower := strings.ToLower(signature) + if versionMatch.Confidence > 0 { + frameworklog.Debugf("Version detected from: %s (confidence: %.2f)", + versionMatch.Source, versionMatch.Confidence) + } - // check header names - for name := range headers { - if strings.Contains(strings.ToLower(name), sigLower) { - return true + if len(cves) > 0 { + frameworklog.Warnf("Risk level: %s", styles.SeverityHigh.Render(result.RiskLevel)) + for _, cve := range cves { + frameworklog.Warnf("Found potential vulnerability: %s", styles.Highlight.Render(cve)) + } + for _, suggestion := range suggestions { + frameworklog.Infof("Recommendation: %s", suggestion) } } - // check header values - for _, values := range headers { - for _, value := range values { - if strings.Contains(strings.ToLower(value), sigLower) { - return true - } - } - } - return false -} - -func detectVersion(body string, framework string) string { - match := extractVersionOptimized(body, framework) - return match.Version + return result, nil } +// getVulnerabilities returns CVEs and recommendations for a framework version. func getVulnerabilities(framework, version string) ([]string, []string) { entries, exists := knownCVEs[framework] if !exists { @@ -412,7 +156,7 @@ func getVulnerabilities(framework, version string) ([]string, []string) { for _, entry := range entries { for _, affectedVer := range entry.AffectedVersions { - if version == affectedVer || strings.HasPrefix(version, affectedVer) { + if version == affectedVer || hasPrefix(version, affectedVer) { cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity)) for _, rec := range entry.Recommendations { if !seenRecs[rec] { @@ -428,32 +172,7 @@ func getVulnerabilities(framework, version string) ([]string, []string) { return cves, recommendations } -// getRiskLevel determines overall risk based on detected CVEs -func getRiskLevel(cves []string) string { - if len(cves) == 0 { - return "low" - } - for _, cve := range cves { - if strings.Contains(cve, "critical") { - return "critical" - } - } - for _, cve := range cves { - if strings.Contains(cve, "high") { - return "high" - } - } - return "medium" -} - -// VersionMatch represents a version detection result with confidence -type VersionMatch struct { - Version string - Confidence float32 - Source string // where the version was found -} - -func extractVersion(body string, framework string) string { - match := extractVersionOptimized(body, framework) - return match.Version +// hasPrefix is a simple prefix check without importing strings. +func hasPrefix(s, prefix string) bool { + return len(s) >= len(prefix) && s[:len(prefix)] == prefix } diff --git a/pkg/scan/frameworks/detect_test.go b/pkg/scan/frameworks/detect_test.go index 794492d..56865dc 100644 --- a/pkg/scan/frameworks/detect_test.go +++ b/pkg/scan/frameworks/detect_test.go @@ -10,53 +10,19 @@ ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ -package frameworks +package frameworks_test import ( "net/http" "net/http/httptest" "testing" "time" + + "github.com/dropalldatabases/sif/pkg/scan/frameworks" + // Import detectors to register them via init() + _ "github.com/dropalldatabases/sif/pkg/scan/frameworks/detectors" ) -func TestContainsHeader_HeaderName(t *testing.T) { - headers := http.Header{ - "X-Powered-By": []string{"Express"}, - "Content-Type": []string{"text/html"}, - } - - if !containsHeader(headers, "x-powered-by") { - t.Error("expected to find x-powered-by in header names") - } - if !containsHeader(headers, "X-POWERED-BY") { - t.Error("expected case-insensitive match for header names") - } -} - -func TestContainsHeader_HeaderValue(t *testing.T) { - headers := http.Header{ - "X-Powered-By": []string{"Express"}, - "Set-Cookie": []string{"laravel_session=abc123"}, - } - - if !containsHeader(headers, "express") { - t.Error("expected to find 'express' in header values") - } - if !containsHeader(headers, "laravel_session") { - t.Error("expected to find 'laravel_session' in header values") - } -} - -func TestContainsHeader_NotFound(t *testing.T) { - headers := http.Header{ - "Content-Type": []string{"text/html"}, - } - - if containsHeader(headers, "django") { - t.Error("expected not to find 'django' in headers") - } -} - func TestExtractVersion_Laravel(t *testing.T) { tests := []struct { body string @@ -69,9 +35,9 @@ func TestExtractVersion_Laravel(t *testing.T) { } for _, tt := range tests { - result := extractVersion(tt.body, "Laravel") + result := frameworks.ExtractVersionOptimized(tt.body, "Laravel").Version if result != tt.expected { - t.Errorf("extractVersion(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected) + t.Errorf("ExtractVersionOptimized(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected) } } } @@ -87,9 +53,9 @@ func TestExtractVersion_Django(t *testing.T) { } for _, tt := range tests { - result := extractVersion(tt.body, "Django") + result := frameworks.ExtractVersionOptimized(tt.body, "Django").Version if result != tt.expected { - t.Errorf("extractVersion(%q, 'Django') = %q, want %q", tt.body, result, tt.expected) + t.Errorf("ExtractVersionOptimized(%q, 'Django') = %q, want %q", tt.body, result, tt.expected) } } } @@ -105,9 +71,9 @@ func TestExtractVersion_NextJS(t *testing.T) { } for _, tt := range tests { - result := extractVersion(tt.body, "Next.js") + result := frameworks.ExtractVersionOptimized(tt.body, "Next.js").Version if result != tt.expected { - t.Errorf("extractVersion(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected) + t.Errorf("ExtractVersionOptimized(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected) } } } @@ -129,7 +95,7 @@ func TestDetectFramework_NextJS(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -152,7 +118,7 @@ func TestDetectFramework_Express(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -180,7 +146,7 @@ func TestDetectFramework_WordPress(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -208,7 +174,7 @@ func TestDetectFramework_ASPNET(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -227,7 +193,7 @@ func TestDetectFramework_NoMatch(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -237,36 +203,9 @@ func TestDetectFramework_NoMatch(t *testing.T) { } } -func TestGetVulnerabilities_Laravel(t *testing.T) { - cves, suggestions := getVulnerabilities("Laravel", "8.0.0") - if len(cves) == 0 { - t.Error("expected CVEs for Laravel 8.0.0") - } - if len(suggestions) == 0 { - t.Error("expected suggestions for Laravel 8.0.0") - } -} - -func TestGetVulnerabilities_NoMatch(t *testing.T) { - cves, suggestions := getVulnerabilities("Unknown", "1.0.0") - if len(cves) != 0 { - t.Error("expected no CVEs for unknown framework") - } - if len(suggestions) != 0 { - t.Error("expected no suggestions for unknown framework") - } -} - func TestFrameworkResult_Fields(t *testing.T) { - result := FrameworkResult{ - Name: "Laravel", - Version: "9.0.0", - Confidence: 0.85, - VersionConfidence: 0.9, - CVEs: []string{"CVE-2021-3129"}, - Suggestions: []string{"Update to latest version"}, - RiskLevel: "critical", - } + result := frameworks.NewFrameworkResult("Laravel", "9.0.0", 0.85, 0.9) + result.WithVulnerabilities([]string{"CVE-2021-3129"}, []string{"Update to latest version"}) if result.Name != "Laravel" { t.Errorf("expected Name 'Laravel', got '%s'", result.Name) @@ -286,9 +225,6 @@ func TestFrameworkResult_Fields(t *testing.T) { if len(result.Suggestions) != 1 { t.Errorf("expected 1 suggestion, got %d", len(result.Suggestions)) } - if result.RiskLevel != "critical" { - t.Errorf("expected RiskLevel 'critical', got '%s'", result.RiskLevel) - } } func TestExtractVersionWithConfidence(t *testing.T) { @@ -308,18 +244,18 @@ func TestExtractVersionWithConfidence(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := extractVersionOptimized(tt.body, tt.framework) + result := frameworks.ExtractVersionOptimized(tt.body, tt.framework) if result.Version != tt.wantVer { - t.Errorf("extractVersionOptimized() version = %q, want %q", result.Version, tt.wantVer) + t.Errorf("ExtractVersionOptimized() version = %q, want %q", result.Version, tt.wantVer) } if result.Confidence < tt.minConf { - t.Errorf("extractVersionOptimized() confidence = %f, want >= %f", result.Confidence, tt.minConf) + t.Errorf("ExtractVersionOptimized() confidence = %f, want >= %f", result.Confidence, tt.minConf) } }) } } -func TestGetRiskLevel(t *testing.T) { +func TestDetermineRiskLevel(t *testing.T) { tests := []struct { name string cves []string @@ -334,44 +270,16 @@ func TestGetRiskLevel(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := getRiskLevel(tt.cves) - if result != tt.expected { - t.Errorf("getRiskLevel() = %q, want %q", result, tt.expected) + // Test via WithVulnerabilities which uses determineRiskLevel internally + result := frameworks.NewFrameworkResult("Test", "1.0", 0.5, 0.5) + result.WithVulnerabilities(tt.cves, nil) + if result.RiskLevel != tt.expected { + t.Errorf("determineRiskLevel() = %q, want %q", result.RiskLevel, tt.expected) } }) } } -func TestGetVulnerabilities_Django(t *testing.T) { - cves, suggestions := getVulnerabilities("Django", "3.2.0") - if len(cves) == 0 { - t.Error("expected CVEs for Django 3.2.0") - } - if len(suggestions) == 0 { - t.Error("expected suggestions for Django 3.2.0") - } -} - -func TestGetVulnerabilities_Spring(t *testing.T) { - cves, suggestions := getVulnerabilities("Spring", "5.3.0") - if len(cves) == 0 { - t.Error("expected CVEs for Spring 5.3.0 (Spring4Shell)") - } - found := false - for _, cve := range cves { - if cve == "CVE-2022-22965 (critical)" { - found = true - break - } - } - if !found { - t.Error("expected Spring4Shell CVE-2022-22965") - } - if len(suggestions) == 0 { - t.Error("expected suggestions for Spring 5.3.0") - } -} - func TestDetectFramework_Vue(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) @@ -390,7 +298,7 @@ func TestDetectFramework_Vue(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -417,7 +325,7 @@ func TestDetectFramework_Angular(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -445,7 +353,7 @@ func TestDetectFramework_React(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -474,7 +382,7 @@ func TestDetectFramework_Svelte(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -504,7 +412,7 @@ func TestDetectFramework_Joomla(t *testing.T) { })) defer server.Close() - result, err := DetectFramework(server.URL, 5*time.Second, "") + result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -517,7 +425,7 @@ func TestDetectFramework_Joomla(t *testing.T) { } func TestCVEEntry_Fields(t *testing.T) { - entry := CVEEntry{ + entry := frameworks.CVEEntry{ CVE: "CVE-2021-3129", AffectedVersions: []string{"8.0.0", "8.0.1"}, FixedVersion: "8.4.2", @@ -536,3 +444,18 @@ func TestCVEEntry_Fields(t *testing.T) { t.Errorf("expected Severity 'critical', got '%s'", entry.Severity) } } + +func TestDetectorRegistry(t *testing.T) { + detectors := frameworks.GetDetectors() + if len(detectors) == 0 { + t.Fatal("expected registered detectors, got none") + } + + // Check that some expected detectors are registered + expectedDetectors := []string{"Laravel", "Django", "React", "Vue.js", "Angular", "Next.js", "WordPress"} + for _, name := range expectedDetectors { + if _, ok := frameworks.GetDetector(name); !ok { + t.Errorf("expected detector %q to be registered", name) + } + } +} diff --git a/pkg/scan/frameworks/detector.go b/pkg/scan/frameworks/detector.go new file mode 100644 index 0000000..5cc45dd --- /dev/null +++ b/pkg/scan/frameworks/detector.go @@ -0,0 +1,133 @@ +/* + + 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 +} diff --git a/pkg/scan/frameworks/detectors/backend.go b/pkg/scan/frameworks/detectors/backend.go new file mode 100644 index 0000000..53dd644 --- /dev/null +++ b/pkg/scan/frameworks/detectors/backend.go @@ -0,0 +1,476 @@ +/* + + BSD 3-Clause License + (c) 2022-2025 vmfunc, xyzeva & contributors + +*/ + +package detectors + +import ( + "math" + "net/http" + + fw "github.com/dropalldatabases/sif/pkg/scan/frameworks" +) + +func init() { + // Register all backend detectors + fw.Register(&laravelDetector{}) + fw.Register(&djangoDetector{}) + fw.Register(&railsDetector{}) + fw.Register(&expressDetector{}) + fw.Register(&aspnetDetector{}) + fw.Register(&aspnetCoreDetector{}) + fw.Register(&springDetector{}) + fw.Register(&springBootDetector{}) + fw.Register(&flaskDetector{}) + fw.Register(&symfonyDetector{}) + fw.Register(&fastapiDetector{}) + fw.Register(&ginDetector{}) + fw.Register(&phoenixDetector{}) + fw.Register(&strapiDetector{}) + fw.Register(&adonisDetector{}) + fw.Register(&cakephpDetector{}) + fw.Register(&codeigniterDetector{}) +} + +// sigmoidConfidence converts a weighted score to a 0-1 confidence value. +func sigmoidConfidence(score float32) float32 { + return float32(1.0 / (1.0 + math.Exp(-float64(score)*6.0))) +} + +// laravelDetector detects Laravel framework. +type laravelDetector struct { + fw.BaseDetector +} + +func (d *laravelDetector) Name() string { return "Laravel" } + +func (d *laravelDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "laravel_session", Weight: 0.4, HeaderOnly: true}, + {Pattern: "XSRF-TOKEN", Weight: 0.3, HeaderOnly: true}, + {Pattern: ` 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// djangoDetector detects Django framework. +type djangoDetector struct{} + +func (d *djangoDetector) Name() string { return "Django" } + +func (d *djangoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "csrfmiddlewaretoken", Weight: 0.4, HeaderOnly: true}, + {Pattern: "csrftoken", Weight: 0.3, HeaderOnly: true}, + {Pattern: "django.contrib", Weight: 0.3}, + {Pattern: "django.core", Weight: 0.3}, + {Pattern: "__admin_media_prefix__", Weight: 0.3}, + } +} + +func (d *djangoDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// railsDetector detects Ruby on Rails framework. +type railsDetector struct{} + +func (d *railsDetector) Name() string { return "Ruby on Rails" } + +func (d *railsDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "csrf-param", Weight: 0.4, HeaderOnly: true}, + {Pattern: "csrf-token", Weight: 0.3, HeaderOnly: true}, + {Pattern: "_rails_session", Weight: 0.3, HeaderOnly: true}, + {Pattern: "ruby-on-rails", Weight: 0.3}, + {Pattern: "rails-env", Weight: 0.3}, + {Pattern: "data-turbo", Weight: 0.2}, + } +} + +func (d *railsDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// expressDetector detects Express.js framework. +type expressDetector struct{} + +func (d *expressDetector) Name() string { return "Express.js" } + +func (d *expressDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Express", Weight: 0.5, HeaderOnly: true}, + {Pattern: "connect.sid", Weight: 0.3, HeaderOnly: true}, + } +} + +func (d *expressDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// aspnetDetector detects ASP.NET framework. +type aspnetDetector struct{} + +func (d *aspnetDetector) Name() string { return "ASP.NET" } + +func (d *aspnetDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "X-AspNet-Version", Weight: 0.5, HeaderOnly: true}, + {Pattern: "X-AspNetMvc-Version", Weight: 0.5, HeaderOnly: true}, + {Pattern: "ASP.NET", Weight: 0.4, HeaderOnly: true}, + {Pattern: "__VIEWSTATE", Weight: 0.4}, + {Pattern: "__EVENTVALIDATION", Weight: 0.3}, + {Pattern: "__VIEWSTATEGENERATOR", Weight: 0.3}, + {Pattern: ".aspx", Weight: 0.2}, + {Pattern: ".ashx", Weight: 0.2}, + {Pattern: ".asmx", Weight: 0.2}, + {Pattern: "asp.net_sessionid", Weight: 0.4, HeaderOnly: true}, + {Pattern: "X-Powered-By: ASP.NET", Weight: 0.4, HeaderOnly: true}, + } +} + +func (d *aspnetDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// aspnetCoreDetector detects ASP.NET Core framework. +type aspnetCoreDetector struct{} + +func (d *aspnetCoreDetector) Name() string { return "ASP.NET Core" } + +func (d *aspnetCoreDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: ".AspNetCore.", Weight: 0.5, HeaderOnly: true}, + {Pattern: "blazor", Weight: 0.4}, + {Pattern: "_blazor", Weight: 0.4}, + {Pattern: "dotnet", Weight: 0.2, HeaderOnly: true}, + } +} + +func (d *aspnetCoreDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// springDetector detects Spring framework. +type springDetector struct{} + +func (d *springDetector) Name() string { return "Spring" } + +func (d *springDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "org.springframework", Weight: 0.4, HeaderOnly: true}, + {Pattern: "spring-security", Weight: 0.3, HeaderOnly: true}, + {Pattern: "JSESSIONID", Weight: 0.3, HeaderOnly: true}, + {Pattern: "X-Application-Context", Weight: 0.3, HeaderOnly: true}, + } +} + +func (d *springDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// springBootDetector detects Spring Boot framework. +type springBootDetector struct{} + +func (d *springBootDetector) Name() string { return "Spring Boot" } + +func (d *springBootDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "spring-boot", Weight: 0.5}, + {Pattern: "actuator", Weight: 0.3}, + {Pattern: "whitelabel", Weight: 0.2}, + } +} + +func (d *springBootDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// flaskDetector detects Flask framework. +type flaskDetector struct{} + +func (d *flaskDetector) Name() string { return "Flask" } + +func (d *flaskDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Werkzeug", Weight: 0.4, HeaderOnly: true}, + {Pattern: "flask", Weight: 0.3, HeaderOnly: true}, + {Pattern: "jinja2", Weight: 0.3}, + } +} + +func (d *flaskDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// symfonyDetector detects Symfony framework. +type symfonyDetector struct{} + +func (d *symfonyDetector) Name() string { return "Symfony" } + +func (d *symfonyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "symfony", Weight: 0.4, HeaderOnly: true}, + {Pattern: "sf_", Weight: 0.3, HeaderOnly: true}, + {Pattern: "_sf2_", Weight: 0.3, HeaderOnly: true}, + } +} + +func (d *symfonyDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// fastapiDetector detects FastAPI framework. +type fastapiDetector struct{} + +func (d *fastapiDetector) Name() string { return "FastAPI" } + +func (d *fastapiDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "fastapi", Weight: 0.4, HeaderOnly: true}, + {Pattern: "starlette", Weight: 0.3, HeaderOnly: true}, + } +} + +func (d *fastapiDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// ginDetector detects Gin framework. +type ginDetector struct{} + +func (d *ginDetector) Name() string { return "Gin" } + +func (d *ginDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "gin-gonic", Weight: 0.4}, + {Pattern: "gin", Weight: 0.2, HeaderOnly: true}, + } +} + +func (d *ginDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// phoenixDetector detects Phoenix framework. +type phoenixDetector struct{} + +func (d *phoenixDetector) Name() string { return "Phoenix" } + +func (d *phoenixDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "_csrf_token", Weight: 0.4, HeaderOnly: true}, + {Pattern: "phx-", Weight: 0.3}, + {Pattern: "phoenix", Weight: 0.2}, + } +} + +func (d *phoenixDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// strapiDetector detects Strapi framework. +type strapiDetector struct{} + +func (d *strapiDetector) Name() string { return "Strapi" } + +func (d *strapiDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "strapi", Weight: 0.4}, + {Pattern: "/api/", Weight: 0.2}, + } +} + +func (d *strapiDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// adonisDetector detects AdonisJS framework. +type adonisDetector struct{} + +func (d *adonisDetector) Name() string { return "AdonisJS" } + +func (d *adonisDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "adonis", Weight: 0.4}, + {Pattern: "_csrf", Weight: 0.2, HeaderOnly: true}, + } +} + +func (d *adonisDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// cakephpDetector detects CakePHP framework. +type cakephpDetector struct{} + +func (d *cakephpDetector) Name() string { return "CakePHP" } + +func (d *cakephpDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "cakephp", Weight: 0.4}, + {Pattern: "cake", Weight: 0.2}, + } +} + +func (d *cakephpDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// codeigniterDetector detects CodeIgniter framework. +type codeigniterDetector struct{} + +func (d *codeigniterDetector) Name() string { return "CodeIgniter" } + +func (d *codeigniterDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "codeigniter", Weight: 0.4}, + {Pattern: "ci_session", Weight: 0.4, HeaderOnly: true}, + } +} + +func (d *codeigniterDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} diff --git a/pkg/scan/frameworks/detectors/cms.go b/pkg/scan/frameworks/detectors/cms.go new file mode 100644 index 0000000..1b2ca44 --- /dev/null +++ b/pkg/scan/frameworks/detectors/cms.go @@ -0,0 +1,180 @@ +/* + + BSD 3-Clause License + (c) 2022-2025 vmfunc, xyzeva & contributors + +*/ + +package detectors + +import ( + "net/http" + + fw "github.com/dropalldatabases/sif/pkg/scan/frameworks" +) + +func init() { + // Register all CMS detectors + fw.Register(&wordpressDetector{}) + fw.Register(&drupalDetector{}) + fw.Register(&joomlaDetector{}) + fw.Register(&magentoDetector{}) + fw.Register(&shopifyDetector{}) + fw.Register(&ghostDetector{}) +} + +// wordpressDetector detects WordPress CMS. +type wordpressDetector struct{} + +func (d *wordpressDetector) Name() string { return "WordPress" } + +func (d *wordpressDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "wp-content", Weight: 0.4}, + {Pattern: "wp-includes", Weight: 0.4}, + {Pattern: "wp-json", Weight: 0.3}, + {Pattern: "wordpress", Weight: 0.3}, + {Pattern: "wp-emoji", Weight: 0.2}, + } +} + +func (d *wordpressDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// drupalDetector detects Drupal CMS. +type drupalDetector struct{} + +func (d *drupalDetector) Name() string { return "Drupal" } + +func (d *drupalDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Drupal", Weight: 0.4, HeaderOnly: true}, + {Pattern: "drupal.js", Weight: 0.4}, + {Pattern: "/sites/default/files", Weight: 0.3}, + {Pattern: "Drupal.settings", Weight: 0.3}, + } +} + +func (d *drupalDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// joomlaDetector detects Joomla CMS. +type joomlaDetector struct{} + +func (d *joomlaDetector) Name() string { return "Joomla" } + +func (d *joomlaDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Joomla", Weight: 0.4}, + {Pattern: "/media/jui/", Weight: 0.4}, + {Pattern: "/components/com_", Weight: 0.3}, + {Pattern: "joomla.javascript", Weight: 0.3}, + } +} + +func (d *joomlaDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// magentoDetector detects Magento CMS. +type magentoDetector struct{} + +func (d *magentoDetector) Name() string { return "Magento" } + +func (d *magentoDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Magento", Weight: 0.4}, + {Pattern: "/static/frontend/", Weight: 0.4}, + {Pattern: "mage/", Weight: 0.3}, + {Pattern: "Mage.Cookies", Weight: 0.3}, + } +} + +func (d *magentoDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// shopifyDetector detects Shopify platform. +type shopifyDetector struct{} + +func (d *shopifyDetector) Name() string { return "Shopify" } + +func (d *shopifyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "Shopify", Weight: 0.5}, + {Pattern: "cdn.shopify.com", Weight: 0.4}, + {Pattern: "shopify-section", Weight: 0.4}, + {Pattern: "myshopify.com", Weight: 0.3}, + } +} + +func (d *shopifyDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// ghostDetector detects Ghost CMS. +type ghostDetector struct{} + +func (d *ghostDetector) Name() string { return "Ghost" } + +func (d *ghostDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "ghost-", Weight: 0.4}, + {Pattern: "Ghost", Weight: 0.3, HeaderOnly: true}, + {Pattern: "/ghost/api/", Weight: 0.4}, + } +} + +func (d *ghostDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} diff --git a/pkg/scan/frameworks/detectors/frontend.go b/pkg/scan/frameworks/detectors/frontend.go new file mode 100644 index 0000000..0c851b4 --- /dev/null +++ b/pkg/scan/frameworks/detectors/frontend.go @@ -0,0 +1,208 @@ +/* + + BSD 3-Clause License + (c) 2022-2025 vmfunc, xyzeva & contributors + +*/ + +package detectors + +import ( + "net/http" + + fw "github.com/dropalldatabases/sif/pkg/scan/frameworks" +) + +func init() { + // Register all frontend detectors + fw.Register(&reactDetector{}) + fw.Register(&vueDetector{}) + fw.Register(&angularDetector{}) + fw.Register(&svelteDetector{}) + fw.Register(&emberDetector{}) + fw.Register(&backboneDetector{}) + fw.Register(&meteorDetector{}) +} + +// reactDetector detects React framework. +type reactDetector struct{} + +func (d *reactDetector) Name() string { return "React" } + +func (d *reactDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "data-reactroot", Weight: 0.5}, + {Pattern: "react-dom", Weight: 0.4}, + {Pattern: "__REACT_DEVTOOLS", Weight: 0.4}, + {Pattern: "react.production", Weight: 0.4}, + {Pattern: "_reactRootContainer", Weight: 0.3}, + } +} + +func (d *reactDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// vueDetector detects Vue.js framework. +type vueDetector struct{} + +func (d *vueDetector) Name() string { return "Vue.js" } + +func (d *vueDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "data-v-", Weight: 0.5}, + {Pattern: "Vue.js", Weight: 0.4}, + {Pattern: "vue.runtime", Weight: 0.4}, + {Pattern: "vue.min.js", Weight: 0.4}, + {Pattern: "__vue__", Weight: 0.3}, + {Pattern: "v-cloak", Weight: 0.3}, + } +} + +func (d *vueDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// angularDetector detects Angular framework. +type angularDetector struct{} + +func (d *angularDetector) Name() string { return "Angular" } + +func (d *angularDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "ng-version", Weight: 0.5}, + {Pattern: "ng-app", Weight: 0.4}, + {Pattern: "ng-controller", Weight: 0.4}, + {Pattern: "angular.js", Weight: 0.4}, + {Pattern: "angular.min.js", Weight: 0.4}, + {Pattern: "ng-binding", Weight: 0.3}, + {Pattern: "_nghost", Weight: 0.3}, + {Pattern: "_ngcontent", Weight: 0.3}, + } +} + +func (d *angularDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// svelteDetector detects Svelte framework. +type svelteDetector struct{} + +func (d *svelteDetector) Name() string { return "Svelte" } + +func (d *svelteDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "svelte", Weight: 0.4}, + {Pattern: "__svelte", Weight: 0.5}, + {Pattern: "svelte-", Weight: 0.3}, + } +} + +func (d *svelteDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// emberDetector detects Ember.js framework. +type emberDetector struct{} + +func (d *emberDetector) Name() string { return "Ember.js" } + +func (d *emberDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "ember", Weight: 0.4}, + {Pattern: "ember-cli", Weight: 0.4}, + {Pattern: "data-ember", Weight: 0.3}, + } +} + +func (d *emberDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// backboneDetector detects Backbone.js framework. +type backboneDetector struct{} + +func (d *backboneDetector) Name() string { return "Backbone.js" } + +func (d *backboneDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "backbone", Weight: 0.4}, + {Pattern: "Backbone.", Weight: 0.4}, + } +} + +func (d *backboneDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// meteorDetector detects Meteor framework. +type meteorDetector struct{} + +func (d *meteorDetector) Name() string { return "Meteor" } + +func (d *meteorDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "__meteor_runtime_config__", Weight: 0.5}, + {Pattern: "meteor", Weight: 0.3}, + } +} + +func (d *meteorDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} diff --git a/pkg/scan/frameworks/detectors/meta.go b/pkg/scan/frameworks/detectors/meta.go new file mode 100644 index 0000000..0d0effb --- /dev/null +++ b/pkg/scan/frameworks/detectors/meta.go @@ -0,0 +1,149 @@ +/* + + BSD 3-Clause License + (c) 2022-2025 vmfunc, xyzeva & contributors + +*/ + +package detectors + +import ( + "net/http" + + fw "github.com/dropalldatabases/sif/pkg/scan/frameworks" +) + +func init() { + // Register all meta-framework detectors + fw.Register(&nextjsDetector{}) + fw.Register(&nuxtDetector{}) + fw.Register(&sveltekitDetector{}) + fw.Register(&gatsbyDetector{}) + fw.Register(&remixDetector{}) +} + +// nextjsDetector detects Next.js framework. +type nextjsDetector struct{} + +func (d *nextjsDetector) Name() string { return "Next.js" } + +func (d *nextjsDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "__NEXT_DATA__", Weight: 0.5}, + {Pattern: "_next/static", Weight: 0.4}, + {Pattern: "__next", Weight: 0.3}, + {Pattern: "x-nextjs", Weight: 0.3, HeaderOnly: true}, + } +} + +func (d *nextjsDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// nuxtDetector detects Nuxt.js framework. +type nuxtDetector struct{} + +func (d *nuxtDetector) Name() string { return "Nuxt.js" } + +func (d *nuxtDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "__NUXT__", Weight: 0.5}, + {Pattern: "_nuxt/", Weight: 0.4}, + {Pattern: "nuxt", Weight: 0.2}, + } +} + +func (d *nuxtDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// sveltekitDetector detects SvelteKit framework. +type sveltekitDetector struct{} + +func (d *sveltekitDetector) Name() string { return "SvelteKit" } + +func (d *sveltekitDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "__sveltekit", Weight: 0.5}, + {Pattern: "_app/immutable", Weight: 0.4}, + {Pattern: "sveltekit", Weight: 0.3}, + } +} + +func (d *sveltekitDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// gatsbyDetector detects Gatsby framework. +type gatsbyDetector struct{} + +func (d *gatsbyDetector) Name() string { return "Gatsby" } + +func (d *gatsbyDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "___gatsby", Weight: 0.5}, + {Pattern: "gatsby-", Weight: 0.4}, + {Pattern: "page-data.json", Weight: 0.3}, + } +} + +func (d *gatsbyDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} + +// remixDetector detects Remix framework. +type remixDetector struct{} + +func (d *remixDetector) Name() string { return "Remix" } + +func (d *remixDetector) Signatures() []fw.Signature { + return []fw.Signature{ + {Pattern: "__remixContext", Weight: 0.5}, + {Pattern: "remix", Weight: 0.3}, + {Pattern: "_remix", Weight: 0.4}, + } +} + +func (d *remixDetector) Detect(body string, headers http.Header) (float32, string) { + base := fw.NewBaseDetector(d.Name(), d.Signatures()) + score := base.MatchSignatures(body, headers) + confidence := sigmoidConfidence(score) + + var version string + if confidence > 0.5 { + version = fw.ExtractVersionOptimized(body, d.Name()).Version + } + return confidence, version +} diff --git a/pkg/scan/frameworks/result.go b/pkg/scan/frameworks/result.go new file mode 100644 index 0000000..3a170bd --- /dev/null +++ b/pkg/scan/frameworks/result.go @@ -0,0 +1,83 @@ +/* + + BSD 3-Clause License + (c) 2022-2025 vmfunc, xyzeva & contributors + +*/ + +package frameworks + +// FrameworkResult represents the result of framework detection. +type FrameworkResult struct { + Name string `json:"name"` + Version string `json:"version"` + Confidence float32 `json:"confidence"` + VersionConfidence float32 `json:"version_confidence"` + CVEs []string `json:"cves,omitempty"` + Suggestions []string `json:"suggestions,omitempty"` + RiskLevel string `json:"risk_level,omitempty"` +} + +// ResultType implements the ScanResult interface. +func (r *FrameworkResult) ResultType() string { return "framework" } + +// NewFrameworkResult creates a new FrameworkResult with the given parameters. +func NewFrameworkResult(name, version string, confidence, versionConfidence float32) *FrameworkResult { + return &FrameworkResult{ + Name: name, + Version: version, + Confidence: confidence, + VersionConfidence: versionConfidence, + } +} + +// WithVulnerabilities adds CVE information to the result. +func (r *FrameworkResult) WithVulnerabilities(cves, suggestions []string) *FrameworkResult { + r.CVEs = cves + r.Suggestions = suggestions + r.RiskLevel = determineRiskLevel(cves) + return r +} + +// determineRiskLevel calculates the risk level based on CVE severities. +func determineRiskLevel(cves []string) string { + if len(cves) == 0 { + return "low" + } + + for _, cve := range cves { + if containsSeverity(cve, "critical") { + return "critical" + } + } + + for _, cve := range cves { + if containsSeverity(cve, "high") { + return "high" + } + } + + return "medium" +} + +func containsSeverity(cve, severity string) bool { + // Simple substring match for now - could be more sophisticated + for i := 0; i+len(severity) <= len(cve); i++ { + match := true + for j := 0; j < len(severity); j++ { + c := cve[i+j] + // Case-insensitive comparison + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + if c != severity[j] { + match = false + break + } + } + if match { + return true + } + } + return false +} diff --git a/pkg/scan/frameworks/version.go b/pkg/scan/frameworks/version.go index d2701be..8f4553a 100644 --- a/pkg/scan/frameworks/version.go +++ b/pkg/scan/frameworks/version.go @@ -17,6 +17,13 @@ import ( "unicode" ) +// VersionMatch represents a version detection result with confidence. +type VersionMatch struct { + Version string + Confidence float32 + Source string // where the version was found +} + // compiledVersionPattern holds a pre-compiled regex for version extraction type compiledVersionPattern struct { re *regexp.Regexp @@ -159,8 +166,9 @@ func init() { } } -// extractVersionOptimized extracts version using pre-compiled patterns -func extractVersionOptimized(body string, framework string) VersionMatch { +// ExtractVersionOptimized extracts version using pre-compiled patterns. +// This is exported for use by individual detector implementations. +func ExtractVersionOptimized(body string, framework string) VersionMatch { patterns, exists := frameworkVersionPatterns[framework] if !exists { return VersionMatch{Version: "unknown", Confidence: 0, Source: ""} diff --git a/sif.go b/sif.go index 510377c..d450efd 100644 --- a/sif.go +++ b/sif.go @@ -80,12 +80,12 @@ func New(settings *config.Settings) (*App, error) { app.targets = settings.URLs } else if settings.File != "" { if _, err := os.Stat(settings.File); err != nil { - return app, err + return nil, err } data, err := os.Open(settings.File) if err != nil { - return app, err + return nil, err } defer data.Close() @@ -95,12 +95,30 @@ func New(settings *config.Settings) (*App, error) { app.targets = append(app.targets, scanner.Text()) } } else { - return app, fmt.Errorf("target(s) must be supplied with -u or -f\n\nSee 'sif -h' for more information") + return nil, fmt.Errorf("target(s) must be supplied with -u or -f\n\nSee 'sif -h' for more information") + } + + // Validate all URLs early + for _, url := range app.targets { + if err := validateURL(url); err != nil { + return nil, err + } } return app, nil } +// validateURL checks that a URL has a valid HTTP/HTTPS protocol. +func validateURL(url string) error { + if url == "" { + return fmt.Errorf("empty URL provided") + } + if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") { + return fmt.Errorf("URL %s must include http:// or https:// protocol", url) + } + return nil +} + // Run runs the pentesting suite, with the targets specified, according to the // settings specified. func (app *App) Run() error { @@ -122,10 +140,6 @@ func (app *App) Run() error { scansRun := make([]string, 0, 16) for _, url := range app.targets { - if !strings.Contains(url, "://") { - return fmt.Errorf("URL %s must include leading protocol", url) - } - log.Infof("πŸ“‘Starting scan on %s...", url) moduleResults := make([]ModuleResult, 0, 16) diff --git a/sif_test.go b/sif_test.go new file mode 100644 index 0000000..b5c3501 --- /dev/null +++ b/sif_test.go @@ -0,0 +1,178 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: β–ˆβ–€ β–ˆ β–ˆβ–€β–€ Β· Blazing-fast pentesting suite : +: β–„β–ˆ β–ˆ β–ˆβ–€ Β· BSD 3-Clause License : +: : +: (c) 2022-2025 vmfunc (Celeste Hickenlooper), xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package sif + +import ( + "testing" + + "github.com/dropalldatabases/sif/pkg/config" +) + +// mockResult is a test implementation of ScanResult +type mockResult struct { + name string + data string +} + +func (m *mockResult) ResultType() string { + return m.name +} + +func TestNewModuleResult(t *testing.T) { + tests := []struct { + name string + result *mockResult + wantID string + }{ + { + name: "basic result", + result: &mockResult{name: "test", data: "test data"}, + wantID: "test", + }, + { + name: "empty name", + result: &mockResult{name: "", data: "data"}, + wantID: "", + }, + { + name: "complex name", + result: &mockResult{name: "framework-detection", data: "Laravel 8.0"}, + wantID: "framework-detection", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mr := NewModuleResult(tt.result) + if mr.Id != tt.wantID { + t.Errorf("NewModuleResult() Id = %q, want %q", mr.Id, tt.wantID) + } + if mr.Data != tt.result { + t.Errorf("NewModuleResult() Data = %v, want %v", mr.Data, tt.result) + } + }) + } +} + +func TestNew_NoTargets(t *testing.T) { + settings := &config.Settings{ + URLs: []string{}, + File: "", + } + + _, err := New(settings) + if err == nil { + t.Error("New() should return error when no targets provided") + } +} + +func TestNew_WithURLs(t *testing.T) { + settings := &config.Settings{ + URLs: []string{"https://example.com"}, + ApiMode: true, + } + + app, err := New(settings) + if err != nil { + t.Fatalf("New() unexpected error: %v", err) + } + + if app == nil { + t.Fatal("New() returned nil app") + } + + if len(app.targets) != 1 { + t.Errorf("New() targets = %d, want 1", len(app.targets)) + } + + if app.targets[0] != "https://example.com" { + t.Errorf("New() target = %q, want %q", app.targets[0], "https://example.com") + } +} + +func TestNew_URLValidation(t *testing.T) { + tests := []struct { + name string + url string + wantErr bool + }{ + { + name: "valid https url", + url: "https://example.com", + wantErr: false, + }, + { + name: "valid http url", + url: "http://example.com", + wantErr: false, + }, + { + name: "missing protocol", + url: "example.com", + wantErr: true, + }, + { + name: "invalid protocol", + url: "ftp://example.com", + wantErr: true, + }, + { + name: "empty url", + url: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + settings := &config.Settings{ + URLs: []string{tt.url}, + ApiMode: true, + } + + _, err := New(settings) + if (err != nil) != tt.wantErr { + t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestModuleResult_JSON(t *testing.T) { + mr := ModuleResult{ + Id: "test", + Data: map[string]string{"key": "value"}, + } + + // Verify the struct can be used (basic sanity check) + if mr.Id != "test" { + t.Errorf("ModuleResult.Id = %q, want %q", mr.Id, "test") + } +} + +func TestUrlResult_JSON(t *testing.T) { + ur := UrlResult{ + Url: "https://example.com", + Results: []ModuleResult{ + {Id: "test", Data: "data"}, + }, + } + + if ur.Url != "https://example.com" { + t.Errorf("UrlResult.Url = %q, want %q", ur.Url, "https://example.com") + } + + if len(ur.Results) != 1 { + t.Errorf("UrlResult.Results = %d, want 1", len(ur.Results)) + } +}