mirror of
https://github.com/lunchcat/sif.git
synced 2026-01-12 21:13:50 -08:00
refactor: rewrite framework detection with modular detector architecture
- create detector interface and registry for extensibility - extract detectors to separate files: backend.go, frontend.go, cms.go, meta.go - reduce detect.go from 785 lines to 178 lines (pure orchestrator) - export VersionMatch and ExtractVersionOptimized for detector use - create result.go with NewFrameworkResult and WithVulnerabilities helpers - add url validation to New() for early error detection - add sif_test.go with main package tests - update detect_test.go to use external test package pattern
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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: `<meta name="csrf-token"`, Weight: 0.3},
|
||||
},
|
||||
"Django": {
|
||||
{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},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{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},
|
||||
},
|
||||
"Express.js": {
|
||||
{Pattern: `Express`, Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: `connect.sid`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{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},
|
||||
},
|
||||
"ASP.NET Core": {
|
||||
{Pattern: `.AspNetCore.`, Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: `blazor`, Weight: 0.4},
|
||||
{Pattern: `_blazor`, Weight: 0.4},
|
||||
{Pattern: `dotnet`, Weight: 0.2, HeaderOnly: true},
|
||||
},
|
||||
"Spring": {
|
||||
{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},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{Pattern: `spring-boot`, Weight: 0.5},
|
||||
{Pattern: `actuator`, Weight: 0.3},
|
||||
{Pattern: `whitelabel`, Weight: 0.2},
|
||||
},
|
||||
"Flask": {
|
||||
{Pattern: `Werkzeug`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `flask`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `jinja2`, Weight: 0.3},
|
||||
},
|
||||
"Next.js": {
|
||||
{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},
|
||||
},
|
||||
"Nuxt.js": {
|
||||
{Pattern: `__NUXT__`, Weight: 0.5},
|
||||
{Pattern: `_nuxt/`, Weight: 0.4},
|
||||
{Pattern: `nuxt`, Weight: 0.2},
|
||||
},
|
||||
"Vue.js": {
|
||||
{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},
|
||||
},
|
||||
"Angular": {
|
||||
{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},
|
||||
},
|
||||
"React": {
|
||||
{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},
|
||||
},
|
||||
"Svelte": {
|
||||
{Pattern: `svelte`, Weight: 0.4},
|
||||
{Pattern: `__svelte`, Weight: 0.5},
|
||||
{Pattern: `svelte-`, Weight: 0.3},
|
||||
},
|
||||
"SvelteKit": {
|
||||
{Pattern: `__sveltekit`, Weight: 0.5},
|
||||
{Pattern: `_app/immutable`, Weight: 0.4},
|
||||
{Pattern: `sveltekit`, Weight: 0.3},
|
||||
},
|
||||
"Remix": {
|
||||
{Pattern: `__remixContext`, Weight: 0.5},
|
||||
{Pattern: `remix`, Weight: 0.3},
|
||||
{Pattern: `_remix`, Weight: 0.4},
|
||||
},
|
||||
"Gatsby": {
|
||||
{Pattern: `___gatsby`, Weight: 0.5},
|
||||
{Pattern: `gatsby-`, Weight: 0.4},
|
||||
{Pattern: `page-data.json`, Weight: 0.3},
|
||||
},
|
||||
"WordPress": {
|
||||
{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},
|
||||
},
|
||||
"Drupal": {
|
||||
{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},
|
||||
},
|
||||
"Joomla": {
|
||||
{Pattern: `Joomla`, Weight: 0.4},
|
||||
{Pattern: `/media/jui/`, Weight: 0.4},
|
||||
{Pattern: `/components/com_`, Weight: 0.3},
|
||||
{Pattern: `joomla.javascript`, Weight: 0.3},
|
||||
},
|
||||
"Magento": {
|
||||
{Pattern: `Magento`, Weight: 0.4},
|
||||
{Pattern: `/static/frontend/`, Weight: 0.4},
|
||||
{Pattern: `mage/`, Weight: 0.3},
|
||||
{Pattern: `Mage.Cookies`, Weight: 0.3},
|
||||
},
|
||||
"Shopify": {
|
||||
{Pattern: `Shopify`, Weight: 0.5},
|
||||
{Pattern: `cdn.shopify.com`, Weight: 0.4},
|
||||
{Pattern: `shopify-section`, Weight: 0.4},
|
||||
{Pattern: `myshopify.com`, Weight: 0.3},
|
||||
},
|
||||
"Ghost": {
|
||||
{Pattern: `ghost-`, Weight: 0.4},
|
||||
{Pattern: `Ghost`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `/ghost/api/`, Weight: 0.4},
|
||||
},
|
||||
"Symfony": {
|
||||
{Pattern: `symfony`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `sf_`, Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `_sf2_`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"FastAPI": {
|
||||
{Pattern: `fastapi`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `starlette`, Weight: 0.3, HeaderOnly: true},
|
||||
},
|
||||
"Gin": {
|
||||
{Pattern: `gin-gonic`, Weight: 0.4},
|
||||
{Pattern: `gin`, Weight: 0.2, HeaderOnly: true},
|
||||
},
|
||||
"Phoenix": {
|
||||
{Pattern: `_csrf_token`, Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: `phx-`, Weight: 0.3},
|
||||
{Pattern: `phoenix`, Weight: 0.2},
|
||||
},
|
||||
"Ember.js": {
|
||||
{Pattern: `ember`, Weight: 0.4},
|
||||
{Pattern: `ember-cli`, Weight: 0.4},
|
||||
{Pattern: `data-ember`, Weight: 0.3},
|
||||
},
|
||||
"Backbone.js": {
|
||||
{Pattern: `backbone`, Weight: 0.4},
|
||||
{Pattern: `Backbone.`, Weight: 0.4},
|
||||
},
|
||||
"Meteor": {
|
||||
{Pattern: `__meteor_runtime_config__`, Weight: 0.5},
|
||||
{Pattern: `meteor`, Weight: 0.3},
|
||||
},
|
||||
"Strapi": {
|
||||
{Pattern: `strapi`, Weight: 0.4},
|
||||
{Pattern: `/api/`, Weight: 0.2},
|
||||
},
|
||||
"AdonisJS": {
|
||||
{Pattern: `adonis`, Weight: 0.4},
|
||||
{Pattern: `_csrf`, Weight: 0.2, HeaderOnly: true},
|
||||
},
|
||||
"CakePHP": {
|
||||
{Pattern: `cakephp`, Weight: 0.4},
|
||||
{Pattern: `cake`, Weight: 0.2},
|
||||
},
|
||||
"CodeIgniter": {
|
||||
{Pattern: `codeigniter`, Weight: 0.4},
|
||||
{Pattern: `ci_session`, Weight: 0.4, HeaderOnly: true},
|
||||
},
|
||||
}
|
||||
|
||||
// frameworkMatch holds the result of checking a single framework
|
||||
type frameworkMatch struct {
|
||||
framework string
|
||||
// detectionResult holds the result from a single detector.
|
||||
type detectionResult struct {
|
||||
name string
|
||||
confidence float32
|
||||
version string
|
||||
}
|
||||
|
||||
// DetectFramework runs all registered detectors against the target URL.
|
||||
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Framework Detection") + "..."))
|
||||
|
||||
@@ -260,9 +46,7 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
|
||||
Prefix: "Framework Detection 🔍",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
@@ -270,136 +54,96 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Limit body read to 5MB to prevent memory exhaustion
|
||||
const maxBodySize = 5 * 1024 * 1024
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
// concurrent framework detection
|
||||
results := make(chan frameworkMatch, len(frameworkSignatures))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for framework, signatures := range frameworkSignatures {
|
||||
wg.Add(1)
|
||||
go func(fw string, sigs []FrameworkSignature) {
|
||||
defer wg.Done()
|
||||
|
||||
var weightedScore float32
|
||||
var totalWeight float32
|
||||
|
||||
for _, sig := range sigs {
|
||||
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 + math.Exp(-float64(weightedScore/totalWeight)*6.0)))
|
||||
results <- frameworkMatch{framework: fw, confidence: confidence}
|
||||
}(framework, signatures)
|
||||
// Get all registered detectors
|
||||
detectors := GetDetectors()
|
||||
if len(detectors) == 0 {
|
||||
frameworklog.Warn("No framework detectors registered")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// close results channel when all goroutines complete
|
||||
// Run all detectors concurrently
|
||||
results := make(chan detectionResult, len(detectors))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, detector := range detectors {
|
||||
wg.Add(1)
|
||||
go func(d Detector) {
|
||||
defer wg.Done()
|
||||
confidence, version := d.Detect(bodyStr, resp.Header)
|
||||
results <- detectionResult{
|
||||
name: d.Name(),
|
||||
confidence: confidence,
|
||||
version: version,
|
||||
}
|
||||
}(detector)
|
||||
}
|
||||
|
||||
// Close results channel when all goroutines complete
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
// find the best match
|
||||
var bestMatch string
|
||||
var highestConfidence float32
|
||||
|
||||
for match := range results {
|
||||
if match.confidence > 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
133
pkg/scan/frameworks/detector.go
Normal file
133
pkg/scan/frameworks/detector.go
Normal file
@@ -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
|
||||
}
|
||||
476
pkg/scan/frameworks/detectors/backend.go
Normal file
476
pkg/scan/frameworks/detectors/backend.go
Normal file
@@ -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: `<meta name="csrf-token"`, Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *laravelDetector) 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
180
pkg/scan/frameworks/detectors/cms.go
Normal file
180
pkg/scan/frameworks/detectors/cms.go
Normal file
@@ -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
|
||||
}
|
||||
208
pkg/scan/frameworks/detectors/frontend.go
Normal file
208
pkg/scan/frameworks/detectors/frontend.go
Normal file
@@ -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
|
||||
}
|
||||
149
pkg/scan/frameworks/detectors/meta.go
Normal file
149
pkg/scan/frameworks/detectors/meta.go
Normal file
@@ -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
|
||||
}
|
||||
83
pkg/scan/frameworks/result.go
Normal file
83
pkg/scan/frameworks/result.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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: ""}
|
||||
|
||||
28
sif.go
28
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)
|
||||
|
||||
178
sif_test.go
Normal file
178
sif_test.go
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user