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))
+ }
+}