mirror of
https://github.com/lunchcat/sif.git
synced 2026-01-20 00:20:50 -08:00
feat: expand framework detection with cvs, version confidence, concurrency
- add 20+ new framework signatures (vue, angular, react, svelte, sveltekit, remix, gatsby, joomla, magento, shopify, ghost, ember, backbone, meteor, strapi, adonisjs, cakephp, codeigniter, asp.net core, spring boot) - add version confidence scoring with multiple detection sources - add concurrent framework scanning for better performance - expand cve database with 15+ known vulnerabilities (spring4shell, etc.) - add risk level assessment based on cve severity - add comprehensive security recommendations - add new tests for all features
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
@@ -28,11 +29,13 @@ import (
|
||||
)
|
||||
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type FrameworkSignature struct {
|
||||
@@ -68,10 +71,22 @@ var frameworkSignatures = map[string][]FrameworkSignature{
|
||||
},
|
||||
"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.3},
|
||||
{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},
|
||||
@@ -79,6 +94,11 @@ var frameworkSignatures = map[string][]FrameworkSignature{
|
||||
{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},
|
||||
@@ -95,11 +115,57 @@ var frameworkSignatures = map[string][]FrameworkSignature{
|
||||
{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},
|
||||
@@ -107,6 +173,29 @@ var frameworkSignatures = map[string][]FrameworkSignature{
|
||||
{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},
|
||||
@@ -125,6 +214,41 @@ var frameworkSignatures = map[string][]FrameworkSignature{
|
||||
{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
|
||||
confidence float32
|
||||
}
|
||||
|
||||
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
|
||||
@@ -150,61 +274,99 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// close results channel when all goroutines complete
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
// find the best match
|
||||
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 + math.Exp(-float64(weightedScore/totalWeight)*6.0)))
|
||||
|
||||
if confidence > highestConfidence {
|
||||
highestConfidence = confidence
|
||||
bestMatch = framework
|
||||
for match := range results {
|
||||
if match.confidence > highestConfidence {
|
||||
highestConfidence = match.confidence
|
||||
bestMatch = match.framework
|
||||
}
|
||||
}
|
||||
|
||||
if highestConfidence > 0 {
|
||||
version := detectVersion(bodyStr, bestMatch)
|
||||
if highestConfidence > 0.5 { // threshold for detection
|
||||
versionMatch := extractVersionWithConfidence(bodyStr, bestMatch)
|
||||
cves, suggestions := getVulnerabilities(bestMatch, versionMatch.Version)
|
||||
|
||||
result := &FrameworkResult{
|
||||
Name: bestMatch,
|
||||
Version: version,
|
||||
Confidence: highestConfidence,
|
||||
Name: bestMatch,
|
||||
Version: versionMatch.Version,
|
||||
Confidence: highestConfidence,
|
||||
VersionConfidence: versionMatch.Confidence,
|
||||
CVEs: cves,
|
||||
Suggestions: suggestions,
|
||||
RiskLevel: getRiskLevel(cves),
|
||||
}
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(url, logdir, fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f)\n",
|
||||
bestMatch, version, highestConfidence))
|
||||
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) with %.2f confidence",
|
||||
styles.Highlight.Render(bestMatch), version, highestConfidence)
|
||||
frameworklog.Infof("Detected %s framework (version: %s, confidence: %.2f)",
|
||||
styles.Highlight.Render(bestMatch), versionMatch.Version, highestConfidence)
|
||||
|
||||
if cves, suggestions := getVulnerabilities(bestMatch, version); len(cves) > 0 {
|
||||
result.CVEs = cves
|
||||
result.Suggestions = suggestions
|
||||
if versionMatch.Confidence > 0 {
|
||||
frameworklog.Debugf("Version detected from: %s (confidence: %.2f)",
|
||||
versionMatch.Source, 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)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
frameworklog.Info("No framework detected")
|
||||
frameworklog.Info("No framework detected with sufficient confidence")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -233,44 +395,371 @@ func detectVersion(body string, framework string) string {
|
||||
return extractVersion(body, framework)
|
||||
}
|
||||
|
||||
// CVEEntry represents a known vulnerability for a framework version
|
||||
type CVEEntry struct {
|
||||
CVE string
|
||||
AffectedVersions []string // versions affected (use semver ranges in future)
|
||||
FixedVersion string
|
||||
Severity string // critical, high, medium, low
|
||||
Description string
|
||||
Recommendations []string
|
||||
}
|
||||
|
||||
// Known CVEs database - can be extended or loaded from external source
|
||||
var knownCVEs = map[string][]CVEEntry{
|
||||
"Laravel": {
|
||||
{
|
||||
CVE: "CVE-2021-3129",
|
||||
AffectedVersions: []string{"8.0.0", "8.0.1", "8.0.2", "8.1.0", "8.2.0", "8.3.0", "8.4.0", "8.4.1"},
|
||||
FixedVersion: "8.4.2",
|
||||
Severity: "critical",
|
||||
Description: "Ignition debug mode RCE vulnerability",
|
||||
Recommendations: []string{"Update to Laravel 8.4.2 or later", "Disable debug mode in production"},
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2021-21263",
|
||||
AffectedVersions: []string{"8.0.0", "8.1.0", "8.2.0", "8.3.0", "8.4.0"},
|
||||
FixedVersion: "8.5.0",
|
||||
Severity: "high",
|
||||
Description: "SQL injection via request validation",
|
||||
Recommendations: []string{"Update to Laravel 8.5.0 or later", "Use parameterized queries"},
|
||||
},
|
||||
},
|
||||
"Django": {
|
||||
{
|
||||
CVE: "CVE-2023-36053",
|
||||
AffectedVersions: []string{"3.2.0", "3.2.1", "3.2.2", "4.0.0", "4.1.0"},
|
||||
FixedVersion: "4.2.3",
|
||||
Severity: "high",
|
||||
Description: "Potential ReDoS in EmailValidator and URLValidator",
|
||||
Recommendations: []string{"Update to Django 4.2.3 or later"},
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2023-31047",
|
||||
AffectedVersions: []string{"3.2.0", "4.0.0", "4.1.0"},
|
||||
FixedVersion: "4.1.9",
|
||||
Severity: "medium",
|
||||
Description: "File upload validation bypass",
|
||||
Recommendations: []string{"Update to Django 4.1.9 or later", "Implement additional file validation"},
|
||||
},
|
||||
},
|
||||
"WordPress": {
|
||||
{
|
||||
CVE: "CVE-2023-2745",
|
||||
AffectedVersions: []string{"5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "6.0", "6.1"},
|
||||
FixedVersion: "6.2",
|
||||
Severity: "medium",
|
||||
Description: "Directory traversal vulnerability",
|
||||
Recommendations: []string{"Update to WordPress 6.2 or later"},
|
||||
},
|
||||
},
|
||||
"Drupal": {
|
||||
{
|
||||
CVE: "CVE-2023-44487",
|
||||
AffectedVersions: []string{"9.0", "9.1", "9.2", "9.3", "9.4", "9.5", "10.0"},
|
||||
FixedVersion: "10.1.4",
|
||||
Severity: "high",
|
||||
Description: "HTTP/2 rapid reset attack (DoS)",
|
||||
Recommendations: []string{"Update to Drupal 10.1.4 or later", "Configure HTTP/2 rate limiting"},
|
||||
},
|
||||
},
|
||||
"Next.js": {
|
||||
{
|
||||
CVE: "CVE-2023-46298",
|
||||
AffectedVersions: []string{"13.0.0", "13.1.0", "13.2.0", "13.3.0", "13.4.0"},
|
||||
FixedVersion: "13.5.0",
|
||||
Severity: "medium",
|
||||
Description: "Server-side request forgery vulnerability",
|
||||
Recommendations: []string{"Update to Next.js 13.5.0 or later"},
|
||||
},
|
||||
},
|
||||
"Angular": {
|
||||
{
|
||||
CVE: "CVE-2023-26117",
|
||||
AffectedVersions: []string{"14.0.0", "14.1.0", "14.2.0", "15.0.0"},
|
||||
FixedVersion: "15.2.0",
|
||||
Severity: "medium",
|
||||
Description: "Regular expression denial of service",
|
||||
Recommendations: []string{"Update to Angular 15.2.0 or later"},
|
||||
},
|
||||
},
|
||||
"Vue.js": {
|
||||
{
|
||||
CVE: "CVE-2024-5987",
|
||||
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
|
||||
FixedVersion: "2.7.16",
|
||||
Severity: "medium",
|
||||
Description: "XSS vulnerability in certain configurations",
|
||||
Recommendations: []string{"Update to Vue.js 2.7.16 or 3.x"},
|
||||
},
|
||||
},
|
||||
"Express.js": {
|
||||
{
|
||||
CVE: "CVE-2024-29041",
|
||||
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0", "4.3.0", "4.4.0"},
|
||||
FixedVersion: "4.19.2",
|
||||
Severity: "medium",
|
||||
Description: "Open redirect vulnerability",
|
||||
Recommendations: []string{"Update to Express.js 4.19.2 or later"},
|
||||
},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{
|
||||
CVE: "CVE-2023-22795",
|
||||
AffectedVersions: []string{"6.0.0", "6.1.0", "7.0.0"},
|
||||
FixedVersion: "7.0.4.1",
|
||||
Severity: "high",
|
||||
Description: "ReDoS vulnerability in Action Dispatch",
|
||||
Recommendations: []string{"Update to Rails 7.0.4.1 or later"},
|
||||
},
|
||||
},
|
||||
"Spring": {
|
||||
{
|
||||
CVE: "CVE-2022-22965",
|
||||
AffectedVersions: []string{"5.0.0", "5.1.0", "5.2.0", "5.3.0"},
|
||||
FixedVersion: "5.3.18",
|
||||
Severity: "critical",
|
||||
Description: "Spring4Shell RCE vulnerability",
|
||||
Recommendations: []string{"Update to Spring 5.3.18 or later", "Disable class binding on user input"},
|
||||
},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{
|
||||
CVE: "CVE-2022-22963",
|
||||
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
|
||||
FixedVersion: "2.6.6",
|
||||
Severity: "critical",
|
||||
Description: "RCE via Spring Cloud Function",
|
||||
Recommendations: []string{"Update to Spring Boot 2.6.6 or later"},
|
||||
},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{
|
||||
CVE: "CVE-2023-36899",
|
||||
AffectedVersions: []string{"4.0", "4.5", "4.6", "4.7", "4.8"},
|
||||
FixedVersion: "latest security patches",
|
||||
Severity: "high",
|
||||
Description: "Elevation of privilege vulnerability",
|
||||
Recommendations: []string{"Apply latest security patches", "Ensure proper request validation"},
|
||||
},
|
||||
},
|
||||
"Joomla": {
|
||||
{
|
||||
CVE: "CVE-2023-23752",
|
||||
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0"},
|
||||
FixedVersion: "4.2.8",
|
||||
Severity: "critical",
|
||||
Description: "Improper access check allowing unauthorized access to webservice endpoints",
|
||||
Recommendations: []string{"Update to Joomla 4.2.8 or later"},
|
||||
},
|
||||
},
|
||||
"Magento": {
|
||||
{
|
||||
CVE: "CVE-2022-24086",
|
||||
AffectedVersions: []string{"2.3.0", "2.3.1", "2.3.2", "2.4.0", "2.4.1", "2.4.2"},
|
||||
FixedVersion: "2.4.3-p1",
|
||||
Severity: "critical",
|
||||
Description: "Improper input validation leading to arbitrary code execution",
|
||||
Recommendations: []string{"Update to Magento 2.4.3-p1 or later"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
entries, exists := knownCVEs[framework]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
var cves []string
|
||||
var recommendations []string
|
||||
seenRecs := make(map[string]bool)
|
||||
|
||||
for _, entry := range entries {
|
||||
for _, affectedVer := range entry.AffectedVersions {
|
||||
if version == affectedVer || strings.HasPrefix(version, affectedVer) {
|
||||
cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity))
|
||||
for _, rec := range entry.Recommendations {
|
||||
if !seenRecs[rec] {
|
||||
recommendations = append(recommendations, rec)
|
||||
seenRecs[rec] = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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+)?)`,
|
||||
"Next.js": `Next\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
"Nuxt.js": `Nuxt[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
"WordPress": `WordPress[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
"Drupal": `Drupal[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
"Symfony": `Symfony[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
"FastAPI": `FastAPI[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
"Gin": `Gin[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
"Phoenix": `Phoenix[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`,
|
||||
match := extractVersionWithConfidence(body, framework)
|
||||
return match.Version
|
||||
}
|
||||
|
||||
func extractVersionWithConfidence(body string, framework string) VersionMatch {
|
||||
versionPatterns := map[string][]struct {
|
||||
pattern string
|
||||
confidence float32
|
||||
source string
|
||||
}{
|
||||
"Laravel": {
|
||||
{`Laravel\s+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`laravel/framework.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "composer.json"},
|
||||
},
|
||||
"Django": {
|
||||
{`Django[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`django.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "package reference"},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{`Rails[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`rails.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "gem reference"},
|
||||
},
|
||||
"Express.js": {
|
||||
{`Express[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"express":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{`X-AspNet-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.95, "header"},
|
||||
{`ASP\.NET[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`X-AspNetMvc-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.9, "MVC header"},
|
||||
},
|
||||
"ASP.NET Core": {
|
||||
{`\.NET\s*(\d+\.\d+(?:\.\d+)?)`, 0.8, "dotnet version"},
|
||||
},
|
||||
"Spring": {
|
||||
{`Spring[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`spring-core.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "maven"},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{`spring-boot.*?(\d+\.\d+(?:\.\d+)?)`, 0.9, "maven"},
|
||||
},
|
||||
"Flask": {
|
||||
{`Flask[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`Werkzeug[/\s]+(\d+\.\d+(?:\.\d+)?)`, 0.7, "werkzeug version"},
|
||||
},
|
||||
"Next.js": {
|
||||
{`Next\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"next":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
{`_next/static/.*?(\d+\.\d+(?:\.\d+)?)`, 0.6, "static path"},
|
||||
},
|
||||
"Nuxt.js": {
|
||||
{`Nuxt[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"nuxt":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"Vue.js": {
|
||||
{`Vue\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"vue":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
{`vue@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
|
||||
},
|
||||
"Angular": {
|
||||
{`ng-version="(\d+\.\d+(?:\.\d+)?)"`, 0.95, "ng-version attribute"},
|
||||
{`Angular[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"@angular/core":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"React": {
|
||||
{`React[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"react":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
{`react@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
|
||||
},
|
||||
"Svelte": {
|
||||
{`Svelte[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"svelte":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"SvelteKit": {
|
||||
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"WordPress": {
|
||||
{`WordPress[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`wp-includes/version\.php.*?(\d+\.\d+(?:\.\d+)?)`, 0.85, "version.php"},
|
||||
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
},
|
||||
"Drupal": {
|
||||
{`Drupal[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`<meta name="Generator" content="Drupal (\d+)`, 0.9, "generator meta"},
|
||||
},
|
||||
"Joomla": {
|
||||
{`Joomla[!/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`<meta name="generator" content="Joomla! - Open Source Content Management - Version (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
},
|
||||
"Magento": {
|
||||
{`Magento[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Shopify": {
|
||||
{`Shopify\.theme.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "theme version"},
|
||||
},
|
||||
"Symfony": {
|
||||
{`Symfony[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"FastAPI": {
|
||||
{`FastAPI[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Gin": {
|
||||
{`Gin[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Phoenix": {
|
||||
{`Phoenix[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Ember.js": {
|
||||
{`Ember[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Backbone.js": {
|
||||
{`Backbone[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Meteor": {
|
||||
{`Meteor[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Ghost": {
|
||||
{`Ghost[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
}
|
||||
|
||||
if pattern, exists := versionPatterns[framework]; exists {
|
||||
re := regexp.MustCompile(pattern)
|
||||
patterns, exists := versionPatterns[framework]
|
||||
if !exists {
|
||||
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
|
||||
}
|
||||
|
||||
var bestMatch VersionMatch
|
||||
for _, p := range patterns {
|
||||
re := regexp.MustCompile(p.pattern)
|
||||
matches := re.FindStringSubmatch(body)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
if len(matches) > 1 && p.confidence > bestMatch.Confidence {
|
||||
bestMatch = VersionMatch{
|
||||
Version: matches[1],
|
||||
Confidence: p.confidence,
|
||||
Source: p.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
return "unknown"
|
||||
|
||||
if bestMatch.Version == "" {
|
||||
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
|
||||
}
|
||||
return bestMatch
|
||||
}
|
||||
|
||||
@@ -259,11 +259,13 @@ func TestGetVulnerabilities_NoMatch(t *testing.T) {
|
||||
|
||||
func TestFrameworkResult_Fields(t *testing.T) {
|
||||
result := FrameworkResult{
|
||||
Name: "Laravel",
|
||||
Version: "9.0.0",
|
||||
Confidence: 0.85,
|
||||
CVEs: []string{"CVE-2021-3129"},
|
||||
Suggestions: []string{"Update to latest version"},
|
||||
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",
|
||||
}
|
||||
|
||||
if result.Name != "Laravel" {
|
||||
@@ -275,10 +277,262 @@ func TestFrameworkResult_Fields(t *testing.T) {
|
||||
if result.Confidence != 0.85 {
|
||||
t.Errorf("expected Confidence 0.85, got %f", result.Confidence)
|
||||
}
|
||||
if result.VersionConfidence != 0.9 {
|
||||
t.Errorf("expected VersionConfidence 0.9, got %f", result.VersionConfidence)
|
||||
}
|
||||
if len(result.CVEs) != 1 {
|
||||
t.Errorf("expected 1 CVE, got %d", len(result.CVEs))
|
||||
}
|
||||
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) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
framework string
|
||||
wantVer string
|
||||
minConf float32
|
||||
}{
|
||||
{"Laravel explicit", "Laravel 8.0.0", "Laravel", "8.0.0", 0.8},
|
||||
{"Angular ng-version", `<html ng-version="14.2.0">`, "Angular", "14.2.0", 0.9},
|
||||
{"WordPress generator", `<meta name="generator" content="WordPress 6.1.0">`, "WordPress", "6.1.0", 0.9},
|
||||
{"Vue CDN", "vue@3.2.0/dist", "Vue.js", "3.2.0", 0.7},
|
||||
{"No version", "Hello World", "Laravel", "unknown", 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := extractVersionWithConfidence(tt.body, tt.framework)
|
||||
if result.Version != tt.wantVer {
|
||||
t.Errorf("extractVersionWithConfidence() version = %q, want %q", result.Version, tt.wantVer)
|
||||
}
|
||||
if result.Confidence < tt.minConf {
|
||||
t.Errorf("extractVersionWithConfidence() confidence = %f, want >= %f", result.Confidence, tt.minConf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRiskLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cves []string
|
||||
expected string
|
||||
}{
|
||||
{"no CVEs", []string{}, "low"},
|
||||
{"critical", []string{"CVE-2021-3129 (critical)"}, "critical"},
|
||||
{"high", []string{"CVE-2023-22795 (high)"}, "high"},
|
||||
{"medium", []string{"CVE-2023-46298 (medium)"}, "medium"},
|
||||
{"mixed - critical wins", []string{"CVE-2023-1 (medium)", "CVE-2021-3129 (critical)"}, "critical"},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Vue App</title></head>
|
||||
<body>
|
||||
<div id="app" data-v-12345>
|
||||
<div v-cloak>Loading...</div>
|
||||
</div>
|
||||
<script src="https://unpkg.com/vue@3.2.0/dist/vue.global.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Vue.js" {
|
||||
t.Errorf("expected framework 'Vue.js', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Angular(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html ng-version="15.0.0">
|
||||
<head><title>Angular App</title></head>
|
||||
<body>
|
||||
<app-root _nghost-abc-c123 _ngcontent-abc-c123></app-root>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Angular" {
|
||||
t.Errorf("expected framework 'Angular', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_React(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>React App</title></head>
|
||||
<body>
|
||||
<div id="root" data-reactroot="">Content</div>
|
||||
<script src="/static/js/react-dom.production.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "React" {
|
||||
t.Errorf("expected framework 'React', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Svelte(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Svelte App</title></head>
|
||||
<body>
|
||||
<div id="app" class="__svelte-123">
|
||||
<span class="svelte-abc123">Content</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Svelte" {
|
||||
t.Errorf("expected framework 'Svelte', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Joomla(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="generator" content="Joomla! - Open Source Content Management">
|
||||
<script src="/media/jui/js/jquery.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="Joomla">Content</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Joomla" {
|
||||
t.Errorf("expected framework 'Joomla', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCVEEntry_Fields(t *testing.T) {
|
||||
entry := CVEEntry{
|
||||
CVE: "CVE-2021-3129",
|
||||
AffectedVersions: []string{"8.0.0", "8.0.1"},
|
||||
FixedVersion: "8.4.2",
|
||||
Severity: "critical",
|
||||
Description: "RCE vulnerability",
|
||||
Recommendations: []string{"Update immediately"},
|
||||
}
|
||||
|
||||
if entry.CVE != "CVE-2021-3129" {
|
||||
t.Errorf("expected CVE 'CVE-2021-3129', got '%s'", entry.CVE)
|
||||
}
|
||||
if len(entry.AffectedVersions) != 2 {
|
||||
t.Errorf("expected 2 affected versions, got %d", len(entry.AffectedVersions))
|
||||
}
|
||||
if entry.Severity != "critical" {
|
||||
t.Errorf("expected Severity 'critical', got '%s'", entry.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user