/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · Blazing-fast pentesting suite : : ▄█ █ █▀ · BSD 3-Clause License : : : : (c) 2022-2026 vmfunc, xyzeva, : : lunchcat alumni & contributors : : : ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· */ package frameworks import ( "net/http" "regexp" "strings" "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 confidence float32 source string } // frameworkVersionPatterns maps framework names to their pre-compiled version patterns. // Patterns are compiled once at package initialization for optimal performance. var frameworkVersionPatterns map[string][]compiledVersionPattern func init() { // Raw patterns to be compiled rawPatterns := map[string][]struct { pattern string confidence float32 source string }{ "Laravel": { {`Laravel\s+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"}, }, "Django": { {`Django[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"}, }, "Ruby on Rails": { {`Rails[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"}, }, "Express.js": { {`Express[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"}, {`"express":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"}, }, "ASP.NET": { // header names are matched case-insensitively: Go's http.Header // canonicalizes "X-AspNet-Version" to "X-Aspnet-Version", so a // case-sensitive literal here would never match the header line // built by headerSearchText. {`(?i:X-AspNet-Version):\s*(\d+\.\d+(?:\.\d+)?)`, 0.95, "header"}, {`ASP\.NET[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"}, {`(?i: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"}, }, "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{1,2}\.\d+(?:\.\d+)?)`, 0.9, "explicit version"}, {`"next":\s*"[~^]?(\d{1,2}\.\d+(?:\.\d+)?)"`, 0.85, "package.json"}, }, "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"}, }, "htmx": { {`htmx(?:\.org)?@(\d+\.\d+(?:\.\d+)?)`, 0.85, "CDN reference"}, {`"htmx\.org":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"}, }, "WordPress": { {` 1 && p.confidence > bestMatch.Confidence { candidate := matches[1] if isValidVersionString(candidate) { bestMatch = VersionMatch{ Version: candidate, Confidence: p.confidence, Source: p.source, } } } } if bestMatch.Version == "" { return VersionMatch{Version: "unknown", Confidence: 0, Source: ""} } return bestMatch } func isValidVersionString(v string) bool { if v == "" || len(v) > 20 { return false } dotCount := 0 digitCount := 0 for _, c := range v { switch { case c == '.': dotCount++ if dotCount > 3 { return false } case unicode.IsDigit(c): digitCount++ default: return false } } return digitCount > 0 }