mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-27 00:43:59 -07:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1feb0648b3 | |||
| f6f9a2bbf7 | |||
| a9fde8c695 | |||
| 94d375fc3b | |||
| d16391186f | |||
| c6741e0f16 | |||
| 6a8ce9c07b | |||
| 355df83b59 | |||
| 570592c317 | |||
| 761e570d59 | |||
| 28a01f0f83 | |||
| 7788550722 | |||
| 40482a8409 | |||
| 3ed9ea4b6f | |||
| 612bb61d00 | |||
| ec53d15a9f | |||
| 064484ff4d | |||
| e2a26c19c6 | |||
| 95523bc344 | |||
| d0e986736d | |||
| 72f59532cf | |||
| 24b573a368 | |||
| 4d680074b8 | |||
| f0aa1895e9 | |||
| 6d903a4752 | |||
| 9c241cf185 | |||
| 26ccbea888 | |||
| bca4831df1 | |||
| 291846dde5 | |||
| 21c1d1c8a5 | |||
| 68075b6901 | |||
| 1bbcefa685 | |||
| aa22e6965a | |||
| 33c1c421c3 | |||
| 27c76e350c | |||
| 2e89a94a25 | |||
| 6f88625997 | |||
| 4cc48597a5 | |||
| 82a36886fa | |||
| 33e8668456 | |||
| fc3f11fb61 | |||
| 28acb16a46 | |||
| d62919523a | |||
| 33961a5c35 | |||
| 8078978a44 | |||
| 6ec0b60e5a | |||
| 22168611e4 | |||
| 4813146afc | |||
| 57b1bd7113 | |||
| ab731d0562 | |||
| ef0408ee8d | |||
| 0383a7bcd2 | |||
| 136ddbddba | |||
| a5f42ddfa6 | |||
| 1237f3f09e | |||
| 546ab091da | |||
| 5166b8d8e6 | |||
| c3a755f934 | |||
| 5050900f29 | |||
| 320fc3d4e7 | |||
| 839c0a779c | |||
| 306f9a864d | |||
| dbe79c495e | |||
| 9401aa669e | |||
| b4e78114d7 | |||
| 65ce36e963 | |||
| d0bdcf1690 | |||
| dd0276893b | |||
| cb194406a7 | |||
| 8823fa76b7 | |||
| ade9860250 | |||
| 912f6e8e0e | |||
| 1d2bc64dbc | |||
| 094f1e7806 | |||
| 9f8045be22 | |||
| 83ac92a4b8 | |||
| 7f0e4cd128 | |||
| 29d94e5352 | |||
| 05fa35d945 | |||
| ce3075ad91 | |||
| 661480a56d | |||
| 76e8893ee2 | |||
| 1231ca3179 | |||
| eb33321102 | |||
| 133224c348 | |||
| 4c650e23e3 | |||
| 75e953cda7 | |||
| f7ef71e835 | |||
| 5e10c1857b | |||
| 3c070a621d | |||
| 94b99ade5a | |||
| 9326465a46 | |||
| 50c9933812 | |||
| af0167859a | |||
| 7efd62c804 | |||
| 1a1ff446d8 | |||
| 648fa8d2c8 | |||
| 8918be4797 | |||
| 4fc0df5a01 | |||
| ece5b2b0b0 | |||
| 79f6b95eaf | |||
| fe2ab240a4 | |||
| 4c6cebf4de | |||
| c7a244ed2f | |||
| 4d3b87a2cb | |||
| bf802a7c0b | |||
| c6143f7f39 | |||
| fbcf96d557 | |||
| cecaa8aa3b | |||
| 571711ff6b | |||
| e86a917364 | |||
| 09314edf5c | |||
| 9863a252d8 | |||
| 84b0b81996 | |||
| 7123e392c9 | |||
| 383e645d85 | |||
| 30bf148768 | |||
| 3d04a61b27 | |||
| 9bd1d8cd14 | |||
| 68fed81eee | |||
| cf72dfff0e | |||
| a469463c19 | |||
| c527668c60 | |||
| 4917eaf7e7 | |||
| 4152e74ade | |||
| 3e0cbbc5dd | |||
| abe8bac165 | |||
| d6c52d3dd8 | |||
| 439e829c1b | |||
| d5067d08b2 | |||
| 98c987bfbb | |||
| 237dfde4d1 | |||
| c69bbe1232 | |||
| d52bcfc736 | |||
| fcffe18ba5 | |||
| 7749b50d25 | |||
| 93783d8bd3 | |||
| fef7806ac2 | |||
| 422245fe7f | |||
| 24a9f4411f | |||
| 22936a3281 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"projectName": "sif",
|
||||
"projectOwner": "lunchcat",
|
||||
"projectOwner": "vmfunc",
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: automatic rebase
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
name: check for large files
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: check for large files
|
||||
run: |
|
||||
large_files=$(find . -path ./.git -prune -o -type f -size +5M -print)
|
||||
|
||||
@@ -22,18 +22,18 @@ jobs:
|
||||
security-events: write
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
- name: initialize codeql
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: go
|
||||
- name: build
|
||||
run: go build ./...
|
||||
- name: perform codeql analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:go"
|
||||
|
||||
@@ -16,9 +16,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: dependency review
|
||||
uses: actions/dependency-review-action@v4
|
||||
uses: actions/dependency-review-action@v5
|
||||
continue-on-error: ${{ github.event_name == 'push' }}
|
||||
- name: check dependency review outcome
|
||||
if: github.event_name == 'push' && failure()
|
||||
|
||||
+11
-10
@@ -17,25 +17,25 @@ jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: latest
|
||||
version: v2.11.4
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: ["1.23", "1.24"]
|
||||
go-version: ["1.25"]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: build
|
||||
@@ -43,8 +43,9 @@ jobs:
|
||||
- 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
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
files: ./coverage.out
|
||||
fail_ci_if_error: false
|
||||
- name: run integration tests
|
||||
run: go test -tags=integration -race ./internal/scan/...
|
||||
|
||||
@@ -15,11 +15,11 @@ jobs:
|
||||
govulncheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
- name: install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
|
||||
- name: run govulncheck
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
check-headers:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: check license headers
|
||||
run: |
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
echo ': █▀ █ █▀▀ · Blazing-fast pentesting suite :'
|
||||
echo ': ▄█ █ █▀ · BSD 3-Clause License :'
|
||||
echo ': :'
|
||||
echo ': (c) 2022-2025 vmfunc, xyzeva, :'
|
||||
echo ': (c) 2022-2026 vmfunc, xyzeva, :'
|
||||
echo ': lunchcat alumni & contributors :'
|
||||
echo ': :'
|
||||
echo '·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·'
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
name: profanity check
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
- name: Profanity check step
|
||||
uses: tailaiw/mind-your-language-action@v1.0.3
|
||||
env:
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: runner / markdownlint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: markdownlint
|
||||
uses: reviewdog/action-markdownlint@v0.26.2
|
||||
with:
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
name: runner / misspell
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: misspell
|
||||
uses: reviewdog/action-misspell@v1.26.0
|
||||
uses: reviewdog/action-misspell@v1.27.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-review
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: pr bot
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
permissions:
|
||||
@@ -9,23 +9,22 @@ permissions:
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@v6
|
||||
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
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
@@ -69,7 +68,7 @@ jobs:
|
||||
needs: [label, size]
|
||||
if: always()
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
- uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
@@ -19,30 +19,33 @@ jobs:
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
|
||||
- name: extract version
|
||||
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
run: |
|
||||
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
# single source of truth so the cross-compile steps can't drift
|
||||
echo "LDFLAGS=-s -w -X main.version=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
|
||||
|
||||
- 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 -ldflags="${{ env.LDFLAGS }}" -o sif-windows-amd64.exe ./cmd/sif
|
||||
GOOS=windows GOARCH=386 go build -ldflags="${{ env.LDFLAGS }}" -o sif-windows-386.exe ./cmd/sif
|
||||
|
||||
- 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 -ldflags="${{ env.LDFLAGS }}" -o sif-macos-amd64 ./cmd/sif
|
||||
GOOS=darwin GOARCH=arm64 go build -ldflags="${{ env.LDFLAGS }}" -o sif-macos-arm64 ./cmd/sif
|
||||
|
||||
- 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 -ldflags="${{ env.LDFLAGS }}" -o sif-linux-amd64 ./cmd/sif
|
||||
GOOS=linux GOARCH=386 go build -ldflags="${{ env.LDFLAGS }}" -o sif-linux-386 ./cmd/sif
|
||||
GOOS=linux GOARCH=arm64 go build -ldflags="${{ env.LDFLAGS }}" -o sif-linux-arm64 ./cmd/sif
|
||||
|
||||
- name: package releases with modules
|
||||
run: |
|
||||
@@ -117,7 +120,7 @@ jobs:
|
||||
|
||||
- name: generate changelog
|
||||
id: changelog
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
@@ -146,7 +149,7 @@ jobs:
|
||||
return log || 'initial release';
|
||||
|
||||
- name: create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
with:
|
||||
name: sif v${{ env.VERSION }}
|
||||
body: |
|
||||
|
||||
@@ -18,6 +18,6 @@ jobs:
|
||||
update-report-card:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: update go report card
|
||||
uses: creekorful/goreportcard-action@v1.0
|
||||
|
||||
@@ -14,11 +14,11 @@ jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: set up go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version: "1.25"
|
||||
- name: build sif
|
||||
run: make
|
||||
- name: run sif with features
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
security-events: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: run scorecard
|
||||
@@ -25,6 +25,6 @@ jobs:
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
- name: upload sarif results
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: runner / shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: shellcheck
|
||||
uses: reviewdog/action-shellcheck@v1.32.0
|
||||
with:
|
||||
|
||||
@@ -15,9 +15,9 @@ jobs:
|
||||
name: runner / yamllint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- name: yamllint
|
||||
uses: reviewdog/action-yamllint@v1.19.0
|
||||
uses: reviewdog/action-yamllint@v1.21.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-review
|
||||
|
||||
+73
-43
@@ -1,10 +1,16 @@
|
||||
---
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
issues-exit-code: 1
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- errcheck # check error returns
|
||||
- govet # suspicious constructs
|
||||
- staticcheck # advanced static analysis
|
||||
- staticcheck # advanced static analysis (absorbs gosimple in v2)
|
||||
- unused # unused code
|
||||
- gosimple # simplifications
|
||||
- ineffassign # useless assignments
|
||||
- misspell # spelling mistakes
|
||||
- gocritic # opinionated lints
|
||||
@@ -18,49 +24,73 @@ linters:
|
||||
- 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
|
||||
settings:
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
# too many structs to reorder, risks breaking serialization
|
||||
- fieldalignment
|
||||
- shadow # common Go pattern, too noisy
|
||||
- unusedwrite # false positives on test data structs
|
||||
errcheck:
|
||||
check-blank: false
|
||||
exclude-functions:
|
||||
# log writes are best-effort
|
||||
- github.com/dropalldatabases/sif/internal/logger.Write
|
||||
# Close on io.Closer is idiomatic best-effort
|
||||
- (io.Closer).Close
|
||||
- (*os.File).Close
|
||||
- (*net/http.Response).Body.Close
|
||||
# fmt.Fprint* returns are rarely actionable
|
||||
- fmt.Fprint
|
||||
- fmt.Fprintf
|
||||
- fmt.Fprintln
|
||||
staticcheck:
|
||||
# QF1003/QF1012 are v2 quickfix suggestions, not bugs.
|
||||
# ST1000/ST1003 were the stylecheck linter in v1
|
||||
# (not previously enabled); skipping to match prior parity.
|
||||
checks:
|
||||
- all
|
||||
- -QF1003
|
||||
- -QF1012
|
||||
- -ST1000
|
||||
- -ST1003
|
||||
revive:
|
||||
rules:
|
||||
# stuttering names (scan.ScanResult) need breaking API changes
|
||||
- name: exported
|
||||
disabled: true
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- style
|
||||
- performance
|
||||
disabled-checks:
|
||||
- commentedOutCode # too opinionated for a project with TODOs
|
||||
- paramTypeCombine # style-only, not worth churn
|
||||
- unnamedResult # style-only
|
||||
- unnecessaryDefer # common pattern in tests
|
||||
# inverting conditions in scan logic hurts readability
|
||||
- nestingReduce
|
||||
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
|
||||
- G305 # tar extraction is traversal-guarded (HasPrefix on the
|
||||
# cleaned target); gosec flags filepath.Join regardless
|
||||
|
||||
run:
|
||||
timeout: 5m
|
||||
issues-exit-code: 1
|
||||
exclusions:
|
||||
rules:
|
||||
# test files get some slack
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- noctx
|
||||
- gosec # fake credentials in secret-scanner fixtures are not real keys
|
||||
- bodyclose # synthetic *http.Response fixtures carry no socket to close
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 50
|
||||
max-same-issues: 50
|
||||
exclude-rules:
|
||||
# test files get some slack
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- errcheck
|
||||
- noctx
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ When opening an issue, please use the search tool and make sure that the issue h
|
||||
|
||||
### Development
|
||||
|
||||
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.
|
||||
To develop sif, you'll need version 1.25 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.
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ RM ?= rm
|
||||
GOFLAGS ?=
|
||||
PREFIX ?= /usr/local
|
||||
BINDIR ?= bin
|
||||
MANDIR ?= share/man/man1
|
||||
|
||||
# stamp local builds with the nearest v* tag (or short sha), matching the
|
||||
# release ci. --match keeps the automated-release-* tags out of the version.
|
||||
VERSION ?= $(shell git describe --tags --match 'v*' --always --dirty 2>/dev/null | sed 's/^v//')
|
||||
GO_LDFLAGS = -X main.version=$(VERSION)
|
||||
|
||||
define COPYRIGHT_ASCII
|
||||
╭────────────────────────────────────────────────────────────╮
|
||||
@@ -32,8 +38,7 @@ define SUPPORT_MESSAGE
|
||||
│ │
|
||||
│ 🌟 Enjoying sif? Please consider: │
|
||||
│ │
|
||||
│ • Starring our repo: https://github.com/lunchcat/sif │
|
||||
│ • Supporting the devs: https://lunchcat.dev │
|
||||
│ • Starring our repo: https://github.com/vmfunc/sif │
|
||||
│ │
|
||||
│ Your support helps us continue improving sif! │
|
||||
│ │
|
||||
@@ -56,7 +61,7 @@ sif: check_go_version
|
||||
@echo "📁 Current directory: $$(pwd)"
|
||||
@echo "🔧 Go flags: $(GOFLAGS)"
|
||||
@echo "📦 Building package: ./cmd/sif"
|
||||
$(GO) build -v $(GOFLAGS) ./cmd/sif
|
||||
$(GO) build -v $(GOFLAGS) -ldflags "$(GO_LDFLAGS)" ./cmd/sif
|
||||
@echo "📊 Build info:"
|
||||
@$(GO) version -m sif
|
||||
@echo "✅ sif built successfully! 🚀"
|
||||
@@ -76,6 +81,9 @@ install: check_go_version
|
||||
fi
|
||||
@mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR))
|
||||
@cp -f sif $(DESTDIR)$(PREFIX)/$(BINDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo cp -f sif $(DESTDIR)$(PREFIX)/$(BINDIR))
|
||||
@echo "📖 Installing man page..."
|
||||
@mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR))
|
||||
@cp -f man/sif.1 $(DESTDIR)$(PREFIX)/$(MANDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo cp -f man/sif.1 $(DESTDIR)$(PREFIX)/$(MANDIR))
|
||||
@echo "✅ sif installed successfully! 🎊"
|
||||
|
||||
uninstall:
|
||||
@@ -86,6 +94,7 @@ uninstall:
|
||||
exit 1; \
|
||||
fi
|
||||
@$(RM) $(DESTDIR)$(PREFIX)/$(BINDIR)/sif || (echo "🔒 Permission denied. Trying with sudo..." && sudo $(RM) $(DESTDIR)$(PREFIX)/$(BINDIR)/sif)
|
||||
@$(RM) $(DESTDIR)$(PREFIX)/$(MANDIR)/sif.1 || (echo "🔒 Permission denied. Trying with sudo..." && sudo $(RM) $(DESTDIR)$(PREFIX)/$(MANDIR)/sif.1)
|
||||
@echo "✅ sif uninstalled successfully!"
|
||||
|
||||
.PHONY: all check_go_version sif clean install uninstall
|
||||
@@ -11,22 +11,36 @@
|
||||
[](https://search.nixos.org/packages?query=sif)
|
||||
[](https://github.com/vmfunc/homebrew-sif)
|
||||
[](https://cloudsmith.io/~sif/repos/deb/packages/)
|
||||
[](https://discord.gg/sifcli)
|
||||
[](https://discord.gg/Yksy9J2BvE)
|
||||
|
||||
**[install](#install) · [usage](#usage) · [modules](#modules) · [docs](docs/) · [contribute](#contribute)**
|
||||
|
||||
*fast, concurrent recon to exploitation in one binary. every scanner shares one connection-pooled http client.*
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## what is sif?
|
||||
|
||||
sif is a modular pentesting toolkit written in go. it's designed to be fast, concurrent, and extensible. run multiple scan types against targets with a single command.
|
||||
sif is a recon and exploitation scanner that runs the whole chain in one binary: subdomain enum, port scan, crawler, nuclei, framework/cve detection, js secret extraction, web-vuln probes (cors/xss/redirect), cloud and takeover checks. 25+ scan types, one command.
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -all
|
||||
sif -u https://example.com -dnslist -ports -crawl -js -framework -nuclei
|
||||
```
|
||||
|
||||
nuclei and colly are compiled in as libraries rather than shelled out to (there's no `exec.Command` in the tree), so it's a single static binary with no runtime dependencies and nothing to wire together.
|
||||
|
||||
every scanner runs through one shared http client and a work-stealing worker pool. `-proxy`, `-H`, `-cookie` and `-rate-limit` apply to the whole run at once, connections get pooled and reused across the scan (a single-host run reuses one connection for ~50 requests instead of dialing 50 times), and a slow host doesn't hold the rest up. that shared client is the practical reason to use it over piping a stack of separate tools together. port scanning is `connect()`-based, so rustscan and nmap are still faster at raw port scans.
|
||||
|
||||
it reads targets from stdin and prints findings one per line under `-silent`, so it composes:
|
||||
|
||||
```bash
|
||||
subfinder -d example.com | sif -silent -crawl -js -nuclei | notify
|
||||
```
|
||||
|
||||
`-diff` turns a re-scan into a monitor that only reports what changed, `-notify` posts to slack/discord/telegram/webhook, and runs export to sarif and markdown.
|
||||
|
||||
## install
|
||||
|
||||
### homebrew (macos)
|
||||
@@ -49,14 +63,14 @@ paru -S sif
|
||||
### nix
|
||||
|
||||
```bash
|
||||
# nixpkgs (declarative — add to configuration.nix or home-manager)
|
||||
# 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
|
||||
nix run nixpkgs#sif -- -u https://example.com -headers -sh -framework
|
||||
```
|
||||
|
||||
the repo also ships a flake if you want to build from source:
|
||||
@@ -84,7 +98,7 @@ cd sif
|
||||
make
|
||||
```
|
||||
|
||||
requires go 1.23+
|
||||
requires go 1.25+
|
||||
|
||||
### aur (manual install)
|
||||
|
||||
@@ -122,15 +136,32 @@ makepkg -si
|
||||
# sql recon + lfi scanning
|
||||
./sif -u https://example.com -sql -lfi
|
||||
|
||||
# web vuln probes (cors, open redirect, reflected xss)
|
||||
./sif -u https://example.com -cors -redirect -xss
|
||||
|
||||
# framework detection (with cve lookup)
|
||||
./sif -u https://example.com -framework
|
||||
|
||||
# everything
|
||||
./sif -u https://example.com -all
|
||||
# a broad sweep
|
||||
./sif -u https://example.com -dirlist small -dnslist small -ports common -headers -sh -cms -framework -git -whois
|
||||
```
|
||||
|
||||
run `./sif -h` for all options.
|
||||
|
||||
## commands
|
||||
|
||||
a couple of subcommands run without scanning:
|
||||
|
||||
```bash
|
||||
# print the version (release builds are stamped; local builds use git describe)
|
||||
./sif version
|
||||
|
||||
# show the latest release notes (also -pn)
|
||||
./sif patchnote
|
||||
```
|
||||
|
||||
the first time you run a new release, sif prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to turn that off.
|
||||
|
||||
## modules
|
||||
|
||||
sif has a modular architecture. modules are defined in yaml and can be extended by users.
|
||||
@@ -140,13 +171,22 @@ sif has a modular architecture. modules are defined in yaml and can be extended
|
||||
| flag | description |
|
||||
|------|-------------|
|
||||
| `-dirlist` | directory and file fuzzing (small/medium/large) |
|
||||
| `-mc` | dirlist: match these status codes (comma list, e.g. 200,301) |
|
||||
| `-fc` | dirlist: filter out these status codes (comma list) |
|
||||
| `-fs` | dirlist: filter out responses of these body sizes (comma list) |
|
||||
| `-fw` | dirlist: filter out responses with these word counts (comma list) |
|
||||
| `-fr` | dirlist: filter out responses whose body matches this regex |
|
||||
| `-ac` | dirlist: auto-calibrate the soft-404 wildcard baseline |
|
||||
| `-w` | dirlist: custom wordlist (local file or url; overrides `-dirlist` size) |
|
||||
| `-e` | dirlist: extensions appended to each word (comma list, e.g. php,bak,env) |
|
||||
| `-dnslist` | subdomain enumeration (small/medium/large) |
|
||||
| `-ports` | port scanning (common/full) |
|
||||
| `-nuclei` | vulnerability scanning with nuclei templates |
|
||||
| `-dork` | automated google dorking |
|
||||
| `-js` | javascript analysis |
|
||||
| `-js` | javascript analysis + secret and endpoint extraction |
|
||||
| `-c3` | cloud storage misconfiguration |
|
||||
| `-headers` | http header analysis |
|
||||
| `-sh` | security header analysis (missing/weak headers) |
|
||||
| `-st` | subdomain takeover detection |
|
||||
| `-cms` | cms detection |
|
||||
| `-whois` | whois lookups |
|
||||
@@ -155,7 +195,109 @@ sif has a modular architecture. modules are defined in yaml and can be extended
|
||||
| `-securitytrails` | domain discovery + target expansion (requires SECURITYTRAILS_API_KEY) |
|
||||
| `-sql` | sql recon |
|
||||
| `-lfi` | local file inclusion |
|
||||
| `-jwt` | jwt discovery + offline weakness analysis (alg:none, weak hmac, exp, sensitive claims) |
|
||||
| `-openapi` | openapi/swagger spec exposure probe (enumerates paths + unauth endpoints) |
|
||||
| `-favicon` | favicon hash fingerprinting (shodan-style mmh3, tech match + pivot query) |
|
||||
| `-cors` | cors misconfiguration probe |
|
||||
| `-redirect` | open redirect probe |
|
||||
| `-xss` | reflected xss probe |
|
||||
| `-framework` | framework detection with cve lookup |
|
||||
| `-crawl` | web crawler (spider same-host links/scripts/forms) |
|
||||
| `-crawl-depth` | max crawl recursion depth (default 2) |
|
||||
| `-passive` | passive subdomain/url discovery (zero traffic to target) |
|
||||
| `-probe` | live-host probe (status, title, server, redirect chain) |
|
||||
|
||||
### http options
|
||||
|
||||
these apply to every outbound request across all scanners:
|
||||
|
||||
| flag | description |
|
||||
|------|-------------|
|
||||
| `-proxy` | route all traffic through a proxy (http/https/socks5 url) |
|
||||
| `-H`, `--header` | custom header to send (repeatable or comma-separated, `"Key: Value"`) |
|
||||
| `-cookie` | cookie header to send with every request |
|
||||
| `-rate-limit` | max requests per second (0 = unlimited, default 0) |
|
||||
|
||||
```bash
|
||||
# scan through a socks5 proxy with a custom header, cookie and 20 req/s cap
|
||||
./sif -u https://example.com -headers -proxy socks5://127.0.0.1:1080 -H "Authorization: Bearer tok" -cookie "session=abc" -rate-limit 20
|
||||
```
|
||||
|
||||
a scanner that sets a header explicitly (e.g. an api key) always wins over the global default.
|
||||
|
||||
### report export
|
||||
|
||||
write the run's findings out to a file for ci/cd or triage:
|
||||
|
||||
| flag | description |
|
||||
|------|-------------|
|
||||
| `-sarif` | write a sarif 2.1.0 report to this file |
|
||||
| `-markdown`, `-md` | write a markdown report to this file |
|
||||
| `-silent` | plain output: chrome to stderr, one finding per line to stdout (for pipelines) |
|
||||
| `-diff` | surface only findings added/removed since the last snapshot of each target |
|
||||
| `-store` | snapshot directory for `-diff` (default: log dir, else `<user-config>/sif/state`) |
|
||||
|
||||
```bash
|
||||
# scan and emit both a sarif and markdown report
|
||||
./sif -u https://example.com -headers -cors -sarif out.sarif -md out.md
|
||||
```
|
||||
|
||||
sarif output is ingestable by github code scanning; markdown is a readable per-target summary.
|
||||
|
||||
### diff mode
|
||||
|
||||
`-diff` turns a re-scan into a monitor: sif snapshots each target's normalized findings to a json file, and on the next run reports only the delta (`+ new` / `- gone`) against that snapshot, then overwrites it. the first run for a target has no baseline, so everything is `+ new`. snapshots land in `-store` (one sanitized file per target); when unset they reuse the log dir, falling back to `<user-config>/sif/state`.
|
||||
|
||||
```bash
|
||||
# baseline run, then re-scan later and see only what moved
|
||||
./sif -u https://example.com -sh -cors -diff
|
||||
./sif -u https://example.com -sh -cors -diff
|
||||
```
|
||||
|
||||
the snapshot is always rewritten, so each run diffs against the previous one. the delta is chrome (it rides the normal output sink / stderr under `-silent`), not the findings stream.
|
||||
|
||||
### notify
|
||||
|
||||
ship findings to a chat/webhook sink so a continuous-recon run alerts on what it turns up. every provider is a single POST through the shared http client, so the global proxy/rate-limit/header config applies.
|
||||
|
||||
| flag | description |
|
||||
|------|-------------|
|
||||
| `-notify` | ship findings to every configured provider after the scan |
|
||||
| `-notify-severity` | minimum severity to send (`info`/`low`/`medium`/`high`/`critical`, default `medium`) |
|
||||
| `-notify-config` | path to a notify-compatible yaml config (overrides env vars) |
|
||||
|
||||
providers are configured env-first; a yaml file (`-notify-config`) overrides per-field. the yaml keys match [projectdiscovery/notify](https://github.com/projectdiscovery/notify) so an existing config ports over:
|
||||
|
||||
| env var | yaml key | provider |
|
||||
|---------|----------|----------|
|
||||
| `SLACK_WEBHOOK_URL` | `slack_webhook_url` | slack incoming webhook |
|
||||
| `DISCORD_WEBHOOK_URL` | `discord_webhook_url` | discord webhook |
|
||||
| `TELEGRAM_BOT_TOKEN` | `telegram_api_key` | telegram bot api (needs chat id too) |
|
||||
| `TELEGRAM_CHAT_ID` | `telegram_chat_id` | telegram destination chat |
|
||||
| `NOTIFY_WEBHOOK_URL` | `webhook_url` | generic json webhook (structured findings) |
|
||||
|
||||
```bash
|
||||
# alert slack on medium+ findings discovered during a scan
|
||||
export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
|
||||
./sif -u https://example.com -cors -xss -notify -notify-severity medium
|
||||
```
|
||||
|
||||
a provider with no destination is skipped; with nothing configured, `-notify` is a silent no-op. slack/discord/telegram receive a fixed-width finding block; the generic webhook receives structured json (`{count, findings[]}`).
|
||||
|
||||
### pipe mode
|
||||
|
||||
sif reads targets from stdin and accepts naked hosts, so it drops into a unix pipeline. `-silent` routes all banner/spinner/log chrome to stderr and prints one normalized finding per line (`[severity] target module title`) to stdout:
|
||||
|
||||
```bash
|
||||
# subfinder feeds hosts, sif probes them, notify ships the findings
|
||||
subfinder -d example.com | sif -silent -probe | notify
|
||||
```
|
||||
|
||||
| flag | description |
|
||||
|------|-------------|
|
||||
| stdin | a piped target stream (one host/url per line) is read alongside `-u`/`-f` |
|
||||
|
||||
scheme-less hosts default to `https://`; an explicit `http://`/`https://` is kept; any other scheme (`ftp://`, ...) is rejected.
|
||||
|
||||
### yaml modules
|
||||
|
||||
@@ -223,7 +365,7 @@ contributions welcome. see [contributing.md](CONTRIBUTING.md) for guidelines.
|
||||
gofmt -w .
|
||||
|
||||
# lint
|
||||
golangci-lint run
|
||||
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 run
|
||||
|
||||
# test
|
||||
go test ./...
|
||||
@@ -243,13 +385,13 @@ 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="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/vmfunc/sif/commits?author=vmfunc" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://projectdiscovery.io"><img src="https://avatars.githubusercontent.com/u/50994705?v=4?s=100" width="100px;" alt="ProjectDiscovery"/><br /><sub><b>ProjectDiscovery</b></sub></a><br /><a href="#platform-projectdiscovery" title="Packaging/porting to new platform">📦</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/macdoos"><img src="https://avatars.githubusercontent.com/u/127897805?v=4?s=100" width="100px;" alt="macdoos"/><br /><sub><b>macdoos</b></sub></a><br /><a href="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="https://github.com/vmfunc/sif/commits?author=macdoos" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://epitech.eu"><img src="https://avatars.githubusercontent.com/u/75166283?v=4?s=100" width="100px;" alt="Matthieu Witrowiez"/><br /><sub><b>Matthieu Witrowiez</b></sub></a><br /><a href="#ideas-D3adPlays" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tessa-u-k"><img src="https://avatars.githubusercontent.com/u/109355732?v=4?s=100" width="100px;" alt="tessa "/><br /><sub><b>tessa </b></sub></a><br /><a href="#infra-tessa-u-k" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#question-tessa-u-k" title="Answering Questions">💬</a> <a href="#userTesting-tessa-u-k" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xyzeva"><img src="https://avatars.githubusercontent.com/u/133499694?v=4?s=100" width="100px;" alt="Eva"/><br /><sub><b>Eva</b></sub></a><br /><a href="#blog-xyzeva" title="Blogposts">📝</a> <a href="#content-xyzeva" title="Content">🖋</a> <a href="#research-xyzeva" title="Research">🔬</a> <a href="#security-xyzeva" title="Security">🛡️</a> <a href="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>
|
||||
<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/vmfunc/sif/commits?author=xyzeva" title="Tests">⚠️</a> <a href="https://github.com/vmfunc/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/vmfunc/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>
|
||||
|
||||
+30
-1
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -13,15 +13,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif"
|
||||
"github.com/dropalldatabases/sif/internal/config"
|
||||
"github.com/dropalldatabases/sif/internal/patchnotes"
|
||||
ver "github.com/dropalldatabases/sif/internal/version"
|
||||
|
||||
// Register framework detectors
|
||||
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
|
||||
)
|
||||
|
||||
// version is stamped at release time via -ldflags "-X main.version=...";
|
||||
// ver.Resolve falls back to the build info or "dev" for other builds.
|
||||
var version = "dev"
|
||||
|
||||
func main() {
|
||||
version = ver.Resolve(version)
|
||||
sif.Version = version
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "patchnote", "patchnotes", "-pn", "--patchnotes":
|
||||
patchnotes.Print("")
|
||||
return
|
||||
case "version", "-version", "--version":
|
||||
fmt.Printf("sif %s\n", version)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
settings := config.Parse()
|
||||
|
||||
app, err := sif.New(settings)
|
||||
@@ -29,6 +52,12 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// patchnotes print to stdout; skip them in api/silent mode so the only thing
|
||||
// on stdout is the machine-readable result stream.
|
||||
if !settings.ApiMode && !settings.Silent {
|
||||
patchnotes.ShowOnce(version)
|
||||
}
|
||||
|
||||
err = app.Run()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
+16
-5
@@ -4,7 +4,7 @@ setting up a development environment for sif.
|
||||
|
||||
## prerequisites
|
||||
|
||||
- go 1.23 or later
|
||||
- go 1.25 or later
|
||||
- git
|
||||
- make
|
||||
|
||||
@@ -28,8 +28,7 @@ sif/
|
||||
│ ├── logger/ # logging utilities
|
||||
│ ├── modules/ # module system
|
||||
│ ├── scan/ # built-in scans
|
||||
│ ├── styles/ # terminal styling
|
||||
│ └── worker/ # worker pool
|
||||
│ └── styles/ # terminal styling
|
||||
├── modules/ # built-in yaml modules
|
||||
│ ├── http/ # http-based modules
|
||||
│ ├── info/ # information gathering
|
||||
@@ -61,8 +60,11 @@ gofmt -w .
|
||||
|
||||
### lint
|
||||
|
||||
ci pins golangci-lint v2.11.4 (`.github/workflows/go.yml`); other versions
|
||||
report spurious issues against the v2 config, so pin it locally too:
|
||||
|
||||
```bash
|
||||
golangci-lint run
|
||||
go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4 run
|
||||
```
|
||||
|
||||
### test
|
||||
@@ -138,6 +140,15 @@ the module system is in `internal/modules/`:
|
||||
go test ./internal/...
|
||||
```
|
||||
|
||||
### integration tests
|
||||
|
||||
run the scanners against a local testbed that plants the artifacts each one
|
||||
should find (network-free, behind a build tag):
|
||||
|
||||
```bash
|
||||
go test -tags=integration ./internal/scan/...
|
||||
```
|
||||
|
||||
### functional test
|
||||
|
||||
```bash
|
||||
@@ -156,7 +167,7 @@ go test ./internal/...
|
||||
1. fork the repository
|
||||
2. create a feature branch
|
||||
3. make changes
|
||||
4. run `gofmt -w .` and `golangci-lint run`
|
||||
4. run `gofmt -w .` and `golangci-lint run` (pinned version, see [lint](#lint))
|
||||
5. submit pr
|
||||
|
||||
### commit messages
|
||||
|
||||
@@ -36,7 +36,7 @@ download `sif-windows-amd64.exe` from releases and add to your PATH.
|
||||
|
||||
## from source
|
||||
|
||||
requires go 1.23+
|
||||
requires go 1.25+
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dropalldatabases/sif.git
|
||||
|
||||
+25
-1
@@ -115,6 +115,18 @@ http:
|
||||
|
||||
each payload creates a separate request for each path.
|
||||
|
||||
#### attack
|
||||
|
||||
how paths and payloads combine into requests.
|
||||
|
||||
```yaml
|
||||
http:
|
||||
attack: pitchfork
|
||||
```
|
||||
|
||||
- `clusterbomb` (default) - every path is tried with every payload
|
||||
- `pitchfork` - path and payload are paired by index, stopping at the shorter list
|
||||
|
||||
#### headers
|
||||
|
||||
custom headers to send.
|
||||
@@ -199,6 +211,18 @@ matchers:
|
||||
condition: or
|
||||
```
|
||||
|
||||
### size matcher
|
||||
|
||||
match the response body length in bytes (measured after the 5 MB response cap, so larger sizes never match).
|
||||
|
||||
```yaml
|
||||
matchers:
|
||||
- type: size
|
||||
size:
|
||||
- 0
|
||||
- 1337
|
||||
```
|
||||
|
||||
### combining matchers
|
||||
|
||||
multiple matchers are combined with AND logic by default.
|
||||
@@ -238,7 +262,7 @@ extractors:
|
||||
|
||||
### kv extractor
|
||||
|
||||
extract key-value pairs.
|
||||
record every response header as a key-value pair, namespaced by `name`.
|
||||
|
||||
```yaml
|
||||
extractors:
|
||||
|
||||
+15
-4
@@ -98,16 +98,27 @@ analyzes javascript files for security issues.
|
||||
|
||||
## http headers (-headers)
|
||||
|
||||
analyzes security headers.
|
||||
dumps the target's response headers.
|
||||
|
||||
## security headers (-sh)
|
||||
|
||||
flags missing or weak security headers and headers that leak server internals.
|
||||
|
||||
### checks
|
||||
|
||||
- strict-transport-security (https only)
|
||||
- content-security-policy
|
||||
- x-frame-options
|
||||
- x-content-type-options
|
||||
- strict-transport-security
|
||||
- x-xss-protection
|
||||
- x-content-type-options (expects nosniff)
|
||||
- referrer-policy
|
||||
- permissions-policy
|
||||
- cross-origin-opener-policy
|
||||
|
||||
### flagged as disclosure
|
||||
|
||||
- server
|
||||
- x-powered-by
|
||||
- x-aspnet-version / x-aspnetmvc-version
|
||||
|
||||
## cms detection (-cms)
|
||||
|
||||
|
||||
+303
-3
@@ -21,6 +21,23 @@ read targets from a file (one url per line):
|
||||
./sif -f targets.txt
|
||||
```
|
||||
|
||||
### stdin (pipe mode)
|
||||
|
||||
when stdin is a pipe, sif reads one target per line from it, alongside any `-u`/`-f` targets. this lets sif slot into a unix pipeline:
|
||||
|
||||
```bash
|
||||
subfinder -d example.com | sif -silent -probe | notify
|
||||
```
|
||||
|
||||
### naked hosts
|
||||
|
||||
targets without a scheme default to `https://`; an explicit `http://`/`https://` is kept as given. any other scheme (`ftp://`, `file://`, ...) is rejected:
|
||||
|
||||
```bash
|
||||
./sif -u example.com # scanned as https://example.com
|
||||
echo example.com | sif -probe # same, over stdin
|
||||
```
|
||||
|
||||
## scan options
|
||||
|
||||
### directory fuzzing
|
||||
@@ -33,6 +50,42 @@ sizes: `small`, `medium`, `large`
|
||||
./sif -u https://example.com -dirlist medium
|
||||
```
|
||||
|
||||
#### response filters
|
||||
|
||||
modern apps serve a catch-all 200 for unknown routes, so a naive scan reports
|
||||
every path. these ffuf-style filters cut the noise (a filter always wins over a
|
||||
match):
|
||||
|
||||
- `-mc <codes>` - match only these status codes (comma list, e.g. `200,301`)
|
||||
- `-fc <codes>` - filter out these status codes
|
||||
- `-fs <sizes>` - filter out responses of these body sizes
|
||||
- `-fw <counts>` - filter out responses with these word counts
|
||||
- `-fr <regex>` - filter out responses whose body matches this regex
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -dirlist medium -mc 200,301 -fs 1234
|
||||
```
|
||||
|
||||
#### wildcard calibration
|
||||
|
||||
`-ac` probes a few paths that cannot exist, learns the soft-404 baseline
|
||||
(status + size + words), and auto-drops any response matching it - so SPA
|
||||
catch-all 200s stop flooding the output:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -dirlist medium -ac
|
||||
```
|
||||
|
||||
#### custom wordlists and extensions
|
||||
|
||||
`-w <path|url>` overrides the size switch with your own list (local file or
|
||||
remote url); `-e <exts>` appends each extension to every word, keeping the bare
|
||||
word too:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -w /path/to/words.txt -e php,bak,env
|
||||
```
|
||||
|
||||
### subdomain enumeration
|
||||
|
||||
`-dnslist <size>` - enumerate subdomains
|
||||
@@ -79,7 +132,7 @@ scopes: `common` (top ports), `full` (all ports)
|
||||
|
||||
### javascript analysis
|
||||
|
||||
`-js` - analyze javascript files
|
||||
`-js` - analyze javascript files + secret and endpoint extraction
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -js
|
||||
@@ -95,12 +148,20 @@ scopes: `common` (top ports), `full` (all ports)
|
||||
|
||||
### http headers
|
||||
|
||||
`-headers` - analyze security headers
|
||||
`-headers` - dump the target's response headers
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -headers
|
||||
```
|
||||
|
||||
### security headers
|
||||
|
||||
`-sh` - flag missing/weak security headers (hsts, csp, x-frame-options, ...) and headers that leak server internals
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -sh
|
||||
```
|
||||
|
||||
### cloud storage
|
||||
|
||||
`-c3` - check for cloud storage misconfigurations
|
||||
@@ -146,6 +207,56 @@ export SHODAN_API_KEY=your-api-key
|
||||
./sif -u https://example.com -lfi
|
||||
```
|
||||
|
||||
### cors probe
|
||||
|
||||
`-cors` - probe for cors misconfigurations (reflected/permissive origins)
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -cors
|
||||
```
|
||||
|
||||
### open redirect probe
|
||||
|
||||
`-redirect` - probe redirect-prone params for open redirects
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com/login?next=home -redirect
|
||||
```
|
||||
|
||||
### reflected xss probe
|
||||
|
||||
`-xss` - inject a canary into params and report unescaped reflections
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com/search?q=test -xss
|
||||
```
|
||||
|
||||
### jwt analysis
|
||||
|
||||
`-jwt` - fetch the target once, harvest jwts from response headers, cookies and body, then analyze each one entirely offline
|
||||
|
||||
flags alg:none, the rs256->hs256 confusion surface, missing/expired exp, plaintext sensitive claims, and cracks a small bundled weak-hmac wordlist. no token is ever sent off-box.
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -jwt
|
||||
```
|
||||
|
||||
### openapi/swagger exposure
|
||||
|
||||
`-openapi` - probe the conventional spec paths (`/swagger.json`, `/openapi.json`, `/v3/api-docs`, ...), parse the first hit (json or yaml) and enumerate every path+method, flagging operations with no security requirement
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -openapi
|
||||
```
|
||||
|
||||
### favicon fingerprint
|
||||
|
||||
`-favicon` - fetch `/favicon.ico` (or the declared `<link rel=icon>`), compute the shodan-style mmh3 hash, match it against a bundled tech map and print the `http.favicon.hash:<n>` pivot query
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -favicon
|
||||
```
|
||||
|
||||
### framework detection
|
||||
|
||||
`-framework` - detect web frameworks with version and cve lookup
|
||||
@@ -154,6 +265,34 @@ export SHODAN_API_KEY=your-api-key
|
||||
./sif -u https://example.com -framework
|
||||
```
|
||||
|
||||
### web crawler
|
||||
|
||||
`-crawl` - spider the target, following same-host links, scripts and forms
|
||||
|
||||
`-crawl-depth` - max recursion depth (default 2). respects robots.txt and stays on the target host.
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -crawl -crawl-depth 3
|
||||
```
|
||||
|
||||
### passive discovery
|
||||
|
||||
`-passive` - gather subdomains from certificate transparency (crt.sh, certspotter) and historical urls from the wayback machine
|
||||
|
||||
keyless and zero traffic to the target itself - all lookups hit third-party feeds.
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -passive
|
||||
```
|
||||
|
||||
### live-host probe
|
||||
|
||||
`-probe` - check whether the target is alive and report its final status, page title, server header, content-length and the redirect chain it walked
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -probe
|
||||
```
|
||||
|
||||
### whois lookup
|
||||
|
||||
`-whois` - perform whois lookups
|
||||
@@ -217,7 +356,7 @@ http request timeout (default: 10s):
|
||||
|
||||
### --threads
|
||||
|
||||
number of concurrent threads (default: 10):
|
||||
number of concurrent threads (default: 10). values below 1 are clamped to 1:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com --threads 20
|
||||
@@ -239,6 +378,142 @@ enable debug logging:
|
||||
./sif -u https://example.com -d
|
||||
```
|
||||
|
||||
## http options
|
||||
|
||||
these apply to every outbound request across all scanners (proxy, custom headers, cookie and rate limiting share one client). a scanner that sets a header explicitly still wins over the global default.
|
||||
|
||||
### -proxy
|
||||
|
||||
route all traffic through a proxy. supports http, https and socks5 urls:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -proxy socks5://127.0.0.1:1080
|
||||
```
|
||||
|
||||
### -H, --header
|
||||
|
||||
add a custom header to every request. repeatable or comma-separated, `"Key: Value"`:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -H "Authorization: Bearer tok" -H "X-Env: staging"
|
||||
```
|
||||
|
||||
### -cookie
|
||||
|
||||
cookie header to send with every request:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -cookie "session=abc; theme=dark"
|
||||
```
|
||||
|
||||
### -rate-limit
|
||||
|
||||
cap outbound requests per second (0 = unlimited, default 0):
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -rate-limit 20
|
||||
```
|
||||
|
||||
## output options
|
||||
|
||||
write the collected findings out to a file after the scan. both formats can be requested in the same run.
|
||||
|
||||
### -sarif
|
||||
|
||||
write a sarif 2.1.0 report (one run, tool `sif`, one result per finding). ingestable by github code scanning and other sarif consumers:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -headers -cors -sarif out.sarif
|
||||
```
|
||||
|
||||
### -md, --markdown
|
||||
|
||||
write a readable markdown report grouped by target, then by module:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -headers -cors -md report.md
|
||||
```
|
||||
|
||||
### -silent
|
||||
|
||||
plain output for pipelines: all banner/spinner/log chrome goes to stderr and stdout carries one normalized finding per line, formatted `[severity] target module title`. implies non-interactive (no spinners), so a downstream consumer sees nothing but findings:
|
||||
|
||||
```bash
|
||||
subfinder -d example.com | sif -silent -probe -sh | notify
|
||||
```
|
||||
|
||||
### -diff
|
||||
|
||||
turn a re-scan into a monitor. sif snapshots each target's normalized findings to a json file under the store dir; on the next run it loads that snapshot, diffs the current findings against it by finding key, and prints only the delta (`+ new` for findings that appeared, `- gone` for findings that vanished). it always rewrites the snapshot afterwards, so each run compares against the previous one.
|
||||
|
||||
the first run for a target has no snapshot, so every finding shows as `+ new`. when nothing changed, sif notes that and writes a fresh snapshot anyway.
|
||||
|
||||
```bash
|
||||
# baseline, then re-scan and see only what moved
|
||||
./sif -u https://example.com -sh -cors -diff
|
||||
./sif -u https://example.com -sh -cors -diff
|
||||
```
|
||||
|
||||
the delta is chrome, not the findings stream: under `-silent` it rides stderr with the rest of the chrome, leaving stdout for the full findings.
|
||||
|
||||
### -store
|
||||
|
||||
snapshot directory for `-diff`. precedence when unset: the `-log` dir if one is given, else `<user-config>/sif/state` (`$XDG_CONFIG_HOME/sif/state` on linux, `~/Library/Application Support/sif/state` on macos). one sanitized file per target, created at `0750`, written `0600`.
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -sh -diff -store ./snapshots
|
||||
```
|
||||
|
||||
|
||||
## notify options
|
||||
|
||||
ship findings to a chat/webhook sink after the scan. every provider is a single POST through the shared http client, so the global proxy/rate-limit/header config applies. with nothing configured, `-notify` is a silent no-op.
|
||||
|
||||
### -notify
|
||||
|
||||
enable delivery to every configured provider:
|
||||
|
||||
```bash
|
||||
export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
|
||||
./sif -u https://example.com -cors -xss -notify
|
||||
```
|
||||
|
||||
### -notify-severity
|
||||
|
||||
minimum severity to send: `info`, `low`, `medium`, `high` or `critical` (default `medium`). findings below the floor are dropped, so info-level recon noise doesn't flood a channel. an unrecognized value falls back to `medium`:
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -cors -notify -notify-severity high
|
||||
```
|
||||
|
||||
### -notify-config
|
||||
|
||||
path to a yaml config that overrides the env vars per-field. the keys match [projectdiscovery/notify](https://github.com/projectdiscovery/notify) so an existing config ports over:
|
||||
|
||||
```yaml
|
||||
slack_webhook_url: https://hooks.slack.com/services/...
|
||||
discord_webhook_url: https://discord.com/api/webhooks/...
|
||||
telegram_api_key: 123456:abcdef
|
||||
telegram_chat_id: "987654"
|
||||
webhook_url: https://example.internal/sif-findings
|
||||
```
|
||||
|
||||
```bash
|
||||
./sif -u https://example.com -cors -notify -notify-config notify.yaml
|
||||
```
|
||||
|
||||
providers are resolved env-first, then overlaid by the yaml file:
|
||||
|
||||
| env var | yaml key | provider |
|
||||
|---------|----------|----------|
|
||||
| `SLACK_WEBHOOK_URL` | `slack_webhook_url` | slack incoming webhook |
|
||||
| `DISCORD_WEBHOOK_URL` | `discord_webhook_url` | discord webhook |
|
||||
| `TELEGRAM_BOT_TOKEN` | `telegram_api_key` | telegram bot api (needs chat id too) |
|
||||
| `TELEGRAM_CHAT_ID` | `telegram_chat_id` | telegram destination chat |
|
||||
| `NOTIFY_WEBHOOK_URL` | `webhook_url` | generic json webhook (structured findings) |
|
||||
|
||||
slack/discord/telegram receive a fixed-width finding block; the generic webhook receives structured json (`{count, findings[]}`) for downstream automation.
|
||||
|
||||
## api options
|
||||
|
||||
### -api
|
||||
@@ -251,6 +526,28 @@ enable api mode for json output:
|
||||
|
||||
output is a json object with scan results.
|
||||
|
||||
## commands
|
||||
|
||||
these run without scanning a target.
|
||||
|
||||
### version
|
||||
|
||||
print the sif version. release builds are stamped via ldflags, local `make` builds derive it from `git describe`, and `go install`ed builds read it from the module build info:
|
||||
|
||||
```bash
|
||||
./sif version
|
||||
```
|
||||
|
||||
### patchnote
|
||||
|
||||
show the latest release's notes, fetched from github (also `-pn`):
|
||||
|
||||
```bash
|
||||
./sif patchnote
|
||||
```
|
||||
|
||||
the first time you run a new release sif also prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to disable that.
|
||||
|
||||
## examples
|
||||
|
||||
### quick recon
|
||||
@@ -273,6 +570,9 @@ output is a json object with scan results.
|
||||
-git \
|
||||
-sql \
|
||||
-lfi \
|
||||
-cors \
|
||||
-redirect \
|
||||
-xss \
|
||||
-am
|
||||
```
|
||||
|
||||
|
||||
Generated
+3
-3
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1767364772,
|
||||
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
|
||||
"lastModified": 1780930886,
|
||||
"narHash": "sha256-rppURzHviaQN131F+nLiLdGfcb0uCd9gGP0E5+iw9MI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
|
||||
"rev": "8c3cede7ddc26bd659d2d383b5610efbd2c7a16e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
version = "unstable-${self.shortRev or self.dirtyShortRev or "dev"}";
|
||||
src = ./.;
|
||||
|
||||
vendorHash = "sha256-ztKXnOjZS/jMxsRjtF0rIZ3lKv4YjMdZd6oQFRuAtR4=";
|
||||
vendorHash = "sha256-fR63/dStMsZon22vancuLWIAvZiEYMLjMwY1kmRDNgM=";
|
||||
|
||||
# Tests require network access (httptest)
|
||||
doCheck = false;
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
module github.com/dropalldatabases/sif
|
||||
|
||||
go 1.24.2
|
||||
go 1.25.7
|
||||
|
||||
require (
|
||||
github.com/antchfx/htmlquery v1.3.5
|
||||
github.com/antchfx/htmlquery v1.3.6
|
||||
github.com/charmbracelet/glamour v1.0.0
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
github.com/charmbracelet/log v1.0.0
|
||||
github.com/gocolly/colly/v2 v2.3.0
|
||||
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/projectdiscovery/nuclei/v3 v3.8.0
|
||||
github.com/projectdiscovery/retryabledns v1.0.114
|
||||
github.com/projectdiscovery/utils v0.10.1
|
||||
github.com/rocketlaunchr/google-search v1.1.6
|
||||
github.com/twmb/murmur3 v1.1.8
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/time v0.14.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
aead.dev/minisign v0.2.0 // indirect
|
||||
aead.dev/minisign v0.3.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
|
||||
filippo.io/edwards25519 v1.1.1 // 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/Azure/go-ntlmssp v0.1.1 // 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/Masterminds/semver/v3 v3.4.0 // 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
|
||||
@@ -41,7 +47,7 @@ require (
|
||||
github.com/VividCortex/ewma v1.2.0 // indirect
|
||||
github.com/akrylysov/pogreb v0.10.2 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 // 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
|
||||
@@ -49,60 +55,61 @@ require (
|
||||
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/antchfx/xmlquery v1.5.0 // indirect
|
||||
github.com/antchfx/xpath v1.3.6 // 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 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // 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/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // 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/internal/v4a v1.4.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.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/smithy-go v1.24.2 // 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/bitset v1.24.4 // 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/buger/jsonparser v1.1.2 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||
github.com/caddyserver/certmagic v0.25.0 // indirect
|
||||
github.com/caddyserver/zerossl v0.1.3 // 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/colorprofile v0.3.2 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.2 // 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/exp/slice v0.0.0-20250908092851-c2208eb08494 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cheggaaa/pb/v3 v3.1.6 // indirect
|
||||
github.com/cheggaaa/pb/v3 v3.1.7 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // 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/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // 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/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davidmz/go-pageant v1.0.2 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
@@ -115,6 +122,7 @@ require (
|
||||
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/ebitengine/purego v0.10.0 // 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
|
||||
@@ -123,7 +131,7 @@ require (
|
||||
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/gaissmai/bart v0.26.1 // 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
|
||||
@@ -131,15 +139,16 @@ require (
|
||||
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-git/go-billy/v5 v5.9.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.19.1 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.11 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.1 // 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-pdf/fpdf v0.9.0 // indirect
|
||||
github.com/go-pg/pg/v10 v10.15.0 // indirect
|
||||
github.com/go-pg/zerochecker v0.2.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -154,7 +163,6 @@ require (
|
||||
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/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
|
||||
@@ -162,7 +170,7 @@ require (
|
||||
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/snappy v0.0.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // indirect
|
||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
@@ -177,7 +185,7 @@ require (
|
||||
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/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect
|
||||
github.com/hdm/jarm-go v0.0.7 // indirect
|
||||
@@ -204,28 +212,28 @@ require (
|
||||
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/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // 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/libdns/libdns v0.2.1 // indirect
|
||||
github.com/lib/pq v1.11.2 // indirect
|
||||
github.com/libdns/libdns v1.1.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/mackerelio/go-osstat v0.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/mackerelio/go-osstat v0.2.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.17 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.28 // indirect
|
||||
github.com/maypok86/otter/v2 v2.2.1 // indirect
|
||||
github.com/mholt/acmez v1.2.0 // indirect
|
||||
github.com/mholt/acmez/v3 v3.1.3 // 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
|
||||
@@ -242,6 +250,7 @@ require (
|
||||
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/nlnwa/whatwg-url v0.6.2 // 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
|
||||
@@ -253,45 +262,44 @@ require (
|
||||
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/pjbgf/sha1cd v0.6.0 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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/projectdiscovery/blackrock v0.0.1 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.2.20 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.2.31 // 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/dsl v0.8.14 // indirect
|
||||
github.com/projectdiscovery/fastdialer v0.5.6 // 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/gologger v1.1.68 // 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/hmap v0.0.100 // indirect
|
||||
github.com/projectdiscovery/httpx v1.9.0 // indirect
|
||||
github.com/projectdiscovery/interactsh v1.3.1 // 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/networkpolicy v0.1.36 // indirect
|
||||
github.com/projectdiscovery/ratelimit v0.0.85 // indirect
|
||||
github.com/projectdiscovery/rawhttp v0.1.90 // 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/retryablehttp-go v1.3.8 // 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/wappalyzergo v0.2.76 // indirect
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.6 // indirect
|
||||
github.com/redis/go-redis/v9 v9.11.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.1 // indirect
|
||||
github.com/refraction-networking/utls v1.8.2 // 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
|
||||
@@ -300,8 +308,7 @@ require (
|
||||
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/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 // 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
|
||||
@@ -312,8 +319,8 @@ require (
|
||||
github.com/spf13/cast v1.9.2 // 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/btree v1.8.1 // indirect
|
||||
github.com/tidwall/buntdb v1.3.2 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/grect v0.1.4 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
@@ -321,8 +328,8 @@ require (
|
||||
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/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
|
||||
github.com/trivago/tgo v1.0.7 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
@@ -335,7 +342,7 @@ require (
|
||||
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/weppos/publicsuffix-go v0.50.3 // 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
|
||||
@@ -350,39 +357,38 @@ require (
|
||||
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/yuin/goldmark-emoji v1.0.6 // 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/zcalusic/sysinfo v1.1.3 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // 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.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // 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
|
||||
go.opentelemetry.io/otel v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.41.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.41.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap/exp v0.3.0 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // 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.53.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/mod v0.36.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
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/tools v0.45.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // 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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
||||
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
||||
aead.dev/minisign v0.3.0 h1:8Xafzy5PEVZqYDNP60yJHARlW1eOQtsKNp/Ph2c0vRA=
|
||||
aead.dev/minisign v0.3.0/go.mod h1:NLvG3Uoq3skkRMDuc3YHpWUTMTrSExqm+Ij73W13F6Y=
|
||||
carvel.dev/ytt v0.52.0 h1:tkJPL8Gun5snVfypNXbmMKwnbwMyspcTi3Ypyso3nRY=
|
||||
carvel.dev/ytt v0.52.0/go.mod h1:QgmuU7E15EXW1r2wxTt7zExVz14IHwEG4WNMmaFBkJo=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
@@ -40,8 +41,8 @@ code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBl
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a h1:3i+FJ7IpSZHL+VAjtpQeZCRhrpP0odl5XfoLBY4fxJ8=
|
||||
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a/go.mod h1:C7hXLmFmPYPjIDGfQl1clsmQ5TMEQfmzWTrJk475bUs=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
|
||||
@@ -62,8 +63,8 @@ github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 h1:nVocQV40OQne5613E
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0/go.mod h1:7QJP7dr2wznCMeqIrhMgWGf7XpAQnVrJqDm9nvV3Cu4=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
|
||||
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
@@ -74,8 +75,8 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
|
||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
@@ -107,15 +108,15 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o
|
||||
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
|
||||
github.com/akrylysov/pogreb v0.10.2 h1:e6PxmeyEhWyi2AKOBIJzAEi4HkiC+lKyCocRGlnDi78=
|
||||
github.com/akrylysov/pogreb v0.10.2/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI=
|
||||
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
||||
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
||||
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
|
||||
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
|
||||
github.com/alecthomas/kingpin/v2 v2.3.1/go.mod h1:oYL5vtsvEHZGHxU7DMp32Dvx+qL+ptGn6lWaot2vCNE=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
|
||||
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -144,27 +145,27 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antchfx/htmlquery v1.2.3/go.mod h1:B0ABL+F5irhhMWg54ymEZinzMSi0Kt3I2if0BLYa3V0=
|
||||
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8=
|
||||
github.com/antchfx/htmlquery v1.3.5 h1:aYthDDClnG2a2xePf6tys/UyyM/kRcsFRm+ifhFKoU0=
|
||||
github.com/antchfx/htmlquery v1.3.5/go.mod h1:5oyIPIa3ovYGtLqMPNjBF2Uf25NPCKsMjCnQ8lvjaoA=
|
||||
github.com/antchfx/htmlquery v1.3.6 h1:RNHHL7YehO5XdO8IM8CynwLKONwRHWkrghbYhQIk9ag=
|
||||
github.com/antchfx/htmlquery v1.3.6/go.mod h1:kcVUqancxPygm26X2rceEcagZFFVkLEE7xgLkGSDl/4=
|
||||
github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM=
|
||||
github.com/antchfx/xmlquery v1.3.15/go.mod h1:zMDv5tIGjOxY/JCNNinnle7V/EwthZ5IT8eeCGJKRWA=
|
||||
github.com/antchfx/xmlquery v1.4.4 h1:mxMEkdYP3pjKSftxss4nUHfjBhnMk4imGoR96FRY2dg=
|
||||
github.com/antchfx/xmlquery v1.4.4/go.mod h1:AEPEEPYE9GnA2mj5Ur2L5Q5/2PycJ0N9Fusrx9b12fc=
|
||||
github.com/antchfx/xmlquery v1.5.0 h1:uAi+mO40ZWfyU6mlUBxRVvL6uBNZ6LMU4M3+mQIBV4c=
|
||||
github.com/antchfx/xmlquery v1.5.0/go.mod h1:lJfWRXzYMK1ss32zm1GQV3gMIW/HFey3xDZmkP1SuNc=
|
||||
github.com/antchfx/xpath v1.1.6/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/xpath v1.1.8/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
|
||||
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.2.4/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.5 h1:PqbXLC3TkfeZyakF5eeh3NTWEbYl4VHNVeufANzDbKQ=
|
||||
github.com/antchfx/xpath v1.3.5/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/antchfx/xpath v1.3.6 h1:s0y+ElRRtTQdfHP609qFu0+c6bglDv20pqOViQjjdPI=
|
||||
github.com/antchfx/xpath v1.3.6/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0=
|
||||
@@ -173,32 +174,32 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6Wm
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82 h1:EO13QJTCD1Ig2IrQnoHTRrn981H9mB7afXsZ89WptI4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82/go.mod h1:AGh1NCg0SH+uyJamiJA5tTQcql4MMRDXGRdMmCxCXzY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.82.0 h1:JubM8CGDDFaAOmBrd8CRYNr49ZNgEAiLwGwgNMdS0nw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.82.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
|
||||
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
|
||||
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
||||
@@ -211,8 +212,9 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bits-and-blooms/bitset v1.8.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
|
||||
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY=
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
@@ -229,16 +231,19 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk=
|
||||
github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/caddyserver/certmagic v0.19.2 h1:HZd1AKLx4592MalEGQS39DKs2ZOAJCEM/xYPMQ2/ui0=
|
||||
github.com/caddyserver/certmagic v0.19.2/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/caddyserver/certmagic v0.25.0 h1:VMleO/XA48gEWes5l+Fh6tRWo9bHkhwAEhx63i+F5ic=
|
||||
github.com/caddyserver/certmagic v0.25.0/go.mod h1:m9yB7Mud24OQbPHOiipAoyKPn9pKHhpSJxXR1jydBxA=
|
||||
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
|
||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||
@@ -253,26 +258,26 @@ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
||||
github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08=
|
||||
github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
|
||||
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
||||
github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw=
|
||||
github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494 h1:O5se1NwLfawEafCaxy3HztOFWgXlYgtLDQnjTTuRsBI=
|
||||
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/cheggaaa/pb/v3 v3.1.6 h1:h0x+vd7EiUohAJ29DJtJy+SNAc55t/elW3jCD086EXk=
|
||||
github.com/cheggaaa/pb/v3 v3.1.6/go.mod h1:urxmfVtaxT+9aWk92DbsvXFZtNSWQSO5TRAp+MJ3l1s=
|
||||
github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI=
|
||||
github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ=
|
||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
@@ -288,11 +293,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
||||
github.com/cloudflare/cfssl v1.6.4 h1:NMOvfrEjFfC63K3SGXgAnFdsgkmiq4kATme5BfcqrO8=
|
||||
github.com/cloudflare/cfssl v1.6.4/go.mod h1:8b3CQMxfWPAeom3zBnGJ6sd+G1NkL5TXqmDXacb+1J0=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a h1:Ohw57yVY2dBTt+gsC6aZdteyxwlxfbtgkFEMTEkwgSw=
|
||||
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a/go.mod h1:pCxVEbcm3AMg7ejXyorUXi6HQCzOIBf7zEDVPtw0/U4=
|
||||
@@ -304,8 +308,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -325,8 +329,8 @@ github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYC
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/docker/cli v27.4.1+incompatible h1:VzPiUlRJ/xh+otB75gva3r05isHMo5wXDfPRi5/b4hI=
|
||||
github.com/docker/cli v27.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=
|
||||
github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
@@ -339,6 +343,8 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
@@ -368,8 +374,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gaissmai/bart v0.26.0 h1:xOZ57E9hJLBiQaSyeZa9wgWhGuzfGACgqp4BE77OkO0=
|
||||
github.com/gaissmai/bart v0.26.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||
github.com/geoffgarside/ber v1.1.0 h1:qTmFG4jJbwiSzSXoNJeHcOprVzZ8Ulde2Rrrifu5U9w=
|
||||
github.com/geoffgarside/ber v1.1.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
|
||||
github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
|
||||
@@ -386,12 +392,12 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
|
||||
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
|
||||
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
|
||||
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
|
||||
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
@@ -406,8 +412,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -420,6 +426,8 @@ github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/go-pg/pg/v10 v10.15.0 h1:6DQwbaxJz/e4wvgzbxBkBLiL/Uuk87MGgHhkURtzx24=
|
||||
github.com/go-pg/pg/v10 v10.15.0/go.mod h1:FIn/x04hahOf9ywQ1p68rXqaDVbTRLYlu4MQR0lhoB8=
|
||||
github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4mU=
|
||||
@@ -457,8 +465,9 @@ github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakr
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||
github.com/gocolly/colly/v2 v2.1.0 h1:k0DuZkDoCsx51bKpRJNEmcxcp+W5N8ziuwGaSDuFoGs=
|
||||
github.com/gocolly/colly/v2 v2.1.0/go.mod h1:I2MuhsLjQ+Ex+IzK3afNS8/1qP3AedHOusRPcRdC5o0=
|
||||
github.com/gocolly/colly/v2 v2.3.0 h1:HSFh0ckbgVd2CSGRE+Y/iA4goUhGROJwyQDCMXGFBWM=
|
||||
github.com/gocolly/colly/v2 v2.3.0/go.mod h1:Qp54s/kQbwCQvFVx8KzKCSTXVJ1wWT4QeAKEu33x1q8=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
@@ -506,8 +515,8 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A=
|
||||
@@ -581,8 +590,8 @@ github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
|
||||
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
@@ -666,13 +675,10 @@ github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
@@ -695,10 +701,10 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa h1:KQKuQDgA3DZX6C396lt3WDYB9Um1gLITLbvficVbqXk=
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa/go.mod h1:HbwNE4XGwjgtUELkvQaAOjWrpianHYZdQVNqSdYW3UM=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
|
||||
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||
github.com/likexian/gokit v0.25.16 h1:wwBeUIN/OdoPp6t00xTnZE8Di/+s969Bl5N2Kw6bzP8=
|
||||
github.com/likexian/gokit v0.25.16/go.mod h1:Wqd4f+iifV0qxA1N3MqePJTUsmRy/lpst9/yXriDx/4=
|
||||
github.com/likexian/whois v1.15.7 h1:sajjDhi2bVD71AHJhjV7jLYxN92H4AWhTwxM8hmj7c0=
|
||||
@@ -712,10 +718,10 @@ github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 h1:z9RDOBcFcf3f2hSfK
|
||||
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8/go.mod h1:37YR9jabpiIxsb8X9VCIx8qFOjTDIIrIHHODa8C4gz0=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d h1:vFzYZc8yji+9DmNRhpEbs8VBK4CgV/DPfGzeVJSSp/8=
|
||||
github.com/lufia/plan9stats v0.0.0-20250821153705-5981dea3221d/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mackerelio/go-osstat v0.2.4 h1:qxGbdPkFo65PXOb/F/nhDKpF2nGmGaCFDLXoZjJTtUs=
|
||||
github.com/mackerelio/go-osstat v0.2.4/go.mod h1:Zy+qzGdZs3A9cuIqmgbJvwbmLQH9dJvtio5ZjJTbdlQ=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr7f2i0=
|
||||
github.com/mackerelio/go-osstat v0.2.6/go.mod h1:lRy8V9ZuHpuRVZh+vyTkODeDPl3/d5MgXHtLSaqG8bA=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
@@ -723,16 +729,16 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
|
||||
github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
|
||||
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
|
||||
github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
|
||||
github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
|
||||
github.com/mholt/acmez/v3 v3.1.3 h1:gUl789rjbJSuM5hYzOFnNaGgWPV1xVfnOs59o0dZEcc=
|
||||
github.com/mholt/acmez/v3 v3.1.3/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=
|
||||
github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -754,6 +760,10 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w=
|
||||
github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
|
||||
github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM=
|
||||
github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ=
|
||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
||||
@@ -783,6 +793,8 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nlnwa/whatwg-url v0.6.2 h1:jU61lU2ig4LANydbEJmA2nPrtCGiKdtgT0rmMd2VZ/Q=
|
||||
github.com/nlnwa/whatwg-url v0.6.2/go.mod h1:x0FPXJzzOEieQtsBT/AKvbiBbQ46YlL6Xa7m02M1ECk=
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 h1:/5oL8dzYivRM/tqX9VcTSWfbpwcbwKG1QtSJr3b3KcU=
|
||||
github.com/nwaples/rardecode/v2 v2.2.2/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
|
||||
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||
@@ -820,8 +832,8 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX
|
||||
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
|
||||
github.com/pierrec/lz4/v4 v4.1.23 h1:oJE7T90aYBGtFNrI8+KbETnPymobAhzRrR8Mu8n1yfU=
|
||||
github.com/pierrec/lz4/v4 v4.1.23/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
|
||||
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -839,14 +851,14 @@ github.com/projectdiscovery/asnmap v1.1.1 h1:ImJiKIaACOT7HPx4Pabb5dksolzaFYsD1kI
|
||||
github.com/projectdiscovery/asnmap v1.1.1/go.mod h1:QT7jt9nQanj+Ucjr9BqGr1Q2veCCKSAVyUzLXfEcQ60=
|
||||
github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ=
|
||||
github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss=
|
||||
github.com/projectdiscovery/cdncheck v1.2.20 h1:sMzoCi5TR7qQsH4LW0NF219PmX/lYjWUeoB2Iiddwcs=
|
||||
github.com/projectdiscovery/cdncheck v1.2.20/go.mod h1:gpeX5OrzaC4DmeUGDcKrC7cPUXQvRGTY/Ui0XrVfdzU=
|
||||
github.com/projectdiscovery/cdncheck v1.2.31 h1:8iD/MLDdMdMziM3RA5FkjUxO6kIwwgAoxWaL6RBIIl0=
|
||||
github.com/projectdiscovery/cdncheck v1.2.31/go.mod h1:6/B6caF1+97hR9cICMlzIYR8hpAN/y3AlJPHI2q48PQ=
|
||||
github.com/projectdiscovery/clistats v0.1.1 h1:8mwbdbwTU4aT88TJvwIzTpiNeow3XnAB72JIg66c8wE=
|
||||
github.com/projectdiscovery/clistats v0.1.1/go.mod h1:4LtTC9Oy//RiuT1+76MfTg8Hqs7FQp1JIGBM3nHK6a0=
|
||||
github.com/projectdiscovery/dsl v0.8.12 h1:gQL8k5zPok+5JGc7poiXzHCElNY/WnaTKoRB2wI3CYA=
|
||||
github.com/projectdiscovery/dsl v0.8.12/go.mod h1:pdMfUTNHMxlt6M94CSrCpZ1QObTP44rLqWifMMWW+IA=
|
||||
github.com/projectdiscovery/fastdialer v0.5.3 h1:Io57Q37ouFzrPK53ZdzK6jsELgqjIMCWcoDs+lRDGMA=
|
||||
github.com/projectdiscovery/fastdialer v0.5.3/go.mod h1:euoxS1E93LDnl0OnNN0UALedAFF+EehBxyU3z+79l0g=
|
||||
github.com/projectdiscovery/dsl v0.8.14 h1:g9szcXk2RRdVf2rsHEzbTXOPxiny3haKonSncU6pg2w=
|
||||
github.com/projectdiscovery/dsl v0.8.14/go.mod h1:LYImt/EiBzqTWG1RswT3Yl0DZbfjUP93Nvq2Z/G7dcE=
|
||||
github.com/projectdiscovery/fastdialer v0.5.6 h1:kIBFmzbXrua41uf4fGsQClTZmT7cm7E3vVgcSj8gs6Q=
|
||||
github.com/projectdiscovery/fastdialer v0.5.6/go.mod h1:QxvCe02Jii+j8vA3hWYkymgZIY8cqMgs2s3Jbz6mvbs=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2 h1:h2cISk5xDhlJEinlBQS6RRx0vOlOirB2y3Yu4PJzpiA=
|
||||
github.com/projectdiscovery/fasttemplate v0.0.2/go.mod h1:XYWWVMxnItd+r0GbjA1GCsUopMw1/XusuQxdyAIHMCw=
|
||||
github.com/projectdiscovery/freeport v0.0.7 h1:Q6uXo/j8SaV/GlAHkEYQi8WQoPXyJWxyspx+aFmz9Qk=
|
||||
@@ -857,18 +869,18 @@ github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb h1:rutG90
|
||||
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb/go.mod h1:FLjF1DmZ+POoGEiIQdWuYVwS++C/GwpX8YaCsTSm1RY=
|
||||
github.com/projectdiscovery/goflags v0.1.74 h1:n85uTRj5qMosm0PFBfsvOL24I7TdWRcWq/1GynhXS7c=
|
||||
github.com/projectdiscovery/goflags v0.1.74/go.mod h1:UMc9/7dFz2oln+10tv6cy+7WZKTHf9UGhaNkF95emh4=
|
||||
github.com/projectdiscovery/gologger v1.1.67 h1:GZU3AjYiJvcwJT5TlfIv+152/TVmaz62Zyn3/wWXlig=
|
||||
github.com/projectdiscovery/gologger v1.1.67/go.mod h1:35oeQP6wvj58S+o+Km6boED/t786FXQkI0exhFHJbNE=
|
||||
github.com/projectdiscovery/gologger v1.1.68 h1:KfdIO/3X7BtHssWZuqhxPZ+A946epCCx2cz+3NnRAnU=
|
||||
github.com/projectdiscovery/gologger v1.1.68/go.mod h1:Xae0t4SeqJVa0RQGK9iECx/+HfXhvq70nqOQp2BuW+o=
|
||||
github.com/projectdiscovery/gostruct v0.0.2 h1:s8gP8ApugGM4go1pA+sVlPDXaWqNP5BBDDSv7VEdG1M=
|
||||
github.com/projectdiscovery/gostruct v0.0.2/go.mod h1:H86peL4HKwMXcQQtEa6lmC8FuD9XFt6gkNR0B/Mu5PE=
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81 h1:yHh46pJovYbyiaHCV7oIDinFmy+Fyq36H1BowJgb0M0=
|
||||
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81/go.mod h1:9lmGPBDGZVANzCGjQg+V32n8Y3Cgjo/4kT0E88lsVTI=
|
||||
github.com/projectdiscovery/hmap v0.0.99 h1:XPfLnD3CUrMqVCIdpK9ozD7Xmp3simx3T+2j4WWhHnU=
|
||||
github.com/projectdiscovery/hmap v0.0.99/go.mod h1:koyUJi83K5G3w35ZLFXOYZIyYJsO+6hQrgDDN1RBrVE=
|
||||
github.com/projectdiscovery/httpx v1.8.1 h1:50NTzbgnqCgTJ1uawvusJq8Q6g0HM8TwEcxZgWdq5d4=
|
||||
github.com/projectdiscovery/httpx v1.8.1/go.mod h1:ws3cY6c7guy99M1eCYRbyaN57K0pEOguTZymMdxRZzc=
|
||||
github.com/projectdiscovery/interactsh v1.2.4 h1:WUSj+fxbcV53J64oIAhbYzCKD1w/IyenyRBhkI5jiqI=
|
||||
github.com/projectdiscovery/interactsh v1.2.4/go.mod h1:E/IVNZ80/WKz8zTwGJWQygxIbhlRmuzZFsZwcGSZTdc=
|
||||
github.com/projectdiscovery/hmap v0.0.100 h1:DBZ3Req9lWf4P1YC9PRa4eiMvLY0Uxud43NRBcocPfs=
|
||||
github.com/projectdiscovery/hmap v0.0.100/go.mod h1:2O06pR8pHOP9wSmxAoxuM45U7E+UqOqOdlSIeddM0bA=
|
||||
github.com/projectdiscovery/httpx v1.9.0 h1:5yn4ik/LqZ+v3MLgU7+CZJQyND9osW9NmZ3squylxsc=
|
||||
github.com/projectdiscovery/httpx v1.9.0/go.mod h1:jGTRyUHddo2WyK4klWIwQXgGF1Lu39XVyzlue4H3pX8=
|
||||
github.com/projectdiscovery/interactsh v1.3.1 h1:5HzeVGVCAX/cjTguJ+7ClOmML5r97Ty7op9s+/F7BiM=
|
||||
github.com/projectdiscovery/interactsh v1.3.1/go.mod h1:MXQ11EoBPROb4bEw+WP9e4DX4fMhrpS6EwfMfZomBsw=
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb h1:MGtI4oE12ruWv11ZlPXXd7hl/uAaQZrFvrIDYDeVMd8=
|
||||
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb/go.mod h1:vmgC0DTFCfoCLp0RAfsfYTZZan0QMVs+cmTbH6blfjk=
|
||||
github.com/projectdiscovery/machineid v0.0.0-20250715113114-c77eb3567582 h1:eR+0HE//Ciyfwy3HC7fjRyKShSJHYoX2Pv7pPshjK/Q=
|
||||
@@ -877,20 +889,20 @@ github.com/projectdiscovery/mapcidr v1.1.97 h1:7FkxNNVXp+m1rIu5Nv/2SrF9k4+LwP8Qu
|
||||
github.com/projectdiscovery/mapcidr v1.1.97/go.mod h1:9dgTJh1SP02gYZdpzMjm6vtYFkEHQHoTyaVNvaeJ7lA=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5 h1:L/e8z8yw1pfT6bg35NiN7yd1XKtJap5Nk6lMwQ0RNi8=
|
||||
github.com/projectdiscovery/n3iwf v0.0.0-20230523120440-b8cd232ff1f5/go.mod h1:pGW2ncnTxTxHtP9wzcIJAB+3/NMp6IiuQWd2NK7K+oc=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.34 h1:TRwNbgMwdx3NC190TKSLwtTvr0JAIZAlnWkOhW0yBME=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.34/go.mod h1:GJ20E7fJoA2vk8ZBSa1Cvc5WyP8RxglF5bZmYgK8jag=
|
||||
github.com/projectdiscovery/nuclei/v3 v3.7.0 h1:XA3QbY9kkYhXNQclykMGlfY0OuEfLjRw5gRctPsC91U=
|
||||
github.com/projectdiscovery/nuclei/v3 v3.7.0/go.mod h1:F0vcGrhwsVzZGuWluMrIeqgzSamBw6sd+YrCeCfi52k=
|
||||
github.com/projectdiscovery/ratelimit v0.0.83 h1:hfb36QvznBrjA4FNfpFE8AYRVBYrfJh8qHVROLQgl54=
|
||||
github.com/projectdiscovery/ratelimit v0.0.83/go.mod h1:z076BrLkBb5yS7uhHNoCTf8X/BvFSGRxwQ8EzEL9afM=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.36 h1:88EAYvEplBmn4vlGKenZJtzsGkEWALX3QzPiY930GtA=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.36/go.mod h1:lrm+DXxtH0cGpM4OKhILC+9ktnzrXVYcM0S2Jk+gQcc=
|
||||
github.com/projectdiscovery/nuclei/v3 v3.8.0 h1:UfIDjoHBsvACtvO4x8XIp6COffH+0G4sqco1qrijZqw=
|
||||
github.com/projectdiscovery/nuclei/v3 v3.8.0/go.mod h1:xBCCFK5nMafAuf3sWyOojzL9pKN91tj4Uwj2TK7HhOM=
|
||||
github.com/projectdiscovery/ratelimit v0.0.85 h1:TrqYis/+6Djac20n3kgFXQbN/xj7ywObJpH3xDOd+40=
|
||||
github.com/projectdiscovery/ratelimit v0.0.85/go.mod h1:enLZ8XGL02WPBhuoHAhgvMgOpuU9ALhFpFgCps5lxmM=
|
||||
github.com/projectdiscovery/rawhttp v0.1.90 h1:LOSZ6PUH08tnKmWsIwvwv1Z/4zkiYKYOSZ6n+8RFKtw=
|
||||
github.com/projectdiscovery/rawhttp v0.1.90/go.mod h1:VZYAM25UI/wVB3URZ95ZaftgOnsbphxyAw/XnQRRz4Y=
|
||||
github.com/projectdiscovery/rdap v0.9.1-0.20221108103045-9865884d1917 h1:m03X4gBVSorSzvmm0bFa7gDV4QNSOWPL/fgZ4kTXBxk=
|
||||
github.com/projectdiscovery/rdap v0.9.1-0.20221108103045-9865884d1917/go.mod h1:JxXtZC9e195awe7EynrcnBJmFoad/BNDzW9mzFkK8Sg=
|
||||
github.com/projectdiscovery/retryabledns v1.0.113 h1:s+DAzdJ8XhLxRgt5636H0HG9OqHsGRjX9wTrLSTMqlQ=
|
||||
github.com/projectdiscovery/retryabledns v1.0.113/go.mod h1:+DyanDr8naxQ2dRO9c4Ezo3NHHXhz8L0tTSRYWhiwyA=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.5 h1:6UXSJOEeeSE/IpI4xPrKRhSLkk3itNajfbgH91WtPPc=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.5/go.mod h1:2ma5Itx44tgfZCtHqnI7xbWEmsLXt1qXh+oOaJfmA+g=
|
||||
github.com/projectdiscovery/retryabledns v1.0.114 h1:COyNKzhA7oa3C/1639WRXeXsKrUJx06paVbN64IHZ3E=
|
||||
github.com/projectdiscovery/retryabledns v1.0.114/go.mod h1:+DyanDr8naxQ2dRO9c4Ezo3NHHXhz8L0tTSRYWhiwyA=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.8 h1:TA075ioaVyaM65R3dSzKSbOCiJSvFrlGScxzScu4ik8=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.8/go.mod h1:/vas835LvB4aqK9vCPGSgKF7Q7hY/BRcIJ/TgM2sPAY=
|
||||
github.com/projectdiscovery/sarif v0.0.1 h1:C2Tyj0SGOKbCLgHrx83vaE6YkzXEVrMXYRGLkKCr/us=
|
||||
github.com/projectdiscovery/sarif v0.0.1/go.mod h1:cEYlDu8amcPf6b9dSakcz2nNnJsoz4aR6peERwV+wuQ=
|
||||
github.com/projectdiscovery/stringsutil v0.0.2 h1:uzmw3IVLJSMW1kEg8eCStG/cGbYYZAja8BH3LqqJXMA=
|
||||
@@ -901,10 +913,10 @@ github.com/projectdiscovery/uncover v1.2.0 h1:31tjYa0v8FB8Ch8hJTxb+2t63vsljdOo0O
|
||||
github.com/projectdiscovery/uncover v1.2.0/go.mod h1:ozqKb++p39Kmh1SmwIpbQ9p0aVGPXuwsb4/X2Kvx6ms=
|
||||
github.com/projectdiscovery/useragent v0.0.107 h1:45gSBda052fv2Gtxtnpx7cu2rWtUpZEQRGAoYGP6F5M=
|
||||
github.com/projectdiscovery/useragent v0.0.107/go.mod h1:yv5ZZLDT/kq6P+NvBcCPq6sjEVQtZGgO+OvvHzZ+WtY=
|
||||
github.com/projectdiscovery/utils v0.9.0 h1:eu9vdbP0VYXI9nGSLfnOpUqBeW9/B/iSli7U8gPKZw8=
|
||||
github.com/projectdiscovery/utils v0.9.0/go.mod h1:zcVu1QTlMi5763qCol/L3ROnbd/UPSBP8fI5PmcnF6s=
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.65 h1:5hWGkuortLiq0whmVIfxbbE9pDl7Zd5e1rVRIEimOyk=
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.65/go.mod h1:Oc+U2RPJObmpi6LW5lTMEDiKagcKZNkEfZfwrVMURa0=
|
||||
github.com/projectdiscovery/utils v0.10.1 h1:9luYfL7PpN1L/cLO4bAES4+ltDaEBKOUnRiTn920XfM=
|
||||
github.com/projectdiscovery/utils v0.10.1/go.mod h1:x3jGS2YIxnUYxlpB9HWBKf0k+AE83nYCGRX/YStC8G8=
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.76 h1:6zQt6Jmi/hIwD8InWswkk1yhJGWaVEAEzshTGiTGbeM=
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.76/go.mod h1:hRsnKNleH693FFJsBOD5NMUDbxw/Q94f0Oq2OV04Q6M=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.6 h1:GCEdIRlQjDux28xTXKszM7n3jlMf152d5nqVpVoetas=
|
||||
github.com/projectdiscovery/yamldoc-go v1.0.6/go.mod h1:R5lWrNzP+7Oyn77NDVPnBsxx2/FyQZBBkIAaSaCQFxw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
@@ -933,8 +945,8 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua
|
||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||
github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs=
|
||||
github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
|
||||
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E=
|
||||
github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
@@ -960,12 +972,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
|
||||
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
|
||||
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
|
||||
github.com/sijms/go-ora/v2 v2.9.0 h1:+iQbUeTeCOFMb5BsOMgUhV8KWyrv9yjKpcK4x7+MFrg=
|
||||
@@ -1011,6 +1019,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
@@ -1022,10 +1032,10 @@ github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fx
|
||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||
github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI=
|
||||
github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8=
|
||||
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
|
||||
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
|
||||
github.com/tidwall/buntdb v1.3.1 h1:HKoDF01/aBhl9RjYtbaLnvX9/OuenwvQiC3OP1CcL4o=
|
||||
github.com/tidwall/buntdb v1.3.1/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
|
||||
github.com/tidwall/btree v1.8.1 h1:27ehoXvm5AG/g+1VxLS1SD3vRhp/H7LuEfwNvddEdmA=
|
||||
github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VXftj1A=
|
||||
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
|
||||
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
|
||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
@@ -1045,18 +1055,19 @@ github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaym
|
||||
github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw=
|
||||
github.com/tim-ywliu/nested-logrus-formatter v1.3.2 h1:jugNJ2/CNCI79SxOJCOhwUHeN3O7/7/bj+ZRGOFlCSw=
|
||||
github.com/tim-ywliu/nested-logrus-formatter v1.3.2/go.mod h1:oGPmcxZB65j9Wo7mCnQKSrKEJtVDqyjD666SGmyStXI=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo=
|
||||
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs=
|
||||
github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
|
||||
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/twmb/murmur3 v1.1.6 h1:mqrRot1BRxm+Yct+vavLMou2/iJt0tNVTTC0QoIjaZg=
|
||||
github.com/twmb/murmur3 v1.1.6/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/twmb/murmur3 v1.1.8 h1:8Yt9taO/WN3l08xErzjeschgZU2QSrwm1kclYq+0aRg=
|
||||
github.com/twmb/murmur3 v1.1.8/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
@@ -1080,8 +1091,8 @@ github.com/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8L
|
||||
github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k=
|
||||
github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY=
|
||||
github.com/weppos/publicsuffix-go v0.40.2/go.mod h1:XsLZnULC3EJ1Gvk9GVjuCTZ8QUu9ufE4TZpOizDShko=
|
||||
github.com/weppos/publicsuffix-go v0.50.3-0.20260104170930-90713dec78f2 h1:LiQSn5u8Nc6V/GixI+SWxt+YkNIyfKIlkVRULSw2Zt0=
|
||||
github.com/weppos/publicsuffix-go v0.50.3-0.20260104170930-90713dec78f2/go.mod h1:CbQCKDtXF8UcT7hrxeMa0MDjwhpOI9iYOU7cfq+yo8k=
|
||||
github.com/weppos/publicsuffix-go v0.50.3 h1:eT5dcjHQcVDNc0igpFEsGHKIip30feuB2zuuI9eJxiE=
|
||||
github.com/weppos/publicsuffix-go v0.50.3/go.mod h1:/rOa781xBykZhHK/I3QeHo92qdDKVmKZKF7s8qAEM/4=
|
||||
github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
|
||||
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
|
||||
@@ -1129,19 +1140,18 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
||||
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zcalusic/sysinfo v1.0.2 h1:nwTTo2a+WQ0NXwo0BGRojOJvJ/5XKvQih+2RrtWqfxc=
|
||||
github.com/zcalusic/sysinfo v1.0.2/go.mod h1:kluzTYflRWo6/tXVMJPdEjShsbPpsFRyy+p1mBQPC30=
|
||||
github.com/zcalusic/sysinfo v1.1.3 h1:u/AVENkuoikKuIZ4sUEJ6iibpmQP6YpGD8SSMCrqAF0=
|
||||
github.com/zcalusic/sysinfo v1.1.3/go.mod h1:NX+qYnWGtJVPV0yWldff9uppNKU4h40hJIRPf/pGLv4=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
||||
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
|
||||
@@ -1160,33 +1170,33 @@ github.com/zmap/zgrab2 v0.1.8/go.mod h1:5d8HSmUwvllx4q1qG50v/KXphkg45ZzWdaQtgTFn
|
||||
github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8=
|
||||
gitlab.com/gitlab-org/api/client-go v0.130.1 h1:1xF5C5Zq3sFeNg3PzS2z63oqrxifne3n/OnbI7nptRc=
|
||||
gitlab.com/gitlab-org/api/client-go v0.130.1/go.mod h1:ZhSxLAWadqP6J9lMh40IAZOlOxBLPRh7yFOXR/bMJWM=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
|
||||
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
|
||||
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
|
||||
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
|
||||
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -1195,6 +1205,10 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
goftp.io/server/v2 v2.0.1 h1:H+9UbCX2N206ePDSVNCjBftOKOgil6kQ5RAQNx5hJwE=
|
||||
@@ -1225,8 +1239,9 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -1237,8 +1252,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1264,8 +1279,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1322,8 +1337,9 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1352,8 +1368,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1420,8 +1436,9 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
@@ -1437,8 +1454,9 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1457,8 +1475,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1513,8 +1531,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1541,8 +1559,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
@@ -1607,8 +1626,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -1649,7 +1668,6 @@ mellium.im/sasl v0.3.2 h1:PT6Xp7ccn9XaXAnJ03FcEjmAn7kK1x7aoXV6F+Vmrl0=
|
||||
mellium.im/sasl v0.3.2/go.mod h1:NKXDi1zkr+BlMHLQjY3ofYuU4KSPFxknb8mfEu6SveY=
|
||||
moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8=
|
||||
moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -21,7 +21,16 @@ import (
|
||||
|
||||
type Settings struct {
|
||||
Dirlist string
|
||||
DirMatchCodes string // -mc dirlist: status codes to keep
|
||||
DirFilterCodes string // -fc dirlist: status codes to drop
|
||||
DirFilterSizes string // -fs dirlist: body sizes to drop
|
||||
DirFilterWords string // -fw dirlist: word counts to drop
|
||||
DirFilterRegex string // -fr dirlist: regex; body match drops response
|
||||
DirCalibrate bool // -ac dirlist: auto-calibrate soft-404 baseline
|
||||
DirWordlist string // -w dirlist: custom wordlist (file path or url)
|
||||
DirExtensions string // -e dirlist: extensions appended to each word
|
||||
Dnslist string
|
||||
Resolvers string // -resolvers dnslist: comma list overriding the bundled pool
|
||||
Debug bool
|
||||
LogDir string
|
||||
NoScan bool
|
||||
@@ -39,19 +48,55 @@ type Settings struct {
|
||||
Template string
|
||||
CMS bool
|
||||
Headers bool
|
||||
SecurityHeaders bool
|
||||
CloudStorage bool
|
||||
SubdomainTakeover bool
|
||||
Shodan bool
|
||||
SecurityTrails bool
|
||||
SQL bool
|
||||
LFI bool
|
||||
JWT bool
|
||||
OpenAPI bool
|
||||
Favicon bool
|
||||
CORS bool
|
||||
Redirect bool
|
||||
XSS bool
|
||||
Framework bool
|
||||
Crawl bool
|
||||
CrawlDepth int
|
||||
Passive bool
|
||||
Probe bool
|
||||
SARIF string // path to write a sarif 2.1.0 report to ("" = off)
|
||||
Markdown string // path to write a markdown report to ("" = off)
|
||||
Silent bool // route chrome to stderr, print one finding per line to stdout
|
||||
Diff bool // surface only findings added/removed vs the last snapshot
|
||||
Store string // snapshot dir for diff mode ("" = default state dir)
|
||||
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
|
||||
Proxy string
|
||||
Header goflags.StringSlice // custom request headers ("Key: Value")
|
||||
Cookie string
|
||||
RateLimit int
|
||||
Notify bool // -notify: ship findings to configured providers
|
||||
NotifySeverity string // -notify-severity: minimum severity to send (info..critical)
|
||||
NotifyConfig string // -notify-config: path to a notify-compatible yaml file
|
||||
}
|
||||
|
||||
// minThreads is the floor for the worker count. Threads feeds wg.Add across the
|
||||
// scanners, so 0 silently runs nothing and a negative value panics with
|
||||
// "negative WaitGroup counter"; clamp the parsed value up to this.
|
||||
const minThreads = 1
|
||||
|
||||
// defaultCrawlDepth bounds how far the spider recurses by default; deep enough
|
||||
// to find linked pages without crawling an entire site.
|
||||
const defaultCrawlDepth = 2
|
||||
|
||||
// defaultNotifySeverity is the floor notify sends at when -notify-severity is
|
||||
// unset: medium drops pure recon/info noise so alerts stay actionable.
|
||||
const defaultNotifySeverity = "medium"
|
||||
|
||||
const (
|
||||
Nil goflags.EnumVariable = iota
|
||||
|
||||
@@ -80,7 +125,16 @@ func Parse() *Settings {
|
||||
portScopes := goflags.AllowdTypes{"common": Common, "full": Full, "none": Nil}
|
||||
flagSet.CreateGroup("scans", "Scans",
|
||||
flagSet.EnumVar(&settings.Dirlist, "dirlist", Nil, "Directory fuzzing scan size (small/medium/large)", listSizes),
|
||||
flagSet.StringVar(&settings.DirMatchCodes, "mc", "", "Dirlist: match these status codes (comma list, e.g. 200,301)"),
|
||||
flagSet.StringVar(&settings.DirFilterCodes, "fc", "", "Dirlist: filter out these status codes (comma list)"),
|
||||
flagSet.StringVar(&settings.DirFilterSizes, "fs", "", "Dirlist: filter out responses of these body sizes (comma list)"),
|
||||
flagSet.StringVar(&settings.DirFilterWords, "fw", "", "Dirlist: filter out responses with these word counts (comma list)"),
|
||||
flagSet.StringVar(&settings.DirFilterRegex, "fr", "", "Dirlist: filter out responses whose body matches this regex"),
|
||||
flagSet.BoolVar(&settings.DirCalibrate, "ac", false, "Dirlist: auto-calibrate the soft-404 wildcard baseline"),
|
||||
flagSet.StringVar(&settings.DirWordlist, "w", "", "Dirlist: custom wordlist (local file path or url; overrides -dirlist size)"),
|
||||
flagSet.StringVar(&settings.DirExtensions, "e", "", "Dirlist: extensions appended to each word (comma list, e.g. php,bak,env)"),
|
||||
flagSet.EnumVar(&settings.Dnslist, "dnslist", Nil, "DNS fuzzing scan size (small/medium/large)", listSizes),
|
||||
flagSet.StringVar(&settings.Resolvers, "resolvers", "", "Dnslist: DNS resolvers to use (comma list, e.g. 1.1.1.1,8.8.8.8; overrides the bundled pool)"),
|
||||
flagSet.EnumVar(&settings.Ports, "ports", Nil, "Port scanning scope (common/full)", portScopes),
|
||||
flagSet.BoolVar(&settings.Dorking, "dork", false, "Enable Google dorking"),
|
||||
flagSet.BoolVar(&settings.Git, "git", false, "Enable git repository scanning"),
|
||||
@@ -90,13 +144,24 @@ func Parse() *Settings {
|
||||
flagSet.BoolVar(&settings.JavaScript, "js", false, "Enable JavaScript scans"),
|
||||
flagSet.BoolVar(&settings.CMS, "cms", false, "Enable CMS detection"),
|
||||
flagSet.BoolVar(&settings.Headers, "headers", false, "Enable HTTP Header Analysis"),
|
||||
flagSet.BoolVarP(&settings.SecurityHeaders, "security-headers", "sh", false, "Enable security header analysis (missing/weak headers)"),
|
||||
flagSet.BoolVar(&settings.CloudStorage, "c3", false, "Enable C3 Misconfiguration Scan"),
|
||||
flagSet.BoolVar(&settings.SubdomainTakeover, "st", false, "Enable Subdomain Takeover Check"),
|
||||
flagSet.BoolVar(&settings.Shodan, "shodan", false, "Enable Shodan lookup (requires SHODAN_API_KEY env var)"),
|
||||
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.JWT, "jwt", false, "Enable JWT discovery + offline weakness analysis"),
|
||||
flagSet.BoolVar(&settings.OpenAPI, "openapi", false, "Enable OpenAPI/Swagger spec exposure probe"),
|
||||
flagSet.BoolVar(&settings.Favicon, "favicon", false, "Enable favicon hash fingerprinting (shodan-style)"),
|
||||
flagSet.BoolVar(&settings.CORS, "cors", false, "Enable CORS misconfiguration probe"),
|
||||
flagSet.BoolVar(&settings.Redirect, "redirect", false, "Enable open redirect probe"),
|
||||
flagSet.BoolVar(&settings.XSS, "xss", false, "Enable reflected XSS probe"),
|
||||
flagSet.BoolVar(&settings.Framework, "framework", false, "Enable framework detection"),
|
||||
flagSet.BoolVar(&settings.Crawl, "crawl", false, "Enable web crawling (spider same-host links/scripts/forms)"),
|
||||
flagSet.IntVar(&settings.CrawlDepth, "crawl-depth", defaultCrawlDepth, "Max crawl recursion depth"),
|
||||
flagSet.BoolVar(&settings.Passive, "passive", false, "Enable passive subdomain/url discovery (zero traffic to target)"),
|
||||
flagSet.BoolVar(&settings.Probe, "probe", false, "Probe the target for liveness (status, title, server, redirect chain)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("runtime", "Runtime",
|
||||
@@ -107,8 +172,29 @@ func Parse() *Settings {
|
||||
flagSet.StringVar(&settings.Template, "template", "", "Sif runtime template to use"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("http", "HTTP",
|
||||
flagSet.StringVar(&settings.Proxy, "proxy", "", "Proxy for all requests (http/https/socks5 url)"),
|
||||
flagSet.StringSliceVarP(&settings.Header, "header", "H", nil, "Custom header to send (repeatable or comma-separated, \"Key: Value\")", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.StringVar(&settings.Cookie, "cookie", "", "Cookie header to send with every request"),
|
||||
flagSet.IntVar(&settings.RateLimit, "rate-limit", 0, "Max requests per second (0 = unlimited)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("output", "Output",
|
||||
flagSet.StringVar(&settings.SARIF, "sarif", "", "Write a SARIF 2.1.0 report to this file"),
|
||||
flagSet.StringVarP(&settings.Markdown, "markdown", "md", "", "Write a markdown report to this file"),
|
||||
flagSet.BoolVar(&settings.Silent, "silent", false, "Plain output: chrome to stderr, one finding per line to stdout (for pipelines)"),
|
||||
flagSet.BoolVar(&settings.Diff, "diff", false, "Diff mode: surface only findings added/removed since the last snapshot of each target"),
|
||||
flagSet.StringVar(&settings.Store, "store", "", "Snapshot directory for -diff (default: log dir, else <user-config>/sif/state)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("notify", "Notify",
|
||||
flagSet.BoolVar(&settings.Notify, "notify", false, "Ship findings to configured providers (slack/discord/telegram/webhook)"),
|
||||
flagSet.StringVar(&settings.NotifySeverity, "notify-severity", defaultNotifySeverity, "Minimum severity to notify on (info/low/medium/high/critical)"),
|
||||
flagSet.StringVar(&settings.NotifyConfig, "notify-config", "", "Path to a notify-compatible yaml config (overrides env vars)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("api", "API",
|
||||
flagSet.BoolVar(&settings.ApiMode, "api", false, "Enable API mode. Only useful for internal lunchcat usage"),
|
||||
flagSet.BoolVar(&settings.ApiMode, "api", false, "Enable API mode. Only useful for internal usage"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("modules", "Modules",
|
||||
@@ -122,5 +208,11 @@ func Parse() *Settings {
|
||||
log.Fatalf("Could not parse flags: %s", err)
|
||||
}
|
||||
|
||||
// threads feeds wg.Add directly; floor it so 0 isn't a silent no-op and a
|
||||
// negative value can't panic the waitgroup.
|
||||
if settings.Threads < minThreads {
|
||||
settings.Threads = minThreads
|
||||
}
|
||||
|
||||
return settings
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -61,6 +61,14 @@ func TestSettingsDefaults(t *testing.T) {
|
||||
if settings.Ports != "" {
|
||||
t.Errorf("expected Ports default to be empty, got %v", settings.Ports)
|
||||
}
|
||||
|
||||
// diff mode is opt-in and its store dir defaults empty (resolved at runtime).
|
||||
if settings.Diff != false {
|
||||
t.Errorf("expected Diff default to be false, got %v", settings.Diff)
|
||||
}
|
||||
if settings.Store != "" {
|
||||
t.Errorf("expected Store default to be empty, got %v", settings.Store)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettingsNoScanBehavior(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package dnsx resolves subdomain candidates against a bundled resolver pool
|
||||
// before anything is probed over http, so the slow/inaccurate path of HTTP-ing
|
||||
// every wordlist entry through the OS resolver is gone. it also fingerprints
|
||||
// wildcard zones (a zone that answers every random label) so a catch-all
|
||||
// nameserver can't flood the caller with phantom subdomains.
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
retryabledns "github.com/projectdiscovery/retryabledns"
|
||||
)
|
||||
|
||||
// bundled default resolver pool. anycast cloudflare/google/quad9 - fast, public,
|
||||
// and unlikely to rate-limit a recon sweep. -resolvers overrides this set.
|
||||
const (
|
||||
resolverCloudflare = "1.1.1.1:53"
|
||||
resolverGoogle = "8.8.8.8:53"
|
||||
resolverQuad9 = "9.9.9.9:53"
|
||||
)
|
||||
|
||||
// defaultResolvers is the bundled pool used when the caller passes none.
|
||||
var defaultResolvers = []string{resolverCloudflare, resolverGoogle, resolverQuad9}
|
||||
|
||||
const (
|
||||
// defaultRetries is how many times retryabledns rotates through the pool on a
|
||||
// timeout before giving up on a name. low enough to stay fast on a big list.
|
||||
defaultRetries = 3
|
||||
|
||||
// wildcardProbes is how many random nonexistent labels we resolve to
|
||||
// fingerprint a wildcard zone. more samples make a rotating catch-all (one
|
||||
// that hands back a different ip per query) harder to miss, but each is a
|
||||
// real lookup so this stays small.
|
||||
wildcardProbes = 3
|
||||
|
||||
// randomLabelLen is the length of each random wildcard-probe label. long
|
||||
// enough that a collision with a real host is astronomically unlikely.
|
||||
randomLabelLen = 16
|
||||
)
|
||||
|
||||
// randomLabelAlphabet is the lowercase-alnum set wildcard probe labels draw
|
||||
// from; a valid dns label so the query isn't rejected before it leaves.
|
||||
const randomLabelAlphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
// defaultDNSPort is appended to any resolver entry given without an explicit
|
||||
// port, so "1.1.1.1" and "1.1.1.1:53" both work on the cli.
|
||||
const defaultDNSPort = "53"
|
||||
|
||||
// ParseResolvers splits a comma list of resolvers into a normalized slice,
|
||||
// appending the default port to bare ips/hosts. an empty or blank input returns
|
||||
// nil so the caller falls back to the bundled pool.
|
||||
func ParseResolvers(raw string) []string {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
out := make([]string, 0, len(parts))
|
||||
for i := 0; i < len(parts); i++ {
|
||||
entry := strings.TrimSpace(parts[i])
|
||||
if entry == "" {
|
||||
continue
|
||||
}
|
||||
// a bare ip/host gets the default port; an entry already carrying ":port"
|
||||
// (or a bracketed ipv6 literal) is left as-is.
|
||||
if !strings.Contains(entry, ":") {
|
||||
entry += ":" + defaultDNSPort
|
||||
}
|
||||
out = append(out, entry)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// resolution is the resolved address set for one host. empty Addrs means the
|
||||
// name did not resolve (nxdomain / no records).
|
||||
type resolution struct {
|
||||
Addrs []string
|
||||
}
|
||||
|
||||
// resolved reports whether the name returned any address.
|
||||
func (r resolution) resolved() bool {
|
||||
return len(r.Addrs) > 0
|
||||
}
|
||||
|
||||
// resolverFn is the test seam: every lookup the package makes goes through this
|
||||
// var, so a fake can answer without touching the network. real runs point it at
|
||||
// a retryabledns-backed client via NewResolver.
|
||||
var resolverFn func(host string) (resolution, error)
|
||||
|
||||
// Resolver resolves candidates against a pool and filters wildcard answers. it
|
||||
// is built once per scan and shared across the worker goroutines; the
|
||||
// underlying retryabledns client is safe for concurrent use.
|
||||
type Resolver struct {
|
||||
// wildcardSigs holds the address sets a wildcard zone answers random labels
|
||||
// with. nil/empty means the zone is not wildcard. a candidate whose answer is
|
||||
// covered by one of these is a catch-all hit, not a real host.
|
||||
wildcardSigs []map[string]struct{}
|
||||
}
|
||||
|
||||
// NewResolver wires resolverFn to a retryabledns client over the given pool
|
||||
// (bundled default when resolvers is empty) and returns a Resolver. it does not
|
||||
// fingerprint anything yet - call FingerprintWildcard with the apex first.
|
||||
func NewResolver(resolvers []string) (*Resolver, error) {
|
||||
pool := resolvers
|
||||
if len(pool) == 0 {
|
||||
pool = defaultResolvers
|
||||
}
|
||||
|
||||
client, err := retryabledns.New(pool, defaultRetries)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dnsx: build resolver over %v: %w", pool, err)
|
||||
}
|
||||
|
||||
// only install the real client when a test hasn't already injected a fake;
|
||||
// the seam wins so hermetic tests never reach this client.
|
||||
if resolverFn == nil {
|
||||
resolverFn = func(host string) (resolution, error) {
|
||||
data, err := client.Resolve(host)
|
||||
if err != nil {
|
||||
return resolution{}, fmt.Errorf("dnsx: resolve %q: %w", host, err)
|
||||
}
|
||||
return resolution{Addrs: mergeAddrs(data)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return &Resolver{}, nil
|
||||
}
|
||||
|
||||
// FingerprintWildcard resolves wildcardProbes random labels under apex. any that
|
||||
// answer mean the zone is a catch-all, so their address sets are recorded as
|
||||
// signatures to filter real candidates against later. a clean zone leaves the
|
||||
// signature list empty and nothing gets filtered.
|
||||
func (r *Resolver) FingerprintWildcard(apex string) error {
|
||||
apex = strings.TrimSuffix(apex, ".")
|
||||
for i := 0; i < wildcardProbes; i++ {
|
||||
label, err := randomLabel(randomLabelLen)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dnsx: wildcard probe label: %w", err)
|
||||
}
|
||||
|
||||
res, err := resolverFn(label + "." + apex)
|
||||
if err != nil {
|
||||
// a probe failure (timeout / nxdomain surfaced as error) just means this
|
||||
// sample says "not wildcard"; don't abort the whole fingerprint on it.
|
||||
continue
|
||||
}
|
||||
if res.resolved() {
|
||||
r.wildcardSigs = append(r.wildcardSigs, toSet(res.Addrs))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve looks up host and reports whether it is a real, non-wildcard hit. a
|
||||
// name that doesn't resolve, or whose answer matches a recorded wildcard
|
||||
// signature, returns false so the caller skips probing it.
|
||||
func (r *Resolver) Resolve(host string) (bool, error) {
|
||||
res, err := resolverFn(host)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("dnsx: resolve %q: %w", host, err)
|
||||
}
|
||||
if !res.resolved() {
|
||||
return false, nil
|
||||
}
|
||||
if r.isWildcard(res.Addrs) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// isWildcard reports whether addrs is covered by any recorded wildcard
|
||||
// signature. a candidate whose every address appears in a wildcard answer is a
|
||||
// catch-all hit; a host with even one address outside the signature is a real,
|
||||
// distinct record and survives.
|
||||
func (r *Resolver) isWildcard(addrs []string) bool {
|
||||
if len(r.wildcardSigs) == 0 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(r.wildcardSigs); i++ {
|
||||
if subset(addrs, r.wildcardSigs[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// mergeAddrs flattens the A and AAAA answers into one sorted, deduped slice so
|
||||
// two equal answers compare equal regardless of record ordering.
|
||||
func mergeAddrs(data *retryabledns.DNSData) []string {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(data.A)+len(data.AAAA))
|
||||
for i := 0; i < len(data.A); i++ {
|
||||
seen[data.A[i]] = struct{}{}
|
||||
}
|
||||
for i := 0; i < len(data.AAAA); i++ {
|
||||
seen[data.AAAA[i]] = struct{}{}
|
||||
}
|
||||
|
||||
addrs := make([]string, 0, len(seen))
|
||||
for addr := range seen {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
sort.Strings(addrs)
|
||||
|
||||
return addrs
|
||||
}
|
||||
|
||||
// toSet turns addrs into a lookup set for subset checks.
|
||||
func toSet(addrs []string) map[string]struct{} {
|
||||
set := make(map[string]struct{}, len(addrs))
|
||||
for i := 0; i < len(addrs); i++ {
|
||||
set[addrs[i]] = struct{}{}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
// subset reports whether every addr is present in sig (and addrs is non-empty);
|
||||
// an empty addrs can't be a wildcard match.
|
||||
func subset(addrs []string, sig map[string]struct{}) bool {
|
||||
if len(addrs) == 0 {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(addrs); i++ {
|
||||
if _, ok := sig[addrs[i]]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// randomLabel returns a cryptographically-random lowercase-alnum dns label of
|
||||
// length n. crypto/rand (not math/rand) so a target can't predict the probe
|
||||
// labels and special-case them to defeat wildcard detection.
|
||||
func randomLabel(n int) (string, error) {
|
||||
var b strings.Builder
|
||||
b.Grow(n)
|
||||
alphabetLen := big.NewInt(int64(len(randomLabelAlphabet)))
|
||||
for i := 0; i < n; i++ {
|
||||
idx, err := rand.Int(rand.Reader, alphabetLen)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("dnsx: random index: %w", err)
|
||||
}
|
||||
b.WriteByte(randomLabelAlphabet[idx.Int64()])
|
||||
}
|
||||
|
||||
return b.String(), nil
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package dnsx
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// withFakeResolver swaps resolverFn for fn for the duration of one test, then
|
||||
// restores it - the seam that keeps every case below network-free.
|
||||
func withFakeResolver(t *testing.T, fn func(host string) (resolution, error)) {
|
||||
t.Helper()
|
||||
orig := resolverFn
|
||||
resolverFn = fn
|
||||
t.Cleanup(func() { resolverFn = orig })
|
||||
}
|
||||
|
||||
// newFingerprinted builds a Resolver and runs the wildcard fingerprint against
|
||||
// apex using the already-injected fake; fatal on error.
|
||||
func newFingerprinted(t *testing.T, apex string) *Resolver {
|
||||
t.Helper()
|
||||
r := &Resolver{}
|
||||
if err := r.FingerprintWildcard(apex); err != nil {
|
||||
t.Fatalf("FingerprintWildcard: %v", err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
const testApex = "example.com"
|
||||
|
||||
// a host that resolves to a real address, in a clean (non-wildcard) zone, is a
|
||||
// genuine hit.
|
||||
func TestResolve_FoundInCleanZone(t *testing.T) {
|
||||
withFakeResolver(t, func(host string) (resolution, error) {
|
||||
// nothing answers a random wildcard probe -> clean zone.
|
||||
if strings.HasSuffix(host, "."+testApex) && host != "www."+testApex {
|
||||
return resolution{}, nil
|
||||
}
|
||||
if host == "www."+testApex {
|
||||
return resolution{Addrs: []string{"93.184.216.34"}}, nil
|
||||
}
|
||||
return resolution{}, nil
|
||||
})
|
||||
|
||||
r := newFingerprinted(t, testApex)
|
||||
if len(r.wildcardSigs) != 0 {
|
||||
t.Fatalf("clean zone should record no wildcard signatures, got %d", len(r.wildcardSigs))
|
||||
}
|
||||
|
||||
ok, err := r.Resolve("www." + testApex)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("a resolving host in a clean zone should be a hit")
|
||||
}
|
||||
}
|
||||
|
||||
// nxdomain (no addresses) is not a hit, so the caller skips probing it.
|
||||
func TestResolve_NxdomainSkipped(t *testing.T) {
|
||||
withFakeResolver(t, func(string) (resolution, error) {
|
||||
// every name, probes included, returns no records.
|
||||
return resolution{}, nil
|
||||
})
|
||||
|
||||
r := newFingerprinted(t, testApex)
|
||||
|
||||
ok, err := r.Resolve("ghost." + testApex)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Error("an nxdomain host must not count as found")
|
||||
}
|
||||
}
|
||||
|
||||
// a wildcard zone answers the random probe labels, so a candidate that resolves
|
||||
// to the same catch-all address is filtered out.
|
||||
func TestResolve_WildcardFiltered(t *testing.T) {
|
||||
const catchAll = "10.0.0.1"
|
||||
withFakeResolver(t, func(string) (resolution, error) {
|
||||
// the zone answers everything - probes and candidates alike - with one ip.
|
||||
return resolution{Addrs: []string{catchAll}}, nil
|
||||
})
|
||||
|
||||
r := newFingerprinted(t, testApex)
|
||||
if len(r.wildcardSigs) == 0 {
|
||||
t.Fatal("wildcard zone should record at least one signature")
|
||||
}
|
||||
|
||||
ok, err := r.Resolve("anything." + testApex)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve: %v", err)
|
||||
}
|
||||
if ok {
|
||||
t.Error("a candidate matching the wildcard answer must be filtered")
|
||||
}
|
||||
}
|
||||
|
||||
// a real host in a wildcard zone that resolves to a distinct address (not the
|
||||
// catch-all) still survives the filter - one address outside the signature is
|
||||
// enough to be a genuine record.
|
||||
func TestResolve_DistinctHostSurvivesWildcard(t *testing.T) {
|
||||
const catchAll = "10.0.0.1"
|
||||
const realHost = "api." + testApex
|
||||
withFakeResolver(t, func(host string) (resolution, error) {
|
||||
if host == realHost {
|
||||
return resolution{Addrs: []string{"203.0.113.7"}}, nil
|
||||
}
|
||||
// everything else (probes + other candidates) hits the catch-all.
|
||||
return resolution{Addrs: []string{catchAll}}, nil
|
||||
})
|
||||
|
||||
r := newFingerprinted(t, testApex)
|
||||
if len(r.wildcardSigs) == 0 {
|
||||
t.Fatal("wildcard zone should record at least one signature")
|
||||
}
|
||||
|
||||
ok, err := r.Resolve(realHost)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Error("a host resolving to a distinct address should survive the wildcard filter")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseResolvers(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want []string
|
||||
}{
|
||||
{"empty falls back to bundled", "", nil},
|
||||
{"blank falls back to bundled", " ", nil},
|
||||
{"bare ips get default port", "1.1.1.1,8.8.8.8", []string{"1.1.1.1:53", "8.8.8.8:53"}},
|
||||
{"explicit port preserved", "9.9.9.9:5353", []string{"9.9.9.9:5353"}},
|
||||
{"whitespace and empties trimmed", " 1.1.1.1 , ,8.8.8.8 ", []string{"1.1.1.1:53", "8.8.8.8:53"}},
|
||||
{"mixed bare and ported", "1.1.1.1,9.9.9.9:5353", []string{"1.1.1.1:53", "9.9.9.9:5353"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := ParseResolvers(tt.in); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("ParseResolvers(%q) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewResolver_DefaultsToBundledPool(t *testing.T) {
|
||||
// keep the seam already installed so New doesn't replace it with a real
|
||||
// client; we only assert the constructor accepts an empty override.
|
||||
withFakeResolver(t, func(string) (resolution, error) { return resolution{}, nil })
|
||||
|
||||
r, err := NewResolver(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewResolver(nil): %v", err)
|
||||
}
|
||||
if r == nil {
|
||||
t.Fatal("NewResolver returned nil resolver")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,730 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package finding is the one normalization layer between the scan results and
|
||||
// the consumers that don't want to know about ~two dozen result structs: notify
|
||||
// (later) gates and renders on it, diff (later) keys runs off it. Flatten is the
|
||||
// single type-switch; adding a scanner without teaching Flatten about it trips
|
||||
// the guard test in flatten_test.go, on purpose.
|
||||
package finding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
"github.com/dropalldatabases/sif/internal/scan/js"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
)
|
||||
|
||||
// Finding is the normalized shape every scanner result collapses to. one
|
||||
// Finding is one underlying item (a single header, one cors hit, one nuclei
|
||||
// match) rather than a whole module's blob, so consumers diff and notify at
|
||||
// item granularity.
|
||||
type Finding struct {
|
||||
Target string // the url/host the scan ran against
|
||||
Module string // the ResultType() of the source scanner
|
||||
Severity Severity // ranked severity, SeverityUnknown when the source has none
|
||||
Key string // stable identity for dedup/diff: module + ":" + identifier
|
||||
Title string // short human label
|
||||
Raw string // short evidence string, not the full body
|
||||
}
|
||||
|
||||
// Line renders a finding as one stable, terse, machine-friendly line for the
|
||||
// -silent plain sink: "[severity] target module title". no styling, no color -
|
||||
// a downstream pipe (notify, grep, awk) keys off the bracketed severity and the
|
||||
// fixed field order, so the shape stays frozen. pointer receiver: Finding is
|
||||
// wide enough that copying it per line is wasteful.
|
||||
func (f *Finding) Line() string {
|
||||
return fmt.Sprintf("[%s] %s %s %s", f.Severity, f.Target, f.Module, f.Title)
|
||||
}
|
||||
|
||||
// static per-module severities for results that carry no severity field of
|
||||
// their own. these are the editorial baseline; a scanner that emits its own
|
||||
// severity (cors, xss, nuclei, ...) overrides this on a per-item basis.
|
||||
const (
|
||||
// a live admin panel / takeover / public bucket is high on its own.
|
||||
sevTakeover = SeverityHigh
|
||||
sevPublicS3 = SeverityHigh
|
||||
sevAdminPanel = SeverityHigh
|
||||
// disclosure-grade signals: dberrors, secrets, supabase keys.
|
||||
sevDBError = SeverityMedium
|
||||
sevSecret = SeverityMedium
|
||||
// pure recon/inventory: headers, crawl urls, passive hosts, ports.
|
||||
sevRecon = SeverityInfo
|
||||
)
|
||||
|
||||
// keySep joins the module id and the per-item identifier into a Key. kept as a
|
||||
// const so the diff layer can split on it without re-deriving the separator.
|
||||
const keySep = ":"
|
||||
|
||||
// key builds a stable per-item identity: module:identifier. identifier is
|
||||
// whatever uniquely names the item within its module (a url, a header name, a
|
||||
// subdomain) so the same finding across two runs produces the same Key.
|
||||
func key(module, identifier string) string {
|
||||
return module + keySep + identifier
|
||||
}
|
||||
|
||||
// Flatten normalizes one module's result into zero or more Findings. result is
|
||||
// the raw data carried in a ModuleResult; the type switch covers every scan
|
||||
// result struct. an unrecognized type yields a single SeverityUnknown finding
|
||||
// keyed "module:unhandled" so a new scanner surfaces loudly instead of
|
||||
// vanishing - the guard test asserts this never happens for a known type.
|
||||
func Flatten(target, module string, result any) []Finding {
|
||||
switch r := result.(type) {
|
||||
case *scan.ShodanResult:
|
||||
return flattenShodan(target, r)
|
||||
case *scan.SQLResult:
|
||||
return flattenSQL(target, r)
|
||||
case *scan.LFIResult:
|
||||
return flattenLFI(target, r)
|
||||
case *scan.JWTResult:
|
||||
return flattenJWT(target, r)
|
||||
case *scan.OpenAPIResult:
|
||||
return flattenOpenAPI(target, r)
|
||||
case *scan.FaviconResult:
|
||||
return flattenFavicon(target, r)
|
||||
case *scan.CMSResult:
|
||||
return flattenCMS(target, r)
|
||||
case *scan.SecurityTrailsResult:
|
||||
return flattenSecurityTrails(target, r)
|
||||
case *scan.CORSResult:
|
||||
return flattenCORS(target, r)
|
||||
case *scan.RedirectResult:
|
||||
return flattenRedirect(target, r)
|
||||
case *scan.XSSResult:
|
||||
return flattenXSS(target, r)
|
||||
case *scan.CrawlResult:
|
||||
return flattenCrawl(target, r)
|
||||
case *scan.PassiveResult:
|
||||
return flattenPassive(target, r)
|
||||
case *scan.ProbeResult:
|
||||
return flattenProbe(target, r)
|
||||
case scan.HeaderResults:
|
||||
return flattenHeaders(target, r)
|
||||
case []scan.HeaderResult:
|
||||
// the headers module appends a literal []HeaderResult, not the named
|
||||
// slice type; both reach here so cover both.
|
||||
return flattenHeaders(target, r)
|
||||
case scan.SecurityHeaderResults:
|
||||
return flattenSecurityHeaders(target, r)
|
||||
case []scan.SecurityHeaderResult:
|
||||
return flattenSecurityHeaders(target, r)
|
||||
case scan.DirectoryResults:
|
||||
return flattenDirlist(target, r)
|
||||
case []scan.DirectoryResult:
|
||||
return flattenDirlist(target, r)
|
||||
case scan.CloudStorageResults:
|
||||
return flattenCloudStorage(target, r)
|
||||
case []scan.CloudStorageResult:
|
||||
return flattenCloudStorage(target, r)
|
||||
case scan.DorkResults:
|
||||
return flattenDork(target, r)
|
||||
case []scan.DorkResult:
|
||||
return flattenDork(target, r)
|
||||
case scan.SubdomainTakeoverResults:
|
||||
return flattenTakeover(target, r)
|
||||
case []scan.SubdomainTakeoverResult:
|
||||
return flattenTakeover(target, r)
|
||||
case *frameworks.FrameworkResult:
|
||||
return flattenFramework(target, r)
|
||||
case *js.JavascriptScanResult:
|
||||
return flattenJS(target, r)
|
||||
case *modules.Result:
|
||||
// yaml/builtin modules carry their own module id; honor it over the
|
||||
// passed-in module so per-module findings stay attributed correctly.
|
||||
return flattenModule(target, r)
|
||||
case []output.ResultEvent:
|
||||
return flattenNuclei(target, r)
|
||||
case []string:
|
||||
// dnslist/portscan/git all hand back a bare []string of discovered
|
||||
// items; module disambiguates which inventory it is.
|
||||
return flattenStrings(target, module, r)
|
||||
default:
|
||||
// unknown type: emit a loud placeholder rather than dropping it.
|
||||
return []Finding{{
|
||||
Target: target,
|
||||
Module: module,
|
||||
Severity: SeverityUnknown,
|
||||
Key: key(module, "unhandled"),
|
||||
Title: fmt.Sprintf("unhandled result type %T", result),
|
||||
Raw: fmt.Sprintf("%T", result),
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
func flattenShodan(target string, r *scan.ShodanResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
// one host snapshot -> one inventory finding; vulns are the interesting bit
|
||||
// so they bump severity and ride along in the evidence string.
|
||||
sev := sevRecon
|
||||
if len(r.Vulns) > 0 {
|
||||
sev = SeverityHigh
|
||||
}
|
||||
raw := fmt.Sprintf("%d ports", len(r.Ports))
|
||||
if len(r.Vulns) > 0 {
|
||||
raw = fmt.Sprintf("%s, %d vulns", raw, len(r.Vulns))
|
||||
}
|
||||
return []Finding{{
|
||||
Target: target,
|
||||
Module: "shodan",
|
||||
Severity: sev,
|
||||
Key: key("shodan", r.IP),
|
||||
Title: "shodan host " + r.IP,
|
||||
Raw: raw,
|
||||
}}
|
||||
}
|
||||
|
||||
func flattenSQL(target string, r *scan.SQLResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.AdminPanels)+len(r.DatabaseErrors)+len(r.ExposedPorts))
|
||||
for i := 0; i < len(r.AdminPanels); i++ {
|
||||
p := r.AdminPanels[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "sql",
|
||||
Severity: sevAdminPanel,
|
||||
Key: key("sql", "admin:"+p.URL),
|
||||
Title: p.Type + " admin panel",
|
||||
Raw: fmt.Sprintf("%s (%d)", p.URL, p.Status),
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(r.DatabaseErrors); i++ {
|
||||
e := r.DatabaseErrors[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "sql",
|
||||
Severity: sevDBError,
|
||||
Key: key("sql", "dberr:"+e.URL+":"+e.DatabaseType),
|
||||
Title: e.DatabaseType + " error disclosure",
|
||||
Raw: e.ErrorPattern,
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(r.ExposedPorts); i++ {
|
||||
p := r.ExposedPorts[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "sql",
|
||||
Severity: SeverityMedium,
|
||||
Key: key("sql", fmt.Sprintf("port:%d", p)),
|
||||
Title: fmt.Sprintf("exposed db port %d", p),
|
||||
Raw: fmt.Sprintf("%d", p),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenLFI(target string, r *scan.LFIResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.Vulnerabilities))
|
||||
for i := 0; i < len(r.Vulnerabilities); i++ {
|
||||
v := r.Vulnerabilities[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "lfi",
|
||||
Severity: ParseSeverity(v.Severity),
|
||||
Key: key("lfi", v.URL+":"+v.Parameter),
|
||||
Title: "lfi via " + v.Parameter,
|
||||
Raw: v.Evidence,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenJWT(target string, r *scan.JWTResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.Tokens))
|
||||
for i := 0; i < len(r.Tokens); i++ {
|
||||
t := r.Tokens[i]
|
||||
// one finding per weakness, not per token: a token with alg:none and a
|
||||
// weak key is two distinct issues a consumer wants to diff separately.
|
||||
for j := 0; j < len(t.Issues); j++ {
|
||||
iss := t.Issues[j]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "jwt",
|
||||
Severity: ParseSeverity(iss.Severity),
|
||||
Key: key("jwt", t.Source+":"+iss.Kind),
|
||||
Title: "jwt " + iss.Kind,
|
||||
Raw: iss.Detail,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenOpenAPI(target string, r *scan.OpenAPIResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
return []Finding{{
|
||||
Target: target,
|
||||
Module: "openapi",
|
||||
Severity: ParseSeverity(r.Severity),
|
||||
Key: key("openapi", r.SpecURL),
|
||||
Title: "openapi spec exposed",
|
||||
Raw: fmt.Sprintf("%s (%d endpoints)", r.SpecURL, len(r.Endpoints)),
|
||||
}}
|
||||
}
|
||||
|
||||
func flattenFavicon(target string, r *scan.FaviconResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
// a matched fingerprint is a real signal; an unmatched hash is just inventory
|
||||
// (still useful as a shodan pivot, so we keep it at recon).
|
||||
sev := sevRecon
|
||||
title := fmt.Sprintf("favicon hash %d", r.Hash)
|
||||
if r.Tech != "" {
|
||||
sev = SeverityLow
|
||||
title = r.Tech + " (favicon)"
|
||||
}
|
||||
return []Finding{{
|
||||
Target: target,
|
||||
Module: "favicon",
|
||||
Severity: sev,
|
||||
Key: key("favicon", fmt.Sprintf("%d", r.Hash)),
|
||||
Title: title,
|
||||
Raw: r.ShodanQ,
|
||||
}}
|
||||
}
|
||||
|
||||
func flattenCMS(target string, r *scan.CMSResult) []Finding {
|
||||
if r == nil || r.Name == "" {
|
||||
return nil
|
||||
}
|
||||
return []Finding{{
|
||||
Target: target,
|
||||
Module: "cms",
|
||||
Severity: sevRecon,
|
||||
Key: key("cms", r.Name),
|
||||
Title: r.Name + " detected",
|
||||
Raw: strings.TrimSpace(r.Name + " " + r.Version),
|
||||
}}
|
||||
}
|
||||
|
||||
func flattenSecurityTrails(target string, r *scan.SecurityTrailsResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.Subdomains)+len(r.AssociatedDomains))
|
||||
for i := 0; i < len(r.Subdomains); i++ {
|
||||
d := r.Subdomains[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "securitytrails",
|
||||
Severity: sevRecon,
|
||||
Key: key("securitytrails", "sub:"+d),
|
||||
Title: "subdomain " + d,
|
||||
Raw: d,
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(r.AssociatedDomains); i++ {
|
||||
d := r.AssociatedDomains[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "securitytrails",
|
||||
Severity: sevRecon,
|
||||
Key: key("securitytrails", "assoc:"+d),
|
||||
Title: "associated domain " + d,
|
||||
Raw: d,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenCORS(target string, r *scan.CORSResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.Findings))
|
||||
for i := 0; i < len(r.Findings); i++ {
|
||||
f := r.Findings[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "cors",
|
||||
Severity: ParseSeverity(f.Severity),
|
||||
Key: key("cors", f.URL+":"+f.OriginTested),
|
||||
Title: f.Note,
|
||||
Raw: "allow-origin: " + f.AllowOrigin,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenRedirect(target string, r *scan.RedirectResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.Findings))
|
||||
for i := 0; i < len(r.Findings); i++ {
|
||||
f := r.Findings[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "redirect",
|
||||
Severity: ParseSeverity(f.Severity),
|
||||
Key: key("redirect", f.URL+":"+f.Parameter+":"+f.Via),
|
||||
Title: "open redirect via " + f.Parameter,
|
||||
Raw: f.Location,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenXSS(target string, r *scan.XSSResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.Findings))
|
||||
for i := 0; i < len(r.Findings); i++ {
|
||||
f := r.Findings[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "xss",
|
||||
Severity: ParseSeverity(f.Severity),
|
||||
Key: key("xss", f.URL+":"+f.Parameter+":"+f.Context),
|
||||
Title: "reflected xss in " + f.Parameter,
|
||||
Raw: strings.Join(f.SurvivedRaw, " "),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenCrawl(target string, r *scan.CrawlResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.URLs))
|
||||
for i := 0; i < len(r.URLs); i++ {
|
||||
u := r.URLs[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "crawl",
|
||||
Severity: sevRecon,
|
||||
Key: key("crawl", u),
|
||||
Title: "crawled url",
|
||||
Raw: u,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenPassive(target string, r *scan.PassiveResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Finding, 0, len(r.Subdomains)+len(r.URLs))
|
||||
for i := 0; i < len(r.Subdomains); i++ {
|
||||
s := r.Subdomains[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "passive",
|
||||
Severity: sevRecon,
|
||||
Key: key("passive", "sub:"+s),
|
||||
Title: "passive subdomain " + s,
|
||||
Raw: s,
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(r.URLs); i++ {
|
||||
u := r.URLs[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "passive",
|
||||
Severity: sevRecon,
|
||||
Key: key("passive", "url:"+u),
|
||||
Title: "passive url",
|
||||
Raw: u,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenProbe(target string, r *scan.ProbeResult) []Finding {
|
||||
if r == nil || !r.Alive {
|
||||
// a dead probe isn't a finding, just an absent host.
|
||||
return nil
|
||||
}
|
||||
return []Finding{{
|
||||
Target: target,
|
||||
Module: "probe",
|
||||
Severity: sevRecon,
|
||||
Key: key("probe", r.URL),
|
||||
Title: fmt.Sprintf("alive %d", r.StatusCode),
|
||||
Raw: strings.TrimSpace(fmt.Sprintf("%d %s", r.StatusCode, r.Title)),
|
||||
}}
|
||||
}
|
||||
|
||||
func flattenHeaders(target string, rs []scan.HeaderResult) []Finding {
|
||||
out := make([]Finding, 0, len(rs))
|
||||
for i := 0; i < len(rs); i++ {
|
||||
h := rs[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "headers",
|
||||
Severity: sevRecon,
|
||||
Key: key("headers", h.Name),
|
||||
Title: h.Name,
|
||||
Raw: h.Value,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenSecurityHeaders(target string, rs []scan.SecurityHeaderResult) []Finding {
|
||||
out := make([]Finding, 0, len(rs))
|
||||
for i := 0; i < len(rs); i++ {
|
||||
h := rs[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "security_headers",
|
||||
Severity: ParseSeverity(h.Severity),
|
||||
Key: key("security_headers", h.Header),
|
||||
Title: h.Header,
|
||||
Raw: h.Note,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dirInteresting bounds the "noteworthy" 3xx range for a listed directory; a
|
||||
// redirect (>=300) or anything past it is worth more than a plain 200 hit.
|
||||
const dirRedirectFloor = 300
|
||||
|
||||
func flattenDirlist(target string, rs []scan.DirectoryResult) []Finding {
|
||||
out := make([]Finding, 0, len(rs))
|
||||
for i := 0; i < len(rs); i++ {
|
||||
d := rs[i]
|
||||
sev := sevRecon
|
||||
if d.StatusCode >= dirRedirectFloor {
|
||||
sev = SeverityLow
|
||||
}
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "dirlist",
|
||||
Severity: sev,
|
||||
Key: key("dirlist", d.Url),
|
||||
Title: fmt.Sprintf("%s [%d]", d.Url, d.StatusCode),
|
||||
Raw: fmt.Sprintf("status=%d size=%d", d.StatusCode, d.Size),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenCloudStorage(target string, rs []scan.CloudStorageResult) []Finding {
|
||||
out := make([]Finding, 0, len(rs))
|
||||
for i := 0; i < len(rs); i++ {
|
||||
b := rs[i]
|
||||
sev := sevRecon
|
||||
if b.IsPublic {
|
||||
sev = sevPublicS3
|
||||
}
|
||||
title := "bucket " + b.BucketName
|
||||
if b.IsPublic {
|
||||
title = "public bucket " + b.BucketName
|
||||
}
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "cloudstorage",
|
||||
Severity: sev,
|
||||
Key: key("cloudstorage", b.BucketName),
|
||||
Title: title,
|
||||
Raw: fmt.Sprintf("public=%t", b.IsPublic),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenDork(target string, rs []scan.DorkResult) []Finding {
|
||||
out := make([]Finding, 0, len(rs))
|
||||
for i := 0; i < len(rs); i++ {
|
||||
d := rs[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "dork",
|
||||
Severity: sevRecon,
|
||||
Key: key("dork", d.Url),
|
||||
Title: "dork hit",
|
||||
Raw: d.Url,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenTakeover(target string, rs []scan.SubdomainTakeoverResult) []Finding {
|
||||
out := make([]Finding, 0, len(rs))
|
||||
for i := 0; i < len(rs); i++ {
|
||||
t := rs[i]
|
||||
// only the vulnerable ones are findings; a safe cname is noise here.
|
||||
if !t.Vulnerable {
|
||||
continue
|
||||
}
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "subdomain_takeover",
|
||||
Severity: sevTakeover,
|
||||
Key: key("subdomain_takeover", t.Subdomain),
|
||||
Title: "takeover: " + t.Subdomain,
|
||||
Raw: t.Service,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenFramework(target string, r *frameworks.FrameworkResult) []Finding {
|
||||
if r == nil || r.Name == "" {
|
||||
return nil
|
||||
}
|
||||
// framework risk maps onto severity; an unset risk falls back to recon.
|
||||
sev := ParseSeverity(r.RiskLevel)
|
||||
if sev == SeverityUnknown {
|
||||
sev = sevRecon
|
||||
}
|
||||
raw := strings.TrimSpace(r.Name + " " + r.Version)
|
||||
if len(r.CVEs) > 0 {
|
||||
raw = fmt.Sprintf("%s, %d cves", raw, len(r.CVEs))
|
||||
}
|
||||
return []Finding{{
|
||||
Target: target,
|
||||
Module: "framework",
|
||||
Severity: sev,
|
||||
Key: key("framework", r.Name),
|
||||
Title: r.Name + " detected",
|
||||
Raw: raw,
|
||||
}}
|
||||
}
|
||||
|
||||
func flattenJS(target string, r *js.JavascriptScanResult) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
supabase := r.SupabaseFindings()
|
||||
out := make([]Finding, 0, len(r.SecretMatches)+len(supabase)+len(r.Endpoints)+len(r.FoundEnvironmentVars))
|
||||
for i := 0; i < len(r.SecretMatches); i++ {
|
||||
s := r.SecretMatches[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "js",
|
||||
Severity: sevSecret,
|
||||
Key: key("js", "secret:"+s.Rule+":"+s.Source),
|
||||
Title: "secret: " + s.Rule,
|
||||
Raw: s.Source,
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(supabase); i++ {
|
||||
s := supabase[i]
|
||||
// a non-anon role on an exposed key is the real bug; anon is just recon.
|
||||
sev := sevRecon
|
||||
if s.Role != "" && s.Role != "anon" {
|
||||
sev = SeverityHigh
|
||||
}
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "js",
|
||||
Severity: sev,
|
||||
Key: key("js", "supabase:"+s.ProjectId),
|
||||
Title: "supabase project " + s.ProjectId,
|
||||
Raw: fmt.Sprintf("role=%s collections=%d", s.Role, s.Collections),
|
||||
})
|
||||
}
|
||||
for i := 0; i < len(r.Endpoints); i++ {
|
||||
e := r.Endpoints[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "js",
|
||||
Severity: sevRecon,
|
||||
Key: key("js", "endpoint:"+e),
|
||||
Title: "js endpoint",
|
||||
Raw: e,
|
||||
})
|
||||
}
|
||||
// env vars are a map; sort-free since the Key carries the name, and diff
|
||||
// keys on the Key not on iteration order.
|
||||
for name, value := range r.FoundEnvironmentVars {
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "js",
|
||||
Severity: sevSecret,
|
||||
Key: key("js", "env:"+name),
|
||||
Title: "env var " + name,
|
||||
Raw: value,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenModule(target string, r *modules.Result) []Finding {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
module := r.ResultType()
|
||||
out := make([]Finding, 0, len(r.Findings))
|
||||
for i := 0; i < len(r.Findings); i++ {
|
||||
f := r.Findings[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: module,
|
||||
Severity: ParseSeverity(f.Severity),
|
||||
Key: key(module, f.URL),
|
||||
Title: module + " finding",
|
||||
Raw: f.Evidence,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenNuclei(target string, events []output.ResultEvent) []Finding {
|
||||
out := make([]Finding, 0, len(events))
|
||||
for i := 0; i < len(events); i++ {
|
||||
e := events[i]
|
||||
// host is the most reliable per-hit identifier; matched-at sharpens it
|
||||
// when several templates fire on one host.
|
||||
ident := e.TemplateID + ":" + e.Host
|
||||
if e.Matched != "" {
|
||||
ident = e.TemplateID + ":" + e.Matched
|
||||
}
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: "nuclei",
|
||||
Severity: ParseSeverity(e.Info.SeverityHolder.Severity.String()),
|
||||
Key: key("nuclei", ident),
|
||||
Title: e.Info.Name,
|
||||
Raw: e.Matched,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func flattenStrings(target, module string, items []string) []Finding {
|
||||
out := make([]Finding, 0, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
v := items[i]
|
||||
out = append(out, Finding{
|
||||
Target: target,
|
||||
Module: module,
|
||||
Severity: sevRecon,
|
||||
Key: key(module, v),
|
||||
Title: module + " item",
|
||||
Raw: v,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package finding
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
"github.com/dropalldatabases/sif/internal/scan/js"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
)
|
||||
|
||||
// scanResultType mirrors the minimal interface the scan packages implement; the
|
||||
// coverage table below carries a value per ResultType() so a new scanner whose
|
||||
// ResultType isn't represented (or isn't handled by Flatten) trips a failure.
|
||||
type scanResultType interface {
|
||||
ResultType() string
|
||||
}
|
||||
|
||||
// coverageCase is one representative, non-empty instance of a result type plus
|
||||
// its expected module attribution. wantItems is how many findings Flatten must
|
||||
// emit for the populated instance, proving the per-item fan-out works.
|
||||
type coverageCase struct {
|
||||
value any // the result as it reaches Flatten
|
||||
typed scanResultType // same value when it implements ResultType(), else nil
|
||||
module string // module id Flatten should stamp
|
||||
wantItems int // findings the populated instance must produce
|
||||
}
|
||||
|
||||
// coverageCases is the registry the guard checks against. there must be one
|
||||
// entry per distinct ResultType() in the scan tree (plus the raw []string and
|
||||
// nuclei []ResultEvent that flow through the report without a ResultType). add a
|
||||
// scanner without adding it here and TestFlattenCoversEveryResultType fails.
|
||||
func coverageCases() []coverageCase {
|
||||
return []coverageCase{
|
||||
{
|
||||
value: &scan.ShodanResult{IP: "1.2.3.4", Ports: []int{80}, Vulns: []string{"CVE-1"}},
|
||||
typed: &scan.ShodanResult{},
|
||||
module: "shodan",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.SQLResult{
|
||||
AdminPanels: []scan.SQLAdminPanel{{URL: "http://x/pma", Type: "phpMyAdmin", Status: 200}},
|
||||
DatabaseErrors: []scan.SQLDatabaseError{{URL: "http://x", DatabaseType: "mysql", ErrorPattern: "syntax"}},
|
||||
ExposedPorts: []int{3306},
|
||||
},
|
||||
typed: &scan.SQLResult{},
|
||||
module: "sql",
|
||||
wantItems: 3,
|
||||
},
|
||||
{
|
||||
value: &scan.LFIResult{Vulnerabilities: []scan.LFIVulnerability{
|
||||
{URL: "http://x", Parameter: "file", Evidence: "root:x", Severity: "high"},
|
||||
}},
|
||||
typed: &scan.LFIResult{},
|
||||
module: "lfi",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.JWTResult{Tokens: []scan.JWTToken{{
|
||||
Source: "header:Authorization",
|
||||
Alg: "none",
|
||||
Issues: []scan.JWTIssue{
|
||||
{Kind: "alg:none", Severity: "critical", Detail: "no signature"},
|
||||
{Kind: "missing exp", Severity: "medium", Detail: "no expiry"},
|
||||
},
|
||||
}}},
|
||||
typed: &scan.JWTResult{},
|
||||
module: "jwt",
|
||||
wantItems: 2,
|
||||
},
|
||||
{
|
||||
value: &scan.OpenAPIResult{
|
||||
SpecURL: "http://x/openapi.json",
|
||||
Severity: "high",
|
||||
Endpoints: []scan.OpenAPIEndpoint{{Path: "/users", Method: "GET", Unauth: true}},
|
||||
},
|
||||
typed: &scan.OpenAPIResult{},
|
||||
module: "openapi",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.FaviconResult{Hash: 116323821, Tech: "Apache Tomcat", ShodanQ: "http.favicon.hash:116323821"},
|
||||
typed: &scan.FaviconResult{},
|
||||
module: "favicon",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.CMSResult{Name: "WordPress", Version: "6.1"},
|
||||
typed: &scan.CMSResult{},
|
||||
module: "cms",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.SecurityTrailsResult{Domain: "x.com", Subdomains: []string{"a.x.com"}, AssociatedDomains: []string{"y.com"}},
|
||||
typed: &scan.SecurityTrailsResult{},
|
||||
module: "securitytrails",
|
||||
wantItems: 2,
|
||||
},
|
||||
{
|
||||
value: &scan.CORSResult{Findings: []scan.CORSFinding{{URL: "http://x", OriginTested: "null", AllowOrigin: "null", Severity: "medium", Note: "null origin"}}},
|
||||
typed: &scan.CORSResult{},
|
||||
module: "cors",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.RedirectResult{Findings: []scan.RedirectFinding{{URL: "http://x", Parameter: "next", Location: "http://evil", Via: "header", Severity: "medium"}}},
|
||||
typed: &scan.RedirectResult{},
|
||||
module: "redirect",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.XSSResult{Findings: []scan.XSSFinding{{URL: "http://x", Parameter: "q", Context: "html", SurvivedRaw: []string{"<"}, Severity: "high"}}},
|
||||
typed: &scan.XSSResult{},
|
||||
module: "xss",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.CrawlResult{URLs: []string{"http://x/a"}},
|
||||
typed: &scan.CrawlResult{},
|
||||
module: "crawl",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &scan.PassiveResult{Subdomains: []string{"a.x.com"}, URLs: []string{"http://x/old"}},
|
||||
typed: &scan.PassiveResult{},
|
||||
module: "passive",
|
||||
wantItems: 2,
|
||||
},
|
||||
{
|
||||
value: &scan.ProbeResult{URL: "http://x", Alive: true, StatusCode: 200, Title: "home"},
|
||||
typed: &scan.ProbeResult{},
|
||||
module: "probe",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: scan.HeaderResults{{Name: "Server", Value: "nginx"}},
|
||||
typed: scan.HeaderResults{},
|
||||
module: "headers",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: scan.SecurityHeaderResults{{Header: "Content-Security-Policy", Present: false, Severity: "medium", Note: "missing"}},
|
||||
typed: scan.SecurityHeaderResults{},
|
||||
module: "security_headers",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: scan.DirectoryResults{{Url: "http://x/admin", StatusCode: 301, Size: 10, Words: 2}},
|
||||
typed: scan.DirectoryResults{},
|
||||
module: "dirlist",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: scan.CloudStorageResults{{BucketName: "x-assets", IsPublic: true}},
|
||||
typed: scan.CloudStorageResults{},
|
||||
module: "cloudstorage",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: scan.DorkResults{{Url: "http://x/leak", Count: 1}},
|
||||
typed: scan.DorkResults{},
|
||||
module: "dork",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: scan.SubdomainTakeoverResults{{Subdomain: "old.x.com", Vulnerable: true, Service: "GitHub Pages"}},
|
||||
typed: scan.SubdomainTakeoverResults{},
|
||||
module: "subdomain_takeover",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &frameworks.FrameworkResult{Name: "Laravel", Version: "9.0", RiskLevel: "high", CVEs: []string{"CVE-2"}},
|
||||
typed: &frameworks.FrameworkResult{},
|
||||
module: "framework",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
value: &js.JavascriptScanResult{
|
||||
SecretMatches: []js.SecretMatch{{Rule: "aws-key", Match: "AKIA...", Source: "http://x/app.js"}},
|
||||
Endpoints: []string{"/api/v1"},
|
||||
},
|
||||
typed: &js.JavascriptScanResult{},
|
||||
module: "js",
|
||||
wantItems: 2,
|
||||
},
|
||||
{
|
||||
value: &modules.Result{ModuleID: "custom-mod", Target: "http://x", Findings: []modules.Finding{{URL: "http://x", Severity: "low", Evidence: "hit"}}},
|
||||
typed: &modules.Result{ModuleID: "custom-mod"},
|
||||
module: "custom-mod",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
// nuclei results aren't ScanResult-typed; they ride through the report
|
||||
// as a raw []ResultEvent, so cover that shape explicitly.
|
||||
value: []output.ResultEvent{{TemplateID: "t1", Host: "x", Matched: "http://x", Info: model.Info{Name: "n", SeverityHolder: severity.Holder{Severity: severity.High}}}},
|
||||
module: "nuclei",
|
||||
wantItems: 1,
|
||||
},
|
||||
{
|
||||
// dnslist/portscan/git all hand Flatten a bare []string keyed only by
|
||||
// the module argument.
|
||||
value: []string{"sub.x.com"},
|
||||
module: "dnslist",
|
||||
wantItems: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const target = "http://target.example"
|
||||
|
||||
// TestFlattenCoversEveryResultType is the guard: every result type in the
|
||||
// coverage table must flatten into the expected module without hitting the
|
||||
// "unhandled" fallback. a new scanner that skips both the table and Flatten's
|
||||
// switch trips this loudly.
|
||||
func TestFlattenCoversEveryResultType(t *testing.T) {
|
||||
for _, tc := range coverageCases() {
|
||||
findings := Flatten(target, tc.module, tc.value)
|
||||
|
||||
if len(findings) != tc.wantItems {
|
||||
t.Errorf("module %q: got %d findings, want %d", tc.module, len(findings), tc.wantItems)
|
||||
}
|
||||
for i := 0; i < len(findings); i++ {
|
||||
f := findings[i]
|
||||
if strings.HasSuffix(f.Key, keySep+"unhandled") {
|
||||
t.Errorf("module %q: Flatten has no case, fell through to unhandled (key=%q)", tc.module, f.Key)
|
||||
}
|
||||
if f.Target != target {
|
||||
t.Errorf("module %q: target=%q, want %q", tc.module, f.Target, target)
|
||||
}
|
||||
if f.Module != tc.module {
|
||||
t.Errorf("module %q: finding stamped module=%q, want %q", tc.module, f.Module, tc.module)
|
||||
}
|
||||
if f.Key == "" {
|
||||
t.Errorf("module %q: empty Key", tc.module)
|
||||
}
|
||||
if !strings.HasPrefix(f.Key, tc.module+keySep) {
|
||||
t.Errorf("module %q: Key %q not prefixed with module", tc.module, f.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestEveryResultTypeIsInCoverageTable cross-checks the table against the actual
|
||||
// ResultType() registry: if a scanner type exists whose ResultType() isn't in
|
||||
// the table, the coverage guard above would never exercise it. enumerate the
|
||||
// known typed entries and assert each ResultType() string is present.
|
||||
func TestEveryResultTypeIsInCoverageTable(t *testing.T) {
|
||||
covered := make(map[string]struct{})
|
||||
for _, tc := range coverageCases() {
|
||||
if tc.typed == nil {
|
||||
continue
|
||||
}
|
||||
covered[tc.typed.ResultType()] = struct{}{}
|
||||
}
|
||||
|
||||
// the full set of ResultType() strings the scan tree exposes. keep this in
|
||||
// lockstep with the ScanResult implementers; a missing entry means the table
|
||||
// (and very likely Flatten) skipped a scanner.
|
||||
want := []string{
|
||||
"shodan", "sql", "lfi", "jwt", "openapi", "favicon", "cms", "securitytrails",
|
||||
"cors", "redirect", "xss", "crawl", "passive", "probe",
|
||||
"headers", "security_headers", "dirlist", "cloudstorage",
|
||||
"dork", "subdomain_takeover", "framework", "js", "custom-mod",
|
||||
}
|
||||
for _, rt := range want {
|
||||
if _, ok := covered[rt]; !ok {
|
||||
t.Errorf("ResultType %q has no entry in coverageCases; Flatten coverage unverified", rt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlattenStableKeysAndSeverities pins the keys and severities for a couple
|
||||
// of representative items so a refactor that quietly reshuffles them is caught.
|
||||
func TestFlattenStableKeysAndSeverities(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value any
|
||||
module string
|
||||
wantKey string
|
||||
wantSev Severity
|
||||
}{
|
||||
{
|
||||
name: "cors honors source severity",
|
||||
value: &scan.CORSResult{Findings: []scan.CORSFinding{{URL: "http://x", OriginTested: "null", AllowOrigin: "null", Severity: "high", Note: "n"}}},
|
||||
module: "cors",
|
||||
wantKey: "cors:http://x:null",
|
||||
wantSev: SeverityHigh,
|
||||
},
|
||||
{
|
||||
name: "public bucket is high",
|
||||
value: scan.CloudStorageResults{{BucketName: "b", IsPublic: true}},
|
||||
module: "cloudstorage",
|
||||
wantKey: "cloudstorage:b",
|
||||
wantSev: SeverityHigh,
|
||||
},
|
||||
{
|
||||
name: "header is recon info",
|
||||
value: scan.HeaderResults{{Name: "Server", Value: "nginx"}},
|
||||
module: "headers",
|
||||
wantKey: "headers:Server",
|
||||
wantSev: SeverityInfo,
|
||||
},
|
||||
{
|
||||
name: "vulnerable takeover is high",
|
||||
value: scan.SubdomainTakeoverResults{{Subdomain: "old.x.com", Vulnerable: true, Service: "GitHub Pages"}},
|
||||
module: "subdomain_takeover",
|
||||
wantKey: "subdomain_takeover:old.x.com",
|
||||
wantSev: SeverityHigh,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
findings := Flatten(target, tt.module, tt.value)
|
||||
if len(findings) != 1 {
|
||||
t.Fatalf("got %d findings, want 1", len(findings))
|
||||
}
|
||||
f := findings[0]
|
||||
if f.Key != tt.wantKey {
|
||||
t.Errorf("Key = %q, want %q", f.Key, tt.wantKey)
|
||||
}
|
||||
if f.Severity != tt.wantSev {
|
||||
t.Errorf("Severity = %v, want %v", f.Severity, tt.wantSev)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFlattenUnhandledTypeIsLoud asserts the fallback fires for a type Flatten
|
||||
// doesn't know - this is what makes the guard above meaningful.
|
||||
func TestFlattenUnhandledTypeIsLoud(t *testing.T) {
|
||||
type bogus struct{}
|
||||
findings := Flatten(target, "mystery", bogus{})
|
||||
if len(findings) != 1 {
|
||||
t.Fatalf("got %d findings, want 1 placeholder", len(findings))
|
||||
}
|
||||
if !strings.HasSuffix(findings[0].Key, keySep+"unhandled") {
|
||||
t.Errorf("unhandled type should key on :unhandled, got %q", findings[0].Key)
|
||||
}
|
||||
if findings[0].Severity != SeverityUnknown {
|
||||
t.Errorf("unhandled severity = %v, want SeverityUnknown", findings[0].Severity)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubdomainTakeoverSkipsSafe confirms a non-vulnerable cname produces no
|
||||
// finding; only the real takeover is a finding.
|
||||
func TestSubdomainTakeoverSkipsSafe(t *testing.T) {
|
||||
value := scan.SubdomainTakeoverResults{
|
||||
{Subdomain: "safe.x.com", Vulnerable: false},
|
||||
{Subdomain: "bad.x.com", Vulnerable: true, Service: "Heroku"},
|
||||
}
|
||||
findings := Flatten(target, "subdomain_takeover", value)
|
||||
if len(findings) != 1 {
|
||||
t.Fatalf("got %d findings, want 1 (only the vulnerable one)", len(findings))
|
||||
}
|
||||
if findings[0].Key != "subdomain_takeover:bad.x.com" {
|
||||
t.Errorf("Key = %q, want subdomain_takeover:bad.x.com", findings[0].Key)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeadProbeIsNotAFinding confirms a host that didn't answer yields nothing.
|
||||
func TestDeadProbeIsNotAFinding(t *testing.T) {
|
||||
findings := Flatten(target, "probe", &scan.ProbeResult{URL: "http://x", Alive: false})
|
||||
if len(findings) != 0 {
|
||||
t.Errorf("dead probe produced %d findings, want 0", len(findings))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package finding
|
||||
|
||||
import "testing"
|
||||
|
||||
// Line is the -silent wire format; its shape is frozen, so pin it.
|
||||
func TestFindingLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
f Finding
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "high severity",
|
||||
f: Finding{Target: "https://x.com", Module: "sql", Severity: SeverityHigh, Title: "admin panel"},
|
||||
want: "[high] https://x.com sql admin panel",
|
||||
},
|
||||
{
|
||||
name: "info recon",
|
||||
f: Finding{Target: "https://y.com", Module: "headers", Severity: SeverityInfo, Title: "Server"},
|
||||
want: "[info] https://y.com headers Server",
|
||||
},
|
||||
{
|
||||
name: "unknown severity",
|
||||
f: Finding{Target: "z.com", Module: "mystery", Severity: SeverityUnknown, Title: "?"},
|
||||
want: "[unknown] z.com mystery ?",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.f.Line(); got != tt.want {
|
||||
t.Errorf("Line() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package finding
|
||||
|
||||
import "strings"
|
||||
|
||||
// Severity is an ordered severity rank shared by every normalized finding.
|
||||
// the order matters: notify gates on a threshold and diff sorts by it, so the
|
||||
// underlying ints have to compare info < low < medium < high < critical.
|
||||
type Severity int
|
||||
|
||||
// severity ranks, lowest to highest. SeverityUnknown sorts below everything so
|
||||
// an unrecognized scanner string never silently outranks a real critical.
|
||||
const (
|
||||
SeverityUnknown Severity = iota
|
||||
SeverityInfo
|
||||
SeverityLow
|
||||
SeverityMedium
|
||||
SeverityHigh
|
||||
SeverityCritical
|
||||
)
|
||||
|
||||
// severityNames maps each rank to its canonical lowercase string. the wire
|
||||
// format scanners emit ("info"/"low"/...) round-trips through this table.
|
||||
var severityNames = map[Severity]string{
|
||||
SeverityUnknown: "unknown",
|
||||
SeverityInfo: "info",
|
||||
SeverityLow: "low",
|
||||
SeverityMedium: "medium",
|
||||
SeverityHigh: "high",
|
||||
SeverityCritical: "critical",
|
||||
}
|
||||
|
||||
// String renders the canonical lowercase name for the rank.
|
||||
func (s Severity) String() string {
|
||||
if name, ok := severityNames[s]; ok {
|
||||
return name
|
||||
}
|
||||
return severityNames[SeverityUnknown]
|
||||
}
|
||||
|
||||
// ParseSeverity maps a scanner's free-form severity string onto a rank. it's
|
||||
// case/space insensitive and folds the common synonyms ("informational",
|
||||
// "warning", "moderate") so the dozen scanners that each picked their own
|
||||
// spelling all land on the same ladder. an empty or unrecognized value is
|
||||
// SeverityUnknown rather than a guess.
|
||||
func ParseSeverity(raw string) Severity {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "critical":
|
||||
return SeverityCritical
|
||||
case "high":
|
||||
return SeverityHigh
|
||||
case "medium", "moderate", "warning":
|
||||
return SeverityMedium
|
||||
case "low":
|
||||
return SeverityLow
|
||||
case "info", "informational", "information", "none":
|
||||
return SeverityInfo
|
||||
default:
|
||||
return SeverityUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// AtLeast reports whether s is at or above threshold; notify uses it to drop
|
||||
// findings below the configured floor.
|
||||
func (s Severity) AtLeast(threshold Severity) bool {
|
||||
return s >= threshold
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package finding
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseSeverity(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want Severity
|
||||
}{
|
||||
{"critical", SeverityCritical},
|
||||
{"CRITICAL", SeverityCritical},
|
||||
{" high ", SeverityHigh},
|
||||
{"medium", SeverityMedium},
|
||||
{"moderate", SeverityMedium},
|
||||
{"warning", SeverityMedium},
|
||||
{"low", SeverityLow},
|
||||
{"info", SeverityInfo},
|
||||
{"informational", SeverityInfo},
|
||||
{"none", SeverityInfo},
|
||||
{"", SeverityUnknown},
|
||||
{"bogus", SeverityUnknown},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := ParseSeverity(tt.in); got != tt.want {
|
||||
t.Errorf("ParseSeverity(%q) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeverityOrdering(t *testing.T) {
|
||||
// the ladder must be strictly increasing for AtLeast/sort to behave.
|
||||
ordered := []Severity{
|
||||
SeverityUnknown, SeverityInfo, SeverityLow,
|
||||
SeverityMedium, SeverityHigh, SeverityCritical,
|
||||
}
|
||||
for i := 1; i < len(ordered); i++ {
|
||||
if ordered[i-1] >= ordered[i] {
|
||||
t.Errorf("severity ladder not increasing at %d: %v !< %v", i, ordered[i-1], ordered[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeverityAtLeast(t *testing.T) {
|
||||
tests := []struct {
|
||||
sev Severity
|
||||
threshold Severity
|
||||
want bool
|
||||
}{
|
||||
{SeverityHigh, SeverityMedium, true},
|
||||
{SeverityMedium, SeverityMedium, true},
|
||||
{SeverityLow, SeverityMedium, false},
|
||||
{SeverityCritical, SeverityInfo, true},
|
||||
{SeverityUnknown, SeverityInfo, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := tt.sev.AtLeast(tt.threshold); got != tt.want {
|
||||
t.Errorf("%v.AtLeast(%v) = %v, want %v", tt.sev, tt.threshold, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSeverityStringRoundTrip(t *testing.T) {
|
||||
// every named rank renders to a string ParseSeverity maps back to the same
|
||||
// rank, so the wire format is lossless for known severities.
|
||||
for _, sev := range []Severity{
|
||||
SeverityInfo, SeverityLow, SeverityMedium, SeverityHigh, SeverityCritical,
|
||||
} {
|
||||
if got := ParseSeverity(sev.String()); got != sev {
|
||||
t.Errorf("round-trip %v -> %q -> %v", sev, sev.String(), got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package httpx is the shared http layer every scanner talks through, so a
|
||||
// single Configure call wires proxy, custom headers, cookies and rate limiting
|
||||
// into every outbound request without touching scanner signatures.
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// allowed proxy schemes
|
||||
const (
|
||||
schemeHTTP = "http"
|
||||
schemeHTTPS = "https"
|
||||
schemeSOCKS5 = "socks5"
|
||||
)
|
||||
|
||||
// a header is "Key: Value"; this is the separator between the two halves.
|
||||
const headerSep = ": "
|
||||
|
||||
// burst lets the limiter absorb a small spike before pacing kicks in; a burst
|
||||
// equal to the per-second rate keeps the cap honest over any one-second window.
|
||||
const limiterBurstPerRate = 1
|
||||
|
||||
// transport pool tuning. go's default transport caps idle conns per host at 2
|
||||
// and reuse only kicks in once a response body is fully drained, so without
|
||||
// these a high thread count just thrashes the dialer instead of pooling.
|
||||
const (
|
||||
// total idle conns kept warm across every host we hit in a run.
|
||||
maxIdleConns = 512
|
||||
// floor for per-host idle conns so a single-target run still pools even
|
||||
// when the thread count is tiny.
|
||||
minIdleConnsPerHost = 8
|
||||
// how long an idle conn lingers before the pool reaps it.
|
||||
idleConnTimeout = 90 * time.Second
|
||||
// keepalive probe interval for live conns; mirrors go's default dialer so
|
||||
// the socks5 branch doesn't silently lose os-level keepalive.
|
||||
dialKeepAlive = 30 * time.Second
|
||||
// dial timeout for the socks5 branch; matches go's default dialer.
|
||||
dialTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
// drainCap bounds how much of an unread body DrainClose will copy before
|
||||
// closing; a body larger than this isn't worth slurping just to reuse the
|
||||
// conn, so we cap the read and let the conn be discarded instead.
|
||||
const drainCap = 16 << 10
|
||||
|
||||
// Options carries the runtime knobs that apply to every outbound request.
|
||||
// RateLimit is requests/sec (0 = unlimited); Headers are "Key: Value" strings.
|
||||
type Options struct {
|
||||
Proxy string
|
||||
Headers []string
|
||||
Cookie string
|
||||
UserAgent string
|
||||
RateLimit int
|
||||
// Threads is the scan worker count; it sizes the per-host idle pool so
|
||||
// concurrent workers hitting one target reuse conns instead of dialing fresh.
|
||||
Threads int
|
||||
}
|
||||
|
||||
// configured holds the package-level transport built once by Configure. nil
|
||||
// means Configure was never called, so Client falls back to a plain client.
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
configured http.RoundTripper
|
||||
)
|
||||
|
||||
// Configure builds the shared transport once at startup from opts. Calling it
|
||||
// again replaces the previous configuration.
|
||||
//
|
||||
//nolint:gocritic // signature is the package's stable startup api; called once.
|
||||
func Configure(opts Options) error {
|
||||
base, err := buildTransport(opts.Proxy, opts.Threads)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
headers, err := parseHeaders(opts.Headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var limiter *rate.Limiter
|
||||
if opts.RateLimit > 0 {
|
||||
limiter = rate.NewLimiter(rate.Limit(opts.RateLimit), opts.RateLimit*limiterBurstPerRate)
|
||||
}
|
||||
|
||||
rt := &roundTripper{
|
||||
base: base,
|
||||
headers: headers,
|
||||
cookie: opts.Cookie,
|
||||
userAgent: opts.UserAgent,
|
||||
limiter: limiter,
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
configured = rt
|
||||
mu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Client returns an http client wired to the configured transport. It works
|
||||
// before Configure is ever called (plain transport) so existing code and tests
|
||||
// behave unchanged. A zero timeout means no timeout, matching http.Client.
|
||||
func Client(timeout time.Duration) *http.Client {
|
||||
mu.RLock()
|
||||
rt := configured
|
||||
mu.RUnlock()
|
||||
|
||||
return &http.Client{Timeout: timeout, Transport: rt}
|
||||
}
|
||||
|
||||
// buildTransport clones the default transport, tunes its pool for the worker
|
||||
// count and applies the proxy. An empty proxy leaves the default behavior
|
||||
// (respects HTTP_PROXY env) intact.
|
||||
func buildTransport(proxyURL string, threads int) (*http.Transport, error) {
|
||||
tr, ok := http.DefaultTransport.(*http.Transport)
|
||||
if !ok {
|
||||
// unreachable in practice, but never trust an assertion silently.
|
||||
return nil, fmt.Errorf("default transport is not *http.Transport")
|
||||
}
|
||||
transport := tr.Clone()
|
||||
|
||||
// size the idle pool so every worker can keep its conn warm. per-host idle
|
||||
// must clear the thread count or workers past the cap re-dial each request;
|
||||
// MaxConnsPerHost stays 0 (unbounded) so the limiter, not the pool, paces us.
|
||||
transport.MaxIdleConns = maxIdleConns
|
||||
transport.MaxIdleConnsPerHost = idlePerHost(threads)
|
||||
transport.MaxConnsPerHost = 0
|
||||
transport.IdleConnTimeout = idleConnTimeout
|
||||
transport.ForceAttemptHTTP2 = true
|
||||
|
||||
if proxyURL == "" {
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse proxy url %q: %w", proxyURL, err)
|
||||
}
|
||||
|
||||
switch parsed.Scheme {
|
||||
case schemeHTTP, schemeHTTPS:
|
||||
transport.Proxy = http.ProxyURL(parsed)
|
||||
case schemeSOCKS5:
|
||||
// socks5 needs a custom dialer. proxy.SOCKS5 takes a forward dialer, so
|
||||
// hand it our own net.Dialer with keepalive set - the default
|
||||
// proxy.Direct has none, which would kill os-level conn pooling.
|
||||
fwd := &net.Dialer{Timeout: dialTimeout, KeepAlive: dialKeepAlive}
|
||||
dialer, err := proxy.SOCKS5("tcp", parsed.Host, nil, fwd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("socks5 proxy %q: %w", proxyURL, err)
|
||||
}
|
||||
ctxDialer, ok := dialer.(proxy.ContextDialer)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("socks5 proxy %q: dialer lacks context support", proxyURL)
|
||||
}
|
||||
transport.DialContext = ctxDialer.DialContext
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proxy scheme %q (want http/https/socks5)", parsed.Scheme)
|
||||
}
|
||||
|
||||
return transport, nil
|
||||
}
|
||||
|
||||
// idlePerHost picks the per-host idle pool size: at least the worker count so
|
||||
// no worker re-dials, never below the floor so a small thread count still pools.
|
||||
func idlePerHost(threads int) int {
|
||||
if threads < minIdleConnsPerHost {
|
||||
return minIdleConnsPerHost
|
||||
}
|
||||
return threads
|
||||
}
|
||||
|
||||
// DrainClose fully reads (up to drainCap) and closes resp.Body. go only returns
|
||||
// a conn to the idle pool when the body is read to EOF, so a caller that only
|
||||
// closes leaks the conn and forces a fresh dial next time. Call this instead of
|
||||
// a bare resp.Body.Close() to keep the pool warm. Safe on a nil response.
|
||||
func DrainClose(resp *http.Response) {
|
||||
if resp == nil || resp.Body == nil {
|
||||
return
|
||||
}
|
||||
// the read result is intentionally ignored: we're discarding the body and
|
||||
// about to close it, so a copy error changes nothing we can act on.
|
||||
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, drainCap))
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// parseHeaders splits each "Key: Value" entry on the first ": ". Entries
|
||||
// without the separator are rejected so a typo fails loud instead of silently.
|
||||
// The returned map is always non-nil so callers can range it unconditionally.
|
||||
func parseHeaders(raw []string) (map[string]string, error) {
|
||||
headers := make(map[string]string, len(raw))
|
||||
for i := 0; i < len(raw); i++ {
|
||||
key, value, ok := strings.Cut(raw[i], headerSep)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid header %q (want \"Key: Value\")", raw[i])
|
||||
}
|
||||
headers[key] = value
|
||||
}
|
||||
|
||||
return headers, nil
|
||||
}
|
||||
|
||||
// roundTripper paces and decorates each request before delegating to base.
|
||||
type roundTripper struct {
|
||||
base *http.Transport
|
||||
headers map[string]string
|
||||
cookie string
|
||||
userAgent string
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if rt.limiter != nil {
|
||||
if err := rt.limiter.Wait(req.Context()); err != nil {
|
||||
return nil, fmt.Errorf("rate limiter: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// only set what the caller hasn't already; a scanner that explicitly sets a
|
||||
// header (e.g. an api key) must win over the global default.
|
||||
for key, value := range rt.headers {
|
||||
if req.Header.Get(key) == "" {
|
||||
req.Header.Set(key, value)
|
||||
}
|
||||
}
|
||||
if rt.cookie != "" && req.Header.Get("Cookie") == "" {
|
||||
req.Header.Set("Cookie", rt.cookie)
|
||||
}
|
||||
if rt.userAgent != "" && req.Header.Get("User-Agent") == "" {
|
||||
req.Header.Set("User-Agent", rt.userAgent)
|
||||
}
|
||||
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
@@ -0,0 +1,491 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// resetConfig clears the package-level transport so each test starts clean.
|
||||
func resetConfig(t *testing.T) {
|
||||
t.Helper()
|
||||
mu.Lock()
|
||||
configured = nil
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
// captureServer records the headers of the last request it served.
|
||||
func captureServer(t *testing.T, seen *http.Header) *httptest.Server {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
*seen = r.Header.Clone()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
func get(t *testing.T, client *http.Client, url string) {
|
||||
t.Helper()
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
func TestClientBeforeConfigure(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
var seen http.Header
|
||||
srv := captureServer(t, &seen)
|
||||
|
||||
// a client must work with no Configure call so existing code is unaffected.
|
||||
get(t, Client(5*time.Second), srv.URL)
|
||||
|
||||
if seen == nil {
|
||||
t.Fatal("request never reached the server")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureHeadersAndCookie(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
opts Options
|
||||
wantKey string
|
||||
wantValue string
|
||||
}{
|
||||
{
|
||||
name: "custom header injected",
|
||||
opts: Options{Headers: []string{"X-Test: sif"}},
|
||||
wantKey: "X-Test",
|
||||
wantValue: "sif",
|
||||
},
|
||||
{
|
||||
name: "cookie injected",
|
||||
opts: Options{Cookie: "session=abc"},
|
||||
wantKey: "Cookie",
|
||||
wantValue: "session=abc",
|
||||
},
|
||||
{
|
||||
name: "user agent injected",
|
||||
opts: Options{UserAgent: "sif-scanner"},
|
||||
wantKey: "User-Agent",
|
||||
wantValue: "sif-scanner",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
if err := Configure(tt.opts); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
var seen http.Header
|
||||
srv := captureServer(t, &seen)
|
||||
get(t, Client(5*time.Second), srv.URL)
|
||||
|
||||
if got := seen.Get(tt.wantKey); got != tt.wantValue {
|
||||
t.Errorf("header %q = %q, want %q", tt.wantKey, got, tt.wantValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureHeaderDoesNotOverride(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
if err := Configure(Options{Headers: []string{"X-Test: global"}}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
var seen http.Header
|
||||
srv := captureServer(t, &seen)
|
||||
|
||||
// a caller that sets the header explicitly must win over the global default.
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
req.Header.Set("X-Test", "caller")
|
||||
resp, err := Client(5 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if got := seen.Get("X-Test"); got != "caller" {
|
||||
t.Errorf("X-Test = %q, want caller (caller value must not be overridden)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureInvalidHeader(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
// a header without ": " should fail loud rather than silently dropping.
|
||||
if err := Configure(Options{Headers: []string{"missing-separator"}}); err == nil {
|
||||
t.Fatal("expected error for malformed header, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureInvalidProxy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
proxy string
|
||||
}{
|
||||
{name: "unsupported scheme", proxy: "ftp://localhost:1080"},
|
||||
{name: "malformed url", proxy: "://nope"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resetConfig(t)
|
||||
if err := Configure(Options{Proxy: tt.proxy}); err == nil {
|
||||
t.Errorf("expected error for proxy %q, got nil", tt.proxy)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
const ratePerSec = 5
|
||||
if err := Configure(Options{RateLimit: ratePerSec}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
var seen http.Header
|
||||
srv := captureServer(t, &seen)
|
||||
client := Client(5 * time.Second)
|
||||
|
||||
// at 5 req/s the limiter starts with a full burst, so the first batch is
|
||||
// immediate and the next request must wait roughly one tick. fire burst+1
|
||||
// requests and assert the extra one forced a measurable delay.
|
||||
const requests = ratePerSec + 1
|
||||
start := time.Now()
|
||||
for i := 0; i < requests; i++ {
|
||||
get(t, client, srv.URL)
|
||||
}
|
||||
elapsed := time.Since(start)
|
||||
|
||||
// one request beyond the burst should cost about 1/rate; allow slack but
|
||||
// require a non-trivial delay so an unthrottled client fails this.
|
||||
minDelay := time.Second / ratePerSec / 2
|
||||
if elapsed < minDelay {
|
||||
t.Errorf("expected rate limiting to add >= %v of delay, got %v", minDelay, elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimitUnlimited(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
// RateLimit 0 means no limiter is installed; requests should fly through.
|
||||
if err := Configure(Options{RateLimit: 0}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
mu.RLock()
|
||||
rt, ok := configured.(*roundTripper)
|
||||
mu.RUnlock()
|
||||
if !ok {
|
||||
t.Fatal("configured transport is not *roundTripper")
|
||||
}
|
||||
if rt.limiter != nil {
|
||||
t.Error("expected no limiter when RateLimit is 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdlePerHost(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
threads int
|
||||
want int
|
||||
}{
|
||||
{name: "below floor clamps up", threads: 1, want: minIdleConnsPerHost},
|
||||
{name: "zero clamps up", threads: 0, want: minIdleConnsPerHost},
|
||||
{name: "at floor", threads: minIdleConnsPerHost, want: minIdleConnsPerHost},
|
||||
{name: "above floor passes through", threads: 64, want: 64},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := idlePerHost(tt.threads); got != tt.want {
|
||||
t.Errorf("idlePerHost(%d) = %d, want %d", tt.threads, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildTransportTuning(t *testing.T) {
|
||||
const threads = 32
|
||||
tr, err := buildTransport("", threads)
|
||||
if err != nil {
|
||||
t.Fatalf("buildTransport: %v", err)
|
||||
}
|
||||
|
||||
if tr.MaxIdleConns != maxIdleConns {
|
||||
t.Errorf("MaxIdleConns = %d, want %d", tr.MaxIdleConns, maxIdleConns)
|
||||
}
|
||||
if tr.MaxIdleConnsPerHost != threads {
|
||||
t.Errorf("MaxIdleConnsPerHost = %d, want %d", tr.MaxIdleConnsPerHost, threads)
|
||||
}
|
||||
if tr.MaxConnsPerHost != 0 {
|
||||
t.Errorf("MaxConnsPerHost = %d, want 0 (unbounded)", tr.MaxConnsPerHost)
|
||||
}
|
||||
if tr.IdleConnTimeout != idleConnTimeout {
|
||||
t.Errorf("IdleConnTimeout = %v, want %v", tr.IdleConnTimeout, idleConnTimeout)
|
||||
}
|
||||
if !tr.ForceAttemptHTTP2 {
|
||||
t.Error("ForceAttemptHTTP2 = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrainClose(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
// serve a body the caller never reads; DrainClose must drain it so the conn
|
||||
// is eligible for reuse rather than abandoned mid-stream.
|
||||
const body = "sif response body that the caller never reads"
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
io.WriteString(w, body)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
resp, err := Client(5 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do request: %v", err)
|
||||
}
|
||||
|
||||
DrainClose(resp)
|
||||
|
||||
// after DrainClose the body is closed; a further read must fail.
|
||||
if _, err := resp.Body.Read(make([]byte, 1)); err == nil {
|
||||
t.Error("expected read after DrainClose to fail on a closed body")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDrainCloseNil(t *testing.T) {
|
||||
// a nil response (e.g. an errored request) must not panic.
|
||||
DrainClose(nil)
|
||||
DrainClose(&http.Response{})
|
||||
}
|
||||
|
||||
// countConns wraps a test server with a ConnState hook that tallies how many
|
||||
// distinct tcp conns the server saw. distinct conns == failed reuse.
|
||||
func countConns(t *testing.T) (*httptest.Server, func() int) {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
conns = make(map[net.Conn]struct{})
|
||||
)
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
// always write a body so reuse depends on the caller draining it.
|
||||
io.WriteString(w, "ok")
|
||||
}))
|
||||
srv.Config.ConnState = func(c net.Conn, state http.ConnState) {
|
||||
if state != http.StateNew {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
conns[c] = struct{}{}
|
||||
mu.Unlock()
|
||||
}
|
||||
srv.Start()
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
return srv, func() int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return len(conns)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportReusesConnections(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
const (
|
||||
threads = 8
|
||||
requests = 30
|
||||
)
|
||||
if err := Configure(Options{Threads: threads}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
srv, distinct := countConns(t)
|
||||
|
||||
// fire N sequential requests through the tuned client, draining each body so
|
||||
// the conn returns to the pool. a working pool serves all of them on one conn.
|
||||
client := Client(5 * time.Second)
|
||||
for i := 0; i < requests; i++ {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("new request %d: %v", i, err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do request %d: %v", i, err)
|
||||
}
|
||||
DrainClose(resp)
|
||||
}
|
||||
|
||||
// sequential reuse should land on exactly one conn; allow a tiny margin for
|
||||
// the rare race where a conn is reaped between requests.
|
||||
const maxReuseConns = 2
|
||||
if got := distinct(); got > maxReuseConns {
|
||||
t.Errorf("tuned client opened %d conns for %d requests, want <= %d (pool not reusing)",
|
||||
got, requests, maxReuseConns)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBareClientDoesNotReuse(t *testing.T) {
|
||||
srv, distinct := countConns(t)
|
||||
|
||||
// the control: a bare DefaultTransport client whose caller closes but never
|
||||
// drains the body. go can't reuse a half-read conn, so each request dials
|
||||
// fresh - this is exactly the pre-tuning behavior we're fixing.
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
}
|
||||
|
||||
const requests = 30
|
||||
for i := 0; i < requests; i++ {
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("new request %d: %v", i, err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do request %d: %v", i, err)
|
||||
}
|
||||
// close without draining - the leak that kills reuse.
|
||||
resp.Body.Close()
|
||||
}
|
||||
|
||||
// most requests should have dialed a fresh conn. don't demand exactly N (the
|
||||
// scheduler occasionally reuses one), just that it's clearly not pooling.
|
||||
const minDistinct = requests / 2
|
||||
if got := distinct(); got < minDistinct {
|
||||
t.Errorf("bare client opened only %d conns for %d requests, want >= %d "+
|
||||
"(expected near-zero reuse without draining)", got, requests, minDistinct)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConnReuse contrasts the tuned, draining client against a bare client
|
||||
// that closes without draining. the reported conns/op metric is the distinct
|
||||
// tcp conns one pass of `requests` opened - tuned≈1, bare≈requests - so the
|
||||
// README can quote real before/after reuse numbers. the conn map is reset per
|
||||
// iteration so the metric stays a per-pass count and the bare path doesn't
|
||||
// accumulate b.N*requests live sockets and exhaust the ephemeral port range.
|
||||
//
|
||||
// run the bare sub-bench with a bounded -benchtime (e.g. -benchtime 5x): its
|
||||
// whole point is that it can't reuse, so a large b.N floods the local port
|
||||
// space with TIME_WAIT sockets. the tuned sub-bench reuses and runs unbounded.
|
||||
func BenchmarkConnReuse(b *testing.B) {
|
||||
const requests = 50
|
||||
|
||||
run := func(b *testing.B, drain bool, client *http.Client) {
|
||||
b.Helper()
|
||||
var (
|
||||
mu sync.Mutex
|
||||
conns = make(map[net.Conn]struct{})
|
||||
)
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
io.WriteString(w, strings.Repeat("x", 256))
|
||||
}))
|
||||
srv.Config.ConnState = func(c net.Conn, state http.ConnState) {
|
||||
if state != http.StateNew {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
conns[c] = struct{}{}
|
||||
mu.Unlock()
|
||||
}
|
||||
srv.Start()
|
||||
defer srv.Close()
|
||||
|
||||
var lastPass int
|
||||
b.ResetTimer()
|
||||
for n := 0; n < b.N; n++ {
|
||||
mu.Lock()
|
||||
conns = make(map[net.Conn]struct{})
|
||||
mu.Unlock()
|
||||
for i := 0; i < requests; i++ {
|
||||
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, srv.URL, http.NoBody)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
b.Fatalf("do: %v", err)
|
||||
}
|
||||
if drain {
|
||||
DrainClose(resp)
|
||||
} else {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
// close idle conns between passes so the bare client's per-pass
|
||||
// sockets land in TIME_WAIT and free up before the next pass.
|
||||
client.CloseIdleConnections()
|
||||
mu.Lock()
|
||||
lastPass = len(conns)
|
||||
mu.Unlock()
|
||||
}
|
||||
b.StopTimer()
|
||||
|
||||
// distinct conns for a single pass of `requests`.
|
||||
b.ReportMetric(float64(lastPass), "conns/op")
|
||||
}
|
||||
|
||||
b.Run("tuned-drain", func(b *testing.B) {
|
||||
resetBench()
|
||||
tr, err := buildTransport("", 8)
|
||||
if err != nil {
|
||||
b.Fatalf("buildTransport: %v", err)
|
||||
}
|
||||
run(b, true, &http.Client{Timeout: 5 * time.Second, Transport: tr})
|
||||
})
|
||||
|
||||
b.Run("bare-noDrain", func(b *testing.B) {
|
||||
run(b, false, &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
Transport: http.DefaultTransport.(*http.Transport).Clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// resetBench clears the package transport without a *testing.T for benchmarks.
|
||||
func resetBench() {
|
||||
mu.Lock()
|
||||
configured = nil
|
||||
mu.Unlock()
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -37,7 +37,7 @@ var defaultLogger = &Logger{
|
||||
// Init creates the log directory if it doesn't exist.
|
||||
func Init(dir string) error {
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.Mkdir(dir, 0o755); err != nil {
|
||||
if err := os.Mkdir(dir, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (l *Logger) getWriter(path string) (*bufio.Writer, error) {
|
||||
return w, nil
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -124,7 +124,10 @@ func (l *Logger) Close() error {
|
||||
|
||||
// CreateFile initializes a log file for the given URL and writes the header.
|
||||
func CreateFile(logFiles *[]string, url string, dir string) error {
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := url
|
||||
if _, after, ok := strings.Cut(url, "://"); ok {
|
||||
sanitizedURL = after
|
||||
}
|
||||
path := filepath.Join(dir, sanitizedURL+".log")
|
||||
|
||||
header := fmt.Sprintf(" _____________\n__________(_)__ __/\n__ ___/_ /__ /_ \n_(__ )_ / _ __/ \n/____/ /_/ /_/ \n\nsif log file for %s\nhttps://sif.sh\n\n", url)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runAnalyticsModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func analyticsExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestAnalyticsUIExposureModules(t *testing.T) {
|
||||
const metabase = "../../modules/recon/metabase-api-exposure.yaml"
|
||||
const zeppelin = "../../modules/recon/zeppelin-api-exposure.yaml"
|
||||
const jupyter = "../../modules/recon/jupyter-api-exposure.yaml"
|
||||
|
||||
metabaseProps := `{"engines":{"postgres":{"driver-name":"PostgreSQL"}},` +
|
||||
`"setup-token":"245f5f7c-8f0b-4c20-9a1e-6b2d7e1f0a33","anon-tracking-enabled":true,` +
|
||||
`"available-locales":[["en","English"]],"password-complexity":{"total":6},` +
|
||||
`"version":{"date":"2023-10-01","tag":"v0.47.2","branch":"release-x.47.x","hash":"abc1234"}}`
|
||||
|
||||
zeppelinVersion := `{"status":"OK","message":"Zeppelin version",` +
|
||||
`"body":{"version":"0.10.1","git-commit-id":"a1b2c3d4e5","git-timestamp":"2022-01-15 10:00:00"}}`
|
||||
|
||||
jupyterStatus := `{"started":"2024-01-01T00:00:00.000000Z",` +
|
||||
`"last_activity":"2024-01-01T01:23:45.000000Z","connections":2,"kernels":3}`
|
||||
|
||||
t.Run("an exposed metabase properties api is flagged and versioned", func(t *testing.T) {
|
||||
res := runAnalyticsModule(t, metabase, 200, metabaseProps)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a metabase finding")
|
||||
}
|
||||
if v := analyticsExtract(res, "metabase_version"); v != "v0.47.2" {
|
||||
t.Errorf("metabase_version=%q, want v0.47.2", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed zeppelin server is flagged and versioned", func(t *testing.T) {
|
||||
res := runAnalyticsModule(t, zeppelin, 200, zeppelinVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a zeppelin finding")
|
||||
}
|
||||
if v := analyticsExtract(res, "zeppelin_version"); v != "0.10.1" {
|
||||
t.Errorf("zeppelin_version=%q, want 0.10.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed jupyter status api is flagged with the kernel count", func(t *testing.T) {
|
||||
res := runAnalyticsModule(t, jupyter, 200, jupyterStatus)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a jupyter finding")
|
||||
}
|
||||
if v := analyticsExtract(res, "jupyter_active_kernels"); v != "3" {
|
||||
t.Errorf("jupyter_active_kernels=%q, want 3", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a live metabase token without the tracking setting is not flagged", func(t *testing.T) {
|
||||
body := `{"setup-token":"245f5f7c-8f0b-4c20-9a1e-6b2d7e1f0a33","name":"app"}`
|
||||
if res := runAnalyticsModule(t, metabase, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a setup token alone should not match metabase, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a metabase tracking setting without a setup token is not flagged", func(t *testing.T) {
|
||||
body := `{"anon-tracking-enabled":true,"name":"app"}`
|
||||
if res := runAnalyticsModule(t, metabase, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a tracking setting alone should not match metabase, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a patched metabase with a null setup token is not flagged", func(t *testing.T) {
|
||||
body := `{"setup-token":null,"anon-tracking-enabled":true,` +
|
||||
`"version":{"tag":"v0.47.2"}}`
|
||||
if res := runAnalyticsModule(t, metabase, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a null setup token should not match metabase, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a zeppelin banner without a git commit id is not flagged", func(t *testing.T) {
|
||||
body := `{"status":"OK","message":"Zeppelin version","body":{}}`
|
||||
if res := runAnalyticsModule(t, zeppelin, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a banner alone should not match zeppelin, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a git commit id without the zeppelin banner is not flagged", func(t *testing.T) {
|
||||
body := `{"git-commit-id":"a1b2c3d","name":"app"}`
|
||||
if res := runAnalyticsModule(t, zeppelin, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a commit id alone should not match zeppelin, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a jupyter status without a kernels field is not flagged", func(t *testing.T) {
|
||||
body := `{"started":"2024-01-01T00:00:00Z","last_activity":"2024-01-01T01:00:00Z","connections":2}`
|
||||
if res := runAnalyticsModule(t, jupyter, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a status without kernels should not match jupyter, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a jupyter status without a connections field is not flagged", func(t *testing.T) {
|
||||
body := `{"started":"2024-01-01T00:00:00Z","last_activity":"2024-01-01T01:00:00Z","kernels":3}`
|
||||
if res := runAnalyticsModule(t, jupyter, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a status without connections should not match jupyter, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic version json is not an analytics service", func(t *testing.T) {
|
||||
body := `{"version":"1.0.0","name":"app"}`
|
||||
for _, file := range []string{metabase, zeppelin, jupyter} {
|
||||
if res := runAnalyticsModule(t, file, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a generic version should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{metabase, zeppelin, jupyter} {
|
||||
if res := runAnalyticsModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{metabase, zeppelin, jupyter} {
|
||||
if res := runAnalyticsModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runAppCfgModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func appCfgExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestAppConfigExposureModules(t *testing.T) {
|
||||
const spring = "../../modules/recon/spring-application-config-exposure.yaml"
|
||||
const appsettings = "../../modules/recon/appsettings-exposure.yaml"
|
||||
const wpconfig = "../../modules/recon/wp-config-backup-exposure.yaml"
|
||||
|
||||
springProps := "spring.application.name=billing\n" +
|
||||
"spring.datasource.url=jdbc:mysql://db.internal:3306/billing\n" +
|
||||
"spring.datasource.username=app\nspring.datasource.password=s3cr3tP@ss\n" +
|
||||
"spring.jpa.hibernate.ddl-auto=update\nserver.port=8080\n"
|
||||
|
||||
springYaml := "spring:\n datasource:\n url: jdbc:postgresql://pg.internal:5432/app\n" +
|
||||
" username: app\n password: hunter2\nserver:\n port: 8443\n"
|
||||
|
||||
appSettings := `{` + "\n" +
|
||||
` "Logging": { "LogLevel": { "Default": "Information" } },` + "\n" +
|
||||
` "ConnectionStrings": {` + "\n" +
|
||||
` "DefaultConnection": "Server=db;Database=app;User Id=sa;Password=P@ssw0rd;"` + "\n" +
|
||||
` },` + "\n" +
|
||||
` "AllowedHosts": "*"` + "\n}"
|
||||
|
||||
wpConfig := "<?php\ndefine( 'DB_NAME', 'wordpress' );\ndefine( 'DB_USER', 'wp' );\n" +
|
||||
"define( 'DB_PASSWORD', 'Tr0ub4dor&3' );\ndefine( 'DB_HOST', 'localhost' );\n" +
|
||||
"$table_prefix = 'wp_';\n"
|
||||
|
||||
t.Run("a spring properties file leaks the jdbc url", func(t *testing.T) {
|
||||
res := runAppCfgModule(t, spring, 200, springProps)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a spring config finding")
|
||||
}
|
||||
if v := appCfgExtract(res, "jdbc_url"); v != "jdbc:mysql://db.internal:3306/billing" {
|
||||
t.Errorf("jdbc_url=%q, want the mysql url", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a spring yaml file also matches and names the jdbc url", func(t *testing.T) {
|
||||
res := runAppCfgModule(t, spring, 200, springYaml)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a spring config finding for yaml")
|
||||
}
|
||||
if v := appCfgExtract(res, "jdbc_url"); v != "jdbc:postgresql://pg.internal:5432/app" {
|
||||
t.Errorf("jdbc_url=%q, want the postgres url", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an appsettings json leaks the connection string", func(t *testing.T) {
|
||||
res := runAppCfgModule(t, appsettings, 200, appSettings)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an appsettings finding")
|
||||
}
|
||||
want := "Server=db;Database=app;User Id=sa;Password=P@ssw0rd;"
|
||||
if v := appCfgExtract(res, "connection_string"); v != want {
|
||||
t.Errorf("connection_string=%q, want %q", v, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a wp-config backup leaks the database password", func(t *testing.T) {
|
||||
res := runAppCfgModule(t, wpconfig, 200, wpConfig)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a wp-config finding")
|
||||
}
|
||||
if v := appCfgExtract(res, "db_password"); v != "Tr0ub4dor&3" {
|
||||
t.Errorf("db_password=%q, want Tr0ub4dor&3", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a spring config with no credential is not flagged", func(t *testing.T) {
|
||||
body := "spring.application.name=app\nserver.port=8080\n"
|
||||
if res := runAppCfgModule(t, spring, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a credential-free config should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a spring config inside an html page is not flagged", func(t *testing.T) {
|
||||
body := "<!DOCTYPE html><html><body><pre>spring.datasource.password=x</pre></body></html>"
|
||||
if res := runAppCfgModule(t, spring, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html page should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an appsettings without a connection string is not flagged", func(t *testing.T) {
|
||||
body := `{"Logging":{"LogLevel":{"Default":"Information"}},"AllowedHosts":"*"}`
|
||||
if res := runAppCfgModule(t, appsettings, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a config without a connection string should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an appsettings with no password is not a credential leak", func(t *testing.T) {
|
||||
body := `{"ConnectionStrings":{"Db":"Server=db;Database=app;Integrated Security=true;"}}`
|
||||
if res := runAppCfgModule(t, appsettings, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a passwordless connection string should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an appsettings password outside a connection strings section is not flagged", func(t *testing.T) {
|
||||
body := `{"Smtp":{"Host":"Server=mail;Password=relaypass;"}}`
|
||||
if res := runAppCfgModule(t, appsettings, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a password outside ConnectionStrings should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("prose that names the wp-config password is not a backup", func(t *testing.T) {
|
||||
body := "set the DB_PASSWORD env var before running the installer"
|
||||
if res := runAppCfgModule(t, wpconfig, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("prose naming DB_PASSWORD should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a wp-config shown in an html page is not flagged", func(t *testing.T) {
|
||||
body := "<html><head><title>setup</title></head><body>define( 'DB_PASSWORD', 'x' ); DB_NAME</body></html>"
|
||||
if res := runAppCfgModule(t, wpconfig, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html page should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{spring, appsettings, wpconfig} {
|
||||
if res := runAppCfgModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{spring, appsettings, wpconfig} {
|
||||
if res := runAppCfgModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runArgocdModule(t *testing.T, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule("../../modules/recon/argocd-api-exposure.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("parse argocd module: %v", err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute argocd module: %v", err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func argocdExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestArgocdExposureModule(t *testing.T) {
|
||||
argocdVersion := `{"Version":"v2.9.3+a1b2c3d","BuildDate":"2024-01-15T12:00:00Z","GitCommit":"a1b2c3d",` +
|
||||
`"GitTreeState":"clean","GoVersion":"go1.21.5","Compiler":"gc","Platform":"linux/amd64",` +
|
||||
`"KustomizeVersion":"v5.2.1 2023-10-19","HelmVersion":"v3.13.2+gadc03ef",` +
|
||||
`"KubectlVersion":"v0.26.11","JsonnetVersion":"v0.20.0"}`
|
||||
|
||||
t.Run("an exposed argocd version endpoint is flagged and versioned", func(t *testing.T) {
|
||||
res := runArgocdModule(t, 200, argocdVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an argocd finding")
|
||||
}
|
||||
if v := argocdExtract(res, "argocd_version"); v != "v2.9.3+a1b2c3d" {
|
||||
t.Errorf("argocd_version=%q, want v2.9.3+a1b2c3d", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an argocd kustomize version without a helm version is not flagged", func(t *testing.T) {
|
||||
body := `{"Version":"v2.9.3","KustomizeVersion":"v5.2.1 2023-10-19"}`
|
||||
if res := runArgocdModule(t, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a kustomize version alone should not match argocd, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an argocd helm version without a kustomize version is not flagged", func(t *testing.T) {
|
||||
body := `{"Version":"v2.9.3","HelmVersion":"v3.13.2+gadc03ef"}`
|
||||
if res := runArgocdModule(t, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a helm version alone should not match argocd, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic version endpoint is not argocd", func(t *testing.T) {
|
||||
body := `{"Version":"v1.0.0","GitCommit":"abc"}`
|
||||
if res := runArgocdModule(t, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic version json should not match argocd, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
if res := runArgocdModule(t, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 body should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
if res := runArgocdModule(t, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
func reqURLs(reqs []*httpRequest) []string {
|
||||
urls := make([]string, len(reqs))
|
||||
for i, r := range reqs {
|
||||
urls[i] = r.URL
|
||||
}
|
||||
sort.Strings(urls)
|
||||
return urls
|
||||
}
|
||||
|
||||
func TestGenerateHTTPRequestsAttack(t *testing.T) {
|
||||
const target = "http://t"
|
||||
paths2 := []string{"{{BaseURL}}/a?x={{payload}}", "{{BaseURL}}/b?x={{payload}}"}
|
||||
pay2 := []string{"1", "2"}
|
||||
cross := []string{"http://t/a?x=1", "http://t/a?x=2", "http://t/b?x=1", "http://t/b?x=2"}
|
||||
paired := []string{"http://t/a?x=1", "http://t/b?x=2"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
paths []string
|
||||
payloads []string
|
||||
attack string
|
||||
want []string
|
||||
}{
|
||||
{"clusterbomb default crosses all", paths2, pay2, "", cross},
|
||||
{"clusterbomb explicit crosses all", paths2, pay2, "clusterbomb", cross},
|
||||
{"pitchfork pairs by index", paths2, pay2, "pitchfork", paired},
|
||||
{"pitchfork stops at fewer payloads", append(paths2, "{{BaseURL}}/c?x={{payload}}"), pay2, "pitchfork", paired},
|
||||
{"pitchfork stops at fewer paths", paths2, []string{"1", "2", "3"}, "pitchfork", paired},
|
||||
{"attack is case insensitive", paths2, pay2, "Pitchfork", paired},
|
||||
{"no payloads ignores attack", []string{"{{BaseURL}}/a", "{{BaseURL}}/b"}, nil, "pitchfork", []string{"http://t/a", "http://t/b"}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &HTTPConfig{Paths: tt.paths, Payloads: tt.payloads, Attack: tt.attack}
|
||||
got := reqURLs(generateHTTPRequests(target, cfg))
|
||||
want := append([]string(nil), tt.want...)
|
||||
sort.Strings(want)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("attack %q:\n got %v\nwant %v", tt.attack, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAttack(t *testing.T) {
|
||||
for _, ok := range []string{"", "clusterbomb", "pitchfork", "Pitchfork", "CLUSTERBOMB"} {
|
||||
if err := validateAttack(ok); err != nil {
|
||||
t.Errorf("validateAttack(%q) = %v, want nil", ok, err)
|
||||
}
|
||||
}
|
||||
for _, bad := range []string{"sniper", "batteringram", "bogus"} {
|
||||
if err := validateAttack(bad); err == nil {
|
||||
t.Errorf("validateAttack(%q) = nil, want error", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAttackValidation(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(name, body string) string {
|
||||
p := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
good := write("good.yaml", "id: ok\ntype: http\nhttp:\n attack: pitchfork\n paths: [\"{{BaseURL}}/\"]\n")
|
||||
if _, err := ParseYAMLModule(good); err != nil {
|
||||
t.Fatalf("valid attack rejected: %v", err)
|
||||
}
|
||||
|
||||
bad := write("bad.yaml", "id: bad\ntype: http\nhttp:\n attack: sniper\n paths: [\"{{BaseURL}}/\"]\n")
|
||||
if _, err := ParseYAMLModule(bad); err == nil {
|
||||
t.Fatal("invalid attack accepted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteHTTPModulePitchfork drives the executor end to end and confirms
|
||||
// pitchfork only fires the index-paired requests, not the full cross product.
|
||||
func TestExecuteHTTPModulePitchfork(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
var hits []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
hits = append(hits, r.URL.Path+"?"+r.URL.RawQuery)
|
||||
mu.Unlock()
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
def := &YAMLModule{
|
||||
ID: "pf",
|
||||
Type: TypeHTTP,
|
||||
HTTP: &HTTPConfig{
|
||||
Attack: "pitchfork",
|
||||
Paths: []string{"{{BaseURL}}/a?x={{payload}}", "{{BaseURL}}/b?x={{payload}}"},
|
||||
Payloads: []string{"1", "2"},
|
||||
Matchers: []Matcher{{Type: "word", Part: "body", Words: []string{"ok"}}},
|
||||
},
|
||||
}
|
||||
|
||||
opts := Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)}
|
||||
if _, err := ExecuteHTTPModule(context.Background(), srv.URL, def, opts); err != nil {
|
||||
t.Fatalf("ExecuteHTTPModule: %v", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
got := append([]string(nil), hits...)
|
||||
mu.Unlock()
|
||||
sort.Strings(got)
|
||||
want := []string{"/a?x=1", "/b?x=2"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("pitchfork hit %v, want %v (clusterbomb would also hit /a?x=2 and /b?x=1)", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runBigDataModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func bigDataExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestBigDataAPIExposureModules(t *testing.T) {
|
||||
const solr = "../../modules/recon/solr-api-exposure.yaml"
|
||||
const spark = "../../modules/recon/spark-api-exposure.yaml"
|
||||
const hadoop = "../../modules/recon/hadoop-yarn-api-exposure.yaml"
|
||||
|
||||
solrSystem := `{"responseHeader":{"status":0,"QTime":15},"mode":"std",` +
|
||||
`"solr_home":"/var/solr/data","lucene":{"solr-spec-version":"9.4.0",` +
|
||||
`"solr-impl-version":"9.4.0","lucene-spec-version":"9.8.0","lucene-impl-version":"9.8.0"},` +
|
||||
`"jvm":{"version":"17.0.9"}}`
|
||||
|
||||
sparkState := `{"url":"spark://master:7077","workers":[{"id":"worker-1","host":"10.0.0.5"}],` +
|
||||
`"aliveworkers":2,"cores":8,"coresused":0,"memory":15360,"activeapps":[],` +
|
||||
`"completedapps":[],"status":"ALIVE"}`
|
||||
|
||||
hadoopInfo := `{"clusterInfo":{"id":1700000000000,"startedOn":1700000000000,"state":"STARTED",` +
|
||||
`"haState":"ACTIVE","resourceManagerVersion":"3.3.6","resourceManagerBuildVersion":"3.3.6 from abc",` +
|
||||
`"hadoopVersion":"3.3.6","hadoopBuildVersion":"3.3.6 from abc","hadoopVersionBuiltOn":"2023-06-18"}}`
|
||||
|
||||
t.Run("an exposed solr admin api is flagged and versioned", func(t *testing.T) {
|
||||
res := runBigDataModule(t, solr, 200, solrSystem)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a solr finding")
|
||||
}
|
||||
if v := bigDataExtract(res, "solr_version"); v != "9.4.0" {
|
||||
t.Errorf("solr_version=%q, want 9.4.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed spark master leaks its url", func(t *testing.T) {
|
||||
res := runBigDataModule(t, spark, 200, sparkState)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a spark finding")
|
||||
}
|
||||
if v := bigDataExtract(res, "spark_master_url"); v != "spark://master:7077" {
|
||||
t.Errorf("spark_master_url=%q, want spark://master:7077", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed hadoop yarn api is flagged and versioned", func(t *testing.T) {
|
||||
res := runBigDataModule(t, hadoop, 200, hadoopInfo)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a hadoop finding")
|
||||
}
|
||||
if v := bigDataExtract(res, "hadoop_version"); v != "3.3.6" {
|
||||
t.Errorf("hadoop_version=%q, want 3.3.6", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a solr spec version without a solr home is not solr", func(t *testing.T) {
|
||||
body := `{"lucene":{"solr-spec-version":"9.4.0"},"name":"otherservice"}`
|
||||
if res := runBigDataModule(t, solr, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("spec version alone should not match solr, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a solr home without a spec version is not solr", func(t *testing.T) {
|
||||
body := `{"solr_home":"/var/solr/data","mode":"std"}`
|
||||
if res := runBigDataModule(t, solr, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("solr home alone should not match solr, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a spark url without alive workers is not flagged", func(t *testing.T) {
|
||||
body := `{"url":"spark://master:7077","workers":[],"status":"ALIVE"}`
|
||||
if res := runBigDataModule(t, spark, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a spark url alone should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("alive workers behind a non spark url is not flagged", func(t *testing.T) {
|
||||
body := `{"url":"http://internal:8080","aliveworkers":2}`
|
||||
if res := runBigDataModule(t, spark, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non spark url should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a cluster info without a resource manager version is not hadoop", func(t *testing.T) {
|
||||
body := `{"clusterInfo":{"id":1,"state":"STARTED","hadoopVersion":"3.3.6"}}`
|
||||
if res := runBigDataModule(t, hadoop, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("cluster info alone should not match hadoop, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a resource manager version without a cluster info is not hadoop", func(t *testing.T) {
|
||||
body := `{"resourceManagerVersion":"3.3.6","app":"custom"}`
|
||||
if res := runBigDataModule(t, hadoop, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("rm version alone should not match hadoop, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic json endpoint is not a spark master", func(t *testing.T) {
|
||||
body := `{"url":"http://app","workers":5,"name":"myservice"}`
|
||||
if res := runBigDataModule(t, spark, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic json should not match spark, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{solr, spark, hadoop} {
|
||||
if res := runBigDataModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{solr, spark, hadoop} {
|
||||
if res := runBigDataModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runPipelineModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func pipelineExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestDataPipelineAPIExposureModules(t *testing.T) {
|
||||
const airflow = "../../modules/recon/airflow-api-exposure.yaml"
|
||||
const flink = "../../modules/recon/flink-api-exposure.yaml"
|
||||
const kafka = "../../modules/recon/kafka-connect-api-exposure.yaml"
|
||||
|
||||
airflowHealth := `{"metadatabase":{"status":"healthy"},"scheduler":{"status":"healthy",` +
|
||||
`"latest_scheduler_heartbeat":"2023-09-13T09:35:49.123456+00:00"}}`
|
||||
|
||||
flinkOverview := `{"taskmanagers":1,"slots-total":4,"slots-available":4,"jobs-running":0,` +
|
||||
`"jobs-finished":2,"jobs-cancelled":0,"jobs-failed":0,"flink-version":"1.17.1","flink-commit":"2750d5c"}`
|
||||
|
||||
kafkaConnect := `{"version":"3.5.0","commit":"c97b88d5db4de28d","kafka_cluster_id":"M_oad8FjQ1eMShri6_jjQg"}`
|
||||
|
||||
t.Run("an exposed airflow health endpoint is flagged", func(t *testing.T) {
|
||||
res := runPipelineModule(t, airflow, 200, airflowHealth)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an airflow finding")
|
||||
}
|
||||
if v := pipelineExtract(res, "airflow_scheduler_heartbeat"); v != "2023-09-13T09:35:49.123456+00:00" {
|
||||
t.Errorf("airflow_scheduler_heartbeat=%q, want the heartbeat timestamp", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed flink dashboard is flagged and versioned", func(t *testing.T) {
|
||||
res := runPipelineModule(t, flink, 200, flinkOverview)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a flink finding")
|
||||
}
|
||||
if v := pipelineExtract(res, "flink_version"); v != "1.17.1" {
|
||||
t.Errorf("flink_version=%q, want 1.17.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed kafka connect api is flagged and versioned", func(t *testing.T) {
|
||||
res := runPipelineModule(t, kafka, 200, kafkaConnect)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a kafka connect finding")
|
||||
}
|
||||
if v := pipelineExtract(res, "kafka_version"); v != "3.5.0" {
|
||||
t.Errorf("kafka_version=%q, want 3.5.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an airflow metadatabase without a scheduler is not flagged", func(t *testing.T) {
|
||||
body := `{"metadatabase":{"status":"healthy"}}`
|
||||
if res := runPipelineModule(t, airflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("metadatabase alone should not match airflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an airflow scheduler without a metadatabase is not flagged", func(t *testing.T) {
|
||||
body := `{"scheduler":{"status":"healthy","latest_scheduler_heartbeat":"2023-09-13T09:35:49.123456+00:00"}}`
|
||||
if res := runPipelineModule(t, airflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("scheduler alone should not match airflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a flink version without a slot total is not flagged", func(t *testing.T) {
|
||||
body := `{"flink-version":"1.17.1","taskmanagers":1}`
|
||||
if res := runPipelineModule(t, flink, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("flink version alone should not match flink, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a slot total without a flink version is not flagged", func(t *testing.T) {
|
||||
body := `{"slots-total":4,"jobs-running":0}`
|
||||
if res := runPipelineModule(t, flink, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a slot total alone should not match flink, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a kafka cluster id without a version is not flagged", func(t *testing.T) {
|
||||
body := `{"kafka_cluster_id":"M_oad8FjQ1eMShri6_jjQg","commit":"abc"}`
|
||||
if res := runPipelineModule(t, kafka, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a cluster id alone should not match kafka connect, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a version without a kafka cluster id is not flagged", func(t *testing.T) {
|
||||
body := `{"version":"3.5.0","name":"someservice"}`
|
||||
if res := runPipelineModule(t, kafka, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a version alone should not match kafka connect, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic health json is not airflow", func(t *testing.T) {
|
||||
body := `{"status":"UP","components":{"db":{"status":"UP"}}}`
|
||||
if res := runPipelineModule(t, airflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic health should not match airflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{airflow, flink, kafka} {
|
||||
if res := runPipelineModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{airflow, flink, kafka} {
|
||||
if res := runPipelineModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDBFileModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func dbFileExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestDatabaseFileExposureModules(t *testing.T) {
|
||||
const sqlDump = "../../modules/recon/sql-dump-exposure.yaml"
|
||||
const sqlite = "../../modules/recon/sqlite-database-exposure.yaml"
|
||||
const redis = "../../modules/recon/redis-dump-exposure.yaml"
|
||||
|
||||
mysqldump := "-- MySQL dump 10.13 Distrib 8.0.32, for Linux (x86_64)\n--\n" +
|
||||
"-- Host: localhost Database: appdb\n--\n-- Server version\t8.0.32\n\n" +
|
||||
"DROP TABLE IF EXISTS `users`;\nCREATE TABLE `users` (\n" +
|
||||
" `id` int NOT NULL AUTO_INCREMENT,\n `email` varchar(255) DEFAULT NULL,\n" +
|
||||
" PRIMARY KEY (`id`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n" +
|
||||
"INSERT INTO `users` VALUES (1,'admin@x.com');\n"
|
||||
|
||||
pgdump := "--\n-- PostgreSQL database dump\n--\n\nSET statement_timeout = 0;\n" +
|
||||
"CREATE TABLE public.accounts (\n id integer NOT NULL,\n email text\n);\n" +
|
||||
"COPY public.accounts (id, email) FROM stdin;\n1\tadmin@x.com\n\\.\n"
|
||||
|
||||
sqliteFile := "SQLite format 3\x00" + strings.Repeat("\x00", 84) +
|
||||
"\x05\x00CREATE TABLE users(id INTEGER PRIMARY KEY, email TEXT, password TEXT)\x00"
|
||||
|
||||
redisDump := "REDIS0011\xfa\x09redis-ver\x055.0.7\xfa\x0aredis-bits\xc0@\xfe\x00\xfb\x02\x00" +
|
||||
"\x03key\x05value\xff\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
t.Run("a mysqldump leaks the dumped table", func(t *testing.T) {
|
||||
res := runDBFileModule(t, sqlDump, 200, mysqldump)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a sql dump finding")
|
||||
}
|
||||
if v := dbFileExtract(res, "dump_table"); v != "users" {
|
||||
t.Errorf("dump_table=%q, want users", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a postgresql dump also matches and names its table", func(t *testing.T) {
|
||||
res := runDBFileModule(t, sqlDump, 200, pgdump)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a sql dump finding for pg_dump")
|
||||
}
|
||||
if v := dbFileExtract(res, "dump_table"); v != "accounts" {
|
||||
t.Errorf("dump_table=%q, want accounts", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a sqlite database file leaks its schema table", func(t *testing.T) {
|
||||
res := runDBFileModule(t, sqlite, 200, sqliteFile)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a sqlite finding")
|
||||
}
|
||||
if v := dbFileExtract(res, "table_name"); v != "users" {
|
||||
t.Errorf("table_name=%q, want users", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a redis rdb snapshot leaks its format version", func(t *testing.T) {
|
||||
res := runDBFileModule(t, redis, 200, redisDump)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a redis rdb finding")
|
||||
}
|
||||
if v := dbFileExtract(res, "rdb_version"); v != "0011" {
|
||||
t.Errorf("rdb_version=%q, want 0011", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sql shown inside an html page is not a dump", func(t *testing.T) {
|
||||
body := "<!DOCTYPE html><html><head><title>SQL tutorial</title></head><body>" +
|
||||
"<pre>DROP TABLE IF EXISTS users; CREATE TABLE users (id int); INSERT INTO users VALUES (1);</pre>" +
|
||||
"</body></html>"
|
||||
if res := runDBFileModule(t, sqlDump, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html tutorial should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a sql file with no dump idiom is not flagged", func(t *testing.T) {
|
||||
body := "-- migration notes\nSELECT id FROM users WHERE active = 1;\n"
|
||||
if res := runDBFileModule(t, sqlDump, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare select should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a page that names the sqlite format is not the file", func(t *testing.T) {
|
||||
body := "This page documents the SQLite format 3 on-disk structure for readers."
|
||||
if res := runDBFileModule(t, sqlite, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("prose about sqlite should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a page that names redis is not an rdb snapshot", func(t *testing.T) {
|
||||
body := "redis-server is running on this host as the REDIS cache backend."
|
||||
if res := runDBFileModule(t, redis, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("prose about redis should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the sqlite magic only counts at the start of the file", func(t *testing.T) {
|
||||
body := "<pre>hexdump of a header: " + sqliteFile + "</pre>"
|
||||
if res := runDBFileModule(t, sqlite, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an embedded sqlite header should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the rdb magic only counts at the start of the file", func(t *testing.T) {
|
||||
body := "log line: loaded snapshot " + redisDump
|
||||
if res := runDBFileModule(t, redis, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an embedded rdb header should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{sqlDump, sqlite, redis} {
|
||||
if res := runDBFileModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{sqlDump, sqlite, redis} {
|
||||
if res := runDBFileModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDeployModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func deployExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestDeployConfigExposureModules(t *testing.T) {
|
||||
const vscode = "../../modules/recon/vscode-sftp-exposure.yaml"
|
||||
const sublime = "../../modules/recon/sublime-sftp-exposure.yaml"
|
||||
const ftpconfig = "../../modules/recon/ftpconfig-exposure.yaml"
|
||||
|
||||
t.Run("vscode sftp config leaks the deploy host", func(t *testing.T) {
|
||||
body := `{"name":"prod","host":"deploy.example.com","protocol":"sftp",` +
|
||||
`"username":"root","password":"s3cr3t","remotePath":"/var/www","uploadOnSave":true}`
|
||||
res := runDeployModule(t, vscode, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a vscode sftp finding")
|
||||
}
|
||||
if v := deployExtract(res, "remote_host"); v != "deploy.example.com" {
|
||||
t.Errorf("remote_host=%q, want deploy.example.com", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("vscode sftp config with key auth still flags and extracts the host", func(t *testing.T) {
|
||||
body := `{"host":"key.example.com","protocol":"sftp",` +
|
||||
`"username":"deploy","privateKeyPath":"~/.ssh/id_rsa","uploadOnSave":true}`
|
||||
res := runDeployModule(t, vscode, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a vscode sftp finding for a key-auth config")
|
||||
}
|
||||
if v := deployExtract(res, "remote_host"); v != "key.example.com" {
|
||||
t.Errorf("remote_host=%q, want key.example.com", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("sublime sftp config leaks the deploy host", func(t *testing.T) {
|
||||
body := `{"type":"sftp","host":"sftp.example.org","user":"www","password":"hunter2",` +
|
||||
`"remote_path":"/srv","upload_on_save":true,"sync_down_on_open":false}`
|
||||
res := runDeployModule(t, sublime, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a sublime sftp finding")
|
||||
}
|
||||
if v := deployExtract(res, "remote_host"); v != "sftp.example.org" {
|
||||
t.Errorf("remote_host=%q, want sftp.example.org", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("atom remote-ftp config leaks the deploy host", func(t *testing.T) {
|
||||
body := `{"protocol":"ftp","host":"ftp.example.net","port":21,"user":"upload",` +
|
||||
`"pass":"letmein","remote":"/","connTimeout":10000,"pasvTimeout":10000}`
|
||||
res := runDeployModule(t, ftpconfig, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an atom remote-ftp finding")
|
||||
}
|
||||
if v := deployExtract(res, "remote_host"); v != "ftp.example.net" {
|
||||
t.Errorf("remote_host=%q, want ftp.example.net", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an html login page carrying the same keys is not a leak", func(t *testing.T) {
|
||||
body := `<html><head><title>Sign in</title></head><body>` +
|
||||
`config keys "remotePath" "password" "host":"evil.example.com"</body></html>`
|
||||
if res := runDeployModule(t, vscode, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html page should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain json config without the tool keys is not a leak", func(t *testing.T) {
|
||||
body := `{"host":"db.internal","username":"admin","user":"admin","pass":"x","password":"hunter2"}`
|
||||
for _, file := range []string{vscode, sublime, ftpconfig} {
|
||||
if res := runDeployModule(t, file, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a config without the tool keys should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tool config with a host but no credential field is not a leak", func(t *testing.T) {
|
||||
bodies := map[string]string{
|
||||
vscode: `{"host":"h.example.com","remotePath":"/var/www","uploadOnSave":true}`,
|
||||
sublime: `{"type":"sftp","host":"h.example.com","upload_on_save":true}`,
|
||||
ftpconfig: `{"protocol":"ftp","host":"h.example.com","connTimeout":10000,"pasvTimeout":10000}`,
|
||||
}
|
||||
for file, body := range bodies {
|
||||
if res := runDeployModule(t, file, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a config with no credential field should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{vscode, sublime, ftpconfig} {
|
||||
if res := runDeployModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDistDBModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func distDBExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestDistributedDBExposureModules(t *testing.T) {
|
||||
const riak = "../../modules/recon/riak-api-exposure.yaml"
|
||||
const couchbase = "../../modules/recon/couchbase-api-exposure.yaml"
|
||||
const druid = "../../modules/recon/druid-api-exposure.yaml"
|
||||
|
||||
riakStats := `{"riak_kv_version":"3.0.16","riak_core_version":"3.0.99","riak_pipe_version":"3.0.16",` +
|
||||
`"sys_otp_release":"22","ring_members":["riak@10.0.0.1"],"ring_num_partitions":64,` +
|
||||
`"storage_backend":"riak_kv_bitcask_backend"}`
|
||||
|
||||
couchbasePools := `{"pools":[{"name":"default","uri":"/pools/default?uuid=abc",` +
|
||||
`"streamingUri":"/poolsStreaming/default?uuid=abc"}],"isAdminCreds":false,"isEnterprise":true,` +
|
||||
`"implementationVersion":"7.2.0-6053-enterprise","uuid":"abc",` +
|
||||
`"componentsVersion":{"ns_server":"7.2.0-6053","couchdb":"3.1.1"}}`
|
||||
|
||||
druidStatus := `{"version":"0.22.1","modules":[{"name":"org.apache.druid.server.initialization.jetty.JettyServerModule",` +
|
||||
`"artifact":"druid-server","version":"0.22.1"},{"name":"org.apache.druid.guice.AnnouncerModule",` +
|
||||
`"artifact":"druid-server","version":"0.22.1"}],"memory":{"maxMemory":1037959168,` +
|
||||
`"totalMemory":1037959168,"freeMemory":900000000,"directMemory":134217728}}`
|
||||
|
||||
t.Run("an exposed riak http api is flagged and versioned", func(t *testing.T) {
|
||||
res := runDistDBModule(t, riak, 200, riakStats)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a riak finding")
|
||||
}
|
||||
if v := distDBExtract(res, "riak_version"); v != "3.0.16" {
|
||||
t.Errorf("riak_version=%q, want 3.0.16", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed couchbase cluster api is flagged and versioned", func(t *testing.T) {
|
||||
res := runDistDBModule(t, couchbase, 200, couchbasePools)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a couchbase finding")
|
||||
}
|
||||
if v := distDBExtract(res, "couchbase_version"); v != "7.2.0-6053-enterprise" {
|
||||
t.Errorf("couchbase_version=%q, want 7.2.0-6053-enterprise", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed druid process is flagged and versioned", func(t *testing.T) {
|
||||
res := runDistDBModule(t, druid, 200, druidStatus)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a druid finding")
|
||||
}
|
||||
if v := distDBExtract(res, "druid_version"); v != "0.22.1" {
|
||||
t.Errorf("druid_version=%q, want 0.22.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a riak kv version without a core version is not flagged", func(t *testing.T) {
|
||||
body := `{"riak_kv_version":"3.0.16","name":"app"}`
|
||||
if res := runDistDBModule(t, riak, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a kv version alone should not match riak, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a riak core version without a kv version is not flagged", func(t *testing.T) {
|
||||
body := `{"riak_core_version":"3.0.16","name":"app"}`
|
||||
if res := runDistDBModule(t, riak, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a core version alone should not match riak, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a couchbase impl version without a components version is not flagged", func(t *testing.T) {
|
||||
body := `{"implementationVersion":"7.2.0","name":"app"}`
|
||||
if res := runDistDBModule(t, couchbase, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an impl version alone should not match couchbase, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a couchbase components version without an impl version is not flagged", func(t *testing.T) {
|
||||
body := `{"componentsVersion":{"ns_server":"7.2.0"},"name":"app"}`
|
||||
if res := runDistDBModule(t, couchbase, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a components version alone should not match couchbase, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a druid package without a memory block is not flagged", func(t *testing.T) {
|
||||
body := `{"modules":[{"name":"org.apache.druid.cli.Main"}],"app":"x"}`
|
||||
if res := runDistDBModule(t, druid, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a druid package alone should not match druid, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a memory block without a druid package is not flagged", func(t *testing.T) {
|
||||
body := `{"memory":{"maxMemory":123},"app":"x"}`
|
||||
if res := runDistDBModule(t, druid, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a memory block alone should not match druid, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic version json is not a distributed db", func(t *testing.T) {
|
||||
body := `{"version":"1.0.0","name":"app"}`
|
||||
for _, file := range []string{riak, couchbase, druid} {
|
||||
if res := runDistDBModule(t, file, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a generic version should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{riak, couchbase, druid} {
|
||||
if res := runDistDBModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{riak, couchbase, druid} {
|
||||
if res := runDistDBModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -14,6 +14,7 @@ package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -26,6 +27,11 @@ import (
|
||||
// MaxBodySize limits response body to prevent memory exhaustion.
|
||||
const MaxBodySize = 5 * 1024 * 1024
|
||||
|
||||
// ErrUnsupportedModuleType signals an executor for a module type that is not
|
||||
// yet implemented. Returning it (rather than an empty result) keeps callers
|
||||
// from mistaking "not implemented" for "scanned, found nothing".
|
||||
var ErrUnsupportedModuleType = errors.New("unsupported module type")
|
||||
|
||||
// httpRequest represents a generated HTTP request.
|
||||
type httpRequest struct {
|
||||
Method string
|
||||
@@ -143,25 +149,52 @@ func generateHTTPRequests(target string, cfg *HTTPConfig) []*httpRequest {
|
||||
return requests
|
||||
}
|
||||
|
||||
// Generate requests with payloads
|
||||
// pitchfork pairs path[i] with payload[i] and stops at the shorter list;
|
||||
// clusterbomb (default) crosses every path with every payload.
|
||||
if strings.EqualFold(cfg.Attack, "pitchfork") {
|
||||
n := len(cfg.Paths)
|
||||
if len(cfg.Payloads) < n {
|
||||
n = len(cfg.Payloads)
|
||||
}
|
||||
for i := 0; i < n; i++ {
|
||||
requests = append(requests, newPayloadRequest(method, target, cfg.Paths[i], cfg.Payloads[i], cfg))
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
requests = append(requests, newPayloadRequest(method, target, path, payload, cfg))
|
||||
}
|
||||
}
|
||||
|
||||
return requests
|
||||
}
|
||||
|
||||
// newPayloadRequest builds one request with the path and body templates
|
||||
// substituted for the given payload.
|
||||
func newPayloadRequest(method, target, path, payload string, cfg *HTTPConfig) *httpRequest {
|
||||
return &httpRequest{
|
||||
Method: method,
|
||||
URL: substituteVariables(path, target, payload),
|
||||
Headers: cfg.Headers,
|
||||
Body: substituteVariables(cfg.Body, target, payload),
|
||||
Payload: payload,
|
||||
Original: path,
|
||||
}
|
||||
}
|
||||
|
||||
// validateAttack rejects an attack mode that is not "", "clusterbomb", or
|
||||
// "pitchfork"; an empty value defaults to clusterbomb.
|
||||
func validateAttack(attack string) error {
|
||||
switch strings.ToLower(attack) {
|
||||
case "", "clusterbomb", "pitchfork":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid attack %q (want \"clusterbomb\" or \"pitchfork\")", attack)
|
||||
}
|
||||
}
|
||||
|
||||
// substituteVariables replaces template variables in a string.
|
||||
func substituteVariables(template, baseURL, payload string) string {
|
||||
result := template
|
||||
@@ -260,6 +293,15 @@ func checkMatcher(m *Matcher, resp *http.Response, body string) bool {
|
||||
case "regex":
|
||||
return checkRegex(part, m.Regex, m.Condition)
|
||||
|
||||
case "size":
|
||||
// size matches the response body length against any listed value.
|
||||
for _, n := range m.Size {
|
||||
if len(body) == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -350,9 +392,9 @@ func runExtractors(extractors []Extractor, resp *http.Response, body string) map
|
||||
result := make(map[string]string)
|
||||
|
||||
for _, e := range extractors {
|
||||
part := getPart(e.Part, resp, body)
|
||||
|
||||
if e.Type == "regex" {
|
||||
switch e.Type {
|
||||
case "regex":
|
||||
part := getPart(e.Part, resp, body)
|
||||
for _, pattern := range e.Regex {
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
@@ -364,6 +406,16 @@ func runExtractors(extractors []Extractor, resp *http.Response, body string) map
|
||||
break
|
||||
}
|
||||
}
|
||||
case "kv":
|
||||
// kv records response header key/values, namespaced by the extractor
|
||||
// name when set (e.g. a headers module surfacing every header).
|
||||
for k, v := range resp.Header {
|
||||
key := k
|
||||
if e.Name != "" {
|
||||
key = e.Name + "." + k
|
||||
}
|
||||
result[key] = strings.Join(v, ", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,22 +431,16 @@ func truncateEvidence(s string) string {
|
||||
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
|
||||
// ExecuteDNSModule runs a DNS-based module (not yet implemented).
|
||||
// returns ErrUnsupportedModuleType so the caller logs a clear failure rather
|
||||
// than reporting an empty (but successful-looking) result.
|
||||
func ExecuteDNSModule(_ context.Context, _ string, def *YAMLModule, _ Options) (*Result, error) {
|
||||
return nil, fmt.Errorf("dns module %q: %w", def.ID, ErrUnsupportedModuleType)
|
||||
}
|
||||
|
||||
// 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
|
||||
// ExecuteTCPModule runs a TCP-based module (not yet implemented).
|
||||
// returns ErrUnsupportedModuleType so the caller logs a clear failure rather
|
||||
// than reporting an empty (but successful-looking) result.
|
||||
func ExecuteTCPModule(_ context.Context, _ string, def *YAMLModule, _ Options) (*Result, error) {
|
||||
return nil, fmt.Errorf("tcp module %q: %w", def.ID, ErrUnsupportedModuleType)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
const testTimeout = 5 * time.Second
|
||||
|
||||
// TestExecuteHTTPModuleMatchAndExtract drives the full executor against a live
|
||||
// httptest server: a request hits a path, a matcher fires, an extractor captures.
|
||||
func TestExecuteHTTPModuleMatchAndExtract(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/admin" {
|
||||
w.Header().Set("X-App", "demo")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`flag{found-it} session=sess-4242`))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
def := &YAMLModule{
|
||||
ID: "test-http-hit",
|
||||
Type: TypeHTTP,
|
||||
Info: YAMLModuleInfo{Severity: "high"},
|
||||
HTTP: &HTTPConfig{
|
||||
Method: "GET",
|
||||
Paths: []string{"{{BaseURL}}/admin", "{{BaseURL}}/missing"},
|
||||
Matchers: []Matcher{
|
||||
{Type: "status", Status: []int{200}},
|
||||
{Type: "word", Part: "body", Words: []string{"flag{found-it}"}},
|
||||
},
|
||||
Extractors: []Extractor{
|
||||
{Type: "regex", Name: "session", Part: "body", Regex: []string{`session=(\S+)`}, Group: 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// route through the shared httpx client so proxy/-H/-rate-limit would apply.
|
||||
opts := Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)}
|
||||
|
||||
result, err := ExecuteHTTPModule(context.Background(), srv.URL, def, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteHTTPModule: %v", err)
|
||||
}
|
||||
|
||||
// only /admin satisfies status+word, /missing returns 404.
|
||||
if len(result.Findings) != 1 {
|
||||
t.Fatalf("got %d findings, want 1", len(result.Findings))
|
||||
}
|
||||
f := result.Findings[0]
|
||||
if f.Severity != "high" {
|
||||
t.Errorf("severity = %q, want high (carried from Info)", f.Severity)
|
||||
}
|
||||
if f.Extracted["session"] != "sess-4242" {
|
||||
t.Errorf("extracted session = %q, want sess-4242", f.Extracted["session"])
|
||||
}
|
||||
if f.URL != srv.URL+"/admin" {
|
||||
t.Errorf("finding url = %q, want %q", f.URL, srv.URL+"/admin")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteHTTPModuleNoMatch confirms a module that matches nothing reports
|
||||
// zero findings without erroring.
|
||||
func TestExecuteHTTPModuleNoMatch(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("nothing interesting"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
def := &YAMLModule{
|
||||
ID: "test-http-miss",
|
||||
Type: TypeHTTP,
|
||||
HTTP: &HTTPConfig{
|
||||
Paths: []string{"{{BaseURL}}/"},
|
||||
Matchers: []Matcher{
|
||||
{Type: "word", Part: "body", Words: []string{"never-present"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ExecuteHTTPModule(context.Background(), srv.URL, def, Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteHTTPModule: %v", err)
|
||||
}
|
||||
if len(result.Findings) != 0 {
|
||||
t.Fatalf("got %d findings, want 0", len(result.Findings))
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteHTTPModulePayloadExpansion verifies payload templates reach the
|
||||
// server and the matching response is captured.
|
||||
func TestExecuteHTTPModulePayloadExpansion(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// only the "boom" payload triggers the vulnerable branch.
|
||||
if r.URL.Query().Get("q") == "boom" {
|
||||
_, _ = w.Write([]byte("error: sql syntax near boom"))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
def := &YAMLModule{
|
||||
ID: "test-http-payload",
|
||||
Type: TypeHTTP,
|
||||
HTTP: &HTTPConfig{
|
||||
Paths: []string{"{{BaseURL}}/search?q={{payload}}"},
|
||||
Payloads: []string{"safe", "boom"},
|
||||
Matchers: []Matcher{
|
||||
{Type: "word", Part: "body", Words: []string{"sql syntax"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ExecuteHTTPModule(context.Background(), srv.URL, def, Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteHTTPModule: %v", err)
|
||||
}
|
||||
if len(result.Findings) != 1 {
|
||||
t.Fatalf("got %d findings, want 1 (only boom payload)", len(result.Findings))
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteHTTPModuleSizeMatcher pins the size matcher: it fires when the
|
||||
// response body length equals a listed value and stays silent otherwise.
|
||||
func TestExecuteHTTPModuleSizeMatcher(t *testing.T) {
|
||||
body := "1234567890" // 10 bytes
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
opts := Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)}
|
||||
mod := func(id string, size int) *YAMLModule {
|
||||
return &YAMLModule{
|
||||
ID: id, Type: TypeHTTP,
|
||||
HTTP: &HTTPConfig{
|
||||
Paths: []string{"{{BaseURL}}/"},
|
||||
Matchers: []Matcher{{Type: "size", Size: []int{size}}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
hit, err := ExecuteHTTPModule(context.Background(), srv.URL, mod("size-hit", len(body)), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteHTTPModule(hit): %v", err)
|
||||
}
|
||||
if len(hit.Findings) != 1 {
|
||||
t.Fatalf("size match: got %d findings, want 1", len(hit.Findings))
|
||||
}
|
||||
|
||||
miss, err := ExecuteHTTPModule(context.Background(), srv.URL, mod("size-miss", len(body)+1), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteHTTPModule(miss): %v", err)
|
||||
}
|
||||
if len(miss.Findings) != 0 {
|
||||
t.Fatalf("size mismatch: got %d findings, want 0", len(miss.Findings))
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteHTTPModuleKvExtractor pins the kv extractor: it records response
|
||||
// header key/values onto the finding, namespaced by the extractor name.
|
||||
func TestExecuteHTTPModuleKvExtractor(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Server", "nginx/1.25.3")
|
||||
w.Header().Set("X-Powered-By", "PHP/8.2.0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("hello"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
def := &YAMLModule{
|
||||
ID: "kv-mod", Type: TypeHTTP,
|
||||
HTTP: &HTTPConfig{
|
||||
Paths: []string{"{{BaseURL}}/"},
|
||||
Matchers: []Matcher{{Type: "status", Status: []int{200}}},
|
||||
Extractors: []Extractor{{Type: "kv", Name: "headers", Part: "header"}},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ExecuteHTTPModule(context.Background(), srv.URL, def, Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)})
|
||||
if err != nil {
|
||||
t.Fatalf("ExecuteHTTPModule: %v", err)
|
||||
}
|
||||
if len(result.Findings) != 1 {
|
||||
t.Fatalf("got %d findings, want 1", len(result.Findings))
|
||||
}
|
||||
ex := result.Findings[0].Extracted
|
||||
if ex["headers.Server"] != "nginx/1.25.3" {
|
||||
t.Errorf("kv headers.Server = %q, want nginx/1.25.3", ex["headers.Server"])
|
||||
}
|
||||
if ex["headers.X-Powered-By"] != "PHP/8.2.0" {
|
||||
t.Errorf("kv headers.X-Powered-By = %q, want PHP/8.2.0", ex["headers.X-Powered-By"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteHTTPModuleNoConfig(t *testing.T) {
|
||||
def := &YAMLModule{ID: "x", Type: TypeHTTP}
|
||||
if _, err := ExecuteHTTPModule(context.Background(), "http://h", def, Options{}); err == nil {
|
||||
t.Fatal("expected error when HTTP config is nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteHTTPModuleContextCancel pins the cancellation path. The dispatch
|
||||
// loop selects between ctx.Done() and the concurrency semaphore, so a cancelled
|
||||
// context can either short-circuit with ctx.Err() or let the in-flight request
|
||||
// fail on the dead context. Both are correct: the contract is "never hang, never
|
||||
// invent a finding", which is what we assert here rather than forcing one race
|
||||
// winner (that made this test flaky under -count).
|
||||
func TestExecuteHTTPModuleContextCancel(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
def := &YAMLModule{
|
||||
ID: "test-http-cancel",
|
||||
Type: TypeHTTP,
|
||||
HTTP: &HTTPConfig{
|
||||
Paths: []string{"{{BaseURL}}/a"},
|
||||
Matchers: []Matcher{{Type: "status", Status: []int{200}}},
|
||||
},
|
||||
}
|
||||
|
||||
result, err := ExecuteHTTPModule(ctx, srv.URL, def, Options{Timeout: testTimeout, Client: httpx.Client(testTimeout)})
|
||||
if err != nil {
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("err = %v, want context.Canceled or nil", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
// no error means the request was dispatched but failed on the dead context;
|
||||
// either way a cancelled scan must not surface findings.
|
||||
if len(result.Findings) != 0 {
|
||||
t.Fatalf("cancelled scan produced %d findings, want 0", len(result.Findings))
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteDNSModuleUnsupported pins the current behavior: DNS execution is
|
||||
// not implemented and must signal it via ErrUnsupportedModuleType, not by
|
||||
// quietly returning an empty (successful-looking) result.
|
||||
func TestExecuteDNSModuleUnsupported(t *testing.T) {
|
||||
def := &YAMLModule{ID: "dns-mod", Type: TypeDNS, DNS: &DNSConfig{Type: "A"}}
|
||||
result, err := ExecuteDNSModule(context.Background(), "example.com", def, Options{})
|
||||
if result != nil {
|
||||
t.Errorf("result = %v, want nil for unsupported type", result)
|
||||
}
|
||||
if !errors.Is(err, ErrUnsupportedModuleType) {
|
||||
t.Fatalf("err = %v, want ErrUnsupportedModuleType", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteTCPModuleUnsupported(t *testing.T) {
|
||||
def := &YAMLModule{ID: "tcp-mod", Type: TypeTCP, TCP: &TCPConfig{Port: 22}}
|
||||
result, err := ExecuteTCPModule(context.Background(), "example.com", def, Options{})
|
||||
if result != nil {
|
||||
t.Errorf("result = %v, want nil for unsupported type", result)
|
||||
}
|
||||
if !errors.Is(err, ErrUnsupportedModuleType) {
|
||||
t.Fatalf("err = %v, want ErrUnsupportedModuleType", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapperExecuteRoutesByType confirms the Module wrapper dispatches each
|
||||
// type to the right executor and propagates the unsupported-type sentinel.
|
||||
func TestWrapperExecuteRoutesByType(t *testing.T) {
|
||||
t.Run("dns routes to unsupported", func(t *testing.T) {
|
||||
def := &YAMLModule{ID: "d", Type: TypeDNS, DNS: &DNSConfig{}}
|
||||
w := newYAMLModuleWrapper(def, "d.yaml")
|
||||
if _, err := w.Execute(context.Background(), "t", Options{}); !errors.Is(err, ErrUnsupportedModuleType) {
|
||||
t.Fatalf("err = %v, want ErrUnsupportedModuleType", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tcp routes to unsupported", func(t *testing.T) {
|
||||
def := &YAMLModule{ID: "t", Type: TypeTCP, TCP: &TCPConfig{}}
|
||||
w := newYAMLModuleWrapper(def, "t.yaml")
|
||||
if _, err := w.Execute(context.Background(), "t", Options{}); !errors.Is(err, ErrUnsupportedModuleType) {
|
||||
t.Fatalf("err = %v, want ErrUnsupportedModuleType", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing http config errors", func(t *testing.T) {
|
||||
def := &YAMLModule{ID: "h", Type: TypeHTTP}
|
||||
w := newYAMLModuleWrapper(def, "h.yaml")
|
||||
if _, err := w.Execute(context.Background(), "t", Options{}); err == nil {
|
||||
t.Fatal("expected error for missing http config")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown type errors", func(t *testing.T) {
|
||||
def := &YAMLModule{ID: "z", Type: ModuleType("bogus")}
|
||||
w := newYAMLModuleWrapper(def, "z.yaml")
|
||||
if _, err := w.Execute(context.Background(), "t", Options{}); err == nil {
|
||||
t.Fatal("expected error for unknown module type")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTruncateEvidence(t *testing.T) {
|
||||
short := "short evidence"
|
||||
if got := truncateEvidence(short); got != short {
|
||||
t.Errorf("short evidence changed: %q", got)
|
||||
}
|
||||
|
||||
long := make([]byte, 600)
|
||||
for i := range long {
|
||||
long[i] = 'a'
|
||||
}
|
||||
got := truncateEvidence(string(long))
|
||||
// 500 chars of content plus the ellipsis marker.
|
||||
if len(got) != 503 {
|
||||
t.Errorf("truncated len = %d, want 503", len(got))
|
||||
}
|
||||
if got[len(got)-3:] != "..." {
|
||||
t.Errorf("truncated evidence missing ellipsis: %q", got[len(got)-3:])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runHTTPDBModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func httpdbExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestHTTPDatabaseExposureModules(t *testing.T) {
|
||||
const influxdb = "../../modules/recon/influxdb-api-exposure.yaml"
|
||||
const arangodb = "../../modules/recon/arangodb-api-exposure.yaml"
|
||||
const neo4j = "../../modules/recon/neo4j-api-exposure.yaml"
|
||||
|
||||
influxHealth := `{"name":"influxdb","message":"ready for queries and writes","status":"pass",` +
|
||||
`"checks":[],"version":"2.9.1","commit":"a1b2c3d4"}`
|
||||
|
||||
arangoVersion := `{"server":"arango","version":"3.11.5","license":"community"}`
|
||||
|
||||
neo4jDiscovery := `{"bolt_routing":"neo4j://localhost:7687","transaction":"http://localhost:7474/db/{databaseName}/tx",` +
|
||||
`"bolt_direct":"bolt://localhost:7687","neo4j_version":"5.13.0","neo4j_edition":"community"}`
|
||||
|
||||
t.Run("an exposed influxdb health endpoint is flagged and versioned", func(t *testing.T) {
|
||||
res := runHTTPDBModule(t, influxdb, 200, influxHealth)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an influxdb finding")
|
||||
}
|
||||
if v := httpdbExtract(res, "influxdb_version"); v != "2.9.1" {
|
||||
t.Errorf("influxdb_version=%q, want 2.9.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an anonymous arangodb version endpoint is flagged and versioned", func(t *testing.T) {
|
||||
res := runHTTPDBModule(t, arangodb, 200, arangoVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an arangodb finding")
|
||||
}
|
||||
if v := httpdbExtract(res, "arangodb_version"); v != "3.11.5" {
|
||||
t.Errorf("arangodb_version=%q, want 3.11.5", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed neo4j discovery endpoint is flagged and versioned", func(t *testing.T) {
|
||||
res := runHTTPDBModule(t, neo4j, 200, neo4jDiscovery)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a neo4j finding")
|
||||
}
|
||||
if v := httpdbExtract(res, "neo4j_version"); v != "5.13.0" {
|
||||
t.Errorf("neo4j_version=%q, want 5.13.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an influxdb name without the health message is not flagged", func(t *testing.T) {
|
||||
body := `{"name":"influxdb","status":"pass"}`
|
||||
if res := runHTTPDBModule(t, influxdb, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an influxdb name alone should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a health message without the influxdb name is not flagged", func(t *testing.T) {
|
||||
body := `{"name":"telegraf","message":"ready for queries and writes"}`
|
||||
if res := runHTTPDBModule(t, influxdb, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("the message alone should not match influxdb, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an arango without a license field is still flagged", func(t *testing.T) {
|
||||
body := `{"server":"arango","version":"3.11.5"}`
|
||||
res := runHTTPDBModule(t, arangodb, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an arangodb finding without a license field (pre-3.12)")
|
||||
}
|
||||
if v := httpdbExtract(res, "arangodb_version"); v != "3.11.5" {
|
||||
t.Errorf("arangodb_version=%q, want 3.11.5", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a non-arango version response is not flagged", func(t *testing.T) {
|
||||
body := `{"server":"foundationdb","version":"1.0.0"}`
|
||||
if res := runHTTPDBModule(t, arangodb, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-arango server should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an arango response without a version is not flagged", func(t *testing.T) {
|
||||
body := `{"server":"arango"}`
|
||||
if res := runHTTPDBModule(t, arangodb, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an arango without a version should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an arango that requires auth is not flagged", func(t *testing.T) {
|
||||
if res := runHTTPDBModule(t, arangodb, 401, arangoVersion); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 arango should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a neo4j version without an edition is not flagged", func(t *testing.T) {
|
||||
body := `{"neo4j_version":"5.13.0","transaction":"http://localhost:7474/db/neo4j/tx"}`
|
||||
if res := runHTTPDBModule(t, neo4j, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a neo4j version alone should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a neo4j edition without a version is not flagged", func(t *testing.T) {
|
||||
body := `{"neo4j_edition":"community","bolt_routing":"neo4j://localhost:7687"}`
|
||||
if res := runHTTPDBModule(t, neo4j, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a neo4j edition alone should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic health json is not influxdb", func(t *testing.T) {
|
||||
body := `{"status":"UP","components":{"db":{"status":"UP"}}}`
|
||||
if res := runHTTPDBModule(t, influxdb, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic health should not match influxdb, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{influxdb, arangodb, neo4j} {
|
||||
if res := runHTTPDBModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{influxdb, arangodb, neo4j} {
|
||||
if res := runHTTPDBModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// writeModule drops a yaml file into a temp dir and returns its path.
|
||||
func writeModule(t *testing.T, dir, name, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||
t.Fatalf("write module: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestParseYAMLModuleValid(t *testing.T) {
|
||||
const doc = `id: example-http
|
||||
type: http
|
||||
info:
|
||||
name: Example
|
||||
author: azzie
|
||||
severity: medium
|
||||
description: a test module
|
||||
tags: [test, demo]
|
||||
http:
|
||||
method: GET
|
||||
paths:
|
||||
- "{{BaseURL}}/admin"
|
||||
matchers:
|
||||
- type: status
|
||||
status: [200]
|
||||
- type: word
|
||||
part: body
|
||||
words: ["admin"]
|
||||
condition: and
|
||||
extractors:
|
||||
- type: regex
|
||||
name: token
|
||||
part: body
|
||||
regex: ["token=(\\w+)"]
|
||||
group: 1
|
||||
`
|
||||
dir := t.TempDir()
|
||||
path := writeModule(t, dir, "ok.yaml", doc)
|
||||
|
||||
def, err := ParseYAMLModule(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseYAMLModule: %v", err)
|
||||
}
|
||||
if def.ID != "example-http" {
|
||||
t.Errorf("id = %q, want example-http", def.ID)
|
||||
}
|
||||
if def.Type != TypeHTTP {
|
||||
t.Errorf("type = %q, want http", def.Type)
|
||||
}
|
||||
if def.Info.Severity != "medium" {
|
||||
t.Errorf("severity = %q, want medium", def.Info.Severity)
|
||||
}
|
||||
if def.HTTP == nil {
|
||||
t.Fatal("http config not parsed")
|
||||
}
|
||||
if len(def.HTTP.Matchers) != 2 {
|
||||
t.Errorf("got %d matchers, want 2", len(def.HTTP.Matchers))
|
||||
}
|
||||
if len(def.HTTP.Extractors) != 1 || def.HTTP.Extractors[0].Group != 1 {
|
||||
t.Errorf("extractor not parsed correctly: %+v", def.HTTP.Extractors)
|
||||
}
|
||||
if len(def.Info.Tags) != 2 {
|
||||
t.Errorf("got %d tags, want 2", len(def.Info.Tags))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseYAMLModuleErrors(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
}{
|
||||
{
|
||||
name: "missing id",
|
||||
content: "type: http\nhttp:\n paths: [\"/\"]\n",
|
||||
},
|
||||
{
|
||||
name: "missing type",
|
||||
content: "id: no-type\nhttp:\n paths: [\"/\"]\n",
|
||||
},
|
||||
{
|
||||
name: "malformed yaml",
|
||||
content: "id: bad\ntype: http\n paths: [unbalanced\n : nope\n",
|
||||
},
|
||||
{
|
||||
// a scalar where a mapping is expected must fail to unmarshal.
|
||||
name: "type mismatch",
|
||||
content: "id: bad-shape\ntype: http\nhttp: \"should-be-a-map\"\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
path := writeModule(t, dir, tt.name+".yaml", tt.content)
|
||||
if _, err := ParseYAMLModule(path); err == nil {
|
||||
t.Fatalf("expected error for %s", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseYAMLModuleMissingFile(t *testing.T) {
|
||||
if _, err := ParseYAMLModule(filepath.Join(t.TempDir(), "does-not-exist.yaml")); err == nil {
|
||||
t.Fatal("expected error for missing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestYAMLModuleWrapperInfoAndType(t *testing.T) {
|
||||
def := &YAMLModule{
|
||||
ID: "wrap-test",
|
||||
Type: TypeHTTP,
|
||||
Info: YAMLModuleInfo{
|
||||
Name: "Wrapped",
|
||||
Author: "azzie",
|
||||
Severity: "low",
|
||||
Description: "desc",
|
||||
Tags: []string{"a", "b"},
|
||||
},
|
||||
}
|
||||
w := newYAMLModuleWrapper(def, "wrap.yaml")
|
||||
|
||||
if w.Type() != TypeHTTP {
|
||||
t.Errorf("Type() = %q, want http", w.Type())
|
||||
}
|
||||
info := w.Info()
|
||||
if info.ID != "wrap-test" || info.Name != "Wrapped" || info.Severity != "low" {
|
||||
t.Errorf("Info() mismatch: %+v", info)
|
||||
}
|
||||
if len(info.Tags) != 2 {
|
||||
t.Errorf("Info().Tags = %v, want 2 entries", info.Tags)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoaderLoadAll exercises the directory walk: a valid module registers, a
|
||||
// malformed one is skipped without aborting the walk.
|
||||
func TestLoaderLoadAll(t *testing.T) {
|
||||
Clear()
|
||||
t.Cleanup(Clear)
|
||||
|
||||
dir := t.TempDir()
|
||||
writeModule(t, dir, "good.yaml", "id: good-mod\ntype: http\nhttp:\n paths: [\"{{BaseURL}}/\"]\n matchers:\n - type: status\n status: [200]\n")
|
||||
writeModule(t, dir, "bad.yml", "id: bad-mod\n") // missing type -> skipped
|
||||
writeModule(t, dir, "ignore.txt", "not a module")
|
||||
|
||||
l := &Loader{builtinDir: dir, userDir: filepath.Join(dir, "nonexistent-user")}
|
||||
if err := l.LoadAll(); err != nil {
|
||||
t.Fatalf("LoadAll: %v", err)
|
||||
}
|
||||
|
||||
// only the good module loads; the malformed one is logged and skipped.
|
||||
if l.Loaded() != 1 {
|
||||
t.Errorf("Loaded() = %d, want 1", l.Loaded())
|
||||
}
|
||||
if _, ok := Get("good-mod"); !ok {
|
||||
t.Error("good-mod not registered")
|
||||
}
|
||||
if _, ok := Get("bad-mod"); ok {
|
||||
t.Error("bad-mod should not have registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLoaderDirs(t *testing.T) {
|
||||
l, err := NewLoader()
|
||||
if err != nil {
|
||||
t.Fatalf("NewLoader: %v", err)
|
||||
}
|
||||
if l.BuiltinDir() == "" {
|
||||
t.Error("BuiltinDir is empty")
|
||||
}
|
||||
if l.UserDir() == "" {
|
||||
t.Error("UserDir is empty")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegistry exercises the package-level registry: register, get, dedupe by
|
||||
// id, filter by tag and type, count and clear.
|
||||
func TestRegistry(t *testing.T) {
|
||||
Clear()
|
||||
t.Cleanup(Clear)
|
||||
|
||||
http1 := newYAMLModuleWrapper(&YAMLModule{ID: "h1", Type: TypeHTTP, Info: YAMLModuleInfo{Tags: []string{"web", "cve"}}}, "h1")
|
||||
http2 := newYAMLModuleWrapper(&YAMLModule{ID: "h2", Type: TypeHTTP, Info: YAMLModuleInfo{Tags: []string{"web"}}}, "h2")
|
||||
dns1 := newYAMLModuleWrapper(&YAMLModule{ID: "d1", Type: TypeDNS, Info: YAMLModuleInfo{Tags: []string{"dns"}}}, "d1")
|
||||
|
||||
Register(http1)
|
||||
Register(http2)
|
||||
Register(dns1)
|
||||
|
||||
if Count() != 3 {
|
||||
t.Fatalf("Count() = %d, want 3", Count())
|
||||
}
|
||||
|
||||
got, ok := Get("h1")
|
||||
if !ok || got.Info().ID != "h1" {
|
||||
t.Errorf("Get(h1) = %v, %v", got, ok)
|
||||
}
|
||||
if _, ok := Get("missing"); ok {
|
||||
t.Error("Get(missing) should report not found")
|
||||
}
|
||||
|
||||
if n := len(ByType(TypeHTTP)); n != 2 {
|
||||
t.Errorf("ByType(http) = %d, want 2", n)
|
||||
}
|
||||
if n := len(ByType(TypeDNS)); n != 1 {
|
||||
t.Errorf("ByType(dns) = %d, want 1", n)
|
||||
}
|
||||
if n := len(ByTag("web")); n != 2 {
|
||||
t.Errorf("ByTag(web) = %d, want 2", n)
|
||||
}
|
||||
if n := len(ByTag("cve")); n != 1 {
|
||||
t.Errorf("ByTag(cve) = %d, want 1", n)
|
||||
}
|
||||
if n := len(ByTag("none")); n != 0 {
|
||||
t.Errorf("ByTag(none) = %d, want 0", n)
|
||||
}
|
||||
if n := len(All()); n != 3 {
|
||||
t.Errorf("All() = %d, want 3", n)
|
||||
}
|
||||
|
||||
// re-registering the same id overwrites rather than duplicating.
|
||||
Register(newYAMLModuleWrapper(&YAMLModule{ID: "h1", Type: TypeHTTP}, "h1-v2"))
|
||||
if Count() != 3 {
|
||||
t.Errorf("Count() after re-register = %d, want 3", Count())
|
||||
}
|
||||
|
||||
Clear()
|
||||
if Count() != 0 {
|
||||
t.Errorf("Count() after Clear = %d, want 0", Count())
|
||||
}
|
||||
}
|
||||
|
||||
// TestResultType pins the ScanResult interface bridge.
|
||||
func TestResultType(t *testing.T) {
|
||||
r := &Result{ModuleID: "abc"}
|
||||
if r.ResultType() != "abc" {
|
||||
t.Errorf("ResultType() = %q, want abc", r.ResultType())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoaderScriptStubNoop confirms the go-script loader is currently a no-op
|
||||
// that registers nothing and returns no error.
|
||||
func TestLoaderScriptStubNoop(t *testing.T) {
|
||||
l := &Loader{}
|
||||
if err := l.loadScript("anything.go"); err != nil {
|
||||
t.Errorf("loadScript stub returned error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runMgmtModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func mgmtExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestManagementAPIExposureModules(t *testing.T) {
|
||||
const kong = "../../modules/recon/kong-api-exposure.yaml"
|
||||
const jolokia = "../../modules/recon/jolokia-api-exposure.yaml"
|
||||
const nats = "../../modules/recon/nats-api-exposure.yaml"
|
||||
|
||||
kongRoot := `{"version":"3.4.0","tagline":"Welcome to kong","hostname":"kong-node","node_id":"abc",` +
|
||||
`"lua_version":"LuaJIT 2.1.0","plugins":{"available_on_server":{}},` +
|
||||
`"configuration":{"database":"postgres","admin_listen":["0.0.0.0:8001"]}}`
|
||||
|
||||
jolokiaVersion := `{"request":{"type":"version"},"value":{"agent":"1.7.2","protocol":"7.2",` +
|
||||
`"config":{"agentType":"servlet"},"info":{"product":"tomcat"}},"status":200,"timestamp":1694598949}`
|
||||
|
||||
natsVarz := `{"server_id":"NDABC","server_name":"NDABC","version":"2.10.1","proto":1,"go":"go1.21.1",` +
|
||||
`"host":"0.0.0.0","port":4222,"max_connections":65536,"max_payload":1048576,"connections":3,"total_connections":10}`
|
||||
|
||||
t.Run("an exposed kong admin api is flagged and versioned", func(t *testing.T) {
|
||||
res := runMgmtModule(t, kong, 200, kongRoot)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a kong finding")
|
||||
}
|
||||
if v := mgmtExtract(res, "kong_version"); v != "3.4.0" {
|
||||
t.Errorf("kong_version=%q, want 3.4.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed jolokia agent is flagged and versioned", func(t *testing.T) {
|
||||
res := runMgmtModule(t, jolokia, 200, jolokiaVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a jolokia finding")
|
||||
}
|
||||
if v := mgmtExtract(res, "jolokia_agent_version"); v != "1.7.2" {
|
||||
t.Errorf("jolokia_agent_version=%q, want 1.7.2", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed nats monitor is flagged and versioned", func(t *testing.T) {
|
||||
res := runMgmtModule(t, nats, 200, natsVarz)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a nats finding")
|
||||
}
|
||||
if v := mgmtExtract(res, "nats_version"); v != "2.10.1" {
|
||||
t.Errorf("nats_version=%q, want 2.10.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an available plugins map without an admin listen is not flagged", func(t *testing.T) {
|
||||
body := `{"plugins":{"available_on_server":{}},"version":"3.4.0"}`
|
||||
if res := runMgmtModule(t, kong, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an available plugins map alone should not match kong, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an admin listen without an available plugins map is not flagged", func(t *testing.T) {
|
||||
body := `{"configuration":{"admin_listen":["0.0.0.0:8001"]},"version":"1.0"}`
|
||||
if res := runMgmtModule(t, kong, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an admin listen alone should not match kong, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a jolokia agent without a protocol is not flagged", func(t *testing.T) {
|
||||
body := `{"value":{"agent":"1.7.2"}}`
|
||||
if res := runMgmtModule(t, jolokia, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an agent alone should not match jolokia, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a jolokia protocol without an agent is not flagged", func(t *testing.T) {
|
||||
body := `{"value":{"protocol":"7.2"},"info":{}}`
|
||||
if res := runMgmtModule(t, jolokia, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a protocol alone should not match jolokia, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a nats server id without a max payload is not flagged", func(t *testing.T) {
|
||||
body := `{"server_id":"NDABC","version":"2.10.1"}`
|
||||
if res := runMgmtModule(t, nats, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a server id alone should not match nats, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a max payload without a nats server id is not flagged", func(t *testing.T) {
|
||||
body := `{"max_payload":1048576,"port":4222}`
|
||||
if res := runMgmtModule(t, nats, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a max payload alone should not match nats, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic version json is not a management api", func(t *testing.T) {
|
||||
body := `{"version":"1.0.0","name":"app"}`
|
||||
for _, file := range []string{kong, jolokia, nats} {
|
||||
if res := runMgmtModule(t, file, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a generic version should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{kong, jolokia, nats} {
|
||||
if res := runMgmtModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{kong, jolokia, nats} {
|
||||
if res := runMgmtModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package modules
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeResponse builds a minimal *http.Response for matcher/extractor tests.
|
||||
// it carries no real socket (Body is http.NoBody), so there is nothing to
|
||||
// close; bodyclose is excluded for test files in .golangci.yml. header drives
|
||||
// the header/all parts without a live server; matchers read the body string
|
||||
// argument, not resp.Body.
|
||||
func fakeResponse(t *testing.T, status int, header http.Header) *http.Response {
|
||||
t.Helper()
|
||||
if header == nil {
|
||||
header = http.Header{}
|
||||
}
|
||||
return &http.Response{StatusCode: status, Header: header, Body: http.NoBody}
|
||||
}
|
||||
|
||||
func TestCheckMatcherStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status int
|
||||
want []int
|
||||
expect bool
|
||||
}{
|
||||
{name: "single match", status: 200, want: []int{200}, expect: true},
|
||||
{name: "one of many", status: 404, want: []int{200, 301, 404}, expect: true},
|
||||
{name: "no match", status: 500, want: []int{200, 404}, expect: false},
|
||||
{name: "empty status list", status: 200, want: nil, expect: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &Matcher{Type: "status", Status: tt.want}
|
||||
resp := fakeResponse(t, tt.status, nil)
|
||||
if got := checkMatcher(m, resp, ""); got != tt.expect {
|
||||
t.Errorf("checkMatcher status = %v, want %v", got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMatcherWord(t *testing.T) {
|
||||
const body = "welcome admin dashboard"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
words []string
|
||||
condition string
|
||||
expect bool
|
||||
}{
|
||||
{name: "and all present", words: []string{"admin", "dashboard"}, condition: "and", expect: true},
|
||||
{name: "and one missing", words: []string{"admin", "missing"}, condition: "and", expect: false},
|
||||
{name: "default is and", words: []string{"admin", "missing"}, condition: "", expect: false},
|
||||
{name: "or one present", words: []string{"missing", "admin"}, condition: "or", expect: true},
|
||||
{name: "or none present", words: []string{"missing", "absent"}, condition: "or", expect: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &Matcher{Type: "word", Part: "body", Words: tt.words, Condition: tt.condition}
|
||||
resp := fakeResponse(t, 200, nil)
|
||||
if got := checkMatcher(m, resp, body); got != tt.expect {
|
||||
t.Errorf("checkMatcher word = %v, want %v", got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMatcherRegex(t *testing.T) {
|
||||
const body = "version 1.2.3 build 99"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
condition string
|
||||
expect bool
|
||||
}{
|
||||
{name: "and all match", patterns: []string{`version \d`, `build \d+`}, condition: "and", expect: true},
|
||||
{name: "and one fails", patterns: []string{`version \d`, `nope\d`}, condition: "and", expect: false},
|
||||
{name: "or one matches", patterns: []string{`nope`, `build \d+`}, condition: "or", expect: true},
|
||||
{name: "or none match", patterns: []string{`nope`, `zilch`}, condition: "or", expect: false},
|
||||
// an invalid pattern under AND must fail closed, not panic.
|
||||
{name: "and invalid pattern fails closed", patterns: []string{`version \d`, `(`}, condition: "and", expect: false},
|
||||
// under OR an invalid pattern is skipped, a later valid one can still hit.
|
||||
{name: "or invalid pattern skipped", patterns: []string{`(`, `build \d+`}, condition: "or", expect: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
m := &Matcher{Type: "regex", Part: "body", Regex: tt.patterns, Condition: tt.condition}
|
||||
resp := fakeResponse(t, 200, nil)
|
||||
if got := checkMatcher(m, resp, body); got != tt.expect {
|
||||
t.Errorf("checkMatcher regex = %v, want %v", got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMatcherHeaderPart(t *testing.T) {
|
||||
header := http.Header{"X-Powered-By": []string{"PHP/8.1"}}
|
||||
resp := fakeResponse(t, 200, header)
|
||||
|
||||
m := &Matcher{Type: "word", Part: "header", Words: []string{"PHP/8.1"}}
|
||||
if !checkMatcher(m, resp, "body-content") {
|
||||
t.Error("expected header-part word matcher to hit on header value")
|
||||
}
|
||||
|
||||
// the same word lives only in the header, so a body-part matcher must miss.
|
||||
mBody := &Matcher{Type: "word", Part: "body", Words: []string{"PHP/8.1"}}
|
||||
if checkMatcher(mBody, resp, "body-content") {
|
||||
t.Error("body-part matcher should not see header-only value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMatcherUnknownType(t *testing.T) {
|
||||
m := &Matcher{Type: "size", Part: "body"}
|
||||
resp := fakeResponse(t, 200, nil)
|
||||
if checkMatcher(m, resp, "anything") {
|
||||
t.Error("unknown matcher type should not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckMatchers(t *testing.T) {
|
||||
resp := fakeResponse(t, 200, http.Header{"Server": []string{"nginx"}})
|
||||
const body = "secret token here"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
matchers []Matcher
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
name: "empty matchers never match",
|
||||
matchers: nil,
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "all matchers pass (AND across matchers)",
|
||||
matchers: []Matcher{
|
||||
{Type: "status", Status: []int{200}},
|
||||
{Type: "word", Part: "body", Words: []string{"secret"}},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "one matcher fails breaks AND",
|
||||
matchers: []Matcher{
|
||||
{Type: "status", Status: []int{200}},
|
||||
{Type: "word", Part: "body", Words: []string{"absent"}},
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "negative inverts a non-match into a pass",
|
||||
matchers: []Matcher{
|
||||
{Type: "word", Part: "body", Words: []string{"absent"}, Negative: true},
|
||||
},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "negative inverts a match into a fail",
|
||||
matchers: []Matcher{
|
||||
{Type: "word", Part: "body", Words: []string{"secret"}, Negative: true},
|
||||
},
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := checkMatchers(tt.matchers, resp, body); got != tt.expect {
|
||||
t.Errorf("checkMatchers = %v, want %v", got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckWords(t *testing.T) {
|
||||
const content = "alpha beta gamma"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
words []string
|
||||
condition string
|
||||
expect bool
|
||||
}{
|
||||
{name: "and all present", words: []string{"alpha", "gamma"}, condition: "and", expect: true},
|
||||
{name: "and missing", words: []string{"alpha", "delta"}, condition: "and", expect: false},
|
||||
{name: "or present", words: []string{"delta", "beta"}, condition: "or", expect: true},
|
||||
{name: "or absent", words: []string{"delta", "epsilon"}, condition: "or", expect: false},
|
||||
{name: "empty under and matches vacuously", words: nil, condition: "and", expect: true},
|
||||
{name: "empty under or matches nothing", words: nil, condition: "or", expect: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := checkWords(content, tt.words, tt.condition); got != tt.expect {
|
||||
t.Errorf("checkWords = %v, want %v", got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRegex(t *testing.T) {
|
||||
const content = "id=42 name=root"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
condition string
|
||||
expect bool
|
||||
}{
|
||||
{name: "and all match", patterns: []string{`id=\d+`, `name=\w+`}, condition: "and", expect: true},
|
||||
{name: "and one fails", patterns: []string{`id=\d+`, `zzz`}, condition: "and", expect: false},
|
||||
{name: "or first matches", patterns: []string{`id=\d+`, `zzz`}, condition: "or", expect: true},
|
||||
{name: "or none match", patterns: []string{`xxx`, `zzz`}, condition: "or", expect: false},
|
||||
{name: "and bad regex fails closed", patterns: []string{`(`}, condition: "and", expect: false},
|
||||
{name: "or bad regex skipped then match", patterns: []string{`(`, `name=\w+`}, condition: "or", expect: true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := checkRegex(content, tt.patterns, tt.condition); got != tt.expect {
|
||||
t.Errorf("checkRegex = %v, want %v", got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPart(t *testing.T) {
|
||||
header := http.Header{"Server": []string{"nginx"}}
|
||||
resp := fakeResponse(t, 200, header)
|
||||
const body = "page body"
|
||||
|
||||
if got := getPart("body", resp, body); got != body {
|
||||
t.Errorf("getPart body = %q, want %q", got, body)
|
||||
}
|
||||
|
||||
headerPart := getPart("header", resp, body)
|
||||
if !strings.Contains(headerPart, "Server") || !strings.Contains(headerPart, "nginx") {
|
||||
t.Errorf("getPart header = %q, want it to include the header", headerPart)
|
||||
}
|
||||
if strings.Contains(headerPart, body) {
|
||||
t.Errorf("getPart header should not include body, got %q", headerPart)
|
||||
}
|
||||
|
||||
all := getPart("all", resp, body)
|
||||
if !strings.Contains(all, "nginx") || !strings.Contains(all, body) {
|
||||
t.Errorf("getPart all = %q, want both header and body", all)
|
||||
}
|
||||
|
||||
// an unrecognised part falls back to the body.
|
||||
if got := getPart("weird", resp, body); got != body {
|
||||
t.Errorf("getPart fallback = %q, want body %q", got, body)
|
||||
}
|
||||
|
||||
// empty part behaves like "all".
|
||||
if got := getPart("", resp, body); !strings.Contains(got, "nginx") || !strings.Contains(got, body) {
|
||||
t.Errorf("getPart empty = %q, want both header and body", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunExtractors(t *testing.T) {
|
||||
resp := fakeResponse(t, 200, http.Header{"X-Token": []string{"abc123"}})
|
||||
const body = `{"session":"sess-7788","role":"admin"}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
extractors []Extractor
|
||||
wantKey string
|
||||
wantVal string
|
||||
wantNil bool
|
||||
}{
|
||||
{
|
||||
name: "no extractors yields nil",
|
||||
extractors: nil,
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "regex capture group on body",
|
||||
extractors: []Extractor{
|
||||
{Type: "regex", Name: "session", Part: "body", Regex: []string{`"session":"([^"]+)"`}, Group: 1},
|
||||
},
|
||||
wantKey: "session",
|
||||
wantVal: "sess-7788",
|
||||
},
|
||||
{
|
||||
name: "group zero is the whole match",
|
||||
extractors: []Extractor{
|
||||
{Type: "regex", Name: "role", Part: "body", Regex: []string{`role":"admin`}, Group: 0},
|
||||
},
|
||||
wantKey: "role",
|
||||
wantVal: `role":"admin`,
|
||||
},
|
||||
{
|
||||
name: "extract from header part",
|
||||
extractors: []Extractor{
|
||||
{Type: "regex", Name: "token", Part: "header", Regex: []string{`X-Token: (\S+)`}, Group: 1},
|
||||
},
|
||||
wantKey: "token",
|
||||
wantVal: "abc123",
|
||||
},
|
||||
{
|
||||
name: "first matching pattern wins",
|
||||
extractors: []Extractor{
|
||||
{Type: "regex", Name: "session", Part: "body", Regex: []string{`nomatch(\d+)`, `"session":"([^"]+)"`}, Group: 1},
|
||||
},
|
||||
wantKey: "session",
|
||||
wantVal: "sess-7788",
|
||||
},
|
||||
{
|
||||
name: "group index out of range is skipped",
|
||||
extractors: []Extractor{
|
||||
{Type: "regex", Name: "session", Part: "body", Regex: []string{`"session":"([^"]+)"`}, Group: 5},
|
||||
},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "invalid pattern is skipped, no capture",
|
||||
extractors: []Extractor{
|
||||
{Type: "regex", Name: "session", Part: "body", Regex: []string{`(`}, Group: 1},
|
||||
},
|
||||
wantNil: true,
|
||||
},
|
||||
{
|
||||
name: "unknown extractor type is ignored",
|
||||
extractors: []Extractor{
|
||||
{Type: "bogus", Name: "session", Part: "body", Regex: []string{`"session":"([^"]+)"`}, Group: 1},
|
||||
},
|
||||
wantNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := runExtractors(tt.extractors, resp, body)
|
||||
if tt.wantNil {
|
||||
if len(got) != 0 {
|
||||
t.Errorf("runExtractors = %v, want empty", got)
|
||||
}
|
||||
return
|
||||
}
|
||||
if got[tt.wantKey] != tt.wantVal {
|
||||
t.Errorf("runExtractors[%q] = %q, want %q", tt.wantKey, got[tt.wantKey], tt.wantVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubstituteVariables(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
template string
|
||||
baseURL string
|
||||
payload string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "baseurl both cases",
|
||||
template: "{{BaseURL}}/x and {{baseurl}}/y",
|
||||
baseURL: "http://h",
|
||||
want: "http://h/x and http://h/y",
|
||||
},
|
||||
{
|
||||
name: "payload both cases",
|
||||
template: "q={{payload}}&r={{Payload}}",
|
||||
payload: "<script>",
|
||||
want: "q=<script>&r=<script>",
|
||||
},
|
||||
{
|
||||
name: "combined base and payload",
|
||||
template: "{{BaseURL}}/search?q={{payload}}",
|
||||
baseURL: "http://h",
|
||||
payload: "x",
|
||||
want: "http://h/search?q=x",
|
||||
},
|
||||
{
|
||||
name: "no placeholders untouched",
|
||||
template: "/static/path",
|
||||
baseURL: "http://h",
|
||||
want: "/static/path",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := substituteVariables(tt.template, tt.baseURL, tt.payload); got != tt.want {
|
||||
t.Errorf("substituteVariables = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHTTPRequests(t *testing.T) {
|
||||
t.Run("paths without payloads", func(t *testing.T) {
|
||||
cfg := &HTTPConfig{
|
||||
Paths: []string{"{{BaseURL}}/a", "{{BaseURL}}/b"},
|
||||
}
|
||||
// trailing slash on the target must be trimmed before substitution.
|
||||
got := generateHTTPRequests("http://h/", cfg)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("got %d requests, want 2", len(got))
|
||||
}
|
||||
if got[0].Method != "GET" {
|
||||
t.Errorf("default method = %q, want GET", got[0].Method)
|
||||
}
|
||||
if got[0].URL != "http://h/a" || got[1].URL != "http://h/b" {
|
||||
t.Errorf("urls = %q,%q", got[0].URL, got[1].URL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("payload expansion is path x payload", func(t *testing.T) {
|
||||
cfg := &HTTPConfig{
|
||||
Method: "POST",
|
||||
Paths: []string{"{{BaseURL}}/q?x={{payload}}"},
|
||||
Payloads: []string{"1", "2", "3"},
|
||||
Body: "data={{payload}}",
|
||||
}
|
||||
got := generateHTTPRequests("http://h", cfg)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("got %d requests, want 3", len(got))
|
||||
}
|
||||
for i, want := range []string{"1", "2", "3"} {
|
||||
if got[i].Payload != want {
|
||||
t.Errorf("req %d payload = %q, want %q", i, got[i].Payload, want)
|
||||
}
|
||||
if got[i].URL != "http://h/q?x="+want {
|
||||
t.Errorf("req %d url = %q", i, got[i].URL)
|
||||
}
|
||||
if got[i].Body != "data="+want {
|
||||
t.Errorf("req %d body = %q", i, got[i].Body)
|
||||
}
|
||||
if got[i].Method != "POST" {
|
||||
t.Errorf("req %d method = %q, want POST", i, got[i].Method)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple paths times multiple payloads", func(t *testing.T) {
|
||||
cfg := &HTTPConfig{
|
||||
Paths: []string{"{{BaseURL}}/a", "{{BaseURL}}/b"},
|
||||
Payloads: []string{"x", "y"},
|
||||
}
|
||||
got := generateHTTPRequests("http://h", cfg)
|
||||
if len(got) != 4 {
|
||||
t.Fatalf("got %d requests, want 4 (2 paths x 2 payloads)", len(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -91,6 +91,7 @@ type Matcher struct {
|
||||
Regex []string `yaml:"regex,omitempty"`
|
||||
Words []string `yaml:"words,omitempty"`
|
||||
Status []int `yaml:"status,omitempty"`
|
||||
Size []int `yaml:"size,omitempty"`
|
||||
Condition string `yaml:"condition"` // and, or
|
||||
Negative bool `yaml:"negative"`
|
||||
}
|
||||
@@ -98,7 +99,7 @@ type Matcher struct {
|
||||
// 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
|
||||
Type string `yaml:"type"` // regex, kv, json
|
||||
Name string `yaml:"name"`
|
||||
Part string `yaml:"part"`
|
||||
Regex []string `yaml:"regex,omitempty"`
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runOrchModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func orchExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestOrchestrationAPIExposureModules(t *testing.T) {
|
||||
const vault = "../../modules/recon/vault-api-exposure.yaml"
|
||||
const consul = "../../modules/recon/consul-api-exposure.yaml"
|
||||
const etcd = "../../modules/recon/etcd-api-exposure.yaml"
|
||||
|
||||
vaultSeal := `{"type":"shamir","initialized":true,"sealed":false,"t":3,"n":5,` +
|
||||
`"progress":0,"nonce":"","version":"1.15.2","build_date":"2023-11-06T11:33:49Z",` +
|
||||
`"migration":false,"cluster_name":"vault-cluster-9d52b1f1","recovery_seal":false,` +
|
||||
`"storage_type":"raft"}`
|
||||
|
||||
consulSelf := `{"Config":{"Datacenter":"dc1","NodeName":"consul-server-1","Server":true,` +
|
||||
`"Version":"1.17.0"},"Member":{"Name":"consul-server-1","Addr":"10.0.0.5","Port":8301}}`
|
||||
|
||||
etcdVersion := `{"etcdserver":"3.5.9","etcdcluster":"3.5.0"}`
|
||||
|
||||
t.Run("an exposed vault seal-status is flagged and versioned", func(t *testing.T) {
|
||||
res := runOrchModule(t, vault, 200, vaultSeal)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a vault finding")
|
||||
}
|
||||
if v := orchExtract(res, "vault_version"); v != "1.15.2" {
|
||||
t.Errorf("vault_version=%q, want 1.15.2", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed consul agent self leaks the datacenter", func(t *testing.T) {
|
||||
res := runOrchModule(t, consul, 200, consulSelf)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a consul finding")
|
||||
}
|
||||
if v := orchExtract(res, "consul_datacenter"); v != "dc1" {
|
||||
t.Errorf("consul_datacenter=%q, want dc1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an exposed etcd version endpoint is flagged and versioned", func(t *testing.T) {
|
||||
res := runOrchModule(t, etcd, 200, etcdVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an etcd finding")
|
||||
}
|
||||
if v := orchExtract(res, "etcd_version"); v != "3.5.9" {
|
||||
t.Errorf("etcd_version=%q, want 3.5.9", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a sealed flag without the other vault keys is not vault", func(t *testing.T) {
|
||||
body := `{"sealed":"yes","status":"ok"}`
|
||||
if res := runOrchModule(t, vault, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare sealed flag should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a datacenter field alone is not consul", func(t *testing.T) {
|
||||
body := `{"Datacenter":"dc1"}`
|
||||
if res := runOrchModule(t, consul, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare datacenter field should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a version response from another service is not etcd", func(t *testing.T) {
|
||||
body := `{"version":"1.2.3","service":"myapp"}`
|
||||
if res := runOrchModule(t, etcd, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("another service version should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an etcdserver without an etcdcluster is not flagged", func(t *testing.T) {
|
||||
body := `{"etcdserver":"3.5.9"}`
|
||||
if res := runOrchModule(t, etcd, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a partial etcd response should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{vault, consul, etcd} {
|
||||
if res := runOrchModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{vault, consul, etcd} {
|
||||
if res := runOrchModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runRailsModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func railsExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestRailsSecretExposureModules(t *testing.T) {
|
||||
const database = "../../modules/recon/rails-database-yml-exposure.yaml"
|
||||
const secrets = "../../modules/recon/rails-secrets-yml-exposure.yaml"
|
||||
const masterKey = "../../modules/recon/rails-master-key-exposure.yaml"
|
||||
|
||||
const keyBase = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +
|
||||
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
const masterKeyValue = "0123456789abcdef0123456789abcdef"
|
||||
|
||||
t.Run("database config leaks the database name and credentials", func(t *testing.T) {
|
||||
body := "default: &default\n adapter: postgresql\n encoding: unicode\n pool: 5\n" +
|
||||
" username: app_user\n password: s3cr3tdbpass\n host: db.internal\n\n" +
|
||||
"production:\n <<: *default\n database: myapp_production\n"
|
||||
res := runRailsModule(t, database, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a database config finding")
|
||||
}
|
||||
if v := railsExtract(res, "database"); v != "myapp_production" {
|
||||
t.Errorf("database=%q, want myapp_production", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a credential free sqlite database config is not a leak", func(t *testing.T) {
|
||||
body := "production:\n adapter: sqlite3\n database: db/production.sqlite3\n pool: 5\n"
|
||||
if res := runRailsModule(t, database, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a sqlite config without credentials should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("secrets config leaks the secret key base", func(t *testing.T) {
|
||||
body := "development:\n secret_key_base: " + keyBase + "\n"
|
||||
res := runRailsModule(t, secrets, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a secrets config finding")
|
||||
}
|
||||
if v := railsExtract(res, "secret_key_base"); v != keyBase {
|
||||
t.Errorf("secret_key_base=%q, want %q", v, keyBase)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("master key file leaks the key", func(t *testing.T) {
|
||||
res := runRailsModule(t, masterKey, 200, masterKeyValue)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a master key finding")
|
||||
}
|
||||
if v := railsExtract(res, "master_key"); v != masterKeyValue {
|
||||
t.Errorf("master_key=%q, want %q", v, masterKeyValue)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a longer hex digest is not the master key", func(t *testing.T) {
|
||||
body := masterKeyValue + masterKeyValue
|
||||
if res := runRailsModule(t, masterKey, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a 64 char digest should not match the 32 char key, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a hex value not at the body start is not the master key", func(t *testing.T) {
|
||||
body := "key=" + masterKeyValue
|
||||
if res := runRailsModule(t, masterKey, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a hex value away from the start should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an html page naming the rails markers is not a leak", func(t *testing.T) {
|
||||
body := "<html><head><title>Error</title></head><body>secret_key_base: " + keyBase + "</body></html>"
|
||||
if res := runRailsModule(t, secrets, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html page should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a config without the rails markers is not a leak", func(t *testing.T) {
|
||||
body := "password: hunter2\nusername: admin\nhost: db.internal\n"
|
||||
for _, file := range []string{database, secrets, masterKey} {
|
||||
if res := runRailsModule(t, file, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a config without the rails markers should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{database, secrets, masterKey} {
|
||||
if res := runRailsModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runSecretModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func secretExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestSecretFileExposureModules(t *testing.T) {
|
||||
const privkey = "../../modules/recon/private-key-exposure.yaml"
|
||||
const gitcred = "../../modules/recon/git-credentials-exposure.yaml"
|
||||
const pypirc = "../../modules/recon/pypirc-exposure.yaml"
|
||||
|
||||
opensshKey := "-----BEGIN OPENSSH PRIVATE KEY-----\n" +
|
||||
"b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQy\n" +
|
||||
"NTUxOQAAACD1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" +
|
||||
"-----END OPENSSH PRIVATE KEY-----\n"
|
||||
|
||||
rsaKey := "-----BEGIN RSA PRIVATE KEY-----\n" +
|
||||
"MIIEpAIBAAKCAQEArandombase64payloadthatstandsinforakeybodyhere1234567890\n" +
|
||||
"-----END RSA PRIVATE KEY-----\n"
|
||||
|
||||
gitCreds := "https://octocat:ghp_AbCdEf0123456789AbCdEf0123456789@github.com\n" +
|
||||
"https://deploy:s3cr3t@gitlab.example.com\n"
|
||||
|
||||
pypiConfig := "[distutils]\nindex-servers =\n pypi\n\n[pypi]\n" +
|
||||
"username = __token__\npassword = pypi-AgEIcHlwaS5vcmcCJDQ2Y2Q\n"
|
||||
|
||||
t.Run("an openssh private key is flagged and typed", func(t *testing.T) {
|
||||
res := runSecretModule(t, privkey, 200, opensshKey)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a private key finding")
|
||||
}
|
||||
if v := secretExtract(res, "key_type"); v != "OPENSSH" {
|
||||
t.Errorf("key_type=%q, want OPENSSH", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an rsa private key is flagged and typed", func(t *testing.T) {
|
||||
res := runSecretModule(t, privkey, 200, rsaKey)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a private key finding")
|
||||
}
|
||||
if v := secretExtract(res, "key_type"); v != "RSA" {
|
||||
t.Errorf("key_type=%q, want RSA", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a git credential store leaks its host", func(t *testing.T) {
|
||||
res := runSecretModule(t, gitcred, 200, gitCreds)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a git credential finding")
|
||||
}
|
||||
if v := secretExtract(res, "git_host"); v != "github.com" {
|
||||
t.Errorf("git_host=%q, want github.com", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a pypirc leaks the upload token", func(t *testing.T) {
|
||||
res := runSecretModule(t, pypirc, 200, pypiConfig)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a pypirc finding")
|
||||
}
|
||||
if v := secretExtract(res, "pypi_token"); v != "pypi-AgEIcHlwaS5vcmcCJDQ2Y2Q" {
|
||||
t.Errorf("pypi_token=%q, want the pypi- token", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a public key is not a private key", func(t *testing.T) {
|
||||
body := "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK\n" +
|
||||
"-----END PUBLIC KEY-----\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB user@host\n"
|
||||
if res := runSecretModule(t, privkey, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a public key should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("prose that names a private key is not the key", func(t *testing.T) {
|
||||
body := "Generate your private key with ssh-keygen and keep id_rsa secret."
|
||||
if res := runSecretModule(t, privkey, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("prose about keys should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a git remote url without a password is not a credential store", func(t *testing.T) {
|
||||
body := "https://github.com/octocat/hello-world.git\n"
|
||||
if res := runSecretModule(t, gitcred, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare remote url should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a pypi section without a credential is not a leak", func(t *testing.T) {
|
||||
body := "[distutils]\nindex-servers =\n pypi\n"
|
||||
if res := runSecretModule(t, pypirc, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a section with no credential should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("credentials shown in an html page are not a store", func(t *testing.T) {
|
||||
body := "<!DOCTYPE html><html><body>clone with https://user:pass@host.example</body></html>"
|
||||
if res := runSecretModule(t, gitcred, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html page should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a pypi config inside an html page is not a leak", func(t *testing.T) {
|
||||
body := "<html><head><title>docs</title></head><body><pre>[pypi]\npassword = pypi-x</pre></body></html>"
|
||||
if res := runSecretModule(t, pypirc, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html page should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{privkey, gitcred, pypirc} {
|
||||
if res := runSecretModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{privkey, gitcred, pypirc} {
|
||||
if res := runSecretModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runVectorDBModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func vectorDBExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestVectorDBExposureModules(t *testing.T) {
|
||||
const qdrant = "../../modules/recon/qdrant-api-exposure.yaml"
|
||||
const weaviate = "../../modules/recon/weaviate-api-exposure.yaml"
|
||||
const chroma = "../../modules/recon/chroma-api-exposure.yaml"
|
||||
|
||||
qdrantCollections := `{"result":{"collections":[{"name":"documents"},{"name":"embeddings"}]},` +
|
||||
`"status":"ok","time":0.000018}`
|
||||
|
||||
weaviateMeta := `{"hostname":"http://[::]:8080","modules":{"text2vec-openai":{"version":"v1.0.0"}},` +
|
||||
`"version":"1.23.7"}`
|
||||
|
||||
chromaHeartbeat := `{"nanosecond heartbeat":1718900000000000000}`
|
||||
|
||||
t.Run("a qdrant collections api is flagged and named", func(t *testing.T) {
|
||||
res := runVectorDBModule(t, qdrant, 200, qdrantCollections)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a qdrant finding")
|
||||
}
|
||||
if v := vectorDBExtract(res, "qdrant_collection"); v != "documents" {
|
||||
t.Errorf("qdrant_collection=%q, want documents", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a weaviate meta api is flagged with its hostname", func(t *testing.T) {
|
||||
res := runVectorDBModule(t, weaviate, 200, weaviateMeta)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a weaviate finding")
|
||||
}
|
||||
if v := vectorDBExtract(res, "weaviate_hostname"); v != "http://[::]:8080" {
|
||||
t.Errorf("weaviate_hostname=%q, want http://[::]:8080", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a chroma heartbeat api is flagged", func(t *testing.T) {
|
||||
res := runVectorDBModule(t, chroma, 200, chromaHeartbeat)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a chroma finding")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a qdrant status without a collections result is not flagged", func(t *testing.T) {
|
||||
body := `{"result":{"points":[{"id":1}]},"status":"ok","time":0.001}`
|
||||
if res := runVectorDBModule(t, qdrant, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a points result should not match qdrant, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a qdrant collections result without an ok status is not flagged", func(t *testing.T) {
|
||||
body := `{"result":{"collections":[{"name":"x"}]}}`
|
||||
if res := runVectorDBModule(t, qdrant, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a collections result without ok status should not match qdrant, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a weaviate meta without a version is not flagged", func(t *testing.T) {
|
||||
body := `{"hostname":"http://x:8080","modules":{"a":{}}}`
|
||||
if res := runVectorDBModule(t, weaviate, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a meta without a version should not match weaviate, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a weaviate hostname that is not a url is not flagged", func(t *testing.T) {
|
||||
body := `{"hostname":"db-internal","version":"1.23.7"}`
|
||||
if res := runVectorDBModule(t, weaviate, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare hostname should not match weaviate, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a chroma 200 without the heartbeat key is not flagged", func(t *testing.T) {
|
||||
body := `{"heartbeat":1718900000}`
|
||||
if res := runVectorDBModule(t, chroma, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain heartbeat key should not match chroma, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic version json is not a vector db", func(t *testing.T) {
|
||||
body := `{"version":"1.0.0","name":"app"}`
|
||||
for _, file := range []string{qdrant, weaviate, chroma} {
|
||||
if res := runVectorDBModule(t, file, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a generic version should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{qdrant, weaviate, chroma} {
|
||||
if res := runVectorDBModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{qdrant, weaviate, chroma} {
|
||||
if res := runVectorDBModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runWebSrvModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(status)
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := modules.ExecuteHTTPModule(context.Background(), srv.URL, def, modules.Options{
|
||||
Timeout: 5 * time.Second,
|
||||
Threads: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("execute %s: %v", file, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func webSrvExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestWebserverConfigExposureModules(t *testing.T) {
|
||||
const htpasswd = "../../modules/recon/htpasswd-exposure.yaml"
|
||||
const webconfig = "../../modules/recon/webconfig-exposure.yaml"
|
||||
const htaccess = "../../modules/recon/htaccess-exposure.yaml"
|
||||
|
||||
t.Run("htpasswd leaks the user and an apache md5 hash", func(t *testing.T) {
|
||||
body := "admin:$apr1$z9c.x1pq$Q8r6Jm0pYh0pX2yq4nN3l1\nbackup:$apr1$ab$cd\n"
|
||||
res := runWebSrvModule(t, htpasswd, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an htpasswd finding")
|
||||
}
|
||||
if v := webSrvExtract(res, "htpasswd_user"); v != "admin" {
|
||||
t.Errorf("htpasswd_user=%q, want admin", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("htpasswd with a bcrypt hash also matches", func(t *testing.T) {
|
||||
body := "deploy:$2y$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZ\n"
|
||||
if res := runWebSrvModule(t, htpasswd, 200, body); len(res.Findings) == 0 {
|
||||
t.Fatal("expected an htpasswd finding for a bcrypt hash")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("web.config leaks a connection string", func(t *testing.T) {
|
||||
body := `<?xml version="1.0"?><configuration><connectionStrings>` +
|
||||
`<add name="Default" connectionString="Server=db;Database=app;User Id=sa;Password=p@ss;" ` +
|
||||
`providerName="System.Data.SqlClient" /></connectionStrings></configuration>`
|
||||
res := runWebSrvModule(t, webconfig, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a web.config finding")
|
||||
}
|
||||
want := "Server=db;Database=app;User Id=sa;Password=p@ss;"
|
||||
if v := webSrvExtract(res, "connection_string"); v != want {
|
||||
t.Errorf("connection_string=%q, want %q", v, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("htaccess leaks the password file path", func(t *testing.T) {
|
||||
body := "RewriteEngine On\nAuthType Basic\nAuthName \"Restricted\"\n" +
|
||||
"AuthUserFile /var/www/.htpasswd\nRequire valid-user\n"
|
||||
res := runWebSrvModule(t, htaccess, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an htaccess finding")
|
||||
}
|
||||
if v := webSrvExtract(res, "auth_user_file"); v != "/var/www/.htpasswd" {
|
||||
t.Errorf("auth_user_file=%q, want /var/www/.htpasswd", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a minimal htaccess with only access control still flags", func(t *testing.T) {
|
||||
body := "Options -Indexes\nDeny from all\n"
|
||||
if res := runWebSrvModule(t, htaccess, 200, body); len(res.Findings) == 0 {
|
||||
t.Fatal("expected a finding for a deny-from-all htaccess")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plaintext password line is not a hash", func(t *testing.T) {
|
||||
body := "admin:notahashedpassword\n"
|
||||
if res := runWebSrvModule(t, htpasswd, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a plaintext line should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a configuration element without a dotnet section is not a leak", func(t *testing.T) {
|
||||
body := `<?xml version="1.0"?><configuration><customRoot><foo/></customRoot></configuration>`
|
||||
if res := runWebSrvModule(t, webconfig, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non dotnet configuration should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an html page is not an htaccess", func(t *testing.T) {
|
||||
body := "<html><head><title>x</title></head><body>RewriteEngine On AuthType Basic</body></html>"
|
||||
if res := runWebSrvModule(t, htaccess, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an html page should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{htpasswd, webconfig, htaccess} {
|
||||
if res := runWebSrvModule(t, file, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{htpasswd, webconfig, htaccess} {
|
||||
if res := runWebSrvModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -16,7 +16,7 @@
|
||||
: SIF - Blazing-fast pentesting suite :
|
||||
: Blaze - BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
-------------------------------------------------------------------------------------------------
|
||||
@@ -58,7 +58,7 @@ type HTTPConfig struct {
|
||||
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
|
||||
Attack string `yaml:"attack,omitempty"` // clusterbomb (default), pitchfork
|
||||
Threads int `yaml:"threads,omitempty"`
|
||||
Matchers []Matcher `yaml:"matchers"`
|
||||
Extractors []Extractor `yaml:"extractors,omitempty"`
|
||||
@@ -100,6 +100,12 @@ func ParseYAMLModule(path string) (*YAMLModule, error) {
|
||||
return nil, fmt.Errorf("module missing required field: type")
|
||||
}
|
||||
|
||||
if ym.HTTP != nil {
|
||||
if err := validateAttack(ym.HTTP.Attack); err != nil {
|
||||
return nil, fmt.Errorf("module %q: %w", ym.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &ym, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// env var names notify reads, env-first. these mirror the conventional names so
|
||||
// an operator who already exports them for other tooling gets notify for free.
|
||||
const (
|
||||
envSlackWebhook = "SLACK_WEBHOOK_URL"
|
||||
envDiscordWebhook = "DISCORD_WEBHOOK_URL"
|
||||
// the name of the env var holding the bot token, not the token itself.
|
||||
envTelegramToken = "TELEGRAM_BOT_TOKEN" //nolint:gosec // env var name, not a secret
|
||||
envTelegramChat = "TELEGRAM_CHAT_ID"
|
||||
envWebhookURL = "NOTIFY_WEBHOOK_URL"
|
||||
)
|
||||
|
||||
// config holds resolved destinations for every provider. yaml tags use
|
||||
// projectdiscovery/notify-compatible key names so an existing notify config file
|
||||
// ports over verbatim; env supplies the same values and yaml overrides it.
|
||||
type config struct {
|
||||
SlackWebhook string `yaml:"slack_webhook_url"`
|
||||
DiscordWebhook string `yaml:"discord_webhook_url"`
|
||||
// telegram needs both a bot token and a chat id. notify spells the token
|
||||
// "telegram_api_key", so accept that key for drop-in compatibility.
|
||||
TelegramToken string `yaml:"telegram_api_key"`
|
||||
TelegramChat string `yaml:"telegram_chat_id"`
|
||||
WebhookURL string `yaml:"webhook_url"`
|
||||
}
|
||||
|
||||
// loadConfig resolves notify destinations env-first, then overlays a yaml file
|
||||
// when path is non-empty. yaml wins per-field so a file value overrides the
|
||||
// matching env var; an unset yaml field leaves the env value intact. an empty
|
||||
// path means env-only. a missing/unparseable file is an error - if the operator
|
||||
// pointed -notify-config somewhere, a typo should fail loud, not silently drop.
|
||||
func loadConfig(path string) (config, error) {
|
||||
cfg := config{
|
||||
SlackWebhook: os.Getenv(envSlackWebhook),
|
||||
DiscordWebhook: os.Getenv(envDiscordWebhook),
|
||||
TelegramToken: os.Getenv(envTelegramToken),
|
||||
TelegramChat: os.Getenv(envTelegramChat),
|
||||
WebhookURL: os.Getenv(envWebhookURL),
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return config{}, fmt.Errorf("read config %q: %w", path, err)
|
||||
}
|
||||
|
||||
// decode into a separate value so only the keys present in the file overlay
|
||||
// the env-derived defaults; a zero field in the yaml must not blank an env var.
|
||||
var file config
|
||||
if err := yaml.Unmarshal(data, &file); err != nil {
|
||||
return config{}, fmt.Errorf("parse config %q: %w", path, err)
|
||||
}
|
||||
overlay(&cfg, &file)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// overlay copies non-empty fields from src onto dst. used to let a yaml file
|
||||
// override env without an empty yaml key wiping out a populated env value.
|
||||
func overlay(dst, src *config) {
|
||||
if src.SlackWebhook != "" {
|
||||
dst.SlackWebhook = src.SlackWebhook
|
||||
}
|
||||
if src.DiscordWebhook != "" {
|
||||
dst.DiscordWebhook = src.DiscordWebhook
|
||||
}
|
||||
if src.TelegramToken != "" {
|
||||
dst.TelegramToken = src.TelegramToken
|
||||
}
|
||||
if src.TelegramChat != "" {
|
||||
dst.TelegramChat = src.TelegramChat
|
||||
}
|
||||
if src.WebhookURL != "" {
|
||||
dst.WebhookURL = src.WebhookURL
|
||||
}
|
||||
}
|
||||
|
||||
// providers builds the live provider list from the resolved config: a provider
|
||||
// is included only when its destination is fully specified. telegram needs both
|
||||
// token and chat id, so a half-configured telegram is dropped rather than POSTing
|
||||
// to a broken endpoint.
|
||||
func (c *config) providers() []provider {
|
||||
var out []provider
|
||||
if c.SlackWebhook != "" {
|
||||
out = append(out, &slackProvider{webhook: c.SlackWebhook})
|
||||
}
|
||||
if c.DiscordWebhook != "" {
|
||||
out = append(out, &discordProvider{webhook: c.DiscordWebhook})
|
||||
}
|
||||
if c.TelegramToken != "" && c.TelegramChat != "" {
|
||||
out = append(out, &telegramProvider{token: c.TelegramToken, chatID: c.TelegramChat})
|
||||
}
|
||||
if c.WebhookURL != "" {
|
||||
out = append(out, &webhookProvider{url: c.WebhookURL})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// clearNotifyEnv unsets every var loadConfig reads so a test starts from a known
|
||||
// blank slate; t.Setenv("", "") still records the var for cleanup restoration.
|
||||
func clearNotifyEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
for _, k := range []string{
|
||||
envSlackWebhook, envDiscordWebhook,
|
||||
envTelegramToken, envTelegramChat, envWebhookURL,
|
||||
} {
|
||||
t.Setenv(k, "")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigEnvOnly(t *testing.T) {
|
||||
clearNotifyEnv(t)
|
||||
t.Setenv(envSlackWebhook, "https://hooks.slack.test/a")
|
||||
t.Setenv(envTelegramToken, "123:abc")
|
||||
t.Setenv(envTelegramChat, "999")
|
||||
|
||||
cfg, err := loadConfig("")
|
||||
if err != nil {
|
||||
t.Fatalf("loadConfig: %v", err)
|
||||
}
|
||||
if cfg.SlackWebhook != "https://hooks.slack.test/a" {
|
||||
t.Errorf("slack webhook = %q, want from env", cfg.SlackWebhook)
|
||||
}
|
||||
if cfg.TelegramToken != "123:abc" || cfg.TelegramChat != "999" {
|
||||
t.Errorf("telegram = %q/%q, want from env", cfg.TelegramToken, cfg.TelegramChat)
|
||||
}
|
||||
|
||||
// slack + telegram (both halves) configured, discord/webhook empty.
|
||||
got := cfg.providers()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("providers = %d, want 2 (slack, telegram)", len(got))
|
||||
}
|
||||
wantNames := map[string]bool{"slack": false, "telegram": false}
|
||||
for _, p := range got {
|
||||
wantNames[p.name()] = true
|
||||
}
|
||||
for name, seen := range wantNames {
|
||||
if !seen {
|
||||
t.Errorf("provider %q missing", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigYAMLOverridesEnv(t *testing.T) {
|
||||
clearNotifyEnv(t)
|
||||
t.Setenv(envSlackWebhook, "https://env.slack.test/x")
|
||||
t.Setenv(envWebhookURL, "https://env.webhook.test/x")
|
||||
|
||||
body := "" +
|
||||
"slack_webhook_url: https://file.slack.test/y\n" +
|
||||
"discord_webhook_url: https://file.discord.test/z\n"
|
||||
path := writeTempConfig(t, body)
|
||||
|
||||
cfg, err := loadConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("loadConfig: %v", err)
|
||||
}
|
||||
// yaml present -> overrides env.
|
||||
if cfg.SlackWebhook != "https://file.slack.test/y" {
|
||||
t.Errorf("slack = %q, want yaml override", cfg.SlackWebhook)
|
||||
}
|
||||
// yaml absent for webhook -> env value survives.
|
||||
if cfg.WebhookURL != "https://env.webhook.test/x" {
|
||||
t.Errorf("webhook = %q, want env value preserved", cfg.WebhookURL)
|
||||
}
|
||||
// yaml introduces discord.
|
||||
if cfg.DiscordWebhook != "https://file.discord.test/z" {
|
||||
t.Errorf("discord = %q, want from yaml", cfg.DiscordWebhook)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigNotifyCompatibleTelegramKey(t *testing.T) {
|
||||
clearNotifyEnv(t)
|
||||
// projectdiscovery/notify spells the bot token "telegram_api_key"; assert a
|
||||
// drop-in config wires telegram from that key.
|
||||
body := "" +
|
||||
"telegram_api_key: 555:tok\n" +
|
||||
"telegram_chat_id: \"42\"\n"
|
||||
path := writeTempConfig(t, body)
|
||||
|
||||
cfg, err := loadConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("loadConfig: %v", err)
|
||||
}
|
||||
if cfg.TelegramToken != "555:tok" || cfg.TelegramChat != "42" {
|
||||
t.Fatalf("telegram = %q/%q, want from notify-compatible keys", cfg.TelegramToken, cfg.TelegramChat)
|
||||
}
|
||||
if len(cfg.providers()) != 1 {
|
||||
t.Fatalf("providers = %d, want 1 (telegram)", len(cfg.providers()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigMissingFileErrors(t *testing.T) {
|
||||
clearNotifyEnv(t)
|
||||
if _, err := loadConfig(filepath.Join(t.TempDir(), "nope.yaml")); err == nil {
|
||||
t.Fatal("loadConfig with missing file: want error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigBadYAMLErrors(t *testing.T) {
|
||||
clearNotifyEnv(t)
|
||||
path := writeTempConfig(t, "slack_webhook_url: [unterminated\n")
|
||||
if _, err := loadConfig(path); err == nil {
|
||||
t.Fatal("loadConfig with malformed yaml: want error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersTelegramNeedsBothHalves(t *testing.T) {
|
||||
// token without chat id must not produce a (broken) telegram provider.
|
||||
cfg := config{TelegramToken: "tok"}
|
||||
if got := cfg.providers(); len(got) != 0 {
|
||||
t.Fatalf("providers = %d, want 0 for half-configured telegram", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersEmptyConfigIsNone(t *testing.T) {
|
||||
var cfg config
|
||||
if got := cfg.providers(); len(got) != 0 {
|
||||
t.Fatalf("providers = %d, want 0 for empty config", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// writeTempConfig writes body to a temp yaml file and returns its path.
|
||||
func writeTempConfig(t *testing.T, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(t.TempDir(), "notify.yaml")
|
||||
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
||||
t.Fatalf("write temp config: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// discordProvider posts to a discord webhook. discord's incoming-webhook body
|
||||
// keys the message on "content" (slack uses "text"); same code-block wrapping so
|
||||
// the finding columns line up in the channel.
|
||||
type discordProvider struct {
|
||||
webhook string
|
||||
}
|
||||
|
||||
func (d *discordProvider) name() string { return "discord" }
|
||||
|
||||
// discordPayload is the minimal webhook body: a single content field.
|
||||
type discordPayload struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
func (d *discordProvider) send(ctx context.Context, client *http.Client, findings []finding.Finding) error {
|
||||
payload := discordPayload{Content: codeBlock(renderFindings(findings))}
|
||||
return postJSON(ctx, client, d.webhook, payload)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
// contentTypeJSON is the body type every provider POSTs; all four speak json.
|
||||
const contentTypeJSON = "application/json"
|
||||
|
||||
// messageHeader prefixes the rendered finding block. kept terse - chat sinks
|
||||
// truncate, so the count and lead-in carry the signal.
|
||||
const messageHeader = "sif found %d finding(s):"
|
||||
|
||||
// renderFindings turns a batch into a single plain-text block, one finding per
|
||||
// line in the same "[severity] target module title" shape as the -silent sink so
|
||||
// a reader sees identical lines across stdout and chat. a strings.Builder keeps
|
||||
// the per-line concat to one allocation path.
|
||||
func renderFindings(findings []finding.Finding) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, messageHeader, len(findings))
|
||||
b.WriteByte('\n')
|
||||
for i := 0; i < len(findings); i++ {
|
||||
b.WriteString(findings[i].Line())
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// postJSON marshals payload and POSTs it to url through the shared client. it
|
||||
// drains+closes the response so the conn returns to httpx's pool, and treats any
|
||||
// non-2xx as a delivery failure so a 4xx from a bad webhook surfaces loudly.
|
||||
func postJSON(ctx context.Context, client *http.Client, url string, payload any) error {
|
||||
body, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal payload: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", contentTypeJSON)
|
||||
|
||||
resp, err := client.Do(req) //nolint:bodyclose // drained and closed via httpx.DrainClose
|
||||
if err != nil {
|
||||
return fmt.Errorf("post: %w", err)
|
||||
}
|
||||
defer httpx.DrainClose(resp)
|
||||
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("unexpected status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package notify ships findings to chat/webhook sinks (slack, discord, telegram,
|
||||
// generic webhook) so a continuous-recon run can alert on what it turns up. every
|
||||
// provider is one POST through httpx.Client, so the global proxy/rate-limit/header
|
||||
// config applies uniformly and there's no extra http stack to keep in sync.
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// Options carries the runtime knobs Send needs. Timeout bounds each provider's
|
||||
// POST; ConfigPath is an optional yaml file whose values override env. severity
|
||||
// filtering is the caller's job - Send ships whatever batch it's handed.
|
||||
type Options struct {
|
||||
Timeout time.Duration
|
||||
ConfigPath string
|
||||
}
|
||||
|
||||
// Send dispatches findings to every configured provider. config resolves
|
||||
// env-first, then a yaml file overlays it (notify-compatible key names). a
|
||||
// provider with no destination is skipped, so zero configured providers makes
|
||||
// Send a silent no-op - notify is opt-in and never errors just for being unwired.
|
||||
// an empty findings slice is also a no-op: nothing to report.
|
||||
func Send(ctx context.Context, findings []finding.Finding, opts Options) error {
|
||||
if len(findings) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(opts.ConfigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notify config: %w", err)
|
||||
}
|
||||
|
||||
providers := cfg.providers()
|
||||
if len(providers) == 0 {
|
||||
// nothing wired up; opt-in feature stays quiet rather than erroring.
|
||||
return nil
|
||||
}
|
||||
|
||||
log := output.Module("NOTIFY")
|
||||
client := httpx.Client(opts.Timeout)
|
||||
|
||||
// run every provider; a failure on one sink must not suppress the others, so
|
||||
// errors accumulate and the first is returned after all have been attempted.
|
||||
var firstErr error
|
||||
for i := 0; i < len(providers); i++ {
|
||||
p := providers[i]
|
||||
if err := p.send(ctx, client, findings); err != nil {
|
||||
log.Error("%s delivery failed: %v", p.name(), err)
|
||||
if firstErr == nil {
|
||||
firstErr = fmt.Errorf("%s: %w", p.name(), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Success("sent %d findings to %s", len(findings), p.name())
|
||||
}
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// provider is one delivery sink. name is for logging; send formats findings into
|
||||
// the sink's payload and POSTs it through the shared client.
|
||||
type provider interface {
|
||||
name() string
|
||||
send(ctx context.Context, client *http.Client, findings []finding.Finding) error
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// sampleFindings returns a small mixed-severity batch for payload assertions.
|
||||
func sampleFindings() []finding.Finding {
|
||||
return []finding.Finding{
|
||||
{Target: "https://a.test", Module: "cors", Severity: finding.SeverityHigh, Key: "cors:a", Title: "reflected origin", Raw: "ACAO echo"},
|
||||
{Target: "https://a.test", Module: "headers", Severity: finding.SeverityInfo, Key: "headers:x", Title: "Server header", Raw: "nginx"},
|
||||
}
|
||||
}
|
||||
|
||||
// capture records the method, content-type and raw body of the request a provider
|
||||
// makes, so each test can assert the wire shape without a real network.
|
||||
type capture struct {
|
||||
method string
|
||||
contentType string
|
||||
path string
|
||||
body []byte
|
||||
}
|
||||
|
||||
// captureServer stands up an httptest server that records the single inbound
|
||||
// request into c and replies 200, the happy path every provider expects.
|
||||
func captureServer(t *testing.T, c *capture) *httptest.Server {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
c.method = r.Method
|
||||
c.contentType = r.Header.Get("Content-Type")
|
||||
c.path = r.URL.Path
|
||||
c.body = body
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
func TestSlackPayloadShape(t *testing.T) {
|
||||
var c capture
|
||||
srv := captureServer(t, &c)
|
||||
|
||||
p := &slackProvider{webhook: srv.URL}
|
||||
if err := p.send(context.Background(), srv.Client(), sampleFindings()); err != nil {
|
||||
t.Fatalf("slack send: %v", err)
|
||||
}
|
||||
|
||||
assertPostJSON(t, c)
|
||||
var payload slackPayload
|
||||
if err := json.Unmarshal(c.body, &payload); err != nil {
|
||||
t.Fatalf("unmarshal slack body: %v", err)
|
||||
}
|
||||
// slack keys on "text"; both findings must appear, code-block fenced.
|
||||
if !strings.Contains(payload.Text, "reflected origin") || !strings.Contains(payload.Text, "Server header") {
|
||||
t.Errorf("slack text missing findings: %q", payload.Text)
|
||||
}
|
||||
if !strings.HasPrefix(payload.Text, "```") {
|
||||
t.Errorf("slack text not code-block fenced: %q", payload.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiscordPayloadShape(t *testing.T) {
|
||||
var c capture
|
||||
srv := captureServer(t, &c)
|
||||
|
||||
p := &discordProvider{webhook: srv.URL}
|
||||
if err := p.send(context.Background(), srv.Client(), sampleFindings()); err != nil {
|
||||
t.Fatalf("discord send: %v", err)
|
||||
}
|
||||
|
||||
assertPostJSON(t, c)
|
||||
var payload discordPayload
|
||||
if err := json.Unmarshal(c.body, &payload); err != nil {
|
||||
t.Fatalf("unmarshal discord body: %v", err)
|
||||
}
|
||||
// discord keys on "content", not "text".
|
||||
if !strings.Contains(payload.Content, "reflected origin") {
|
||||
t.Errorf("discord content missing finding: %q", payload.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTelegramPayloadShape(t *testing.T) {
|
||||
var c capture
|
||||
srv := captureServer(t, &c)
|
||||
|
||||
// repoint the bot api base at the test server for the lifetime of this test.
|
||||
orig := telegramAPIBase
|
||||
telegramAPIBase = srv.URL
|
||||
t.Cleanup(func() { telegramAPIBase = orig })
|
||||
|
||||
p := &telegramProvider{token: "555:tok", chatID: "42"}
|
||||
if err := p.send(context.Background(), srv.Client(), sampleFindings()); err != nil {
|
||||
t.Fatalf("telegram send: %v", err)
|
||||
}
|
||||
|
||||
assertPostJSON(t, c)
|
||||
// the token rides the path and the method is sendMessage.
|
||||
if c.path != "/bot555:tok/sendMessage" {
|
||||
t.Errorf("telegram path = %q, want /bot555:tok/sendMessage", c.path)
|
||||
}
|
||||
var payload telegramPayload
|
||||
if err := json.Unmarshal(c.body, &payload); err != nil {
|
||||
t.Fatalf("unmarshal telegram body: %v", err)
|
||||
}
|
||||
if payload.ChatID != "42" {
|
||||
t.Errorf("telegram chat_id = %q, want 42", payload.ChatID)
|
||||
}
|
||||
if !strings.Contains(payload.Text, "reflected origin") {
|
||||
t.Errorf("telegram text missing finding: %q", payload.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWebhookPayloadShape(t *testing.T) {
|
||||
var c capture
|
||||
srv := captureServer(t, &c)
|
||||
|
||||
p := &webhookProvider{url: srv.URL}
|
||||
if err := p.send(context.Background(), srv.Client(), sampleFindings()); err != nil {
|
||||
t.Fatalf("webhook send: %v", err)
|
||||
}
|
||||
|
||||
assertPostJSON(t, c)
|
||||
var payload webhookPayload
|
||||
if err := json.Unmarshal(c.body, &payload); err != nil {
|
||||
t.Fatalf("unmarshal webhook body: %v", err)
|
||||
}
|
||||
// generic webhook carries structured findings, not a prerendered blob.
|
||||
if payload.Count != 2 || len(payload.Findings) != 2 {
|
||||
t.Fatalf("webhook count = %d / %d findings, want 2", payload.Count, len(payload.Findings))
|
||||
}
|
||||
first := payload.Findings[0]
|
||||
if first.Severity != "high" {
|
||||
t.Errorf("webhook severity = %q, want canonical string \"high\"", first.Severity)
|
||||
}
|
||||
if first.Key != "cors:a" || first.Module != "cors" {
|
||||
t.Errorf("webhook finding fields wrong: %+v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderNon2xxIsError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
p := &slackProvider{webhook: srv.URL}
|
||||
if err := p.send(context.Background(), srv.Client(), sampleFindings()); err == nil {
|
||||
t.Fatal("send to 403 endpoint: want error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendNoProviderIsNoop(t *testing.T) {
|
||||
clearNotifyEnv(t)
|
||||
// no env, no config file -> zero providers -> Send must not error.
|
||||
if err := Send(context.Background(), sampleFindings(), Options{Timeout: time.Second}); err != nil {
|
||||
t.Fatalf("Send with no provider: want nil, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendEmptyFindingsIsNoop(t *testing.T) {
|
||||
// even with a provider configured, an empty batch must not POST anything.
|
||||
hit := false
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
hit = true
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
clearNotifyEnv(t)
|
||||
t.Setenv(envSlackWebhook, srv.URL)
|
||||
if err := Send(context.Background(), nil, Options{Timeout: time.Second}); err != nil {
|
||||
t.Fatalf("Send with empty findings: want nil, got %v", err)
|
||||
}
|
||||
if hit {
|
||||
t.Fatal("Send with empty findings posted to provider, want no-op")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendDeliversToConfiguredProvider(t *testing.T) {
|
||||
var c capture
|
||||
srv := captureServer(t, &c)
|
||||
|
||||
clearNotifyEnv(t)
|
||||
t.Setenv(envSlackWebhook, srv.URL)
|
||||
if err := Send(context.Background(), sampleFindings(), Options{Timeout: time.Second}); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
if c.method != http.MethodPost {
|
||||
t.Fatalf("provider not hit (method=%q)", c.method)
|
||||
}
|
||||
}
|
||||
|
||||
// assertPostJSON checks the request was a json POST.
|
||||
func assertPostJSON(t *testing.T, c capture) {
|
||||
t.Helper()
|
||||
if c.method != http.MethodPost {
|
||||
t.Errorf("method = %q, want POST", c.method)
|
||||
}
|
||||
if c.contentType != contentTypeJSON {
|
||||
t.Errorf("content-type = %q, want %q", c.contentType, contentTypeJSON)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// slackProvider posts to a slack incoming webhook. the webhook url already pins
|
||||
// the channel, so the payload is just the rendered text in slack's mrkdwn-aware
|
||||
// "text" field wrapped in a code block to keep the fixed-width finding lines.
|
||||
type slackProvider struct {
|
||||
webhook string
|
||||
}
|
||||
|
||||
func (s *slackProvider) name() string { return "slack" }
|
||||
|
||||
// slackPayload is the minimal incoming-webhook body: a single text field.
|
||||
type slackPayload struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (s *slackProvider) send(ctx context.Context, client *http.Client, findings []finding.Finding) error {
|
||||
payload := slackPayload{Text: codeBlock(renderFindings(findings))}
|
||||
return postJSON(ctx, client, s.webhook, payload)
|
||||
}
|
||||
|
||||
// codeBlock wraps body in a triple-backtick fence; both slack and discord render
|
||||
// it fixed-width, which preserves the column-aligned finding lines.
|
||||
func codeBlock(body string) string {
|
||||
return "```\n" + body + "```"
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// telegramAPIBase is the bot api root. it's a var so tests can repoint it at an
|
||||
// httptest server; the token is appended path-side per telegram's scheme.
|
||||
var telegramAPIBase = "https://api.telegram.org"
|
||||
|
||||
// telegramProvider posts via the bot api's sendMessage. unlike slack/discord the
|
||||
// destination isn't a single opaque webhook: it needs the bot token (in the url
|
||||
// path) plus the chat id (in the body).
|
||||
type telegramProvider struct {
|
||||
token string
|
||||
chatID string
|
||||
}
|
||||
|
||||
func (t *telegramProvider) name() string { return "telegram" }
|
||||
|
||||
// telegramPayload is the sendMessage body. parse_mode "MarkdownV2" would force
|
||||
// escaping every special char in the finding lines, so we send plain text and
|
||||
// let the lines stand as-is.
|
||||
type telegramPayload struct {
|
||||
ChatID string `json:"chat_id"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (t *telegramProvider) send(ctx context.Context, client *http.Client, findings []finding.Finding) error {
|
||||
endpoint := telegramAPIBase + "/bot" + t.token + "/sendMessage"
|
||||
payload := telegramPayload{ChatID: t.chatID, Text: renderFindings(findings)}
|
||||
return postJSON(ctx, client, endpoint, payload)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// webhookProvider posts a structured json payload to an arbitrary endpoint. unlike
|
||||
// the chat sinks it carries the findings as data, not a prerendered blob, so
|
||||
// downstream automation (a siem, a bot, ci) keys off the fields directly.
|
||||
type webhookProvider struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func (w *webhookProvider) name() string { return "webhook" }
|
||||
|
||||
// webhookFinding is the per-item wire shape: the normalized Finding fields with
|
||||
// severity flattened to its canonical string so a json consumer never sees the
|
||||
// internal integer rank.
|
||||
type webhookFinding struct {
|
||||
Target string `json:"target"`
|
||||
Module string `json:"module"`
|
||||
Severity string `json:"severity"`
|
||||
Key string `json:"key"`
|
||||
Title string `json:"title"`
|
||||
Raw string `json:"raw,omitempty"`
|
||||
}
|
||||
|
||||
// webhookPayload wraps the batch with a count so a consumer can size buffers /
|
||||
// assert completeness without walking the slice first.
|
||||
type webhookPayload struct {
|
||||
Count int `json:"count"`
|
||||
Findings []webhookFinding `json:"findings"`
|
||||
}
|
||||
|
||||
func (w *webhookProvider) send(ctx context.Context, client *http.Client, findings []finding.Finding) error {
|
||||
items := make([]webhookFinding, 0, len(findings))
|
||||
for i := 0; i < len(findings); i++ {
|
||||
f := findings[i]
|
||||
items = append(items, webhookFinding{
|
||||
Target: f.Target,
|
||||
Module: f.Module,
|
||||
Severity: f.Severity.String(),
|
||||
Key: f.Key,
|
||||
Title: f.Title,
|
||||
Raw: f.Raw,
|
||||
})
|
||||
}
|
||||
payload := webhookPayload{Count: len(items), Findings: items}
|
||||
return postJSON(ctx, client, w.url, payload)
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -14,22 +14,22 @@ package format
|
||||
|
||||
import (
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
nucleiout "github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
)
|
||||
|
||||
func FormatLine(event *output.ResultEvent) string {
|
||||
output := event.TemplateID
|
||||
func FormatLine(event *nucleiout.ResultEvent) string {
|
||||
line := event.TemplateID
|
||||
|
||||
if event.MatcherName != "" {
|
||||
output += ":" + styles.Highlight.Render(event.MatcherName)
|
||||
line += ":" + styles.Highlight.Render(event.MatcherName)
|
||||
} else if event.ExtractorName != "" {
|
||||
output += ":" + styles.Highlight.Render(event.ExtractorName)
|
||||
line += ":" + styles.Highlight.Render(event.ExtractorName)
|
||||
}
|
||||
|
||||
output += " [" + event.Type + "]"
|
||||
output += " [" + formatSeverity(event.Info.SeverityHolder.Severity.String()) + "]"
|
||||
line += " [" + event.Type + "]"
|
||||
line += " [" + formatSeverity(event.Info.SeverityHolder.Severity.String()) + "]"
|
||||
|
||||
return output
|
||||
return line
|
||||
}
|
||||
|
||||
func formatSeverity(severity string) string {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -21,6 +21,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
)
|
||||
@@ -53,10 +55,20 @@ func Install(logger *log.Logger) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tarball.Close()
|
||||
defer func() {
|
||||
if cerr := tarball.Close(); cerr != nil {
|
||||
logger.Warnf("closing gzip reader: %v", cerr)
|
||||
}
|
||||
}()
|
||||
|
||||
data := tar.NewReader(tarball)
|
||||
|
||||
dest, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cleanDest := filepath.Clean(dest)
|
||||
|
||||
for {
|
||||
header, err := data.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
@@ -66,17 +78,25 @@ func Install(logger *log.Logger) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// guard against path traversal ("Zip Slip"): the resolved path must
|
||||
// stay within the extraction directory before any filesystem op.
|
||||
target := filepath.Join(cleanDest, header.Name)
|
||||
if !strings.HasPrefix(target, cleanDest+string(os.PathSeparator)) {
|
||||
return fmt.Errorf("invalid archive entry %q: escapes extraction directory", header.Name)
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.Mkdir(header.Name, 0o755); err != nil {
|
||||
if err := os.Mkdir(target, 0o750); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg:
|
||||
file, err := os.Create(header.Name)
|
||||
file, err := os.Create(target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(file, data); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
file.Close()
|
||||
|
||||
+62
-26
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -14,6 +14,7 @@ package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -126,13 +127,47 @@ func SetAPIMode(enabled bool) {
|
||||
apiMode = enabled
|
||||
}
|
||||
|
||||
// sink is where all banner/spinner/log chrome is written. it defaults to stdout
|
||||
// so normal runs are unchanged; -silent repoints it at stderr so stdout carries
|
||||
// nothing but the machine-readable findings a downstream pipe consumes.
|
||||
var sink io.Writer = os.Stdout
|
||||
|
||||
// silent is the plain-sink mode: chrome goes to stderr and interactive widgets
|
||||
// (spinners, live progress) are suppressed so a piped consumer never sees them.
|
||||
var silent bool
|
||||
|
||||
// SetSilent routes all chrome to stderr and marks the run non-interactive.
|
||||
// findings are printed to stdout by the caller via Finding/PrintFinding; the
|
||||
// output package itself never touches stdout once silent is on.
|
||||
func SetSilent(enabled bool) {
|
||||
silent = enabled
|
||||
if enabled {
|
||||
sink = os.Stderr
|
||||
return
|
||||
}
|
||||
sink = os.Stdout
|
||||
}
|
||||
|
||||
// Silent reports whether plain-sink mode is active. callers gate interactive
|
||||
// behaviour (spinners, prompts) on this.
|
||||
func Silent() bool {
|
||||
return silent
|
||||
}
|
||||
|
||||
// Writer is the current chrome sink (stdout normally, stderr under -silent).
|
||||
// callers that render their own chrome (the startup banner) write here so it
|
||||
// follows the same routing as everything else.
|
||||
func Writer() io.Writer {
|
||||
return sink
|
||||
}
|
||||
|
||||
// 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)
|
||||
fmt.Fprintf(sink, "%s %s\n", prefixInfo.Render("[*]"), msg)
|
||||
}
|
||||
|
||||
// Success prints a success message with [+] prefix
|
||||
@@ -141,7 +176,7 @@ func Success(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", prefixSuccess.Render("[+]"), msg)
|
||||
fmt.Fprintf(sink, "%s %s\n", prefixSuccess.Render("[+]"), msg)
|
||||
}
|
||||
|
||||
// Warn prints a warning message with [!] prefix
|
||||
@@ -150,7 +185,7 @@ func Warn(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", prefixWarning.Render("[!]"), msg)
|
||||
fmt.Fprintf(sink, "%s %s\n", prefixWarning.Render("[!]"), msg)
|
||||
}
|
||||
|
||||
// Error prints an error message with [-] prefix
|
||||
@@ -159,7 +194,7 @@ func Error(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", prefixError.Render("[-]"), msg)
|
||||
fmt.Fprintf(sink, "%s %s\n", prefixError.Render("[-]"), msg)
|
||||
}
|
||||
|
||||
// ScanStart prints a styled scan start message
|
||||
@@ -167,7 +202,7 @@ func ScanStart(scanName string) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s starting %s\n", prefixInfo.Render("[*]"), scanName)
|
||||
fmt.Fprintf(sink, "%s starting %s\n", prefixInfo.Render("[*]"), scanName)
|
||||
}
|
||||
|
||||
// ScanComplete prints a styled scan completion message
|
||||
@@ -175,7 +210,7 @@ func ScanComplete(scanName string, resultCount int, resultType string) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s %s complete (%d %s)\n", prefixInfo.Render("[*]"), scanName, resultCount, resultType)
|
||||
fmt.Fprintf(sink, "%s %s complete (%d %s)\n", prefixInfo.Render("[*]"), scanName, resultCount, resultType)
|
||||
}
|
||||
|
||||
// Module creates a prefixed logger for a specific module/tool
|
||||
@@ -202,7 +237,7 @@ func (m *ModuleLogger) Info(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s\n", m.prefix(), msg)
|
||||
fmt.Fprintf(sink, "%s %s\n", m.prefix(), msg)
|
||||
}
|
||||
|
||||
// Success prints a success message with module prefix
|
||||
@@ -211,7 +246,7 @@ func (m *ModuleLogger) Success(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s %s\n", m.prefix(), prefixSuccess.Render("✓"), msg)
|
||||
fmt.Fprintf(sink, "%s %s %s\n", m.prefix(), prefixSuccess.Render("✓"), msg)
|
||||
}
|
||||
|
||||
// Warn prints a warning message with module prefix
|
||||
@@ -220,7 +255,7 @@ func (m *ModuleLogger) Warn(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s %s\n", m.prefix(), prefixWarning.Render("!"), msg)
|
||||
fmt.Fprintf(sink, "%s %s %s\n", m.prefix(), prefixWarning.Render("!"), msg)
|
||||
}
|
||||
|
||||
// Error prints an error message with module prefix
|
||||
@@ -229,7 +264,7 @@ func (m *ModuleLogger) Error(format string, args ...interface{}) {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s %s %s\n", m.prefix(), prefixError.Render("✗"), msg)
|
||||
fmt.Fprintf(sink, "%s %s %s\n", m.prefix(), prefixError.Render("✗"), msg)
|
||||
}
|
||||
|
||||
// Start prints a scan start message with module prefix (adds newline before for separation)
|
||||
@@ -237,7 +272,7 @@ func (m *ModuleLogger) Start() {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("\n%s starting scan\n", m.prefix())
|
||||
fmt.Fprintf(sink, "\n%s starting scan\n", m.prefix())
|
||||
}
|
||||
|
||||
// Complete prints a scan complete message with module prefix
|
||||
@@ -245,15 +280,16 @@ func (m *ModuleLogger) Complete(resultCount int, resultType string) {
|
||||
if apiMode {
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s complete (%d %s)\n", m.prefix(), resultCount, resultType)
|
||||
fmt.Fprintf(sink, "%s complete (%d %s)\n", m.prefix(), resultCount, resultType)
|
||||
}
|
||||
|
||||
// ClearLine clears the current line (for progress bar updates)
|
||||
// ClearLine clears the current line (for progress bar updates). silent mode is
|
||||
// non-interactive, so there's no live line to clear and stdout stays untouched.
|
||||
func ClearLine() {
|
||||
if !IsTTY {
|
||||
if !IsTTY || silent {
|
||||
return
|
||||
}
|
||||
fmt.Print("\033[2K\r")
|
||||
fmt.Fprint(sink, "\033[2K\r")
|
||||
}
|
||||
|
||||
// Summary styles
|
||||
@@ -274,22 +310,22 @@ func PrintSummary(scans []string, logFiles []string) {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
|
||||
fmt.Println()
|
||||
fmt.Printf(" %s\n", summaryHeader.Render("SCAN COMPLETE"))
|
||||
fmt.Println()
|
||||
fmt.Fprintln(sink)
|
||||
fmt.Fprintln(sink, summaryLine.Render("────────────────────────────────────────────────────────────"))
|
||||
fmt.Fprintln(sink)
|
||||
fmt.Fprintf(sink, " %s\n", summaryHeader.Render("SCAN COMPLETE"))
|
||||
fmt.Fprintln(sink)
|
||||
|
||||
// Print scans
|
||||
scanList := strings.Join(scans, ", ")
|
||||
fmt.Printf(" %s %s\n", Muted.Render("Scans:"), scanList)
|
||||
fmt.Fprintf(sink, " %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.Fprintf(sink, " %s %s\n", Muted.Render("Output:"), strings.Join(logFiles, ", "))
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
|
||||
fmt.Println()
|
||||
fmt.Fprintln(sink)
|
||||
fmt.Fprintln(sink, summaryLine.Render("────────────────────────────────────────────────────────────"))
|
||||
fmt.Fprintln(sink)
|
||||
}
|
||||
|
||||
+38
-12
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -28,12 +28,13 @@ const (
|
||||
|
||||
// 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
|
||||
total int64
|
||||
current int64
|
||||
message string
|
||||
lastItem string
|
||||
mu sync.Mutex
|
||||
paused bool
|
||||
lastShown int // last printed milestone bucket in non-tty mode
|
||||
}
|
||||
|
||||
// NewProgress creates a new progress bar
|
||||
@@ -97,7 +98,7 @@ func (p *Progress) Done() {
|
||||
}
|
||||
|
||||
func (p *Progress) render() {
|
||||
if apiMode {
|
||||
if apiMode || silent {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -105,11 +106,36 @@ func (p *Progress) render() {
|
||||
if !IsTTY {
|
||||
current := atomic.LoadInt64(&p.current)
|
||||
total := p.total
|
||||
if total <= 0 {
|
||||
return
|
||||
}
|
||||
percent := int(current * 100 / total)
|
||||
|
||||
// Print at 0%, 25%, 50%, 75%, 100%
|
||||
if current == 1 || percent == 25 || percent == 50 || percent == 75 || current == total {
|
||||
fmt.Printf(" [%d%%] %d/%d\n", percent, current, total)
|
||||
// map current to a milestone bucket (0=none,1..5). concurrent workers
|
||||
// hammer the same bucket, so only print when the bucket advances.
|
||||
bucket := 0
|
||||
switch {
|
||||
case current >= total:
|
||||
bucket = 5
|
||||
case percent >= 75:
|
||||
bucket = 4
|
||||
case percent >= 50:
|
||||
bucket = 3
|
||||
case percent >= 25:
|
||||
bucket = 2
|
||||
case current >= 1:
|
||||
bucket = 1
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
advanced := bucket > p.lastShown
|
||||
if advanced {
|
||||
p.lastShown = bucket
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
if advanced {
|
||||
fmt.Fprintf(sink, " [%d%%] %d/%d\n", percent, current, total)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -164,5 +190,5 @@ func (p *Progress) render() {
|
||||
)
|
||||
|
||||
ClearLine()
|
||||
fmt.Print(line)
|
||||
fmt.Fprint(sink, line)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package output
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// the non-tty milestone path divides current*100/total, so a zero-total bar
|
||||
// used to panic with integer divide-by-zero when piped or redirected.
|
||||
func TestProgressZeroTotalNoPanic(t *testing.T) {
|
||||
p := NewProgress(0, "scanning")
|
||||
p.Increment("item")
|
||||
p.Set(0, "item")
|
||||
p.Done()
|
||||
}
|
||||
|
||||
func TestProgressCounts(t *testing.T) {
|
||||
p := NewProgress(4, "scanning")
|
||||
for i := 0; i < 4; i++ {
|
||||
p.Increment("x")
|
||||
}
|
||||
if p.current != 4 {
|
||||
t.Errorf("current = %d, want 4", p.current)
|
||||
}
|
||||
}
|
||||
|
||||
// many concurrent workers used to spam the same milestone bucket (e.g. ten
|
||||
// "[25%] .../1000" lines). each bucket must now print at most once.
|
||||
func TestProgressNonTTYDedupesMilestones(t *testing.T) {
|
||||
savedTTY, savedAPI := IsTTY, apiMode
|
||||
IsTTY, apiMode = false, false
|
||||
defer func() { IsTTY, apiMode = savedTTY, savedAPI }()
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
p := NewProgress(1000, "scanning")
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 40; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 25; j++ {
|
||||
p.Increment("x")
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
lines := strings.Count(out, "\n")
|
||||
if lines > 5 {
|
||||
t.Errorf("printed %d milestone lines, want <=5:\n%s", lines, out)
|
||||
}
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe: %v", err)
|
||||
}
|
||||
saved := os.Stdout
|
||||
os.Stdout = w
|
||||
|
||||
done := make(chan string, 1)
|
||||
go func() {
|
||||
buf := make([]byte, 0, 4096)
|
||||
tmp := make([]byte, 1024)
|
||||
for {
|
||||
n, rerr := r.Read(tmp)
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
done <- string(buf)
|
||||
}()
|
||||
|
||||
fn()
|
||||
os.Stdout = saved
|
||||
w.Close()
|
||||
return <-done
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package output
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// in silent mode chrome must land on stderr and leave stdout untouched, so a
|
||||
// piped consumer downstream never sees a banner or log line.
|
||||
func TestSetSilentRoutesChromeToStderr(t *testing.T) {
|
||||
defer SetSilent(false)
|
||||
|
||||
outStr, errStr := captureStdoutStderr(t, func() {
|
||||
// SetSilent reads os.Stderr at call time, so swap then set.
|
||||
SetSilent(true)
|
||||
Info("scanning %s", "example.com")
|
||||
Success("done")
|
||||
})
|
||||
|
||||
if outStr != "" {
|
||||
t.Errorf("silent mode wrote chrome to stdout: %q", outStr)
|
||||
}
|
||||
if !strings.Contains(errStr, "scanning example.com") {
|
||||
t.Errorf("silent chrome missing from stderr: %q", errStr)
|
||||
}
|
||||
}
|
||||
|
||||
// the default (non-silent) sink is stdout; flipping silent off must restore it.
|
||||
func TestSetSilentOffRoutesChromeToStdout(t *testing.T) {
|
||||
outStr, errStr := captureStdoutStderr(t, func() {
|
||||
SetSilent(false)
|
||||
Info("hello")
|
||||
})
|
||||
|
||||
if !strings.Contains(outStr, "hello") {
|
||||
t.Errorf("non-silent chrome missing from stdout: %q", outStr)
|
||||
}
|
||||
if strings.Contains(errStr, "hello") {
|
||||
t.Errorf("non-silent chrome leaked to stderr: %q", errStr)
|
||||
}
|
||||
}
|
||||
|
||||
// Silent() reflects the toggle so callers can gate interactive widgets.
|
||||
func TestSilentToggle(t *testing.T) {
|
||||
defer SetSilent(false)
|
||||
SetSilent(true)
|
||||
if !Silent() {
|
||||
t.Error("Silent() = false after SetSilent(true)")
|
||||
}
|
||||
SetSilent(false)
|
||||
if Silent() {
|
||||
t.Error("Silent() = true after SetSilent(false)")
|
||||
}
|
||||
}
|
||||
|
||||
// captureStdoutStderr swaps both real streams for pipes, runs fn, and returns
|
||||
// what landed on each. SetSilent reads os.Stdout/os.Stderr at call time, so the
|
||||
// swap has to happen before fn flips the sink - fn does that itself.
|
||||
func captureStdoutStderr(t *testing.T, fn func()) (string, string) {
|
||||
t.Helper()
|
||||
|
||||
outR, outW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe stdout: %v", err)
|
||||
}
|
||||
errR, errW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("pipe stderr: %v", err)
|
||||
}
|
||||
|
||||
savedOut, savedErr := os.Stdout, os.Stderr
|
||||
os.Stdout, os.Stderr = outW, errW
|
||||
|
||||
outCh := drain(outR)
|
||||
errCh := drain(errR)
|
||||
|
||||
fn()
|
||||
|
||||
os.Stdout, os.Stderr = savedOut, savedErr
|
||||
outW.Close()
|
||||
errW.Close()
|
||||
return <-outCh, <-errCh
|
||||
}
|
||||
|
||||
func drain(r *os.File) <-chan string {
|
||||
ch := make(chan string, 1)
|
||||
go func() {
|
||||
buf := make([]byte, 0, 4096)
|
||||
tmp := make([]byte, 1024)
|
||||
for {
|
||||
n, rerr := r.Read(tmp)
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
ch <- string(buf)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -14,7 +14,6 @@ package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
@@ -42,7 +41,7 @@ func NewSpinner(message string) *Spinner {
|
||||
|
||||
// Start begins the spinner animation
|
||||
func (s *Spinner) Start() {
|
||||
if apiMode {
|
||||
if apiMode || silent {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,7 +56,7 @@ func (s *Spinner) Start() {
|
||||
|
||||
// In non-TTY mode, just print the message once
|
||||
if !IsTTY {
|
||||
fmt.Printf(" %s...\n", s.message)
|
||||
fmt.Fprintf(sink, " %s...\n", s.message)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ func (s *Spinner) Start() {
|
||||
|
||||
// Stop halts the spinner and clears the line
|
||||
func (s *Spinner) Stop() {
|
||||
if apiMode {
|
||||
if apiMode || silent {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -112,8 +111,8 @@ func (s *Spinner) animate() {
|
||||
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)
|
||||
fmt.Fprint(sink, "\033[2K") // Clear line
|
||||
fmt.Fprint(sink, line)
|
||||
|
||||
frame = (frame + 1) % len(spinnerFrames)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package patchnotes shows release notes pulled from the github releases.
|
||||
package patchnotes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/glamour"
|
||||
)
|
||||
|
||||
const releasesAPI = "https://api.github.com/repos/vmfunc/sif/releases"
|
||||
|
||||
type release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
URL string `json:"html_url"`
|
||||
}
|
||||
|
||||
func fetch(ctx context.Context, path string) (*release, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releasesAPI+path, http.NoBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("github returned %s", resp.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var r release
|
||||
if err := json.Unmarshal(body, &r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// render turns a release's markdown body into styled terminal output, falling
|
||||
// back to the raw body if glamour can't render it.
|
||||
func render(r *release) string {
|
||||
out, err := glamour.Render(r.Body, "dark")
|
||||
if err != nil {
|
||||
return r.Body
|
||||
}
|
||||
return fmt.Sprintf("%s\n%s", r.TagName, out)
|
||||
}
|
||||
|
||||
// Print fetches the latest release and writes its notes to stdout. tag may be
|
||||
// empty for the latest release, or a "vX" tag for a specific one.
|
||||
func Print(tag string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
path := "/latest"
|
||||
if tag != "" {
|
||||
path = "/tags/" + tag
|
||||
}
|
||||
|
||||
r, err := fetch(ctx, path)
|
||||
if err != nil {
|
||||
fmt.Printf("couldn't fetch patch notes: %v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Print(render(r))
|
||||
}
|
||||
|
||||
// ShowOnce prints the running version's notes the first time that version runs,
|
||||
// then records it so it isn't shown again. best-effort: dev builds, the
|
||||
// SIF_NO_PATCHNOTES opt-out, and any network failure stay silent.
|
||||
func ShowOnce(version string) {
|
||||
// only clean release tags (e.g. 2026.6.7) map to a github release; skip dev
|
||||
// and pseudo-versions (a commit/dirty build) so we don't make a doomed call.
|
||||
if version == "" || version == "dev" || strings.ContainsAny(version, "-+") || os.Getenv("SIF_NO_PATCHNOTES") != "" {
|
||||
return
|
||||
}
|
||||
|
||||
path, err := statePath()
|
||||
if err != nil || hasSeen(path, version) {
|
||||
return
|
||||
}
|
||||
// record before fetching so a flaky network doesn't nag on every run
|
||||
recordSeen(path, version)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
r, err := fetch(ctx, "/tags/v"+version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fmt.Printf("\nwhat's new in this release:\n%s", render(r))
|
||||
}
|
||||
|
||||
func statePath() (string, error) {
|
||||
dir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(dir, "sif", "seen_version"), nil
|
||||
}
|
||||
|
||||
func hasSeen(path, version string) bool {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(data)) == version
|
||||
}
|
||||
|
||||
func recordSeen(path, version string) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
|
||||
return
|
||||
}
|
||||
_ = os.WriteFile(path, []byte(version), 0o600)
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package patchnotes
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSeenRoundTrip(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "sif", "seen_version")
|
||||
|
||||
if hasSeen(path, "2026.6.7") {
|
||||
t.Fatal("nothing recorded yet, hasSeen should be false")
|
||||
}
|
||||
|
||||
recordSeen(path, "2026.6.7")
|
||||
if !hasSeen(path, "2026.6.7") {
|
||||
t.Error("recorded version should read back as seen")
|
||||
}
|
||||
if hasSeen(path, "2026.6.8") {
|
||||
t.Error("a different version should not be seen")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderIncludesTag(t *testing.T) {
|
||||
out := render(&release{TagName: "v2026.6.7", Body: "## what's changed\n- a thing"})
|
||||
if !strings.Contains(out, "v2026.6.7") {
|
||||
t.Errorf("rendered notes should include the tag, got %q", out)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package pool spreads independent per-item work across a fixed set of workers
|
||||
// that all pull from one shared channel. that's the point over a static
|
||||
// modulo-stride partition: a slow or timing-out item only stalls the one worker
|
||||
// holding it, the rest keep draining the queue instead of idling behind it.
|
||||
package pool
|
||||
|
||||
import "sync"
|
||||
|
||||
// Each runs fn for every item in items, concurrently, across at most workers
|
||||
// goroutines. order isn't preserved - fn must be safe to call from multiple
|
||||
// goroutines and guard any shared state itself. blocks until every item is done.
|
||||
func Each[T any](items []T, workers int, fn func(T)) {
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
// floor at one worker; a non-positive count would otherwise spawn nothing
|
||||
// and silently drop the work.
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
// never spin more workers than there is work for.
|
||||
if workers > len(items) {
|
||||
workers = len(items)
|
||||
}
|
||||
|
||||
queue := make(chan T, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
queue <- items[i]
|
||||
}
|
||||
close(queue)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(workers)
|
||||
for i := 0; i < workers; i++ {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
// pull until the queue is drained; a worker that finishes its
|
||||
// current item just grabs the next, which is the work-stealing.
|
||||
for item := range queue {
|
||||
fn(item)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package pool
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// every item runs exactly once across a spread of sizes and worker counts,
|
||||
// including the floors (zero/negative workers) and workers > len.
|
||||
func TestEachProcessesAllExactlyOnce(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
items int
|
||||
workers int
|
||||
}{
|
||||
{"empty", 0, 4},
|
||||
{"single item", 1, 8},
|
||||
{"workers floored from zero", 5, 0},
|
||||
{"workers floored from negative", 5, -3},
|
||||
{"more workers than items", 3, 16},
|
||||
{"even split", 100, 4},
|
||||
{"uneven split", 101, 7},
|
||||
{"one worker", 50, 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
items := make([]int, tt.items)
|
||||
for i := 0; i < tt.items; i++ {
|
||||
items[i] = i
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
seen := make(map[int]int, tt.items)
|
||||
Each(items, tt.workers, func(v int) {
|
||||
mu.Lock()
|
||||
seen[v]++
|
||||
mu.Unlock()
|
||||
})
|
||||
|
||||
if len(seen) != tt.items {
|
||||
t.Fatalf("processed %d distinct items, want %d", len(seen), tt.items)
|
||||
}
|
||||
for v, n := range seen {
|
||||
if n != 1 {
|
||||
t.Errorf("item %d processed %d times, want 1", v, n)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// no more than `workers` (capped at len(items)) callbacks ever run at once.
|
||||
func TestEachRespectsWorkerCap(t *testing.T) {
|
||||
const (
|
||||
items = 200
|
||||
workers = 6
|
||||
)
|
||||
work := make([]int, items)
|
||||
|
||||
var inFlight, peak int64
|
||||
var release = make(chan struct{})
|
||||
var started sync.WaitGroup
|
||||
started.Add(items)
|
||||
|
||||
go func() {
|
||||
Each(work, workers, func(int) {
|
||||
cur := atomic.AddInt64(&inFlight, 1)
|
||||
for {
|
||||
p := atomic.LoadInt64(&peak)
|
||||
if cur <= p || atomic.CompareAndSwapInt64(&peak, p, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
started.Done()
|
||||
<-release
|
||||
atomic.AddInt64(&inFlight, -1)
|
||||
})
|
||||
}()
|
||||
|
||||
// the cap means at most `workers` callbacks block on release at once, so
|
||||
// release exactly that many at a time until everything drains.
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
for i := 0; i < items; i++ {
|
||||
release <- struct{}{}
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
<-done
|
||||
|
||||
if got := atomic.LoadInt64(&peak); got > workers {
|
||||
t.Fatalf("peak concurrency %d exceeded worker cap %d", got, workers)
|
||||
}
|
||||
}
|
||||
|
||||
// the cap is min(workers, len(items)): fewer items than workers must not spin
|
||||
// idle goroutines past the item count.
|
||||
func TestEachCapsAtItemCount(t *testing.T) {
|
||||
const (
|
||||
items = 3
|
||||
workers = 32
|
||||
)
|
||||
work := make([]int, items)
|
||||
|
||||
var inFlight, peak int64
|
||||
var ready sync.WaitGroup
|
||||
ready.Add(items)
|
||||
release := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
for i := 0; i < items; i++ {
|
||||
release <- struct{}{}
|
||||
}
|
||||
}()
|
||||
|
||||
Each(work, workers, func(int) {
|
||||
cur := atomic.AddInt64(&inFlight, 1)
|
||||
for {
|
||||
p := atomic.LoadInt64(&peak)
|
||||
if cur <= p || atomic.CompareAndSwapInt64(&peak, p, cur) {
|
||||
break
|
||||
}
|
||||
}
|
||||
<-release
|
||||
atomic.AddInt64(&inFlight, -1)
|
||||
})
|
||||
|
||||
if got := atomic.LoadInt64(&peak); got > items {
|
||||
t.Fatalf("peak concurrency %d exceeded item count %d", got, items)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Markdown renders results as a readable report grouped by target, then by
|
||||
// module, with each module's finding pretty-printed as a json code block.
|
||||
func Markdown(results []Result) []byte {
|
||||
var b strings.Builder
|
||||
b.WriteString("# sif scan report\n\n")
|
||||
|
||||
// group module results under their target so the report reads target-first
|
||||
// regardless of the order results came in.
|
||||
byTarget := make(map[string][]Result)
|
||||
order := make([]string, 0)
|
||||
for i := 0; i < len(results); i++ {
|
||||
t := results[i].Target
|
||||
if _, seen := byTarget[t]; !seen {
|
||||
order = append(order, t)
|
||||
}
|
||||
byTarget[t] = append(byTarget[t], results[i])
|
||||
}
|
||||
|
||||
for i := 0; i < len(order); i++ {
|
||||
target := order[i]
|
||||
b.WriteString("## ")
|
||||
b.WriteString(target)
|
||||
b.WriteString("\n\n")
|
||||
|
||||
mods := byTarget[target]
|
||||
// sort modules so the report is deterministic across runs
|
||||
sort.SliceStable(mods, func(a, c int) bool { return mods[a].Module < mods[c].Module })
|
||||
|
||||
for j := 0; j < len(mods); j++ {
|
||||
b.WriteString("### ")
|
||||
b.WriteString(mods[j].Module)
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString("```json\n")
|
||||
b.WriteString(prettyJSON(mods[j].Data))
|
||||
b.WriteString("\n```\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return []byte(b.String())
|
||||
}
|
||||
|
||||
// prettyJSON re-indents the raw finding for readability; if it doesn't parse as
|
||||
// json (shouldn't happen, but never trust it) the raw bytes are returned as-is.
|
||||
func prettyJSON(raw json.RawMessage) string {
|
||||
if len(raw) == 0 {
|
||||
return "null"
|
||||
}
|
||||
var indented bytes.Buffer
|
||||
if err := json.Indent(&indented, raw, "", " "); err != nil {
|
||||
return string(raw)
|
||||
}
|
||||
return indented.String()
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
// Package report serializes collected scan results to sarif and markdown. it's
|
||||
// deliberately decoupled from the scan package: callers map their own results
|
||||
// into report.Result, so report never imports a scanner type.
|
||||
package report
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Result is one module's output for one target. Data is whatever the scanner
|
||||
// returned, carried as raw json so report stays free of scan types.
|
||||
type Result struct {
|
||||
Target string
|
||||
Module string
|
||||
Data json.RawMessage
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeResults are a couple of representative findings across two targets used by
|
||||
// every test below.
|
||||
func fakeResults() []Result {
|
||||
return []Result{
|
||||
{Target: "https://a.example.com", Module: "cors", Data: json.RawMessage(`{"severity":"high"}`)},
|
||||
{Target: "https://a.example.com", Module: "probe", Data: json.RawMessage(`{"status_code":200}`)},
|
||||
{Target: "https://b.example.com", Module: "redirect", Data: json.RawMessage(`{"parameter":"next"}`)},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSARIF_ValidAndContainsFindings(t *testing.T) {
|
||||
out, err := SARIF(fakeResults())
|
||||
if err != nil {
|
||||
t.Fatalf("SARIF: %v", err)
|
||||
}
|
||||
|
||||
// the output must parse back into the sarif shape
|
||||
var doc sarifLog
|
||||
if err := json.Unmarshal(out, &doc); err != nil {
|
||||
t.Fatalf("sarif output is not valid json: %v", err)
|
||||
}
|
||||
|
||||
if doc.Version != "2.1.0" {
|
||||
t.Errorf("expected sarif version 2.1.0, got %q", doc.Version)
|
||||
}
|
||||
if len(doc.Runs) != 1 {
|
||||
t.Fatalf("expected exactly one run, got %d", len(doc.Runs))
|
||||
}
|
||||
run := doc.Runs[0]
|
||||
if run.Tool.Driver.Name != "sif" {
|
||||
t.Errorf("expected tool name sif, got %q", run.Tool.Driver.Name)
|
||||
}
|
||||
if len(run.Results) != 3 {
|
||||
t.Fatalf("expected 3 results, got %d", len(run.Results))
|
||||
}
|
||||
|
||||
// each finding's module id surfaces as the ruleId and its target as the uri
|
||||
tests := []struct {
|
||||
ruleID string
|
||||
target string
|
||||
}{
|
||||
{"cors", "https://a.example.com"},
|
||||
{"probe", "https://a.example.com"},
|
||||
{"redirect", "https://b.example.com"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if !sarifHasResult(run.Results, tt.ruleID, tt.target) {
|
||||
t.Errorf("expected sarif result rule=%q target=%q, got %+v", tt.ruleID, tt.target, run.Results)
|
||||
}
|
||||
}
|
||||
|
||||
// rules list each module id once, deduped across targets
|
||||
if len(run.Tool.Driver.Rules) != 3 {
|
||||
t.Errorf("expected 3 deduped rules, got %d: %+v", len(run.Tool.Driver.Rules), run.Tool.Driver.Rules)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSARIF_DedupesRulesAcrossTargets(t *testing.T) {
|
||||
// the same module on two targets must yield one rule but two results.
|
||||
results := []Result{
|
||||
{Target: "https://a.example.com", Module: "cors", Data: json.RawMessage(`{}`)},
|
||||
{Target: "https://b.example.com", Module: "cors", Data: json.RawMessage(`{}`)},
|
||||
}
|
||||
out, err := SARIF(results)
|
||||
if err != nil {
|
||||
t.Fatalf("SARIF: %v", err)
|
||||
}
|
||||
var doc sarifLog
|
||||
if err := json.Unmarshal(out, &doc); err != nil {
|
||||
t.Fatalf("invalid json: %v", err)
|
||||
}
|
||||
run := doc.Runs[0]
|
||||
if len(run.Tool.Driver.Rules) != 1 {
|
||||
t.Errorf("expected 1 deduped rule, got %d", len(run.Tool.Driver.Rules))
|
||||
}
|
||||
if len(run.Results) != 2 {
|
||||
t.Errorf("expected 2 results, got %d", len(run.Results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSARIF_Empty(t *testing.T) {
|
||||
out, err := SARIF(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SARIF: %v", err)
|
||||
}
|
||||
var doc sarifLog
|
||||
if err := json.Unmarshal(out, &doc); err != nil {
|
||||
t.Fatalf("empty sarif is not valid json: %v", err)
|
||||
}
|
||||
if len(doc.Runs) != 1 {
|
||||
t.Fatalf("expected one run even when empty, got %d", len(doc.Runs))
|
||||
}
|
||||
if len(doc.Runs[0].Results) != 0 {
|
||||
t.Errorf("expected no results, got %d", len(doc.Runs[0].Results))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdown_ContainsTargetsAndModules(t *testing.T) {
|
||||
out := string(Markdown(fakeResults()))
|
||||
|
||||
wants := []string{
|
||||
"# sif scan report",
|
||||
"## https://a.example.com",
|
||||
"## https://b.example.com",
|
||||
"### cors",
|
||||
"### probe",
|
||||
"### redirect",
|
||||
`"severity": "high"`, // re-indented finding body
|
||||
`"parameter": "next"`,
|
||||
}
|
||||
for _, want := range wants {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("markdown report missing %q\n---\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdown_GroupsByTarget(t *testing.T) {
|
||||
// a.example.com's two modules must both appear before b.example.com's header.
|
||||
out := string(Markdown(fakeResults()))
|
||||
aHeader := strings.Index(out, "## https://a.example.com")
|
||||
bHeader := strings.Index(out, "## https://b.example.com")
|
||||
if aHeader < 0 || bHeader < 0 {
|
||||
t.Fatalf("missing target headers in:\n%s", out)
|
||||
}
|
||||
if aHeader > bHeader {
|
||||
t.Errorf("expected target a before target b, got a=%d b=%d", aHeader, bHeader)
|
||||
}
|
||||
// both of a's modules sit between a's header and b's header
|
||||
corsIdx := strings.Index(out, "### cors")
|
||||
probeIdx := strings.Index(out, "### probe")
|
||||
if corsIdx < aHeader || corsIdx > bHeader || probeIdx < aHeader || probeIdx > bHeader {
|
||||
t.Errorf("expected a's modules grouped under a, cors=%d probe=%d (a=%d b=%d)", corsIdx, probeIdx, aHeader, bHeader)
|
||||
}
|
||||
}
|
||||
|
||||
// sarifHasResult reports whether any result carries the given rule id and target
|
||||
// uri, the pairing that proves a finding survived serialization.
|
||||
func sarifHasResult(results []sarifResult, ruleID, target string) bool {
|
||||
for i := 0; i < len(results); i++ {
|
||||
r := results[i]
|
||||
if r.RuleID != ruleID {
|
||||
continue
|
||||
}
|
||||
for j := 0; j < len(r.Locations); j++ {
|
||||
if r.Locations[j].PhysicalLocation.ArtifactLocation.URI == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
: :
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
*/
|
||||
|
||||
package report
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// sarif format/version constants pinned to the 2.1.0 schema so the output is
|
||||
// ingestable by github code scanning and other sarif consumers.
|
||||
const (
|
||||
sarifVersion = "2.1.0"
|
||||
sarifSchema = "https://json.schemastore.org/sarif-2.1.0.json"
|
||||
toolName = "sif"
|
||||
)
|
||||
|
||||
// sarifLog is the minimal valid 2.1.0 shape: one run from one tool.
|
||||
type sarifLog struct {
|
||||
Schema string `json:"$schema"`
|
||||
Version string `json:"version"`
|
||||
Runs []sarifRun `json:"runs"`
|
||||
}
|
||||
|
||||
type sarifRun struct {
|
||||
Tool sarifTool `json:"tool"`
|
||||
Results []sarifResult `json:"results"`
|
||||
}
|
||||
|
||||
type sarifTool struct {
|
||||
Driver sarifDriver `json:"driver"`
|
||||
}
|
||||
|
||||
type sarifDriver struct {
|
||||
Name string `json:"name"`
|
||||
Rules []sarifRule `json:"rules"`
|
||||
}
|
||||
|
||||
type sarifRule struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type sarifResult struct {
|
||||
RuleID string `json:"ruleId"`
|
||||
Level string `json:"level"`
|
||||
Message sarifMessage `json:"message"`
|
||||
Locations []sarifLocation `json:"locations"`
|
||||
}
|
||||
|
||||
type sarifMessage struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type sarifLocation struct {
|
||||
PhysicalLocation sarifPhysicalLocation `json:"physicalLocation"`
|
||||
}
|
||||
|
||||
type sarifPhysicalLocation struct {
|
||||
ArtifactLocation sarifArtifactLocation `json:"artifactLocation"`
|
||||
}
|
||||
|
||||
type sarifArtifactLocation struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
// sarifLevel is the default severity for findings; sif results don't carry a
|
||||
// uniform severity field, so "warning" is the neutral middle ground.
|
||||
const sarifLevel = "warning"
|
||||
|
||||
// SARIF serializes results to a minimal valid sarif 2.1.0 log. Each module
|
||||
// result becomes one sarif result tagged with its module id (the rule) and the
|
||||
// target uri, with the raw module data inlined into the message for context.
|
||||
func SARIF(results []Result) ([]byte, error) {
|
||||
sarifResults := make([]sarifResult, 0, len(results))
|
||||
ruleSet := make(map[string]struct{}, len(results))
|
||||
|
||||
for i := 0; i < len(results); i++ {
|
||||
res := results[i]
|
||||
ruleSet[res.Module] = struct{}{}
|
||||
|
||||
sarifResults = append(sarifResults, sarifResult{
|
||||
RuleID: res.Module,
|
||||
Level: sarifLevel,
|
||||
Message: sarifMessage{Text: messageFor(res)},
|
||||
Locations: []sarifLocation{{
|
||||
PhysicalLocation: sarifPhysicalLocation{
|
||||
ArtifactLocation: sarifArtifactLocation{URI: res.Target},
|
||||
},
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
// rules must list each id exactly once; build it from the set so duplicate
|
||||
// modules across targets don't duplicate the rule.
|
||||
rules := make([]sarifRule, 0, len(ruleSet))
|
||||
for id := range ruleSet {
|
||||
rules = append(rules, sarifRule{ID: id})
|
||||
}
|
||||
|
||||
doc := sarifLog{
|
||||
Schema: sarifSchema,
|
||||
Version: sarifVersion,
|
||||
Runs: []sarifRun{{
|
||||
Tool: sarifTool{Driver: sarifDriver{Name: toolName, Rules: rules}},
|
||||
Results: sarifResults,
|
||||
}},
|
||||
}
|
||||
|
||||
out, err := json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal sarif: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// messageFor builds a human-readable result message: the module id plus the raw
|
||||
// finding json so a sarif viewer shows what was actually found.
|
||||
func messageFor(res Result) string {
|
||||
if len(res.Data) == 0 {
|
||||
return fmt.Sprintf("%s finding on %s", res.Module, res.Target)
|
||||
}
|
||||
return fmt.Sprintf("%s finding on %s: %s", res.Module, res.Target, string(res.Data))
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,9 +15,10 @@ package builtin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type FrameworksModule struct{}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,6 +15,7 @@ package builtin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
@@ -51,7 +52,8 @@ func (m *NucleiModule) Execute(ctx context.Context, target string, opts modules.
|
||||
}
|
||||
|
||||
// Process nuclei results into module findings
|
||||
for _, event := range nucleiResults {
|
||||
for i := range nucleiResults {
|
||||
event := &nucleiResults[i]
|
||||
severity := "info"
|
||||
|
||||
switch event.Info.SeverityHolder.Severity.String() {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -15,9 +15,10 @@ package builtin
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ShodanModule struct{}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -14,6 +14,7 @@ package builtin
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
||||
: ▄█ █ █▀ · BSD 3-Clause License :
|
||||
: :
|
||||
: (c) 2022-2025 vmfunc, xyzeva, :
|
||||
: (c) 2022-2026 vmfunc, xyzeva, :
|
||||
: lunchcat alumni & contributors :
|
||||
: :
|
||||
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
||||
@@ -21,19 +21,24 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/logger"
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
)
|
||||
|
||||
// s3EndpointFmt is a var so integration tests can repoint it at a fixture; the
|
||||
// %s is the bucket name.
|
||||
var s3EndpointFmt = "https://%s.s3.amazonaws.com"
|
||||
|
||||
type CloudStorageResult struct {
|
||||
BucketName string `json:"bucket_name"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
}
|
||||
|
||||
func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStorageResult, error) {
|
||||
fmt.Println(styles.Separator.Render("☁️ Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
|
||||
fmt.Println(styles.Separator.Render("Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
|
||||
|
||||
sanitizedURL := strings.Split(url, "://")[1]
|
||||
sanitizedURL := stripScheme(url)
|
||||
|
||||
if logdir != "" {
|
||||
if err := logger.WriteHeader(sanitizedURL, logdir, "Cloud Storage Misconfiguration Scan"); err != nil {
|
||||
@@ -43,12 +48,10 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
|
||||
}
|
||||
|
||||
cloudlog := log.NewWithOptions(os.Stderr, log.Options{
|
||||
Prefix: "C3 ☁️",
|
||||
Prefix: "C3",
|
||||
}).With("url", url)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
client := httpx.Client(timeout)
|
||||
|
||||
potentialBuckets := extractPotentialBuckets(sanitizedURL)
|
||||
|
||||
@@ -81,8 +84,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
|
||||
}
|
||||
|
||||
func extractPotentialBuckets(url string) []string {
|
||||
// This is a simple implementation.
|
||||
// TODO: add more cases
|
||||
// TODO: handle non-adjacent label combos and strip the tld
|
||||
parts := strings.Split(url, ".")
|
||||
var buckets []string
|
||||
for i, part := range parts {
|
||||
@@ -97,16 +99,17 @@ func extractPotentialBuckets(url string) []string {
|
||||
}
|
||||
|
||||
func checkS3Bucket(ctx context.Context, bucket string, client *http.Client) (bool, error) {
|
||||
url := fmt.Sprintf("https://%s.s3.amazonaws.com", bucket)
|
||||
url := fmt.Sprintf(s3EndpointFmt, bucket)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
resp, err := client.Do(req) //nolint:bodyclose // drained and closed via httpx.DrainClose
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// status only; drain on close so the conn returns to the pool.
|
||||
defer httpx.DrainClose(resp)
|
||||
|
||||
// If we can access the bucket listing, it's public
|
||||
return resp.StatusCode == http.StatusOK, nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user