mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-06 04:34:11 -07:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 912f6e8e0e | |||
| 83ac92a4b8 | |||
| 7f0e4cd128 | |||
| 29d94e5352 | |||
| 05fa35d945 | |||
| ce3075ad91 | |||
| 661480a56d | |||
| 76e8893ee2 | |||
| 1231ca3179 | |||
| eb33321102 | |||
| 133224c348 | |||
| 4c650e23e3 | |||
| 75e953cda7 | |||
| f7ef71e835 | |||
| 5e10c1857b | |||
| 3c070a621d | |||
| 94b99ade5a | |||
| 9326465a46 | |||
| 50c9933812 | |||
| af0167859a | |||
| 7efd62c804 | |||
| 1a1ff446d8 | |||
| 648fa8d2c8 | |||
| 8918be4797 | |||
| 4fc0df5a01 | |||
| ece5b2b0b0 |
@@ -47,3 +47,5 @@ jobs:
|
||||
with:
|
||||
files: ./coverage.out
|
||||
fail_ci_if_error: false
|
||||
- name: run integration tests
|
||||
run: go test -tags=integration -race ./internal/scan/...
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
echo ': █▀ █ █▀▀ · Blazing-fast pentesting suite :'
|
||||
echo ': ▄█ █ █▀ · BSD 3-Clause License :'
|
||||
echo ': :'
|
||||
echo ': (c) 2022-2025 vmfunc, xyzeva, :'
|
||||
echo ': (c) 2022-2026 vmfunc, xyzeva, :'
|
||||
echo ': lunchcat alumni & contributors :'
|
||||
echo ': :'
|
||||
echo '·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·'
|
||||
|
||||
+2
-21
@@ -72,16 +72,14 @@ linters:
|
||||
- unnecessaryDefer # common pattern in tests
|
||||
# inverting conditions in scan logic hurts readability
|
||||
- nestingReduce
|
||||
- importShadow # nuclei output pkg alias conflict, intentional
|
||||
- rangeValCopy # nuclei module iterates value types, fine here
|
||||
gosec:
|
||||
excludes:
|
||||
- G104 # errcheck covers this
|
||||
- G107 # pentesting tool -- variable URLs are the whole point
|
||||
- G110 # nuclei template decompression, acceptable context
|
||||
- G301 # log/template dirs need 0755 for common tooling
|
||||
- G302 # log files intentionally world-readable for tailing
|
||||
- G304 # sif reads user-supplied wordlist paths -- intentional
|
||||
- G305 # tar extraction is traversal-guarded (HasPrefix on the
|
||||
# cleaned target); gosec flags filepath.Join regardless
|
||||
|
||||
exclusions:
|
||||
rules:
|
||||
@@ -90,23 +88,6 @@ linters:
|
||||
linters:
|
||||
- errcheck
|
||||
- noctx
|
||||
# net.* calls predate context plumbing; refactor tracked separately
|
||||
- path: internal/scan/(ports|shodan|subdomaintakeover)\.go
|
||||
linters:
|
||||
- noctx
|
||||
# Close on concrete types errcheck can't match to (io.Closer).Close
|
||||
- path: internal/nuclei/templates/templates\.go
|
||||
text: "tarball.Close"
|
||||
linters:
|
||||
- errcheck
|
||||
- path: internal/scan/ports\.go
|
||||
text: "tcp.Close"
|
||||
linters:
|
||||
- errcheck
|
||||
- path: sif\.go
|
||||
text: "logger.Close"
|
||||
linters:
|
||||
- errcheck
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 50
|
||||
|
||||
@@ -9,6 +9,12 @@ RM ?= rm
|
||||
GOFLAGS ?=
|
||||
PREFIX ?= /usr/local
|
||||
BINDIR ?= bin
|
||||
MANDIR ?= share/man/man1
|
||||
|
||||
# stamp local builds with the nearest v* tag (or short sha), matching the
|
||||
# release ci. --match keeps the automated-release-* tags out of the version.
|
||||
VERSION ?= $(shell git describe --tags --match 'v*' --always --dirty 2>/dev/null | sed 's/^v//')
|
||||
GO_LDFLAGS = -X main.version=$(VERSION)
|
||||
|
||||
define COPYRIGHT_ASCII
|
||||
╭────────────────────────────────────────────────────────────╮
|
||||
@@ -56,7 +62,7 @@ sif: check_go_version
|
||||
@echo "📁 Current directory: $$(pwd)"
|
||||
@echo "🔧 Go flags: $(GOFLAGS)"
|
||||
@echo "📦 Building package: ./cmd/sif"
|
||||
$(GO) build -v $(GOFLAGS) ./cmd/sif
|
||||
$(GO) build -v $(GOFLAGS) -ldflags "$(GO_LDFLAGS)" ./cmd/sif
|
||||
@echo "📊 Build info:"
|
||||
@$(GO) version -m sif
|
||||
@echo "✅ sif built successfully! 🚀"
|
||||
@@ -76,6 +82,9 @@ install: check_go_version
|
||||
fi
|
||||
@mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR))
|
||||
@cp -f sif $(DESTDIR)$(PREFIX)/$(BINDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo cp -f sif $(DESTDIR)$(PREFIX)/$(BINDIR))
|
||||
@echo "📖 Installing man page..."
|
||||
@mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR))
|
||||
@cp -f man/sif.1 $(DESTDIR)$(PREFIX)/$(MANDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo cp -f man/sif.1 $(DESTDIR)$(PREFIX)/$(MANDIR))
|
||||
@echo "✅ sif installed successfully! 🎊"
|
||||
|
||||
uninstall:
|
||||
@@ -86,6 +95,7 @@ uninstall:
|
||||
exit 1; \
|
||||
fi
|
||||
@$(RM) $(DESTDIR)$(PREFIX)/$(BINDIR)/sif || (echo "🔒 Permission denied. Trying with sudo..." && sudo $(RM) $(DESTDIR)$(PREFIX)/$(BINDIR)/sif)
|
||||
@$(RM) $(DESTDIR)$(PREFIX)/$(MANDIR)/sif.1 || (echo "🔒 Permission denied. Trying with sudo..." && sudo $(RM) $(DESTDIR)$(PREFIX)/$(MANDIR)/sif.1)
|
||||
@echo "✅ sif uninstalled successfully!"
|
||||
|
||||
.PHONY: all check_go_version sif clean install uninstall
|
||||
@@ -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,12 +125,26 @@ 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.
|
||||
|
||||
## commands
|
||||
|
||||
a couple of subcommands run without scanning:
|
||||
|
||||
```bash
|
||||
# print the version (release builds are stamped; local builds use git describe)
|
||||
./sif version
|
||||
|
||||
# show the latest release notes (also -pn)
|
||||
./sif patchnote
|
||||
```
|
||||
|
||||
the first time you run a new release, sif prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to turn that off.
|
||||
|
||||
## modules
|
||||
|
||||
sif has a modular architecture. modules are defined in yaml and can be extended by users.
|
||||
@@ -147,6 +161,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 |
|
||||
|
||||
+28
-1
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,15 +13,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif"
|
||||
"github.com/dropalldatabases/sif/internal/config"
|
||||
"github.com/dropalldatabases/sif/internal/patchnotes"
|
||||
ver "github.com/dropalldatabases/sif/internal/version"
|
||||
|
||||
// Register framework detectors
|
||||
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
|
||||
)
|
||||
|
||||
// version is stamped at release time via -ldflags "-X main.version=...";
|
||||
// ver.Resolve falls back to the build info or "dev" for other builds.
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
version = ver.Resolve(version)
|
||||
sif.Version = version
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "patchnote", "patchnotes", "-pn", "--patchnotes":
|
||||
patchnotes.Print("")
|
||||
return
|
||||
case "version", "-version", "--version":
|
||||
fmt.Printf("sif %s\n", version)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
settings := config.Parse()
|
||||
|
||||
app, err := sif.New(settings)
|
||||
@@ -29,6 +52,10 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !settings.ApiMode {
|
||||
patchnotes.ShowOnce(version)
|
||||
}
|
||||
|
||||
err = app.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
+11
-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
|
||||
@@ -138,6 +137,15 @@ the module system is in `internal/modules/`:
|
||||
go test ./internal/...
|
||||
```
|
||||
|
||||
### integration tests
|
||||
|
||||
run the scanners against a local testbed that plants the artifacts each one
|
||||
should find (network-free, behind a build tag):
|
||||
|
||||
```bash
|
||||
go test -tags=integration ./internal/scan/...
|
||||
```
|
||||
|
||||
### functional test
|
||||
|
||||
```bash
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
+31
-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
|
||||
@@ -251,6 +259,28 @@ enable api mode for json output:
|
||||
|
||||
output is a json object with scan results.
|
||||
|
||||
## commands
|
||||
|
||||
these run without scanning a target.
|
||||
|
||||
### version
|
||||
|
||||
print the sif version. release builds are stamped via ldflags, local `make` builds derive it from `git describe`, and `go install`ed builds read it from the module build info:
|
||||
|
||||
```bash
|
||||
./sif version
|
||||
```
|
||||
|
||||
### patchnote
|
||||
|
||||
show the latest release's notes, fetched from github (also `-pn`):
|
||||
|
||||
```bash
|
||||
./sif patchnote
|
||||
```
|
||||
|
||||
the first time you run a new release sif also prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to disable that.
|
||||
|
||||
## examples
|
||||
|
||||
### quick recon
|
||||
|
||||
@@ -4,6 +4,7 @@ go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/antchfx/htmlquery v1.3.6
|
||||
github.com/charmbracelet/glamour v0.10.0
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||
github.com/charmbracelet/log v1.0.0
|
||||
github.com/likexian/whois v1.15.7
|
||||
@@ -91,7 +92,6 @@ require (
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||
github.com/charmbracelet/glamour v0.10.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494 // indirect
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -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)"),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -37,7 +37,7 @@ var defaultLogger = &Logger{
|
||||
// Init creates the log directory if it doesn't exist.
|
||||
func Init(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(dir, 0o755); err != nil {
|
||||
if err := os.Mkdir(dir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (l *Logger) getWriter(path string) (*bufio.Writer, error) {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -124,7 +124,10 @@ func (l *Logger) Close() error {
|
||||
|
||||
// CreateFile initializes a log file for the given URL and writes the header.
|
||||
func CreateFile(logFiles *[]string, url string, dir string) error {
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := url
|
||||
if _, after, ok := strings.Cut(url, "://"); ok {
|
||||
sanitizedURL = after
|
||||
}
|
||||
path := filepath.Join(dir, sanitizedURL+".log")
|
||||
|
||||
header := fmt.Sprintf(" _____________\n__________(_)__ __/\n__ ___/_ /__ /_ \n_(__ )_ / _ __/ \n/____/ /_/ /_/ \n\nsif log file for %s\nhttps://sif.sh\n\n", url)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -16,7 +16,7 @@
|
||||
: SIF - Blazing-fast pentesting suite :
|
||||
: Blaze - BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
-------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -14,22 +14,22 @@ package format
|
||||
|
||||
import (
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
nucleiout "github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
)
|
||||
|
||||
func FormatLine(event *output.ResultEvent) string {
|
||||
output := event.TemplateID
|
||||
func FormatLine(event *nucleiout.ResultEvent) string {
|
||||
line := event.TemplateID
|
||||
|
||||
if event.MatcherName != "" {
|
||||
output += ":" + styles.Highlight.Render(event.MatcherName)
|
||||
line += ":" + styles.Highlight.Render(event.MatcherName)
|
||||
} else if event.ExtractorName != "" {
|
||||
output += ":" + styles.Highlight.Render(event.ExtractorName)
|
||||
line += ":" + styles.Highlight.Render(event.ExtractorName)
|
||||
}
|
||||
|
||||
output += " [" + event.Type + "]"
|
||||
output += " [" + formatSeverity(event.Info.SeverityHolder.Severity.String()) + "]"
|
||||
line += " [" + event.Type + "]"
|
||||
line += " [" + formatSeverity(event.Info.SeverityHolder.Severity.String()) + "]"
|
||||
|
||||
return output
|
||||
return line
|
||||
}
|
||||
|
||||
func formatSeverity(severity string) string {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
@@ -53,10 +55,20 @@ func Install(logger *log.Logger) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tarball.Close()
|
||||
defer func() {
|
||||
if cerr := tarball.Close(); cerr != nil {
|
||||
logger.Warnf("closing gzip reader: %v", cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
data := tar.NewReader(tarball)
|
||||
|
||||
dest, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanDest := filepath.Clean(dest)
|
||||
|
||||
for {
|
||||
header, err := data.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
@@ -66,17 +78,25 @@ func Install(logger *log.Logger) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// guard against path traversal ("Zip Slip"): the resolved path must
|
||||
// stay within the extraction directory before any filesystem op.
|
||||
target := filepath.Join(cleanDest, header.Name)
|
||||
if !strings.HasPrefix(target, cleanDest+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("invalid archive entry %q: escapes extraction directory", header.Name)
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.Mkdir(header.Name, 0o755); err != nil {
|
||||
if err := os.Mkdir(target, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
file, err := os.Create(header.Name)
|
||||
file, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(file, data); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package patchnotes shows release notes pulled from the github releases.
|
||||
package patchnotes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
)
|
||||
|
||||
const releasesAPI = "https://api.github.com/repos/vmfunc/sif/releases"
|
||||
|
||||
type release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
URL string `json:"html_url"`
|
||||
}
|
||||
|
||||
func fetch(ctx context.Context, path string) (*release, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releasesAPI+path, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("github returned %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r release
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// render turns a release's markdown body into styled terminal output, falling
|
||||
// back to the raw body if glamour can't render it.
|
||||
func render(r *release) string {
|
||||
out, err := glamour.Render(r.Body, "dark")
|
||||
if err != nil {
|
||||
return r.Body
|
||||
}
|
||||
return fmt.Sprintf("%s\n%s", r.TagName, out)
|
||||
}
|
||||
|
||||
// Print fetches the latest release and writes its notes to stdout. tag may be
|
||||
// empty for the latest release, or a "vX" tag for a specific one.
|
||||
func Print(tag string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
path := "/latest"
|
||||
if tag != "" {
|
||||
path = "/tags/" + tag
|
||||
}
|
||||
|
||||
r, err := fetch(ctx, path)
|
||||
if err != nil {
|
||||
fmt.Printf("couldn't fetch patch notes: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Print(render(r))
|
||||
}
|
||||
|
||||
// ShowOnce prints the running version's notes the first time that version runs,
|
||||
// then records it so it isn't shown again. best-effort: dev builds, the
|
||||
// SIF_NO_PATCHNOTES opt-out, and any network failure stay silent.
|
||||
func ShowOnce(version string) {
|
||||
// only clean release tags (e.g. 2026.6.7) map to a github release; skip dev
|
||||
// and pseudo-versions (a commit/dirty build) so we don't make a doomed call.
|
||||
if version == "" || version == "dev" || strings.ContainsAny(version, "-+") || os.Getenv("SIF_NO_PATCHNOTES") != "" {
|
||||
return
|
||||
}
|
||||
|
||||
path, err := statePath()
|
||||
if err != nil || hasSeen(path, version) {
|
||||
return
|
||||
}
|
||||
// record before fetching so a flaky network doesn't nag on every run
|
||||
recordSeen(path, version)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r, err := fetch(ctx, "/tags/v"+version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Printf("\nwhat's new in this release:\n%s", render(r))
|
||||
}
|
||||
|
||||
func statePath() (string, error) {
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "sif", "seen_version"), nil
|
||||
}
|
||||
|
||||
func hasSeen(path, version string) bool {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(data)) == version
|
||||
}
|
||||
|
||||
func recordSeen(path, version string) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(path, []byte(version), 0o600)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package patchnotes
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSeenRoundTrip(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "sif", "seen_version")
|
||||
|
||||
if hasSeen(path, "2026.6.7") {
|
||||
t.Fatal("nothing recorded yet, hasSeen should be false")
|
||||
}
|
||||
|
||||
recordSeen(path, "2026.6.7")
|
||||
if !hasSeen(path, "2026.6.7") {
|
||||
t.Error("recorded version should read back as seen")
|
||||
}
|
||||
if hasSeen(path, "2026.6.8") {
|
||||
t.Error("a different version should not be seen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderIncludesTag(t *testing.T) {
|
||||
out := render(&release{TagName: "v2026.6.7", Body: "## what's changed\n- a thing"})
|
||||
if !strings.Contains(out, "v2026.6.7") {
|
||||
t.Errorf("rendered notes should include the tag, got %q", out)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,9 +15,10 @@ package builtin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FrameworksModule struct{}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,6 +15,7 @@ package builtin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
@@ -51,7 +52,8 @@ func (m *NucleiModule) Execute(ctx context.Context, target string, opts modules.
|
||||
}
|
||||
|
||||
// Process nuclei results into module findings
|
||||
for _, event := range nucleiResults {
|
||||
for i := range nucleiResults {
|
||||
event := &nucleiResults[i]
|
||||
severity := "info"
|
||||
|
||||
switch event.Info.SeverityHolder.Severity.String() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,9 +15,10 @@ package builtin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ShodanModule struct{}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -14,6 +14,7 @@ package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -25,15 +25,19 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
// s3EndpointFmt is a var so integration tests can repoint it at a fixture; the
|
||||
// %s is the bucket name.
|
||||
var s3EndpointFmt = "https://%s.s3.amazonaws.com"
|
||||
|
||||
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") + "..."))
|
||||
fmt.Println(styles.Separator.Render("Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Cloud Storage Misconfiguration Scan"); err != nil {
|
||||
@@ -43,7 +47,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 +85,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 {
|
||||
@@ -97,7 +100,7 @@ func extractPotentialBuckets(url string) []string {
|
||||
}
|
||||
|
||||
func checkS3Bucket(ctx context.Context, bucket string, client *http.Client) (bool, error) {
|
||||
url := fmt.Sprintf("https://%s.s3.amazonaws.com", bucket)
|
||||
url := fmt.Sprintf(s3EndpointFmt, bucket)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -35,7 +35,7 @@ func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
|
||||
spin := output.NewSpinner("Detecting content management system")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "CMS detection"); err != nil {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -27,11 +26,13 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// directoryURL is a var so integration tests can repoint it at a fixture.
|
||||
var directoryURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dirlist/"
|
||||
|
||||
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"
|
||||
smallFile = "directory-list-2.3-small.txt"
|
||||
mediumFile = "directory-list-2.3-medium.txt"
|
||||
bigFile = "directory-list-2.3-big.txt"
|
||||
)
|
||||
|
||||
type DirectoryResult struct {
|
||||
@@ -44,7 +45,7 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
|
||||
log := output.Module("DIRLIST")
|
||||
log.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" directory fuzzing"); err != nil {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -26,8 +25,14 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// dnsURL is a var so integration tests can repoint it at a fixture.
|
||||
var dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
|
||||
|
||||
// dnsTransport is a var so integration tests can route the per-host probes at a
|
||||
// local server instead of resolving real DNS. nil keeps http.DefaultTransport.
|
||||
var dnsTransport http.RoundTripper
|
||||
|
||||
const (
|
||||
dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
|
||||
dnsSmallFile = "subdomains-100.txt"
|
||||
dnsMediumFile = "subdomains-1000.txt"
|
||||
dnsBigFile = "subdomains-10000.txt"
|
||||
@@ -67,7 +72,7 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
|
||||
dns = append(dns, scanner.Text())
|
||||
}
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
|
||||
@@ -77,7 +82,8 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
Timeout: timeout,
|
||||
Transport: dnsTransport,
|
||||
}
|
||||
|
||||
progress := output.NewProgress(len(dns), "enumerating")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -60,7 +59,7 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
|
||||
spin := output.NewSpinner("Running Google dorks")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL dorking"); err != nil {
|
||||
@@ -93,6 +92,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 +124,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)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVersionAffected(t *testing.T) {
|
||||
tests := []struct {
|
||||
version string
|
||||
affected string
|
||||
want bool
|
||||
}{
|
||||
{"4.2", "4.2", true},
|
||||
{"4.2.1", "4.2", true},
|
||||
{"4.2.13", "4.2", true},
|
||||
{"4.20", "4.2", false}, // the boundary bug: 4.20 is not a 4.2.x release
|
||||
{"4.20.0", "4.2", false},
|
||||
{"5.0", "4.2", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got := versionAffected(tt.version, tt.affected); got != tt.want {
|
||||
t.Errorf("versionAffected(%q, %q) = %v, want %v", tt.version, tt.affected, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -99,9 +100,11 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
|
||||
}()
|
||||
|
||||
// Find the best match
|
||||
// results arrive in goroutine-completion order; tie-break on name so the
|
||||
// winner is deterministic when two detectors land on the same confidence.
|
||||
var best detectionResult
|
||||
for r := range results {
|
||||
if r.confidence > best.confidence {
|
||||
if r.confidence > best.confidence || (r.confidence == best.confidence && r.name < best.name) {
|
||||
best = r
|
||||
}
|
||||
}
|
||||
@@ -169,7 +172,7 @@ func getVulnerabilities(framework, version string) ([]string, []string) {
|
||||
|
||||
for _, entry := range entries {
|
||||
for _, affectedVer := range entry.AffectedVersions {
|
||||
if version == affectedVer || hasPrefix(version, affectedVer) {
|
||||
if versionAffected(version, affectedVer) {
|
||||
cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity))
|
||||
for _, rec := range entry.Recommendations {
|
||||
if !seenRecs[rec] {
|
||||
@@ -185,7 +188,9 @@ func getVulnerabilities(framework, version string) ([]string, []string) {
|
||||
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
|
||||
// versionAffected reports whether version falls under an affected-version
|
||||
// entry. the entry is a version prefix, matched only on dotted boundaries, so
|
||||
// "4.2" covers 4.2 and 4.2.1 but not 4.20.
|
||||
func versionAffected(version, affected string) bool {
|
||||
return version == affected || strings.HasPrefix(version, affected+".")
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,7 +13,7 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
(c) 2022-2026 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,7 +13,7 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
(c) 2022-2026 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
@@ -47,9 +47,11 @@ func init() {
|
||||
fw.Register(&codeigniterDetector{})
|
||||
}
|
||||
|
||||
// sigmoidConfidence converts a weighted score to a 0-1 confidence value.
|
||||
// sigmoidConfidence maps the matched-weight fraction to a 0-1 confidence,
|
||||
// centered at 0.3 so a single weak signature match no longer clears the 0.5
|
||||
// detection threshold (it used to: sigmoid(0) was 0.5, so any match "detected").
|
||||
func sigmoidConfidence(score float32) float32 {
|
||||
return float32(1.0 / (1.0 + math.Exp(-float64(score)*6.0)))
|
||||
return float32(1.0 / (1.0 + math.Exp(-(float64(score)-0.3)*10.0)))
|
||||
}
|
||||
|
||||
// laravelDetector detects Laravel framework.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,7 +13,7 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
(c) 2022-2026 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSigmoidConfidence(t *testing.T) {
|
||||
// a weak match (small matched-weight fraction) must stay below the 0.5
|
||||
// detection threshold; a strong match must clear it. the old curve put any
|
||||
// match above 0.5, which is what false-detected magento on a plain page.
|
||||
if c := sigmoidConfidence(0); c >= 0.5 {
|
||||
t.Errorf("no match conf = %.3f, want < 0.5", c)
|
||||
}
|
||||
if c := sigmoidConfidence(0.2); c >= 0.5 {
|
||||
t.Errorf("weak match conf = %.3f, want < 0.5", c)
|
||||
}
|
||||
if c := sigmoidConfidence(0.5); c <= 0.5 {
|
||||
t.Errorf("strong match conf = %.3f, want > 0.5", c)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,7 +13,7 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
(c) 2022-2026 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,7 +13,7 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
(c) 2022-2026 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,7 +13,7 @@
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
(c) 2022-2026 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -26,10 +26,10 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const (
|
||||
gitURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/git/"
|
||||
gitFile = "git.txt"
|
||||
)
|
||||
// gitURL is a var so integration tests can repoint it at a fixture.
|
||||
var gitURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/git/"
|
||||
|
||||
const gitFile = "git.txt"
|
||||
|
||||
func Git(url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
log := output.Module("GIT")
|
||||
@@ -38,7 +38,7 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
|
||||
spin := output.NewSpinner("Scanning for exposed git repositories")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "git directory fuzzing"); err != nil {
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,7 +15,6 @@ package scan
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
@@ -31,7 +30,7 @@ func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult,
|
||||
log := output.Module("HEADERS")
|
||||
log.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "HTTP Header Analysis"); err != nil {
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
//go:build integration
|
||||
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// These tests run the real scanners against a local server standing in for a
|
||||
// deliberately-vulnerable app, asserting the findings each one should produce.
|
||||
// They're behind the `integration` build tag so the default `go test` stays
|
||||
// network-free; run with `go test -tags=integration ./internal/scan/...`.
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// newVulnApp serves the planted artifacts each scanner is meant to find, plus
|
||||
// the wordlists the remote-list scanners fetch.
|
||||
func newVulnApp() *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// wordlists the remote-list scanners download
|
||||
mux.HandleFunc("/git.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(".git/HEAD\n.git/config\n"))
|
||||
})
|
||||
mux.HandleFunc("/directory-list-2.3-small.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("admin\nlogin\nnope\n"))
|
||||
})
|
||||
mux.HandleFunc("/subdomains-100.txt", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("dev\nstaging\n"))
|
||||
})
|
||||
|
||||
// an exposed git repo: HEAD is a real find, config is html so it's excluded
|
||||
mux.HandleFunc("/.git/HEAD", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Write([]byte("ref: refs/heads/main\n"))
|
||||
})
|
||||
mux.HandleFunc("/.git/config", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte("<html>nope</html>"))
|
||||
})
|
||||
|
||||
// live directories for dirlist
|
||||
mux.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
|
||||
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
|
||||
|
||||
// an exposed db admin panel for sql recon
|
||||
mux.HandleFunc("/phpmyadmin/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("<title>phpMyAdmin</title>"))
|
||||
})
|
||||
|
||||
// homepage doubles as the cms fingerprint and the lfi sink
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if strings.Contains(r.URL.RawQuery, "passwd") || strings.Contains(r.URL.RawQuery, "etc") {
|
||||
w.Write([]byte("root:x:0:0:root:/root:/bin/bash\n"))
|
||||
return
|
||||
}
|
||||
w.Header().Set("X-Powered-By", "PHP/8.1.0")
|
||||
w.Write([]byte(`<html><head><link href="/wp-content/themes/x/style.css"></head><body>hi</body></html>`))
|
||||
})
|
||||
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
|
||||
func TestIntegrationGit(t *testing.T) {
|
||||
srv := newVulnApp()
|
||||
defer srv.Close()
|
||||
orig := gitURL
|
||||
gitURL = srv.URL + "/"
|
||||
defer func() { gitURL = orig }()
|
||||
|
||||
found, err := Git(srv.URL, 5*time.Second, 2, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Git: %v", err)
|
||||
}
|
||||
if len(found) != 1 {
|
||||
t.Fatalf("expected 1 git find (HEAD, not the html config), got %d: %v", len(found), found)
|
||||
}
|
||||
if !strings.HasSuffix(found[0], ".git/HEAD") {
|
||||
t.Errorf("expected .git/HEAD, got %s", found[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDirlist(t *testing.T) {
|
||||
srv := newVulnApp()
|
||||
defer srv.Close()
|
||||
orig := directoryURL
|
||||
directoryURL = srv.URL + "/"
|
||||
defer func() { directoryURL = orig }()
|
||||
|
||||
results, err := Dirlist("small", srv.URL, 5*time.Second, 3, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Dirlist: %v", err)
|
||||
}
|
||||
|
||||
got := map[string]bool{}
|
||||
for _, r := range results {
|
||||
got[r.Url] = true
|
||||
}
|
||||
if !hasSuffixIn(got, "/admin") || !hasSuffixIn(got, "/login") {
|
||||
t.Errorf("expected admin and login to be found, got %v", results)
|
||||
}
|
||||
if hasSuffixIn(got, "/nope") {
|
||||
t.Errorf("404 path nope should not be reported, got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationCMS(t *testing.T) {
|
||||
srv := newVulnApp()
|
||||
defer srv.Close()
|
||||
|
||||
result, err := CMS(srv.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CMS: %v", err)
|
||||
}
|
||||
if result == nil || result.Name != "WordPress" {
|
||||
t.Errorf("expected WordPress, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationHeaders(t *testing.T) {
|
||||
srv := newVulnApp()
|
||||
defer srv.Close()
|
||||
|
||||
results, err := Headers(srv.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Headers: %v", err)
|
||||
}
|
||||
if len(results) == 0 {
|
||||
t.Error("expected at least one header back")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationSQL(t *testing.T) {
|
||||
srv := newVulnApp()
|
||||
defer srv.Close()
|
||||
|
||||
result, err := SQL(srv.URL, 5*time.Second, 5, "")
|
||||
if err != nil {
|
||||
t.Fatalf("SQL: %v", err)
|
||||
}
|
||||
if result == nil || len(result.AdminPanels) == 0 {
|
||||
t.Fatalf("expected an admin panel finding, got %+v", result)
|
||||
}
|
||||
if result.AdminPanels[0].Type != "phpMyAdmin" {
|
||||
t.Errorf("expected phpMyAdmin, got %s", result.AdminPanels[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationLFI(t *testing.T) {
|
||||
srv := newVulnApp()
|
||||
defer srv.Close()
|
||||
|
||||
result, err := LFI(srv.URL, 5*time.Second, 5, "")
|
||||
if err != nil {
|
||||
t.Fatalf("LFI: %v", err)
|
||||
}
|
||||
if result == nil || len(result.Vulnerabilities) == 0 {
|
||||
t.Errorf("expected an lfi finding from the passwd sink, got %+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationPorts(t *testing.T) {
|
||||
// a real listener stands in for an open port; a tiny server hands its number
|
||||
// to Ports via the commonPorts wordlist.
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
port := ln.Addr().(*net.TCPAddr).Port
|
||||
|
||||
list := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(strconv.Itoa(port) + "\n"))
|
||||
}))
|
||||
defer list.Close()
|
||||
orig := commonPorts
|
||||
commonPorts = list.URL
|
||||
defer func() { commonPorts = orig }()
|
||||
|
||||
open, err := Ports(context.Background(), "common", "tcp://127.0.0.1", 2*time.Second, 1, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Ports: %v", err)
|
||||
}
|
||||
found := false
|
||||
for _, p := range open {
|
||||
if p == strconv.Itoa(port) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected open port %d in %v", port, open)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationShodan(t *testing.T) {
|
||||
// a local server stands in for api.shodan.io; example.com resolves to a real
|
||||
// IP but the lookup never leaves the box.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query().Get("key") != "test-key" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(shodanHostResponse{
|
||||
IP: "93.184.216.34",
|
||||
Hostnames: []string{"example.com"},
|
||||
Org: "EDGECAST",
|
||||
Ports: []int{80, 443},
|
||||
Data: []shodanData{
|
||||
{Port: 80, Transport: "tcp", Product: "nginx", Version: "1.18.0"},
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
orig := shodanBaseURL
|
||||
shodanBaseURL = srv.URL
|
||||
defer func() { shodanBaseURL = orig }()
|
||||
|
||||
t.Setenv("SHODAN_API_KEY", "test-key")
|
||||
|
||||
result, err := Shodan("https://example.com", 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Shodan: %v", err)
|
||||
}
|
||||
if result == nil || result.IP != "93.184.216.34" {
|
||||
t.Fatalf("expected parsed shodan result, got %+v", result)
|
||||
}
|
||||
if len(result.Services) != 1 || result.Services[0].Product != "nginx" {
|
||||
t.Errorf("expected one nginx service, got %+v", result.Services)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationSecurityTrails(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("APIKEY") != "test-key" {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(r.URL.Path, "/subdomains"):
|
||||
json.NewEncoder(w).Encode(stSubdomainsResponse{Subdomains: []string{"www", "api"}})
|
||||
case strings.HasSuffix(r.URL.Path, "/associated"):
|
||||
json.NewEncoder(w).Encode(stAssociatedResponse{Records: []stAssociatedRecord{{Hostname: "example.org"}}})
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
orig := securityTrailsBaseURL
|
||||
securityTrailsBaseURL = srv.URL
|
||||
defer func() { securityTrailsBaseURL = orig }()
|
||||
|
||||
t.Setenv("SECURITYTRAILS_API_KEY", "test-key")
|
||||
|
||||
result, err := SecurityTrails("https://example.com", 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("SecurityTrails: %v", err)
|
||||
}
|
||||
if len(result.Subdomains) != 2 {
|
||||
t.Errorf("expected 2 subdomains, got %v", result.Subdomains)
|
||||
}
|
||||
if len(result.AssociatedDomains) != 1 || result.AssociatedDomains[0] != "example.org" {
|
||||
t.Errorf("expected example.org associated, got %v", result.AssociatedDomains)
|
||||
}
|
||||
|
||||
urls := result.DiscoveredURLs()
|
||||
if !contains(urls, "https://www.example.com") || !contains(urls, "https://example.org") {
|
||||
t.Errorf("expected discovered urls to expand subs and associated, got %v", urls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationCloudStorage(t *testing.T) {
|
||||
// the fixture returns 200 only for the planted bucket, so any candidate that
|
||||
// matches it is reported public.
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/example" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
orig := s3EndpointFmt
|
||||
s3EndpointFmt = srv.URL + "/%s"
|
||||
defer func() { s3EndpointFmt = orig }()
|
||||
|
||||
results, err := CloudStorage("https://example.com", 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CloudStorage: %v", err)
|
||||
}
|
||||
|
||||
var public bool
|
||||
for _, r := range results {
|
||||
if r.BucketName == "example" && r.IsPublic {
|
||||
public = true
|
||||
}
|
||||
}
|
||||
if !public {
|
||||
t.Errorf("expected the example bucket to be flagged public, got %+v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationDnslist(t *testing.T) {
|
||||
// the probe server answers any host routed to it; dnsTransport pins every
|
||||
// dial here so no real DNS is touched.
|
||||
probe := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer probe.Close()
|
||||
probeAddr := strings.TrimPrefix(probe.URL, "http://")
|
||||
|
||||
list := newVulnApp()
|
||||
defer list.Close()
|
||||
origURL := dnsURL
|
||||
dnsURL = list.URL + "/"
|
||||
defer func() { dnsURL = origURL }()
|
||||
|
||||
origTr := dnsTransport
|
||||
dnsTransport = &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, probeAddr)
|
||||
},
|
||||
}
|
||||
defer func() { dnsTransport = origTr }()
|
||||
|
||||
found, err := Dnslist("small", "http://example.com", 5*time.Second, 2, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Dnslist: %v", err)
|
||||
}
|
||||
// http probes land on the plain-http probe server; https fails the tls
|
||||
// handshake and is dropped, which is fine - the planted sub still shows up.
|
||||
if !hasSuffixIn(sliceSet(found), "dev.example.com") {
|
||||
t.Errorf("expected dev.example.com among findings, got %v", found)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s []string, v string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sliceSet(s []string) map[string]bool {
|
||||
set := make(map[string]bool, len(s))
|
||||
for i := 0; i < len(s); i++ {
|
||||
set[s[i]] = true
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
func hasSuffixIn(set map[string]bool, suffix string) bool {
|
||||
for k := range set {
|
||||
if strings.HasSuffix(k, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -135,7 +135,7 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
|
||||
content := string(bodyBytes)
|
||||
|
||||
charmlog.Debugf("Running supabase scanner on %s", script)
|
||||
scriptSupabaseResults, err := ScanSupabase(content, script)
|
||||
scriptSupabaseResults, err := ScanSupabase(content, script, timeout)
|
||||
|
||||
if err != nil {
|
||||
charmlog.Errorf("Error while scanning supabase: %s", err)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -70,8 +70,8 @@ type supabaseOpenAPIResponse struct {
|
||||
}
|
||||
|
||||
// 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) //nolint:bodyclose // closed in doSupabaseRequest
|
||||
func getSupabaseArrayResponse(projectId, path, apikey string, auth *string, timeout time.Duration) (*supabaseArrayResponse, error) {
|
||||
body, resp, err := doSupabaseRequest(projectId, path, apikey, auth, timeout) //nolint:bodyclose // closed in doSupabaseRequest
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -95,8 +95,8 @@ func getSupabaseArrayResponse(projectId, path, apikey string, auth *string) (*su
|
||||
}
|
||||
|
||||
// getSupabaseOpenAPI fetches the OpenAPI spec from Supabase.
|
||||
func getSupabaseOpenAPI(projectId, apikey string, auth *string) (*supabaseOpenAPIResponse, error) {
|
||||
body, _, err := doSupabaseRequest(projectId, "/rest/v1/", apikey, auth) //nolint:bodyclose // closed in doSupabaseRequest
|
||||
func getSupabaseOpenAPI(projectId, apikey string, auth *string, timeout time.Duration) (*supabaseOpenAPIResponse, error) {
|
||||
body, _, err := doSupabaseRequest(projectId, "/rest/v1/", apikey, auth, timeout) //nolint:bodyclose // closed in doSupabaseRequest
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,8 +109,8 @@ func getSupabaseOpenAPI(projectId, apikey string, auth *string) (*supabaseOpenAP
|
||||
}
|
||||
|
||||
// doSupabaseRequest performs a GET request to the Supabase API.
|
||||
func doSupabaseRequest(projectId, path, apikey string, auth *string) ([]byte, *http.Response, error) {
|
||||
client := http.Client{}
|
||||
func doSupabaseRequest(projectId, path, apikey string, auth *string, timeout time.Duration) ([]byte, *http.Response, error) {
|
||||
client := http.Client{Timeout: timeout}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "https://"+projectId+".supabase.co"+path, http.NoBody)
|
||||
if err != nil {
|
||||
@@ -142,9 +142,9 @@ func doSupabaseRequest(projectId, path, apikey string, auth *string) ([]byte, *h
|
||||
return body, resp, nil
|
||||
}
|
||||
|
||||
func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error) {
|
||||
func ScanSupabase(jsContent string, jsUrl string, timeout time.Duration) ([]supabaseScanResult, error) {
|
||||
supabaselog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "🚧 JavaScript > Supabase ⚡️",
|
||||
Prefix: "JavaScript > Supabase",
|
||||
}).With("url", jsUrl)
|
||||
|
||||
var results = []supabaseScanResult{}
|
||||
@@ -182,7 +182,7 @@ func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error)
|
||||
}
|
||||
|
||||
supabaselog.Infof("Found valid supabase project %s with role %s", *supabaseJwt.ProjectId, *supabaseJwt.Role)
|
||||
client := http.Client{}
|
||||
client := http.Client{Timeout: timeout}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.TODO(), http.MethodPost, "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 {
|
||||
@@ -219,7 +219,7 @@ func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error)
|
||||
|
||||
var collections = []supabaseCollection{}
|
||||
|
||||
openAPI, err := getSupabaseOpenAPI(*supabaseJwt.ProjectId, jwt, &auth)
|
||||
openAPI, err := getSupabaseOpenAPI(*supabaseJwt.ProjectId, jwt, &auth, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error)
|
||||
continue
|
||||
}
|
||||
|
||||
sampleResp, err := getSupabaseArrayResponse(*supabaseJwt.ProjectId, "/rest/v1"+path, jwt, &auth)
|
||||
sampleResp, err := getSupabaseArrayResponse(*supabaseJwt.ProjectId, "/rest/v1"+path, jwt, &auth, timeout)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -119,7 +118,7 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
|
||||
spin := output.NewSpinner("Scanning for LFI vulnerabilities")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
sanitizedURL := stripScheme(targetURL)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,7 +15,6 @@ package scan
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -69,7 +68,7 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou
|
||||
}
|
||||
defer ne.Close()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
ne.LoadTargets([]string{sanitizedURL}, false)
|
||||
|
||||
var results []output.ResultEvent
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -28,13 +27,14 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const commonPorts = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/ports/top-ports.txt"
|
||||
// commonPorts is a var so integration tests can repoint it at a fixture.
|
||||
var 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) {
|
||||
func Ports(ctx context.Context, scope string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
log := output.Module("PORTS")
|
||||
log.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, scope+" port scanning"); err != nil {
|
||||
log.Error("Error creating log file: %v", err)
|
||||
@@ -89,7 +89,8 @@ func Ports(scope string, url string, timeout time.Duration, threads int, logdir
|
||||
progress.Increment(strconv.Itoa(port))
|
||||
|
||||
charmlog.Debugf("Looking up: %d", port)
|
||||
tcp, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", sanitizedURL, port), timeout)
|
||||
addr := fmt.Sprintf("%s:%d", sanitizedURL, port)
|
||||
tcp, err := (&net.Dialer{Timeout: timeout}).DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
charmlog.Debugf("Error %d: %v", port, err)
|
||||
} else {
|
||||
@@ -100,7 +101,7 @@ func Ports(scope string, url string, timeout time.Duration, threads int, logdir
|
||||
mu.Lock()
|
||||
openPorts = append(openPorts, strconv.Itoa(port))
|
||||
mu.Unlock()
|
||||
tcp.Close()
|
||||
_ = tcp.Close()
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -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)
|
||||
|
||||
+11
-2
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -31,6 +31,15 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// stripScheme drops the scheme:// prefix from url, or returns it unchanged when
|
||||
// there's no scheme (so a bare host doesn't panic).
|
||||
func stripScheme(url string) string {
|
||||
if _, rest, ok := strings.Cut(url, "://"); ok {
|
||||
return rest
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func fetchRobotsTXT(url string, client *http.Client) *http.Response {
|
||||
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
@@ -67,7 +76,7 @@ func fetchRobotsTXT(url string, client *http.Client) *http.Response {
|
||||
func Scan(url string, timeout time.Duration, threads int, logdir string) {
|
||||
output.ScanStart("base URL scanning")
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL scanning"); err != nil {
|
||||
|
||||
@@ -200,3 +200,24 @@ func TestHeaderResult(t *testing.T) {
|
||||
t.Errorf("expected value 'application/json', got '%s'", result.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripScheme(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
want string
|
||||
}{
|
||||
{"https with path", "https://example.com/path", "example.com/path"},
|
||||
{"http", "http://example.com", "example.com"},
|
||||
{"no scheme stays put", "example.com", "example.com"},
|
||||
{"empty", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := stripScheme(tt.url); got != tt.want {
|
||||
t.Errorf("stripScheme(%q) = %q, want %q", tt.url, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 := stripScheme(url)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -27,7 +27,8 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const securityTrailsBaseURL = "https://api.securitytrails.com/v1"
|
||||
// securityTrailsBaseURL is a var so integration tests can repoint it at a fixture.
|
||||
var securityTrailsBaseURL = "https://api.securitytrails.com/v1"
|
||||
|
||||
// SecurityTrailsResult holds discovered domains from SecurityTrails API
|
||||
type SecurityTrailsResult struct {
|
||||
@@ -100,7 +101,7 @@ func SecurityTrails(targetURL string, timeout time.Duration, logdir string) (*Se
|
||||
spin.Stop()
|
||||
|
||||
if logdir != "" {
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
sanitizedURL := stripScheme(targetURL)
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "SecurityTrails lookup"); err != nil {
|
||||
output.Error("error writing log header: %v", err)
|
||||
}
|
||||
|
||||
+10
-10
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -28,7 +28,8 @@ import (
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const shodanBaseURL = "https://api.shodan.io"
|
||||
// shodanBaseURL is a var so integration tests can repoint it at a fixture.
|
||||
var shodanBaseURL = "https://api.shodan.io"
|
||||
|
||||
// ShodanResult represents the results from a Shodan host lookup
|
||||
type ShodanResult struct {
|
||||
@@ -73,7 +74,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"`
|
||||
@@ -135,7 +135,7 @@ func Shodan(targetURL string, timeout time.Duration, logdir string) (*ShodanResu
|
||||
|
||||
// log results
|
||||
if logdir != "" {
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
sanitizedURL := stripScheme(targetURL)
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Shodan lookup"); err != nil {
|
||||
output.Error("Error writing log header: %v", err)
|
||||
}
|
||||
@@ -160,20 +160,20 @@ func resolveHostname(hostname string) (string, error) {
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
ips, err := net.LookupIP(hostname)
|
||||
addrs, err := net.DefaultResolver.LookupIPAddr(context.TODO(), hostname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// prefer IPv4
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
return ip.String(), nil
|
||||
for _, addr := range addrs {
|
||||
if addr.IP.To4() != nil {
|
||||
return addr.IP.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
return ips[0].String(), nil
|
||||
if len(addrs) > 0 {
|
||||
return addrs[0].IP.String(), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no IP addresses found for %s", hostname)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -121,7 +121,7 @@ func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*
|
||||
spin := output.NewSpinner("Scanning for SQL exposures")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
sanitizedURL := stripScheme(targetURL)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "SQL reconnaissance"); err != nil {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -36,22 +36,12 @@ 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]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Subdomain Takeover Vulnerability Check"); err != nil {
|
||||
@@ -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{
|
||||
@@ -124,7 +114,7 @@ func checkSubdomainTakeover(subdomain string, client *http.Client) (bool, string
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "no such host") {
|
||||
// Check if CNAME exists
|
||||
cname, err := net.LookupCNAME(subdomain)
|
||||
cname, err := net.DefaultResolver.LookupCNAME(context.TODO(), subdomain)
|
||||
if err == nil && cname != "" {
|
||||
return true, "Dangling CNAME"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,8 +13,6 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
@@ -24,7 +22,7 @@ import (
|
||||
func Whois(url string, logdir string) {
|
||||
output.ScanStart("WHOIS lookup")
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, " WHOIS scanning"); err != nil {
|
||||
output.Error("Error creating log file: %v", err)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package version resolves sif's version from the build.
|
||||
package version
|
||||
|
||||
import (
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Resolve returns the best version available: the build-time ldflag if it was
|
||||
// stamped, else the go build info (module tag or vcs revision), else "dev". the
|
||||
// leading v is dropped so it matches the bare form the rest of sif uses.
|
||||
func Resolve(ldflag string) string {
|
||||
if ldflag != "" && ldflag != "dev" {
|
||||
return normalize(ldflag)
|
||||
}
|
||||
if v := fromBuildInfo(); v != "" {
|
||||
return normalize(v)
|
||||
}
|
||||
return "dev"
|
||||
}
|
||||
|
||||
func fromBuildInfo() string {
|
||||
info, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if v := info.Main.Version; v != "" && v != "(devel)" {
|
||||
return v
|
||||
}
|
||||
|
||||
// no module tag (a local build) - fall back to the commit it was built from
|
||||
var revision, modified string
|
||||
for _, s := range info.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
revision = s.Value
|
||||
case "vcs.modified":
|
||||
modified = s.Value
|
||||
}
|
||||
}
|
||||
if revision == "" {
|
||||
return ""
|
||||
}
|
||||
if len(revision) > 12 {
|
||||
revision = revision[:12]
|
||||
}
|
||||
if modified == "true" {
|
||||
revision += "-dirty"
|
||||
}
|
||||
return revision
|
||||
}
|
||||
|
||||
func normalize(v string) string {
|
||||
return strings.TrimPrefix(v, "v")
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package version
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestResolveLdflag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ldflag string
|
||||
want string
|
||||
}{
|
||||
{"tag with v", "v2026.6.7", "2026.6.7"},
|
||||
{"tag without v", "2026.6.7", "2026.6.7"},
|
||||
{"pseudo version", "2026.2.17-57-geb33321", "2026.2.17-57-geb33321"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := Resolve(tt.ldflag); got != tt.want {
|
||||
t.Errorf("Resolve(%q) = %q, want %q", tt.ldflag, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// with no ldflag, Resolve falls back to build info; in a test binary that's
|
||||
// non-deterministic, so just assert it never returns an empty string.
|
||||
func TestResolveFallbackNonEmpty(t *testing.T) {
|
||||
if Resolve("dev") == "" {
|
||||
t.Error("Resolve fallback should never be empty")
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 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-2025 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
.\" man page for sif - the blazing-fast pentesting suite
|
||||
.TH sif 1 "2026-06-08" "sif" "sif manual"
|
||||
.SH NAME
|
||||
sif \- blazing-fast pentesting suite
|
||||
.SH SYNOPSIS
|
||||
.B sif
|
||||
.B \-u
|
||||
.I url
|
||||
.RI [ scans ]
|
||||
.RI [ options ]
|
||||
.br
|
||||
.B sif
|
||||
.B \-f
|
||||
.I file
|
||||
.RI [ scans ]
|
||||
.RI [ options ]
|
||||
.br
|
||||
.B sif
|
||||
.RB { patchnote | version }
|
||||
.SH DESCRIPTION
|
||||
.B sif
|
||||
is a modular recon and exploitation suite. it runs multiple scan types
|
||||
concurrently against one or more targets, and can be extended with yaml
|
||||
modules. targets must include a
|
||||
.B http://
|
||||
or
|
||||
.B https://
|
||||
scheme.
|
||||
.SH TARGETS
|
||||
.TP
|
||||
.BR \-u ", " \-\-urls " \fIlist\fR"
|
||||
comma\-separated list of urls to scan.
|
||||
.TP
|
||||
.BR \-f ", " \-\-file " \fIpath\fR"
|
||||
file with one url per line.
|
||||
.SH SCANS
|
||||
.TP
|
||||
.BR \-dirlist " \fIsize\fR"
|
||||
directory and file fuzzing (small/medium/large).
|
||||
.TP
|
||||
.BR \-dnslist " \fIsize\fR"
|
||||
subdomain enumeration (small/medium/large).
|
||||
.TP
|
||||
.BR \-ports " \fIscope\fR"
|
||||
port scanning (common/full).
|
||||
.TP
|
||||
.B \-nuclei
|
||||
vulnerability scanning with nuclei templates.
|
||||
.TP
|
||||
.B \-dork
|
||||
automated google dorking.
|
||||
.TP
|
||||
.B \-js
|
||||
javascript analysis.
|
||||
.TP
|
||||
.B \-c3
|
||||
cloud storage misconfiguration scan.
|
||||
.TP
|
||||
.B \-headers
|
||||
dump the target's response headers.
|
||||
.TP
|
||||
.BR \-sh ", " \-\-security\-headers
|
||||
flag missing or weak security headers and headers that leak server internals.
|
||||
.TP
|
||||
.B \-st
|
||||
subdomain takeover detection (requires \fB\-dnslist\fR).
|
||||
.TP
|
||||
.B \-cms
|
||||
cms detection.
|
||||
.TP
|
||||
.B \-whois
|
||||
whois lookup.
|
||||
.TP
|
||||
.B \-git
|
||||
exposed git repository detection.
|
||||
.TP
|
||||
.B \-shodan
|
||||
shodan host lookup (requires \fBSHODAN_API_KEY\fR).
|
||||
.TP
|
||||
.B \-securitytrails
|
||||
domain discovery and target expansion (requires \fBSECURITYTRAILS_API_KEY\fR).
|
||||
.TP
|
||||
.B \-sql
|
||||
sql reconnaissance (admin panels, error disclosure).
|
||||
.TP
|
||||
.B \-lfi
|
||||
local file inclusion reconnaissance.
|
||||
.TP
|
||||
.B \-framework
|
||||
framework detection with cve lookup.
|
||||
.TP
|
||||
.B \-noscan
|
||||
skip the base url scan (robots.txt, etc).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.BR \-d ", " \-\-debug
|
||||
enable debug logging.
|
||||
.TP
|
||||
.BR \-t ", " \-\-timeout " \fIduration\fR"
|
||||
per\-request timeout (default 10s).
|
||||
.TP
|
||||
.BR \-l ", " \-\-log " \fIdir\fR"
|
||||
directory to write logs to.
|
||||
.TP
|
||||
.BR \-\-threads " \fIn\fR"
|
||||
number of concurrent workers (default 10).
|
||||
.TP
|
||||
.BR \-\-template " \fIname\fR"
|
||||
sif runtime template to use.
|
||||
.TP
|
||||
.B \-api
|
||||
emit json results and suppress the interactive output.
|
||||
.SH MODULES
|
||||
.TP
|
||||
.BR \-m ", " \-\-modules " \fIids\fR"
|
||||
comma\-separated module ids to run.
|
||||
.TP
|
||||
.BR \-mt ", " \-\-module\-tags " \fItags\fR"
|
||||
run modules matching these tags.
|
||||
.TP
|
||||
.BR \-am ", " \-\-all\-modules
|
||||
run all loaded modules.
|
||||
.TP
|
||||
.BR \-lm ", " \-\-list\-modules
|
||||
list available modules and exit.
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
.B sif patchnote
|
||||
fetch the latest github release and print its notes. also available as
|
||||
.BR \-pn .
|
||||
.TP
|
||||
.B sif version
|
||||
print the sif version and exit.
|
||||
.SH ENVIRONMENT
|
||||
.TP
|
||||
.B SHODAN_API_KEY
|
||||
api key used by \fB\-shodan\fR.
|
||||
.TP
|
||||
.B SECURITYTRAILS_API_KEY
|
||||
api key used by \fB\-securitytrails\fR.
|
||||
.TP
|
||||
.B SIF_NO_PATCHNOTES
|
||||
set to any value to suppress the once\-per\-version patch note shown at startup.
|
||||
.SH FILES
|
||||
.TP
|
||||
.I ~/.config/sif/modules/
|
||||
user\-defined yaml modules.
|
||||
.TP
|
||||
.I ~/.config/sif/seen_version
|
||||
records the last release whose notes were shown at startup.
|
||||
.SH EXAMPLES
|
||||
.TP
|
||||
run a few scans against a host:
|
||||
.B sif \-u https://example.com \-headers \-sh \-cms \-framework
|
||||
.TP
|
||||
fuzz directories and enumerate subdomains:
|
||||
.B sif \-u https://example.com \-dirlist medium \-dnslist medium
|
||||
.TP
|
||||
scan a list of targets and write logs:
|
||||
.B sif \-f targets.txt \-headers \-l ./logs
|
||||
.SH SEE ALSO
|
||||
project page: https://github.com/vmfunc/sif
|
||||
.SH AUTHORS
|
||||
vmfunc, xyzeva, and the lunchcat contributors.
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -42,6 +42,9 @@ type App struct {
|
||||
logFiles []string
|
||||
}
|
||||
|
||||
// Version is set by main to the resolved build version and shown on the banner.
|
||||
var Version = "dev"
|
||||
|
||||
type UrlResult struct {
|
||||
Url string `json:"url"`
|
||||
Results []ModuleResult
|
||||
@@ -76,7 +79,11 @@ func New(settings *config.Settings) (*App, error) {
|
||||
|
||||
if !settings.ApiMode {
|
||||
fmt.Println(output.Box.Render(" █▀ █ █▀▀\n ▄█ █ █▀ "))
|
||||
fmt.Println(output.Subheading.Render("\nblazing-fast pentesting suite\n\nbsd 3-clause · (c) 2022-2025 vmfunc, xyzeva & contributors\n"))
|
||||
tagline := "blazing-fast pentesting suite"
|
||||
if Version != "dev" {
|
||||
tagline += " · v" + Version
|
||||
}
|
||||
fmt.Println(output.Subheading.Render("\n" + tagline + "\n\nbsd 3-clause · (c) 2022-2026 vmfunc, xyzeva & contributors\n"))
|
||||
} else {
|
||||
output.SetAPIMode(true)
|
||||
}
|
||||
@@ -166,7 +173,11 @@ func (app *App) Run() error {
|
||||
if err := logger.Init(app.settings.LogDir); err != nil {
|
||||
return err
|
||||
}
|
||||
defer logger.Close()
|
||||
defer func() {
|
||||
if err := logger.Close(); err != nil {
|
||||
log.Errorf("closing logger: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// target expansion - securitytrails discovers new domains before scanning
|
||||
@@ -253,7 +264,7 @@ func (app *App) Run() error {
|
||||
}
|
||||
|
||||
if app.settings.Ports != "none" {
|
||||
result, err := scan.Ports(app.settings.Ports, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
||||
result, err := scan.Ports(context.Background(), app.settings.Ports, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
|
||||
if err != nil {
|
||||
log.Errorf("Error while running port scan: %s", err)
|
||||
} else {
|
||||
@@ -317,6 +328,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 {
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
Reference in New Issue
Block a user