From 1443721c906ea2c0d62bbf041cffa946659a4a2a Mon Sep 17 00:00:00 2001 From: vmfunc Date: Fri, 22 Nov 2024 03:20:32 -0500 Subject: [PATCH 1/3] feat: framework detection module --- pkg/config/config.go | 2 + pkg/scan/frameworks/detect.go | 179 ++++++++++++++++++++++++++++++++++ sif.go | 11 +++ 3 files changed, 192 insertions(+) create mode 100644 pkg/scan/frameworks/detect.go diff --git a/pkg/config/config.go b/pkg/config/config.go index 30bb8eb..c2c3e43 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -53,6 +53,7 @@ type Settings struct { Headers bool CloudStorage bool SubdomainTakeover bool + Framework bool } const ( @@ -95,6 +96,7 @@ func Parse() *Settings { flagSet.BoolVar(&settings.Headers, "headers", false, "Enable HTTP Header Analysis"), flagSet.BoolVar(&settings.CloudStorage, "c3", false, "Enable C3 Misconfiguration Scan"), flagSet.BoolVar(&settings.SubdomainTakeover, "st", false, "Enable Subdomain Takeover Check"), + flagSet.BoolVar(&settings.Framework, "framework", false, "Enable framework detection"), ) flagSet.CreateGroup("runtime", "Runtime", diff --git a/pkg/scan/frameworks/detect.go b/pkg/scan/frameworks/detect.go new file mode 100644 index 0000000..c96638a --- /dev/null +++ b/pkg/scan/frameworks/detect.go @@ -0,0 +1,179 @@ +package frameworks + +import ( + "fmt" + "io" + "net/http" + "os" + "regexp" + "strings" + "time" + + "github.com/charmbracelet/log" + "github.com/dropalldatabases/sif/internal/styles" + "github.com/dropalldatabases/sif/pkg/logger" +) + +type FrameworkResult struct { + Name string `json:"name"` + Version string `json:"version"` + Confidence float32 `json:"confidence"` + CVEs []string `json:"cves,omitempty"` + Suggestions []string `json:"suggestions,omitempty"` +} + +var frameworkSignatures = map[string][]string{ + "Laravel": { + `laravel_session`, + `XSRF-TOKEN`, + ` highestConfidence { + highestConfidence = confidence + bestMatch = framework + } + } + + if highestConfidence > 0 { + version := detectVersion(bodyStr, bestMatch) + result := &FrameworkResult{ + Name: bestMatch, + Version: version, + Confidence: highestConfidence, + } + + if logdir != "" { + logger.Write(url, logdir, fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f)\n", + bestMatch, version, highestConfidence)) + } + + frameworklog.Infof("Detected %s framework (version: %s) with %.2f confidence", + styles.Highlight.Render(bestMatch), version, highestConfidence) + + // Add CVEs and suggestions based on version + if cves, suggestions := getVulnerabilities(bestMatch, version); len(cves) > 0 { + result.CVEs = cves + result.Suggestions = suggestions + for _, cve := range cves { + frameworklog.Warnf("Found potential vulnerability: %s", styles.Highlight.Render(cve)) + } + } + + return result, nil + } + + frameworklog.Info("No framework detected") + return nil, nil +} + +func containsHeader(headers http.Header, signature string) bool { + for _, values := range headers { + for _, value := range values { + if strings.Contains(strings.ToLower(value), strings.ToLower(signature)) { + return true + } + } + } + return false +} + +func detectVersion(body string, framework string) string { + patterns := map[string]*regexp.Regexp{ + "Laravel": regexp.MustCompile(`Laravel[/\s+]?([\d.]+)`), + "Django": regexp.MustCompile(`Django/([\d.]+)`), + "Ruby on Rails": regexp.MustCompile(`Rails/([\d.]+)`), + "Express.js": regexp.MustCompile(`express/([\d.]+)`), + "ASP.NET": regexp.MustCompile(`ASP\.NET[/\s+]?([\d.]+)`), + "Spring": regexp.MustCompile(`spring-(core|framework)/([\d.]+)`), + "Flask": regexp.MustCompile(`Flask/([\d.]+)`), + } + + if pattern, exists := patterns[framework]; exists { + matches := pattern.FindStringSubmatch(body) + if len(matches) > 1 { + return matches[1] + } + } + return "Unknown" +} + +func getVulnerabilities(framework, version string) ([]string, []string) { + // TODO: Implement CVE database lookup + if framework == "Laravel" && version == "8.0.0" { + return []string{ + "CVE-2021-3129", + }, []string{ + "Update to Laravel 8.4.2 or later", + "Implement additional input validation", + } + } + return nil, nil +} diff --git a/sif.go b/sif.go index e6cf276..4f16a7a 100644 --- a/sif.go +++ b/sif.go @@ -16,6 +16,7 @@ import ( "github.com/dropalldatabases/sif/pkg/config" "github.com/dropalldatabases/sif/pkg/logger" "github.com/dropalldatabases/sif/pkg/scan" + "github.com/dropalldatabases/sif/pkg/scan/frameworks" jsscan "github.com/dropalldatabases/sif/pkg/scan/js" ) @@ -113,6 +114,16 @@ func (app *App) Run() error { scansRun = append(scansRun, "Basic Scan") } + if app.settings.Framework { + result, err := frameworks.DetectFramework(url, app.settings.Timeout, app.settings.LogDir) + if err != nil { + log.Errorf("Error while running framework detection: %s", err) + } else if result != nil { + moduleResults = append(moduleResults, ModuleResult{"framework", result}) + scansRun = append(scansRun, "Framework Detection") + } + } + if app.settings.Dirlist != "none" { result, err := scan.Dirlist(app.settings.Dirlist, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir) if err != nil { From 13c7ce1475e7721eddfe800f34765e8686ae8e69 Mon Sep 17 00:00:00 2001 From: vmfunc Date: Fri, 22 Nov 2024 03:48:34 -0500 Subject: [PATCH 2/3] feat(framework-detection): weighted bayesian detection algorithm - weighted signature matching for more accurate framework detection - sigmoid normalization for confidence scores - version detection with semantic versioning support - header-only pattern --- pkg/scan/frameworks/detect.go | 130 +++++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 42 deletions(-) diff --git a/pkg/scan/frameworks/detect.go b/pkg/scan/frameworks/detect.go index c96638a..c1b0814 100644 --- a/pkg/scan/frameworks/detect.go +++ b/pkg/scan/frameworks/detect.go @@ -22,42 +22,48 @@ type FrameworkResult struct { Suggestions []string `json:"suggestions,omitempty"` } -var frameworkSignatures = map[string][]string{ +type FrameworkSignature struct { + Pattern string + Weight float32 + HeaderOnly bool +} + +var frameworkSignatures = map[string][]FrameworkSignature{ "Laravel": { - `laravel_session`, - `XSRF-TOKEN`, - ` highestConfidence { highestConfidence = confidence bestMatch = framework @@ -118,7 +133,6 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo frameworklog.Infof("Detected %s framework (version: %s) with %.2f confidence", styles.Highlight.Render(bestMatch), version, highestConfidence) - // Add CVEs and suggestions based on version if cves, suggestions := getVulnerabilities(bestMatch, version); len(cves) > 0 { result.CVEs = cves result.Suggestions = suggestions @@ -146,23 +160,34 @@ func containsHeader(headers http.Header, signature string) bool { } func detectVersion(body string, framework string) string { - patterns := map[string]*regexp.Regexp{ - "Laravel": regexp.MustCompile(`Laravel[/\s+]?([\d.]+)`), - "Django": regexp.MustCompile(`Django/([\d.]+)`), - "Ruby on Rails": regexp.MustCompile(`Rails/([\d.]+)`), - "Express.js": regexp.MustCompile(`express/([\d.]+)`), - "ASP.NET": regexp.MustCompile(`ASP\.NET[/\s+]?([\d.]+)`), - "Spring": regexp.MustCompile(`spring-(core|framework)/([\d.]+)`), - "Flask": regexp.MustCompile(`Flask/([\d.]+)`), + version := extractVersion(body, framework) + if version == "Unknown" { + return version } - if pattern, exists := patterns[framework]; exists { - matches := pattern.FindStringSubmatch(body) - if len(matches) > 1 { - return matches[1] - } + parts := strings.Split(version, ".") + var normalized string + if len(parts) >= 3 { + normalized = fmt.Sprintf("%05s.%05s.%05s", parts[0], parts[1], parts[2]) } - return "Unknown" + return normalized +} + +func exp(x float64) float64 { + if x > 88.0 { + return 1e38 + } + if x < -88.0 { + return 0 + } + + sum := 1.0 + term := 1.0 + for i := 1; i <= 20; i++ { + term *= x / float64(i) + sum += term + } + return sum } func getVulnerabilities(framework, version string) ([]string, []string) { @@ -177,3 +202,24 @@ func getVulnerabilities(framework, version string) ([]string, []string) { } return nil, nil } + +func extractVersion(body string, framework string) string { + versionPatterns := map[string]string{ + "Laravel": `Laravel\s+[Vv]?(\d+\.\d+\.\d+)`, + "Django": `Django\s+[Vv]?(\d+\.\d+\.\d+)`, + "Ruby on Rails": `Rails\s+[Vv]?(\d+\.\d+\.\d+)`, + "Express.js": `Express\s+[Vv]?(\d+\.\d+\.\d+)`, + "ASP.NET": `ASP\.NET\s+[Vv]?(\d+\.\d+\.\d+)`, + "Spring": `Spring\s+[Vv]?(\d+\.\d+\.\d+)`, + "Flask": `Flask\s+[Vv]?(\d+\.\d+\.\d+)`, + } + + if pattern, exists := versionPatterns[framework]; exists { + re := regexp.MustCompile(pattern) + matches := re.FindStringSubmatch(body) + if len(matches) > 1 { + return matches[1] + } + } + return "Unknown" +} From ebc9b99a2d1bbe366dcaa82f5b4e05c6b6ab55fe Mon Sep 17 00:00:00 2001 From: vmfunc Date: Fri, 22 Nov 2024 10:45:21 -0500 Subject: [PATCH 3/3] chore(actions): add framework to CI --- .github/workflows/runtest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/runtest.yml b/.github/workflows/runtest.yml index 7486550..feacb6c 100644 --- a/.github/workflows/runtest.yml +++ b/.github/workflows/runtest.yml @@ -20,7 +20,7 @@ jobs: run: make - name: Run Sif with features run: | - ./sif -u https://google.com -dnslist small -dirlist small -dork -git -whois -cms + ./sif -u https://example.com -dnslist small -dirlist small -dork -git -whois -cms -framework if [ $? -eq 0 ]; then echo "Sif ran successfully" else