Compare commits

..

5 Commits

Author SHA1 Message Date
vmfunc 97aeb4c8b0 feat: add shodan integration for host reconnaissance
adds a new --shodan flag that queries the shodan api for information
about the target host. requires SHODAN_API_KEY environment variable.

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

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

also fixes go vet warnings for non-constant format strings
2026-01-02 18:03:27 -08:00
vmfunc 3df0064e4b fix: update readme badges and use banner image
- update badges to point to vmfunc/sif
- replace ascii art with banner image
- fix header check action to check first 5 lines
- remove obsolete LICENSE.md
2026-01-02 17:54:17 -08:00
vmfunc c20c37463a chore: delete old license 2026-01-02 17:45:14 -08:00
vmfunc 9190fa4741 license: switch to bsd 3-clause, update headers and readme
- replace proprietary license with bsd 3-clause
- update all go file headers with new retro terminal style
- add header-check github action to enforce license headers
- completely rewrite readme to be modern, sleek, and lowercase
- fix broken badges
2026-01-02 17:41:18 -08:00
114 changed files with 1942 additions and 12821 deletions
+9 -15
View File
@@ -10,7 +10,7 @@
"contributors": [
{
"login": "vmfunc",
"name": "vmfunc",
"name": "mel",
"avatar_url": "https://avatars.githubusercontent.com/u/59031302?v=4",
"profile": "https://vmfunc.re",
"contributions": [
@@ -18,7 +18,12 @@
"mentoring",
"projectManagement",
"security",
"code"
"test",
"business",
"code",
"design",
"financial",
"ideas"
]
},
{
@@ -36,7 +41,6 @@
"avatar_url": "https://avatars.githubusercontent.com/u/127897805?v=4",
"profile": "https://github.com/macdoos",
"contributions": [
"code"
]
},
{
@@ -48,7 +52,7 @@
"ideas"
]
},
{
{
"login": "tessa-u-k",
"name": "tessa ",
"avatar_url": "https://avatars.githubusercontent.com/u/109355732?v=4",
@@ -72,16 +76,6 @@
"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"
]
}
-1
View File
@@ -1 +0,0 @@
* @vmfunc
-15
View File
@@ -1,15 +0,0 @@
# These are supported funding model platforms
github: vmfunc
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
-17
View File
@@ -1,17 +0,0 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- deps
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- deps
-44
View File
@@ -1,44 +0,0 @@
ci:
- changed-files:
- any-glob-to-any-file: ".github/**"
deps:
- changed-files:
- any-glob-to-any-file:
- "go.mod"
- "go.sum"
- "flake.nix"
- "flake.lock"
scan:
- changed-files:
- any-glob-to-any-file: "internal/scan/**"
nuclei:
- changed-files:
- any-glob-to-any-file: "internal/nuclei/**"
modules:
- changed-files:
- any-glob-to-any-file:
- "internal/modules/**"
- "internal/scan/builtin/**"
- "internal/scan/js/**"
- "modules/**"
docs:
- changed-files:
- any-glob-to-any-file:
- "**/*.md"
- "docs/**"
tests:
- changed-files:
- any-glob-to-any-file: "**/*_test.go"
config:
- changed-files:
- any-glob-to-any-file:
- "internal/config/**"
- ".golangci.yml"
- ".editorconfig"
+3 -8
View File
@@ -1,12 +1,7 @@
name: automatic rebase
name: Automatic Rebase
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
jobs:
rebase:
name: Rebase
@@ -14,10 +9,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: automatic rebase
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+7 -18
View File
@@ -1,29 +1,18 @@
name: check large files
name: Check Large Files
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-large-files:
name: check for large files
name: Check for large files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: check for large files
- uses: actions/checkout@v4
- name: Check for large files
run: |
large_files=$(find . -path ./.git -prune -o -type f -size +5M -print)
if [ -n "$large_files" ]; then
echo "$large_files" | while read -r file; do
echo "::error file=${file}::File ${file} is larger than 5MB"
done
exit 1
fi
find . -type f -size +5M | while read file; do
echo "::error file=${file}::File ${file} is larger than 5MB"
done
+12 -27
View File
@@ -1,39 +1,24 @@
name: code quality
name: Qodana
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
schedule:
- cron: "0 6 * * 1" # monday 06:00 UTC
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
codeql:
qodana:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
contents: write
pull-requests: write
checks: write
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
with:
go-version: "1.24"
- name: initialize codeql
uses: github/codeql-action/init@v3
with:
languages: go
- name: build
run: go build ./...
- name: perform codeql analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:go"
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2024.3
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
+5 -9
View File
@@ -1,4 +1,4 @@
name: dependency review
name: "Dependency Review"
on:
pull_request:
push:
@@ -7,20 +7,16 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: checkout repository
uses: actions/checkout@v6
- name: dependency review
- name: "Checkout Repository"
uses: actions/checkout@v4
- name: "Dependency Review"
uses: actions/dependency-review-action@v4
continue-on-error: ${{ github.event_name == 'push' }}
- name: check dependency review outcome
- name: "Check Dependency Review Outcome"
if: github.event_name == 'push' && failure()
run: |
echo "::warning::Dependency review failed. Please check the dependencies for potential issues."
+6 -39
View File
@@ -1,50 +1,17 @@
name: go
name: Go
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
build:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.23", "1.24"]
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: build
go-version: "1.23"
- name: Build
run: make
- name: run tests with coverage
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
- name: upload coverage to codecov
if: matrix.go-version == '1.24'
uses: codecov/codecov-action@v5
with:
files: ./coverage.out
fail_ci_if_error: false
-27
View File
@@ -1,27 +0,0 @@
name: govulncheck
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1" # monday 06:00 UTC
permissions:
contents: read
jobs:
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
with:
go-version: "1.24"
- name: install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- name: run govulncheck
run: govulncheck ./...
continue-on-error: true
+2 -5
View File
@@ -8,14 +8,11 @@ on:
paths:
- '**.go'
permissions:
contents: read
jobs:
check-headers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: check license headers
run: |
@@ -44,7 +41,7 @@ jobs:
echo ': █▀ █ █▀▀ · Blazing-fast pentesting suite :'
echo ': ▄█ █ █▀ · BSD 3-Clause License :'
echo ': :'
echo ': (c) 2022-2025 vmfunc, xyzeva, :'
echo ': (c) 2022-2025 vmfunc (vmfunc), xyzeva, :'
echo ': lunchcat alumni & contributors :'
echo ': :'
echo '·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·'
+2 -8
View File
@@ -1,4 +1,4 @@
name: mind your language
name: Mind your language
on:
issues:
types:
@@ -12,19 +12,13 @@ on:
types:
- created
- edited
permissions:
contents: read
issues: write
pull-requests: write
jobs:
echo_issue_comment:
runs-on: ubuntu-latest
name: profanity check
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Profanity check step
uses: tailaiw/mind-your-language-action@v1.0.3
env:
+3 -7
View File
@@ -1,22 +1,18 @@
name: markdown lint
name: Markdown Lint
on:
pull_request:
paths:
- "**/*.md"
permissions:
contents: read
pull-requests: write
jobs:
markdownlint:
name: runner / markdownlint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: markdownlint
uses: reviewdog/action-markdownlint@v0.26.2
uses: reviewdog/action-markdownlint@v0.24.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
+2 -10
View File
@@ -1,24 +1,16 @@
name: misspell check
name: Misspell Check
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
misspell:
name: runner / misspell
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: misspell
uses: reviewdog/action-misspell@v1.26.0
with:
-139
View File
@@ -1,139 +0,0 @@
name: pr bot
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
configuration-path: .github/labeler.yml
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: label pr size
uses: actions/github-script@v8
with:
script: |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
per_page: 100,
});
const changes = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
let size;
if (changes < 10) size = "size/xs";
else if (changes < 50) size = "size/s";
else if (changes < 200) size = "size/m";
else if (changes < 500) size = "size/l";
else size = "size/xl";
const sizeLabels = ["size/xs", "size/s", "size/m", "size/l", "size/xl"];
const currentLabels = context.payload.pull_request.labels.map(l => l.name);
const toRemove = currentLabels.filter(l => sizeLabels.includes(l) && l !== size);
for (const label of toRemove) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
name: label,
}).catch(() => {});
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: [size],
});
ci-summary:
runs-on: ubuntu-latest
needs: [label, size]
if: always()
steps:
- uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
const { data: checks } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: pr.head.sha,
per_page: 100,
});
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});
const additions = files.reduce((sum, f) => sum + f.additions, 0);
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
const fileCount = files.length;
let body = `### pr summary\n\n`;
body += `**${fileCount}** files changed (+${additions} -${deletions})\n\n`;
const goFiles = files.filter(f => f.filename.endsWith('.go')).length;
const testFiles = files.filter(f => f.filename.endsWith('_test.go')).length;
const ciFiles = files.filter(f => f.filename.startsWith('.github/')).length;
const modFiles = files.filter(f => f.filename === 'go.mod' || f.filename === 'go.sum').length;
if (goFiles > 0 || testFiles > 0 || ciFiles > 0 || modFiles > 0) {
body += `| category | files |\n|----------|-------|\n`;
if (goFiles > 0) body += `| go source | ${goFiles} |\n`;
if (testFiles > 0) body += `| tests | ${testFiles} |\n`;
if (ciFiles > 0) body += `| ci/workflows | ${ciFiles} |\n`;
if (modFiles > 0) body += `| deps | ${modFiles} |\n`;
body += `\n`;
}
// find existing bot comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
});
const marker = '<!-- sif-pr-bot -->';
body = marker + '\n' + body;
const existing = comments.find(c =>
c.user.type === 'Bot' && c.body.includes(marker)
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body,
});
}
+39 -171
View File
@@ -1,9 +1,8 @@
name: release
name: Release
on:
push:
tags:
- "v*"
branches: [main]
permissions:
contents: write
@@ -19,189 +18,58 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.24"
go-version: "1.23"
- name: extract version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
- name: build for windows
- name: Build for Windows
run: |
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-windows-amd64.exe ./cmd/sif
GOOS=windows GOARCH=386 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-windows-386.exe ./cmd/sif
GOOS=windows GOARCH=amd64 go build -o sif-windows-amd64.exe ./cmd/sif
GOOS=windows GOARCH=386 go build -o sif-windows-386.exe ./cmd/sif
- name: build for macOS
- name: Build for macOS
run: |
GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-macos-amd64 ./cmd/sif
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-macos-arm64 ./cmd/sif
GOOS=darwin GOARCH=amd64 go build -o sif-macos-amd64 ./cmd/sif
GOOS=darwin GOARCH=arm64 go build -o sif-macos-arm64 ./cmd/sif
- name: build for linux
- name: Build for Linux
run: |
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-linux-amd64 ./cmd/sif
GOOS=linux GOARCH=386 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-linux-386 ./cmd/sif
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-linux-arm64 ./cmd/sif
GOOS=linux GOARCH=amd64 go build -o sif-linux-amd64 ./cmd/sif
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: Set release version
run: echo "RELEASE_VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: build debian packages
run: |
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_${{ env.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: ${{ env.VERSION }}
Section: security
Priority: optional
Architecture: ${arch}
Maintainer: vmfunc <celeste@linux.com>
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: generate checksums
run: |
sha256sum \
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_*.deb \
> checksums-sha256.txt
- name: generate SBOM
uses: anchore/sbom-action@v0
with:
artifact-name: sbom.spdx.json
output-file: sbom.spdx.json
- name: generate changelog
id: changelog
uses: actions/github-script@v8
with:
result-encoding: string
script: |
const { data: releases } = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 1,
});
const prev = releases.length > 0 ? releases[0].tag_name : '';
const range = prev ? `${prev}...${context.ref}` : '';
const { data: commits } = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: prev ? `${prev}...${{ github.ref_name }}` : `${{ github.sha }}~10...${{ github.sha }}`,
}).catch(() => ({ data: { commits: [] } }));
let log = '';
for (const c of commits.commits || []) {
const msg = c.commit.message.split('\n')[0];
const sha = c.sha.substring(0, 7);
log += `- ${msg} (${sha})\n`;
}
return log || 'initial release';
- name: create release
- name: Create Release and Upload Assets
uses: softprops/action-gh-release@v2
with:
name: sif v${{ env.VERSION }}
tag_name: automated-release-${{ env.RELEASE_VERSION }}
name: Release ${{ env.RELEASE_VERSION }}
body: |
## what's changed
Automated release v${{ env.RELEASE_VERSION }}
${{ steps.changelog.outputs.result }}
## 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`
## install
**homebrew / linuxbrew**
```bash
# coming soon
```
**debian / ubuntu**
```bash
sudo dpkg -i sif_${{ env.VERSION }}_amd64.deb
```
**go install**
```bash
go install github.com/dropalldatabases/sif/cmd/sif@v${{ env.VERSION }}
```
**binary download** - grab the right archive from below.
## verification
```bash
sha256sum -c checksums-sha256.txt
```
For more details, check the [commit history](https://github.com/${{ github.repository }}/commits/main).
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
prerelease: false
files: |
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
checksums-sha256.txt
sbom.spdx.json
sif-windows-amd64.exe
sif-windows-386.exe
sif-macos-amd64
sif-macos-arm64
sif-linux-amd64
sif-linux-386
sif-linux-arm64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: push to cloudsmith
if: ${{ !contains(github.ref_name, '-') }}
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
+3 -10
View File
@@ -1,4 +1,4 @@
name: update report card
name: Update Report Card
on:
push:
@@ -7,17 +7,10 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
update-report-card:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: update go report card
- uses: actions/checkout@v4
- name: Update Go Report Card
uses: creekorful/goreportcard-action@v1.0
+8 -24
View File
@@ -1,4 +1,4 @@
name: functional test
name: Functional Test
on:
push:
@@ -7,39 +7,23 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.24"
- name: build sif
go-version: "1.23"
- name: Build Sif
run: make
- name: run sif with features
- name: Run Sif with features
run: |
./sif -u https://example.com -dnslist small -dirlist small -dork -git -whois -cms -framework
./sif -u https://google.com -dnslist small -dirlist small -dork -git -whois -cms
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
-30
View File
@@ -1,30 +0,0 @@
name: scorecard
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1" # monday 06:00 UTC
permissions: read-all
jobs:
analysis:
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: run scorecard
uses: ossf/scorecard-action@v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: upload sarif results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
+3 -7
View File
@@ -1,22 +1,18 @@
name: shell check
name: Shell Check
on:
pull_request:
paths:
- "**/*.sh"
permissions:
contents: read
pull-requests: write
jobs:
shellcheck:
name: runner / shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: shellcheck
uses: reviewdog/action-shellcheck@v1.32.0
uses: reviewdog/action-shellcheck@v1.27.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
+2 -6
View File
@@ -1,4 +1,4 @@
name: yaml lint
name: YAML Lint
on:
pull_request:
@@ -6,16 +6,12 @@ on:
- "**/*.yml"
- "**/*.yaml"
permissions:
contents: read
pull-requests: write
jobs:
yamllint:
name: runner / yamllint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: yamllint
uses: reviewdog/action-yamllint@v1.19.0
with:
+1 -44
View File
@@ -7,49 +7,12 @@ linters:
- gosimple # simplifications
- ineffassign # useless assignments
- misspell # spelling mistakes
- gocritic # opinionated lints
- revive # replacement for golint
- unconvert # unnecessary type conversions
- bodyclose # http response body not closed
- noctx # http requests without context
- gosec # security issues
- errorlint # error wrapping and comparison
- nilnil # return nil, nil
- wastedassign # assignments to variables never read
- usetesting # os.Setenv in tests instead of t.Setenv, etc.
linters-settings:
govet:
enable-all: true
disable:
- fieldalignment # too many structs to reorder, risks breaking serialization
- shadow # common Go pattern, too noisy
- unusedwrite # false positives on test data structs
errcheck:
check-blank: false
exclude-functions:
- github.com/dropalldatabases/sif/internal/logger.Write # log writes are best-effort
revive:
rules:
- name: exported
disabled: true # stuttering names (scan.ScanResult) require breaking API changes
gocritic:
enabled-tags:
- diagnostic
- style
- performance
disabled-checks:
- commentedOutCode # too opinionated for a project with TODO comments
- paramTypeCombine # style-only, not worth churn
- unnamedResult # style-only
- unnecessaryDefer # common pattern in tests
- nestingReduce # inverting conditions in scan logic hurts readability
gosec:
excludes:
- G104 # errcheck covers this
- G107 # pentesting tool -- variable URLs are the whole point
- G110 # nuclei template decompression, acceptable context
- G304 # sif reads user-supplied wordlist paths -- intentional
run:
timeout: 5m
@@ -57,10 +20,4 @@ run:
issues:
max-issues-per-linter: 50
max-same-issues: 50
exclude-rules:
# test files get some slack
- path: _test\.go
linters:
- errcheck
- noctx
max-same-issues: 3
+4 -124
View File
@@ -4,19 +4,17 @@ 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/vmfunc/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/dropalldatabases/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/vmfunc/sif/issues).
- Have a look at our [issue tracker](https://github.com/dropalldatabases/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
@@ -24,7 +22,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.com/invite/sifcli) 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.gg/dropalldatabases) 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.
@@ -35,7 +33,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
@@ -55,124 +53,6 @@ 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 -1
View File
@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2022-2025 vmfunc, xyzeva, lunchcat alumni,
Copyright (c) 2022-2025 vmfunc (vmfunc), xyzeva, lunchcat alumni,
and other sif contributors.
Redistribution and use in source and binary forms, with or without
+19 -164
View File
@@ -7,13 +7,8 @@
[![go version](https://img.shields.io/github/go-mod/go-version/vmfunc/sif?style=flat-square&color=00ADD8)](https://go.dev/)
[![build](https://img.shields.io/github/actions/workflow/status/vmfunc/sif/go.yml?style=flat-square)](https://github.com/vmfunc/sif/actions)
[![license](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square)](LICENSE)
[![aur](https://img.shields.io/aur/version/sif?style=flat-square&logo=archlinux&logoColor=white&color=1793D1)](https://aur.archlinux.org/packages/sif)
[![nixpkgs](https://img.shields.io/badge/nixpkgs-sif-5277C3?style=flat-square&logo=nixos&logoColor=white)](https://search.nixos.org/packages?query=sif)
[![homebrew](https://img.shields.io/badge/homebrew-tap-FBB040?style=flat-square&logo=homebrew&logoColor=white)](https://github.com/vmfunc/homebrew-sif)
[![apt](https://img.shields.io/badge/apt-cloudsmith-2A5ADF?style=flat-square&logo=debian&logoColor=white)](https://cloudsmith.io/~sif/repos/deb/packages/)
[![discord](https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/sifcli)
**[install](#install) · [usage](#usage) · [modules](#modules) · [docs](docs/) · [contribute](#contribute)**
**[install](#install) · [usage](#usage) · [modules](#modules) · [contribute](#contribute)**
</div>
@@ -29,49 +24,6 @@ sif is a modular pentesting toolkit written in go. it's designed to be fast, con
## install
### homebrew (macos)
```bash
brew tap vmfunc/sif
brew install sif
```
### arch linux (aur)
install using your preferred aur helper:
```bash
yay -S sif
# or
paru -S sif
```
### nix
```bash
# nixpkgs (declarative — add to configuration.nix or home-manager)
environment.systemPackages = [ pkgs.sif ];
# or imperatively
nix profile install nixpkgs#sif
# or just run it without installing
nix run nixpkgs#sif -- -u https://example.com -all
```
the repo also ships a flake if you want to build from source:
```bash
nix run github:vmfunc/sif
```
### 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).
@@ -79,21 +31,13 @@ grab the latest binary from [releases](https://github.com/vmfunc/sif/releases).
### from source
```bash
git clone https://github.com/vmfunc/sif.git
git clone https://github.com/dropalldatabases/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
@@ -112,19 +56,6 @@ makepkg -si
# 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
# securitytrails domain discovery (requires SECURITYTRAILS_API_KEY env var)
# discovers subdomains + associated domains, then scans all of them
./sif -u https://example.com -securitytrails -headers
# 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
```
@@ -133,86 +64,20 @@ run `./sif -h` for all options.
## modules
sif has a modular architecture. modules are defined in yaml and can be extended by users.
### built-in scan flags
| 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) |
| `-securitytrails` | domain discovery + target expansion (requires SECURITYTRAILS_API_KEY) |
| `-sql` | sql recon |
| `-lfi` | local file inclusion |
| `-framework` | framework detection with cve lookup |
### yaml modules
list available modules:
```bash
./sif -lm
```
run specific modules:
```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.
| module | description |
|--------|-------------|
| `dirlist` | directory and file fuzzing |
| `dnslist` | subdomain enumeration |
| `ports` | port and service scanning |
| `nuclei` | vulnerability scanning with nuclei templates |
| `dork` | automated google dorking |
| `js` | javascript framework detection (next.js, supabase) |
| `c3` | cloud storage misconfiguration scanning |
| `headers` | http header analysis |
| `takeover` | subdomain takeover detection |
| `cms` | cms detection |
| `whois` | whois lookups |
| `git` | exposed git repository detection |
## contribute
@@ -229,12 +94,6 @@ golangci-lint run
go test ./...
```
## community
join our discord for support, feature discussions, and pentesting tips:
[![discord](https://img.shields.io/badge/join%20our%20discord-5865F2?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/sifcli)
## contributors
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
@@ -243,16 +102,12 @@ join our discord for support, feature discussions, and pentesting tips:
<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="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://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://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="https://github.com/lunchcat/sif/commits?author=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="#code-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="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>
<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>
</tr>
</tbody>
</table>
-15
View File
@@ -1,15 +0,0 @@
# security policy
## reporting a vulnerability
if you find a security issue in sif, email celeste@linux.com directly.
don't open a public issue.
expect a response within 48 hours. if it's confirmed, i'll push a fix
and credit you in the release notes (unless you'd rather stay anonymous).
## scope
sif is a pentesting tool — "it can scan things" is not a vulnerability.
actual bugs: command injection in user input handling, path traversal in
template extraction, credential leaks, that kind of thing.
+2 -5
View File
@@ -4,7 +4,7 @@
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
@@ -15,10 +15,7 @@ package main
import (
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif"
"github.com/dropalldatabases/sif/internal/config"
// Register framework detectors
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
"github.com/dropalldatabases/sif/pkg/config"
)
func main() {
-52
View File
@@ -1,52 +0,0 @@
# 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
-160
View File
@@ -1,160 +0,0 @@
# 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
-162
View File
@@ -1,162 +0,0 @@
# 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.
-185
View File
@@ -1,185 +0,0 @@
# 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
-93
View File
@@ -1,93 +0,0 @@
# 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
View File
@@ -1,387 +0,0 @@
# 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
```
-102
View File
@@ -1,102 +0,0 @@
# 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
View File
@@ -1,228 +0,0 @@
# 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
View File
@@ -1,293 +0,0 @@
# 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
+62 -4
View File
@@ -1,12 +1,35 @@
{
"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": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"lastModified": 1693844670,
"narHash": "sha256-t69F2nBB8DNQUWHD809oJZJVE+23XBrth4QZuVd6IE0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"rev": "3c15feef7770eb5500a4b8792623e2d6f598c9c1",
"type": "github"
},
"original": {
@@ -18,7 +41,42 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"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"
}
}
},
+28 -49
View File
@@ -1,57 +1,36 @@
{
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 }:
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;
outputs = { self, nixpkgs, utils, gomod2nix }:
utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs {
inherit system;
overlays = [ gomod2nix.overlays.default ];
};
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [ go gopls ];
};
});
};
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
];
};
});
}
+127 -294
View File
@@ -1,392 +1,225 @@
module github.com/dropalldatabases/sif
go 1.24.2
go 1.24.0
toolchain go1.25.5
require (
github.com/antchfx/htmlquery v1.3.5
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/log v0.4.2
github.com/likexian/whois v1.15.7
github.com/projectdiscovery/goflags v0.1.74
github.com/projectdiscovery/nuclei/v3 v3.7.0
github.com/projectdiscovery/utils v0.9.0
github.com/antchfx/htmlquery v1.3.0
github.com/charmbracelet/lipgloss v0.8.0
github.com/charmbracelet/log v0.2.4
github.com/likexian/whois v1.15.1
github.com/projectdiscovery/goflags v0.1.54
github.com/projectdiscovery/nuclei/v2 v2.9.14
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 (
aead.dev/minisign v0.2.0 // indirect
carvel.dev/ytt v0.52.0 // indirect
code.gitea.io/sdk/gitea v0.17.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d // indirect
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 // indirect
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
github.com/akrylysov/pogreb v0.10.1 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
github.com/alecthomas/jsonschema v0.0.0-20211022214203-8b29eab41725 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/alexsnet/go-vnc v0.1.0 // indirect
github.com/alitto/pond v1.9.2 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/andygrunwald/go-jira v1.16.1 // indirect
github.com/antchfx/xmlquery v1.4.4 // indirect
github.com/antchfx/xpath v1.3.5 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/andygrunwald/go-jira v1.16.0 // indirect
github.com/antchfx/xmlquery v1.3.15 // indirect
github.com/antchfx/xpath v1.2.4 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.36.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.82.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.28 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.5.0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/brianvoe/gofakeit/v7 v7.2.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/caddyserver/certmagic v0.19.2 // indirect
github.com/censys/censys-sdk-go v0.19.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/glamour v0.10.0 // indirect
github.com/charmbracelet/x/ansi v0.8.0 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cheggaaa/pb/v3 v3.1.6 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
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.6.1 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.5.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // 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
github.com/distribution/reference v0.6.0 // indirect
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/docker v28.3.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/free5gc/util v1.0.5-0.20230511064842-2e120956883b // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gaissmai/bart v0.26.0 // indirect
github.com/geoffgarside/ber v1.1.0 // indirect
github.com/getkin/kin-openapi v0.132.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.5 // indirect
github.com/go-ldap/ldap/v3 v3.4.11 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-pg/pg/v10 v10.15.0 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-rod/rod v0.116.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/go-rod/rod v0.114.0 // indirect
github.com/goburrow/cache v0.1.4 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gobwas/ws v1.2.1 // indirect
github.com/gocolly/colly/v2 v2.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // 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
github.com/google/certificate-transparency-go v1.3.2 // indirect
github.com/google/certificate-transparency-go v1.1.4 // indirect
github.com/google/go-github v17.0.0+incompatible // indirect
github.com/google/go-github/v30 v30.1.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gosimple/slug v1.15.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect
github.com/hdm/jarm-go v0.0.7 // indirect
github.com/iangcarroll/cookiemonster v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/itchyny/gojq v0.12.17 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/itchyny/gojq v0.12.13 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
github.com/kataras/jwt v0.1.10 // indirect
github.com/kataras/jwt v0.1.8 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kitabisa/go-ci v1.0.3 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labstack/echo/v4 v4.13.4 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/logrusorgru/aurora/v4 v4.0.0 // indirect
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d // indirect
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mackerelio/go-osstat v0.2.4 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/maypok86/otter/v2 v2.2.1 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/mholt/archives v0.1.5 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/microsoft/go-mssqldb v1.9.2 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/mholt/archiver v3.1.1+incompatible // indirect
github.com/mholt/archiver/v3 v3.5.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/miekg/dns v1.1.56 // indirect
github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.0.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/praetorian-inc/fingerprintx v1.1.15 // indirect
github.com/projectdiscovery/asnmap v1.1.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/projectdiscovery/asnmap v1.1.0 // indirect
github.com/projectdiscovery/blackrock v0.0.1 // indirect
github.com/projectdiscovery/cdncheck v1.2.20 // indirect
github.com/projectdiscovery/clistats v0.1.1 // indirect
github.com/projectdiscovery/dsl v0.8.12 // indirect
github.com/projectdiscovery/fastdialer v0.5.3 // indirect
github.com/projectdiscovery/cdncheck v1.0.9 // indirect
github.com/projectdiscovery/clistats v0.0.19 // indirect
github.com/projectdiscovery/dsl v0.0.20 // indirect
github.com/projectdiscovery/fastdialer v0.1.1 // indirect
github.com/projectdiscovery/fasttemplate v0.0.2 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c // indirect
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb // indirect
github.com/projectdiscovery/gologger v1.1.67 // indirect
github.com/projectdiscovery/gostruct v0.0.2 // indirect
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81 // indirect
github.com/projectdiscovery/hmap v0.0.99 // indirect
github.com/projectdiscovery/httpx v1.8.1 // indirect
github.com/projectdiscovery/interactsh v1.2.4 // indirect
github.com/projectdiscovery/freeport v0.0.5 // indirect
github.com/projectdiscovery/gologger v1.1.12 // indirect
github.com/projectdiscovery/gostruct v0.0.1 // indirect
github.com/projectdiscovery/hmap v0.0.45 // indirect
github.com/projectdiscovery/httpx v1.3.4 // indirect
github.com/projectdiscovery/interactsh v1.2.0 // indirect
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 // indirect
github.com/projectdiscovery/mapcidr v1.1.97 // indirect
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5 // indirect
github.com/projectdiscovery/networkpolicy v0.1.34 // indirect
github.com/projectdiscovery/ratelimit v0.0.83 // indirect
github.com/projectdiscovery/rawhttp v0.1.90 // indirect
github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect
github.com/projectdiscovery/mapcidr v1.1.34 // indirect
github.com/projectdiscovery/networkpolicy v0.0.8 // indirect
github.com/projectdiscovery/rawhttp v0.1.18 // indirect
github.com/projectdiscovery/rdap v0.9.1-0.20221108103045-9865884d1917 // indirect
github.com/projectdiscovery/retryabledns v1.0.113 // indirect
github.com/projectdiscovery/retryablehttp-go v1.3.5 // indirect
github.com/projectdiscovery/retryabledns v1.0.62 // indirect
github.com/projectdiscovery/retryablehttp-go v1.0.63 // indirect
github.com/projectdiscovery/sarif v0.0.1 // indirect
github.com/projectdiscovery/tlsx v1.2.2 // indirect
github.com/projectdiscovery/uncover v1.2.0 // indirect
github.com/projectdiscovery/useragent v0.0.107 // indirect
github.com/projectdiscovery/wappalyzergo v0.2.65 // indirect
github.com/projectdiscovery/yamldoc-go v1.0.6 // indirect
github.com/redis/go-redis/v9 v9.11.0 // indirect
github.com/projectdiscovery/tlsx v1.1.4 // indirect
github.com/projectdiscovery/yamldoc-go v1.0.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.7 // indirect
github.com/rs/xid v1.6.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.37.0 // indirect
github.com/sashabaranov/go-openai v1.14.2 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shirou/gopsutil/v3 v3.23.7 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/sijms/go-ora/v2 v2.9.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.9.2 // 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.7.0 // indirect
github.com/tidwall/buntdb v1.3.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/tidwall/buntdb v1.3.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/tim-ywliu/nested-logrus-formatter v1.3.2 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.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/vmihailenco/bufpool v0.1.11 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/vulncheck-oss/go-exploit v1.51.0 // indirect
github.com/weppos/publicsuffix-go v0.50.3-0.20260104170930-90713dec78f2 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yassinebenaid/godump v0.11.1 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db // indirect
github.com/xanzy/go-gitlab v0.84.0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/got v0.34.1 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-emoji v1.0.5 // indirect
github.com/ysmood/leakless v0.8.0 // indirect
github.com/yuin/goldmark v1.5.4 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zcalusic/sysinfo v1.0.2 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // indirect
github.com/zmap/zgrab2 v0.1.8 // indirect
gitlab.com/gitlab-org/api/client-go v0.130.1 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
go.uber.org/zap v1.25.0 // indirect
goftp.io/server/v2 v2.0.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.49.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.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.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.40.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.39.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.36.6 // 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/warnings.v0 v0.1.2 // indirect
gopkg.in/djherbis/times.v1 v1.3.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
mellium.im/sasl v0.3.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
moul.io/http2curl v1.0.0 // indirect
)
+280 -1154
View File
File diff suppressed because it is too large Load Diff
+624
View File
@@ -0,0 +1,624 @@
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="
-157
View File
@@ -1,157 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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)
}
}
-159
View File
@@ -1,159 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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, 0o755); 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, 0o666)
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()
}
-196
View File
@@ -1,196 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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)
}
}
-400
View File
@@ -1,400 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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 i := range matchers {
matched := checkMatcher(&matchers[i], resp, body)
if matchers[i].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)
if e.Type == "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
}
-153
View File
@@ -1,153 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
-106
View File
@@ -1,106 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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"`
}
-92
View File
@@ -1,92 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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)
}
-155
View File
@@ -1,155 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
-------------------------------------------------------------------------------------------------
: :
: SIF - Blazing-fast pentesting suite :
: Blaze - BSD 3-Clause License :
: :
: (c) 2022-2025 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)
}
}
+5 -3
View File
@@ -4,7 +4,7 @@
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
@@ -13,8 +13,10 @@
package format
import (
"fmt"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v2/pkg/output"
)
func FormatLine(event *output.ResultEvent) string {
@@ -27,7 +29,7 @@ func FormatLine(event *output.ResultEvent) string {
}
output += " [" + event.Type + "]"
output += " [" + formatSeverity(event.Info.SeverityHolder.Severity.String()) + "]"
output += " [" + formatSeverity(fmt.Sprintf("%s", event.Info.SeverityHolder.Severity)) + "]"
return output
}
+5 -11
View File
@@ -4,7 +4,7 @@
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
@@ -15,7 +15,6 @@ package templates
import (
"archive/tar"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
@@ -38,12 +37,7 @@ func Install(logger *log.Logger) error {
logger.Infof("nuclei-templates directory not found. Installing...")
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf(archive, ref), http.NoBody)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
resp, err := http.Get(fmt.Sprintf(archive, ref))
if err != nil {
return err
}
@@ -59,7 +53,7 @@ func Install(logger *log.Logger) error {
for {
header, err := data.Next()
if errors.Is(err, io.EOF) {
if errors.Is(io.EOF, err) {
break
}
if err != nil {
@@ -68,7 +62,7 @@ func Install(logger *log.Logger) error {
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(header.Name, 0o755); err != nil {
if err := os.Mkdir(header.Name, 0755); err != nil {
return err
}
case tar.TypeReg:
@@ -83,7 +77,7 @@ func Install(logger *log.Logger) error {
}
}
if err := os.Rename(fmt.Sprintf("nuclei-templates-%s", ref), "nuclei-templates"); err != nil {
if err = os.Rename(fmt.Sprintf("nuclei-templates-%s", ref), "nuclei-templates"); err != nil {
return err
}
-295
View File
@@ -1,295 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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()
}
-168
View File
@@ -1,168 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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++ {
switch {
case i < filled:
bar += progressFilled
case i == filled && current < total:
bar += progressCurrent
default:
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)
}
-121
View File
@@ -1,121 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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)
}
}
}
@@ -1,94 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
-141
View File
@@ -1,141 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
@@ -1,79 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package builtin
import (
"context"
"fmt"
"strings"
"github.com/dropalldatabases/sif/internal/modules"
"github.com/dropalldatabases/sif/internal/scan"
)
type SecurityTrailsModule struct{}
func (m *SecurityTrailsModule) Info() modules.Info {
return modules.Info{
ID: "securitytrails-lookup",
Name: "SecurityTrails Domain Discovery",
Author: "sif",
Severity: "info",
Description: "Queries SecurityTrails API for subdomains and associated domains (requires SECURITYTRAILS_API_KEY)",
Tags: []string{"recon", "osint", "dns", "subdomains"},
}
}
func (m *SecurityTrailsModule) Type() modules.ModuleType {
return modules.TypeScript
}
func (m *SecurityTrailsModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
stResult, err := scan.SecurityTrails(target, opts.Timeout, opts.LogDir)
if err != nil {
return nil, err
}
result := &modules.Result{
ModuleID: m.Info().ID,
Target: target,
Findings: []modules.Finding{},
}
if stResult == nil {
return result, nil
}
finding := modules.Finding{
URL: target,
Severity: "info",
Evidence: fmt.Sprintf("discovered %d subdomains and %d associated domains",
len(stResult.Subdomains), len(stResult.AssociatedDomains)),
Extracted: map[string]string{
"domain": stResult.Domain,
"subdomain_count": fmt.Sprintf("%d", len(stResult.Subdomains)),
"associated_count": fmt.Sprintf("%d", len(stResult.AssociatedDomains)),
},
}
if len(stResult.Subdomains) > 0 {
finding.Extracted["subdomains"] = strings.Join(stResult.Subdomains, ", ")
}
if len(stResult.AssociatedDomains) > 0 {
finding.Extracted["associated_domains"] = strings.Join(stResult.AssociatedDomains, ", ")
}
result.Findings = append(result.Findings, finding)
return result, nil
}
-157
View File
@@ -1,157 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package builtin
import (
"context"
"fmt"
"github.com/dropalldatabases/sif/internal/modules"
"github.com/dropalldatabases/sif/internal/scan"
"strings"
)
type ShodanModule struct{}
func (m *ShodanModule) Info() modules.Info {
return modules.Info{
ID: "shodan-lookup",
Name: "Shodan Host Intelligence",
Author: "sif",
Severity: "info",
Description: "Queries Shodan API for host information, open ports, and vulnerabilities (requires SHODAN_API_KEY)",
Tags: []string{"recon", "osint", "shodan", "vulns"},
}
}
func (m *ShodanModule) Type() modules.ModuleType {
return modules.TypeScript
}
func (m *ShodanModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
// Call existing legacy scan.Shodan function
shodanResult, err := scan.Shodan(target, opts.Timeout, opts.LogDir)
if err != nil {
return nil, err
}
result := &modules.Result{
ModuleID: m.Info().ID,
Target: target,
Findings: []modules.Finding{},
}
// If nothing returned, return empty result
if shodanResult == nil || shodanResult.IP == "" {
return result, nil
}
// Create main finding
evidence := fmt.Sprintf("Shodan data found for %s", shodanResult.IP)
severity := "info"
if len(shodanResult.Vulns) > 0 {
severity = "high"
evidence = fmt.Sprintf("Host %s has %d known vulnerabilities", shodanResult.IP, len(shodanResult.Vulns))
}
finding := modules.Finding{
URL: target,
Severity: severity,
Evidence: evidence,
Extracted: map[string]string{
"ip": shodanResult.IP,
},
}
// Add hostnames
if len(shodanResult.Hostnames) > 0 {
finding.Extracted["hostnames"] = strings.Join(shodanResult.Hostnames, ", ")
}
// Add organization info
if shodanResult.Organization != "" {
finding.Extracted["organization"] = shodanResult.Organization
}
// Add ISP info
if shodanResult.ISP != "" {
finding.Extracted["isp"] = shodanResult.ISP
}
// Add ASN
if shodanResult.ASN != "" {
finding.Extracted["asn"] = shodanResult.ASN
}
// Add location
if shodanResult.Country != "" {
location := shodanResult.Country
if shodanResult.City != "" {
location = shodanResult.City + ", " + shodanResult.Country
}
finding.Extracted["location"] = location
}
// Add OS
if shodanResult.OS != "" {
finding.Extracted["os"] = shodanResult.OS
}
// Add open ports
if len(shodanResult.Ports) > 0 {
portStrs := make([]string, len(shodanResult.Ports))
for i, port := range shodanResult.Ports {
portStrs[i] = fmt.Sprintf("%d", port)
}
finding.Extracted["open_ports"] = strings.Join(portStrs, ", ")
finding.Extracted["port_count"] = fmt.Sprintf("%d", len(shodanResult.Ports))
}
// Add vulnerabilities
if len(shodanResult.Vulns) > 0 {
finding.Extracted["vulnerabilities"] = strings.Join(shodanResult.Vulns, ", ")
finding.Extracted["vuln_count"] = fmt.Sprintf("%d", len(shodanResult.Vulns))
}
// Add last update
if shodanResult.LastUpdate != "" {
finding.Extracted["last_update"] = shodanResult.LastUpdate
}
// Add service count
if len(shodanResult.Services) > 0 {
finding.Extracted["service_count"] = fmt.Sprintf("%d", len(shodanResult.Services))
// Add service details
serviceDetails := make([]string, 0, len(shodanResult.Services))
for _, service := range shodanResult.Services {
detail := fmt.Sprintf("%d/%s", service.Port, service.Protocol)
if service.Product != "" {
detail += " " + service.Product
if service.Version != "" {
detail += " " + service.Version
}
}
serviceDetails = append(serviceDetails, detail)
}
finding.Extracted["services"] = strings.Join(serviceDetails, "; ")
}
result.Findings = append(result.Findings, finding)
return result, nil
}
-56
View File
@@ -1,56 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
-184
View File
@@ -1,184 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
// CVEEntry represents a known vulnerability for a framework version
type CVEEntry struct {
CVE string
AffectedVersions []string // versions affected (use semver ranges in future)
FixedVersion string
Severity string // critical, high, medium, low
Description string
Recommendations []string
}
// knownCVEs contains known vulnerabilities for popular frameworks.
// This database can be extended or loaded from an external source.
var knownCVEs = map[string][]CVEEntry{
"Laravel": {
{
CVE: "CVE-2021-3129",
AffectedVersions: []string{"8.0.0", "8.0.1", "8.0.2", "8.1.0", "8.2.0", "8.3.0", "8.4.0", "8.4.1"},
FixedVersion: "8.4.2",
Severity: "critical",
Description: "Ignition debug mode RCE vulnerability",
Recommendations: []string{"Update to Laravel 8.4.2 or later", "Disable debug mode in production"},
},
{
CVE: "CVE-2021-21263",
AffectedVersions: []string{"8.0.0", "8.1.0", "8.2.0", "8.3.0", "8.4.0"},
FixedVersion: "8.5.0",
Severity: "high",
Description: "SQL injection via request validation",
Recommendations: []string{"Update to Laravel 8.5.0 or later", "Use parameterized queries"},
},
},
"Django": {
{
CVE: "CVE-2023-36053",
AffectedVersions: []string{"3.2.0", "3.2.1", "3.2.2", "4.0.0", "4.1.0"},
FixedVersion: "4.2.3",
Severity: "high",
Description: "Potential ReDoS in EmailValidator and URLValidator",
Recommendations: []string{"Update to Django 4.2.3 or later"},
},
{
CVE: "CVE-2023-31047",
AffectedVersions: []string{"3.2.0", "4.0.0", "4.1.0"},
FixedVersion: "4.1.9",
Severity: "medium",
Description: "File upload validation bypass",
Recommendations: []string{"Update to Django 4.1.9 or later", "Implement additional file validation"},
},
},
"WordPress": {
{
CVE: "CVE-2023-2745",
AffectedVersions: []string{"5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "6.0", "6.1"},
FixedVersion: "6.2",
Severity: "medium",
Description: "Directory traversal vulnerability",
Recommendations: []string{"Update to WordPress 6.2 or later"},
},
},
"Drupal": {
{
CVE: "CVE-2023-44487",
AffectedVersions: []string{"9.0", "9.1", "9.2", "9.3", "9.4", "9.5", "10.0"},
FixedVersion: "10.1.4",
Severity: "high",
Description: "HTTP/2 rapid reset attack (DoS)",
Recommendations: []string{"Update to Drupal 10.1.4 or later", "Configure HTTP/2 rate limiting"},
},
},
"Next.js": {
{
CVE: "CVE-2023-46298",
AffectedVersions: []string{"13.0.0", "13.1.0", "13.2.0", "13.3.0", "13.4.0"},
FixedVersion: "13.5.0",
Severity: "medium",
Description: "Server-side request forgery vulnerability",
Recommendations: []string{"Update to Next.js 13.5.0 or later"},
},
},
"Angular": {
{
CVE: "CVE-2023-26117",
AffectedVersions: []string{"14.0.0", "14.1.0", "14.2.0", "15.0.0"},
FixedVersion: "15.2.0",
Severity: "medium",
Description: "Regular expression denial of service",
Recommendations: []string{"Update to Angular 15.2.0 or later"},
},
},
"Vue.js": {
{
CVE: "CVE-2024-5987",
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
FixedVersion: "2.7.16",
Severity: "medium",
Description: "XSS vulnerability in certain configurations",
Recommendations: []string{"Update to Vue.js 2.7.16 or 3.x"},
},
},
"Express.js": {
{
CVE: "CVE-2024-29041",
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0", "4.3.0", "4.4.0"},
FixedVersion: "4.19.2",
Severity: "medium",
Description: "Open redirect vulnerability",
Recommendations: []string{"Update to Express.js 4.19.2 or later"},
},
},
"Ruby on Rails": {
{
CVE: "CVE-2023-22795",
AffectedVersions: []string{"6.0.0", "6.1.0", "7.0.0"},
FixedVersion: "7.0.4.1",
Severity: "high",
Description: "ReDoS vulnerability in Action Dispatch",
Recommendations: []string{"Update to Rails 7.0.4.1 or later"},
},
},
"Spring": {
{
CVE: "CVE-2022-22965",
AffectedVersions: []string{"5.0.0", "5.1.0", "5.2.0", "5.3.0"},
FixedVersion: "5.3.18",
Severity: "critical",
Description: "Spring4Shell RCE vulnerability",
Recommendations: []string{"Update to Spring 5.3.18 or later", "Disable class binding on user input"},
},
},
"Spring Boot": {
{
CVE: "CVE-2022-22963",
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
FixedVersion: "2.6.6",
Severity: "critical",
Description: "RCE via Spring Cloud Function",
Recommendations: []string{"Update to Spring Boot 2.6.6 or later"},
},
},
"ASP.NET": {
{
CVE: "CVE-2023-36899",
AffectedVersions: []string{"4.0", "4.5", "4.6", "4.7", "4.8"},
FixedVersion: "latest security patches",
Severity: "high",
Description: "Elevation of privilege vulnerability",
Recommendations: []string{"Apply latest security patches", "Ensure proper request validation"},
},
},
"Joomla": {
{
CVE: "CVE-2023-23752",
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0"},
FixedVersion: "4.2.8",
Severity: "critical",
Description: "Improper access check allowing unauthorized access to webservice endpoints",
Recommendations: []string{"Update to Joomla 4.2.8 or later"},
},
},
"Magento": {
{
CVE: "CVE-2022-24086",
AffectedVersions: []string{"2.3.0", "2.3.1", "2.3.2", "2.4.0", "2.4.1", "2.4.2"},
FixedVersion: "2.4.3-p1",
Severity: "critical",
Description: "Improper input validation leading to arbitrary code execution",
Recommendations: []string{"Update to Magento 2.4.3-p1 or later"},
},
},
}
-191
View File
@@ -1,191 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import (
"context"
"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}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
spin.Stop()
return nil, err
}
resp, err := client.Do(req) //nolint:bodyclose // closed via defer below
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 //nolint:nilnil // no detectors registered is not an error
}
// 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 //nolint:nilnil // no framework detected is not an error
}
// 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
}
-520
View File
@@ -1,520 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/scan/frameworks"
// Import detectors to register them via init()
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
)
func TestExtractVersion_Laravel(t *testing.T) {
tests := []struct {
body string
expected string
}{
{"Laravel 8.0.0", "8.0.0"},
{"Laravel v9.52.1", "9.52.1"},
{"Laravel 10.0", "10.0"},
{"no version here", "unknown"},
}
for _, tt := range tests {
result := frameworks.ExtractVersionOptimized(tt.body, "Laravel").Version
if result != tt.expected {
t.Errorf("ExtractVersionOptimized(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestExtractVersion_Django(t *testing.T) {
tests := []struct {
body string
expected string
}{
{"Django 4.2.0", "4.2.0"},
{"Django/3.2.1", "3.2.1"},
{"no version", "unknown"},
}
for _, tt := range tests {
result := frameworks.ExtractVersionOptimized(tt.body, "Django").Version
if result != tt.expected {
t.Errorf("ExtractVersionOptimized(%q, 'Django') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestExtractVersion_NextJS(t *testing.T) {
tests := []struct {
body string
expected string
}{
{"Next.js 13.4.0", "13.4.0"},
{"Next.js/14.0.1", "14.0.1"},
{"no version", "unknown"},
}
for _, tt := range tests {
result := frameworks.ExtractVersionOptimized(tt.body, "Next.js").Version
if result != tt.expected {
t.Errorf("ExtractVersionOptimized(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestDetectFramework_NextJS(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body>
<script id="__NEXT_DATA__" type="application/json">{"props":{}}</script>
<script src="/_next/static/chunks/main.js"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Next.js" {
t.Errorf("expected framework 'Next.js', got '%s'", result.Name)
}
if result.Confidence <= 0 {
t.Error("expected positive confidence")
}
}
func TestDetectFramework_Express(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Powered-By", "Express")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<!DOCTYPE html><html><body>Hello</body></html>`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Express.js" {
t.Errorf("expected framework 'Express.js', got '%s'", result.Name)
}
}
func TestDetectFramework_WordPress(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/wp-content/themes/theme/style.css">
<script src="/wp-includes/js/jquery.js"></script>
</head>
<body></body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "WordPress" {
t.Errorf("expected framework 'WordPress', got '%s'", result.Name)
}
}
func TestDetectFramework_ASPNET(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-AspNet-Version", "4.0.30319")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<body>
<input type="hidden" name="__VIEWSTATE" value="abc123">
<input type="hidden" name="__EVENTVALIDATION" value="xyz789">
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "ASP.NET" {
t.Errorf("expected framework 'ASP.NET', got '%s'", result.Name)
}
}
func TestDetectFramework_NoMatch(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<!DOCTYPE html><html><body>Simple page</body></html>`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// result can be nil or have low confidence for unrecognized frameworks
if result != nil && result.Confidence > 0.6 {
t.Errorf("expected low confidence or nil result for plain HTML, got %s with %.2f", result.Name, result.Confidence)
}
}
func TestFrameworkResult_Fields(t *testing.T) {
result := frameworks.NewFrameworkResult("Laravel", "9.0.0", 0.85, 0.9)
result.WithVulnerabilities([]string{"CVE-2021-3129"}, []string{"Update to latest version"})
if result.Name != "Laravel" {
t.Errorf("expected Name 'Laravel', got '%s'", result.Name)
}
if result.Version != "9.0.0" {
t.Errorf("expected Version '9.0.0', got '%s'", result.Version)
}
if result.Confidence != 0.85 {
t.Errorf("expected Confidence 0.85, got %f", result.Confidence)
}
if result.VersionConfidence != 0.9 {
t.Errorf("expected VersionConfidence 0.9, got %f", result.VersionConfidence)
}
if len(result.CVEs) != 1 {
t.Errorf("expected 1 CVE, got %d", len(result.CVEs))
}
if len(result.Suggestions) != 1 {
t.Errorf("expected 1 suggestion, got %d", len(result.Suggestions))
}
}
func TestExtractVersionWithConfidence(t *testing.T) {
tests := []struct {
name string
body string
framework string
wantVer string
minConf float32
}{
{"Laravel explicit", "Laravel 8.0.0", "Laravel", "8.0.0", 0.8},
{"Angular ng-version", `<html ng-version="14.2.0">`, "Angular", "14.2.0", 0.9},
{"WordPress generator", `<meta name="generator" content="WordPress 6.1.0">`, "WordPress", "6.1.0", 0.9},
{"Vue CDN", "vue@3.2.0/dist", "Vue.js", "3.2.0", 0.7},
{"No version", "Hello World", "Laravel", "unknown", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := frameworks.ExtractVersionOptimized(tt.body, tt.framework)
if result.Version != tt.wantVer {
t.Errorf("ExtractVersionOptimized() version = %q, want %q", result.Version, tt.wantVer)
}
if result.Confidence < tt.minConf {
t.Errorf("ExtractVersionOptimized() confidence = %f, want >= %f", result.Confidence, tt.minConf)
}
})
}
}
func TestDetermineRiskLevel(t *testing.T) {
tests := []struct {
name string
cves []string
expected string
}{
{"no CVEs", []string{}, "low"},
{"critical", []string{"CVE-2021-3129 (critical)"}, "critical"},
{"high", []string{"CVE-2023-22795 (high)"}, "high"},
{"medium", []string{"CVE-2023-46298 (medium)"}, "medium"},
{"mixed - critical wins", []string{"CVE-2023-1 (medium)", "CVE-2021-3129 (critical)"}, "critical"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Test via WithVulnerabilities which uses determineRiskLevel internally
result := frameworks.NewFrameworkResult("Test", "1.0", 0.5, 0.5)
result.WithVulnerabilities(tt.cves, nil)
if result.RiskLevel != tt.expected {
t.Errorf("determineRiskLevel() = %q, want %q", result.RiskLevel, tt.expected)
}
})
}
}
func TestDetectFramework_Vue(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>Vue App</title></head>
<body>
<div id="app" data-v-12345>
<div v-cloak>Loading...</div>
</div>
<script src="https://unpkg.com/vue@3.2.0/dist/vue.global.js"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Vue.js" {
t.Errorf("expected framework 'Vue.js', got '%s'", result.Name)
}
}
func TestDetectFramework_Angular(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html ng-version="15.0.0">
<head><title>Angular App</title></head>
<body>
<app-root _nghost-abc-c123 _ngcontent-abc-c123></app-root>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Angular" {
t.Errorf("expected framework 'Angular', got '%s'", result.Name)
}
}
func TestDetectFramework_React(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>React App</title></head>
<body>
<div id="root" data-reactroot="">Content</div>
<script src="/static/js/react-dom.production.min.js"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "React" {
t.Errorf("expected framework 'React', got '%s'", result.Name)
}
}
func TestDetectFramework_Svelte(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><title>Svelte App</title></head>
<body>
<div id="app" class="__svelte-123">
<span class="svelte-abc123">Content</span>
</div>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Svelte" {
t.Errorf("expected framework 'Svelte', got '%s'", result.Name)
}
}
func TestDetectFramework_Joomla(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head>
<meta name="generator" content="Joomla! - Open Source Content Management">
<script src="/media/jui/js/jquery.js"></script>
</head>
<body>
<div class="Joomla">Content</div>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Joomla" {
t.Errorf("expected framework 'Joomla', got '%s'", result.Name)
}
}
func 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)
}
}
}
-145
View File
@@ -1,145 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
@@ -1,488 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
-192
View File
@@ -1,192 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
@@ -1,220 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
-191
View File
@@ -1,191 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
-46
View File
@@ -1,46 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import "testing"
func FuzzIsValidVersionString(f *testing.F) {
f.Add("1.0")
f.Add("1.0.0")
f.Add("10.2.3.4")
f.Add("999.999.999")
f.Add("")
f.Add("abc")
f.Add("1.")
f.Add(".1")
f.Add("1.2.3.4.5")
f.Add("aaaaaaaaaaaaaaaaaaaaaaaaa")
f.Fuzz(func(t *testing.T, v string) {
// should never panic
isValidVersionString(v)
})
}
func FuzzExtractVersionOptimized(f *testing.F) {
f.Add("<meta name=\"generator\" content=\"WordPress 6.4.2\">", "WordPress")
f.Add("Laravel v10.0.1", "Laravel")
f.Add("<html>nothing</html>", "Django")
f.Add("", "unknown")
f.Add("X-Powered-By: Express/4.18.2", "Express")
f.Fuzz(func(t *testing.T, body string, framework string) {
// should never panic
ExtractVersionOptimized(body, framework)
})
}
-95
View File
@@ -1,95 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 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
}
-221
View File
@@ -1,221 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import (
"regexp"
"unicode"
)
// VersionMatch represents a version detection result with confidence.
type VersionMatch struct {
Version string
Confidence float32
Source string // where the version was found
}
// compiledVersionPattern holds a pre-compiled regex for version extraction
type compiledVersionPattern struct {
re *regexp.Regexp
confidence float32
source string
}
// frameworkVersionPatterns maps framework names to their pre-compiled version patterns.
// Patterns are compiled once at package initialization for optimal performance.
var frameworkVersionPatterns map[string][]compiledVersionPattern
func init() {
// Raw patterns to be compiled
rawPatterns := map[string][]struct {
pattern string
confidence float32
source string
}{
"Laravel": {
{`Laravel\s+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`laravel/framework.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "composer.json"},
},
"Django": {
{`Django[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`django.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "package reference"},
},
"Ruby on Rails": {
{`Rails[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`rails.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "gem reference"},
},
"Express.js": {
{`Express[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"express":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"ASP.NET": {
{`X-AspNet-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.95, "header"},
{`ASP\.NET[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`X-AspNetMvc-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.9, "MVC header"},
},
"ASP.NET Core": {
{`\.NET\s*(\d+\.\d+(?:\.\d+)?)`, 0.8, "dotnet version"},
},
"Spring": {
{`Spring[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`spring-core.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "maven"},
},
"Spring Boot": {
{`spring-boot.*?(\d+\.\d+(?:\.\d+)?)`, 0.9, "maven"},
},
"Flask": {
{`Flask[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`Werkzeug[/\s]+(\d+\.\d+(?:\.\d+)?)`, 0.7, "werkzeug version"},
},
"Next.js": {
{`Next\.js[/\s]+[Vv]?(\d{1,2}\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"next":\s*"[~^]?(\d{1,2}\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"Nuxt.js": {
{`Nuxt[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"nuxt":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"Vue.js": {
{`Vue\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"vue":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
{`vue@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
},
"Angular": {
{`ng-version="(\d+\.\d+(?:\.\d+)?)"`, 0.95, "ng-version attribute"},
{`Angular[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"@angular/core":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"React": {
{`React[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"react":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
{`react@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
},
"Svelte": {
{`Svelte[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"svelte":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"SvelteKit": {
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"WordPress": {
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Drupal": {
{`Drupal[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`<meta name="Generator" content="Drupal (\d+)`, 0.9, "generator meta"},
},
"Joomla": {
{`Joomla[!/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`<meta name="generator" content="Joomla! - Open Source Content Management - Version (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
},
"Magento": {
{`Magento[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Shopify": {
{`Shopify\.theme.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "theme version"},
},
"Symfony": {
{`Symfony[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"FastAPI": {
{`FastAPI[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Gin": {
{`Gin[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Phoenix": {
{`Phoenix[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Ember.js": {
{`Ember[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Backbone.js": {
{`Backbone[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Meteor": {
{`Meteor[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Ghost": {
{`Ghost[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"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 v == "" || 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
}
-63
View File
@@ -1,63 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import "testing"
func FuzzDetectLFIFromResponse(f *testing.F) {
f.Add("root:x:0:0:root:/root:/bin/bash")
f.Add("<html><body>Hello World</body></html>")
f.Add("[boot loader]\ntimeout=30")
f.Add("DOCUMENT_ROOT=/var/www/html")
f.Add("<?php echo 'hello'; ?>")
f.Add("127.0.0.1 localhost")
f.Add("")
f.Add("PD9waHAgZWNobyAnaGVsbG8nOyA/Pg==")
f.Fuzz(func(t *testing.T, body string) {
// should never panic
DetectLFIFromResponse(body)
})
}
func FuzzIsAdminPanel(f *testing.F) {
f.Add("<html>phpMyAdmin</html>", "phpMyAdmin")
f.Add("<html>adminer</html>", "Adminer")
f.Add("<html>pgadmin</html>", "pgAdmin")
f.Add("<html>nothing here</html>", "phpMyAdmin")
f.Add("", "unknown")
f.Add("<html>database query mysql</html>", "generic")
f.Fuzz(func(t *testing.T, body string, panelType string) {
// should never panic
isAdminPanel(body, panelType)
})
}
func FuzzDatabaseErrorPatterns(f *testing.F) {
f.Add("you have an error in your sql syntax")
f.Add("Warning: mysql_fetch_array()")
f.Add("postgresql error at character 42")
f.Add("ORA-12345: some oracle error")
f.Add("sqlite3_prepare_v2 failed")
f.Add("document bson error in mongodb")
f.Add("<html>normal page</html>")
f.Add("")
f.Fuzz(func(t *testing.T, body string) {
// should never panic on any input
for _, pattern := range databaseErrorPatterns {
pattern.pattern.MatchString(body)
}
})
}
-317
View File
@@ -1,317 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"context"
"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())
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, testURL, http.NoBody)
if err != nil {
charmlog.Debugf("Error creating request for %s: %v", testURL, err)
continue
}
resp, err := client.Do(req)
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 //nolint:nilnil // no LFI found is not an error
}
return result, nil
}
// DetectLFIFromResponse checks a response body for LFI evidence
func DetectLFIFromResponse(body string) (bool, string) {
for _, evidence := range lfiEvidencePatterns {
if evidence.pattern.MatchString(body) {
return true, evidence.description
}
}
return false, ""
}
-316
View File
@@ -1,316 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestDetectLFIFromResponse_EtcPasswd(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
expectDesc string
}{
{
name: "root entry",
body: "root:x:0:0:root:/root:/bin/bash",
expectFound: true,
expectDesc: "/etc/passwd content",
},
{
name: "daemon entry",
body: "daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin",
expectFound: true,
expectDesc: "/etc/passwd content",
},
{
name: "nobody entry",
body: "nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin",
expectFound: true,
expectDesc: "/etc/passwd content",
},
{
name: "no evidence",
body: "<html><body>Hello World</body></html>",
expectFound: false,
expectDesc: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, desc := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
if desc != tt.expectDesc {
t.Errorf("DetectLFIFromResponse() desc = %v, want %v", desc, tt.expectDesc)
}
})
}
}
func TestDetectLFIFromResponse_WindowsFiles(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
}{
{
name: "boot.ini boot loader",
body: "[boot loader]\ntimeout=30",
expectFound: true,
},
{
name: "boot.ini operating systems",
body: "[operating systems]\nmulti(0)",
expectFound: true,
},
{
name: "win.ini fonts section",
body: "; for 16-bit app support\n[fonts]",
expectFound: true,
},
{
name: "hosts file",
body: "127.0.0.1 localhost",
expectFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, _ := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
})
}
}
func TestDetectLFIFromResponse_EnvironmentVars(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
}{
{
name: "DOCUMENT_ROOT",
body: "DOCUMENT_ROOT=/var/www/html",
expectFound: true,
},
{
name: "PATH variable",
body: "PATH=/usr/local/bin:/usr/bin:/bin",
expectFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, _ := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
})
}
}
func TestDetectLFIFromResponse_PHPSource(t *testing.T) {
tests := []struct {
name string
body string
expectFound bool
}{
{
name: "PHP opening tag",
body: "<?php echo 'hello'; ?>",
expectFound: true,
},
{
name: "base64 encoded PHP",
body: "PD9waHAgZWNobyAnaGVsbG8nOyA/Pg==",
expectFound: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
found, _ := DetectLFIFromResponse(tt.body)
if found != tt.expectFound {
t.Errorf("DetectLFIFromResponse() found = %v, want %v", found, tt.expectFound)
}
})
}
}
func TestLFIResult_Fields(t *testing.T) {
result := LFIResult{
Vulnerabilities: []LFIVulnerability{
{
URL: "http://example.com/?file=../../../etc/passwd",
Parameter: "file",
Payload: "../../../etc/passwd",
Evidence: "/etc/passwd content",
Severity: "high",
FileIncluded: "/etc/passwd",
},
},
TestedParams: 10,
TestedPayloads: 25,
}
if len(result.Vulnerabilities) != 1 {
t.Errorf("expected 1 vulnerability, got %d", len(result.Vulnerabilities))
}
if result.Vulnerabilities[0].Parameter != "file" {
t.Errorf("expected parameter 'file', got '%s'", result.Vulnerabilities[0].Parameter)
}
if result.Vulnerabilities[0].Severity != "high" {
t.Errorf("expected severity 'high', got '%s'", result.Vulnerabilities[0].Severity)
}
if result.TestedParams != 10 {
t.Errorf("expected 10 tested params, got %d", result.TestedParams)
}
}
func TestLFIVulnerability_Fields(t *testing.T) {
vuln := LFIVulnerability{
URL: "http://example.com/?page=../../../etc/passwd",
Parameter: "page",
Payload: "../../../etc/passwd",
Evidence: "/etc/passwd content",
Severity: "high",
FileIncluded: "/etc/passwd",
}
if vuln.URL != "http://example.com/?page=../../../etc/passwd" {
t.Errorf("unexpected URL: %s", vuln.URL)
}
if vuln.Parameter != "page" {
t.Errorf("expected parameter 'page', got '%s'", vuln.Parameter)
}
if vuln.Payload != "../../../etc/passwd" {
t.Errorf("unexpected payload: %s", vuln.Payload)
}
if vuln.Evidence != "/etc/passwd content" {
t.Errorf("unexpected evidence: %s", vuln.Evidence)
}
if vuln.Severity != "high" {
t.Errorf("expected severity 'high', got '%s'", vuln.Severity)
}
}
func TestLFIPayloads_Exist(t *testing.T) {
if len(lfiPayloads) == 0 {
t.Error("lfiPayloads should not be empty")
}
// check that all payloads have required fields
for i, payload := range lfiPayloads {
if payload.payload == "" {
t.Errorf("payload %d has empty payload", i)
}
if payload.target == "" {
t.Errorf("payload %d has empty target", i)
}
if payload.severity == "" {
t.Errorf("payload %d has empty severity", i)
}
if payload.severity != "critical" && payload.severity != "high" && payload.severity != "medium" && payload.severity != "low" {
t.Errorf("payload %d has invalid severity: %s", i, payload.severity)
}
}
}
func TestCommonLFIParams_Exist(t *testing.T) {
if len(commonLFIParams) == 0 {
t.Error("commonLFIParams should not be empty")
}
expectedParams := []string{"file", "page", "path", "include"}
for _, expected := range expectedParams {
found := false
for _, param := range commonLFIParams {
if param == expected {
found = true
break
}
}
if !found {
t.Errorf("expected common param '%s' not found", expected)
}
}
}
func TestLFIEvidencePatterns_Exist(t *testing.T) {
if len(lfiEvidencePatterns) == 0 {
t.Error("lfiEvidencePatterns should not be empty")
}
// verify patterns compile and match expected content
testCases := []struct {
content string
shouldMatch bool
description string
}{
{"root:x:0:0:root:/root:/bin/bash", true, "etc passwd root"},
{"nobody:x:65534:65534:nobody", true, "etc passwd nobody"},
{"[boot loader]", true, "boot.ini"},
{"[operating systems]", true, "boot.ini"},
{"127.0.0.1 localhost", true, "hosts file"},
{"<html>Hello</html>", false, "normal html"},
}
for _, tc := range testCases {
matched := false
for _, pattern := range lfiEvidencePatterns {
if pattern.pattern.MatchString(tc.content) {
matched = true
break
}
}
if matched != tc.shouldMatch {
t.Errorf("pattern match for %s: got %v, want %v", tc.description, matched, tc.shouldMatch)
}
}
}
func TestLFI_MockServer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
file := r.URL.Query().Get("file")
if file == "../../../../../../../etc/passwd" || file == "/etc/passwd" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("root:x:0:0:root:/root:/bin/bash\nnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin"))
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte("<html><body>Normal page</body></html>"))
}
}))
defer server.Close()
// verify server returns passwd content for LFI payload
resp, err := http.Get(server.URL + "/?file=../../../../../../../etc/passwd")
if err != nil {
t.Fatalf("failed to make request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
}
-91
View File
@@ -1,91 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package scan
import (
"context"
"os"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/nuclei/format"
"github.com/dropalldatabases/sif/internal/nuclei/templates"
sifoutput "github.com/dropalldatabases/sif/internal/output"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
)
func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]output.ResultEvent, error) {
sifoutput.ScanStart("nuclei template scanning")
spin := sifoutput.NewSpinner("Running nuclei templates")
spin.Start()
nucleilog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "nuclei",
}).With("url", url)
_ = templates.Install(nucleilog)
pwd, err := os.Getwd()
if err != nil {
spin.Stop()
return nil, err
}
ctx := context.Background()
ne, err := nuclei.NewNucleiEngineCtx(ctx,
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
Templates: []string{pwd + "/nuclei-templates"},
}),
nuclei.WithConcurrency(nuclei.Concurrency{
TemplateConcurrency: threads,
HostConcurrency: 1,
HeadlessHostConcurrency: 1,
HeadlessTemplateConcurrency: 1,
JavascriptTemplateConcurrency: 1,
TemplatePayloadConcurrency: 25,
ProbeConcurrency: 50,
}),
nuclei.WithNetworkConfig(nuclei.NetworkConfig{
Timeout: int(timeout.Seconds()),
}),
nuclei.DisableUpdateCheck(),
)
if err != nil {
spin.Stop()
return nil, err
}
defer ne.Close()
sanitizedURL := strings.Split(url, "://")[1]
ne.LoadTargets([]string{sanitizedURL}, false)
var results []output.ResultEvent
var mu sync.Mutex
err = ne.ExecuteCallbackWithCtx(ctx, func(event *output.ResultEvent) {
if event.Matched != "" {
nucleilog.Infof("%s", format.FormatLine(event))
mu.Lock()
results = append(results, *event)
mu.Unlock()
}
})
spin.Stop()
sifoutput.ScanComplete("nuclei template scanning", len(results), "found")
return results, err
}
-60
View File
@@ -1,60 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package scan
// Named slice types for scan results.
// These provide better type safety and allow method implementations.
type (
HeaderResults []HeaderResult
DirectoryResults []DirectoryResult
CloudStorageResults []CloudStorageResult
DorkResults []DorkResult
SubdomainTakeoverResults []SubdomainTakeoverResult
)
// ScanResult is the interface that all scan result types implement.
// This enables type-safe handling of heterogeneous scan results.
type ScanResult interface {
// ResultType returns the unique identifier for this result type.
ResultType() string
}
// ResultType implementations for pointer result types.
func (r *ShodanResult) ResultType() string { return "shodan" }
func (r *SQLResult) ResultType() string { return "sql" }
func (r *LFIResult) ResultType() string { return "lfi" }
func (r *CMSResult) ResultType() string { return "cms" }
func (r *SecurityTrailsResult) ResultType() string { return "securitytrails" }
// 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 = (*SecurityTrailsResult)(nil)
_ ScanResult = HeaderResults(nil)
_ ScanResult = DirectoryResults(nil)
_ ScanResult = CloudStorageResults(nil)
_ ScanResult = DorkResults(nil)
_ ScanResult = SubdomainTakeoverResults(nil)
)
-253
View File
@@ -1,253 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package scan
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
const securityTrailsBaseURL = "https://api.securitytrails.com/v1"
// SecurityTrailsResult holds discovered domains from SecurityTrails API
type SecurityTrailsResult struct {
Domain string `json:"domain"`
Subdomains []string `json:"subdomains,omitempty"`
AssociatedDomains []string `json:"associated_domains,omitempty"`
}
// stSubdomainsResponse is the raw response from the subdomains endpoint -
// returns prefix labels, not FQDNs
type stSubdomainsResponse struct {
Subdomains []string `json:"subdomains"`
}
type stAssociatedResponse struct {
Records []stAssociatedRecord `json:"records"`
}
type stAssociatedRecord struct {
Hostname string `json:"hostname"`
}
// SecurityTrails queries the SecurityTrails API for subdomains and associated domains.
// API key should be provided via the SECURITYTRAILS_API_KEY environment variable.
func SecurityTrails(targetURL string, timeout time.Duration, logdir string) (*SecurityTrailsResult, error) {
output.ScanStart("SecurityTrails lookup")
spin := output.NewSpinner("querying SecurityTrails API")
spin.Start()
apiKey := os.Getenv("SECURITYTRAILS_API_KEY")
if apiKey == "" {
spin.Stop()
output.Warn("SECURITYTRAILS_API_KEY environment variable not set, skipping SecurityTrails lookup")
return nil, fmt.Errorf("SECURITYTRAILS_API_KEY environment variable not set")
}
parsedURL, err := url.Parse(targetURL)
if err != nil {
spin.Stop()
return nil, fmt.Errorf("failed to parse URL: %w", err)
}
hostname := parsedURL.Hostname()
client := &http.Client{Timeout: timeout}
result := &SecurityTrailsResult{
Domain: hostname,
}
// fetch subdomains
spin.Update("fetching subdomains for " + hostname)
subs, err := querySTSubdomains(client, hostname, apiKey)
if err != nil {
// non-fatal - still try associated domains
output.Warn("SecurityTrails subdomains failed: %v", err)
} else {
result.Subdomains = subs
}
// fetch associated domains
spin.Update("fetching associated domains for " + hostname)
assoc, err := querySTAssociated(client, hostname, apiKey)
if err != nil {
output.Warn("SecurityTrails associated domains failed: %v", err)
} else {
result.AssociatedDomains = assoc
}
spin.Stop()
if logdir != "" {
sanitizedURL := strings.Split(targetURL, "://")[1]
if err := logger.WriteHeader(sanitizedURL, logdir, "SecurityTrails lookup"); err != nil {
output.Error("error writing log header: %v", err)
}
logSecurityTrailsResults(sanitizedURL, logdir, result)
}
printSecurityTrailsResults(result)
total := len(result.Subdomains) + len(result.AssociatedDomains)
output.ScanComplete("SecurityTrails lookup", total, "domains discovered")
return result, nil
}
// DiscoveredURLs returns all discovered domains as https:// URLs.
// used by the orchestration layer for target expansion.
func (r *SecurityTrailsResult) DiscoveredURLs() []string {
seen := make(map[string]struct{})
var urls []string
for _, sub := range r.Subdomains {
fqdn := sub + "." + r.Domain
if _, ok := seen[fqdn]; !ok {
seen[fqdn] = struct{}{}
urls = append(urls, "https://"+fqdn)
}
}
for _, assoc := range r.AssociatedDomains {
if _, ok := seen[assoc]; !ok {
seen[assoc] = struct{}{}
urls = append(urls, "https://"+assoc)
}
}
return urls
}
func querySTSubdomains(client *http.Client, hostname, apiKey string) ([]string, error) {
reqURL := fmt.Sprintf("%s/domain/%s/subdomains", securityTrailsBaseURL, hostname)
body, err := doSTRequest(client, reqURL, apiKey)
if err != nil {
return nil, err
}
var resp stSubdomainsResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("parse subdomains response: %w", err)
}
return resp.Subdomains, nil
}
func querySTAssociated(client *http.Client, hostname, apiKey string) ([]string, error) {
reqURL := fmt.Sprintf("%s/domain/%s/associated", securityTrailsBaseURL, hostname)
body, err := doSTRequest(client, reqURL, apiKey)
if err != nil {
return nil, err
}
var resp stAssociatedResponse
if err := json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("parse associated response: %w", err)
}
domains := make([]string, 0, len(resp.Records))
for _, rec := range resp.Records {
if rec.Hostname != "" {
domains = append(domains, rec.Hostname)
}
}
return domains, nil
}
// doSTRequest makes an authenticated GET to the SecurityTrails API
func doSTRequest(client *http.Client, reqURL, apiKey string) ([]byte, error) {
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, reqURL, http.NoBody)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("APIKEY", apiKey)
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("SecurityTrails request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
return nil, fmt.Errorf("invalid SecurityTrails API key (status %d)", resp.StatusCode)
}
if resp.StatusCode == http.StatusTooManyRequests {
return nil, fmt.Errorf("SecurityTrails rate limit exceeded")
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))
return nil, fmt.Errorf("SecurityTrails 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("read response: %w", err)
}
return body, nil
}
func printSecurityTrailsResults(result *SecurityTrailsResult) {
output.Info("Domain: %s", output.Highlight.Render(result.Domain))
if len(result.Subdomains) > 0 {
output.Info("Subdomains found: %d", len(result.Subdomains))
for _, sub := range result.Subdomains {
output.Success(" %s.%s", sub, result.Domain)
}
}
if len(result.AssociatedDomains) > 0 {
output.Info("Associated domains found: %d", len(result.AssociatedDomains))
for _, assoc := range result.AssociatedDomains {
output.Success(" %s", assoc)
}
}
}
func logSecurityTrailsResults(sanitizedURL, logdir string, result *SecurityTrailsResult) {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Domain: %s\n", result.Domain))
if len(result.Subdomains) > 0 {
sb.WriteString(fmt.Sprintf("\nSubdomains (%d):\n", len(result.Subdomains)))
for _, sub := range result.Subdomains {
sb.WriteString(fmt.Sprintf(" %s.%s\n", sub, result.Domain))
}
}
if len(result.AssociatedDomains) > 0 {
sb.WriteString(fmt.Sprintf("\nAssociated Domains (%d):\n", len(result.AssociatedDomains)))
for _, assoc := range result.AssociatedDomains {
sb.WriteString(fmt.Sprintf(" %s\n", assoc))
}
}
logger.Write(sanitizedURL, logdir, sb.String())
}
-208
View File
@@ -1,208 +0,0 @@
package scan
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestSecurityTrailsResult_DiscoveredURLs(t *testing.T) {
result := &SecurityTrailsResult{
Domain: "example.com",
Subdomains: []string{"www", "api", "mail"},
AssociatedDomains: []string{"example.org", "example.net"},
}
urls := result.DiscoveredURLs()
if len(urls) != 5 {
t.Errorf("expected 5 URLs, got %d: %v", len(urls), urls)
}
expected := map[string]bool{
"https://www.example.com": false,
"https://api.example.com": false,
"https://mail.example.com": false,
"https://example.org": false,
"https://example.net": false,
}
for _, u := range urls {
if _, ok := expected[u]; !ok {
t.Errorf("unexpected URL: %s", u)
}
expected[u] = true
}
for u, seen := range expected {
if !seen {
t.Errorf("missing expected URL: %s", u)
}
}
}
func TestSecurityTrailsResult_DiscoveredURLs_Dedup(t *testing.T) {
result := &SecurityTrailsResult{
Domain: "example.com",
Subdomains: []string{"www"},
AssociatedDomains: []string{"www.example.com"},
}
urls := result.DiscoveredURLs()
if len(urls) != 1 {
t.Errorf("expected 1 URL (deduped), got %d: %v", len(urls), urls)
}
}
func TestSecurityTrailsResult_DiscoveredURLs_Empty(t *testing.T) {
result := &SecurityTrailsResult{
Domain: "example.com",
}
urls := result.DiscoveredURLs()
if len(urls) != 0 {
t.Errorf("expected 0 URLs, got %d: %v", len(urls), urls)
}
}
func TestDoSTRequest_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("APIKEY") != "test-key" {
w.WriteHeader(http.StatusForbidden)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"test": true}`))
}))
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
body, err := doSTRequest(client, server.URL, "test-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(body) == 0 {
t.Error("expected non-empty body")
}
}
func TestDoSTRequest_Unauthorized(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
_, err := doSTRequest(client, server.URL, "bad-key")
if err == nil {
t.Error("expected error for forbidden response")
}
}
func TestDoSTRequest_RateLimit(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusTooManyRequests)
}))
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
_, err := doSTRequest(client, server.URL, "test-key")
if err == nil {
t.Error("expected error for rate limit response")
}
}
func TestQuerySTSubdomains(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("APIKEY") != "test-key" {
w.WriteHeader(http.StatusForbidden)
return
}
resp := stSubdomainsResponse{
Subdomains: []string{"www", "api", "mail", "dev"},
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
// query the mock server directly via doSTRequest + unmarshal
body, err := doSTRequest(client, server.URL, "test-key")
if err != nil {
t.Fatalf("request failed: %v", err)
}
var resp stSubdomainsResponse
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
if len(resp.Subdomains) != 4 {
t.Errorf("expected 4 subdomains, got %d", len(resp.Subdomains))
}
}
func TestQuerySTAssociated(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("APIKEY") != "test-key" {
w.WriteHeader(http.StatusForbidden)
return
}
resp := stAssociatedResponse{
Records: []stAssociatedRecord{
{Hostname: "related.com"},
{Hostname: "sibling.net"},
{Hostname: ""},
},
}
json.NewEncoder(w).Encode(resp)
}))
defer server.Close()
client := &http.Client{Timeout: 5 * time.Second}
body, err := doSTRequest(client, server.URL, "test-key")
if err != nil {
t.Fatalf("request failed: %v", err)
}
var resp stAssociatedResponse
if err := json.Unmarshal(body, &resp); err != nil {
t.Fatalf("unmarshal failed: %v", err)
}
// should have 3 records total (including empty one)
if len(resp.Records) != 3 {
t.Errorf("expected 3 records, got %d", len(resp.Records))
}
// filter empty hostnames like the real code does
var domains []string
for _, rec := range resp.Records {
if rec.Hostname != "" {
domains = append(domains, rec.Hostname)
}
}
if len(domains) != 2 {
t.Errorf("expected 2 non-empty domains, got %d", len(domains))
}
}
func TestSecurityTrailsResult_ResultType(t *testing.T) {
result := &SecurityTrailsResult{}
if result.ResultType() != "securitytrails" {
t.Errorf("expected ResultType 'securitytrails', got '%s'", result.ResultType())
}
}
func TestSecurityTrailsIntegration(t *testing.T) {
t.Skip("integration test - requires valid SECURITYTRAILS_API_KEY")
_, err := SecurityTrails("https://example.com", 10*time.Second, "")
if err != nil {
t.Logf("SecurityTrails lookup failed: %v", err)
}
}
-338
View File
@@ -1,338 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package scan
import (
"context"
"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
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, checkURL, http.NoBody)
if err != nil {
charmlog.Debugf("Error creating request for %s: %v", checkURL, err)
continue
}
resp, err := client.Do(req)
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 //nolint:nilnil // no SQLi found is not an error
}
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) {
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, checkURL, http.NoBody)
if err != nil {
return
}
resp, err := client.Do(req)
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
}
}
}
-280
View File
@@ -1,280 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestIsAdminPanel_phpMyAdmin(t *testing.T) {
tests := []struct {
name string
body string
expected bool
}{
{"contains phpMyAdmin", "<html><title>phpMyAdmin</title></html>", true},
{"contains pma_", "<script>var pma_token = '123';</script>", true},
{"empty body", "", false},
{"unrelated content", "<html><title>Hello World</title></html>", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isAdminPanel(tt.body, "phpMyAdmin")
if result != tt.expected {
t.Errorf("isAdminPanel(%q, 'phpMyAdmin') = %v, want %v", tt.body, result, tt.expected)
}
})
}
}
func TestIsAdminPanel_Adminer(t *testing.T) {
tests := []struct {
name string
body string
expected bool
}{
{"contains Adminer", "<html><title>Adminer</title></html>", true},
{"lowercase adminer", "<div>adminer version 4.8</div>", true},
{"empty body", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isAdminPanel(tt.body, "Adminer")
if result != tt.expected {
t.Errorf("isAdminPanel(%q, 'Adminer') = %v, want %v", tt.body, result, tt.expected)
}
})
}
}
func TestIsAdminPanel_GenericDatabase(t *testing.T) {
tests := []struct {
name string
body string
expected bool
}{
{"contains database", "<html><title>Database Manager</title></html>", true},
{"contains sql", "<div>SQL Query Interface</div>", true},
{"contains mysql", "<script>mysql_query()</script>", true},
{"contains postgresql", "<div>PostgreSQL Admin</div>", true},
{"empty body", "", false},
{"unrelated content", "<html><title>Blog</title></html>", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isAdminPanel(tt.body, "Database Interface")
if result != tt.expected {
t.Errorf("isAdminPanel(%q, 'Database Interface') = %v, want %v", tt.body, result, tt.expected)
}
})
}
}
func TestSQLResult_Fields(t *testing.T) {
result := SQLResult{
AdminPanels: []SQLAdminPanel{
{
URL: "http://example.com/phpmyadmin/",
Type: "phpMyAdmin",
Status: 200,
},
},
DatabaseErrors: []SQLDatabaseError{
{
URL: "http://example.com/?id=1'",
DatabaseType: "MySQL",
ErrorPattern: "mysql.*error",
},
},
}
if len(result.AdminPanels) != 1 {
t.Errorf("expected 1 admin panel, got %d", len(result.AdminPanels))
}
if result.AdminPanels[0].Type != "phpMyAdmin" {
t.Errorf("expected type 'phpMyAdmin', got '%s'", result.AdminPanels[0].Type)
}
if len(result.DatabaseErrors) != 1 {
t.Errorf("expected 1 database error, got %d", len(result.DatabaseErrors))
}
if result.DatabaseErrors[0].DatabaseType != "MySQL" {
t.Errorf("expected database type 'MySQL', got '%s'", result.DatabaseErrors[0].DatabaseType)
}
}
func TestDatabaseErrorPatterns_MySQL(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"mysql error", "MySQL Error: Something went wrong", true},
{"mysql syntax", "You have an error in your SQL syntax", true},
{"mysql fetch", "Warning: mysql_fetch_array()", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestDatabaseErrorPatterns_PostgreSQL(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"postgresql error", "PostgreSQL Error: connection failed", true},
{"pg_query", "Warning: pg_query(): Query failed", true},
{"unterminated string", "ERROR: unterminated quoted string", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestDatabaseErrorPatterns_SQLServer(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"mssql error", "MSSQL Error: invalid query", true},
{"sql server error", "Microsoft SQL Server Error", true},
{"unclosed quote", "Unclosed quotation mark after the character string", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestDatabaseErrorPatterns_Oracle(t *testing.T) {
testCases := []struct {
name string
body string
expected bool
}{
{"ora error code", "ORA-00942: table or view does not exist", true},
{"oracle error", "Oracle Error: invalid identifier", true},
{"no error", "Welcome to our website", false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found := false
for _, pattern := range databaseErrorPatterns {
if pattern.pattern.MatchString(tc.body) {
found = true
break
}
}
if found != tc.expected {
t.Errorf("pattern match for %q = %v, want %v", tc.body, found, tc.expected)
}
})
}
}
func TestSQLAdminPanelDetection(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/phpmyadmin/":
w.WriteHeader(http.StatusOK)
w.Write([]byte("<html><title>phpMyAdmin</title></html>"))
case "/adminer/":
w.WriteHeader(http.StatusOK)
w.Write([]byte("<html><title>Adminer</title></html>"))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()
// this is a basic test to verify the server mock works
resp, err := http.Get(server.URL + "/phpmyadmin/")
if err != nil {
t.Fatalf("failed to get phpmyadmin: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200 for /phpmyadmin/, got %d", resp.StatusCode)
}
}
func TestSQLDatabaseErrorDetection(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("id") == "1'" {
w.WriteHeader(http.StatusOK)
w.Write([]byte("MySQL Error: You have an error in your SQL syntax"))
} else {
w.WriteHeader(http.StatusOK)
w.Write([]byte("Welcome to our website"))
}
}))
defer server.Close()
// verify server returns mysql error for injection attempt
resp, err := http.Get(server.URL + "/?id=1'")
if err != nil {
t.Fatalf("failed to make request: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
}
+48 -22
View File
@@ -4,39 +4,65 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
// Package styles provides custom styling options for the SIF tool's console output.
// This package re-exports styles from internal/output for backwards compatibility.
// It uses the lipgloss library to create visually appealing and consistent text styles.
package styles
import (
"github.com/charmbracelet/lipgloss"
"github.com/dropalldatabases/sif/internal/output"
)
import "github.com/charmbracelet/lipgloss"
// Re-export styles from output package
var (
Status = output.Status
Highlight = output.Highlight
Box = output.Box
Subheading = output.Subheading
// 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)
)
// 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
// Severity level styles for color-coding vulnerability severities
var (
SeverityLow = output.SeverityLow
SeverityMedium = output.SeverityMedium
SeverityHigh = output.SeverityHigh
SeverityCritical = output.SeverityCritical
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"))
)
-170
View File
@@ -1,170 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
// Package worker provides a generic worker pool for concurrent task processing.
package worker
import "sync"
// Pool manages a pool of workers that process items concurrently.
// It uses channel-based distribution for efficient load balancing.
type Pool[T any, R any] struct {
workers int
fn func(T) R
}
// New creates a new worker pool with the specified number of workers
// and a processing function.
func New[T any, R any](workers int, fn func(T) R) *Pool[T, R] {
if workers < 1 {
workers = 1
}
return &Pool[T, R]{
workers: workers,
fn: fn,
}
}
// Run processes all items concurrently and returns the results.
// Items are distributed via a channel for optimal load balancing.
func (p *Pool[T, R]) Run(items []T) []R {
if len(items) == 0 {
return nil
}
input := make(chan T, len(items))
output := make(chan R, len(items))
var wg sync.WaitGroup
wg.Add(p.workers)
// Start workers
for i := 0; i < p.workers; i++ {
go func() {
defer wg.Done()
for item := range input {
output <- p.fn(item)
}
}()
}
// Feed items to workers
for _, item := range items {
input <- item
}
close(input)
// Wait for all workers to finish, then close output
go func() {
wg.Wait()
close(output)
}()
// Collect results
results := make([]R, 0, len(items))
for r := range output {
results = append(results, r)
}
return results
}
// RunWithFilter processes items concurrently and returns only non-zero results.
// Useful when the processing function may return zero values for filtered items.
func (p *Pool[T, R]) RunWithFilter(items []T, filter func(R) bool) []R {
if len(items) == 0 {
return nil
}
input := make(chan T, len(items))
output := make(chan R, len(items))
var wg sync.WaitGroup
wg.Add(p.workers)
// Start workers
for i := 0; i < p.workers; i++ {
go func() {
defer wg.Done()
for item := range input {
result := p.fn(item)
if filter(result) {
output <- result
}
}
}()
}
// Feed items to workers
for _, item := range items {
input <- item
}
close(input)
// Wait for all workers to finish, then close output
go func() {
wg.Wait()
close(output)
}()
// Collect results
results := make([]R, 0, len(items)/2) // Estimate half will pass filter
for r := range output {
results = append(results, r)
}
return results
}
// ForEach processes items concurrently without collecting results.
// Useful for side-effect operations like logging or writing to external stores.
func (p *Pool[T, R]) ForEach(items []T, callback func(R)) {
if len(items) == 0 {
return
}
input := make(chan T, len(items))
output := make(chan R, len(items))
var wg sync.WaitGroup
wg.Add(p.workers)
// Start workers
for i := 0; i < p.workers; i++ {
go func() {
defer wg.Done()
for item := range input {
output <- p.fn(item)
}
}()
}
// Feed items to workers
for _, item := range items {
input <- item
}
close(input)
// Process results as they come in
var outputWg sync.WaitGroup
outputWg.Add(1)
go func() {
defer outputWg.Done()
for r := range output {
callback(r)
}
}()
wg.Wait()
close(output)
outputWg.Wait()
}
-183
View File
@@ -1,183 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package worker
import (
"sort"
"sync/atomic"
"testing"
)
func TestPoolRun(t *testing.T) {
pool := New(4, func(x int) int {
return x * 2
})
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
results := pool.Run(items)
if len(results) != len(items) {
t.Errorf("Expected %d results, got %d", len(items), len(results))
}
// Sort results since order is not guaranteed
sort.Ints(results)
expected := []int{2, 4, 6, 8, 10, 12, 14, 16, 18, 20}
for i, v := range results {
if v != expected[i] {
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
}
}
}
func TestPoolRunEmpty(t *testing.T) {
pool := New(4, func(x int) int {
return x * 2
})
results := pool.Run(nil)
if results != nil {
t.Errorf("Expected nil for empty input, got %v", results)
}
results = pool.Run([]int{})
if results != nil {
t.Errorf("Expected nil for empty slice, got %v", results)
}
}
func TestPoolRunWithFilter(t *testing.T) {
pool := New(4, func(x int) int {
return x * 2
})
items := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
results := pool.RunWithFilter(items, func(r int) bool {
return r > 10 // Only keep results > 10
})
// Should have 5 results: 12, 14, 16, 18, 20
if len(results) != 5 {
t.Errorf("Expected 5 results, got %d", len(results))
}
sort.Ints(results)
expected := []int{12, 14, 16, 18, 20}
for i, v := range results {
if v != expected[i] {
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
}
}
}
func TestPoolForEach(t *testing.T) {
var sum atomic.Int64
pool := New(4, func(x int) int {
return x * 2
})
items := []int{1, 2, 3, 4, 5}
pool.ForEach(items, func(r int) {
sum.Add(int64(r))
})
// Sum should be 2+4+6+8+10 = 30
if sum.Load() != 30 {
t.Errorf("Expected sum = 30, got %d", sum.Load())
}
}
func TestPoolSingleWorker(t *testing.T) {
pool := New(1, func(x int) int {
return x + 1
})
items := []int{1, 2, 3}
results := pool.Run(items)
if len(results) != 3 {
t.Errorf("Expected 3 results, got %d", len(results))
}
sort.Ints(results)
expected := []int{2, 3, 4}
for i, v := range results {
if v != expected[i] {
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
}
}
}
func TestPoolZeroWorkers(t *testing.T) {
// Zero workers should default to 1
pool := New(0, func(x int) int {
return x
})
if pool.workers != 1 {
t.Errorf("Expected workers = 1, got %d", pool.workers)
}
}
func TestPoolStringProcessing(t *testing.T) {
pool := New(2, func(s string) int {
return len(s)
})
items := []string{"a", "bb", "ccc", "dddd"}
results := pool.Run(items)
sort.Ints(results)
expected := []int{1, 2, 3, 4}
for i, v := range results {
if v != expected[i] {
t.Errorf("Expected results[%d] = %d, got %d", i, expected[i], v)
}
}
}
func TestPoolStructProcessing(t *testing.T) {
type input struct {
a int
b int
}
type output struct {
sum int
prod int
}
pool := New(3, func(in input) output {
return output{sum: in.a + in.b, prod: in.a * in.b}
})
items := []input{{1, 2}, {3, 4}, {5, 6}}
results := pool.Run(items)
if len(results) != 3 {
t.Errorf("Expected 3 results, got %d", len(results))
}
// Verify all expected outputs are present
found := make(map[output]bool)
for _, r := range results {
found[r] = true
}
expectedOutputs := []output{{3, 2}, {7, 12}, {11, 30}}
for _, exp := range expectedOutputs {
if !found[exp] {
t.Errorf("Expected output %v not found in results", exp)
}
}
}
-49
View File
@@ -1,49 +0,0 @@
# 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
-66
View File
@@ -1,66 +0,0 @@
# 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
-41
View File
@@ -1,41 +0,0 @@
# 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
-35
View File
@@ -1,35 +0,0 @@
# 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
-37
View File
@@ -1,37 +0,0 @@
# 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
-50
View File
@@ -1,50 +0,0 @@
# 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
-45
View File
@@ -1,45 +0,0 @@
# 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
-39
View File
@@ -1,39 +0,0 @@
# 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
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -42,14 +42,6 @@ type Settings struct {
CloudStorage bool
SubdomainTakeover bool
Shodan bool
SecurityTrails 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 (
@@ -93,10 +85,6 @@ func Parse() *Settings {
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.SecurityTrails, "securitytrails", false, "Enable SecurityTrails domain discovery (requires SECURITYTRAILS_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",
@@ -111,13 +99,6 @@ 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)
}
+67
View File
@@ -0,0 +1,67 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
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))
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -13,7 +13,6 @@
package scan
import (
"context"
"fmt"
"net/http"
"os"
@@ -21,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 {
@@ -55,7 +54,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
var results []CloudStorageResult
for _, bucket := range potentialBuckets {
isPublic, err := checkS3Bucket(context.TODO(), bucket, client)
isPublic, err := checkS3Bucket(bucket, client)
if err != nil {
cloudlog.Errorf("Error checking S3 bucket %s: %v", bucket, err)
continue
@@ -70,7 +69,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
if isPublic {
cloudlog.Warnf("Public S3 bucket found: %s", styles.Highlight.Render(bucket))
if logdir != "" {
_ = logger.Write(sanitizedURL, logdir, fmt.Sprintf("Public S3 bucket found: %s\n", bucket))
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Public S3 bucket found: %s\n", bucket))
}
} else {
cloudlog.Infof("S3 bucket is not public/found: %s", bucket)
@@ -86,23 +85,22 @@ func extractPotentialBuckets(url string) []string {
parts := strings.Split(url, ".")
var buckets []string
for i, part := range parts {
buckets = append(buckets, part, part+"-s3", "s3-"+part)
buckets = append(buckets, part)
buckets = append(buckets, part+"-s3")
buckets = append(buckets, "s3-"+part)
if i < len(parts)-1 {
domainExtension := part + "-" + parts[i+1]
buckets = append(buckets, domainExtension, parts[i+1]+"-"+part)
buckets = append(buckets, domainExtension)
buckets = append(buckets, parts[i+1]+"-"+part)
}
}
return buckets
}
func checkS3Bucket(ctx context.Context, bucket string, client *http.Client) (bool, error) {
func checkS3Bucket(bucket string, client *http.Client) (bool, error) {
url := fmt.Sprintf("https://%s.s3.amazonaws.com", bucket)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return false, err
}
resp, err := client.Do(req)
resp, err := client.Get(url)
if err != nil {
return false, err
}
+22 -42
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -13,14 +13,16 @@
package scan
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
type CMSResult struct {
@@ -29,77 +31,60 @@ type CMSResult struct {
}
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()
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 {
spin.Stop()
log.Error("Error creating log file: %v", err)
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,
}
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody)
resp, err := client.Get(url)
if err != nil {
spin.Stop()
return nil, err
}
resp, err := client.Do(req)
if err != nil {
spin.Stop()
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
body, err := io.ReadAll(resp.Body)
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")
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") {
spin.Stop()
result := &CMSResult{Name: "Drupal", Version: "Unknown"}
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
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") {
spin.Stop()
result := &CMSResult{Name: "Joomla", Version: "Unknown"}
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
return result, nil
}
spin.Stop()
log.Info("No CMS detected")
log.Complete(0, "detected")
return nil, nil //nolint:nilnil // no CMS found is not an error
cmslog.Info("No CMS detected")
return nil, nil
}
func detectWordPress(url string, client *http.Client, bodyString string) bool {
@@ -125,15 +110,10 @@ func detectWordPress(url string, client *http.Client, bodyString string) bool {
}
for _, file := range wpFiles {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url+file, http.NoBody)
if err != nil {
continue
}
resp, err := client.Do(req)
resp, err := client.Get(url + file)
if err == nil {
found := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound
resp.Body.Close()
if found {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound {
return true
}
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,17 +14,17 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
const (
@@ -40,20 +40,36 @@ type DirectoryResult struct {
}
// 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) {
log := output.Module("DIRLIST")
log.Start()
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.Error("Error creating log file: %v", err)
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
@@ -63,18 +79,14 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
list = directoryURL + bigFile
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, list, http.NoBody)
dirlog.Infof("Starting %s directory listing", size)
resp, err := http.Get(list)
if err != nil {
log.Error("Error creating directory list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Error downloading directory list: %s", err)
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)
@@ -86,13 +98,10 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
Timeout: timeout,
}
progress := output.NewProgress(len(directories), "fuzzing")
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
results := make([]DirectoryResult, 0, 64)
results := []DirectoryResult{}
for thread := 0; thread < threads; thread++ {
go func(thread int) {
defer wg.Done()
@@ -102,45 +111,30 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
continue
}
progress.Increment(directory)
charmlog.Debugf("%s", directory)
dirReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+directory, http.NoBody)
log.Debugf("%s", directory)
resp, err := client.Get(url + "/" + directory)
if err != nil {
charmlog.Debugf("Error creating request for %s: %s", directory, err)
continue
}
resp, err := client.Do(dirReq)
if err != nil {
charmlog.Debugf("Error %s: %s", directory, err)
continue
log.Debugf("Error %s: %s", directory, err)
return
}
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()
// 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))
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()
}
resp.Body.Close()
}
}(thread)
}
wg.Wait()
progress.Done()
log.Complete(len(results), "found")
return results, nil
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,16 +14,16 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strings"
"sync"
"time"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
const (
@@ -34,11 +34,27 @@ const (
)
// 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) {
log := output.Module("DNS")
log.Start()
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
@@ -48,18 +64,14 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
list = dnsURL + dnsBigFile
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, list, http.NoBody)
dnslog.Infof("Starting %s DNS listing", size)
resp, err := http.Get(list)
if err != nil {
log.Error("Error creating request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Error downloading DNS list: %s", err)
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)
@@ -71,7 +83,7 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
log.Error("Error creating log file: %v", err)
log.Errorf("Error creating log file: %v", err)
return nil, err
}
}
@@ -80,13 +92,10 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
Timeout: timeout,
}
progress := output.NewProgress(len(dns), "enumerating")
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
urls := make([]string, 0, 64)
urls := []string{}
for thread := 0; thread < threads; thread++ {
go func(thread int) {
defer wg.Done()
@@ -96,64 +105,39 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
continue
}
progress.Increment(domain)
charmlog.Debugf("Looking up: %s", domain)
// Check HTTP
httpReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "http://"+domain+"."+sanitizedURL, http.NoBody)
log.Debugf("Looking up: %s", domain)
resp, err := client.Get("http://" + domain + "." + sanitizedURL)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
continue
}
resp, err := client.Do(httpReq)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
log.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
mu.Unlock()
resp.Body.Close()
progress.Pause()
log.Success("found: %s.%s [http]", output.Highlight.Render(domain), sanitizedURL)
progress.Resume()
dnslog.Infof("%s %s.%s", styles.Status.Render("[http]"), styles.Highlight.Render(domain), sanitizedURL)
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[http] %s.%s\n", domain, sanitizedURL))
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))
}
}
// Check HTTPS
httpsReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "https://"+domain+"."+sanitizedURL, http.NoBody)
resp, err = client.Get("https://" + domain + "." + sanitizedURL)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
continue
}
resp, err = client.Do(httpsReq)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
log.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
mu.Unlock()
resp.Body.Close()
progress.Pause()
log.Success("found: %s.%s [https]", output.Highlight.Render(domain), sanitizedURL)
progress.Resume()
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))
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
}
+18 -27
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -17,17 +17,17 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
googlesearch "github.com/rocketlaunchr/google-search"
)
@@ -55,32 +55,27 @@ type DorkResult struct {
// - []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()
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 {
spin.Stop()
output.Error("Error creating log file: %v", err)
log.Errorf("Error creating log file: %v", err)
return nil, err
}
}
ctx := context.TODO()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, dorkURL+dorkFile, http.NoBody)
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 {
spin.Stop()
output.Error("Error creating dork list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
output.Error("Error downloading dork list: %s", err)
log.Errorf("Error downloading dork list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -106,17 +101,15 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
continue
}
results, err := googlesearch.Search(context.TODO(), fmt.Sprintf("%s %s", dork, sanitizedURL))
results, err := googlesearch.Search(nil, fmt.Sprintf("%s %s", dork, sanitizedURL))
if err != nil {
log.Debugf("error searching for dork %s: %v", dork, err)
dorklog.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()
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, strconv.Itoa(len(results))+" dork results found for dork ["+dork+"]\n")
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s dork results found for dork [%s]\n", strconv.Itoa(len(results)), dork))
}
result := DorkResult{
@@ -130,8 +123,6 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
}(thread)
}
wg.Wait()
spin.Stop()
output.ScanComplete("URL dorking", len(dorkResults), "found")
return dorkResults, nil
}
+22 -37
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,16 +14,17 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
const (
@@ -32,32 +33,27 @@ const (
)
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()
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 {
spin.Stop()
log.Error("Error creating log file: %v", err)
log.Errorf("Error creating log file: %v", err)
return nil, err
}
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, gitURL+gitFile, http.NoBody)
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 {
spin.Stop()
log.Error("Error creating git list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
log.Error("Error downloading git list: %s", err)
log.Errorf("Error downloading git list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -86,36 +82,25 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
continue
}
charmlog.Debugf("%s", repourl)
gitReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+repourl, http.NoBody)
log.Debugf("%s", repourl)
resp, err := client.Get(url + "/" + repourl)
if err != nil {
charmlog.Debugf("Error creating request for %s: %s", repourl, err)
continue
}
resp, err := client.Do(gitReq)
if err != nil {
charmlog.Debugf("Error %s: %s", repourl, err)
continue
log.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()
// 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, strconv.Itoa(resp.StatusCode)+" git found at ["+repourl+"]\n")
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s git found at [%s]\n", strconv.Itoa(resp.StatusCode), repourl))
}
foundUrls = append(foundUrls, resp.Request.URL.String())
}
resp.Body.Close()
}
}(thread)
}
wg.Wait()
spin.Stop()
log.Complete(len(foundUrls), "found")
return foundUrls, nil
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc, xyzeva, :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -13,13 +13,15 @@
package scan
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
type HeaderResult struct {
@@ -28,27 +30,26 @@ type HeaderResult struct {
}
func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult, error) {
log := output.Module("HEADERS")
log.Start()
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.Error("Error creating log file: %v", err)
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,
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
resp, err := client.Get(url)
if err != nil {
return nil, err
}
@@ -59,13 +60,12 @@ func Headers(url string, timeout time.Duration, logdir string) ([]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)
headerlog.Infof("%s: %s", styles.Highlight.Render(name), value)
if logdir != "" {
logger.Write(sanitizedURL, logdir, name+": "+value+"\n")
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s: %s\n", name, value))
}
}
}
log.Complete(len(results), "found")
return results, nil
}

Some files were not shown because too many files have changed in this diff Show More