mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 11:01:24 -07:00
Merge pull request #106 from vmfunc/feat/security-headers
feat: security-headers scan + scanner fixes and cleanup
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
sif is a modular pentesting toolkit written in go. it's designed to be fast, concurrent, and extensible. run multiple scan types against targets with a single command.
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -all
|
||||
./sif -u https://example.com -headers -sh -cms -framework -git
|
||||
```
|
||||
|
||||
## install
|
||||
@@ -56,7 +56,7 @@ environment.systemPackages = [ pkgs.sif ];
|
||||
nix profile install nixpkgs#sif
|
||||
|
||||
# or just run it without installing
|
||||
nix run nixpkgs#sif -- -u https://example.com -all
|
||||
nix run nixpkgs#sif -- -u https://example.com -headers -sh -framework
|
||||
```
|
||||
|
||||
the repo also ships a flake if you want to build from source:
|
||||
@@ -125,8 +125,8 @@ makepkg -si
|
||||
# framework detection (with cve lookup)
|
||||
./sif -u https://example.com -framework
|
||||
|
||||
# everything
|
||||
./sif -u https://example.com -all
|
||||
# a broad sweep
|
||||
./sif -u https://example.com -dirlist small -dnslist small -ports common -headers -sh -cms -framework -git -whois
|
||||
```
|
||||
|
||||
run `./sif -h` for all options.
|
||||
@@ -147,6 +147,7 @@ sif has a modular architecture. modules are defined in yaml and can be extended
|
||||
| `-js` | javascript analysis |
|
||||
| `-c3` | cloud storage misconfiguration |
|
||||
| `-headers` | http header analysis |
|
||||
| `-sh` | security header analysis (missing/weak headers) |
|
||||
| `-st` | subdomain takeover detection |
|
||||
| `-cms` | cms detection |
|
||||
| `-whois` | whois lookups |
|
||||
|
||||
+2
-3
@@ -4,7 +4,7 @@ setting up a development environment for sif.
|
||||
|
||||
## prerequisites
|
||||
|
||||
- go 1.23 or later
|
||||
- go 1.25 or later
|
||||
- git
|
||||
- make
|
||||
|
||||
@@ -28,8 +28,7 @@ sif/
|
||||
│ ├── logger/ # logging utilities
|
||||
│ ├── modules/ # module system
|
||||
│ ├── scan/ # built-in scans
|
||||
│ ├── styles/ # terminal styling
|
||||
│ └── worker/ # worker pool
|
||||
│ └── styles/ # terminal styling
|
||||
├── modules/ # built-in yaml modules
|
||||
│ ├── http/ # http-based modules
|
||||
│ ├── info/ # information gathering
|
||||
|
||||
@@ -36,7 +36,7 @@ download `sif-windows-amd64.exe` from releases and add to your PATH.
|
||||
|
||||
## from source
|
||||
|
||||
requires go 1.23+
|
||||
requires go 1.25+
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dropalldatabases/sif.git
|
||||
|
||||
+15
-4
@@ -98,16 +98,27 @@ analyzes javascript files for security issues.
|
||||
|
||||
## http headers (-headers)
|
||||
|
||||
analyzes security headers.
|
||||
dumps the target's response headers.
|
||||
|
||||
## security headers (-sh)
|
||||
|
||||
flags missing or weak security headers and headers that leak server internals.
|
||||
|
||||
### checks
|
||||
|
||||
- strict-transport-security (https only)
|
||||
- content-security-policy
|
||||
- x-frame-options
|
||||
- x-content-type-options
|
||||
- strict-transport-security
|
||||
- x-xss-protection
|
||||
- x-content-type-options (expects nosniff)
|
||||
- referrer-policy
|
||||
- permissions-policy
|
||||
- cross-origin-opener-policy
|
||||
|
||||
### flagged as disclosure
|
||||
|
||||
- server
|
||||
- x-powered-by
|
||||
- x-aspnet-version / x-aspnetmvc-version
|
||||
|
||||
## cms detection (-cms)
|
||||
|
||||
|
||||
+9
-1
@@ -95,12 +95,20 @@ scopes: `common` (top ports), `full` (all ports)
|
||||
|
||||
### http headers
|
||||
|
||||
`-headers` - analyze security headers
|
||||
`-headers` - dump the target's response headers
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -headers
|
||||
```
|
||||
|
||||
### security headers
|
||||
|
||||
`-sh` - flag missing/weak security headers (hsts, csp, x-frame-options, ...) and headers that leak server internals
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -sh
|
||||
```
|
||||
|
||||
### cloud storage
|
||||
|
||||
`-c3` - check for cloud storage misconfigurations
|
||||
|
||||
@@ -39,6 +39,7 @@ type Settings struct {
|
||||
Template string
|
||||
CMS bool
|
||||
Headers bool
|
||||
SecurityHeaders bool
|
||||
CloudStorage bool
|
||||
SubdomainTakeover bool
|
||||
Shodan bool
|
||||
@@ -90,6 +91,7 @@ func Parse() *Settings {
|
||||
flagSet.BoolVar(&settings.JavaScript, "js", false, "Enable JavaScript scans"),
|
||||
flagSet.BoolVar(&settings.CMS, "cms", false, "Enable CMS detection"),
|
||||
flagSet.BoolVar(&settings.Headers, "headers", false, "Enable HTTP Header Analysis"),
|
||||
flagSet.BoolVarP(&settings.SecurityHeaders, "security-headers", "sh", false, "Enable security header analysis (missing/weak headers)"),
|
||||
flagSet.BoolVar(&settings.CloudStorage, "c3", false, "Enable C3 Misconfiguration Scan"),
|
||||
flagSet.BoolVar(&settings.SubdomainTakeover, "st", false, "Enable Subdomain Takeover Check"),
|
||||
flagSet.BoolVar(&settings.Shodan, "shodan", false, "Enable Shodan lookup (requires SHODAN_API_KEY env var)"),
|
||||
|
||||
@@ -105,6 +105,9 @@ func (p *Progress) render() {
|
||||
if !IsTTY {
|
||||
current := atomic.LoadInt64(&p.current)
|
||||
total := p.total
|
||||
if total <= 0 {
|
||||
return
|
||||
}
|
||||
percent := int(current * 100 / total)
|
||||
|
||||
// Print at 0%, 25%, 50%, 75%, 100%
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package output
|
||||
|
||||
import "testing"
|
||||
|
||||
// the non-tty milestone path divides current*100/total, so a zero-total bar
|
||||
// used to panic with integer divide-by-zero when piped or redirected.
|
||||
func TestProgressZeroTotalNoPanic(t *testing.T) {
|
||||
p := NewProgress(0, "scanning")
|
||||
p.Increment("item")
|
||||
p.Set(0, "item")
|
||||
p.Done()
|
||||
}
|
||||
|
||||
func TestProgressCounts(t *testing.T) {
|
||||
p := NewProgress(4, "scanning")
|
||||
for i := 0; i < 4; i++ {
|
||||
p.Increment("x")
|
||||
}
|
||||
if p.current != 4 {
|
||||
t.Errorf("current = %d, want 4", p.current)
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ type CloudStorageResult struct {
|
||||
}
|
||||
|
||||
func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStorageResult, error) {
|
||||
fmt.Println(styles.Separator.Render("☁️ Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
|
||||
fmt.Println(styles.Separator.Render("Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
@@ -43,7 +43,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
|
||||
}
|
||||
|
||||
cloudlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "C3 ☁️",
|
||||
Prefix: "C3",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
@@ -81,8 +81,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
|
||||
}
|
||||
|
||||
func extractPotentialBuckets(url string) []string {
|
||||
// This is a simple implementation.
|
||||
// TODO: add more cases
|
||||
// TODO: handle non-adjacent label combos and strip the tld
|
||||
parts := strings.Split(url, ".")
|
||||
var buckets []string
|
||||
for i, part := range parts {
|
||||
|
||||
@@ -93,6 +93,7 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
|
||||
|
||||
// util.InitProgressBar()
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
wg.Add(threads)
|
||||
|
||||
dorkResults := []DorkResult{}
|
||||
@@ -124,7 +125,9 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
|
||||
Count: len(results),
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
dorkResults = append(dorkResults, result)
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
|
||||
@@ -74,6 +74,7 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
wg.Add(threads)
|
||||
|
||||
foundUrls := []string{}
|
||||
@@ -106,7 +107,9 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
|
||||
logger.Write(sanitizedURL, logdir, strconv.Itoa(resp.StatusCode)+" git found at ["+repourl+"]\n")
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
foundUrls = append(foundUrls, resp.Request.URL.String())
|
||||
mu.Unlock()
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func doSupabaseRequest(projectId, path, apikey string, auth *string) ([]byte, *h
|
||||
|
||||
func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error) {
|
||||
supabaselog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "🚧 JavaScript > Supabase ⚡️",
|
||||
Prefix: "JavaScript > Supabase",
|
||||
}).With("url", jsUrl)
|
||||
|
||||
var results = []supabaseScanResult{}
|
||||
|
||||
@@ -16,6 +16,7 @@ package scan
|
||||
// These provide better type safety and allow method implementations.
|
||||
type (
|
||||
HeaderResults []HeaderResult
|
||||
SecurityHeaderResults []SecurityHeaderResult
|
||||
DirectoryResults []DirectoryResult
|
||||
CloudStorageResults []CloudStorageResult
|
||||
DorkResults []DorkResult
|
||||
@@ -23,7 +24,6 @@ type (
|
||||
)
|
||||
|
||||
// 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
|
||||
@@ -40,6 +40,7 @@ func (r *SecurityTrailsResult) ResultType() string { return "securitytrails" }
|
||||
// ResultType implementations for slice result types.
|
||||
|
||||
func (r HeaderResults) ResultType() string { return "headers" }
|
||||
func (r SecurityHeaderResults) ResultType() string { return "security_headers" }
|
||||
func (r DirectoryResults) ResultType() string { return "dirlist" }
|
||||
func (r CloudStorageResults) ResultType() string { return "cloudstorage" }
|
||||
func (r DorkResults) ResultType() string { return "dork" }
|
||||
@@ -53,6 +54,7 @@ var (
|
||||
_ ScanResult = (*CMSResult)(nil)
|
||||
_ ScanResult = (*SecurityTrailsResult)(nil)
|
||||
_ ScanResult = HeaderResults(nil)
|
||||
_ ScanResult = SecurityHeaderResults(nil)
|
||||
_ ScanResult = DirectoryResults(nil)
|
||||
_ ScanResult = CloudStorageResults(nil)
|
||||
_ ScanResult = DorkResults(nil)
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
type SecurityHeaderResult struct {
|
||||
Header string `json:"header"`
|
||||
Present bool `json:"present"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Severity string `json:"severity"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
type recommendedHeader struct {
|
||||
name string
|
||||
severity string
|
||||
}
|
||||
|
||||
var recommendedHeaders = []recommendedHeader{
|
||||
{"Strict-Transport-Security", "high"},
|
||||
{"Content-Security-Policy", "medium"},
|
||||
{"X-Frame-Options", "medium"},
|
||||
{"X-Content-Type-Options", "low"},
|
||||
{"Referrer-Policy", "low"},
|
||||
{"Permissions-Policy", "low"},
|
||||
{"Cross-Origin-Opener-Policy", "low"},
|
||||
}
|
||||
|
||||
// headers that leak server/framework details when present.
|
||||
var disclosureHeaders = []string{"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version"}
|
||||
|
||||
const hstsMinMaxAge = 31536000 // a year, in seconds
|
||||
|
||||
func SecurityHeaders(url string, timeout time.Duration, logdir string) (SecurityHeaderResults, error) {
|
||||
log := output.Module("SECHEADERS")
|
||||
log.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Security Header Analysis"); err != nil {
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
results := gradeSecurityHeaders(resp.Header, strings.HasPrefix(url, "https://"))
|
||||
|
||||
for _, r := range results {
|
||||
line := r.Header + " " + r.Note
|
||||
log.Warn("%s [%s]", line, r.Severity)
|
||||
if logdir != "" {
|
||||
_ = logger.Write(sanitizedURL, logdir, line+" ["+r.Severity+"]\n")
|
||||
}
|
||||
}
|
||||
|
||||
if len(results) == 0 {
|
||||
log.Success("all recommended security headers present")
|
||||
}
|
||||
|
||||
log.Complete(len(results), "issues")
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func gradeSecurityHeaders(header http.Header, https bool) SecurityHeaderResults {
|
||||
var results SecurityHeaderResults
|
||||
|
||||
for _, h := range recommendedHeaders {
|
||||
// hsts does nothing over plain http, so don't flag its absence there
|
||||
if h.name == "Strict-Transport-Security" && !https {
|
||||
continue
|
||||
}
|
||||
|
||||
value := header.Get(h.name)
|
||||
switch {
|
||||
case value == "":
|
||||
results = append(results, SecurityHeaderResult{
|
||||
Header: h.name,
|
||||
Severity: h.severity,
|
||||
Note: "missing",
|
||||
})
|
||||
case h.name == "Strict-Transport-Security" && hstsMaxAge(value) < hstsMinMaxAge:
|
||||
results = append(results, SecurityHeaderResult{
|
||||
Header: h.name,
|
||||
Present: true,
|
||||
Value: value,
|
||||
Severity: h.severity,
|
||||
Note: "max-age too short",
|
||||
})
|
||||
case h.name == "X-Content-Type-Options" && !strings.EqualFold(value, "nosniff"):
|
||||
results = append(results, SecurityHeaderResult{
|
||||
Header: h.name,
|
||||
Present: true,
|
||||
Value: value,
|
||||
Severity: "low",
|
||||
Note: "should be nosniff",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, name := range disclosureHeaders {
|
||||
if value := header.Get(name); value != "" {
|
||||
results = append(results, SecurityHeaderResult{
|
||||
Header: name,
|
||||
Present: true,
|
||||
Value: value,
|
||||
Severity: "low",
|
||||
Note: "discloses " + value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// hstsMaxAge returns the max-age seconds from an hsts value, or 0 if absent.
|
||||
func hstsMaxAge(value string) int {
|
||||
for _, part := range strings.Split(value, ";") {
|
||||
if age, ok := strings.CutPrefix(strings.ToLower(strings.TrimSpace(part)), "max-age="); ok {
|
||||
n, err := strconv.Atoi(strings.TrimSpace(age))
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func buildHeader(kv map[string]string) http.Header {
|
||||
h := http.Header{}
|
||||
for k, v := range kv {
|
||||
h.Set(k, v)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func findFinding(results SecurityHeaderResults, name string) (SecurityHeaderResult, bool) {
|
||||
for _, r := range results {
|
||||
if r.Header == name {
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
return SecurityHeaderResult{}, false
|
||||
}
|
||||
|
||||
func TestGradeSecurityHeaders_MissingOverHTTPS(t *testing.T) {
|
||||
results := gradeSecurityHeaders(http.Header{}, true)
|
||||
|
||||
for _, h := range recommendedHeaders {
|
||||
f, ok := findFinding(results, h.name)
|
||||
if !ok {
|
||||
t.Errorf("expected %s to be flagged", h.name)
|
||||
continue
|
||||
}
|
||||
if f.Present {
|
||||
t.Errorf("%s should not be marked present", h.name)
|
||||
}
|
||||
if f.Severity != h.severity {
|
||||
t.Errorf("%s severity = %q, want %q", h.name, f.Severity, h.severity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGradeSecurityHeaders_HSTSSkippedOverHTTP(t *testing.T) {
|
||||
results := gradeSecurityHeaders(http.Header{}, false)
|
||||
if _, ok := findFinding(results, "Strict-Transport-Security"); ok {
|
||||
t.Error("HSTS should only be graded for https targets")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGradeSecurityHeaders_AllPresent(t *testing.T) {
|
||||
h := buildHeader(map[string]string{
|
||||
"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
|
||||
"Content-Security-Policy": "default-src 'self'",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"Permissions-Policy": "geolocation=()",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
})
|
||||
|
||||
if results := gradeSecurityHeaders(h, true); len(results) != 0 {
|
||||
t.Errorf("expected no findings, got %d: %+v", len(results), results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGradeSecurityHeaders_ContentTypeNotNosniff(t *testing.T) {
|
||||
h := buildHeader(map[string]string{
|
||||
"Strict-Transport-Security": "max-age=63072000",
|
||||
"Content-Security-Policy": "default-src 'self'",
|
||||
"X-Frame-Options": "DENY",
|
||||
"X-Content-Type-Options": "sniff",
|
||||
"Referrer-Policy": "no-referrer",
|
||||
"Permissions-Policy": "geolocation=()",
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
})
|
||||
|
||||
f, ok := findFinding(gradeSecurityHeaders(h, true), "X-Content-Type-Options")
|
||||
if !ok {
|
||||
t.Fatal("expected X-Content-Type-Options to be flagged when not nosniff")
|
||||
}
|
||||
if !f.Present || f.Value != "sniff" {
|
||||
t.Errorf("finding = %+v, want present with value sniff", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGradeSecurityHeaders_WeakHSTS(t *testing.T) {
|
||||
// max-age=0 actively disables hsts, so a present header still has to be flagged
|
||||
h := buildHeader(map[string]string{"Strict-Transport-Security": "max-age=0"})
|
||||
|
||||
f, ok := findFinding(gradeSecurityHeaders(h, true), "Strict-Transport-Security")
|
||||
if !ok {
|
||||
t.Fatal("expected a short-lived hsts header to be flagged")
|
||||
}
|
||||
if !f.Present || f.Severity != "high" {
|
||||
t.Errorf("finding = %+v, want present high", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGradeSecurityHeaders_Disclosure(t *testing.T) {
|
||||
h := buildHeader(map[string]string{
|
||||
"Server": "Apache/2.4.1 (Ubuntu)",
|
||||
"X-Powered-By": "PHP/8.1.2",
|
||||
})
|
||||
|
||||
results := gradeSecurityHeaders(h, false)
|
||||
for _, name := range []string{"Server", "X-Powered-By"} {
|
||||
f, ok := findFinding(results, name)
|
||||
if !ok {
|
||||
t.Errorf("expected disclosure finding for %s", name)
|
||||
continue
|
||||
}
|
||||
if !f.Present || f.Severity != "low" {
|
||||
t.Errorf("%s finding = %+v, want present low", name, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityHeaders_LiveResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||
w.Header().Set("Server", "nginx/1.25.3")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
results, err := SecurityHeaders(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("SecurityHeaders returned error: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := findFinding(results, "X-Frame-Options"); ok {
|
||||
t.Error("X-Frame-Options was set, should not be flagged")
|
||||
}
|
||||
if _, ok := findFinding(results, "Content-Security-Policy"); !ok {
|
||||
t.Error("expected missing Content-Security-Policy to be flagged")
|
||||
}
|
||||
if _, ok := findFinding(results, "Server"); !ok {
|
||||
t.Error("expected Server disclosure to be flagged")
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,6 @@ type shodanHostResponse struct {
|
||||
}
|
||||
|
||||
// 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"`
|
||||
|
||||
@@ -36,20 +36,10 @@ type SubdomainTakeoverResult struct {
|
||||
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
|
||||
// SubdomainTakeover checks dnsResults for dangling subdomains pointing at
|
||||
// unclaimed third-party services.
|
||||
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") + "..."))
|
||||
fmt.Println(styles.Separator.Render("Starting " + styles.Status.Render("Subdomain Takeover Vulnerability Check") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
@@ -61,7 +51,7 @@ func SubdomainTakeover(url string, dnsResults []string, timeout time.Duration, t
|
||||
}
|
||||
|
||||
subdomainlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Subdomain Takeover 🔍",
|
||||
Prefix: "Subdomain Takeover",
|
||||
})
|
||||
|
||||
client := &http.Client{
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package worker provides a generic worker pool for concurrent task processing.
|
||||
package worker
|
||||
|
||||
import "sync"
|
||||
|
||||
// Pool manages a pool of workers that process items concurrently.
|
||||
// It uses channel-based distribution for efficient load balancing.
|
||||
type Pool[T any, R any] struct {
|
||||
workers int
|
||||
fn func(T) R
|
||||
}
|
||||
|
||||
// New creates a new worker pool with the specified number of workers
|
||||
// and a processing function.
|
||||
func New[T any, R any](workers int, fn func(T) R) *Pool[T, R] {
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
return &Pool[T, R]{
|
||||
workers: workers,
|
||||
fn: fn,
|
||||
}
|
||||
}
|
||||
|
||||
// Run processes all items concurrently and returns the results.
|
||||
// Items are distributed via a channel for optimal load balancing.
|
||||
func (p *Pool[T, R]) Run(items []T) []R {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
input := make(chan T, len(items))
|
||||
output := make(chan R, len(items))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(p.workers)
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < p.workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range input {
|
||||
output <- p.fn(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Feed items to workers
|
||||
for _, item := range items {
|
||||
input <- item
|
||||
}
|
||||
close(input)
|
||||
|
||||
// Wait for all workers to finish, then close output
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(output)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
results := make([]R, 0, len(items))
|
||||
for r := range output {
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// RunWithFilter processes items concurrently and returns only non-zero results.
|
||||
// Useful when the processing function may return zero values for filtered items.
|
||||
func (p *Pool[T, R]) RunWithFilter(items []T, filter func(R) bool) []R {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
input := make(chan T, len(items))
|
||||
output := make(chan R, len(items))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(p.workers)
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < p.workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range input {
|
||||
result := p.fn(item)
|
||||
if filter(result) {
|
||||
output <- result
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Feed items to workers
|
||||
for _, item := range items {
|
||||
input <- item
|
||||
}
|
||||
close(input)
|
||||
|
||||
// Wait for all workers to finish, then close output
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(output)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
results := make([]R, 0, len(items)/2) // Estimate half will pass filter
|
||||
for r := range output {
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ForEach processes items concurrently without collecting results.
|
||||
// Useful for side-effect operations like logging or writing to external stores.
|
||||
func (p *Pool[T, R]) ForEach(items []T, callback func(R)) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
input := make(chan T, len(items))
|
||||
output := make(chan R, len(items))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(p.workers)
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < p.workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range input {
|
||||
output <- p.fn(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Feed items to workers
|
||||
for _, item := range items {
|
||||
input <- item
|
||||
}
|
||||
close(input)
|
||||
|
||||
// Process results as they come in
|
||||
var outputWg sync.WaitGroup
|
||||
outputWg.Add(1)
|
||||
go func() {
|
||||
defer outputWg.Done()
|
||||
for r := range output {
|
||||
callback(r)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(output)
|
||||
outputWg.Wait()
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package worker
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPoolRun(t *testing.T) {
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
results := pool.Run(items)
|
||||
|
||||
if len(results) != len(items) {
|
||||
t.Errorf("Expected %d results, got %d", len(items), len(results))
|
||||
}
|
||||
|
||||
// Sort results since order is not guaranteed
|
||||
sort.Ints(results)
|
||||
expected := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolRunEmpty(t *testing.T) {
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
results := pool.Run(nil)
|
||||
if results != nil {
|
||||
t.Errorf("Expected nil for empty input, got %v", results)
|
||||
}
|
||||
|
||||
results = pool.Run([]int{})
|
||||
if results != nil {
|
||||
t.Errorf("Expected nil for empty slice, got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolRunWithFilter(t *testing.T) {
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
results := pool.RunWithFilter(items, func(r int) bool {
|
||||
return r > 10 // Only keep results > 10
|
||||
})
|
||||
|
||||
// Should have 5 results: 12, 14, 16, 18, 20
|
||||
if len(results) != 5 {
|
||||
t.Errorf("Expected 5 results, got %d", len(results))
|
||||
}
|
||||
|
||||
sort.Ints(results)
|
||||
expected := []int{12, 14, 16, 18, 20}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolForEach(t *testing.T) {
|
||||
var sum atomic.Int64
|
||||
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3, 4, 5}
|
||||
pool.ForEach(items, func(r int) {
|
||||
sum.Add(int64(r))
|
||||
})
|
||||
|
||||
// Sum should be 2+4+6+8+10 = 30
|
||||
if sum.Load() != 30 {
|
||||
t.Errorf("Expected sum = 30, got %d", sum.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolSingleWorker(t *testing.T) {
|
||||
pool := New(1, func(x int) int {
|
||||
return x + 1
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3}
|
||||
results := pool.Run(items)
|
||||
|
||||
if len(results) != 3 {
|
||||
t.Errorf("Expected 3 results, got %d", len(results))
|
||||
}
|
||||
|
||||
sort.Ints(results)
|
||||
expected := []int{2, 3, 4}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolZeroWorkers(t *testing.T) {
|
||||
// Zero workers should default to 1
|
||||
pool := New(0, func(x int) int {
|
||||
return x
|
||||
})
|
||||
|
||||
if pool.workers != 1 {
|
||||
t.Errorf("Expected workers = 1, got %d", pool.workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolStringProcessing(t *testing.T) {
|
||||
pool := New(2, func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
|
||||
items := []string{"a", "bb", "ccc", "dddd"}
|
||||
results := pool.Run(items)
|
||||
|
||||
sort.Ints(results)
|
||||
expected := []int{1, 2, 3, 4}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolStructProcessing(t *testing.T) {
|
||||
type input struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
type output struct {
|
||||
sum int
|
||||
prod int
|
||||
}
|
||||
|
||||
pool := New(3, func(in input) output {
|
||||
return output{sum: in.a + in.b, prod: in.a * in.b}
|
||||
})
|
||||
|
||||
items := []input{{1, 2}, {3, 4}, {5, 6}}
|
||||
results := pool.Run(items)
|
||||
|
||||
if len(results) != 3 {
|
||||
t.Errorf("Expected 3 results, got %d", len(results))
|
||||
}
|
||||
|
||||
// Verify all expected outputs are present
|
||||
found := make(map[output]bool)
|
||||
for _, r := range results {
|
||||
found[r] = true
|
||||
}
|
||||
|
||||
expectedOutputs := []output{{3, 2}, {7, 12}, {11, 30}}
|
||||
for _, exp := range expectedOutputs {
|
||||
if !found[exp] {
|
||||
t.Errorf("Expected output %v not found in results", exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,6 +321,16 @@ func (app *App) Run() error {
|
||||
}
|
||||
}
|
||||
|
||||
if app.settings.SecurityHeaders {
|
||||
result, err := scan.SecurityHeaders(url, app.settings.Timeout, app.settings.LogDir)
|
||||
if err != nil {
|
||||
log.Errorf("Error while running Security Header Analysis: %s", err)
|
||||
} else {
|
||||
moduleResults = append(moduleResults, NewModuleResult(result))
|
||||
scansRun = append(scansRun, "Security Headers")
|
||||
}
|
||||
}
|
||||
|
||||
if app.settings.CloudStorage {
|
||||
result, err := scan.CloudStorage(url, app.settings.Timeout, app.settings.LogDir)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user