Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: framework detection module #40

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/runtest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ type Settings struct {
Headers bool
CloudStorage bool
SubdomainTakeover bool
Framework bool
}

const (
Expand Down Expand Up @@ -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",
Expand Down
225 changes: 225 additions & 0 deletions pkg/scan/frameworks/detect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
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"`
}

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: `<meta name="csrf-token"`, Weight: 0.3},
},
"Django": {
{Pattern: `csrfmiddlewaretoken`, Weight: 0.4, HeaderOnly: true},
{Pattern: `django.contrib`, Weight: 0.3},
{Pattern: `django.core`, Weight: 0.3},
{Pattern: `__admin_media_prefix__`, Weight: 0.3},
},
"Ruby on Rails": {
{Pattern: `csrf-param`, Weight: 0.4, HeaderOnly: true},
{Pattern: `csrf-token`, Weight: 0.3, HeaderOnly: true},
{Pattern: `ruby-on-rails`, Weight: 0.3},
{Pattern: `rails-env`, Weight: 0.3},
},
"Express.js": {
{Pattern: `express`, Weight: 0.4, HeaderOnly: true},
{Pattern: `connect.sid`, Weight: 0.3, HeaderOnly: true},
},
"ASP.NET": {
{Pattern: `ASP.NET`, Weight: 0.4, HeaderOnly: true},
{Pattern: `__VIEWSTATE`, Weight: 0.3},
{Pattern: `__EVENTVALIDATION`, Weight: 0.3},
},
"Spring": {
{Pattern: `org.springframework`, Weight: 0.4, HeaderOnly: true},
{Pattern: `spring-security`, Weight: 0.3, HeaderOnly: true},
{Pattern: `jsessionid`, Weight: 0.3, HeaderOnly: true},
},
"Flask": {
{Pattern: `flask`, Weight: 0.4, HeaderOnly: true},
{Pattern: `werkzeug`, Weight: 0.3, HeaderOnly: true},
{Pattern: `jinja2`, Weight: 0.3},
},
}

func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Framework Detection") + "..."))

frameworklog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Framework Detection 🔍",
}).With("url", url)

client := &http.Client{
Timeout: timeout,
}

resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
bodyStr := string(body)

var bestMatch string
var highestConfidence float32

for framework, signatures := range frameworkSignatures {
var weightedScore float32
var totalWeight float32

for _, sig := range signatures {
totalWeight += sig.Weight

if sig.HeaderOnly {
if containsHeader(resp.Header, sig.Pattern) {
weightedScore += sig.Weight
}
} else if strings.Contains(bodyStr, sig.Pattern) {
weightedScore += sig.Weight
}
}

confidence := float32(1.0 / (1.0 + exp(-float64(weightedScore/totalWeight)*6.0)))

if confidence > 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)

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 {
version := extractVersion(body, framework)
if version == "Unknown" {
return version
}

parts := strings.Split(version, ".")
var normalized string
if len(parts) >= 3 {
normalized = fmt.Sprintf("%05s.%05s.%05s", parts[0], parts[1], parts[2])
}
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) {
// 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
}

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"
}
11 changes: 11 additions & 0 deletions sif.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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 {
Expand Down
Loading