Compare commits

..

1 Commits

Author SHA1 Message Date
vmfunc 1feb0648b3 ci(pr-bot): run on pull_request_target so fork PRs get labeled
fork PRs get a read-only token on pull_request, so the label, size and
ci-summary jobs 403 and the summary check shows red on every external
PR. run on pull_request_target (write token, base-repo context), key the
concurrency group on the PR number so runs don't collide, and drop the
size job's unused checkout. none of these jobs check out or run PR code,
they only call the github API with the event payload, so this is the
safe labeler pattern.

supersedes #146 (same fix by @TBX3D, which conflicted after the checkout
bump in #143).
2026-06-22 17:20:58 -07:00
84 changed files with 307 additions and 4662 deletions
-44
View File
@@ -1,44 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
-50
View File
@@ -1,50 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr *)'
-38
View File
@@ -57,12 +57,6 @@ enable verbose logging:
./sif -u https://example.com -d
```
### templates
`-template` loads a batch of scan settings from a built-in preset or a local yaml file, so a run does not have to pass every flag. see the [usage guide](usage.md) for the presets and file format. command-line flags still take precedence over the template.
sif also reads an ambient config at `~/.config/sif/config.yaml` (created on first run) keyed by the same flag names. passing `-template` uses that template as the config for the run instead of the ambient file.
## user modules
place custom modules in:
@@ -98,38 +92,6 @@ info:
# ...
```
## custom signatures
framework detection (`-framework`) also loads user-defined detectors from yaml
files, so a framework sif does not ship can be detected without rebuilding:
- linux/macos: `~/.config/sif/signatures/`
- windows: `%LOCALAPPDATA%\sif\signatures\`
each file defines one detector; place them directly in the directory, as
subdirectories are not scanned. `header: true` matches a response header name or
value (case-insensitive) instead of the body; the optional `version` block pulls
a version out of the body.
```yaml
# ~/.config/sif/signatures/ghost.yaml
name: Ghost
signatures:
- pattern: 'content="Ghost'
weight: 0.6
- pattern: 'X-Ghost-Cache'
weight: 0.4
header: true
version:
regex: 'content="Ghost ([0-9.]+)'
group: 1
```
a detector reports a match once its matched signature weights sum past half, so
weight your signatures to total about `1.0`. a name matching a built-in detector
overrides it and inherits that built-in's version patterns and known cves, the
same as user modules.
## performance tuning
### fast scans
-27
View File
@@ -378,33 +378,6 @@ enable debug logging:
./sif -u https://example.com -d
```
### --template
load a batch of scan settings from a template instead of passing each flag. the value is either a built-in preset or a local yaml file keyed by flag long-names:
```bash
./sif -u https://example.com --template recon
./sif -u https://example.com --template ./my-scans.yaml
```
built-in presets:
- `minimal`: liveness and fingerprint only (probe, headers, favicon)
- `recon`: broad non-intrusive discovery, no attack payloads
- `full`: every scan except the api-key ones (shodan, securitytrails), including the intrusive probes (xss, sql, lfi, redirect)
`full` sends attack payloads, so only run it against targets you are authorized to test.
a local template lists flag long-names, for example:
```yaml
cms: true
dirlist: medium
threads: 20
```
flags passed on the command line take precedence over the template, so `--template recon -xss` runs the recon preset with an added xss probe.
## 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.
+37 -47
View File
@@ -10,13 +10,13 @@ require (
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.9.0
github.com/projectdiscovery/retryabledns v1.0.115
github.com/projectdiscovery/utils v0.11.1
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.15.0
golang.org/x/time v0.14.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -33,18 +33,16 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.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/FalconOpsLLC/goexec v0.3.0 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // 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
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d // indirect
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415 // indirect
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 // indirect
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
@@ -119,7 +117,8 @@ require (
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/docker v28.3.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -132,8 +131,8 @@ 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.28.0 // indirect
github.com/geoffgarside/ber v1.2.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
github.com/gin-gonic/gin v1.9.1 // indirect
@@ -142,7 +141,7 @@ require (
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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.12 // indirect
github.com/go-ldap/ldap/v3 v3.4.11 // 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
@@ -164,6 +163,7 @@ 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/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
@@ -191,7 +191,6 @@ require (
github.com/hdm/jarm-go v0.0.7 // indirect
github.com/iangcarroll/cookiemonster v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/indece-official/go-ebcdic v1.2.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/itchyny/gojq v0.12.17 // indirect
@@ -200,7 +199,6 @@ require (
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/goidentity/v6 v6.0.1 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
@@ -213,7 +211,7 @@ require (
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kitabisa/go-ci v1.0.3 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/compress v1.18.2 // 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
@@ -231,7 +229,7 @@ require (
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.22 // indirect
github.com/mattn/go-isatty v0.0.20 // 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
@@ -245,8 +243,7 @@ require (
github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.54.2 // indirect
github.com/moby/moby/client v0.4.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
@@ -257,9 +254,6 @@ require (
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/oiweiwei/go-msrpc v1.2.12 // indirect
github.com/oiweiwei/go-smb2.fork v1.0.0 // indirect
github.com/oiweiwei/gokrb5.fork/v9 v9.0.6 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.0.8 // indirect
@@ -275,42 +269,40 @@ require (
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.39 // indirect
github.com/projectdiscovery/clistats v0.1.4 // indirect
github.com/projectdiscovery/dsl v0.8.19 // indirect
github.com/projectdiscovery/fastdialer v0.5.10 // indirect
github.com/projectdiscovery/cdncheck v1.2.31 // indirect
github.com/projectdiscovery/clistats v0.1.1 // 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.70 // indirect
github.com/projectdiscovery/gologger v1.1.68 // indirect
github.com/projectdiscovery/gostruct v0.0.2 // indirect
github.com/projectdiscovery/govaluate v0.0.0-20260504230327-80320480bb6e // indirect
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 // indirect
github.com/projectdiscovery/hmap v0.0.101 // indirect
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81 // 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.40 // indirect
github.com/projectdiscovery/ratelimit v0.0.88 // 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/retryablehttp-go v1.3.14 // indirect
github.com/projectdiscovery/sarif v0.1.0 // 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.1 // indirect
github.com/projectdiscovery/useragent v0.0.108 // indirect
github.com/projectdiscovery/wappalyzergo v0.2.84 // indirect
github.com/projectdiscovery/uncover v1.2.0 // indirect
github.com/projectdiscovery/useragent v0.0.107 // 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.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
github.com/rs/zerolog v1.34.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/sashabaranov/go-openai v1.37.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
@@ -319,13 +311,12 @@ require (
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.4 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
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.8.1 // indirect
@@ -373,14 +364,14 @@ require (
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 v1.9.1 // indirect
gitlab.com/gitlab-org/api/client-go v0.130.1 // 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.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.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
@@ -388,7 +379,7 @@ require (
goftp.io/server/v2 v2.0.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3 // 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.21.0 // indirect
@@ -397,12 +388,11 @@ require (
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.11 // 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
gopkg.in/yaml.v2 v2.4.0 // indirect
mellium.im/sasl v0.3.2 // indirect
moul.io/http2curl v1.0.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
)
+112 -110
View File
@@ -73,8 +73,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/FalconOpsLLC/goexec v0.3.0 h1:ryLMkrGT6asnkqdc5rFMNOSTYdMH/iCfyEuwu0D6ZhA=
github.com/FalconOpsLLC/goexec v0.3.0/go.mod h1:kiyxVbmFCGbbwXRyZmOSKlOy7PiK+JH2gq07Ztag/k8=
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.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=
@@ -86,8 +86,6 @@ github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb888350
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809/go.mod h1:upgc3Zs45jBDnBT4tVRgRcgm26ABpaP7MoTSdgysca4=
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnOU538A/YD86/dfqSNTvQsAXgwagxmpu4=
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d/go.mod h1:uzdh/m6XQJI7qRvufeBPDa+lj5SVCJO8B9eLxTbtI5U=
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415 h1:lpSZPcbowbxvKFaFvE1reLTCStezWXcRVk0zzBtUatg=
github.com/Mzack9999/goimpacket v0.0.0-20260422121140-7085336a0415/go.mod h1:Wvb2f1Aq6NVL4Fza/dPNxv6/canpeizpgmiTCBGMD50=
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 h1:54I+OF5vS4a/rxnUrN5J3hi0VEYKcrTlpc8JosDyP+c=
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697/go.mod h1:yNqYRqxYkSROY1J+LX+A0tOSA/6soXQs5m8hZSqYBac=
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 h1:+Is1AS20q3naP+qJophNpxuvx1daFOx9C0kLIuI0GVk=
@@ -103,8 +101,6 @@ github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBK
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687 h1:HHkHNwakgAFX3qlMm88c090vIYNFWiO5+4WyAr0xJuM=
github.com/RedTeamPentesting/adauth v0.5.4-0.20260511073005-3d18e8a5a687/go.mod h1:l6FRaC2TliS/JMNWskO3J1tmrcKJyOaMFhWC6hHeWno=
github.com/RumbleDiscovery/rumble-tools v0.0.0-20201105153123-f2adbb3244d2/go.mod h1:jD2+mU+E2SZUuAOHZvZj4xP4frlOo+N/YrXDvASFhkE=
github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=
github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=
@@ -129,8 +125,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/alexsnet/go-vnc v0.1.0 h1:vBCwPNy79WEL8V/Z5A0ngEFCvTWBAjmS048lkR2rdmY=
github.com/alexsnet/go-vnc v0.1.0/go.mod h1:bbRsg41Sh3zvrnWsw+REKJVGZd8Of2+S0V1G0ZaBhlU=
github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs=
@@ -250,6 +246,8 @@ github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+Y
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=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/censys/censys-sdk-go v0.19.1 h1:CG8rQKgwrKuoICd3oU0uddALMfJnboeMkDg/e74HYyc=
github.com/censys/censys-sdk-go v0.19.1/go.mod h1:DgPz5NgL+EfoueXLPG9UG1e7hS0OhtlywgpkIuu3ZRE=
@@ -308,7 +306,8 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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.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=
@@ -332,8 +331,10 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
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/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
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=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=
@@ -373,10 +374,10 @@ 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.28.0 h1:89yZLo8NmyqD0RYgJ3QO9HhqqGGw+oWhf90cZm69Lko=
github.com/gaissmai/bart v0.28.0/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
github.com/geoffgarside/ber v1.2.0 h1:/loowoRcs/MWLYmGX9QtIAbA+V/FrnVLsMMPhwiRm64=
github.com/geoffgarside/ber v1.2.0/go.mod h1:jVPKeCbj6MvQZhwLYsGwaGI52oUorHoHKNecGT85ZCc=
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=
github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@@ -405,8 +406,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
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=
@@ -467,9 +468,10 @@ github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuu
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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
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=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
@@ -569,15 +571,13 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORR
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -610,8 +610,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/indece-official/go-ebcdic v1.2.0 h1:nKCubkNoXrGvBp3MSYuplOQnhANCDEY512Ry5Mwr4a0=
github.com/indece-official/go-ebcdic v1.2.0/go.mod h1:RBddVJt0Ks0eDLRG5dhPwBDRiTNA7n+yv0dVFpSs46Q=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
@@ -669,12 +667,13 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kitabisa/go-ci v1.0.3 h1:JmIUIvcercRQc/9x/v02ydCCqU4MadSHaNaOF8T2pGA=
github.com/kitabisa/go-ci v1.0.3/go.mod h1:e3wBSzaJbcifXrr/Gw2ZBLn44MmeqP5WySwXyHlCK/U=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -725,13 +724,10 @@ github.com/mackerelio/go-osstat v0.2.6 h1:gs4U8BZeS1tjrL08tt5VUliVvSWP26Ai2Ob8Lr
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.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
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.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -764,10 +760,14 @@ 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.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
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=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -783,6 +783,8 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
@@ -801,12 +803,6 @@ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//J
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/oiweiwei/go-msrpc v1.2.12 h1:gaxnv1cyX3v9l+NNxyr4ONyvh/mnmh8Egel9r8zxNxE=
github.com/oiweiwei/go-msrpc v1.2.12/go.mod h1:T6/ENmAoD1nYCr3NW8PS8AjIX0tZEAL7yO0tsejtK18=
github.com/oiweiwei/go-smb2.fork v1.0.0 h1:xHq/eYPM8hQEO/nwCez8YwHWHC8mlcsgw/Neu52fPN4=
github.com/oiweiwei/go-smb2.fork v1.0.0/go.mod h1:h0CzLVvGAmq39izdYVHKyI5cLv6aHdbQAMKEe4dz4N8=
github.com/oiweiwei/gokrb5.fork/v9 v9.0.6 h1:ZMXO5OtzPPSqZ7KPgknVuvHE5iAbSXq5JLgzrkiXknM=
github.com/oiweiwei/gokrb5.fork/v9 v9.0.6/go.mod h1:KEnkAYUYqZ5VwzxLFbv3JHlRhCvdFahjrdjjssMJJkI=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
@@ -855,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.39 h1:gNE2dyaK+ZqEdEWyVUFlq8PvromEhSxamhsmFZR2Voc=
github.com/projectdiscovery/cdncheck v1.2.39/go.mod h1:9oE9KKxCSHNvUf0UaMeqqUwWpC38FkNaTll0ScIBT3w=
github.com/projectdiscovery/clistats v0.1.4 h1:kDnXoNxIdOvQElOF7k2Mt6XosGa5GbMKPtRXdPHMVzU=
github.com/projectdiscovery/clistats v0.1.4/go.mod h1:hjJYNcUubk9T3cuFvA+JkLhZGjzYW50fkC48dqUAtbU=
github.com/projectdiscovery/dsl v0.8.19 h1:qA5OFJMfghSCjKqS4AdsEtnur/SoriXDw3geE7+mReU=
github.com/projectdiscovery/dsl v0.8.19/go.mod h1:Twk93q7fxQ43v/8nR+0TJV8/eFTdBAC5tIXe3qzua9Y=
github.com/projectdiscovery/fastdialer v0.5.10 h1:dB9MSu4cSo22qne4pHiK9iYSxfOgpwlKB6zfOHvz3RI=
github.com/projectdiscovery/fastdialer v0.5.10/go.mod h1:W1ZkULr9mMR6i0oRFTztANnpVyEEzPUovK8sUM4eAw8=
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.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=
@@ -873,16 +869,14 @@ 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.70 h1:A1ZAsUJfRUXO6qqwTwyTWXLVlBrVu/Gpi1zzL1hg5LY=
github.com/projectdiscovery/gologger v1.1.70/go.mod h1:kpLKNafZWRN9P7WpJYtIOY/XvY/v41GDdU9NzICdKmo=
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/govaluate v0.0.0-20260504230327-80320480bb6e h1:o+ulEIaC2+9V2Ezr6mI5xEhKWsf0V/+FUQIS723Aj6U=
github.com/projectdiscovery/govaluate v0.0.0-20260504230327-80320480bb6e/go.mod h1:xH7bPwHxUlz1yx9UlVeTF+UVCUaKhTnZgaxHb5z362E=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76 h1:AN70bbi6BBs7KpIM9w0LxygUN7uzT/oH+owDIQ+Fz/k=
github.com/projectdiscovery/gozero v0.1.1-0.20260530071156-fa1dad563d76/go.mod h1:cWHYnRXoYWHtTpOYyAp5laGYX8GH8ITUhgQaP8G/8FA=
github.com/projectdiscovery/hmap v0.0.101 h1:zXM6YtLmsn8Q0CUUw8QavhqWmiQYwaw+/U679Rr00pc=
github.com/projectdiscovery/hmap v0.0.101/go.mod h1:w6N9/a5H8kvyx53AhtPDUWe5Qq3D6NBDPA23glHpa/Q=
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.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=
@@ -895,34 +889,34 @@ 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.40 h1:kYin4u1/dgb0nuz5fE1bz4Q0Zh66mOKIdkSHJ00bjGY=
github.com/projectdiscovery/networkpolicy v0.1.40/go.mod h1:9ULLaMbdv9UnT0C5rmuK4nIwYs0o776xMnkPUb8TtaE=
github.com/projectdiscovery/nuclei/v3 v3.9.0 h1:kNHrWZH7mM8Ntf5qacYgHNCEGzmPtywcU0feKm2YnhU=
github.com/projectdiscovery/nuclei/v3 v3.9.0/go.mod h1:6gkhTSiX+7ay5NTHM62+WUUCg7toWwHaWady+3tblbY=
github.com/projectdiscovery/ratelimit v0.0.88 h1:AcurW9aLRzlEyPe9kSjnOpr3XzLMWTpiWAlW/w73ALU=
github.com/projectdiscovery/ratelimit v0.0.88/go.mod h1:CU1s+68UUG2mctSl2wi32/DHLJA6TMg+4rxgP59LfVk=
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.115 h1:RKV63FNIznFHUoawg/1hs53pVH3wqPtFhwstCuxVSoA=
github.com/projectdiscovery/retryabledns v1.0.115/go.mod h1:+fEMWoPigw+M0lGNKY7AZ+g8FIgj+4sONjsinMmeL3k=
github.com/projectdiscovery/retryablehttp-go v1.3.14 h1:vCBLwK8iIuua3i97jEac5/+TWkYTLhTkGblHu9ETPVc=
github.com/projectdiscovery/retryablehttp-go v1.3.14/go.mod h1:reVhQ+DzMAPYEQHdawCQ6h0tX3CpFyMH4XjcAyq9+U8=
github.com/projectdiscovery/sarif v0.1.0 h1:O541T+a448nSJsmIMnXXSOeDQEzpnCAYvRfe0eG5h74=
github.com/projectdiscovery/sarif v0.1.0/go.mod h1:LBC+reM3bkI3qIIhE0rZaINaYX6VG+En6u2hHa5mA7E=
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=
github.com/projectdiscovery/stringsutil v0.0.2/go.mod h1:EJ3w6bC5fBYjVou6ryzodQq37D5c6qbAYQpGmAy+DC0=
github.com/projectdiscovery/tlsx v1.2.2 h1:Y96QBqeD2anpzEtBl4kqNbwzXh2TrzJuXfgiBLvK+SE=
github.com/projectdiscovery/tlsx v1.2.2/go.mod h1:ZJl9F1sSl0sdwE+lR0yuNHVX4Zx6tCSTqnNxnHCFZB4=
github.com/projectdiscovery/uncover v1.2.1 h1:8U46T/96CLT7BPoXBgkTvWqB06lOyeTSLvh5+UjzATE=
github.com/projectdiscovery/uncover v1.2.1/go.mod h1:0p8onrWxfpXQEYs90ZDzTSpu1107gWmodX1NWqu/+z4=
github.com/projectdiscovery/useragent v0.0.108 h1:fb+uLuFJvC+MHZjCtxQJxtvp1X6A8n98CUGPyFcg3NE=
github.com/projectdiscovery/useragent v0.0.108/go.mod h1:XdNRrlvtDmYfVL1Oybat4uMe+W6cLwsK9S18ond17CI=
github.com/projectdiscovery/utils v0.11.1 h1:PWj1KjIASxt8icxommH72C0TQqNOvGkcSODRkiq0SQw=
github.com/projectdiscovery/utils v0.11.1/go.mod h1:yktGrHGk2CTjNiccXovnvGrLHX9sV2bqz9nSnbA3V8M=
github.com/projectdiscovery/wappalyzergo v0.2.84 h1:19c+ea8KZCnZIuZPztafFKK2uczDXxcZ/z6/l6DEEEs=
github.com/projectdiscovery/wappalyzergo v0.2.84/go.mod h1:gMH0o5lBp65sKMwHx/tuUdOtW2RjodC6Ti+9QDsYMkY=
github.com/projectdiscovery/uncover v1.2.0 h1:31tjYa0v8FB8Ch8hJTxb+2t63vsljdOo0OSFylJcX4M=
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.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=
@@ -966,8 +960,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=
@@ -992,9 +984,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@@ -1009,10 +1000,8 @@ github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0b
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
@@ -1149,6 +1138,7 @@ github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
@@ -1178,8 +1168,8 @@ github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6/go.mod h1:HXD
github.com/zmap/zgrab2 v0.1.8 h1:PFnXrIBcGjYFec1JNbxMKQuSXXzS+SbqE89luuF4ORY=
github.com/zmap/zgrab2 v0.1.8/go.mod h1:5d8HSmUwvllx4q1qG50v/KXphkg45ZzWdaQtgTFnegE=
github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8=
gitlab.com/gitlab-org/api/client-go v1.9.1 h1:tZm+URa36sVy8UCEHQyGGJ8COngV4YqMHpM6k9O5tK8=
gitlab.com/gitlab-org/api/client-go v1.9.1/go.mod h1:71yTJk1lnHCWcZLvM5kPAXzeJ2fn5GjaoV8gTOPd4ME=
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.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=
@@ -1191,18 +1181,24 @@ 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.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.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
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.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.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.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=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -1256,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-20260527015227-08cc5374adb3 h1:VHEvKbpgPXcPXn40t9cDTGK3JZwMikIEyF/CTrFfu7k=
golang.org/x/exp v0.0.0-20260527015227-08cc5374adb3/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
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=
@@ -1320,6 +1316,7 @@ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -1362,6 +1359,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -1426,7 +1424,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1484,8 +1481,8 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
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=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -1525,9 +1522,11 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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=
@@ -1592,6 +1591,11 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1604,6 +1608,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -1620,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.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
@@ -1662,10 +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=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
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=
software.sslmate.com/src/go-pkcs12 v0.7.0 h1:Db8W44cB54TWD7stUFFSWxdfpdn6fZVcDl0w3R4RVM0=
software.sslmate.com/src/go-pkcs12 v0.7.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
+6 -31
View File
@@ -13,7 +13,6 @@
package config
import (
"os"
"time"
"github.com/charmbracelet/log"
@@ -111,9 +110,9 @@ const (
Full
)
// registerFlags builds the flag set for the given settings without parsing it,
// so callers (Parse and tests) can inspect the registered flags.
func registerFlags(settings *Settings) *goflags.FlagSet {
func Parse() *Settings {
settings := &Settings{}
flagSet := goflags.NewFlagSet()
flagSet.SetDescription("a blazing-fast pentesting (recon/exploitation) suite")
@@ -170,7 +169,7 @@ func registerFlags(settings *Settings) *goflags.FlagSet {
flagSet.DurationVarP(&settings.Timeout, "timeout", "t", 10*time.Second, "HTTP request timeout"),
flagSet.StringVarP(&settings.LogDir, "log", "l", "", "Directory to store logs in"),
flagSet.IntVar(&settings.Threads, "threads", 10, "Number of threads to run scans on"),
flagSet.StringVar(&settings.Template, "template", "", "Load scan settings from a template (preset minimal/recon/full, or a local yaml file)"),
flagSet.StringVar(&settings.Template, "template", "", "Sif runtime template to use"),
)
flagSet.CreateGroup("http", "HTTP",
@@ -205,32 +204,8 @@ func registerFlags(settings *Settings) *goflags.FlagSet {
flagSet.BoolVarP(&settings.ListModules, "list-modules", "lm", false, "List available modules and exit"),
)
return flagSet
}
func Parse() *Settings {
settings := &Settings{}
flagSet := registerFlags(settings)
// -template presets a batch of scans from a yaml file or named preset; point
// goflags at it before Parse so it merges as config (cli flags still win) and
// replaces the ambient config for this run.
templatePath, cleanup, err := templateConfigPath(os.Args[1:])
if err != nil {
log.Fatalf("Could not load template: %s", err)
}
if templatePath != "" {
flagSet.SetConfigFilePath(templatePath)
}
// Parse merges the template config synchronously, so a temp preset file can
// be removed right after, before any fatal exit (no leaking defer).
parseErr := flagSet.Parse()
if cleanup != nil {
cleanup()
}
if parseErr != nil {
log.Fatalf("Could not parse flags: %s", parseErr)
if err := flagSet.Parse(); err != nil {
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
-116
View File
@@ -1,116 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package config
import (
"embed"
"fmt"
"os"
"strings"
)
//go:embed templates/*.yaml
var embeddedTemplates embed.FS
// presetNames are the templates shipped in the binary, listed in help and
// error text. each presets a batch of scans without listing every flag.
var presetNames = []string{"minimal", "recon", "full"}
// templateConfigPath resolves the -template value into a config file path for
// goflags to merge, plus a cleanup to run after Parse (embedded presets are
// written to a temp file). it returns "" when -template is unset.
func templateConfigPath(args []string) (string, func(), error) {
value := templateFlagValue(args)
if value == "" {
return "", nil, nil
}
return resolveTemplate(value)
}
// templateFlagValue pulls the -template value out of raw args; the config path
// has to be known before Parse, so it cannot come from the parsed flag itself.
func templateFlagValue(args []string) string {
for i, arg := range args {
if arg == "-template" || arg == "--template" {
if i+1 < len(args) {
return args[i+1]
}
return ""
}
if v, ok := strings.CutPrefix(arg, "-template="); ok {
return v
}
if v, ok := strings.CutPrefix(arg, "--template="); ok {
return v
}
}
return ""
}
// resolveTemplate turns the -template value into a config file path. an existing
// local file wins; a named preset is materialized from the embedded set; a
// path-shaped miss or an unknown name is a hard error.
func resolveTemplate(value string) (string, func(), error) {
info, err := os.Stat(value) //nolint:gosec // G304: user-supplied local template path, by design (same as the -f/-w wordlist paths)
switch {
case err == nil && info.IsDir():
return "", nil, fmt.Errorf("template path %q is a directory", value)
case err == nil:
return value, nil, nil
}
if data, ok := embeddedPreset(value); ok {
return materializePreset(data)
}
if looksLikePath(value) {
return "", nil, fmt.Errorf("template file %q not found", value)
}
return "", nil, fmt.Errorf("unknown template %q; use a local yaml file or one of: %s",
value, strings.Join(presetNames, ", "))
}
// embeddedPreset returns the bytes of a named preset shipped in the binary.
func embeddedPreset(name string) ([]byte, bool) {
data, err := embeddedTemplates.ReadFile("templates/" + name + ".yaml")
if err != nil {
return nil, false
}
return data, true
}
// materializePreset writes preset bytes to a temp file so goflags, which merges
// a config by path, can read it; the cleanup removes the file after Parse.
func materializePreset(data []byte) (string, func(), error) {
file, err := os.CreateTemp("", "sif-template-*.yaml")
if err != nil {
return "", nil, err
}
cleanup := func() { _ = os.Remove(file.Name()) }
if _, err := file.Write(data); err != nil {
cleanup()
return "", nil, err
}
if err := file.Close(); err != nil {
cleanup()
return "", nil, err
}
return file.Name(), cleanup, nil
}
// looksLikePath reports whether the value addresses a file rather than a named
// preset: a path separator or a yaml suffix marks a file.
func looksLikePath(value string) bool {
if strings.ContainsAny(value, `/\`) {
return true
}
return strings.HasSuffix(value, ".yaml") || strings.HasSuffix(value, ".yml")
}
-211
View File
@@ -1,211 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package config
import (
"flag"
"os"
"path/filepath"
"testing"
"github.com/projectdiscovery/goflags"
"gopkg.in/yaml.v3"
)
// writeTemplate drops a yaml template in a temp dir and returns its path.
func writeTemplate(t *testing.T, body string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "tmpl.yaml")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("write template: %s", err)
}
return path
}
// loadPreset registers the real flags, merges the named embedded preset, and
// returns the resulting settings (no cli scan flags set).
func loadPreset(t *testing.T, name string) *Settings {
t.Helper()
goflags.DisableAutoConfigMigration = true
path, cleanup, err := resolveTemplate(name)
if err != nil {
t.Fatalf("resolve %s: %s", name, err)
}
if cleanup != nil {
defer cleanup()
}
settings := &Settings{}
flagSet := registerFlags(settings)
flagSet.SetConfigFilePath(path)
if err := flagSet.Parse("-silent"); err != nil {
t.Fatalf("parse %s: %s", name, err)
}
return settings
}
// every key in an embedded preset must be a real flag long-name. goflags drops
// unknown config keys silently, so a typo would otherwise ship as a dead no-op.
func TestPresetKeysAreRegisteredFlags(t *testing.T) {
valid := map[string]bool{}
registerFlags(&Settings{}).CommandLine.VisitAll(func(f *flag.Flag) {
valid[f.Name] = true
})
for _, name := range presetNames {
data, ok := embeddedPreset(name)
if !ok {
t.Errorf("preset %q is not embedded", name)
continue
}
var keys map[string]any
if err := yaml.Unmarshal(data, &keys); err != nil {
t.Errorf("preset %q is not valid yaml: %s", name, err)
continue
}
for key := range keys {
if !valid[key] {
t.Errorf("preset %q references unknown flag %q", name, key)
}
}
}
}
func TestPresetMinimal(t *testing.T) {
s := loadPreset(t, "minimal")
if !s.Probe || !s.Headers || !s.Favicon {
t.Errorf("minimal should enable probe/headers/favicon, got probe=%v headers=%v favicon=%v",
s.Probe, s.Headers, s.Favicon)
}
if s.XSS || s.SQL || s.Nuclei {
t.Error("minimal should not enable heavy or intrusive scans")
}
}
func TestPresetReconIsNonIntrusive(t *testing.T) {
s := loadPreset(t, "recon")
if !s.Passive || !s.Whois || !s.CMS || !s.Probe {
t.Errorf("recon should enable passive/whois/cms/probe, got %v/%v/%v/%v",
s.Passive, s.Whois, s.CMS, s.Probe)
}
if s.XSS || s.SQL || s.LFI || s.Redirect {
t.Errorf("recon must not enable payload-injecting scans: xss=%v sql=%v lfi=%v redirect=%v",
s.XSS, s.SQL, s.LFI, s.Redirect)
}
}
func TestPresetFull(t *testing.T) {
s := loadPreset(t, "full")
if !s.XSS || !s.SQL || !s.LFI || !s.Redirect {
t.Error("full should enable the intrusive scans")
}
if s.Dirlist != "large" || s.Ports != "full" {
t.Errorf("full should set dirlist=large ports=full, got dirlist=%q ports=%q",
s.Dirlist, s.Ports)
}
}
// the template merges as the goflags config: it fills flags left at their
// default, an explicit cli flag still wins, and an untouched flag stays put.
func TestTemplateConfigPrecedence(t *testing.T) {
goflags.DisableAutoConfigMigration = true
tmpl := writeTemplate(t, "cms: true\nthreads: 99\n")
var cms, sql bool
var threads int
flagSet := goflags.NewFlagSet()
flagSet.BoolVar(&cms, "cms", false, "")
flagSet.BoolVar(&sql, "sql", false, "")
flagSet.IntVar(&threads, "threads", 10, "")
flagSet.SetConfigFilePath(tmpl)
if err := flagSet.Parse("-threads", "5"); err != nil {
t.Fatalf("parse: %s", err)
}
if !cms {
t.Error("expected template to set cms=true")
}
if threads != 5 {
t.Errorf("expected cli threads 5 to win over template, got %d", threads)
}
if sql {
t.Error("expected sql left untouched to stay false")
}
}
func TestTemplateFlagValue(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{"long with space", []string{"-template", "a.yaml"}, "a.yaml"},
{"double dash with space", []string{"--template", "b.yaml"}, "b.yaml"},
{"long with equals", []string{"-template=c.yaml"}, "c.yaml"},
{"double dash with equals", []string{"--template=d.yaml"}, "d.yaml"},
{"absent", []string{"-u", "x"}, ""},
{"trailing without value", []string{"-u", "x", "-template"}, ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := templateFlagValue(tc.args); got != tc.want {
t.Errorf("expected %q, got %q", tc.want, got)
}
})
}
}
func TestResolveTemplateExistingFile(t *testing.T) {
path := writeTemplate(t, "cms: true\n")
got, cleanup, err := resolveTemplate(path)
if cleanup != nil {
defer cleanup()
}
if err != nil {
t.Fatalf("resolveTemplate: %s", err)
}
if got != path {
t.Errorf("expected %q, got %q", path, got)
}
}
func TestResolveTemplateNamedPreset(t *testing.T) {
path, cleanup, err := resolveTemplate("recon")
if cleanup != nil {
defer cleanup()
}
if err != nil {
t.Fatalf("recon preset should resolve: %s", err)
}
if path == "" {
t.Fatal("expected a materialized preset path")
}
}
func TestResolveTemplateMissingFile(t *testing.T) {
if _, _, err := resolveTemplate("./does-not-exist.yaml"); err == nil {
t.Fatal("expected an error for a missing template file")
}
}
func TestResolveTemplateDirectory(t *testing.T) {
if _, _, err := resolveTemplate(t.TempDir()); err == nil {
t.Fatal("expected an error for a directory")
}
}
func TestResolveTemplateUnknownName(t *testing.T) {
if _, _, err := resolveTemplate("bogus"); err == nil {
t.Fatal("expected an error for an unknown template name")
}
}
-28
View File
@@ -1,28 +0,0 @@
# full: thorough and active, including intrusive probes that inject payloads
# (xss, sql, lfi, redirect). api-key scans (shodan, securitytrails) stay opt-in
# via their own flags.
passive: true
whois: true
dork: true
favicon: true
headers: true
security-headers: true
cms: true
framework: true
probe: true
git: true
js: true
nuclei: true
openapi: true
cors: true
jwt: true
c3: true
st: true
crawl: true
sql: true
lfi: true
xss: true
redirect: true
dirlist: large
dnslist: large
ports: full
-4
View File
@@ -1,4 +0,0 @@
# minimal: fast liveness + fingerprint, a handful of benign GETs per target.
probe: true
headers: true
favicon: true
-11
View File
@@ -1,11 +0,0 @@
# recon: broad non-intrusive discovery (light traffic, no attack payloads).
passive: true
whois: true
dork: true
favicon: true
headers: true
security-headers: true
cms: true
framework: true
probe: true
dnslist: small
-56
View File
@@ -1,56 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
// Package fingerprint holds small response-fingerprinting primitives shared by
// the scan checks and the module engine, so both compute identical values.
package fingerprint
import (
"encoding/base64"
"strings"
"github.com/twmb/murmur3"
)
// b64LineLen is python's base64.encodebytes line width. mmh3/shodan hash the
// chunked base64 (newline every 76 chars, trailing newline), so we must wrap at
// exactly this width to land on the same hash.
const b64LineLen = 76
// FaviconHash computes shodan's favicon hash: murmur3 32-bit over the python
// base64.encodebytes encoding of the raw icon (newline every 76 chars plus a
// trailing newline), reinterpreted as a signed int32 (both load-bearing, golden-pinned).
func FaviconHash(data []byte) int32 {
encoded := encodeFaviconBase64(data)
return int32(murmur3.Sum32(encoded)) //nolint:gosec // shodan stores the signed reinterpretation on purpose
}
// encodeFaviconBase64 mirrors python's base64.encodebytes: standard base64 with
// a newline inserted every 76 output characters and a trailing newline. this is
// the exact byte stream shodan feeds to mmh3, so it must match byte-for-byte.
func encodeFaviconBase64(data []byte) []byte {
raw := base64.StdEncoding.EncodeToString(data)
var b strings.Builder
// final size: the base64 body plus one '\n' per (full or partial) 76-char
// line. preallocate so the builder never regrows mid-loop.
b.Grow(len(raw) + len(raw)/b64LineLen + 1)
for i := 0; i < len(raw); i += b64LineLen {
end := i + b64LineLen
if end > len(raw) {
end = len(raw)
}
b.WriteString(raw[i:end])
b.WriteByte('\n')
}
return []byte(b.String())
}
-68
View File
@@ -1,68 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package fingerprint
import (
"strings"
"testing"
)
// goldenFaviconBytes is a fixed payload long enough to span multiple base64
// lines, so the python-style 76-char chunking is actually exercised by the hash.
var goldenFaviconBytes = []byte(strings.Repeat("sif-favicon-golden-test-bytes-", 8))
// goldenFaviconHash is the pinned shodan mmh3 hash of goldenFaviconBytes: the python
// base64.encodebytes byte stream (76-char lines + trailing newline) through murmur3-32,
// reinterpreted as a signed int32. if the chunking or signedness regress, this test fails.
const goldenFaviconHash int32 = -1554620260
// goldenHelloHash pins a short single-line case so a regression in the trailing
// newline (which the small case still has) is caught independently.
const goldenHelloHash int32 = 1155597304
func TestFaviconHashGolden(t *testing.T) {
tests := []struct {
name string
in []byte
want int32
}{
{name: "multi-line fixture", in: goldenFaviconBytes, want: goldenFaviconHash},
{name: "single-line hello", in: []byte("hello"), want: goldenHelloHash},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := FaviconHash(tt.in); got != tt.want {
t.Errorf("FaviconHash = %d, want %d", got, tt.want)
}
})
}
}
// TestFaviconBase64Chunking pins the encode step against python's
// base64.encodebytes: a 60-byte input encodes to 80 base64 chars, so it must
// wrap into two newline-terminated lines.
func TestFaviconBase64Chunking(t *testing.T) {
in := []byte(strings.Repeat("A", 60))
got := string(encodeFaviconBase64(in))
lines := strings.Split(strings.TrimRight(got, "\n"), "\n")
if len(lines) != 2 {
t.Fatalf("expected 2 wrapped lines, got %d: %q", len(lines), got)
}
if len(lines[0]) != b64LineLen {
t.Errorf("first line = %d chars, want %d", len(lines[0]), b64LineLen)
}
if !strings.HasSuffix(got, "\n") {
t.Errorf("encoding must end in a trailing newline, got %q", got)
}
}
@@ -1,205 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runCMSCfgModule(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 cmsCfgExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestCMSConfigExposureModules(t *testing.T) {
const joomla = "../../modules/recon/joomla-config-exposure.yaml"
const drupal = "../../modules/recon/drupal-config-exposure.yaml"
const magento = "../../modules/recon/magento-config-exposure.yaml"
joomlaConfig := "<?php\nclass JConfig {\n\tpublic $offline = '0';\n" +
"\tpublic $host = 'localhost';\n\tpublic $user = 'joomla_user';\n" +
"\tpublic $password = 'S3cretJoomlaPass';\n\tpublic $db = 'joomla_db';\n" +
"\tpublic $dbprefix = 'jos_';\n\tpublic $secret = 'AbCdEfGhIjKlMnOp';\n}\n"
drupalConfig := "<?php\n$databases['default']['default'] = array (\n" +
" 'database' => 'drupal_db',\n 'username' => 'drupal_user',\n" +
" 'password' => 'S3cretDrupalPass',\n 'host' => 'localhost',\n" +
" 'driver' => 'mysql',\n);\n$settings['hash_salt'] = 'longrandomhashsalt';\n"
magentoConfig := "<?php\nreturn [\n 'backend' => ['frontName' => 'admin_x7y'],\n" +
" 'crypt' => ['key' => 'a1b2c3d4e5f6g7h8'],\n 'db' => [\n" +
" 'connection' => ['default' => [\n 'host' => 'localhost',\n" +
" 'dbname' => 'magento',\n 'username' => 'magento_user',\n" +
" 'password' => 'S3cretMagentoPass',\n ]],\n ],\n 'MAGE_MODE' => 'production',\n];\n"
t.Run("an exposed joomla configuration leaks the password", func(t *testing.T) {
res := runCMSCfgModule(t, joomla, 200, joomlaConfig)
if len(res.Findings) == 0 {
t.Fatal("expected a joomla finding")
}
if v := cmsCfgExtract(res, "joomla_password"); v != "S3cretJoomlaPass" {
t.Errorf("joomla_password=%q, want S3cretJoomlaPass", v)
}
})
t.Run("an exposed drupal settings leaks the password", func(t *testing.T) {
res := runCMSCfgModule(t, drupal, 200, drupalConfig)
if len(res.Findings) == 0 {
t.Fatal("expected a drupal finding")
}
if v := cmsCfgExtract(res, "drupal_password"); v != "S3cretDrupalPass" {
t.Errorf("drupal_password=%q, want S3cretDrupalPass", v)
}
})
t.Run("an exposed magento env leaks the crypt key", func(t *testing.T) {
res := runCMSCfgModule(t, magento, 200, magentoConfig)
if len(res.Findings) == 0 {
t.Fatal("expected a magento finding")
}
if v := cmsCfgExtract(res, "magento_crypt_key"); v != "a1b2c3d4e5f6g7h8" {
t.Errorf("magento_crypt_key=%q, want a1b2c3d4e5f6g7h8", v)
}
})
t.Run("a joomla config missing the password is not flagged", func(t *testing.T) {
body := "<?php\nclass JConfig {\n\tpublic $host = 'localhost';\n" +
"\tpublic $db = 'joomla_db';\n\tpublic $dbprefix = 'jos_';\n}\n"
if res := runCMSCfgModule(t, joomla, 200, body); len(res.Findings) > 0 {
t.Errorf("a config without a password should not match, got %d findings", len(res.Findings))
}
})
t.Run("a php class with a public password but no jconfig is not joomla", func(t *testing.T) {
body := "<?php\nclass MyAuth {\n\tpublic $password = 'changeme';\n" +
"\tpublic $username = 'admin';\n}\n"
if res := runCMSCfgModule(t, joomla, 200, body); len(res.Findings) > 0 {
t.Errorf("a generic class should not match joomla, got %d findings", len(res.Findings))
}
})
t.Run("a php array with a password but no databases is not drupal", func(t *testing.T) {
body := "<?php\n$config = array('password' => 'x', 'host' => 'y');\n"
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
t.Errorf("a generic array should not match drupal, got %d findings", len(res.Findings))
}
})
t.Run("a drupal databases array without a password is not flagged", func(t *testing.T) {
body := "<?php\n$databases['default']['default'] = array (\n" +
" 'database' => 'drupal_db',\n 'host' => 'localhost',\n 'driver' => 'mysql',\n);\n"
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
t.Errorf("a databases array without a password should not match, got %d findings", len(res.Findings))
}
})
t.Run("a php return array with a password but no magento markers is not flagged", func(t *testing.T) {
body := "<?php\nreturn ['db' => ['password' => 'secret', 'host' => 'localhost']];\n"
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
t.Errorf("a generic return array should not match magento, got %d findings", len(res.Findings))
}
})
t.Run("a magento config without a credential is not flagged", func(t *testing.T) {
body := "<?php\nreturn ['MAGE_MODE' => 'production', 'db' => ['host' => 'localhost']];\n"
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
t.Errorf("a magento config without a credential should not match, got %d findings", len(res.Findings))
}
})
t.Run("an html page demonstrating a joomla config is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>class JConfig { public $password = 'x'; public $db = 'y'; }</pre></body></html>"
if res := runCMSCfgModule(t, joomla, 200, body); len(res.Findings) > 0 {
t.Errorf("an html joomla tutorial should not match, got %d findings", len(res.Findings))
}
})
t.Run("a drupal settings using env indirection is not a literal password leak", func(t *testing.T) {
body := "<?php\n$databases['default']['default'] = array (\n" +
" 'database' => 'drupal_db',\n 'username' => 'drupal_user',\n" +
" 'password' => getenv('DB_PASS'),\n 'host' => 'localhost',\n);\n"
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
t.Errorf("env indirection should not match, got %d findings", len(res.Findings))
}
})
t.Run("a magento env with a cloud placeholder key is not a literal leak", func(t *testing.T) {
body := "<?php\nreturn ['crypt' => ['key' => '#env.CRYPT_KEY#'], 'MAGE_MODE' => 'production'];\n"
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
t.Errorf("a cloud placeholder should not match, got %d findings", len(res.Findings))
}
})
t.Run("a magento env with a placeholder key but a literal password is flagged not mis-extracted", func(t *testing.T) {
body := "<?php\nreturn ['crypt' => ['key' => '#env.CRYPT_KEY#'],\n" +
" 'db' => ['connection' => ['default' => ['password' => 'RealDbPass']]],\n" +
" 'MAGE_MODE' => 'production'];\n"
res := runCMSCfgModule(t, magento, 200, body)
if len(res.Findings) == 0 {
t.Fatal("expected a magento finding on the literal password")
}
if v := cmsCfgExtract(res, "magento_crypt_key"); v == "#env.CRYPT_KEY#" {
t.Errorf("extractor surfaced the placeholder %q as the crypt key", v)
}
})
t.Run("an html page demonstrating a drupal config is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>$databases['default']['default'] = array('password' => 'x');</pre></body></html>"
if res := runCMSCfgModule(t, drupal, 200, body); len(res.Findings) > 0 {
t.Errorf("an html drupal tutorial should not match, got %d findings", len(res.Findings))
}
})
t.Run("an html page demonstrating a magento config is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>'crypt' => ['key' => 'x'], 'MAGE_MODE' => 'production'</pre></body></html>"
if res := runCMSCfgModule(t, magento, 200, body); len(res.Findings) > 0 {
t.Errorf("an html magento tutorial 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{joomla, drupal, magento} {
if res := runCMSCfgModule(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{joomla, drupal, magento} {
if res := runCMSCfgModule(t, file, 404, "not found"); len(res.Findings) > 0 {
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
}
}
})
}
@@ -1,113 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runCredModule(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 credExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestCredentialExposureModules(t *testing.T) {
const aws = "../../modules/recon/aws-credentials-exposure.yaml"
const npmrc = "../../modules/recon/npmrc-exposure.yaml"
const docker = "../../modules/recon/docker-config-exposure.yaml"
t.Run("aws credentials leak the access key id", func(t *testing.T) {
body := "[default]\naws_access_key_id = AKIAIOSFODNN7EXAMPLE\n" +
"aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n"
res := runCredModule(t, aws, 200, body)
if len(res.Findings) == 0 {
t.Fatal("expected an aws credentials finding")
}
if v := credExtract(res, "aws_access_key_id"); v != "AKIAIOSFODNN7EXAMPLE" {
t.Errorf("aws_access_key_id=%q, want AKIAIOSFODNN7EXAMPLE", v)
}
})
t.Run("npmrc leaks the registry of an auth token", func(t *testing.T) {
body := "//registry.npmjs.org/:_authToken=npm_AbCdEf0123456789AbCdEf0123456789\n"
res := runCredModule(t, npmrc, 200, body)
if len(res.Findings) == 0 {
t.Fatal("expected an npmrc finding")
}
if v := credExtract(res, "npm_registry"); v != "registry.npmjs.org" {
t.Errorf("npm_registry=%q, want registry.npmjs.org", v)
}
})
t.Run("docker config leaks the registry host", func(t *testing.T) {
body := `{"auths":{"registry.example.com":{"auth":"dXNlcm5hbWU6c3VwZXJzZWNyZXRwYXNz"}}}`
res := runCredModule(t, docker, 200, body)
if len(res.Findings) == 0 {
t.Fatal("expected a docker config finding")
}
if v := credExtract(res, "docker_registry"); v != "registry.example.com" {
t.Errorf("docker_registry=%q, want registry.example.com", v)
}
})
t.Run("html page mentioning the key name is not a leak", func(t *testing.T) {
body := `<html><head><title>Docs</title></head><body>` +
`set your aws_secret_access_key in ~/.aws/credentials</body></html>`
if res := runCredModule(t, aws, 200, body); len(res.Findings) > 0 {
t.Errorf("an html doc mentioning the key should not match, got %d findings", len(res.Findings))
}
})
t.Run("a 404 is not a leak", func(t *testing.T) {
for _, file := range []string{aws, npmrc, docker} {
if res := runCredModule(t, file, 404, "not found"); len(res.Findings) > 0 {
t.Errorf("%s: a 404 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{aws, npmrc, docker} {
if res := runCredModule(t, file, 200, "nothing to see here"); len(res.Findings) > 0 {
t.Errorf("%s: a plain 200 body should not match, got %d findings", file, len(res.Findings))
}
}
})
t.Run("a docker auth field holding a jwt is not a leak", func(t *testing.T) {
body := `{"token":"x","auth":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"}`
if res := runCredModule(t, docker, 200, body); len(res.Findings) > 0 {
t.Errorf("a jwt in an auth field should not match, got %d findings", len(res.Findings))
}
})
}
-88
View File
@@ -1,88 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runDBModule(t *testing.T, file string, status int, headers map[string]string, 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) {
for k, v := range headers {
w.Header().Set(k, v)
}
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 dbExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestDBPanelModules(t *testing.T) {
const adminer = "../../modules/info/adminer-panel.yaml"
const phpmyadmin = "../../modules/info/phpmyadmin-panel.yaml"
adminerLogin := `<form action=""><input type="hidden" name="auth[driver]" value="server">` +
`<input name="auth[username]"></form>` +
`<p class="links"><a href="https://www.adminer.org/">Adminer</a> <span class="version">4.8.1</span></p>`
pmaLogin := `<link rel="stylesheet" href="themes/pmahomme/css/theme.css">` +
`<input type="text" name="pma_username"><script>var data = {"PMA_VERSION":"5.2.1"};</script>`
t.Run("adminer login", func(t *testing.T) {
res := runDBModule(t, adminer, 200, nil, adminerLogin)
if len(res.Findings) == 0 {
t.Fatal("expected an adminer finding")
}
if v := dbExtract(res, "adminer_version"); v != "4.8.1" {
t.Errorf("adminer_version=%q, want 4.8.1", v)
}
})
t.Run("adminer unrelated page", func(t *testing.T) {
if res := runDBModule(t, adminer, 200, nil, "<html><body>nothing</body></html>"); len(res.Findings) > 0 {
t.Errorf("unrelated page should not match, got %d findings", len(res.Findings))
}
})
t.Run("phpmyadmin login", func(t *testing.T) {
res := runDBModule(t, phpmyadmin, 200, map[string]string{"Set-Cookie": "phpMyAdmin=abc123; path=/"}, pmaLogin)
if len(res.Findings) == 0 {
t.Fatal("expected a phpmyadmin finding")
}
if v := dbExtract(res, "phpmyadmin_version"); v != "5.2.1" {
t.Errorf("phpmyadmin_version=%q, want 5.2.1", v)
}
})
t.Run("phpmyadmin unrelated page", func(t *testing.T) {
if res := runDBModule(t, phpmyadmin, 200, nil, "<html><body>nothing</body></html>"); len(res.Findings) > 0 {
t.Errorf("unrelated page should not match, got %d findings", len(res.Findings))
}
})
}
-121
View File
@@ -1,121 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runDebugModule(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 debugExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestDebugExposureModules(t *testing.T) {
const ignition = "../../modules/recon/laravel-ignition-exposure.yaml"
const profiler = "../../modules/recon/symfony-profiler-exposure.yaml"
const heapdump = "../../modules/recon/spring-heapdump-exposure.yaml"
t.Run("ignition health check exposes command execution", func(t *testing.T) {
res := runDebugModule(t, ignition, 200, `{"can_execute_commands":true,"config":{}}`)
if len(res.Findings) == 0 {
t.Fatal("expected an ignition finding")
}
if v := debugExtract(res, "can_execute_commands"); v != "true" {
t.Errorf("can_execute_commands=%q, want true", v)
}
})
t.Run("ignition exposed with debug off still flags and extracts false", func(t *testing.T) {
res := runDebugModule(t, ignition, 200, `{"can_execute_commands":false}`)
if len(res.Findings) == 0 {
t.Fatal("expected an ignition finding even when command execution is off")
}
if v := debugExtract(res, "can_execute_commands"); v != "false" {
t.Errorf("can_execute_commands=%q, want false", v)
}
})
t.Run("symfony profiler exposes a request token", func(t *testing.T) {
body := `<html><head><title>Symfony Profiler</title></head><body>` +
`<a href="/_profiler/5f3a2b">GET /</a></body></html>`
res := runDebugModule(t, profiler, 200, body)
if len(res.Findings) == 0 {
t.Fatal("expected a symfony profiler finding")
}
if v := debugExtract(res, "profiler_token"); v != "5f3a2b" {
t.Errorf("profiler_token=%q, want 5f3a2b", v)
}
})
t.Run("spring heap dump exposes the hprof magic", func(t *testing.T) {
body := "JAVA PROFILE 1.0.2\x00\x00\x00\x08heap bytes follow"
res := runDebugModule(t, heapdump, 200, body)
if len(res.Findings) == 0 {
t.Fatal("expected a heap dump finding")
}
if v := debugExtract(res, "hprof_version"); v != "1.0.2" {
t.Errorf("hprof_version=%q, want 1.0.2", v)
}
})
t.Run("the hprof magic must be at the start not merely present", func(t *testing.T) {
body := "<html><body>docs about the JAVA PROFILE 1.0.2 hprof header</body></html>"
if res := runDebugModule(t, heapdump, 200, body); len(res.Findings) > 0 {
t.Errorf("the magic away from the start should not match, got %d findings", len(res.Findings))
}
})
t.Run("a page that only names ignition is not the endpoint", func(t *testing.T) {
body := `<html><body>we use ignition to render errors in development</body></html>`
if res := runDebugModule(t, ignition, 200, body); len(res.Findings) > 0 {
t.Errorf("a prose mention 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{ignition, profiler, heapdump} {
if res := runDebugModule(t, file, 200, "<html><body>plain</body></html>"); 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{ignition, profiler, heapdump} {
if res := runDebugModule(t, file, 404, "not found"); len(res.Findings) > 0 {
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
}
}
})
}
@@ -1,164 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runDotfileModule(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 dotfileExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestDotfileCredentialExposureModules(t *testing.T) {
const netrc = "../../modules/recon/netrc-exposure.yaml"
const pgpass = "../../modules/recon/pgpass-exposure.yaml"
const mycnf = "../../modules/recon/mysql-client-config-exposure.yaml"
netrcBody := "machine api.example.com\n login deploy\n password s3cr3tP@ss\n" +
"machine ftp.example.com\n login anon\n password anon@site\n"
pgpassBody := "db.example.com:5432:appdb:appuser:Sup3rSecret\n*:*:*:replication:replpass\n"
mycnfBody := "[client]\nuser=root\npassword=R00tPass!\nhost=127.0.0.1\nport=3306\n"
t.Run("an exposed netrc leaks the machine host", func(t *testing.T) {
res := runDotfileModule(t, netrc, 200, netrcBody)
if len(res.Findings) == 0 {
t.Fatal("expected a netrc finding")
}
if v := dotfileExtract(res, "netrc_machine"); v != "api.example.com" {
t.Errorf("netrc_machine=%q, want api.example.com", v)
}
})
t.Run("an exposed pgpass leaks the host", func(t *testing.T) {
res := runDotfileModule(t, pgpass, 200, pgpassBody)
if len(res.Findings) == 0 {
t.Fatal("expected a pgpass finding")
}
if v := dotfileExtract(res, "pgpass_host"); v != "db.example.com" {
t.Errorf("pgpass_host=%q, want db.example.com", v)
}
})
t.Run("an exposed my.cnf leaks the client user", func(t *testing.T) {
res := runDotfileModule(t, mycnf, 200, mycnfBody)
if len(res.Findings) == 0 {
t.Fatal("expected a my.cnf finding")
}
if v := dotfileExtract(res, "mysql_user"); v != "root" {
t.Errorf("mysql_user=%q, want root", v)
}
})
t.Run("prose that names machine login and password out of order is not a netrc", func(t *testing.T) {
body := "this machine requires a login; store the password securely"
if res := runDotfileModule(t, netrc, 200, body); len(res.Findings) > 0 {
t.Errorf("out of order prose should not match, got %d findings", len(res.Findings))
}
})
t.Run("an html page demonstrating a netrc is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>machine api.example.com login deploy password s3cret</pre></body></html>"
if res := runDotfileModule(t, netrc, 200, body); len(res.Findings) > 0 {
t.Errorf("an html netrc tutorial should not match, got %d findings", len(res.Findings))
}
})
t.Run("a yaml db config with colon keys is not a pgpass", func(t *testing.T) {
body := "database:\n host: db.example.com\n port: 5432\n user: appuser\n password: secret\n"
if res := runDotfileModule(t, pgpass, 200, body); len(res.Findings) > 0 {
t.Errorf("a yaml db config should not match, got %d findings", len(res.Findings))
}
})
t.Run("a pgpass shaped line with a non numeric port is not flagged", func(t *testing.T) {
body := "db.example.com:default:appdb:appuser:Sup3rSecret\n"
if res := runDotfileModule(t, pgpass, 200, body); len(res.Findings) > 0 {
t.Errorf("a non numeric port should not match, got %d findings", len(res.Findings))
}
})
t.Run("a multi line config with a number field does not match across lines", func(t *testing.T) {
body := "timeout:30:seconds configured\nsee http://docs.example.com:8080 for details\n"
if res := runDotfileModule(t, pgpass, 200, body); len(res.Findings) > 0 {
t.Errorf("fields must stay on one line, got %d findings", len(res.Findings))
}
})
t.Run("an html page demonstrating a pgpass is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html>\n<html><body><pre>\ndb.example.com:5432:appdb:appuser:secret\n</pre></body></html>\n"
if res := runDotfileModule(t, pgpass, 200, body); len(res.Findings) > 0 {
t.Errorf("an html pgpass tutorial should not match, got %d findings", len(res.Findings))
}
})
t.Run("a my.cnf client section without a password is not flagged", func(t *testing.T) {
body := "[client]\nuser=root\nhost=localhost\nport=3306\n"
if res := runDotfileModule(t, mycnf, 200, body); len(res.Findings) > 0 {
t.Errorf("a section without a password should not match, got %d findings", len(res.Findings))
}
})
t.Run("a password line without a my.cnf section is not flagged", func(t *testing.T) {
body := "password=hunter2\nfoo=bar\n"
if res := runDotfileModule(t, mycnf, 200, body); len(res.Findings) > 0 {
t.Errorf("a password without a section should not match, got %d findings", len(res.Findings))
}
})
t.Run("an html page demonstrating a my.cnf is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>[client]\npassword=secret</pre></body></html>"
if res := runDotfileModule(t, mycnf, 200, body); len(res.Findings) > 0 {
t.Errorf("an html my.cnf tutorial 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{netrc, pgpass, mycnf} {
if res := runDotfileModule(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{netrc, pgpass, mycnf} {
if res := runDotfileModule(t, file, 404, "not found"); len(res.Findings) > 0 {
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
}
}
})
}
-112
View File
@@ -1,112 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
// runOpsModule runs a shipped module end to end against a server that returns
// the same status and body for every path it requests.
func runOpsModule(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 opsExtracted(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v, ok := f.Extracted[key]; ok {
return v
}
}
return ""
}
func TestOpsPanelModules(t *testing.T) {
cases := []struct {
name string
file string
status int
body string
wantFinding bool
versionKey string
versionVal string
}{
{
name: "portainer status api", file: "../../modules/info/portainer-panel.yaml", status: 200,
body: `{"Edition":"CE","Version":"2.19.4","InstanceID":"a1b2c3"}`,
wantFinding: true, versionKey: "portainer_version", versionVal: "2.19.4",
},
{
name: "portainer version-only json is not a match", file: "../../modules/info/portainer-panel.yaml", status: 200,
body: `{"Version":"1.0.0"}`, wantFinding: false,
},
{
name: "portainer real body behind a 404 is not a match", file: "../../modules/info/portainer-panel.yaml", status: 404,
body: `{"Edition":"CE","Version":"2.19.4","InstanceID":"a1b2c3"}`, wantFinding: false,
},
{
name: "traefik version api", file: "../../modules/info/traefik-panel.yaml", status: 200,
body: `{"Version":"2.10.4","Codename":"saintnectaire","startDate":"2024-01-01T00:00:00Z"}`,
wantFinding: true, versionKey: "traefik_version", versionVal: "2.10.4",
},
{
name: "traefik without codename is not a match", file: "../../modules/info/traefik-panel.yaml", status: 200,
body: `{"Version":"2.10.4"}`, wantFinding: false,
},
{
name: "keycloak realm endpoint", file: "../../modules/info/keycloak-panel.yaml", status: 200,
body: `{"realm":"master","public_key":"MIIBIjAN","token-service":"https://h/realms/master/protocol/openid-connect","account-service":"https://h/realms/master/account"}`,
wantFinding: true, versionKey: "keycloak_realm", versionVal: "master",
},
{
name: "keycloak partial realm json is not a match", file: "../../modules/info/keycloak-panel.yaml", status: 200,
body: `{"realm":"master","public_key":"MIIBIjAN"}`, wantFinding: false,
},
{
name: "rabbitmq management ui", file: "../../modules/info/rabbitmq-panel.yaml", status: 200,
body: `<!DOCTYPE html><html><head><title>RabbitMQ Management</title></head><body><img src="img/rabbitmqlogo.svg"></body></html>`,
wantFinding: true,
},
{
name: "rabbitmq unrelated page is not a match", file: "../../modules/info/rabbitmq-panel.yaml", status: 200,
body: `<html><body>nothing to see</body></html>`, wantFinding: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
res := runOpsModule(t, tc.file, tc.status, tc.body)
got := len(res.Findings) > 0
if got != tc.wantFinding {
t.Fatalf("findings=%d, want match=%v", len(res.Findings), tc.wantFinding)
}
if tc.versionKey != "" {
if v := opsExtracted(res, tc.versionKey); v != tc.versionVal {
t.Errorf("extracted[%q]=%q, want %q", tc.versionKey, v, tc.versionVal)
}
}
})
}
}
-118
View File
@@ -1,118 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runRegistryModule(t *testing.T, file string, status int, headers map[string]string, 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) {
for k, v := range headers {
w.Header().Set(k, v)
}
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 registryExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestRegistryExposureModules(t *testing.T) {
const dockerRegistry = "../../modules/recon/docker-registry-api-exposure.yaml"
const harbor = "../../modules/recon/harbor-api-exposure.yaml"
registryHeader := map[string]string{"Docker-Distribution-Api-Version": "registry/2.0"}
harborInfo := `{"auth_mode":"db_auth","registry_url":"harbor.example.com",` +
`"external_url":"https://harbor.example.com","harbor_version":"v2.9.1-1f4a3c9d",` +
`"self_registration":true,"has_ca_root":false,"read_only":false}`
t.Run("an anonymous docker registry is flagged with its api version", func(t *testing.T) {
res := runRegistryModule(t, dockerRegistry, 200, registryHeader, "{}")
if len(res.Findings) == 0 {
t.Fatal("expected a docker registry finding")
}
if v := registryExtract(res, "docker_registry_api_version"); v != "registry/2.0" {
t.Errorf("docker_registry_api_version=%q, want registry/2.0", v)
}
})
t.Run("a plain 200 without the registry header is not flagged", func(t *testing.T) {
if res := runRegistryModule(t, dockerRegistry, 200, nil, "{}"); len(res.Findings) > 0 {
t.Errorf("a 200 without the api-version header should not match, got %d findings", len(res.Findings))
}
})
t.Run("a registry that requires auth is not flagged", func(t *testing.T) {
if res := runRegistryModule(t, dockerRegistry, 401, registryHeader, ""); len(res.Findings) > 0 {
t.Errorf("a 401 registry should not match, got %d findings", len(res.Findings))
}
})
t.Run("an exposed harbor systeminfo is flagged and versioned", func(t *testing.T) {
res := runRegistryModule(t, harbor, 200, nil, harborInfo)
if len(res.Findings) == 0 {
t.Fatal("expected a harbor finding")
}
if v := registryExtract(res, "harbor_version"); v != "v2.9.1-1f4a3c9d" {
t.Errorf("harbor_version=%q, want v2.9.1-1f4a3c9d", v)
}
})
t.Run("a harbor version without an auth mode is not flagged", func(t *testing.T) {
body := `{"harbor_version":"v2.9.1","registry_url":"harbor.example.com"}`
if res := runRegistryModule(t, harbor, 200, nil, body); len(res.Findings) > 0 {
t.Errorf("a harbor version alone should not match, got %d findings", len(res.Findings))
}
})
t.Run("an auth mode without a harbor version is not flagged", func(t *testing.T) {
body := `{"auth_mode":"db_auth","self_registration":true}`
if res := runRegistryModule(t, harbor, 200, nil, body); len(res.Findings) > 0 {
t.Errorf("an auth mode alone 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{dockerRegistry, harbor} {
if res := runRegistryModule(t, file, 200, nil, "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{dockerRegistry, harbor} {
if res := runRegistryModule(t, file, 404, nil, "not found"); len(res.Findings) > 0 {
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
}
}
})
}
@@ -1,155 +0,0 @@
package modules_test
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/modules"
)
func runVCSModule(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 vcsExtract(res *modules.Result, key string) string {
for _, f := range res.Findings {
if v := f.Extracted[key]; v != "" {
return v
}
}
return ""
}
func TestVCSMetadataExposureModules(t *testing.T) {
const svn = "../../modules/recon/svn-exposure.yaml"
const hg = "../../modules/recon/mercurial-exposure.yaml"
const bzr = "../../modules/recon/bazaar-exposure.yaml"
svnWcDb := "SQLite format 3\x00" + strings.Repeat("\x00", 80) +
"CREATE TABLE WCROOT (id INTEGER PRIMARY KEY);" +
"CREATE TABLE REPOSITORY (root TEXT, uuid TEXT);" +
"\x01root\x00https://svn.example.com/myrepo/trunk\x00"
hgRequires := "revlogv1\nstore\nfncache\ndotencode\ngeneraldelta\nsparserevlog\n"
bzrFormat := "Bazaar-NG meta directory, format 1\n"
t.Run("an exposed svn wc.db leaks the repository url", func(t *testing.T) {
res := runVCSModule(t, svn, 200, svnWcDb)
if len(res.Findings) == 0 {
t.Fatal("expected an svn finding")
}
if v := vcsExtract(res, "svn_repository"); v != "https://svn.example.com/myrepo/trunk" {
t.Errorf("svn_repository=%q, want https://svn.example.com/myrepo/trunk", v)
}
})
t.Run("an exposed mercurial requires is flagged", func(t *testing.T) {
res := runVCSModule(t, hg, 200, hgRequires)
if len(res.Findings) == 0 {
t.Fatal("expected a mercurial finding")
}
if v := vcsExtract(res, "hg_requirement"); v != "revlogv1" {
t.Errorf("hg_requirement=%q, want revlogv1", v)
}
})
t.Run("an exposed bazaar branch-format is flagged", func(t *testing.T) {
res := runVCSModule(t, bzr, 200, bzrFormat)
if len(res.Findings) == 0 {
t.Fatal("expected a bazaar finding")
}
if v := vcsExtract(res, "bzr_format"); v != "Bazaar-NG meta directory, format 1" {
t.Errorf("bzr_format=%q, want the meta directory signature", v)
}
})
t.Run("a generic sqlite database without svn tables is not flagged", func(t *testing.T) {
body := "SQLite format 3\x00" + strings.Repeat("\x00", 80) +
"CREATE TABLE users (id INTEGER, name TEXT, email TEXT);"
if res := runVCSModule(t, svn, 200, body); len(res.Findings) > 0 {
t.Errorf("a generic sqlite db should not match svn, got %d findings", len(res.Findings))
}
})
t.Run("a generic sqlite with a nodes table is not an svn working copy", func(t *testing.T) {
body := "SQLite format 3\x00" + strings.Repeat("\x00", 80) +
"CREATE TABLE NODES (id INTEGER PRIMARY KEY, parent INTEGER, label TEXT);"
if res := runVCSModule(t, svn, 200, body); len(res.Findings) > 0 {
t.Errorf("a generic nodes table should not match svn, got %d findings", len(res.Findings))
}
})
t.Run("an svn magic that is not at byte zero is not flagged", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>SQLite format 3 WCROOT REPOSITORY</pre></body></html>"
if res := runVCSModule(t, svn, 200, body); len(res.Findings) > 0 {
t.Errorf("an unanchored magic should not match, got %d findings", len(res.Findings))
}
})
t.Run("a page naming mercurial without the requires format is not flagged", func(t *testing.T) {
body := "this project uses mercurial for distributed version control"
if res := runVCSModule(t, hg, 200, body); len(res.Findings) > 0 {
t.Errorf("prose naming mercurial should not match, got %d findings", len(res.Findings))
}
})
t.Run("an html page demonstrating hg requires is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>revlogv1\nstore\ndotencode</pre></body></html>"
if res := runVCSModule(t, hg, 200, body); len(res.Findings) > 0 {
t.Errorf("an html hg tutorial should not match, got %d findings", len(res.Findings))
}
})
t.Run("a bazaar marketplace page is not a repository", func(t *testing.T) {
body := "Welcome to the Bazaar, the finest open air marketplace in town"
if res := runVCSModule(t, bzr, 200, body); len(res.Findings) > 0 {
t.Errorf("a marketplace page should not match, got %d findings", len(res.Findings))
}
})
t.Run("an html page demonstrating bzr branch-format is not a leak", func(t *testing.T) {
body := "<!DOCTYPE html><html><body><pre>Bazaar-NG meta directory, format 1</pre></body></html>"
if res := runVCSModule(t, bzr, 200, body); len(res.Findings) > 0 {
t.Errorf("an html bzr tutorial 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{svn, hg, bzr} {
if res := runVCSModule(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{svn, hg, bzr} {
if res := runVCSModule(t, file, 404, "not found"); len(res.Findings) > 0 {
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
}
}
})
}
+8 -15
View File
@@ -84,22 +84,15 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
}
func extractPotentialBuckets(url string) []string {
labels := strings.Split(url, ".")
// drop the tld label so we don't waste guesses on it ("com", "com-s3", ...);
// a single-label host has no tld to strip.
if len(labels) > 1 {
labels = labels[:len(labels)-1]
}
// TODO: handle non-adjacent label combos and strip the tld
parts := strings.Split(url, ".")
var buckets []string
for _, label := range labels {
buckets = append(buckets, label, label+"-s3", "s3-"+label)
}
// combine every label with every other, not just adjacent ones, so a deep
// host like shop.cdn.example yields shop-example too.
for i, a := range labels {
for _, b := range labels[i+1:] {
buckets = append(buckets, a+"-"+b, b+"-"+a)
for i, part := range parts {
buckets = append(buckets, part, part+"-s3", "s3-"+part)
if i < len(parts)-1 {
domainExtension := part + "-" + parts[i+1]
buckets = append(buckets, domainExtension, parts[i+1]+"-"+part)
}
}
return buckets
-62
View File
@@ -1,62 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"slices"
"testing"
)
func TestExtractPotentialBuckets(t *testing.T) {
tests := []struct {
name string
host string
want []string // candidates that must be generated
absent []string // candidates that must not be generated
}{
{
name: "strips the tld and pairs labels both ways",
host: "shop.example.com",
want: []string{"shop", "shop-s3", "s3-shop", "example", "shop-example", "example-shop"},
absent: []string{"com", "com-s3", "s3-com", "example-com", "com-example"},
},
{
name: "combines non-adjacent labels",
host: "a.b.c.example.com",
want: []string{"a-c", "c-a", "a-example", "example-a", "b-example"},
absent: []string{"com", "example-com"},
},
{
name: "single-label host keeps its only label and makes no pairs",
host: "localhost",
want: []string{"localhost", "localhost-s3", "s3-localhost"},
absent: []string{"localhost-localhost", ""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractPotentialBuckets(tt.host)
for _, w := range tt.want {
if !slices.Contains(got, w) {
t.Errorf("extractPotentialBuckets(%q) missing %q; got %v", tt.host, w, got)
}
}
for _, a := range tt.absent {
if slices.Contains(got, a) {
t.Errorf("extractPotentialBuckets(%q) should not generate %q; got %v", tt.host, a, got)
}
}
})
}
}
+8 -19
View File
@@ -102,12 +102,12 @@ func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
}
func detectWordPress(url string, client *http.Client, bodyString string) bool {
// wordpress asset paths only; the bare word "wordpress" matched pages that
// merely mention it (wp-hosting marketing), so it is dropped.
// Check for common WordPress indicators in the HTML
wpIndicators := []string{
"wp-content",
"wp-includes",
"wp-json",
"wordpress",
}
for _, indicator := range wpIndicators {
@@ -128,23 +128,12 @@ func detectWordPress(url string, client *http.Client, bodyString string) bool {
if err != nil {
continue
}
resp, err := client.Do(req)
if err != nil {
continue
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
resp.Body.Close()
if err != nil {
continue
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusFound {
continue
}
// the client follows redirects, so soft-404 and catch-all sites also land
// here with a 200; require an actual WordPress marker in the body.
probeBody := string(body)
for _, indicator := range wpIndicators {
if strings.Contains(probeBody, indicator) {
resp, err := client.Do(req) //nolint:bodyclose // drained and closed via httpx.DrainClose
if err == nil {
found := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound
// status only; drain so the conn returns to the pool.
httpx.DrainClose(resp)
if found {
return true
}
}
@@ -1,49 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
// notFound serves 404 for every path so the file probe never fires; only the
// passed homepage body decides the result.
func notFound(t *testing.T) string {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
t.Cleanup(srv.Close)
return srv.URL
}
// a page that only mentions wordpress in prose (no asset paths) is not running it.
func TestDetectWordPress_BareMentionNotFlagged(t *testing.T) {
client := &http.Client{Timeout: 5 * time.Second}
body := "<html><body>we offer managed wordpress hosting</body></html>"
if detectWordPress(notFound(t), client, body) {
t.Error("a page merely mentioning wordpress was flagged as WordPress")
}
}
// a real wordpress homepage references wp-content asset paths.
func TestDetectWordPress_AssetPathsDetected(t *testing.T) {
client := &http.Client{Timeout: 5 * time.Second}
body := `<link href="/wp-content/themes/x/style.css">`
if !detectWordPress(notFound(t), client, body) {
t.Error("wp-content asset path should be detected as WordPress")
}
}
-102
View File
@@ -1,102 +0,0 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
// a soft-404 (200 for every path) is not wordpress; a bare 200 on the probe must
// not flag without a marker in the body.
func TestDetectWordPress_SoftFourOhFourNotFlagged(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("<html><body>welcome to my static site</body></html>"))
}))
defer srv.Close()
client := &http.Client{Timeout: 5 * time.Second}
if detectWordPress(srv.URL, client, "<html><body>welcome to my static site</body></html>") {
t.Error("soft-404 site (200 for every path, no wordpress markers) wrongly detected as WordPress")
}
}
// a catch-all 302 is followed to a non-wordpress 200; without a marker it must
// not flag.
func TestDetectWordPress_CatchAllRedirectNotFlagged(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/home", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("<html><body>landing page</body></html>"))
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/home", http.StatusFound)
})
srv := httptest.NewServer(mux)
defer srv.Close()
client := &http.Client{Timeout: 5 * time.Second}
if detectWordPress(srv.URL, client, "<html><body>landing page</body></html>") {
t.Error("catch-all 302 redirect to a non-wordpress homepage wrongly detected as WordPress")
}
}
// a real wp-login.php response references wp-includes assets even when the
// homepage hides its wordpress markers, so the file probe should still detect it.
func TestDetectWordPress_LoginPageProbeDetected(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/wp-login.php" {
_, _ = w.Write([]byte(`<link rel="stylesheet" href="/wp-includes/css/dashicons.min.css">`))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
client := &http.Client{Timeout: 5 * time.Second}
if !detectWordPress(srv.URL, client, "<html><body>custom theme, no markers</body></html>") {
t.Error("wp-login.php referencing wp-includes assets should be detected as WordPress")
}
}
// end-to-end through CMS() with the real redirect-following client: a soft-404
// host must not be reported as a CMS.
func TestCMS_SoftFourOhFourNotWordPress(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("<html><body>welcome to my static site</body></html>"))
}))
defer srv.Close()
result, err := CMS(srv.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("CMS: %v", err)
}
if result != nil {
t.Errorf("soft-404 host wrongly classified as CMS %q", result.Name)
}
}
// a probe that hits a redirect loop errors out in the client; it must be skipped
// gracefully, never panicking or counting as a detection.
func TestDetectWordPress_RedirectLoopHandled(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/loop", http.StatusFound)
}))
defer srv.Close()
client := &http.Client{Timeout: 5 * time.Second}
if detectWordPress(srv.URL, client, "<html><body>no markers</body></html>") {
t.Error("redirect loop wrongly detected as WordPress")
}
}
+9 -4
View File
@@ -42,6 +42,10 @@ type CORSFinding struct {
Note string `json:"note"`
}
// corsMaxRedirects caps the redirect chain so we read the cors headers off the
// host we actually asked about, not whatever it bounces us to.
const corsMaxRedirects = 3
// the sentinel attacker origin; if it comes back in Access-Control-Allow-Origin
// the target reflects arbitrary origins and any site can read the response.
const corsEvilOrigin = "https://sif-cors-probe.evil.com"
@@ -93,10 +97,11 @@ func CORS(targetURL string, timeout time.Duration, threads int, logdir string) (
host := parsedURL.Host
client := httpx.Client(timeout)
// don't follow redirects: cors is judged on the host we asked about, so a
// bounce to a permissive third party can't be pinned on the target.
client.CheckRedirect = func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
client.CheckRedirect = func(_ *http.Request, via []*http.Request) error {
if len(via) >= corsMaxRedirects {
return http.ErrUseLastResponse
}
return nil
}
result := &CORSResult{Findings: make([]CORSFinding, 0, len(corsOrigins))}
-33
View File
@@ -15,7 +15,6 @@ package scan
import (
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
)
@@ -133,38 +132,6 @@ func TestCORS_NoFalsePositiveOnSafeServer(t *testing.T) {
}
}
// TestCORS_JudgesRequestedHostNotRedirectTarget pins the redirect behavior: the
// requested host bounces to a reflecting third party, so following the redirect would
// pin that party's misconfig on the target. the counter proves we never left the host.
func TestCORS_JudgesRequestedHostNotRedirectTarget(t *testing.T) {
var destHits int32
dest := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&destHits, 1)
if origin := r.Header.Get("Origin"); origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
w.WriteHeader(http.StatusOK)
}))
defer dest.Close()
redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, dest.URL, http.StatusFound)
}))
defer redirector.Close()
result, err := CORS(redirector.URL, 5*time.Second, 3, "")
if err != nil {
t.Fatalf("CORS: %v", err)
}
if n := atomic.LoadInt32(&destHits); n != 0 {
t.Errorf("followed the redirect to the reflecting host %d time(s); cors must stay on the requested host", n)
}
if result != nil && len(result.Findings) > 0 {
t.Errorf("expected no findings: the reflection is on the redirect target, not the requested host; got %+v", result.Findings)
}
}
func TestCORSResult_ResultType(t *testing.T) {
r := &CORSResult{}
if r.ResultType() != "cors" {
+4 -14
View File
@@ -303,11 +303,7 @@ func fetchWordlist(listURL string, client *http.Client) ([]string, error) {
return nil, fmt.Errorf("download wordlist %q: %w", listURL, err)
}
defer resp.Body.Close()
lines, err := scanLines(resp.Body)
if err != nil {
return nil, fmt.Errorf("download wordlist %q: %w", listURL, err)
}
return lines, nil
return scanLines(resp.Body), nil
}
// readWordlistFile loads a local wordlist file.
@@ -317,15 +313,11 @@ func readWordlistFile(path string) ([]string, error) {
return nil, fmt.Errorf("open wordlist %q: %w", path, err)
}
defer f.Close()
lines, err := scanLines(f)
if err != nil {
return nil, fmt.Errorf("read wordlist %q: %w", path, err)
}
return lines, nil
return scanLines(f), nil
}
// scanLines reads non-empty lines into a slice.
func scanLines(r io.Reader) ([]string, error) {
func scanLines(r io.Reader) []string {
var lines []string
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanLines)
@@ -335,9 +327,7 @@ func scanLines(r io.Reader) ([]string, error) {
lines = append(lines, line)
}
}
// a line past bufio's 64k cap halts the scan; surface it instead of
// silently dropping that line and everything after it.
return lines, scanner.Err()
return lines
}
// calibrate probes a few paths that cannot exist and records the response shapes
-23
View File
@@ -13,8 +13,6 @@
package scan
import (
"bufio"
"errors"
"net/http"
"net/http/httptest"
"os"
@@ -360,24 +358,3 @@ func has(set map[string]struct{}, key string) bool {
_, ok := set[key]
return ok
}
func TestScanLines(t *testing.T) {
got, err := scanLines(strings.NewReader("admin\n\nlogin\n"))
if err != nil {
t.Fatalf("scanLines: %v", err)
}
want := []string{"admin", "login"}
if !reflect.DeepEqual(got, want) {
t.Errorf("scanLines = %v, want %v", got, want)
}
}
func TestScanLinesErrorsOnOverlongLine(t *testing.T) {
// a line past bufio's 64k cap must surface as an error, not silently
// truncate the wordlist (dropping that line and everything after it).
huge := strings.Repeat("a", bufio.MaxScanTokenSize+1)
_, err := scanLines(strings.NewReader("first\n" + huge + "\nlast\n"))
if !errors.Is(err, bufio.ErrTooLong) {
t.Fatalf("scanLines err = %v, want bufio.ErrTooLong", err)
}
}
+40 -2
View File
@@ -14,6 +14,7 @@ package scan
import (
"context"
"encoding/base64"
"fmt"
"io"
"net/http"
@@ -21,10 +22,10 @@ import (
"strings"
"time"
"github.com/dropalldatabases/sif/internal/fingerprint"
"github.com/dropalldatabases/sif/internal/httpx"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
"github.com/twmb/murmur3"
)
// FaviconResult is the computed shodan-style favicon hash plus the pivot query
@@ -41,6 +42,11 @@ type FaviconResult struct {
// stream forever.
const faviconBodyReadCap = 1 << 20
// b64LineLen is python's base64.encodebytes line width. mmh3/shodan hash the
// chunked base64 (newline every 76 chars, trailing newline), so we must wrap at
// exactly this width to land on the same hash.
const b64LineLen = 76
// faviconLinkRegex pulls the href off a <link rel="...icon..."> tag so we can
// fall back to a declared icon when /favicon.ico is absent.
var faviconLinkRegex = regexp.MustCompile(`(?i)<link[^>]+rel=["'][^"']*icon[^"']*["'][^>]*>`)
@@ -89,7 +95,7 @@ func Favicon(targetURL string, timeout time.Duration, logdir string) (*FaviconRe
return nil, nil //nolint:nilnil // a missing favicon is not an error
}
hash := fingerprint.FaviconHash(data)
hash := FaviconHash(data)
result := &FaviconResult{
FaviconURL: iconURL,
Hash: hash,
@@ -210,6 +216,38 @@ func resolveFaviconURL(base, href string) string {
return base + "/" + href
}
// FaviconHash computes shodan's favicon hash: murmur3 32-bit over the python
// base64.encodebytes encoding of the raw icon (newline every 76 chars plus a
// trailing newline), reinterpreted as a signed int32. the chunking and the sign
// are both load-bearing - shodan stores the value python's mmh3.hash() returns,
// which is signed, over the wrapped base64, not the raw bytes. the golden test
// pins this exactly.
func FaviconHash(data []byte) int32 {
encoded := encodeFaviconBase64(data)
return int32(murmur3.Sum32(encoded)) //nolint:gosec // shodan stores the signed reinterpretation on purpose
}
// encodeFaviconBase64 mirrors python's base64.encodebytes: standard base64 with
// a newline inserted every 76 output characters and a trailing newline. this is
// the exact byte stream shodan feeds to mmh3, so it must match byte-for-byte.
func encodeFaviconBase64(data []byte) []byte {
raw := base64.StdEncoding.EncodeToString(data)
var b strings.Builder
// final size: the base64 body plus one '\n' per (full or partial) 76-char
// line. preallocate so the builder never regrows mid-loop.
b.Grow(len(raw) + len(raw)/b64LineLen + 1)
for i := 0; i < len(raw); i += b64LineLen {
end := i + b64LineLen
if end > len(raw) {
end = len(raw)
}
b.WriteString(raw[i:end])
b.WriteByte('\n')
}
return []byte(b.String())
}
// ResultType identifies favicon findings for the result registry.
func (r *FaviconResult) ResultType() string { return "favicon" }
+42
View File
@@ -31,6 +31,48 @@ var goldenFaviconBytes = []byte(strings.Repeat("sif-favicon-golden-test-bytes-",
// signedness regress, this number changes and the test fails.
const goldenFaviconHash int32 = -1554620260
// goldenHelloHash pins a short single-line case so a regression in the trailing
// newline (which the small case still has) is caught independently.
const goldenHelloHash int32 = 1155597304
func TestFaviconHash_Golden(t *testing.T) {
tests := []struct {
name string
in []byte
want int32
}{
{name: "multi-line fixture", in: goldenFaviconBytes, want: goldenFaviconHash},
{name: "single-line hello", in: []byte("hello"), want: goldenHelloHash},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := FaviconHash(tt.in)
if got != tt.want {
t.Errorf("FaviconHash = %d, want %d", got, tt.want)
}
})
}
}
// TestFaviconBase64Chunking pins the encode step against python's
// base64.encodebytes: a 50-byte input encodes to >76 base64 chars, so it must
// wrap into two newline-terminated lines.
func TestFaviconBase64Chunking(t *testing.T) {
in := []byte(strings.Repeat("A", 60)) // 60 bytes -> 80 base64 chars -> two lines
got := string(encodeFaviconBase64(in))
lines := strings.Split(strings.TrimRight(got, "\n"), "\n")
if len(lines) != 2 {
t.Fatalf("expected 2 wrapped lines, got %d: %q", len(lines), got)
}
if len(lines[0]) != b64LineLen {
t.Errorf("first line = %d chars, want %d", len(lines[0]), b64LineLen)
}
if !strings.HasSuffix(got, "\n") {
t.Errorf("encoding must end in a trailing newline, got %q", got)
}
}
// fixtureFaviconServer serves the golden bytes at /favicon.ico.
func fixtureFaviconServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-193
View File
@@ -1,193 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package frameworks
import (
"fmt"
"math"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/output"
"gopkg.in/yaml.v3"
)
// customDetector is a Detector defined in a user yaml file rather than compiled
// in. it scores with the same weighted signature match as the built-ins and
// optionally pulls a version out of the body.
type customDetector struct {
BaseDetector
versionRe *regexp.Regexp
versionGroup int
}
// Detect returns the weighted signature confidence and, when a version regex is
// set and matches, the captured version. confidence is the matched-weight
// fraction directly (not the built-ins' sigmoid), so it clears 0.5 only past half.
func (d *customDetector) Detect(body string, headers http.Header) (float32, string) {
confidence := d.MatchSignatures(body, headers)
if confidence == 0 || d.versionRe == nil {
return confidence, ""
}
matches := d.versionRe.FindStringSubmatch(body)
if len(matches) > d.versionGroup {
return confidence, matches[d.versionGroup]
}
return confidence, ""
}
// signatureSpec / versionSpec / customDetectorSpec mirror the yaml on disk.
type signatureSpec struct {
Pattern string `yaml:"pattern"`
Weight float32 `yaml:"weight"`
Header bool `yaml:"header"`
}
type versionSpec struct {
Regex string `yaml:"regex"`
Group int `yaml:"group"`
}
type customDetectorSpec struct {
Name string `yaml:"name"`
Signatures []signatureSpec `yaml:"signatures"`
Version *versionSpec `yaml:"version"`
}
// build validates the parsed spec and turns it into a Detector, so a broken
// file fails loudly instead of registering a detector that can never match.
func (spec customDetectorSpec) build() (Detector, error) {
name := strings.TrimSpace(spec.Name)
if name == "" {
return nil, fmt.Errorf("missing name")
}
if len(spec.Signatures) == 0 {
return nil, fmt.Errorf("%q has no signatures", name)
}
sigs := make([]Signature, 0, len(spec.Signatures))
for i, s := range spec.Signatures {
if s.Pattern == "" {
return nil, fmt.Errorf("%q: signature %d has an empty pattern", name, i+1)
}
if s.Weight <= 0 || math.IsInf(float64(s.Weight), 0) || math.IsNaN(float64(s.Weight)) {
return nil, fmt.Errorf("%q: signature %q needs a positive, finite weight", name, s.Pattern)
}
sigs = append(sigs, Signature{Pattern: s.Pattern, Weight: s.Weight, HeaderOnly: s.Header})
}
d := &customDetector{BaseDetector: NewBaseDetector(name, sigs)}
if spec.Version != nil {
if spec.Version.Group < 0 {
return nil, fmt.Errorf("%q: version group must be >= 0", name)
}
re, err := regexp.Compile(spec.Version.Regex)
if err != nil {
return nil, fmt.Errorf("%q: version regex: %w", name, err)
}
d.versionRe = re
d.versionGroup = spec.Version.Group
}
return d, nil
}
// parseCustomDetector reads and validates one signature file.
func parseCustomDetector(path string) (Detector, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read: %w", err)
}
var spec customDetectorSpec
if err := yaml.Unmarshal(data, &spec); err != nil {
return nil, fmt.Errorf("parse: %w", err)
}
return spec.build()
}
// customSignaturesDir is the per-user directory that holds yaml-defined
// detectors, alongside the user modules directory.
func customSignaturesDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
if runtime.GOOS == "windows" {
return filepath.Join(home, "AppData", "Local", "sif", "signatures"), nil
}
return filepath.Join(home, ".config", "sif", "signatures"), nil
}
// loadCustomDetectors registers every signature file under the user directory.
// it is driven once, lazily, from DetectFramework.
func loadCustomDetectors() {
dir, err := customSignaturesDir()
if err != nil {
return
}
loadCustomDetectorsFromDir(dir)
}
// loadCustomDetectorsFromDir registers every signature file in dir and returns
// how many loaded. a custom detector whose name matches a built-in overrides
// it, matching the user-module convention.
func loadCustomDetectorsFromDir(dir string) int {
detectors := collectCustomDetectors(dir)
for _, d := range detectors {
Register(d)
}
if len(detectors) > 0 {
output.Module("FRAMEWORK").Info("Loaded %d custom signatures", len(detectors))
}
return len(detectors)
}
// collectCustomDetectors parses (without registering) the .yaml/.yml detectors
// in dir, so discovery and validation stay pure and testable. a missing dir is
// fine; an unparseable file warns and is skipped rather than failing the scan.
func collectCustomDetectors(dir string) []Detector {
entries, err := os.ReadDir(dir)
if err != nil {
return nil
}
var detectors []Detector
for _, e := range entries {
if e.IsDir() {
continue
}
switch filepath.Ext(e.Name()) {
case ".yaml", ".yml":
default:
continue
}
d, err := parseCustomDetector(filepath.Join(dir, e.Name()))
if err != nil {
charmlog.Warnf("custom signature %s: %v", e.Name(), err)
continue
}
detectors = append(detectors, d)
}
return detectors
}
@@ -1,173 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package frameworks
import (
"math"
"net/http"
"os"
"path/filepath"
"testing"
)
func TestCustomDetectorSpecBuild(t *testing.T) {
valid := customDetectorSpec{
Name: "Ghost",
Signatures: []signatureSpec{{Pattern: `content="Ghost`, Weight: 1.0}},
}
cases := []struct {
name string
spec customDetectorSpec
wantErr bool
}{
{"valid", valid, false},
{"empty name", customDetectorSpec{Signatures: valid.Signatures}, true},
{"whitespace name", customDetectorSpec{Name: " ", Signatures: valid.Signatures}, true},
{"no signatures", customDetectorSpec{Name: "X"}, true},
{"empty pattern", customDetectorSpec{Name: "X", Signatures: []signatureSpec{{Pattern: "", Weight: 1}}}, true},
{"zero weight", customDetectorSpec{Name: "X", Signatures: []signatureSpec{{Pattern: "p", Weight: 0}}}, true},
{"negative weight", customDetectorSpec{Name: "X", Signatures: []signatureSpec{{Pattern: "p", Weight: -1}}}, true},
{"bad version regex", customDetectorSpec{Name: "X", Signatures: []signatureSpec{{Pattern: "p", Weight: 1}}, Version: &versionSpec{Regex: "("}}, true},
{"negative version group", customDetectorSpec{Name: "X", Signatures: valid.Signatures, Version: &versionSpec{Regex: `v([0-9]+)`, Group: -1}}, true},
{"nan weight", customDetectorSpec{Name: "X", Signatures: []signatureSpec{{Pattern: "p", Weight: float32(math.NaN())}}}, true},
{"inf weight", customDetectorSpec{Name: "X", Signatures: []signatureSpec{{Pattern: "p", Weight: float32(math.Inf(1))}}}, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if _, err := tc.spec.build(); (err != nil) != tc.wantErr {
t.Fatalf("build() err = %v, wantErr = %v", err, tc.wantErr)
}
})
}
}
func TestCustomDetectorDetect(t *testing.T) {
// the version regex is independent of the signature patterns so a body that
// matches it but no signature still must not surface a version.
spec := customDetectorSpec{
Name: "Acme",
Signatures: []signatureSpec{
{Pattern: "AcmeCMS", Weight: 0.6},
{Pattern: "X-Acme", Weight: 0.4, Header: true},
},
Version: &versionSpec{Regex: `ver=([0-9.]+)`, Group: 1},
}
d, err := spec.build()
if err != nil {
t.Fatal(err)
}
withHeader := func() http.Header {
h := http.Header{}
h.Set("X-Acme", "1")
return h
}
t.Run("all signatures match: confidence 1, version extracted", func(t *testing.T) {
conf, ver := d.Detect("powered by AcmeCMS ver=4.2.0", withHeader())
if conf != 1.0 {
t.Errorf("confidence = %v, want 1.0", conf)
}
if ver != "4.2.0" {
t.Errorf("version = %q, want 4.2.0", ver)
}
})
t.Run("only body signature matches: linear 0.6", func(t *testing.T) {
conf, ver := d.Detect("powered by AcmeCMS", http.Header{})
if conf != 0.6 {
t.Errorf("confidence = %v, want 0.6 (0.6/1.0 matched fraction)", conf)
}
if ver != "" {
t.Errorf("version = %q, want empty", ver)
}
})
t.Run("no signature matches: 0 confidence, no version even when present", func(t *testing.T) {
conf, ver := d.Detect("ver=9.9.9 but no marker here", http.Header{})
if conf != 0 {
t.Errorf("confidence = %v, want 0", conf)
}
if ver != "" {
t.Errorf("version = %q, want empty (not detected, so not extracted)", ver)
}
})
}
func TestParseCustomDetectorFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "fw.yaml")
content := `name: Parsed
signatures:
- pattern: "Marker"
weight: 0.5
- pattern: "X-Hdr"
weight: 0.5
header: true
version:
regex: 'Parsed/([0-9.]+)'
group: 1
`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
d, err := parseCustomDetector(path)
if err != nil {
t.Fatal(err)
}
if d.Name() != "Parsed" {
t.Errorf("name = %q, want Parsed", d.Name())
}
if len(d.Signatures()) != 2 {
t.Errorf("signatures = %d, want 2", len(d.Signatures()))
}
h := http.Header{}
h.Set("X-Hdr", "1")
conf, ver := d.Detect("Marker Parsed/3.1", h)
if conf != 1.0 {
t.Errorf("confidence = %v, want 1.0", conf)
}
if ver != "3.1" {
t.Errorf("version = %q, want 3.1", ver)
}
}
func TestCollectCustomDetectors(t *testing.T) {
dir := t.TempDir()
write := func(name, content string) {
if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
write("good.yaml", "name: ZZCustomTest\nsignatures:\n - pattern: \"ZZCustomMarker\"\n weight: 1.0\n")
write("bad.yaml", "name: \"\"\nsignatures: []\n") // invalid: skipped with a warning
write("ignore.txt", "not a signature file") // wrong extension: ignored
got := collectCustomDetectors(dir)
if len(got) != 1 {
t.Fatalf("collected %d detectors, want 1 (good.yaml only)", len(got))
}
if got[0].Name() != "ZZCustomTest" {
t.Errorf("detector name = %q, want ZZCustomTest", got[0].Name())
}
if conf, _ := got[0].Detect("page with ZZCustomMarker", http.Header{}); conf != 1.0 {
t.Errorf("confidence = %v, want 1.0", conf)
}
}
func TestCollectCustomDetectorsMissingDir(t *testing.T) {
if got := collectCustomDetectors(filepath.Join(t.TempDir(), "nope")); got != nil {
t.Errorf("missing dir should yield nil, got %v", got)
}
}
-6
View File
@@ -40,14 +40,8 @@ type detectionResult struct {
version string
}
// loadCustomOnce loads the user signature directory the first time a scan runs,
// so config-defined detectors join the registry without a per-target re-read.
var loadCustomOnce sync.Once
// DetectFramework runs all registered detectors against the target URL.
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
loadCustomOnce.Do(loadCustomDetectors)
log := output.Module("FRAMEWORK")
log.Start()
-76
View File
@@ -186,30 +186,6 @@ func TestDetectFramework_ASPNET(t *testing.T) {
}
}
// the dead "X-Powered-By: ASP.NET" signature only inflated the total weight
// (containsHeader never builds a "name: value" string to match it against), so a
// genuine asp.net response scored just under the threshold until it was removed.
func TestDetectFramework_ASPNETPoweredByHeader(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-AspNetMvc-Version", "5.2")
w.Header().Set("X-Powered-By", "ASP.NET")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<!DOCTYPE html><html><body><a href="/home/index.aspx">home</a></body></html>`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "ASP.NET" {
t.Errorf("expected framework 'ASP.NET', got '%s'", result.Name)
}
}
func TestDetectFramework_NoMatch(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@@ -728,55 +704,3 @@ func TestDetectorRegistry(t *testing.T) {
}
}
}
func TestExtractVersion_Htmx(t *testing.T) {
tests := []struct {
body string
expected string
}{
{`<script src="https://unpkg.com/htmx.org@1.9.10"></script>`, "1.9.10"},
{`https://cdn.jsdelivr.net/npm/htmx@2.0.3/dist/htmx.min.js`, "2.0.3"},
{`"htmx.org": "^1.9.12"`, "1.9.12"},
{"no version", "unknown"},
}
for _, tt := range tests {
result := frameworks.ExtractVersionOptimized(tt.body, "htmx").Version
if result != tt.expected {
t.Errorf("ExtractVersionOptimized(%q, 'htmx') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestDetectFramework_Htmx(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html>
<head><script src="https://unpkg.com/htmx.org@1.9.10"></script></head>
<body>
<button hx-get="/clicked" hx-target="#out" hx-swap="outerHTML">Click</button>
<form hx-post="/submit" hx-boost="true"></form>
<div id="out"></div>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "htmx" {
t.Errorf("expected framework 'htmx', got '%s'", result.Name)
}
if result.Version != "1.9.10" {
t.Errorf("expected version '1.9.10', got '%s'", result.Version)
}
}
@@ -177,6 +177,7 @@ func (d *aspnetDetector) Signatures() []fw.Signature {
{Pattern: ".ashx", Weight: 0.2},
{Pattern: ".asmx", Weight: 0.2},
{Pattern: "asp.net_sessionid", Weight: 0.4, HeaderOnly: true},
{Pattern: "X-Powered-By: ASP.NET", Weight: 0.4, HeaderOnly: true},
}
}
@@ -34,7 +34,6 @@ func init() {
fw.Register(&emberDetector{})
fw.Register(&backboneDetector{})
fw.Register(&meteorDetector{})
fw.Register(&htmxDetector{})
}
// reactDetector detects React framework.
@@ -196,34 +195,6 @@ func (d *backboneDetector) Detect(body string, headers http.Header) (float32, st
return confidence, version
}
// htmxDetector detects the htmx library.
type htmxDetector struct{}
func (d *htmxDetector) Name() string { return "htmx" }
func (d *htmxDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "hx-get", Weight: 0.5},
{Pattern: "hx-post", Weight: 0.5},
{Pattern: "hx-swap", Weight: 0.4},
{Pattern: "hx-target", Weight: 0.4},
{Pattern: "hx-boost", Weight: 0.4},
{Pattern: "htmx.org", Weight: 0.5},
}
}
func (d *htmxDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// meteorDetector detects Meteor framework.
type meteorDetector struct{}
-4
View File
@@ -107,10 +107,6 @@ func init() {
"SvelteKit": {
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"htmx": {
{`htmx(?:\.org)?@(\d+\.\d+(?:\.\d+)?)`, 0.85, "CDN reference"},
{`"htmx\.org":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"WordPress": {
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
+7 -12
View File
@@ -23,9 +23,9 @@
package frameworks
import (
"bufio"
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
@@ -37,10 +37,6 @@ import (
// nextPagesRegex matches JavaScript file references in Next.js build manifest.
var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`)
// maxManifestSize caps the build manifest read so a huge or hostile file
// cannot exhaust memory.
const maxManifestSize = 5 * 1024 * 1024
func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
baseUrl, err := urlutil.Parse(scriptUrl)
if err != nil {
@@ -62,14 +58,13 @@ func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, maxManifestSize))
if err != nil {
fmt.Println(err)
return nil, err
var sb strings.Builder
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
sb.WriteString(scanner.Text())
}
// the manifest ships minified on one line; strip line breaks so the regex
// treats a (rare) pretty-printed one the same as the minified form.
manifestText := strings.NewReplacer("\r", "", "\n", "").Replace(string(body))
manifestText := sb.String()
list := nextPagesRegex.FindAllStringSubmatch(manifestText, -1)
-50
View File
@@ -1,50 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package frameworks
import (
"bufio"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestGetPagesRouterScriptsReadsPastLongLine(t *testing.T) {
// a manifest token past bufio's 64k cap must not truncate the read and
// drop the script references that follow it.
huge := strings.Repeat("x", bufio.MaxScanTokenSize+1)
manifest := `["early.js"]` + "\n" + huge + "\n" + `["late.js"]`
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(manifest))
}))
defer srv.Close()
scripts, err := GetPagesRouterScripts(srv.URL + "/_buildManifest.js")
if err != nil {
t.Fatalf("GetPagesRouterScripts: %v", err)
}
found := func(needle string) bool {
for _, s := range scripts {
if strings.Contains(s, needle) {
return true
}
}
return false
}
if !found("early.js") || !found("late.js") {
t.Errorf("want both early.js and late.js, got %v", scripts)
}
}
+10 -5
View File
@@ -13,6 +13,7 @@
package js
import (
"bufio"
"context"
"io"
"net/http"
@@ -63,10 +64,6 @@ func (r *JavascriptScanResult) SupabaseFindings() []SupabaseFinding {
return out
}
// maxHTMLBodySize caps how much of a page we read for script extraction so a
// huge or hostile response cannot exhaust memory.
const maxHTMLBodySize = 5 * 1024 * 1024
func JavascriptScan(url string, timeout time.Duration, threads int, logdir string) (*JavascriptScanResult, error) {
log := output.Module("JS")
log.Start()
@@ -93,7 +90,15 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
}
defer resp.Body.Close()
doc, err := htmlquery.Parse(io.LimitReader(resp.Body, maxHTMLBodySize))
var sb strings.Builder
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
sb.WriteString(scanner.Text())
}
html := sb.String()
doc, err := htmlquery.Parse(strings.NewReader(html))
if err != nil {
return nil, err
}
+7 -29
View File
@@ -16,11 +16,9 @@ import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt"
"hash"
"io"
"net/http"
"regexp"
@@ -235,7 +233,7 @@ func analyzeJWT(source, raw string) (JWTToken, bool) {
// only bother cracking when the alg is actually hmac; an asymmetric token
// has no shared secret to guess.
if isHMACAlg(alg) {
if secret, ok := crackHMAC(raw, alg); ok {
if secret, ok := crackHMAC(raw); ok {
token.WeakKey = secret
token.Issues = append(token.Issues, JWTIssue{
Kind: "weak hmac secret",
@@ -311,15 +309,11 @@ func jwtClaimIssues(payload map[string]any) []JWTIssue {
return issues
}
// crackHMAC tries every bundled weak secret against the token's signature offline,
// using the hash that matches alg (HS256/HS384/HS512). a verifying secret means the
// token is forgeable; the wordlist catches lazy defaults, it is not a real cracker.
func crackHMAC(raw, alg string) (string, bool) {
newHash, ok := hmacHash(alg)
if !ok {
return "", false
}
// crackHMAC tries every bundled weak secret against the token's HS256 signature
// offline. a verifying secret means the token is forgeable by anyone who knows
// it. only HS256 is attempted; the wordlist exists to catch lazy defaults, not
// to be a real cracker.
func crackHMAC(raw string) (string, bool) {
parts := strings.Split(raw, ".")
if len(parts) != 3 {
return "", false
@@ -332,7 +326,7 @@ func crackHMAC(raw, alg string) (string, bool) {
for i := 0; i < len(jwtWeakSecrets); i++ {
secret := jwtWeakSecrets[i]
mac := hmac.New(newHash, []byte(secret))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signingInput))
if hmac.Equal(mac.Sum(nil), want) {
return secret, true
@@ -341,22 +335,6 @@ func crackHMAC(raw, alg string) (string, bool) {
return "", false
}
// hmacHash maps an HMAC jwt alg to its hash constructor; ok is false for any
// non-HMAC or unknown alg. it is stricter than isHMACAlg: the confusion-surface
// finding fires on any HS* alg, but cracking needs a computable digest width.
func hmacHash(alg string) (func() hash.Hash, bool) {
switch strings.ToUpper(alg) {
case "HS256":
return sha256.New, true
case "HS384":
return sha512.New384, true
case "HS512":
return sha512.New, true
default:
return nil, false
}
}
// decodeJWTSegment base64url-decodes one jwt segment into a claims map. jwt uses
// unpadded base64url, but some emitters pad anyway, so try raw first then padded.
func decodeJWTSegment(seg string) (map[string]any, error) {
-136
View File
@@ -39,25 +39,6 @@ const (
jwtSensitive = "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9." +
"eyJzdWIiOiAieCIsICJwYXNzd29yZCI6ICJodW50ZXIyIiwgImV4cCI6IDk5OTk5OTk5OTl9." +
"rjEf0CUa7_qppuINi6zL9vupJIX0rzSBhul7kKM9uSA"
// HS384, signed with the bundled weak secret "secret".
jwtWeakHS384 = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3RlciJ9." +
"OXNTuzKiGLxnpUjL24vvKlQzdOD-YKMinN8eu_v5luTXDUF65bHAQnz-M3VG2TVh"
// HS512, signed with the bundled weak secret "secret".
jwtWeakHS512 = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3RlciJ9." +
"CXcZz0F9TTPg--B4WV1Vzty3gG_wcDG86H5QDSRe94MpcVXIcRTBK6H7OmqFyG4nNWYNXPOODCu426bgQMOzRQ"
// HS384/HS512 signed with a strong secret absent from the wordlist; these
// must never be cracked (no false positive on the wide-digest path).
jwtStrongHS384 = "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3RlciJ9." +
"vHhjZPoXZnnZEvVYxX64J2wm8qWk-e6y_T20qTy_Su6sPmoUSMHS4tv6_D-hfwrY"
jwtStrongHS512 = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InRlc3RlciJ9." +
"80ueFa0oI88ftySkn_MJ12GAd1r2cahXt_ICtCfWx58wJoAvEocbjBPC_efzOp8vm_39GlcCCDLeb6cFix3DBw"
)
// hasIssue reports whether the analyzed token carries an issue of the given kind.
@@ -127,123 +108,6 @@ func TestJWT_WeakSecretCracked(t *testing.T) {
}
}
func TestJWT_WeakSecretCrackedHS384HS512(t *testing.T) {
cases := []struct {
name string
token string
}{
{"HS384", jwtWeakHS384},
{"HS512", jwtWeakHS512},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "session", Value: tc.token})
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
result, err := JWT(srv.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("JWT: %v", err)
}
if result == nil || len(result.Tokens) != 1 {
t.Fatalf("expected one token, got %+v", result)
}
token := &result.Tokens[0]
if token.WeakKey != "secret" {
t.Errorf("expected weak secret 'secret' cracked on %s, got %q", tc.name, token.WeakKey)
}
if !hasIssue(token, "weak hmac secret") {
t.Errorf("expected weak hmac secret issue on %s, got %+v", tc.name, token.Issues)
}
})
}
}
func TestJWT_StrongSecretNotCrackedHS384HS512(t *testing.T) {
cases := []struct {
name string
token string
}{
{"HS384", jwtStrongHS384},
{"HS512", jwtStrongHS512},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.SetCookie(w, &http.Cookie{Name: "session", Value: tc.token})
w.WriteHeader(http.StatusOK)
}))
defer srv.Close()
result, err := JWT(srv.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("JWT: %v", err)
}
if result == nil || len(result.Tokens) != 1 {
t.Fatalf("expected one token, got %+v", result)
}
token := &result.Tokens[0]
if token.WeakKey != "" {
t.Errorf("strong-secret %s token must not be cracked, got %q", tc.name, token.WeakKey)
}
if hasIssue(token, "weak hmac secret") {
t.Errorf("strong-secret %s token must not raise a weak-secret issue, got %+v", tc.name, token.Issues)
}
})
}
}
func TestHMACHash(t *testing.T) {
cases := []struct {
alg string
wantOK bool
wantSize int // digest bytes when ok
}{
{"HS256", true, 32},
{"HS384", true, 48},
{"HS512", true, 64},
{"hs256", true, 32}, // alg match is case-insensitive
{"", false, 0},
{"none", false, 0},
{"RS256", false, 0},
{"ES256", false, 0},
{"HS1", false, 0},
{"HS", false, 0},
}
for _, tc := range cases {
newHash, ok := hmacHash(tc.alg)
if ok != tc.wantOK {
t.Errorf("hmacHash(%q) ok = %v, want %v", tc.alg, ok, tc.wantOK)
continue
}
if ok && newHash().Size() != tc.wantSize {
t.Errorf("hmacHash(%q) digest size = %d, want %d", tc.alg, newHash().Size(), tc.wantSize)
}
}
}
func TestCrackHMAC_RejectsMalformedAndNonHMAC(t *testing.T) {
cases := []struct {
name string
raw string
alg string
}{
{"non-hmac alg", jwtWeakHS384, "RS256"},
{"unknown hs alg", jwtWeakHS384, "HS1"},
{"too few parts", "only.two", "HS256"},
{"non-base64 signature", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ4In0.!!!notb64!!!", "HS256"},
}
for _, tc := range cases {
if secret, ok := crackHMAC(tc.raw, tc.alg); ok || secret != "" {
t.Errorf("%s: crackHMAC = (%q, %v), want (\"\", false)", tc.name, secret, ok)
}
}
}
func TestJWT_ExpiredFlagged(t *testing.T) {
// token in the response body.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+9 -28
View File
@@ -92,10 +92,10 @@ type openapiInfo struct {
Version string `json:"version" yaml:"version"`
}
// rawOps captures the per-operation security block. a pointer so an absent block
// (inherit global) is distinct from an explicit empty one (security: [] = public).
// rawOps captures just the per-operation security block so we can tell whether
// an operation requires auth. the rest of the operation object is irrelevant.
type rawOps struct {
Security *[]map[string][]string `json:"security" yaml:"security"`
Security []map[string][]string `json:"security" yaml:"security"`
}
// OpenAPI probes the candidate spec paths concurrently and, on the first hit,
@@ -252,26 +252,11 @@ func parseOpenAPISpec(body []byte) (*openapiSpec, bool) {
return &spec, true
}
// securityAllowsAnonymous reports whether a security requirement list lets a
// caller through without credentials: an empty list (security: []) or an empty
// requirement object ({}) inside it both permit anonymous access.
func securityAllowsAnonymous(reqs []map[string][]string) bool {
if len(reqs) == 0 {
return true
}
for _, req := range reqs {
if len(req) == 0 {
return true
}
}
return false
}
// specToResult flattens the parsed spec into enumerated endpoints and ranks the
// exposure. an operation is flagged unauthenticated when its effective security
// permits anonymous access, which bumps the overall severity to high.
// exposure. an operation with no security requirement (and no top-level default)
// is flagged unauthenticated, which bumps the overall severity to high.
func specToResult(spec *openapiSpec) *OpenAPIResult {
globalAllowsAnon := securityAllowsAnonymous(spec.Security)
hasGlobalSecurity := len(spec.Security) > 0
endpoints := make([]OpenAPIEndpoint, 0, len(spec.Paths))
anyUnauth := false
@@ -292,13 +277,9 @@ func specToResult(spec *openapiSpec) *OpenAPIResult {
if !ok {
continue
}
// an explicit block decides on its own; an absent one inherits global.
var unauth bool
if op.Security != nil {
unauth = securityAllowsAnonymous(*op.Security)
} else {
unauth = globalAllowsAnon
}
// an operation is unauth when neither it nor the global default
// declares a security requirement.
unauth := len(op.Security) == 0 && !hasGlobalSecurity
if unauth {
anyUnauth = true
}
-104
View File
@@ -56,35 +56,6 @@ paths:
summary: health
`
// a globally-secured spec mixing public opt-outs (security: [] and security: [{}])
// with operations that inherit or declare their own requirement.
const openapiJSONPublicOverride = `{
"openapi": "3.0.1",
"info": {"title": "Override API", "version": "1.0"},
"security": [{"bearerAuth": []}],
"paths": {
"/me": {"get": {"summary": "authed, inherits global"}},
"/admin": {"get": {"summary": "authed, explicit non-empty", "security": [{"bearerAuth": []}]}},
"/login": {"post": {"summary": "public override", "security": []}},
"/optional": {"get": {"summary": "anonymous allowed", "security": [{}]}}
}
}`
// a yaml spec with global auth and an operation that opts out via security: [],
// to lock the empty-vs-absent distinction on the yaml decode path too.
const openapiYAMLPublicOverride = `openapi: "3.0.1"
info:
title: YAML Override API
version: "1.0"
security:
- bearerAuth: []
paths:
/token:
post:
summary: public
security: []
`
// hasEndpoint reports whether the result enumerated the given path+method.
func hasEndpoint(r *OpenAPIResult, path, method string) (OpenAPIEndpoint, bool) {
for i := 0; i < len(r.Endpoints); i++ {
@@ -170,81 +141,6 @@ func TestOpenAPI_SecuredSpecIsMedium(t *testing.T) {
}
}
// TestOpenAPI_PublicOverridesAreUnauth checks that operations allowing anonymous
// access (security: [] or security: [{}]) are flagged unauthenticated, while ones
// that inherit the enforced global default or declare their own requirement stay authed.
func TestOpenAPI_PublicOverridesAreUnauth(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/openapi.json" {
_, _ = w.Write([]byte(openapiJSONPublicOverride))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
result, err := OpenAPI(srv.URL, 5*time.Second, 4, "")
if err != nil {
t.Fatalf("OpenAPI: %v", err)
}
if result == nil {
t.Fatal("expected a result, got nil")
}
for _, want := range []struct {
path, method string
unauth bool
why string
}{
{"/login", http.MethodPost, true, "security: [] removes the global requirement"},
{"/optional", http.MethodGet, true, "security: [{}] permits anonymous access"},
{"/me", http.MethodGet, false, "inherits the enforced global requirement"},
{"/admin", http.MethodGet, false, "declares its own non-empty requirement"},
} {
ep, ok := hasEndpoint(result, want.path, want.method)
if !ok {
t.Fatalf("expected %s %s to be enumerated", want.method, want.path)
}
if ep.Unauth != want.unauth {
t.Errorf("%s %s unauth=%v, want %v (%s)", want.method, want.path, ep.Unauth, want.unauth, want.why)
}
}
if result.Severity != openapiSevHigh {
t.Errorf("an unauthenticated operation should rank the exposure high, got %q", result.Severity)
}
}
// TestOpenAPI_YAMLPublicOverrideIsUnauth locks the empty-vs-absent distinction on
// the yaml decode path: yaml.v3 must preserve security: [] as a non-nil empty
// block, or the whole fix silently regresses on yaml specs.
func TestOpenAPI_YAMLPublicOverrideIsUnauth(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v3/api-docs" {
w.Header().Set("Content-Type", "application/yaml")
_, _ = w.Write([]byte(openapiYAMLPublicOverride))
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
result, err := OpenAPI(srv.URL, 5*time.Second, 4, "")
if err != nil {
t.Fatalf("OpenAPI: %v", err)
}
if result == nil {
t.Fatal("expected a yaml result, got nil")
}
ep, ok := hasEndpoint(result, "/token", http.MethodPost)
if !ok {
t.Fatal("expected /token POST to be enumerated")
}
if !ep.Unauth {
t.Error("yaml security: [] should be flagged unauthenticated; yaml.v3 must keep it non-nil")
}
}
func TestOpenAPI_YAMLSpec(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v3/api-docs" {
+2 -2
View File
@@ -178,8 +178,8 @@ func checkSubdomainTakeover(subdomain string, client *http.Client) (bool, string
"WordPress": "Do you want to register *.wordpress.com?",
"Amazon S3": "The specified bucket does not exist",
"Bitbucket": "Repository not found",
"Ghost": "Failed to resolve DNS path for this host",
"Pantheon": "404 - Unknown site",
"Ghost": "The thing you were looking for is no longer here, or never was",
"Pantheon": "The gods are wise, but do not know of the site which you seek.",
"Fastly": "Fastly error: unknown domain",
"Zendesk": "Help Center Closed",
"Teamwork": "Oops - We didn't find your site.",
@@ -1,66 +0,0 @@
/*
··
: :
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package scan
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
// the ghost and pantheon unclaimed-domain pages, verified live against
// <random>.ghost.io and <random>.pantheonsite.io (both 404). the old fingerprints
// keyed on each platform's earlier takeover copy, so a real takeover went undetected.
func TestCheckSubdomainTakeover_CurrentUnclaimedPageDetected(t *testing.T) {
client := &http.Client{Timeout: 5 * time.Second}
cases := []struct{ service, body string }{
{"Ghost", "<html><body>Failed to resolve DNS path for this host</body></html>"},
{"Pantheon", "<html><head><title>404 - Unknown site</title></head><body>404 Unknown site</body></html>"},
}
for _, c := range cases {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(c.body))
}))
host := strings.TrimPrefix(srv.URL, "http://")
vulnerable, service := checkSubdomainTakeover(host, client)
srv.Close()
if !vulnerable || service != c.service {
t.Errorf("%s unclaimed page not detected, got vulnerable=%v service=%q", c.service, vulnerable, service)
}
}
}
// the retired fingerprints were each platform's older takeover copy, gone from
// the live pages; a page still carrying them must no longer raise a takeover.
func TestCheckSubdomainTakeover_StaleFingerprintRetired(t *testing.T) {
client := &http.Client{Timeout: 5 * time.Second}
stale := []string{
"The thing you were looking for is no longer here, or never was",
"The gods are wise, but do not know of the site which you seek.",
}
for _, body := range stale {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte("<html><body>" + body + "</body></html>"))
}))
host := strings.TrimPrefix(srv.URL, "http://")
vulnerable, service := checkSubdomainTakeover(host, client)
srv.Close()
if vulnerable || service != "" {
t.Errorf("stale fingerprint still raised a takeover (service %q) for body %q", service, body)
}
}
}
+5 -39
View File
@@ -47,10 +47,6 @@ type XSSFinding struct {
// xssMaxBody caps the body we scan for the canary (100KB).
const xssMaxBody = 1024 * 100
// xssMaxRedirects caps the redirect chain we follow before reading the body; a
// reflection can land a hop past the first response.
const xssMaxRedirects = 3
// canaryToken is a unique, alnum-only marker we can grep for unambiguously; it
// survives every output encoder so a missing reflection means no echo at all.
const canaryToken = "sifxss9173canary" //nolint:gosec // not a credential, just a reflection marker
@@ -101,7 +97,7 @@ func XSS(targetURL string, timeout time.Duration, threads int, logdir string) (*
client := httpx.Client(timeout)
client.CheckRedirect = func(_ *http.Request, via []*http.Request) error {
if len(via) >= xssMaxRedirects {
if len(via) >= corsMaxRedirects {
return http.ErrUseLastResponse
}
return nil
@@ -238,9 +234,8 @@ func probeXSS(client *http.Client, parsedURL *url.URL, existing url.Values, para
// classifyXSSContext guesses where the canary was reflected. We look at the
// markup immediately around the token: a live <canary> tag means html text, a
// reflection inside a <script> block means js, a reflection sitting inside a tag
// is an attribute value, and anything else is inert element text. The html-tag
// check wins because it's the most directly exploitable.
// reflection inside a <script> block means js, otherwise it sits in an attribute
// value. The html-tag check wins because it's the most directly exploitable.
func classifyXSSContext(body string) string {
// a surviving "<canary>" means the < and > both passed through into markup
if strings.Contains(body, "<"+canaryToken+">") {
@@ -264,34 +259,8 @@ func classifyXSSContext(body string) string {
body = body[open+closeIdx+len("</script>"):]
}
// only an attribute value when the canary actually lands inside a tag; a quote
// can only break out of a delimiter that exists. assuming attribute by default
// flags inert quotes in element text (angle brackets escaped) as a high finding.
if reflectedInsideTag(body) {
return "attribute"
}
// reflected in element text: with the angle brackets escaped there's no markup
// to break into, so surviving quotes are harmless.
return "text"
}
// reflectedInsideTag reports whether any canary occurrence sits inside an open html
// tag, the only place a surviving quote can close an attribute value and break out.
// true when the nearest preceding '<' is not yet closed by a '>'. it's a cheap byte-scan,
// not a parser, so a stray '<' or a quoted '>' can mis-bucket rare malformed markup.
func reflectedInsideTag(body string) bool {
for off := 0; ; {
i := strings.Index(body[off:], canaryToken)
if i < 0 {
return false
}
pos := off + i
if strings.LastIndex(body[:pos], "<") > strings.LastIndex(body[:pos], ">") {
return true
}
off = pos + len(canaryToken)
}
// default: echoed inside an html attribute value
return "attribute"
}
// survivingBreakChars reports which dangerous chars came back next to the canary
@@ -340,9 +309,6 @@ func survivingBreakChars(body string) []string {
// backticks matter inside attributes/scripts.
func relevantForContext(reflectCtx string, survived []string) []string {
wanted := make(map[string]bool, len(survived))
// a context with no exploitable delimiter (e.g. "text") is left unlisted and
// falls through to an empty set, which drops the finding; a default case here
// would resurrect the inert-reflection false positive.
switch reflectCtx {
case "html":
wanted["<"] = true
-75
View File
@@ -16,7 +16,6 @@ import (
"html"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
@@ -33,36 +32,6 @@ func reflectsRaw(param string) *httptest.Server {
}))
}
// reflectsQuotesInText echoes the param into element text but escapes only the
// angle brackets, the way an encoder limited to < > & does. quotes survive raw,
// yet in text context they delimit nothing, so this is not an injection sink.
func reflectsQuotesInText(param string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
v := r.URL.Query().Get(param)
v = strings.ReplaceAll(v, "<", "&lt;")
v = strings.ReplaceAll(v, ">", "&gt;")
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
//nolint:gosec // fixture: quotes raw in element text is not exploitable
w.Write([]byte("<html><body><p>no results for " + v + "</p></body></html>"))
}))
}
// reflectsInAttribute echoes the param into a tag attribute value with the angle
// brackets escaped but quotes raw. a surviving quote closes the value and breaks
// out, so this is a genuine attribute-context sink the fix must still report.
func reflectsInAttribute(param string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
v := r.URL.Query().Get(param)
v = strings.ReplaceAll(v, "<", "&lt;")
v = strings.ReplaceAll(v, ">", "&gt;")
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
//nolint:gosec // deliberate attribute-context xss fixture for the probe under test
w.Write([]byte(`<html><body><input value="` + v + `"></body></html>`))
}))
}
func TestXSS_DetectsRawHTMLReflection(t *testing.T) {
srv := reflectsRaw("q")
defer srv.Close()
@@ -129,45 +98,6 @@ func TestXSS_NoFalsePositiveWhenNotReflected(t *testing.T) {
}
}
func TestXSS_NoFalsePositiveOnQuotesInText(t *testing.T) {
srv := reflectsQuotesInText("q")
defer srv.Close()
result, err := XSS(srv.URL, 5*time.Second, 4, "")
if err != nil {
t.Fatalf("XSS: %v", err)
}
if result != nil && len(result.Findings) > 0 {
t.Errorf("quotes reflected in element text are inert; expected no findings, got %+v", result.Findings)
}
}
func TestXSS_DetectsAttributeReflection(t *testing.T) {
srv := reflectsInAttribute("q")
defer srv.Close()
result, err := XSS(srv.URL, 5*time.Second, 4, "")
if err != nil {
t.Fatalf("XSS: %v", err)
}
if result == nil || len(result.Findings) == 0 {
t.Fatalf("expected an attribute-context finding, got %+v", result)
}
var found *XSSFinding
for i := range result.Findings {
if result.Findings[i].Parameter == "q" {
found = &result.Findings[i]
}
}
if found == nil {
t.Fatalf("expected a finding on param 'q', got %+v", result.Findings)
}
if found.Context != "attribute" {
t.Errorf("expected attribute context, got %s", found.Context)
}
}
func TestClassifyXSSContext(t *testing.T) {
tests := []struct {
name string
@@ -189,11 +119,6 @@ func TestClassifyXSSContext(t *testing.T) {
body: `<input value="` + canaryToken + `">`,
want: "attribute",
},
{
name: "escaped brackets in element text",
body: `<p>no results for &lt;` + canaryToken + `&gt;"` + canaryToken + `'</p>`,
want: "text",
},
}
for _, tt := range tests {
-44
View File
@@ -1,44 +0,0 @@
# GraphQL Introspection Detection Module
id: graphql-introspection
info:
name: GraphQL Introspection Enabled
author: sif
severity: low
description: Detects GraphQL endpoints with introspection enabled
tags: [graphql, introspection, exposure, info]
type: http
http:
method: POST
paths:
- "{{BaseURL}}"
- "{{BaseURL}}/graphql"
- "{{BaseURL}}/api/graphql"
- "{{BaseURL}}/graphql/v1"
- "{{BaseURL}}/v1/graphql"
- "{{BaseURL}}/query"
- "{{BaseURL}}/gql"
headers:
Content-Type: application/json
Accept: application/json
body: '{"query":"{__schema{queryType{name}}}"}'
matchers:
- type: regex
part: body
regex:
- '"__schema"\s*:\s*\{'
- '"queryType"\s*:\s*\{'
condition: and
extractors:
- type: regex
name: query_type
part: body
regex:
- '"queryType"\s*:\s*\{\s*"name"\s*:\s*"([^"]+)"'
group: 1
-47
View File
@@ -1,47 +0,0 @@
# Adminer Database Panel Detection Module
id: adminer-panel
info:
name: Adminer Database Panel
author: sif
severity: info
description: Detects exposed Adminer database management login panels
tags: [adminer, database, panel, login, detection, info]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/adminer.php"
- "{{BaseURL}}/adminer/"
- "{{BaseURL}}/adminer-4.8.1.php"
- "{{BaseURL}}/_adminer.php"
- "{{BaseURL}}/db/adminer.php"
- "{{BaseURL}}/adminer/adminer.php"
threads: 5
matchers:
- type: status
status:
- 200
- type: word
part: all
condition: or
words:
- 'name="auth[driver]"'
- 'name="auth[server]"'
- 'name="auth[username]"'
- 'name="auth[password]"'
- "www.adminer.org"
extractors:
- type: regex
name: adminer_version
part: body
regex:
- 'class="version">v?([0-9]+\.[0-9]+(?:\.[0-9]+)?)'
- 'Adminer[^0-9<]{0,40}([0-9]+\.[0-9]+(?:\.[0-9]+)?)'
group: 1
-38
View File
@@ -1,38 +0,0 @@
# Keycloak Panel Detection Module
id: keycloak-panel
info:
name: Keycloak Panel
author: sif
severity: info
description: Detects an exposed Keycloak identity server via its public realm endpoint
tags: [keycloak, iam, sso, panel, login, detection, info]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/realms/master"
- "{{BaseURL}}/auth/realms/master"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: and
words:
- '"public_key"'
- '"token-service"'
- '"account-service"'
extractors:
- type: regex
name: keycloak_realm
part: body
regex:
- '"realm"\s*:\s*"([^"]+)"'
group: 1
-48
View File
@@ -1,48 +0,0 @@
# phpMyAdmin Database Panel Detection Module
id: phpmyadmin-panel
info:
name: phpMyAdmin Database Panel
author: sif
severity: info
description: Detects exposed phpMyAdmin database management login panels
tags: [phpmyadmin, database, mysql, panel, login, detection, info]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/phpmyadmin/"
- "{{BaseURL}}/phpMyAdmin/"
- "{{BaseURL}}/pma/"
- "{{BaseURL}}/PMA/"
- "{{BaseURL}}/mysql/"
- "{{BaseURL}}/dbadmin/"
- "{{BaseURL}}/phpmyadmin/index.php"
threads: 5
matchers:
- type: status
status:
- 200
- type: word
part: all
condition: or
words:
- 'name="pma_username"'
- 'name="pma_password"'
- "pmahomme"
- 'content="phpMyAdmin"'
- "phpMyAdmin="
extractors:
- type: regex
name: phpmyadmin_version
part: all
regex:
- 'PMA_VERSION["'']?\s*[:=]\s*["'']([0-9]+\.[0-9]+(?:\.[0-9]+)?)'
- 'phpMyAdmin[^0-9<]{0,30}([0-9]+\.[0-9]+(?:\.[0-9]+)?)'
group: 1
-38
View File
@@ -1,38 +0,0 @@
# Portainer Panel Detection Module
id: portainer-panel
info:
name: Portainer Panel
author: sif
severity: info
description: Detects exposed Portainer container management instances via the public status API
tags: [portainer, docker, container, panel, detection, info]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/api/status"
- "{{BaseURL}}/portainer/api/status"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: and
words:
- '"Edition"'
- '"Version"'
- '"InstanceID"'
extractors:
- type: regex
name: portainer_version
part: body
regex:
- '"Version":\s*"([0-9]+\.[0-9]+(?:\.[0-9]+)?)"'
group: 1
-30
View File
@@ -1,30 +0,0 @@
# RabbitMQ Management Detection Module
id: rabbitmq-panel
info:
name: RabbitMQ Management
author: sif
severity: info
description: Detects an exposed RabbitMQ management UI login panel
tags: [rabbitmq, amqp, messaging, panel, login, detection, info]
type: http
http:
method: GET
paths:
- "{{BaseURL}}"
- "{{BaseURL}}/rabbitmq/"
matchers:
- type: status
status:
- 200
- type: word
part: all
condition: or
words:
- "RabbitMQ Management"
- "rabbitmqlogo"
- "<title>RabbitMQ"
-38
View File
@@ -1,38 +0,0 @@
# Traefik Dashboard Detection Module
id: traefik-panel
info:
name: Traefik Dashboard
author: sif
severity: info
description: Detects an exposed Traefik API and dashboard via the public version endpoint
tags: [traefik, proxy, dashboard, panel, detection, info]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/api/version"
- "{{BaseURL}}/traefik/api/version"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: and
words:
- '"Version"'
- '"Codename"'
- '"startDate"'
extractors:
- type: regex
name: traefik_version
part: body
regex:
- '"Version":\s*"v?([0-9]+\.[0-9]+(?:\.[0-9]+)?)"'
group: 1
@@ -1,53 +0,0 @@
# AWS Credentials File Exposure Detection Module
id: aws-credentials-exposure
info:
name: AWS Credentials File Exposure
author: sif
severity: high
description: Detects exposed AWS credential files that leak access key ids and secret keys
tags: [aws, credentials, secrets, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.aws/credentials"
- "{{BaseURL}}/.s3cfg"
- "{{BaseURL}}/.boto"
threads: 3
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: or
words:
- "aws_secret_access_key"
- "aws_access_key_id"
- "secret_key"
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<!doctype"
- "<html"
- "<HTML"
- "<head>"
- "<title>"
extractors:
- type: regex
name: aws_access_key_id
part: body
regex:
- '((?:AKIA|ASIA)[0-9A-Z]{16})'
group: 1
-47
View File
@@ -1,47 +0,0 @@
# Exposed Bazaar Repository Detection Module
id: bazaar-exposure
info:
name: Exposed Bazaar Repository
author: sif
severity: high
description: Detects an exposed .bzr repository through its branch-format file that may leak source code
tags: [bazaar, bzr, exposure, source-code, misconfiguration]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.bzr/branch-format"
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "Bazaar"
- type: word
part: body
words:
- "meta directory"
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: bzr_format
part: body
regex:
- '(Bazaar[^\r\n]*)'
group: 1
-34
View File
@@ -1,34 +0,0 @@
# CouchDB Exposure Detection Module
id: couchdb-exposure
info:
name: CouchDB Exposure
author: sif
severity: high
description: Detects an exposed unauthenticated CouchDB database list
tags: [couchdb, datastore, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/_all_dbs"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '^\s*\['
- type: regex
part: body
regex:
- '"_users"'
- '"_replicator"'
- '"_global_changes"'
condition: or
-39
View File
@@ -1,39 +0,0 @@
# Django Debug Page Exposure Detection Module
id: django-debug-exposure
info:
name: Django Debug Page Exposure
author: sif
severity: high
description: Detects an exposed Django DEBUG=True page leaking internals
tags: [django, debug, exposure, misconfiguration, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/sif-probe-nonexistent"
matchers:
- type: status
status:
- 400
- 403
- 404
- 500
- type: regex
part: body
regex:
- 'seeing this error because you have <code>DEBUG = True</code>'
- '(Request Method:|Django Version:|Using the URLconf defined in)'
condition: and
extractors:
- type: regex
name: django_version
part: body
regex:
- 'Django Version:[^0-9]{0,30}([0-9]+(?:\.[0-9]+)+)'
group: 1
-47
View File
@@ -1,47 +0,0 @@
# Docker Config Credential Exposure Detection Module
id: docker-config-exposure
info:
name: Docker Config Credential Exposure
author: sif
severity: high
description: Detects exposed docker config files that leak base64 encoded registry credentials
tags: [docker, registry, credentials, secrets, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.docker/config.json"
- "{{BaseURL}}/.dockercfg"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '"auth"\s*:\s*"[A-Za-z0-9+/=]{20,}"'
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<!doctype"
- "<html"
- "<HTML"
- "<head>"
- "<title>"
extractors:
- type: regex
name: docker_registry
part: body
regex:
- '"auths"\s*:\s*\{\s*"([^"]+)"'
group: 1
@@ -1,34 +0,0 @@
# Docker Registry API Exposure Detection Module
id: docker-registry-api-exposure
info:
name: Docker Registry API Exposure
author: sif
severity: high
description: Detects a Docker registry reachable anonymously through its v2 api base
tags: [docker, registry, container, api, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/v2/"
matchers:
- type: status
status:
- 200
- type: regex
part: header
regex:
- 'Docker-Distribution-Api-Version:\s*registry/2\.0'
extractors:
- type: regex
name: docker_registry_api_version
part: header
regex:
- 'Docker-Distribution-Api-Version:\s*(\S+)'
group: 1
-52
View File
@@ -1,52 +0,0 @@
# Drupal Settings Backup Exposure Detection Module
id: drupal-config-exposure
info:
name: Drupal Settings Backup Exposure
author: sif
severity: high
description: Detects an exposed Drupal settings.php backup that leaks the database password
tags: [drupal, cms, config, credentials, recon, exposure]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/sites/default/settings.php.bak"
- "{{BaseURL}}/sites/default/settings.php~"
- "{{BaseURL}}/sites/default/settings.php.old"
- "{{BaseURL}}/sites/default/settings.php.save"
- "{{BaseURL}}/sites/default/settings.php.orig"
- "{{BaseURL}}/sites/default/settings.php.txt"
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "$databases"
- type: regex
part: body
regex:
- "'password'\\s*=>\\s*'[^']+'"
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: drupal_password
part: body
regex:
- "'password'\\s*=>\\s*'([^']+)'"
group: 1
-36
View File
@@ -1,36 +0,0 @@
# Elasticsearch Exposure Detection Module
id: elasticsearch-exposure
info:
name: Elasticsearch Exposure
author: sif
severity: high
description: Detects an exposed unauthenticated Elasticsearch HTTP API
tags: [elasticsearch, datastore, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '"tagline"\s*:\s*"You Know, for Search"'
- '"lucene_version"\s*:\s*"[0-9]'
condition: and
extractors:
- type: regex
name: elasticsearch_version
part: body
regex:
- '"number"\s*:\s*"([0-9]+(?:\.[0-9]+)+)'
group: 1
-39
View File
@@ -1,39 +0,0 @@
# Harbor API Exposure Detection Module
id: harbor-api-exposure
info:
name: Harbor API Exposure
author: sif
severity: medium
description: Detects an exposed Harbor registry through its unauthenticated systeminfo endpoint
tags: [harbor, registry, container, api, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/api/v2.0/systeminfo"
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "\"harbor_version\""
- type: word
part: body
words:
- "\"auth_mode\""
extractors:
- type: regex
name: harbor_version
part: body
regex:
- '"harbor_version"\s*:\s*"([^"]+)"'
group: 1
-56
View File
@@ -1,56 +0,0 @@
# Joomla Configuration Backup Exposure Detection Module
id: joomla-config-exposure
info:
name: Joomla Configuration Backup Exposure
author: sif
severity: high
description: Detects an exposed Joomla configuration.php backup that leaks the database password
tags: [joomla, cms, config, credentials, recon, exposure]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/configuration.php.bak"
- "{{BaseURL}}/configuration.php~"
- "{{BaseURL}}/configuration.php.old"
- "{{BaseURL}}/configuration.php.save"
- "{{BaseURL}}/configuration.php.orig"
- "{{BaseURL}}/configuration.php.txt"
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "public $password"
- type: word
part: body
condition: or
words:
- "JConfig"
- "public $dbprefix"
- "public $secret"
- "public $db"
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: joomla_password
part: body
regex:
- "password\\s*=\\s*'([^']+)'"
group: 1
@@ -1,34 +0,0 @@
# Laravel Ignition Debug Exposure Detection Module
id: laravel-ignition-exposure
info:
name: Laravel Ignition Debug Exposure
author: sif
severity: high
description: Detects an exposed Laravel Ignition debug endpoint that may allow remote code execution
tags: [laravel, ignition, debug, rce, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/_ignition/health-check"
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "can_execute_commands"
extractors:
- type: regex
name: can_execute_commands
part: body
regex:
- '"can_execute_commands"\s*:\s*(true|false)'
group: 1
@@ -1,54 +0,0 @@
# Magento env.php Backup Exposure Detection Module
id: magento-config-exposure
info:
name: Magento env.php Backup Exposure
author: sif
severity: high
description: Detects an exposed Magento app/etc/env.php backup that leaks the crypt key and database password
tags: [magento, cms, config, credentials, recon, exposure]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/app/etc/env.php.bak"
- "{{BaseURL}}/app/etc/env.php~"
- "{{BaseURL}}/app/etc/env.php.old"
- "{{BaseURL}}/app/etc/env.php.save"
- "{{BaseURL}}/app/etc/env.php.orig"
- "{{BaseURL}}/app/etc/env.php.txt"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: or
words:
- "'crypt'"
- "MAGE_MODE"
- type: regex
part: body
regex:
- "'(?:password|key)'\\s*=>\\s*'[^'#][^']*'"
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: magento_crypt_key
part: body
regex:
- "'key'\\s*=>\\s*'([^'#][^']*)'"
group: 1
-47
View File
@@ -1,47 +0,0 @@
# Exposed Mercurial Repository Detection Module
id: mercurial-exposure
info:
name: Exposed Mercurial Repository
author: sif
severity: high
description: Detects an exposed .hg repository through its requires file that may leak source code
tags: [mercurial, hg, exposure, source-code, misconfiguration]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.hg/requires"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: or
words:
- "revlogv1"
- "dotencode"
- "fncache"
- "generaldelta"
- "sparserevlog"
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: hg_requirement
part: body
regex:
- '(revlogv1|dotencode|fncache|generaldelta|sparserevlog|store|treemanifest)'
group: 1
@@ -1,53 +0,0 @@
# MySQL Client Config Exposure Detection Module
id: mysql-client-config-exposure
info:
name: MySQL Client Config Exposure
author: sif
severity: high
description: Detects an exposed .my.cnf file that leaks the mysql client password in cleartext
tags: [mysql, my-cnf, credentials, secrets, recon, exposure]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.my.cnf"
- "{{BaseURL}}/my.cnf"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: or
words:
- "[client]"
- "[mysql]"
- "[mysqldump]"
- type: word
part: body
condition: or
words:
- "password="
- "password ="
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: mysql_user
part: body
regex:
- 'user\s*=\s*(\S+)'
group: 1
-43
View File
@@ -1,43 +0,0 @@
# netrc Credential File Exposure Detection Module
id: netrc-exposure
info:
name: netrc Credential File Exposure
author: sif
severity: high
description: Detects an exposed .netrc file that leaks machine login credentials in cleartext
tags: [netrc, credentials, secrets, recon, exposure]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.netrc"
- "{{BaseURL}}/netrc"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '(?:machine\s+\S+|default)\s+login\s+\S+\s+password\s+\S+'
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: netrc_machine
part: body
regex:
- 'machine\s+(\S+)'
group: 1
-50
View File
@@ -1,50 +0,0 @@
# npmrc Token Exposure Detection Module
id: npmrc-exposure
info:
name: npmrc Token Exposure
author: sif
severity: high
description: Detects exposed .npmrc files that leak registry auth tokens or passwords
tags: [npm, npmrc, token, secrets, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.npmrc"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: or
words:
- "_authToken"
- "_auth="
- "_auth ="
- ":_password"
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<!doctype"
- "<html"
- "<HTML"
- "<head>"
- "<title>"
extractors:
- type: regex
name: npm_registry
part: body
regex:
- '//([^/:]+)/:_authToken'
group: 1
-43
View File
@@ -1,43 +0,0 @@
# PostgreSQL pgpass File Exposure Detection Module
id: pgpass-exposure
info:
name: PostgreSQL pgpass File Exposure
author: sif
severity: high
description: Detects an exposed .pgpass file that leaks postgres connection passwords in cleartext
tags: [pgpass, postgres, credentials, secrets, recon, exposure]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.pgpass"
- "{{BaseURL}}/pgpass.conf"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '(?m)^[^:\s#]+:(?:\d+|\*):[^:\n]*:[^:\n]*:[^\n]+$'
- type: word
part: body
negative: true
condition: or
words:
- "<!DOCTYPE"
- "<html"
extractors:
- type: regex
name: pgpass_host
part: body
regex:
- '(?m)^([^:\s#]+):(?:\d+|\*):'
group: 1
-47
View File
@@ -1,47 +0,0 @@
# PHP Info Exposure Detection Module
id: phpinfo-exposure
info:
name: PHP Info Exposure
author: sif
severity: high
description: Detects exposed phpinfo() pages leaking config and environment
tags: [php, phpinfo, exposure, misconfiguration, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/phpinfo.php"
- "{{BaseURL}}/info.php"
- "{{BaseURL}}/php_info.php"
- "{{BaseURL}}/test.php"
- "{{BaseURL}}/i.php"
matchers:
- type: status
status:
- 200
- type: regex
part: body
condition: or
regex:
- '<title>(PHP [0-9][0-9.]* - )?phpinfo\(\)</title>'
- 'Zend Scripting Language Engine:<br />Zend Engine v'
- type: regex
part: body
condition: or
regex:
- 'class="e">PHP Version\s*</td><td class="v">'
- 'class="e">System\s*</td>'
extractors:
- type: regex
name: php_version
part: body
regex:
- 'class="e">PHP Version\s*</td><td class="v">\s*([0-9]+(?:\.[0-9]+)*)'
group: 1
@@ -1,38 +0,0 @@
# Prometheus Metrics Exposure Detection Module
id: prometheus-metrics-exposure
info:
name: Prometheus Metrics Exposure
author: sif
severity: medium
description: Detects an exposed Prometheus metrics endpoint leaking internals
tags: [prometheus, metrics, exposure, misconfiguration, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/metrics"
- "{{BaseURL}}/actuator/prometheus"
matchers:
- type: status
status:
- 200
- type: regex
part: body
condition: and
regex:
- '(?m)^# HELP \S+ '
- '(?m)^# TYPE \S+ (counter|gauge|histogram|summary|untyped)'
- '(?m)^[a-zA-Z_][a-zA-Z0-9_:]*(\{[^}]*\})? -?[0-9]'
extractors:
- type: regex
name: go_version
part: body
regex:
- 'go_info\{version="([^"]+)"'
group: 1
-44
View File
@@ -1,44 +0,0 @@
# Server Status Page Exposure Detection Module
id: server-status-exposure
info:
name: Server Status Page Exposure
author: sif
severity: medium
description: Detects exposed Apache mod_status, mod_info and nginx stub_status pages
tags: [apache, nginx, status, exposure, misconfiguration, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/server-status"
- "{{BaseURL}}/server-status?auto"
- "{{BaseURL}}/server-info"
- "{{BaseURL}}/nginx_status"
- "{{BaseURL}}/stub_status"
- "{{BaseURL}}/status"
- "{{BaseURL}}/basic_status"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '<h1>Apache Server Status for'
- '<h1>Apache Server Information'
- '(?m)^Scoreboard: [._SRWKDCGLI]'
- '(?s)Active connections: [0-9].{0,40}server accepts handled requests'
condition: or
extractors:
- type: regex
name: server_version
part: body
regex:
- 'Server ?Version: (Apache/[0-9.]+)'
group: 1
@@ -1,44 +0,0 @@
# Spring Boot Actuator Exposure Detection Module
id: spring-actuator-exposure
info:
name: Spring Boot Actuator Exposure
author: sif
severity: high
description: Detects exposed Spring Boot Actuator endpoints leaking internals
tags: [spring, actuator, exposure, misconfiguration, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/actuator"
- "{{BaseURL}}/actuator/env"
- "{{BaseURL}}/actuator/health"
- "{{BaseURL}}/actuator/metrics"
- "{{BaseURL}}/actuator/configprops"
- "{{BaseURL}}/env"
- "{{BaseURL}}/health"
matchers:
- type: status
status:
- 200
- type: regex
part: body
condition: or
regex:
- '"propertySources"\s*:\s*\['
- '"_links"(?s).*?"href"\s*:\s*"[^"]*/actuator"'
- '"components"\s*:\s*\{(?s).*?"status"\s*:\s*"(UP|DOWN)"'
- '"names"\s*:\s*\[(?s).*?"jvm\.'
extractors:
- type: regex
name: active_profiles
part: body
regex:
- '"activeProfiles"\s*:\s*\[\s*"([^"]+)"'
group: 1
@@ -1,35 +0,0 @@
# Spring Boot Heap Dump Exposure Detection Module
id: spring-heapdump-exposure
info:
name: Spring Boot Heap Dump Exposure
author: sif
severity: high
description: Detects an exposed Spring Boot actuator heap dump that leaks application memory and secrets
tags: [spring, actuator, heapdump, exposure, secrets, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/actuator/heapdump"
- "{{BaseURL}}/heapdump"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '^JAVA PROFILE 1\.0'
extractors:
- type: regex
name: hprof_version
part: body
regex:
- '^JAVA PROFILE (1\.0\.\d)'
group: 1
-41
View File
@@ -1,41 +0,0 @@
# Exposed Subversion Repository Detection Module
id: svn-exposure
info:
name: Exposed Subversion Repository
author: sif
severity: high
description: Detects an exposed .svn working copy database that may leak source code
tags: [svn, subversion, exposure, source-code, misconfiguration]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.svn/wc.db"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- '^SQLite format 3\x00'
- type: word
part: body
condition: or
words:
- "WCROOT"
- "PRISTINE"
extractors:
- type: regex
name: svn_repository
part: body
regex:
- '((?:svn\+ssh|svn|https?|file)://[^\x00\s"]+)'
group: 1
@@ -1,38 +0,0 @@
# Symfony Profiler Exposure Detection Module
id: symfony-profiler-exposure
info:
name: Symfony Profiler Exposure
author: sif
severity: high
description: Detects an exposed Symfony web profiler that leaks requests, configuration and environment
tags: [symfony, profiler, debug, exposure, info-disclosure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/_profiler"
- "{{BaseURL}}/app_dev.php/_profiler"
matchers:
- type: status
status:
- 200
- type: word
part: body
condition: or
words:
- "Symfony Profiler"
- "sf-profiler"
- "sf-toolbar"
extractors:
- type: regex
name: profiler_token
part: body
regex:
- '/_profiler/([0-9a-f]{6,})'
group: 1
@@ -1,36 +0,0 @@
# Werkzeug Debugger Exposure Detection Module
id: werkzeug-debugger-exposure
info:
name: Werkzeug Debugger Exposure
author: sif
severity: high
description: Detects an exposed Flask/Werkzeug interactive debugger
tags: [werkzeug, flask, debug, exposure, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/?__debugger__=yes&cmd=resource&f=debugger.js"
matchers:
- type: status
status:
- 200
- type: regex
part: body
regex:
- 'if \(CONSOLE_MODE && EVALEX\) \{'
- 'EVALEX_TRUSTED'
condition: and
extractors:
- type: regex
name: werkzeug_version
part: header
regex:
- 'Werkzeug/([0-9]+(?:\.[0-9]+)+)'
group: 1
-28
View File
@@ -639,16 +639,6 @@ func (app *App) Run() error {
}
}
seen := make(map[string]bool, len(toRun))
deduped := make([]modules.Module, 0, len(toRun))
for _, m := range toRun {
if id := m.Info().ID; !seen[id] {
seen[id] = true
deduped = append(deduped, m)
}
}
toRun = deduped
// Execute modules
opts := modules.Options{
Timeout: app.settings.Timeout,
@@ -657,24 +647,6 @@ func (app *App) Run() error {
}
for _, m := range toRun {
switch m.Info().ID {
case "nuclei-scan":
if app.settings.Nuclei {
continue
}
case "framework-detection":
if app.settings.Framework {
continue
}
case "shodan-lookup":
if app.settings.Shodan {
continue
}
case "whois-lookup":
if app.settings.Whois {
continue
}
}
modLog := output.Module(m.Info().ID)
modLog.Start()
result, err := m.Execute(context.Background(), url, opts)