mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-03 11:24:54 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8421cb8daa |
@@ -12,15 +12,11 @@ on:
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# OIDC tokens and repo secrets are withheld from pull_request runs that
|
||||
# originate from forks or dependabot, so the action cannot authenticate
|
||||
# there and the check fails for every external PR. Skip those cases (the
|
||||
# job simply does not run) instead of failing. Same-repo branch PRs still
|
||||
# get reviewed. To review fork PRs too, switch the trigger to
|
||||
# pull_request_target (has a security trade-off) rather than loosening this.
|
||||
if: >-
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
github.actor != 'dependabot[bot]'
|
||||
# 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:
|
||||
@@ -31,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: markdownlint
|
||||
uses: reviewdog/action-markdownlint@v0.27.0
|
||||
uses: reviewdog/action-markdownlint@v0.26.2
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-review
|
||||
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
|
||||
**go install**
|
||||
```bash
|
||||
go install github.com/vmfunc/sif/cmd/sif@v${{ env.VERSION }}
|
||||
go install github.com/dropalldatabases/sif/cmd/sif@v${{ env.VERSION }}
|
||||
```
|
||||
|
||||
**binary download** - grab the right archive from below.
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: yamllint
|
||||
uses: reviewdog/action-yamllint@v1.22.0
|
||||
uses: reviewdog/action-yamllint@v1.21.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
reporter: github-pr-review
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ linters:
|
||||
check-blank: false
|
||||
exclude-functions:
|
||||
# log writes are best-effort
|
||||
- github.com/vmfunc/sif/internal/logger.Write
|
||||
- github.com/dropalldatabases/sif/internal/logger.Write
|
||||
# Close on io.Closer is idiomatic best-effort
|
||||
- (io.Closer).Close
|
||||
- (*os.File).Close
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ If you like the project, but don't have time to contribute, that's okay too! Her
|
||||
|
||||
## Reporting issues
|
||||
|
||||
If you believe you've found a bug, or you have a new feature to request, please hop on the [Discord server](https://discord.gg/Yksy9J2BvE) first to discuss it.
|
||||
If you believe you've found a bug, or you have a new feature to request, please hop on the [Discord server](https://discord.com/invite/sifcli) first to discuss it.
|
||||
This way, if it's an easy fix, we could help you solve it more quickly, and if it's a feature request we could workshop it together into something more mature.
|
||||
|
||||
When opening an issue, please use the search tool and make sure that the issue has not been discussed before. In the case of a bug report, run sif with the `-d/-debug` flag for full debug logs.
|
||||
|
||||
@@ -375,7 +375,7 @@ go test ./...
|
||||
|
||||
join our discord for support, feature discussions, and pentesting tips:
|
||||
|
||||
[](https://discord.gg/Yksy9J2BvE)
|
||||
[](https://discord.gg/sifcli)
|
||||
|
||||
## contributors
|
||||
|
||||
|
||||
+5
-5
@@ -17,13 +17,13 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/vmfunc/sif"
|
||||
"github.com/vmfunc/sif/internal/config"
|
||||
"github.com/vmfunc/sif/internal/patchnotes"
|
||||
ver "github.com/vmfunc/sif/internal/version"
|
||||
"github.com/dropalldatabases/sif"
|
||||
"github.com/dropalldatabases/sif/internal/config"
|
||||
"github.com/dropalldatabases/sif/internal/patchnotes"
|
||||
ver "github.com/dropalldatabases/sif/internal/version"
|
||||
|
||||
// Register framework detectors
|
||||
_ "github.com/vmfunc/sif/internal/scan/frameworks/detectors"
|
||||
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
|
||||
)
|
||||
|
||||
// version is stamped at release time via -ldflags "-X main.version=...";
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ welcome to the sif documentation. sif is a modular pentesting toolkit designed t
|
||||
|
||||
```bash
|
||||
# install
|
||||
git clone https://github.com/vmfunc/sif.git && cd sif && make
|
||||
git clone https://github.com/dropalldatabases/sif.git && cd sif && make
|
||||
|
||||
# basic scan
|
||||
./sif -u https://example.com
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ setting up a development environment for sif.
|
||||
## clone and build
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vmfunc/sif.git
|
||||
git clone https://github.com/dropalldatabases/sif.git
|
||||
cd sif
|
||||
make
|
||||
```
|
||||
|
||||
@@ -39,7 +39,7 @@ download `sif-windows-amd64.exe` from releases and add to your PATH.
|
||||
requires go 1.25+
|
||||
|
||||
```bash
|
||||
git clone https://github.com/vmfunc/sif.git
|
||||
git clone https://github.com/dropalldatabases/sif.git
|
||||
cd sif
|
||||
make
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module github.com/vmfunc/sif
|
||||
module github.com/dropalldatabases/sif
|
||||
|
||||
go 1.25.7
|
||||
|
||||
@@ -10,11 +10,11 @@ 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.10.0
|
||||
github.com/projectdiscovery/nuclei/v3 v3.9.0
|
||||
github.com/projectdiscovery/retryabledns v1.0.115
|
||||
github.com/projectdiscovery/utils v0.11.1
|
||||
github.com/rocketlaunchr/google-search v1.1.6
|
||||
github.com/tidwall/gjson v1.19.0
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/twmb/murmur3 v1.1.8
|
||||
golang.org/x/net v0.56.0
|
||||
golang.org/x/time v0.15.0
|
||||
@@ -35,12 +35,14 @@ require (
|
||||
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/Masterminds/semver/v3 v3.5.0 // 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
|
||||
@@ -83,6 +85,7 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.4 // indirect
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0 // indirect
|
||||
github.com/bluele/gcache v0.0.2 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
@@ -117,7 +120,6 @@ 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/dlclark/regexp2/v2 v2.2.1 // indirect
|
||||
github.com/docker/go-connections v0.7.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
@@ -171,6 +173,7 @@ require (
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/certificate-transparency-go v1.3.2 // indirect
|
||||
github.com/google/go-github v17.0.0+incompatible // indirect
|
||||
github.com/google/go-github/v30 v30.1.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
|
||||
@@ -191,6 +194,7 @@ require (
|
||||
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
|
||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
@@ -214,6 +218,8 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.4 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/leslie-qiwa/flat v0.0.0-20230424180412-f9d1cf014baa // indirect
|
||||
github.com/lib/pq v1.11.2 // indirect
|
||||
@@ -270,19 +276,17 @@ 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.42 // indirect
|
||||
github.com/projectdiscovery/cdncheck v1.2.39 // indirect
|
||||
github.com/projectdiscovery/clistats v0.1.4 // indirect
|
||||
github.com/projectdiscovery/dsl v0.8.20 // indirect
|
||||
github.com/projectdiscovery/fastdialer v0.5.11 // indirect
|
||||
github.com/projectdiscovery/dsl v0.8.19 // indirect
|
||||
github.com/projectdiscovery/fastdialer v0.5.10 // 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/goja v0.0.0-20260618133720-acb73e419534 // indirect
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d // indirect
|
||||
github.com/projectdiscovery/gologger v1.1.71 // indirect
|
||||
github.com/projectdiscovery/gologger v1.1.70 // indirect
|
||||
github.com/projectdiscovery/gostruct v0.0.2 // indirect
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e // 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/httpx v1.9.0 // indirect
|
||||
@@ -291,16 +295,16 @@ require (
|
||||
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.41 // indirect
|
||||
github.com/projectdiscovery/networkpolicy v0.1.40 // indirect
|
||||
github.com/projectdiscovery/ratelimit v0.0.88 // 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.16 // indirect
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.14 // indirect
|
||||
github.com/projectdiscovery/sarif v0.1.0 // 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.87 // indirect
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.84 // 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
|
||||
@@ -341,6 +345,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/vmihailenco/bufpool v0.1.11 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
|
||||
github.com/vmihailenco/tagparser v0.1.2 // indirect
|
||||
@@ -396,6 +401,7 @@ require (
|
||||
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
|
||||
|
||||
@@ -70,13 +70,13 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
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/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
@@ -88,6 +88,10 @@ github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d h1:DofPB5AcjTnO
|
||||
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=
|
||||
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883/go.mod h1:K+FhM7iKGKtalkeXGEviafPPwyVjDv1a/ehomabLF2w=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
|
||||
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
@@ -217,6 +221,8 @@ github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoG
|
||||
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0 h1:AKDvi1V3xJCmSR6QhcBfHbCN4Vf8FfxeWkMNQfmAGhY=
|
||||
github.com/bits-and-blooms/bloom/v3 v3.5.0/go.mod h1:Y8vrn7nk1tPIlmLtW2ZPV+W7StdVMor6bC1xgpjMZFs=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=
|
||||
@@ -324,8 +330,6 @@ github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYC
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0=
|
||||
github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||
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=
|
||||
@@ -459,8 +463,6 @@ github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||
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=
|
||||
@@ -532,6 +534,7 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
|
||||
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
|
||||
github.com/google/go-github/v30 v30.1.0 h1:VLDx+UolQICEOKu2m4uAoMti1SxuEBAl7RSEG16L+Oo=
|
||||
github.com/google/go-github/v30 v30.1.0/go.mod h1:n8jBpHl45a/rlBUtRJMOG4GhNADUQFEufcolZ95JfU8=
|
||||
@@ -611,6 +614,8 @@ github.com/indece-official/go-ebcdic v1.2.0 h1:nKCubkNoXrGvBp3MSYuplOQnhANCDEY51
|
||||
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=
|
||||
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
|
||||
github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg=
|
||||
github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY=
|
||||
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
|
||||
@@ -688,6 +693,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
@@ -816,8 +825,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/runc v1.3.6 h1:SLGIymCtsk80iNPWgbc8dtjI30r+5mTVV+4dN8/17Sk=
|
||||
github.com/opencontainers/runc v1.3.6/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4=
|
||||
github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q=
|
||||
github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
|
||||
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
|
||||
@@ -846,14 +855,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.42 h1:Y1Q9MPq7uuv25+aGlgjA5nToOcsk+9gNEKjicyhIwQI=
|
||||
github.com/projectdiscovery/cdncheck v1.2.42/go.mod h1:9oE9KKxCSHNvUf0UaMeqqUwWpC38FkNaTll0ScIBT3w=
|
||||
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.20 h1:CxWcKuoHFpOSS1kzqnbJuK5No/6qoRG8IzNDMnZ6c/M=
|
||||
github.com/projectdiscovery/dsl v0.8.20/go.mod h1:e1oHi7mxAxF+UhBhD5gOk90Ga6LQqvFea2voMO1E5D0=
|
||||
github.com/projectdiscovery/fastdialer v0.5.11 h1:eI7jfwz0i73Ot1cowIBezQLxbg0i6INdAsFGJjfwPa0=
|
||||
github.com/projectdiscovery/fastdialer v0.5.11/go.mod h1:W1ZkULr9mMR6i0oRFTztANnpVyEEzPUovK8sUM4eAw8=
|
||||
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/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=
|
||||
@@ -864,16 +873,12 @@ 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/goja v0.0.0-20260618133720-acb73e419534 h1:hYd1zQA/dxO2ASyQ6Re73TcJkW1LjLQvt4+86Hxefz8=
|
||||
github.com/projectdiscovery/goja v0.0.0-20260618133720-acb73e419534/go.mod h1:SO0AP+uKfYeeoR6jyVH/PKRYJE/f5gJrPMAM00iGEMc=
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d h1:fqqH9LHpN2WDz9QuxFrhKNxXSRtzk+Sa6jAhbB7tXcQ=
|
||||
github.com/projectdiscovery/goja_nodejs v0.0.0-20260618132410-8519f75f703d/go.mod h1:Ezmbgdaw4EunGGBU4MQViLoGMJc37LA3ip55YV3KeRI=
|
||||
github.com/projectdiscovery/gologger v1.1.71 h1:IYU4mw9viKdSzMTIGVpYuw1Gtg7QIHIStqAQgeNXcBQ=
|
||||
github.com/projectdiscovery/gologger v1.1.71/go.mod h1:mJwODZcFDg70ihINpOvZevmBtgvpP8H9/l8Y+OPhZPY=
|
||||
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/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-20260615100919-5ee2581bbf7e h1:vxzgQlz2Cy/YvizYDQx9OhucBcmBotfDhbQ4yCY2vfA=
|
||||
github.com/projectdiscovery/govaluate v0.0.0-20260615100919-5ee2581bbf7e/go.mod h1:xH7bPwHxUlz1yx9UlVeTF+UVCUaKhTnZgaxHb5z362E=
|
||||
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=
|
||||
@@ -890,10 +895,10 @@ 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.41 h1:6daf4A8Vj1+iQ7nH2FR4+sOYd7q1WH1qfN61EyQU74c=
|
||||
github.com/projectdiscovery/networkpolicy v0.1.41/go.mod h1:9ULLaMbdv9UnT0C5rmuK4nIwYs0o776xMnkPUb8TtaE=
|
||||
github.com/projectdiscovery/nuclei/v3 v3.10.0 h1:hKWXhfqKvpxGa1vFaS0TlkSPY7EZy4VG7ml4JAMJavo=
|
||||
github.com/projectdiscovery/nuclei/v3 v3.10.0/go.mod h1:pWZNFVtdIHHSWCV6ouGHUPzvdARD+OGN5K9WrVw0plQ=
|
||||
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/rawhttp v0.1.90 h1:LOSZ6PUH08tnKmWsIwvwv1Z/4zkiYKYOSZ6n+8RFKtw=
|
||||
@@ -902,8 +907,8 @@ github.com/projectdiscovery/rdap v0.9.1-0.20221108103045-9865884d1917 h1:m03X4gB
|
||||
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.16 h1:M/xICwSaiSHhd5OIarU3+5JoU7VmbiSAAwYUCK7CTjw=
|
||||
github.com/projectdiscovery/retryablehttp-go v1.3.16/go.mod h1:s0azLAqAbcVCjHI9t0ezPhamevYGM1eoOvFkn4QmpZ8=
|
||||
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/stringsutil v0.0.2 h1:uzmw3IVLJSMW1kEg8eCStG/cGbYYZAja8BH3LqqJXMA=
|
||||
@@ -916,8 +921,8 @@ github.com/projectdiscovery/useragent v0.0.108 h1:fb+uLuFJvC+MHZjCtxQJxtvp1X6A8n
|
||||
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.87 h1:KuUpeRSQ80L6tx9YaAPhfYWTF47bpEURYbAymr76zto=
|
||||
github.com/projectdiscovery/wappalyzergo v0.2.87/go.mod h1:gMH0o5lBp65sKMwHx/tuUdOtW2RjodC6Ti+9QDsYMkY=
|
||||
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/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=
|
||||
@@ -1013,9 +1018,8 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@@ -1044,8 +1048,8 @@ github.com/tidwall/btree v1.8.1/go.mod h1:jBbTdUWhSZClZWoDg54VnvV7/54modSOzDN7VX
|
||||
github.com/tidwall/buntdb v1.3.2 h1:qd+IpdEGs0pZci37G4jF51+fSKlkuUTMXuHhXL1AkKg=
|
||||
github.com/tidwall/buntdb v1.3.2/go.mod h1:lZZrZUWzlyDJKlLQ6DKAy53LnG7m5kHyrEHvvcDmBpU=
|
||||
github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/grect v0.1.4 h1:dA3oIgNgWdSspFzn1kS4S/RDpZFLrIxAZOdJKjYapOg=
|
||||
github.com/tidwall/grect v0.1.4/go.mod h1:9FBsaYRaR0Tcy4UwefBX/UDcDcDy9V5jUcxHzv2jd5Q=
|
||||
github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8=
|
||||
@@ -1082,6 +1086,8 @@ github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94=
|
||||
github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ=
|
||||
github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc=
|
||||
@@ -1638,6 +1644,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -80,7 +80,6 @@ type Settings struct {
|
||||
Header goflags.StringSlice // custom request headers ("Key: Value")
|
||||
Cookie string
|
||||
RateLimit int
|
||||
MaxRetries int // -max-retries: retries on 429/503 (0 = off)
|
||||
Notify bool // -notify: ship findings to configured providers
|
||||
NotifySeverity string // -notify-severity: minimum severity to send (info..critical)
|
||||
NotifyConfig string // -notify-config: path to a notify-compatible yaml file
|
||||
@@ -179,7 +178,6 @@ func registerFlags(settings *Settings) *goflags.FlagSet {
|
||||
flagSet.StringSliceVarP(&settings.Header, "header", "H", nil, "Custom header to send (repeatable or comma-separated, \"Key: Value\")", goflags.CommaSeparatedStringSliceOptions),
|
||||
flagSet.StringVar(&settings.Cookie, "cookie", "", "Cookie header to send with every request"),
|
||||
flagSet.IntVar(&settings.RateLimit, "rate-limit", 0, "Max requests per second (0 = unlimited)"),
|
||||
flagSet.IntVar(&settings.MaxRetries, "max-retries", 2, "Retries on 429/503 with Retry-After backoff (0 = off)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("output", "Output",
|
||||
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
"github.com/dropalldatabases/sif/internal/scan/js"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/vmfunc/sif/internal/scan"
|
||||
"github.com/vmfunc/sif/internal/scan/frameworks"
|
||||
"github.com/vmfunc/sif/internal/scan/js"
|
||||
)
|
||||
|
||||
// Finding is the normalized shape every scanner result collapses to. one
|
||||
|
||||
@@ -16,13 +16,13 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
"github.com/dropalldatabases/sif/internal/scan/js"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/vmfunc/sif/internal/scan"
|
||||
"github.com/vmfunc/sif/internal/scan/frameworks"
|
||||
"github.com/vmfunc/sif/internal/scan/js"
|
||||
)
|
||||
|
||||
// scanResultType mirrors the minimal interface the scan packages implement; the
|
||||
|
||||
+17
-113
@@ -16,13 +16,11 @@
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -76,8 +74,6 @@ type Options struct {
|
||||
Cookie string
|
||||
UserAgent string
|
||||
RateLimit int
|
||||
// MaxRetries is how many 429/503 responses to retry with backoff (0 = off).
|
||||
MaxRetries int
|
||||
// Threads is the scan worker count; it sizes the per-host idle pool so
|
||||
// concurrent workers hitting one target reuse conns instead of dialing fresh.
|
||||
Threads int
|
||||
@@ -111,12 +107,11 @@ func Configure(opts Options) error {
|
||||
}
|
||||
|
||||
rt := &roundTripper{
|
||||
base: base,
|
||||
headers: headers,
|
||||
cookie: opts.Cookie,
|
||||
userAgent: opts.UserAgent,
|
||||
limiter: limiter,
|
||||
maxRetries: opts.MaxRetries,
|
||||
base: base,
|
||||
headers: headers,
|
||||
cookie: opts.Cookie,
|
||||
userAgent: opts.UserAgent,
|
||||
limiter: limiter,
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
@@ -231,15 +226,20 @@ func parseHeaders(raw []string) (map[string]string, error) {
|
||||
|
||||
// roundTripper paces and decorates each request before delegating to base.
|
||||
type roundTripper struct {
|
||||
base *http.Transport
|
||||
headers map[string]string
|
||||
cookie string
|
||||
userAgent string
|
||||
limiter *rate.Limiter
|
||||
maxRetries int
|
||||
base *http.Transport
|
||||
headers map[string]string
|
||||
cookie string
|
||||
userAgent string
|
||||
limiter *rate.Limiter
|
||||
}
|
||||
|
||||
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
if rt.limiter != nil {
|
||||
if err := rt.limiter.Wait(req.Context()); err != nil {
|
||||
return nil, fmt.Errorf("rate limiter: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// only set what the caller hasn't already; a scanner that explicitly sets a
|
||||
// header (e.g. an api key) must win over the global default.
|
||||
for key, value := range rt.headers {
|
||||
@@ -254,101 +254,5 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("User-Agent", rt.userAgent)
|
||||
}
|
||||
|
||||
for attempt := 0; ; attempt++ {
|
||||
if rt.limiter != nil {
|
||||
if err := rt.limiter.Wait(req.Context()); err != nil {
|
||||
return nil, fmt.Errorf("rate limiter: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := rt.base.RoundTrip(req)
|
||||
if err != nil || attempt >= rt.maxRetries || !retryableStatus(resp.StatusCode) {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// back off and retry, unless the body can't be replayed.
|
||||
if !rewind(req) {
|
||||
return resp, nil
|
||||
}
|
||||
wait := retryAfter(resp, attempt)
|
||||
DrainClose(resp)
|
||||
|
||||
if err := sleepCtx(req.Context(), wait); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retryableStatus(code int) bool {
|
||||
return code == http.StatusTooManyRequests || code == http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
const (
|
||||
retryAfterCap = 20 * time.Second
|
||||
retryBackoffBase = 500 * time.Millisecond
|
||||
// clamp the shift so a large -max-retries can't overflow the duration.
|
||||
retryBackoffMaxShift = 16
|
||||
)
|
||||
|
||||
// retryAfter honors a Retry-After header (delta-seconds or HTTP-date) and
|
||||
// otherwise falls back to capped exponential backoff.
|
||||
func retryAfter(resp *http.Response, attempt int) time.Duration {
|
||||
if v := strings.TrimSpace(resp.Header.Get("Retry-After")); v != "" {
|
||||
if secs, err := strconv.Atoi(v); err == nil && secs >= 0 {
|
||||
return capDuration(time.Duration(secs) * time.Second)
|
||||
}
|
||||
if t, err := http.ParseTime(v); err == nil {
|
||||
return capDuration(time.Until(t))
|
||||
}
|
||||
}
|
||||
shift := attempt
|
||||
if shift > retryBackoffMaxShift {
|
||||
shift = retryBackoffMaxShift
|
||||
}
|
||||
return capDuration(retryBackoffBase << shift)
|
||||
}
|
||||
|
||||
// capDuration clamps d to [0, retryAfterCap].
|
||||
func capDuration(d time.Duration) time.Duration {
|
||||
switch {
|
||||
case d < 0:
|
||||
return 0
|
||||
case d > retryAfterCap:
|
||||
return retryAfterCap
|
||||
default:
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
// rewind restores req.Body for a resend. Only a GetBody-backed body (set by
|
||||
// net/http for the in-memory bodies sif uses) is replayable; a nil or NoBody
|
||||
// request needs nothing, anything else can't be retried.
|
||||
func rewind(req *http.Request) bool {
|
||||
if req.Body == nil || req.Body == http.NoBody {
|
||||
return true
|
||||
}
|
||||
if req.GetBody == nil {
|
||||
return false
|
||||
}
|
||||
body, err := req.GetBody()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
req.Body = body
|
||||
return true
|
||||
}
|
||||
|
||||
// sleepCtx waits for d or until ctx is cancelled, whichever comes first.
|
||||
func sleepCtx(ctx context.Context, d time.Duration) error {
|
||||
if d <= 0 {
|
||||
return nil
|
||||
}
|
||||
timer := time.NewTimer(d)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
return rt.base.RoundTrip(req)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -490,228 +489,3 @@ func resetBench() {
|
||||
configured = nil
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
// retrySequenceServer serves codes[n] on the n-th hit, repeating the last once
|
||||
// the slice runs out; retryable codes carry Retry-After: 0 to keep tests fast.
|
||||
func retrySequenceServer(t *testing.T, hits *atomic.Int64, codes ...int) *httptest.Server {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
n := int(hits.Add(1)) - 1
|
||||
code := codes[len(codes)-1]
|
||||
if n < len(codes) {
|
||||
code = codes[n]
|
||||
}
|
||||
if code == http.StatusTooManyRequests || code == http.StatusServiceUnavailable {
|
||||
w.Header().Set("Retry-After", "0")
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
|
||||
// getStatus performs a GET and returns the final status code the caller sees.
|
||||
func getStatus(t *testing.T, client *http.Client, url string) int {
|
||||
t.Helper()
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody)
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp.StatusCode
|
||||
}
|
||||
|
||||
func TestRetryRecoversAfter429(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
var hits atomic.Int64
|
||||
srv := retrySequenceServer(t, &hits, http.StatusTooManyRequests, http.StatusOK)
|
||||
if err := Configure(Options{MaxRetries: 2}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
if code := getStatus(t, Client(5*time.Second), srv.URL); code != http.StatusOK {
|
||||
t.Errorf("status = %d, want 200 after retry", code)
|
||||
}
|
||||
if got := hits.Load(); got != 2 {
|
||||
t.Errorf("server hits = %d, want 2 (initial + one retry)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryRecoversAfter503(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
var hits atomic.Int64
|
||||
srv := retrySequenceServer(t, &hits, http.StatusServiceUnavailable, http.StatusOK)
|
||||
if err := Configure(Options{MaxRetries: 2}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
if code := getStatus(t, Client(5*time.Second), srv.URL); code != http.StatusOK {
|
||||
t.Errorf("status = %d, want 200 after retry", code)
|
||||
}
|
||||
if got := hits.Load(); got != 2 {
|
||||
t.Errorf("server hits = %d, want 2", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryDisabled(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
var hits atomic.Int64
|
||||
srv := retrySequenceServer(t, &hits, http.StatusTooManyRequests, http.StatusOK)
|
||||
if err := Configure(Options{MaxRetries: 0}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
if code := getStatus(t, Client(5*time.Second), srv.URL); code != http.StatusTooManyRequests {
|
||||
t.Errorf("status = %d, want 429 with retries off", code)
|
||||
}
|
||||
if got := hits.Load(); got != 1 {
|
||||
t.Errorf("server hits = %d, want 1 (no retry)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryExhausted(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
var hits atomic.Int64
|
||||
srv := retrySequenceServer(t, &hits, http.StatusTooManyRequests) // always 429
|
||||
if err := Configure(Options{MaxRetries: 2}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
if code := getStatus(t, Client(5*time.Second), srv.URL); code != http.StatusTooManyRequests {
|
||||
t.Errorf("status = %d, want 429 after exhausting retries", code)
|
||||
}
|
||||
if got := hits.Load(); got != 3 {
|
||||
t.Errorf("server hits = %d, want 3 (initial + 2 retries)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryIgnoresNonRetryableStatus(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
var hits atomic.Int64
|
||||
srv := retrySequenceServer(t, &hits, http.StatusInternalServerError, http.StatusOK)
|
||||
if err := Configure(Options{MaxRetries: 2}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
if code := getStatus(t, Client(5*time.Second), srv.URL); code != http.StatusInternalServerError {
|
||||
t.Errorf("status = %d, want 500 (not retried)", code)
|
||||
}
|
||||
if got := hits.Load(); got != 1 {
|
||||
t.Errorf("server hits = %d, want 1 (500 not retried)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryReplaysRequestBody(t *testing.T) {
|
||||
resetConfig(t)
|
||||
|
||||
var hits atomic.Int64
|
||||
var bmu sync.Mutex
|
||||
var bodies []string
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
bmu.Lock()
|
||||
bodies = append(bodies, string(body))
|
||||
bmu.Unlock()
|
||||
if hits.Add(1) == 1 {
|
||||
w.Header().Set("Retry-After", "0")
|
||||
w.WriteHeader(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
if err := Configure(Options{MaxRetries: 2}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, srv.URL, strings.NewReader("payload"))
|
||||
if err != nil {
|
||||
t.Fatalf("new request: %v", err)
|
||||
}
|
||||
resp, err := Client(5 * time.Second).Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("do request: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200 after body replay", resp.StatusCode)
|
||||
}
|
||||
|
||||
bmu.Lock()
|
||||
defer bmu.Unlock()
|
||||
if len(bodies) != 2 {
|
||||
t.Fatalf("server saw %d requests, want 2", len(bodies))
|
||||
}
|
||||
for i, body := range bodies {
|
||||
if body != "payload" {
|
||||
t.Errorf("body[%d] = %q, want %q (rewind dropped the body)", i, body, "payload")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryAfterHeader(t *testing.T) {
|
||||
noHeader := &http.Response{Header: http.Header{}}
|
||||
if got := retryAfter(noHeader, 0); got != retryBackoffBase {
|
||||
t.Errorf("missing header: attempt 0 = %v, want %v", got, retryBackoffBase)
|
||||
}
|
||||
if got := retryAfter(noHeader, 1); got != 2*retryBackoffBase {
|
||||
t.Errorf("missing header: attempt 1 = %v, want %v", got, 2*retryBackoffBase)
|
||||
}
|
||||
if got := retryAfter(noHeader, 1000); got != retryAfterCap {
|
||||
t.Errorf("missing header: attempt 1000 = %v, want cap %v", got, retryAfterCap)
|
||||
}
|
||||
|
||||
withSeconds := func(v string) *http.Response {
|
||||
return &http.Response{Header: http.Header{"Retry-After": {v}}}
|
||||
}
|
||||
if got := retryAfter(withSeconds("3"), 0); got != 3*time.Second {
|
||||
t.Errorf("Retry-After 3 = %v, want 3s", got)
|
||||
}
|
||||
if got := retryAfter(withSeconds("0"), 5); got != 0 {
|
||||
t.Errorf("Retry-After 0 = %v, want 0", got)
|
||||
}
|
||||
if got := retryAfter(withSeconds("9999"), 0); got != retryAfterCap {
|
||||
t.Errorf("Retry-After 9999 = %v, want cap %v", got, retryAfterCap)
|
||||
}
|
||||
if got := retryAfter(withSeconds("soon"), 0); got != retryBackoffBase {
|
||||
t.Errorf("Retry-After junk = %v, want backoff %v", got, retryBackoffBase)
|
||||
}
|
||||
|
||||
future := time.Now().Add(5 * time.Second).UTC().Format(http.TimeFormat)
|
||||
if got := retryAfter(withSeconds(future), 0); got <= 0 || got > 5*time.Second {
|
||||
t.Errorf("Retry-After http-date = %v, want (0, 5s]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapDuration(t *testing.T) {
|
||||
cases := []struct{ in, want time.Duration }{
|
||||
{-time.Second, 0},
|
||||
{0, 0},
|
||||
{5 * time.Second, 5 * time.Second},
|
||||
{retryAfterCap, retryAfterCap},
|
||||
{retryAfterCap + time.Second, retryAfterCap},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := capDuration(c.in); got != c.want {
|
||||
t.Errorf("capDuration(%v) = %v, want %v", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSleepCtxCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if err := sleepCtx(ctx, time.Hour); err == nil {
|
||||
t.Error("sleepCtx on a cancelled context should return its error, not block")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,216 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runOrchestrationModule(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 orchestrationExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestAIOrchestrationExposureModules(t *testing.T) {
|
||||
const langflow = "../../modules/recon/langflow-exposure.yaml"
|
||||
const dify = "../../modules/recon/dify-console-exposure.yaml"
|
||||
const ray = "../../modules/recon/ray-dashboard-exposure.yaml"
|
||||
const skypilot = "../../modules/recon/skypilot-api-exposure.yaml"
|
||||
|
||||
langflowVersion := `{"version":"1.0.19","main_version":"1.0.19","package":"Langflow"}`
|
||||
|
||||
difyFeatures := `{"enable_app_deploy":true,"sso_enforced_for_signin":false,` +
|
||||
`"sso_enforced_for_signin_protocol":"","enable_marketplace":true,"enable_email_code_login":false,` +
|
||||
`"enable_email_password_login":true,"enable_social_oauth_login":false,"is_allow_register":true,` +
|
||||
`"is_allow_create_workspace":false,"is_email_setup":true,"license":{"status":"none","expired_at":""}}`
|
||||
|
||||
rayVersion := `{"version":"4","ray_version":"2.9.3","ray_commit":"a1b2c3d4e5","session_name":"session_2024"}`
|
||||
|
||||
skypilotHealth := `{"status":"healthy","api_version":"14","version":"0.9.3","version_on_disk":"0.9.3",` +
|
||||
`"commit":"abc1234def","basic_auth_enabled":false}`
|
||||
|
||||
t.Run("a langflow version api is flagged", func(t *testing.T) {
|
||||
res := runOrchestrationModule(t, langflow, 200, langflowVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a langflow finding")
|
||||
}
|
||||
if v := orchestrationExtract(res, "langflow_version"); v != "1.0.19" {
|
||||
t.Errorf("langflow_version=%q, want 1.0.19", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a langflow base build is still flagged", func(t *testing.T) {
|
||||
body := `{"version":"1.0.19","main_version":"1.0.19","package":"Langflow Base"}`
|
||||
if res := runOrchestrationModule(t, langflow, 200, body); len(res.Findings) == 0 {
|
||||
t.Fatal("expected a finding for a langflow base build")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a version api from another package is not flagged as langflow", func(t *testing.T) {
|
||||
body := `{"version":"1.0","main_version":"1.0","package":"SomeApp"}`
|
||||
if res := runOrchestrationModule(t, langflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("another package should not match langflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a langflow package without main_version is not flagged", func(t *testing.T) {
|
||||
if res := runOrchestrationModule(t, langflow, 200, `{"package":"Langflow"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a package-only body should not match langflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a dify system-features is flagged and reports open registration", func(t *testing.T) {
|
||||
res := runOrchestrationModule(t, dify, 200, difyFeatures)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a dify finding")
|
||||
}
|
||||
if v := orchestrationExtract(res, "dify_allow_register"); v != "true" {
|
||||
t.Errorf("dify_allow_register=%q, want true", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without sso_enforced_for_signin is not flagged as dify", func(t *testing.T) {
|
||||
body := `{"enable_email_password_login":true,"is_allow_create_workspace":false,"is_allow_register":true}`
|
||||
if res := runOrchestrationModule(t, dify, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without sso_enforced_for_signin should not match dify, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without enable_email_password_login is not flagged as dify", func(t *testing.T) {
|
||||
body := `{"sso_enforced_for_signin":false,"is_allow_create_workspace":false}`
|
||||
if res := runOrchestrationModule(t, dify, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without enable_email_password_login should not match dify, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without is_allow_create_workspace is not flagged as dify", func(t *testing.T) {
|
||||
body := `{"sso_enforced_for_signin":false,"enable_email_password_login":true}`
|
||||
if res := runOrchestrationModule(t, dify, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without is_allow_create_workspace should not match dify, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a dify with registration disabled is not flagged", func(t *testing.T) {
|
||||
body := `{"sso_enforced_for_signin":false,"enable_email_password_login":true,"is_allow_create_workspace":true,"is_allow_register":false}`
|
||||
if res := runOrchestrationModule(t, dify, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a closed-registration dify should not be flagged, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a ray dashboard version is flagged", func(t *testing.T) {
|
||||
res := runOrchestrationModule(t, ray, 200, rayVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a ray finding")
|
||||
}
|
||||
if v := orchestrationExtract(res, "ray_version"); v != "2.9.3" {
|
||||
t.Errorf("ray_version=%q, want 2.9.3", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic version api is not flagged as ray", func(t *testing.T) {
|
||||
body := `{"version":"4","api_version":"v1","build":"123"}`
|
||||
if res := runOrchestrationModule(t, ray, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic version api should not match ray, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a ray_version without a ray_commit is not flagged", func(t *testing.T) {
|
||||
body := `{"version":"4","ray_version":"2.9.3"}`
|
||||
if res := runOrchestrationModule(t, ray, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("ray_version alone should not match ray, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a skypilot health is flagged with its version and auth state", func(t *testing.T) {
|
||||
res := runOrchestrationModule(t, skypilot, 200, skypilotHealth)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a skypilot finding")
|
||||
}
|
||||
if v := orchestrationExtract(res, "skypilot_version"); v != "0.9.3" {
|
||||
t.Errorf("skypilot_version=%q, want 0.9.3", v)
|
||||
}
|
||||
if v := orchestrationExtract(res, "skypilot_basic_auth"); v != "false" {
|
||||
t.Errorf("skypilot_basic_auth=%q, want false", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a bare status health is not flagged as skypilot", func(t *testing.T) {
|
||||
if res := runOrchestrationModule(t, skypilot, 200, `{"status":"healthy"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("an auth-gated bare health should not match skypilot, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without version_on_disk is not flagged as skypilot", func(t *testing.T) {
|
||||
body := `{"status":"healthy","api_version":"14","commit":"abc","basic_auth_enabled":false}`
|
||||
if res := runOrchestrationModule(t, skypilot, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without version_on_disk should not match skypilot, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without basic_auth_enabled is not flagged as skypilot", func(t *testing.T) {
|
||||
body := `{"status":"healthy","version_on_disk":"0.9.3","commit":"abc"}`
|
||||
if res := runOrchestrationModule(t, skypilot, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without basic_auth_enabled should not match skypilot, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without commit is not flagged as skypilot", func(t *testing.T) {
|
||||
body := `{"status":"healthy","version_on_disk":"0.9.3","basic_auth_enabled":false}`
|
||||
if res := runOrchestrationModule(t, skypilot, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without commit should not match skypilot, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a skypilot with basic auth enabled is not flagged", func(t *testing.T) {
|
||||
body := `{"status":"healthy","version_on_disk":"0.9.3","commit":"abc","basic_auth_enabled":true}`
|
||||
if res := runOrchestrationModule(t, skypilot, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an auth-enabled skypilot should not be flagged, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{langflow, dify, ray, skypilot} {
|
||||
if res := runOrchestrationModule(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{langflow, dify, ray, skypilot} {
|
||||
if res := runOrchestrationModule(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,87 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runAlertmanagerModule(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 alertmanagerExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestAlertmanagerExposureModule(t *testing.T) {
|
||||
const am = "../../modules/recon/alertmanager-status-exposure.yaml"
|
||||
|
||||
t.Run("an alertmanager status is flagged with its version", func(t *testing.T) {
|
||||
body := `{"cluster":{"name":"01HXYZ","status":"ready","peers":[{"name":"01HX","address":"10.0.0.7:9094"}]},` +
|
||||
`"versionInfo":{"branch":"HEAD","buildDate":"20240228","buildUser":"root@host","goVersion":"go1.21.7",` +
|
||||
`"revision":"0aa3c2a","version":"0.27.0"},"config":{"original":"global:\n smtp_smarthost: 'smtp:587'\n ` +
|
||||
`smtp_auth_password: 'hunter2'\nreceivers:\n- name: team\n slack_configs:\n - api_url: 'https://hooks.slack.com/services/T/B/X'\n"},` +
|
||||
`"uptime":"2024-06-01T10:00:00.000Z"}`
|
||||
res := runAlertmanagerModule(t, am, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an alertmanager finding")
|
||||
}
|
||||
if v := alertmanagerExtract(res, "alertmanager_version"); v != "0.27.0" {
|
||||
t.Errorf("alertmanager_version=%q, want 0.27.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a versionInfo+cluster body without config is not flagged", func(t *testing.T) {
|
||||
body := `{"cluster":{"name":"01HXYZ","status":"ready"},"versionInfo":{"version":"0.27.0"},"uptime":"x"}`
|
||||
if res := runAlertmanagerModule(t, am, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a configless status should not match alertmanager, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a config+versionInfo body without cluster is not flagged", func(t *testing.T) {
|
||||
body := `{"versionInfo":{"version":"0.27.0"},"config":{"original":"global:\n"},"uptime":"x"}`
|
||||
if res := runAlertmanagerModule(t, am, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a clusterless status should not match alertmanager, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
if res := runAlertmanagerModule(t, am, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 body should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
if res := runAlertmanagerModule(t, am, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runAnalyticsModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runAppCfgModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runArgocdModule(t *testing.T, status int, body string) *modules.Result {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/vmfunc/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
func reqURLs(reqs []*httpRequest) []string {
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runAutomationModule(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 automationExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestAutomationServerExposureModules(t *testing.T) {
|
||||
const jenkins = "../../modules/recon/jenkins-api-exposure.yaml"
|
||||
const nifi = "../../modules/recon/nifi-api-exposure.yaml"
|
||||
|
||||
t.Run("a jenkins controller api is flagged with the first job", func(t *testing.T) {
|
||||
body := `{"_class":"hudson.model.Hudson","assignedLabels":[{}],"mode":"NORMAL","nodeDescription":` +
|
||||
`"the master Jenkins node","nodeName":"","numExecutors":2,"jobs":[{"_class":` +
|
||||
`"hudson.model.FreeStyleProject","name":"deploy-prod","url":"http://ci/job/deploy-prod/","color":"blue"}],` +
|
||||
`"useSecurity":true,"views":[{"_class":"hudson.model.AllView","name":"all","url":"http://ci/"}]}`
|
||||
res := runAutomationModule(t, jenkins, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a jenkins finding")
|
||||
}
|
||||
if v := automationExtract(res, "jenkins_job"); v != "deploy-prod" {
|
||||
t.Errorf("jenkins_job=%q, want deploy-prod", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a non-root jenkins object is not flagged as the controller", func(t *testing.T) {
|
||||
body := `{"_class":"hudson.model.FreeStyleProject","name":"deploy-prod","jobs":[],"color":"blue"}`
|
||||
if res := runAutomationModule(t, jenkins, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a FreeStyleProject object should not match the controller, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a hudson root without a jobs key is not flagged", func(t *testing.T) {
|
||||
if res := runAutomationModule(t, jenkins, 200, `{"_class":"hudson.model.Hudson","mode":"NORMAL"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a jobless controller blob should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a nifi about is flagged with its version", func(t *testing.T) {
|
||||
body := `{"about":{"title":"NiFi","version":"1.28.1","uri":"https://nifi:8443/nifi-api/",` +
|
||||
`"contentViewerUrl":"../nifi-content-viewer/","timezone":"UTC","buildTag":"nifi-1.28.1-RC1",` +
|
||||
`"buildRevision":"abc123","buildBranch":"support/nifi-1.x","buildTimestamp":"06/01/2024 10:00:00 UTC"}}`
|
||||
res := runAutomationModule(t, nifi, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a nifi finding")
|
||||
}
|
||||
if v := automationExtract(res, "nifi_version"); v != "1.28.1" {
|
||||
t.Errorf("nifi_version=%q, want 1.28.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an about block for another product is not flagged as nifi", func(t *testing.T) {
|
||||
if res := runAutomationModule(t, nifi, 200, `{"about":{"title":"SomeApp","version":"2.0.0"}}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-nifi about block 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{jenkins, nifi} {
|
||||
if res := runAutomationModule(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 403 or 401 is not a leak", func(t *testing.T) {
|
||||
if res := runAutomationModule(t, jenkins, 403, `{"_class":"hudson.model.Hudson","jobs":[]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a 403 jenkins should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
if res := runAutomationModule(t, nifi, 401, `{"about":{"title":"NiFi","version":"1.28.1"}}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 nifi should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runBigDataModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runGridModule(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 gridExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const seleniumStatusBody = `{"value":{"ready":true,"message":"Selenium Grid ready.","nodes":[` +
|
||||
`{"id":"028ca108-bfc4-430e-806c-6477b6b8569e","uri":"http://10.0.0.5:5555","maxSessions":1,` +
|
||||
`"osInfo":{"arch":"amd64","name":"Linux","version":"5.15.0"},"heartbeatPeriod":60000,` +
|
||||
`"availability":"UP","version":"4.18.1 (revision b1d3319b48)","slots":[{"lastStarted":` +
|
||||
`"2024-06-01T10:00:00Z","session":null,"id":{"hostId":"028ca108","id":"fdd41c10"},` +
|
||||
`"stereotype":{"browserName":"chrome","platformName":"LINUX"}}]}]}}`
|
||||
|
||||
const selenoidStatusBody = `{"total":80,"used":10,"queued":0,"pending":1,"browsers":{"chrome":` +
|
||||
`{"124.0":{"user1":{"count":1,"sessions":[{"id":"abc","container":"sel-abc"}]}}},"firefox":{"125.0":{}}}}`
|
||||
|
||||
func TestBrowserGridExposureModules(t *testing.T) {
|
||||
const selenium = "../../modules/recon/selenium-grid-exposure.yaml"
|
||||
const selenoid = "../../modules/recon/selenoid-exposure.yaml"
|
||||
|
||||
t.Run("a selenium grid status is flagged with a node version", func(t *testing.T) {
|
||||
res := runGridModule(t, selenium, 200, seleniumStatusBody)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a selenium finding")
|
||||
}
|
||||
if v := gridExtract(res, "selenium_version"); v != "4.18.1 (revision b1d3319b48)" {
|
||||
t.Errorf("selenium_version=%q, want the node build string", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a value+nodes envelope without the selenium grid message is not flagged", func(t *testing.T) {
|
||||
body := `{"value":{"ready":true,"nodes":[{"id":"n1","name":"router"}]}}`
|
||||
if res := runGridModule(t, selenium, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic value/nodes blob should not match selenium, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a selenoid status is flagged with the first browser", func(t *testing.T) {
|
||||
res := runGridModule(t, selenoid, 200, selenoidStatusBody)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a selenoid finding")
|
||||
}
|
||||
if v := gridExtract(res, "selenoid_browser"); v != "chrome" {
|
||||
t.Errorf("selenoid_browser=%q, want chrome", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a capacity blob without browsers is not flagged as selenoid", func(t *testing.T) {
|
||||
if res := runGridModule(t, selenoid, 200, `{"total":80,"used":10,"queued":0,"pending":1}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a browserless capacity blob should not match selenoid, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the two grid modules do not cross-match each other", func(t *testing.T) {
|
||||
if res := runGridModule(t, selenoid, 200, seleniumStatusBody); len(res.Findings) > 0 {
|
||||
t.Errorf("selenium status should not match selenoid, got %d findings", len(res.Findings))
|
||||
}
|
||||
if res := runGridModule(t, selenium, 200, selenoidStatusBody); len(res.Findings) > 0 {
|
||||
t.Errorf("selenoid status should not match selenium, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{selenium, selenoid} {
|
||||
if res := runGridModule(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{selenium, selenoid} {
|
||||
if res := runGridModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runBuildCredModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runCICDModule(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 cicdExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestCICDServerExposureModules(t *testing.T) {
|
||||
const concourse = "../../modules/recon/concourse-info-exposure.yaml"
|
||||
const woodpecker = "../../modules/recon/woodpecker-version-exposure.yaml"
|
||||
const gocd = "../../modules/recon/gocd-version-exposure.yaml"
|
||||
|
||||
t.Run("a concourse info is flagged with its version", func(t *testing.T) {
|
||||
body := `{"version":"7.11.2","worker_version":"2.4","external_url":"https://ci.example.com","cluster_name":"prod"}`
|
||||
res := runCICDModule(t, concourse, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a concourse finding")
|
||||
}
|
||||
if v := cicdExtract(res, "concourse_version"); v != "7.11.2" {
|
||||
t.Errorf("concourse_version=%q, want 7.11.2", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an info without worker_version is not flagged as concourse", func(t *testing.T) {
|
||||
body := `{"version":"1.0","external_url":"https://x"}`
|
||||
if res := runCICDModule(t, concourse, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without worker_version should not match concourse, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a woodpecker version is flagged with its version", func(t *testing.T) {
|
||||
body := `{"source":"https://github.com/woodpecker-ci/woodpecker","version":"2.1.0"}`
|
||||
res := runCICDModule(t, woodpecker, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a woodpecker finding")
|
||||
}
|
||||
if v := cicdExtract(res, "woodpecker_version"); v != "2.1.0" {
|
||||
t.Errorf("woodpecker_version=%q, want 2.1.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a drone version with the same shape is not flagged as woodpecker", func(t *testing.T) {
|
||||
body := `{"source":"https://github.com/harness/drone","version":"2.20.0"}`
|
||||
if res := runCICDModule(t, woodpecker, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a drone source should not match woodpecker, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a gocd version is flagged with its version", func(t *testing.T) {
|
||||
body := `{"_links":{"self":{"href":"https://ci/go/api/version"}},"version":"21.4.0",` +
|
||||
`"build_number":"13183","git_sha":"abc123","full_version":"21.4.0 (13183-abc123)",` +
|
||||
`"commit_url":"https://github.com/gocd/gocd/commit/abc123"}`
|
||||
res := runCICDModule(t, gocd, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a gocd finding")
|
||||
}
|
||||
if v := cicdExtract(res, "gocd_version"); v != "21.4.0" {
|
||||
t.Errorf("gocd_version=%q, want 21.4.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a version with a non-gocd commit url is not flagged as gocd", func(t *testing.T) {
|
||||
body := `{"version":"1.0","full_version":"1.0 (1-x)","commit_url":"https://github.com/other/repo/commit/x"}`
|
||||
if res := runCICDModule(t, gocd, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-gocd commit url should not match gocd, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{concourse, woodpecker, gocd} {
|
||||
if res := runCICDModule(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{concourse, woodpecker, gocd} {
|
||||
if res := runCICDModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runCMSCfgModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runCredModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDataOrchModule(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 dataOrchExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestDataOrchestrationExposureModules(t *testing.T) {
|
||||
const dagster = "../../modules/recon/dagster-webserver-exposure.yaml"
|
||||
const mage = "../../modules/recon/mage-status-exposure.yaml"
|
||||
|
||||
t.Run("a dagster server_info is flagged with its version", func(t *testing.T) {
|
||||
body := `{"dagster_webserver_version":"1.7.0","dagster_version":"1.7.0","dagster_graphql_version":"1.7.0"}`
|
||||
res := runDataOrchModule(t, dagster, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a dagster finding")
|
||||
}
|
||||
if v := dataOrchExtract(res, "dagster_version"); v != "1.7.0" {
|
||||
t.Errorf("dagster_version=%q, want 1.7.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a bare core-version body is not flagged as dagster", func(t *testing.T) {
|
||||
if res := runDataOrchModule(t, dagster, 200, `{"dagster_version":"1.7.0"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without dagster_webserver_version should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a mage status is flagged with its repo path", func(t *testing.T) {
|
||||
body := `{"statuses":[{"is_instance_manager":false,"repo_path":"/home/src/default_repo",` +
|
||||
`"repo_path_relative":"default_repo","scheduler_status":"running","project_type":"standalone",` +
|
||||
`"project_uuid":"abc-123"}]}`
|
||||
res := runDataOrchModule(t, mage, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a mage finding")
|
||||
}
|
||||
if v := dataOrchExtract(res, "mage_repo_path"); v != "/home/src/default_repo" {
|
||||
t.Errorf("mage_repo_path=%q, want /home/src/default_repo", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a statuses collection without scheduler fields is not flagged as mage", func(t *testing.T) {
|
||||
if res := runDataOrchModule(t, mage, 200, `{"statuses":[{"id":1,"name":"ok"}]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic statuses array should not match mage, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{dagster, mage} {
|
||||
if res := runDataOrchModule(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{dagster, mage} {
|
||||
if res := runDataOrchModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runPipelineModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDBFileModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDBHTTPModule(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 dbHTTPExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestDatabaseHTTPExposureModules(t *testing.T) {
|
||||
const clickhouse = "../../modules/recon/clickhouse-http-exposure.yaml"
|
||||
const dgraph = "../../modules/recon/dgraph-api-exposure.yaml"
|
||||
|
||||
t.Run("a clickhouse FORMAT JSON result is flagged with the version", func(t *testing.T) {
|
||||
body := `{"meta":[{"name":"version()","type":"String"}],"data":[{"version()":"24.3.1.2672"}],` +
|
||||
`"rows":1,"statistics":{"elapsed":0.000123,"rows_read":1,"bytes_read":1}}`
|
||||
res := runDBHTTPModule(t, clickhouse, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a clickhouse finding")
|
||||
}
|
||||
if v := dbHTTPExtract(res, "clickhouse_version"); v != "24.3.1.2672" {
|
||||
t.Errorf("clickhouse_version=%q, want 24.3.1.2672", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a json result without the statistics envelope is not flagged as clickhouse", func(t *testing.T) {
|
||||
body := `{"meta":[{"name":"x"}],"data":[{"x":1}],"rows":1}`
|
||||
if res := runDBHTTPModule(t, clickhouse, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a statless json result should not match clickhouse, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a dgraph alpha health is flagged with its version", func(t *testing.T) {
|
||||
body := `[{"instance":"alpha","address":"localhost:7080","status":"healthy","group":"0",` +
|
||||
`"version":"v23.1.0","uptime":3600,"lastEcho":1700000000,"ongoing":["opRollup"],"max_assigned":30002}]`
|
||||
res := runDBHTTPModule(t, dgraph, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a dgraph finding")
|
||||
}
|
||||
if v := dbHTTPExtract(res, "dgraph_version"); v != "v23.1.0" {
|
||||
t.Errorf("dgraph_version=%q, want v23.1.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a dgraph alpha health without max_assigned is not flagged", func(t *testing.T) {
|
||||
body := `[{"instance":"alpha","status":"healthy","lastEcho":1700000000}]`
|
||||
if res := runDBHTTPModule(t, dgraph, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a partial alpha health should not match dgraph, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a non-alpha instance health is not flagged as dgraph", func(t *testing.T) {
|
||||
body := `[{"instance":"zero","max_assigned":30002,"lastEcho":1700000000}]`
|
||||
if res := runDBHTTPModule(t, dgraph, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a zero-node health should not match dgraph alpha, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{clickhouse, dgraph} {
|
||||
if res := runDBHTTPModule(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 403 or 404 is not a leak", func(t *testing.T) {
|
||||
if res := runDBHTTPModule(t, clickhouse, 403, "Authentication failed"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 403 clickhouse should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
for _, file := range []string{clickhouse, dgraph} {
|
||||
if res := runDBHTTPModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDBModule(t *testing.T, file string, status int, headers map[string]string, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDebugModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDeployModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDistDBModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runDotfileModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
// runEnvModule runs the env exposure module end to end against a server that
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
const testTimeout = 5 * time.Second
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/vmfunc/sif/internal/fingerprint"
|
||||
"github.com/dropalldatabases/sif/internal/fingerprint"
|
||||
)
|
||||
|
||||
// checkFaviconHash reports whether the body's shodan mmh3 hash matches any
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/vmfunc/sif/internal/fingerprint"
|
||||
"github.com/vmfunc/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/fingerprint"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
// faviconFixture hashes to a negative int32, so its signed and unsigned forms
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runGrafanaModule(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 grafanaExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestGrafanaAnonymousExposureModule(t *testing.T) {
|
||||
const grafana = "../../modules/recon/grafana-anonymous-exposure.yaml"
|
||||
|
||||
t.Run("an anonymous search result is flagged with a dashboard title", func(t *testing.T) {
|
||||
body := `[{"id":1,"uid":"abc123","title":"Production Overview","uri":"db/production-overview",` +
|
||||
`"url":"/d/abc123/production-overview","slug":"","type":"dash-db","tags":["prod"],` +
|
||||
`"isStarred":false,"sortMeta":0}]`
|
||||
res := runGrafanaModule(t, grafana, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a grafana finding")
|
||||
}
|
||||
if v := grafanaExtract(res, "grafana_dashboard"); v != "Production Overview" {
|
||||
t.Errorf("grafana_dashboard=%q, want Production Overview", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a folder-only result is not flagged", func(t *testing.T) {
|
||||
body := `[{"id":1,"uid":"f","title":"General","uri":"db/general","url":"/dashboards/f/general",` +
|
||||
`"type":"dash-folder","isStarred":false}]`
|
||||
if res := runGrafanaModule(t, grafana, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a folder-only search should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a dash-db result without isStarred is not flagged", func(t *testing.T) {
|
||||
body := `[{"uid":"abc","title":"x","uri":"db/x","type":"dash-db"}]`
|
||||
if res := runGrafanaModule(t, grafana, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a partial search blob should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a login-required grafana is not flagged", func(t *testing.T) {
|
||||
body := `{"message":"Unauthorized"}`
|
||||
if res := runGrafanaModule(t, grafana, 401, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 grafana should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
if res := runGrafanaModule(t, grafana, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 body should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
if res := runGrafanaModule(t, grafana, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
const zkMonitorBody = `{"command":"monitor","error":null,"version":"3.9.2-a8d7f3e",` +
|
||||
`"server_state":"leader","avg_latency":0,"max_latency":12,"min_latency":0,` +
|
||||
`"packets_received":1532,"packets_sent":1531,"num_alive_connections":3,` +
|
||||
`"outstanding_requests":0,"znode_count":127,"watch_count":4,"ephemerals_count":2,` +
|
||||
`"approximate_data_size":45219,"open_file_descriptor_count":67,"max_file_descriptor_count":1048576}`
|
||||
|
||||
const nameNodeInfoBody = `{"beans":[{"name":"Hadoop:service=NameNode,name=NameNodeInfo",` +
|
||||
`"modelerType":"org.apache.hadoop.hdfs.server.namenode.FSNamesystem",` +
|
||||
`"SoftwareVersion":"3.3.6","Version":"3.3.6, rUNKNOWN","Total":2147483648,"Free":1073741824,` +
|
||||
`"Safemode":"","ClusterId":"CID-abc123","BlockPoolId":"BP-998-10.0.0.1-170",` +
|
||||
`"LiveNodes":"{\"dn1.hdfs.internal:9866\":{\"infoAddr\":\"10.0.0.2:9864\"}}",` +
|
||||
`"DeadNodes":"{}","DecomNodes":"{}","TotalBlocks":1024,"TotalFiles":512}]}`
|
||||
|
||||
func runHadoopZKModule(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 hadoopZKExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestHadoopZooKeeperExposureModules(t *testing.T) {
|
||||
const zk = "../../modules/recon/zookeeper-admin-exposure.yaml"
|
||||
const namenode = "../../modules/recon/hadoop-namenode-exposure.yaml"
|
||||
|
||||
t.Run("an open zookeeper monitor is flagged with the version", func(t *testing.T) {
|
||||
res := runHadoopZKModule(t, zk, 200, zkMonitorBody)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a zookeeper finding")
|
||||
}
|
||||
if v := hadoopZKExtract(res, "zookeeper_version"); v != "3.9.2-a8d7f3e" {
|
||||
t.Errorf("zookeeper_version=%q, want 3.9.2-a8d7f3e", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a different adminserver command is not flagged", func(t *testing.T) {
|
||||
body := `{"command":"configuration","error":null,"version":"3.9.2","clientPort":2181}`
|
||||
if res := runHadoopZKModule(t, zk, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-monitor command should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a firewalled or absent adminserver is not flagged", func(t *testing.T) {
|
||||
if res := runHadoopZKModule(t, zk, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match zookeeper, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a namenode jmx body does not match the zookeeper module", func(t *testing.T) {
|
||||
if res := runHadoopZKModule(t, zk, 200, nameNodeInfoBody); len(res.Findings) > 0 {
|
||||
t.Errorf("a namenode body should not match zookeeper, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open namenode jmx is flagged with the hdfs version", func(t *testing.T) {
|
||||
res := runHadoopZKModule(t, namenode, 200, nameNodeInfoBody)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a namenode finding")
|
||||
}
|
||||
if v := hadoopZKExtract(res, "hdfs_version"); v != "3.3.6" {
|
||||
t.Errorf("hdfs_version=%q, want 3.3.6", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a different hadoop jmx bean is not flagged", func(t *testing.T) {
|
||||
body := `{"beans":[{"name":"Hadoop:service=DataNode,name=DataNodeInfo",` +
|
||||
`"SoftwareVersion":"3.3.6","LiveNodes":"","DeadNodes":""}]}`
|
||||
if res := runHadoopZKModule(t, namenode, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-NameNodeInfo bean should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a kerberos-secured namenode is not flagged", func(t *testing.T) {
|
||||
if res := runHadoopZKModule(t, namenode, 401, "Authentication required"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 namenode should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a zookeeper monitor body does not match the namenode module", func(t *testing.T) {
|
||||
if res := runHadoopZKModule(t, namenode, 200, zkMonitorBody); len(res.Findings) > 0 {
|
||||
t.Errorf("a zookeeper body should not match namenode, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plain 200 bodies are not leaks", func(t *testing.T) {
|
||||
if res := runHadoopZKModule(t, zk, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 should not match zookeeper, got %d findings", len(res.Findings))
|
||||
}
|
||||
if res := runHadoopZKModule(t, namenode, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 should not match namenode, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runHTTPDBModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runImageGenModule(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 imageGenExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestImageGenExposureModules(t *testing.T) {
|
||||
const comfyui = "../../modules/recon/comfyui-api-exposure.yaml"
|
||||
const a1111 = "../../modules/recon/automatic1111-api-exposure.yaml"
|
||||
const fooocus = "../../modules/recon/fooocus-api-exposure.yaml"
|
||||
const iopaint = "../../modules/recon/iopaint-api-exposure.yaml"
|
||||
|
||||
comfyStats := `{"system":{"os":"posix","ram_total":67430219776,"ram_free":12345678,` +
|
||||
`"comfyui_version":"0.3.40","python_version":"3.11.9","pytorch_version":"2.3.1",` +
|
||||
`"embedded_python":false,"argv":["main.py"]},"devices":[{"name":"cuda:0 NVIDIA RTX 4090",` +
|
||||
`"type":"cuda","index":0,"vram_total":25757220864,"vram_free":24000000000,` +
|
||||
`"torch_vram_total":268435456,"torch_vram_free":260000000}]}`
|
||||
|
||||
a1111Models := `[{"title":"sd_xl_base_1.0.safetensors [31e35c80fc]","model_name":"sd_xl_base_1.0",` +
|
||||
`"hash":"31e35c80fc","sha256":"31e35c80fc4829d14f90153f4c74cd59c90b779f6afe05a74cd6120b893f7e5b",` +
|
||||
`"filename":"/home/sd/models/Stable-diffusion/sd_xl_base_1.0.safetensors","config":null}]`
|
||||
|
||||
fooocusModels := `{"model_filenames":["juggernautXL_v8.safetensors","sd_xl_base_1.0.safetensors"],` +
|
||||
`"lora_filenames":["sdxl_lcm_lora.safetensors"]}`
|
||||
|
||||
iopaintConfig := `{"plugins":[{"name":"RemoveBG","support_gen_image":true,"support_gen_mask":false}],` +
|
||||
`"modelInfos":[{"name":"lama","path":"lama","model_type":"inpaint"}],` +
|
||||
`"removeBGModel":"briaai/RMBG-1.4","removeBGModels":["briaai/RMBG-1.4","u2net"],` +
|
||||
`"realesrganModel":"realesr-general-x4v3","realesrganModels":["realesr-general-x4v3"],` +
|
||||
`"interactiveSegModel":"sam2_1_tiny","interactiveSegModels":["vit_b","sam2_1_tiny"],` +
|
||||
`"enableFileManager":true,"enableAutoSaving":false,"enableControlnet":false,` +
|
||||
`"controlnetMethod":null,"disableModelSwitch":false,"isDesktop":false,` +
|
||||
`"samplers":["DPM++ 2M","Euler","Euler a"]}`
|
||||
|
||||
t.Run("a comfyui system_stats is flagged with its version", func(t *testing.T) {
|
||||
res := runImageGenModule(t, comfyui, 200, comfyStats)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a comfyui finding")
|
||||
}
|
||||
if v := imageGenExtract(res, "comfyui_version"); v != "0.3.40" {
|
||||
t.Errorf("comfyui_version=%q, want 0.3.40", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a system_stats without comfyui keys is not flagged", func(t *testing.T) {
|
||||
body := `{"system":{"os":"linux","ram_total":123},"devices":[]}`
|
||||
if res := runImageGenModule(t, comfyui, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic system stats should not match comfyui, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a comfyui version without device memory is not flagged", func(t *testing.T) {
|
||||
body := `{"system":{"comfyui_version":"0.3.40"}}`
|
||||
if res := runImageGenModule(t, comfyui, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a version-only body should not match comfyui, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an automatic1111 sd-models list is flagged with its checkpoint", func(t *testing.T) {
|
||||
res := runImageGenModule(t, a1111, 200, a1111Models)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an automatic1111 finding")
|
||||
}
|
||||
if v := imageGenExtract(res, "sd_model_name"); v != "sd_xl_base_1.0" {
|
||||
t.Errorf("sd_model_name=%q, want sd_xl_base_1.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a list with a model_name but no filename is not flagged as a1111", func(t *testing.T) {
|
||||
body := `[{"title":"some entry","model_name":"thing"}]`
|
||||
if res := runImageGenModule(t, a1111, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a partial entry should not match automatic1111, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic titled list is not flagged as a1111", func(t *testing.T) {
|
||||
body := `[{"title":"My Blog Post","filename":"post.md","author":"someone"}]`
|
||||
if res := runImageGenModule(t, a1111, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic list should not match automatic1111, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a model_name and filename without a title is not flagged as a1111", func(t *testing.T) {
|
||||
body := `[{"model_name":"thing","filename":"/tmp/thing.bin"}]`
|
||||
if res := runImageGenModule(t, a1111, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a titleless entry should not match automatic1111, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a fooocus-api all-models is flagged with its checkpoint", func(t *testing.T) {
|
||||
res := runImageGenModule(t, fooocus, 200, fooocusModels)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a fooocus finding")
|
||||
}
|
||||
if v := imageGenExtract(res, "fooocus_model"); v != "juggernautXL_v8.safetensors" {
|
||||
t.Errorf("fooocus_model=%q, want juggernautXL_v8.safetensors", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without model_filenames is not flagged as fooocus", func(t *testing.T) {
|
||||
if res := runImageGenModule(t, fooocus, 200, `{"lora_filenames":["x.safetensors"]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without model_filenames should not match fooocus, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without lora_filenames is not flagged as fooocus", func(t *testing.T) {
|
||||
if res := runImageGenModule(t, fooocus, 200, `{"model_filenames":["x.safetensors"]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without lora_filenames should not match fooocus, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an iopaint server-config is flagged with its file-manager state", func(t *testing.T) {
|
||||
res := runImageGenModule(t, iopaint, 200, iopaintConfig)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an iopaint finding")
|
||||
}
|
||||
if v := imageGenExtract(res, "iopaint_file_manager"); v != "true" {
|
||||
t.Errorf("iopaint_file_manager=%q, want true", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a config without interactiveSegModel is not flagged as iopaint", func(t *testing.T) {
|
||||
body := `{"modelInfos":[],"enableFileManager":true,"disableModelSwitch":false,"samplers":[]}`
|
||||
if res := runImageGenModule(t, iopaint, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without interactiveSegModel should not match iopaint, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a config without enableFileManager is not flagged as iopaint", func(t *testing.T) {
|
||||
body := `{"interactiveSegModel":"vit_b","disableModelSwitch":false,"samplers":[]}`
|
||||
if res := runImageGenModule(t, iopaint, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without enableFileManager should not match iopaint, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a config without disableModelSwitch is not flagged as iopaint", func(t *testing.T) {
|
||||
body := `{"interactiveSegModel":"vit_b","enableFileManager":true,"samplers":[]}`
|
||||
if res := runImageGenModule(t, iopaint, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without disableModelSwitch should not match iopaint, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{comfyui, a1111, fooocus, iopaint} {
|
||||
if res := runImageGenModule(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{comfyui, a1111, fooocus, iopaint} {
|
||||
if res := runImageGenModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runInfraModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runInfraControlplaneModule(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 controlplaneExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestInfraControlplaneExposureModules(t *testing.T) {
|
||||
const traefik = "../../modules/recon/traefik-api-exposure.yaml"
|
||||
const nomad = "../../modules/recon/nomad-agent-exposure.yaml"
|
||||
const portainer = "../../modules/recon/portainer-status-exposure.yaml"
|
||||
|
||||
t.Run("a traefik overview is flagged with its first provider", func(t *testing.T) {
|
||||
body := `{"http":{"routers":{"total":12,"warnings":0,"errors":1},"services":{"total":8,"warnings":0,` +
|
||||
`"errors":0},"middlewares":{"total":5,"warnings":0,"errors":0}},"tcp":{"routers":{"total":0},` +
|
||||
`"services":{"total":0}},"udp":{"routers":{"total":0},"services":{"total":0}},` +
|
||||
`"features":{"tracing":"Noop","metrics":"Prometheus","accessLog":true},"providers":["Docker","File"]}`
|
||||
res := runInfraControlplaneModule(t, traefik, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a traefik finding")
|
||||
}
|
||||
if v := controlplaneExtract(res, "traefik_provider"); v != "Docker" {
|
||||
t.Errorf("traefik_provider=%q, want Docker", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a routing summary without features is not flagged as traefik", func(t *testing.T) {
|
||||
body := `{"http":{"routers":{"total":1}},"providers":["Docker"]}`
|
||||
if res := runInfraControlplaneModule(t, traefik, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without features should not match traefik, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open nomad agent self is flagged with its version", func(t *testing.T) {
|
||||
body := `{"config":{"Region":"global","Datacenter":"dc1","BindAddr":"0.0.0.0"},` +
|
||||
`"member":{"Name":"node1.global","Addr":"10.0.0.5","Port":4648,` +
|
||||
`"Tags":{"role":"nomad","region":"global","dc":"dc1","build":"1.7.2","vsn":"1"},"Status":"alive"},` +
|
||||
`"stats":{"nomad":{"server":"true"},"runtime":{"version":"go1.21"}}}`
|
||||
res := runInfraControlplaneModule(t, nomad, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a nomad finding")
|
||||
}
|
||||
if v := controlplaneExtract(res, "nomad_version"); v != "1.7.2" {
|
||||
t.Errorf("nomad_version=%q, want 1.7.2", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an acl-enabled nomad returns 403 and is not flagged", func(t *testing.T) {
|
||||
if res := runInfraControlplaneModule(t, nomad, 403, `{"errors":["Permission denied"]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a 403 from an acl-enabled nomad should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a config+stats body without member is not flagged as nomad", func(t *testing.T) {
|
||||
body := `{"config":{"a":1},"stats":{"b":2}}`
|
||||
if res := runInfraControlplaneModule(t, nomad, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without member should not match nomad, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a portainer status is flagged with its instance id", func(t *testing.T) {
|
||||
body := `{"Version":"2.19.4","InstanceID":"299ab403-70a8-4c05-92f7-bf7a994d50df"}`
|
||||
res := runInfraControlplaneModule(t, portainer, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a portainer finding")
|
||||
}
|
||||
if v := controlplaneExtract(res, "portainer_instance_id"); v != "299ab403-70a8-4c05-92f7-bf7a994d50df" {
|
||||
t.Errorf("portainer_instance_id=%q, want the uuid", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a bare version body is not flagged as portainer", func(t *testing.T) {
|
||||
if res := runInfraControlplaneModule(t, portainer, 200, `{"Version":"2.19.4"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare version should not match portainer, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{traefik, nomad, portainer} {
|
||||
if res := runInfraControlplaneModule(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{traefik, nomad, portainer} {
|
||||
if res := runInfraControlplaneModule(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,126 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runJobDashModule(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 jobDashExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestJobDashboardExposureModules(t *testing.T) {
|
||||
const sidekiq = "../../modules/recon/sidekiq-web-exposure.yaml"
|
||||
const flower = "../../modules/recon/celery-flower-exposure.yaml"
|
||||
const rq = "../../modules/recon/rq-dashboard-exposure.yaml"
|
||||
|
||||
t.Run("a sidekiq stats dump is flagged with its redis version", func(t *testing.T) {
|
||||
body := `{"sidekiq":{"processed":12345,"failed":67,"busy":3,"processes":2,"enqueued":10,` +
|
||||
`"scheduled":5,"retries":1,"dead":0,"default_latency":0},"redis":{"redis_version":"7.2.4",` +
|
||||
`"uptime_in_days":"12","connected_clients":"8","used_memory_human":"2.50M",` +
|
||||
`"used_memory_peak_human":"3.10M"},"server_utc_time":"18:00:00 UTC"}`
|
||||
res := runJobDashModule(t, sidekiq, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a sidekiq finding")
|
||||
}
|
||||
if v := jobDashExtract(res, "redis_version"); v != "7.2.4" {
|
||||
t.Errorf("redis_version=%q, want 7.2.4", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a bare redis-info body without default_latency is not flagged as sidekiq", func(t *testing.T) {
|
||||
if res := runJobDashModule(t, sidekiq, 200, `{"redis_version":"7.2.4","server_utc_time":"x"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a redis info blob should not match sidekiq, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a flower workers api is flagged with the celery version", func(t *testing.T) {
|
||||
body := `{"celery@worker1":{"active_queues":[{"name":"celery","exchange":{"name":"celery",` +
|
||||
`"type":"direct"},"routing_key":"celery"}],"conf":{"broker_url":"redis://localhost:6379/0",` +
|
||||
`"result_backend":"redis://localhost:6379/0"},"registered":["tasks.add","tasks.send_email"],` +
|
||||
`"stats":{"sw_ident":"py-celery","sw_ver":"5.3.6","sw_sys":"Linux","pool":{"max-concurrency":4},` +
|
||||
`"broker":{"hostname":"localhost","transport":"redis"}},"timestamp":1719345600.0}}`
|
||||
res := runJobDashModule(t, flower, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a flower finding")
|
||||
}
|
||||
if v := jobDashExtract(res, "celery_version"); v != "5.3.6" {
|
||||
t.Errorf("celery_version=%q, want 5.3.6", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a worker blob without conf is not flagged as flower", func(t *testing.T) {
|
||||
if res := runJobDashModule(t, flower, 200, `{"celery@w":{"active_queues":[],"registered":["tasks.add"]}}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a confless worker blob should not match flower, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an rq queues dump is flagged with the first queue name", func(t *testing.T) {
|
||||
body := `{"queues":[{"name":"default","count":42,"queued_url":"/0/view/jobs/default/queued/...",` +
|
||||
`"failed_job_registry_count":3,"failed_url":"...","started_job_registry_count":1,"started_url":"...",` +
|
||||
`"deferred_job_registry_count":0,"deferred_url":"...","finished_job_registry_count":100,` +
|
||||
`"finished_url":"...","canceled_job_registry_count":0,"canceled_url":"...",` +
|
||||
`"scheduled_job_registry_count":5,"scheduled_url":"..."}]}`
|
||||
res := runJobDashModule(t, rq, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an rq finding")
|
||||
}
|
||||
if v := jobDashExtract(res, "rq_queue_name"); v != "default" {
|
||||
t.Errorf("rq_queue_name=%q, want default", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a queues blob without the registry counts is not flagged as rq", func(t *testing.T) {
|
||||
if res := runJobDashModule(t, rq, 200, `{"queues":[{"name":"q","failed_job_registry_count":0}]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a partial queues blob should not match rq, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{sidekiq, flower, rq} {
|
||||
if res := runJobDashModule(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{sidekiq, flower, rq} {
|
||||
if res := runJobDashModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/vmfunc/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
func TestRunExtractorsJSON(t *testing.T) {
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
const kafkaUIClustersBody = `[{"name":"production","defaultCluster":true,"status":"online",` +
|
||||
`"brokerCount":3,"onlinePartitionCount":128,"topicCount":42,"bytesInPerSec":10485.5,` +
|
||||
`"bytesOutPerSec":20971.0,"readOnly":false,"version":"3.7.0",` +
|
||||
`"features":["TOPIC_DELETION","KAFKA_CONNECT","KSQL_DB"]}]`
|
||||
|
||||
const kafdropOverviewBody = `{"summary":{"topicCount":42,"partitionCount":128,` +
|
||||
`"underReplicatedCount":0,"preferredReplicaPercent":1.0,"brokerLeaderPartitionCount":{"1":43},` +
|
||||
`"brokerPreferredLeaderPartitionCount":{"1":43},"expectedBrokerIds":[1,2,3]},` +
|
||||
`"brokers":[{"id":1,"host":"kafka-0.svc.internal","port":9092,"controller":true,"rack":"r1"}],` +
|
||||
`"topics":[{"name":"orders","partitions":[]}]}`
|
||||
|
||||
func runKafkaModule(t *testing.T, file string, status int, body string) (*modules.Result, http.Header) {
|
||||
t.Helper()
|
||||
def, err := modules.ParseYAMLModule(file)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s: %v", file, err)
|
||||
}
|
||||
var gotHeaders http.Header
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotHeaders = r.Header.Clone()
|
||||
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, gotHeaders
|
||||
}
|
||||
|
||||
func kafkaExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestKafkaManagementUIExposureModules(t *testing.T) {
|
||||
const kafkaUI = "../../modules/recon/kafka-ui-exposure.yaml"
|
||||
const kafdrop = "../../modules/recon/kafdrop-exposure.yaml"
|
||||
|
||||
t.Run("an open kafka-ui /api/clusters is flagged with the version", func(t *testing.T) {
|
||||
res, _ := runKafkaModule(t, kafkaUI, 200, kafkaUIClustersBody)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a kafka-ui finding")
|
||||
}
|
||||
if v := kafkaExtract(res, "kafka_version"); v != "3.7.0" {
|
||||
t.Errorf("kafka_version=%q, want 3.7.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a kafka-ui cluster with a non-enum status is not flagged", func(t *testing.T) {
|
||||
body := `[{"name":"x","defaultCluster":true,"status":"degraded","brokerCount":1}]`
|
||||
if res, _ := runKafkaModule(t, kafkaUI, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-enum status should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a login-protected kafka-ui is not flagged", func(t *testing.T) {
|
||||
if res, _ := runKafkaModule(t, kafkaUI, 401, `{"message":"Unauthorized"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 kafka-ui should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a kafdrop overview does not match the kafka-ui module", func(t *testing.T) {
|
||||
if res, _ := runKafkaModule(t, kafkaUI, 200, kafdropOverviewBody); len(res.Findings) > 0 {
|
||||
t.Errorf("a kafdrop body should not match kafka-ui, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open kafdrop overview is flagged with a broker host", func(t *testing.T) {
|
||||
res, hdr := runKafkaModule(t, kafdrop, 200, kafdropOverviewBody)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a kafdrop finding")
|
||||
}
|
||||
if v := kafkaExtract(res, "kafka_broker"); v != "kafka-0.svc.internal" {
|
||||
t.Errorf("kafka_broker=%q, want kafka-0.svc.internal", v)
|
||||
}
|
||||
if got := hdr.Get("Accept"); got != "application/json" {
|
||||
t.Errorf("Accept header=%q, want application/json", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a cluster overview without preferredReplicaPercent is not flagged", func(t *testing.T) {
|
||||
body := `{"summary":{"topicCount":1},"brokers":[{"id":1,"host":"h"}],"topics":[]}`
|
||||
if res, _ := runKafkaModule(t, kafdrop, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a preferredReplicaPercent-less overview should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a basic-auth-protected kafdrop is not flagged", func(t *testing.T) {
|
||||
if res, _ := runKafkaModule(t, kafdrop, 401, "Unauthorized"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 kafdrop should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a kafka-ui cluster list does not match the kafdrop module", func(t *testing.T) {
|
||||
if res, _ := runKafkaModule(t, kafdrop, 200, kafkaUIClustersBody); len(res.Findings) > 0 {
|
||||
t.Errorf("a kafka-ui body should not match kafdrop, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plain 200 bodies are not leaks", func(t *testing.T) {
|
||||
if res, _ := runKafkaModule(t, kafkaUI, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 should not match kafka-ui, got %d findings", len(res.Findings))
|
||||
}
|
||||
if res, _ := runKafkaModule(t, kafdrop, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match kafdrop, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runGatewayModule(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 gatewayExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestLLMGatewayExposureModules(t *testing.T) {
|
||||
const oneapi = "../../modules/recon/oneapi-status-exposure.yaml"
|
||||
|
||||
oneapiStatus := `{"success":true,"message":"","data":{"version":"v0.8.4","start_time":1719100000,` +
|
||||
`"system_name":"New API","quota_per_unit":500000,"github_oauth":false}}`
|
||||
|
||||
t.Run("a oneapi status is flagged with its version", func(t *testing.T) {
|
||||
res := runGatewayModule(t, oneapi, 200, oneapiStatus)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a oneapi finding")
|
||||
}
|
||||
if v := gatewayExtract(res, "oneapi_version"); v != "v0.8.4" {
|
||||
t.Errorf("oneapi_version=%q, want v0.8.4", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without system_name is not flagged as oneapi", func(t *testing.T) {
|
||||
body := `{"data":{"start_time":1,"quota_per_unit":500000}}`
|
||||
if res := runGatewayModule(t, oneapi, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without system_name should not match oneapi, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without quota_per_unit is not flagged as oneapi", func(t *testing.T) {
|
||||
body := `{"data":{"system_name":"X","start_time":1}}`
|
||||
if res := runGatewayModule(t, oneapi, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without quota_per_unit should not match oneapi, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
if res := runGatewayModule(t, oneapi, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 body should not match oneapi, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
if res := runGatewayModule(t, oneapi, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match oneapi, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,161 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runGPUServingModule(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 gpuServingExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestLLMGPUServingExposureModules(t *testing.T) {
|
||||
const triton = "../../modules/recon/triton-api-exposure.yaml"
|
||||
const sglang = "../../modules/recon/sglang-api-exposure.yaml"
|
||||
const torchserve = "../../modules/recon/torchserve-api-exposure.yaml"
|
||||
|
||||
tritonMeta := `{"name":"triton","version":"2.45.0","extensions":["classification","sequence",` +
|
||||
`"model_repository","schedule_policy","model_configuration","statistics","trace","logging"]}`
|
||||
|
||||
sglangInfo := `{"model_path":"meta-llama/Llama-3-8B","tokenizer_path":"meta-llama/Llama-3-8B",` +
|
||||
`"is_generation":true,"preferred_sampling_params":null,"weight_version":"default",` +
|
||||
`"has_image_understanding":false,"has_audio_understanding":false,"model_type":"llama",` +
|
||||
`"architectures":["LlamaForCausalLM"]}`
|
||||
|
||||
torchserveModels := `{"nextPageToken":"4","models":[{"modelName":"resnet-18","modelUrl":"resnet-18.mar"},` +
|
||||
`{"modelName":"noop","modelUrl":"noop-v1.0"}]}`
|
||||
|
||||
t.Run("a triton metadata api is flagged with its version", func(t *testing.T) {
|
||||
res := runGPUServingModule(t, triton, 200, tritonMeta)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a triton finding")
|
||||
}
|
||||
if v := gpuServingExtract(res, "triton_version"); v != "2.45.0" {
|
||||
t.Errorf("triton_version=%q, want 2.45.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a sglang model_info is flagged with its model path", func(t *testing.T) {
|
||||
res := runGPUServingModule(t, sglang, 200, sglangInfo)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a sglang finding")
|
||||
}
|
||||
if v := gpuServingExtract(res, "sglang_model"); v != "meta-llama/Llama-3-8B" {
|
||||
t.Errorf("sglang_model=%q, want meta-llama/Llama-3-8B", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a torchserve models api is flagged with its model name", func(t *testing.T) {
|
||||
res := runGPUServingModule(t, torchserve, 200, torchserveModels)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a torchserve finding")
|
||||
}
|
||||
if v := gpuServingExtract(res, "torchserve_model"); v != "resnet-18" {
|
||||
t.Errorf("torchserve_model=%q, want resnet-18", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a hugging face config with model_type is not flagged as sglang", func(t *testing.T) {
|
||||
body := `{"model_type":"llama","architectures":["LlamaForCausalLM"],"hidden_size":4096}`
|
||||
if res := runGPUServingModule(t, sglang, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a model config should not match sglang, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generation flag alone is not flagged as sglang", func(t *testing.T) {
|
||||
body := `{"is_generation":true,"model":"x"}`
|
||||
if res := runGPUServingModule(t, sglang, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("is_generation alone should not match sglang, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an image understanding flag alone is not flagged as sglang", func(t *testing.T) {
|
||||
body := `{"has_image_understanding":false,"model_path":"x"}`
|
||||
if res := runGPUServingModule(t, sglang, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("has_image_understanding alone should not match sglang, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a model url without a page token is not flagged as torchserve", func(t *testing.T) {
|
||||
body := `{"models":[{"modelName":"x","modelUrl":"x.mar"}]}`
|
||||
if res := runGPUServingModule(t, torchserve, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("modelUrl without nextPageToken should not match torchserve, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an ollama style models list is not flagged as torchserve", func(t *testing.T) {
|
||||
body := `{"models":[{"name":"llama3:latest","model":"llama3:latest"}]}`
|
||||
if res := runGPUServingModule(t, torchserve, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an ollama model list should not match torchserve, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a paginated list without a model url is not flagged as torchserve", func(t *testing.T) {
|
||||
body := `{"nextPageToken":"4","items":[{"id":"a"}]}`
|
||||
if res := runGPUServingModule(t, torchserve, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic paginated list should not match torchserve, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a kserve server that is not triton is not flagged", func(t *testing.T) {
|
||||
body := `{"name":"my-model-server","version":"1.0","extensions":["model_repository"]}`
|
||||
if res := runGPUServingModule(t, triton, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-triton kserve server should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a triton name without extensions is not flagged", func(t *testing.T) {
|
||||
if res := runGPUServingModule(t, triton, 200, `{"name":"triton"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a name-only body should not match triton, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{triton, sglang, torchserve} {
|
||||
if res := runGPUServingModule(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{triton, sglang, torchserve} {
|
||||
if res := runGPUServingModule(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,125 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runInferenceExposureModule(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 inferenceExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestLLMInferenceExposureModules(t *testing.T) {
|
||||
const tgi = "../../modules/recon/tgi-api-exposure.yaml"
|
||||
const tei = "../../modules/recon/tei-api-exposure.yaml"
|
||||
|
||||
tgiInfo := `{"model_id":"meta-llama/Llama-2-7b-chat-hf","model_sha":"abc","model_pipeline_tag":"text-generation",` +
|
||||
`"max_concurrent_requests":128,"max_best_of":2,"max_input_tokens":4095,"max_total_tokens":4096,` +
|
||||
`"max_batch_total_tokens":16384,"router":"text-generation-router","version":"2.0.4","sha":"deadbeef"}`
|
||||
|
||||
teiInfo := `{"model_id":"BAAI/bge-large-en-v1.5","model_sha":"abc","model_dtype":"float16",` +
|
||||
`"model_type":{"embedding":{"pooling":"cls"}},"max_concurrent_requests":512,"max_input_length":512,` +
|
||||
`"max_batch_tokens":16384,"max_client_batch_size":32,"tokenization_workers":8,"version":"1.5.0"}`
|
||||
|
||||
t.Run("a tgi info is flagged with its model id", func(t *testing.T) {
|
||||
res := runInferenceExposureModule(t, tgi, 200, tgiInfo)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a tgi finding")
|
||||
}
|
||||
if v := inferenceExtract(res, "tgi_model"); v != "meta-llama/Llama-2-7b-chat-hf" {
|
||||
t.Errorf("tgi_model=%q, want meta-llama/Llama-2-7b-chat-hf", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tei info is flagged with its model id", func(t *testing.T) {
|
||||
res := runInferenceExposureModule(t, tei, 200, teiInfo)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a tei finding")
|
||||
}
|
||||
if v := inferenceExtract(res, "tei_model"); v != "BAAI/bge-large-en-v1.5" {
|
||||
t.Errorf("tei_model=%q, want BAAI/bge-large-en-v1.5", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tgi info is not flagged as tei", func(t *testing.T) {
|
||||
if res := runInferenceExposureModule(t, tei, 200, tgiInfo); len(res.Findings) > 0 {
|
||||
t.Errorf("a tgi info should not match the tei module, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tei info is not flagged as tgi", func(t *testing.T) {
|
||||
if res := runInferenceExposureModule(t, tgi, 200, teiInfo); len(res.Findings) > 0 {
|
||||
t.Errorf("a tei info should not match the tgi module, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a hugging face model config is not flagged as tei", func(t *testing.T) {
|
||||
body := `{"model_type":"bert","hidden_size":768,"num_attention_heads":12,"vocab_size":30522}`
|
||||
if res := runInferenceExposureModule(t, tei, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a model config.json should not match tei, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a batch-tokens body without model_type is not flagged as tei", func(t *testing.T) {
|
||||
body := `{"max_batch_tokens":16384,"max_concurrent_requests":512}`
|
||||
if res := runInferenceExposureModule(t, tei, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without model_type should not match tei, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a different router is not flagged as tgi", func(t *testing.T) {
|
||||
body := `{"router":"some-other-router","max_concurrent_requests":10}`
|
||||
if res := runInferenceExposureModule(t, tgi, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-tgi router 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{tgi, tei} {
|
||||
if res := runInferenceExposureModule(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{tgi, tei} {
|
||||
if res := runInferenceExposureModule(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,212 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runLocalRunnerModule(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 localRunnerExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestLLMLocalRunnerExposureModules(t *testing.T) {
|
||||
const ollama = "../../modules/recon/ollama-api-exposure.yaml"
|
||||
const koboldcpp = "../../modules/recon/koboldcpp-api-exposure.yaml"
|
||||
const tabby = "../../modules/recon/tabby-api-exposure.yaml"
|
||||
const oobabooga = "../../modules/recon/oobabooga-api-exposure.yaml"
|
||||
|
||||
ollamaTags := `{"models":[{"name":"deepseek-r1:latest","model":"deepseek-r1:latest",` +
|
||||
`"modified_at":"2025-05-10T08:06:48.639712648-07:00","size":4683075271,` +
|
||||
`"digest":"0a8c266910232fd3291e71e5ba1e058cc5af9d411192cf88b6d30e92b6e73163",` +
|
||||
`"details":{"parent_model":"","format":"gguf","family":"qwen2","families":["qwen2"],` +
|
||||
`"parameter_size":"7.6B","quantization_level":"Q4_K_M"}}]}`
|
||||
|
||||
koboldVersion := `{"result":"KoboldCpp","version":"1.71.1","protected":false,"llm":true,` +
|
||||
`"txt2img":true,"vision":false,"audio":false,"transcribe":false,"multiplayer":false,` +
|
||||
`"websearch":false,"tts":false,"embeddings":false,"music":false,"savedata":false,` +
|
||||
`"admin":0,"router":false,"guidance":false,"jinja":true,"mcp":false}`
|
||||
|
||||
tabbyHealth := `{"model":"TabbyML/StarCoder-1B","chat_model":"Qwen2.5-Coder-7B-Instruct",` +
|
||||
`"device":"cuda","cuda_devices":["NVIDIA GeForce RTX 4090"],"models":{"completion":{"vram":1234}},` +
|
||||
`"arch":"x86_64","cpu_info":"AMD Ryzen 9 5950X","cpu_count":32,` +
|
||||
`"version":{"build_date":"2024-06-01","build_timestamp":"2024-06-01T00:00:00Z",` +
|
||||
`"git_sha":"deadbeef","git_describe":"v0.13.0"},"webserver":true}`
|
||||
|
||||
oobaModelInfo := `{"model_name":"TheBloke_Llama-2-13B-chat-GPTQ","lora_names":["alpaca-lora"],` +
|
||||
`"loader":"ExLlamav2_HF"}`
|
||||
|
||||
t.Run("an ollama tags api is flagged with its model name", func(t *testing.T) {
|
||||
res := runLocalRunnerModule(t, ollama, 200, ollamaTags)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an ollama finding")
|
||||
}
|
||||
if v := localRunnerExtract(res, "ollama_model"); v != "deepseek-r1:latest" {
|
||||
t.Errorf("ollama_model=%q, want deepseek-r1:latest", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an ollama tags list without model details is not flagged", func(t *testing.T) {
|
||||
body := `{"models":[{"name":"llama3:latest","digest":"abc"}]}`
|
||||
if res := runLocalRunnerModule(t, ollama, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare model list should not match ollama, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an ollama version response is not flagged", func(t *testing.T) {
|
||||
if res := runLocalRunnerModule(t, ollama, 200, `{"version":"0.5.1"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a version response should not match ollama, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a koboldcpp version probe is flagged with its version", func(t *testing.T) {
|
||||
res := runLocalRunnerModule(t, koboldcpp, 200, koboldVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a koboldcpp finding")
|
||||
}
|
||||
if v := localRunnerExtract(res, "koboldcpp_version"); v != "1.71.1" {
|
||||
t.Errorf("koboldcpp_version=%q, want 1.71.1", v)
|
||||
}
|
||||
if v := localRunnerExtract(res, "koboldcpp_protected"); v != "false" {
|
||||
t.Errorf("koboldcpp_protected=%q, want false", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a koboldcpp result without txt2img is not flagged", func(t *testing.T) {
|
||||
body := `{"result":"KoboldCpp","protected":false}`
|
||||
if res := runLocalRunnerModule(t, koboldcpp, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a probe without txt2img should not match koboldcpp, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a koboldcpp result without protected is not flagged", func(t *testing.T) {
|
||||
body := `{"result":"KoboldCpp","txt2img":true}`
|
||||
if res := runLocalRunnerModule(t, koboldcpp, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a probe without protected should not match koboldcpp, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("another server reporting capabilities is not flagged as koboldcpp", func(t *testing.T) {
|
||||
body := `{"result":"SomeOtherServer","protected":false,"txt2img":true}`
|
||||
if res := runLocalRunnerModule(t, koboldcpp, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-kobold result should not match koboldcpp, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tabby health is flagged with its model", func(t *testing.T) {
|
||||
res := runLocalRunnerModule(t, tabby, 200, tabbyHealth)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a tabby finding")
|
||||
}
|
||||
if v := localRunnerExtract(res, "tabby_model"); v != "TabbyML/StarCoder-1B" {
|
||||
t.Errorf("tabby_model=%q, want TabbyML/StarCoder-1B", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tabby health without git_describe is not flagged", func(t *testing.T) {
|
||||
body := `{"cpu_info":"AMD Ryzen","cuda_devices":["RTX 4090"],"device":"cuda"}`
|
||||
if res := runLocalRunnerModule(t, tabby, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a health body without git_describe should not match tabby, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tabby health without cuda_devices is not flagged", func(t *testing.T) {
|
||||
body := `{"cpu_info":"AMD Ryzen","version":{"git_describe":"v0.13.0"}}`
|
||||
if res := runLocalRunnerModule(t, tabby, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a health body without cuda_devices should not match tabby, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a build info without cpu_info is not flagged as tabby", func(t *testing.T) {
|
||||
body := `{"cuda_devices":["RTX 4090"],"version":{"git_describe":"v0.13.0"}}`
|
||||
if res := runLocalRunnerModule(t, tabby, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without cpu_info should not match tabby, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an oobabooga model info is flagged with its model name", func(t *testing.T) {
|
||||
res := runLocalRunnerModule(t, oobabooga, 200, oobaModelInfo)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an oobabooga finding")
|
||||
}
|
||||
if v := localRunnerExtract(res, "oobabooga_model"); v != "TheBloke_Llama-2-13B-chat-GPTQ" {
|
||||
t.Errorf("oobabooga_model=%q, want TheBloke_Llama-2-13B-chat-GPTQ", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without lora_names is not flagged as oobabooga", func(t *testing.T) {
|
||||
body := `{"model_name":"some-model","loader":"Transformers"}`
|
||||
if res := runLocalRunnerModule(t, oobabooga, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without lora_names should not match oobabooga, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without model_name is not flagged as oobabooga", func(t *testing.T) {
|
||||
body := `{"lora_names":["x"],"loader":"Transformers"}`
|
||||
if res := runLocalRunnerModule(t, oobabooga, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without model_name should not match oobabooga, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a model_names plural list is not flagged as oobabooga", func(t *testing.T) {
|
||||
body := `{"model_names":["TheBloke_Llama-2-13B-chat-GPTQ","mistral-7b-instruct"]}`
|
||||
if res := runLocalRunnerModule(t, oobabooga, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a model_names plural list should not match oobabooga, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an idle oobabooga with no model loaded is still flagged", func(t *testing.T) {
|
||||
body := `{"model_name":"None","lora_names":[],"loader":"Transformers"}`
|
||||
if res := runLocalRunnerModule(t, oobabooga, 200, body); len(res.Findings) == 0 {
|
||||
t.Error("expected an idle oobabooga (model_name None) to still be flagged")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{ollama, koboldcpp, oobabooga, tabby} {
|
||||
if res := runLocalRunnerModule(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{ollama, koboldcpp, oobabooga, tabby} {
|
||||
if res := runLocalRunnerModule(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,225 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runOpenAICompatModule(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 openAICompatExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestLLMOpenAICompatExposureModules(t *testing.T) {
|
||||
const vllm = "../../modules/recon/vllm-api-exposure.yaml"
|
||||
const llamacpp = "../../modules/recon/llamacpp-api-exposure.yaml"
|
||||
const infinity = "../../modules/recon/infinity-embedding-api-exposure.yaml"
|
||||
const lmstudio = "../../modules/recon/lmstudio-api-exposure.yaml"
|
||||
|
||||
vllmModels := `{"object":"list","data":[{"id":"meta-llama/Llama-3.1-8B-Instruct","object":"model",` +
|
||||
`"created":1718900000,"owned_by":"vllm","root":"meta-llama/Llama-3.1-8B-Instruct",` +
|
||||
`"parent":null,"max_model_len":131072}]}`
|
||||
|
||||
llamacppModels := `{"object":"list","data":[{"id":"/models/llama-2-7b.Q4_K_M.gguf","object":"model",` +
|
||||
`"created":1718900000,"owned_by":"llamacpp"}]}`
|
||||
|
||||
infinityModels := `{"data":[{"id":"BAAI/bge-small-en-v1.5","stats":{"queue_fraction":0.0,` +
|
||||
`"queue_absolute":0,"results_pending":0,"results_queue":0},"object":"model","owned_by":"infinity",` +
|
||||
`"created":1718900000,"backend":"torch","capabilities":["embed"]}],"object":"list"}`
|
||||
|
||||
lmstudioModels := `{"object":"list","data":[{"id":"qwen2-vl-7b-instruct","object":"model",` +
|
||||
`"type":"vlm","publisher":"mlx-community","arch":"qwen2_vl","compatibility_type":"mlx",` +
|
||||
`"quantization":"4bit","state":"not-loaded","max_context_length":32768},` +
|
||||
`{"id":"text-embedding-nomic-embed-text-v1.5","object":"model","type":"embeddings",` +
|
||||
`"publisher":"nomic-ai","arch":"nomic-bert","compatibility_type":"gguf","quantization":"Q4_0",` +
|
||||
`"state":"not-loaded","max_context_length":2048}]}`
|
||||
|
||||
t.Run("a vllm models api is flagged with its model id", func(t *testing.T) {
|
||||
res := runOpenAICompatModule(t, vllm, 200, vllmModels)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a vllm finding")
|
||||
}
|
||||
if v := openAICompatExtract(res, "vllm_model"); v != "meta-llama/Llama-3.1-8B-Instruct" {
|
||||
t.Errorf("vllm_model=%q, want meta-llama/Llama-3.1-8B-Instruct", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a vllm-prefixed but distinct owned_by is not flagged as vllm", func(t *testing.T) {
|
||||
body := `{"object":"list","data":[{"id":"meta-llama/Llama-3.1-8B-Instruct","object":"model",` +
|
||||
`"created":1718900000,"owned_by":"vllm-frontend-rs","root":"meta-llama/Llama-3.1-8B-Instruct"}]}`
|
||||
if res := runOpenAICompatModule(t, vllm, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("owned_by vllm-frontend-rs should not match the anchored vllm regex, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a llamacpp models api is flagged with its model id", func(t *testing.T) {
|
||||
res := runOpenAICompatModule(t, llamacpp, 200, llamacppModels)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a llamacpp finding")
|
||||
}
|
||||
if v := openAICompatExtract(res, "llamacpp_model"); v != "/models/llama-2-7b.Q4_K_M.gguf" {
|
||||
t.Errorf("llamacpp_model=%q, want /models/llama-2-7b.Q4_K_M.gguf", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a llamacpp models api is not flagged as vllm", func(t *testing.T) {
|
||||
if res := runOpenAICompatModule(t, vllm, 200, llamacppModels); len(res.Findings) > 0 {
|
||||
t.Errorf("a llamacpp list should not match the vllm module, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a vllm models api is not flagged as llamacpp", func(t *testing.T) {
|
||||
if res := runOpenAICompatModule(t, llamacpp, 200, vllmModels); len(res.Findings) > 0 {
|
||||
t.Errorf("a vllm list should not match the llamacpp module, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an openai compatible list owned by openai is not flagged as vllm", func(t *testing.T) {
|
||||
body := `{"object":"list","data":[{"id":"gpt-4o","object":"model","owned_by":"openai"}]}`
|
||||
if res := runOpenAICompatModule(t, vllm, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an openai-owned list should not match vllm, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an infinity models list is flagged with its model id", func(t *testing.T) {
|
||||
res := runOpenAICompatModule(t, infinity, 200, infinityModels)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an infinity finding")
|
||||
}
|
||||
if v := openAICompatExtract(res, "infinity_model"); v != "BAAI/bge-small-en-v1.5" {
|
||||
t.Errorf("infinity_model=%q, want BAAI/bge-small-en-v1.5", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an infinity list without owned_by is not flagged", func(t *testing.T) {
|
||||
body := `{"data":[{"id":"x","backend":"torch","capabilities":["embed"]}],"object":"list"}`
|
||||
if res := runOpenAICompatModule(t, infinity, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a list without owned_by should not match infinity, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an infinity list without a backend is not flagged", func(t *testing.T) {
|
||||
body := `{"data":[{"id":"x","owned_by":"infinity","capabilities":["embed"]}],"object":"list"}`
|
||||
if res := runOpenAICompatModule(t, infinity, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a list without backend should not match infinity, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an infinity list without capabilities is not flagged", func(t *testing.T) {
|
||||
body := `{"data":[{"id":"x","owned_by":"infinity","backend":"torch"}],"object":"list"}`
|
||||
if res := runOpenAICompatModule(t, infinity, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a list without capabilities should not match infinity, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a non-infinity server with backend and capabilities is not flagged", func(t *testing.T) {
|
||||
body := `{"object":"list","data":[{"id":"m","object":"model","owned_by":"acme-org",` +
|
||||
`"backend":"vllm","capabilities":["chat"]}]}`
|
||||
if res := runOpenAICompatModule(t, infinity, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("backend+capabilities with a non-infinity owned_by should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a vllm models list is not flagged as infinity", func(t *testing.T) {
|
||||
if res := runOpenAICompatModule(t, infinity, 200, vllmModels); len(res.Findings) > 0 {
|
||||
t.Errorf("a vllm list should not match infinity, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an infinity list is not flagged as vllm", func(t *testing.T) {
|
||||
if res := runOpenAICompatModule(t, vllm, 200, infinityModels); len(res.Findings) > 0 {
|
||||
t.Errorf("an infinity list should not match the vllm module, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an lmstudio models api is flagged with its model id", func(t *testing.T) {
|
||||
res := runOpenAICompatModule(t, lmstudio, 200, lmstudioModels)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an lmstudio finding")
|
||||
}
|
||||
if v := openAICompatExtract(res, "lmstudio_model"); v != "qwen2-vl-7b-instruct" {
|
||||
t.Errorf("lmstudio_model=%q, want qwen2-vl-7b-instruct", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without compatibility_type is not flagged as lmstudio", func(t *testing.T) {
|
||||
body := `{"object":"list","data":[{"id":"x","quantization":"4bit","max_context_length":4096}]}`
|
||||
if res := runOpenAICompatModule(t, lmstudio, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without compatibility_type should not match lmstudio, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without quantization is not flagged as lmstudio", func(t *testing.T) {
|
||||
body := `{"object":"list","data":[{"id":"x","compatibility_type":"gguf","max_context_length":4096}]}`
|
||||
if res := runOpenAICompatModule(t, lmstudio, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without quantization should not match lmstudio, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without max_context_length is not flagged as lmstudio", func(t *testing.T) {
|
||||
body := `{"object":"list","data":[{"id":"x","compatibility_type":"gguf","quantization":"4bit"}]}`
|
||||
if res := runOpenAICompatModule(t, lmstudio, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without max_context_length should not match lmstudio, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an openai compatible v1 models list is not flagged as lmstudio", func(t *testing.T) {
|
||||
if res := runOpenAICompatModule(t, lmstudio, 200, vllmModels); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain v1 models list should not match lmstudio, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an lmstudio models list is not flagged as vllm", func(t *testing.T) {
|
||||
if res := runOpenAICompatModule(t, vllm, 200, lmstudioModels); len(res.Findings) > 0 {
|
||||
t.Errorf("an lmstudio list should not match the vllm module, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{vllm, llamacpp, lmstudio, infinity} {
|
||||
if res := runOpenAICompatModule(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{vllm, llamacpp, lmstudio, infinity} {
|
||||
if res := runOpenAICompatModule(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,201 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runRAGUIModule(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 ragUIExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestLLMRAGUIExposureModules(t *testing.T) {
|
||||
const fastgpt = "../../modules/recon/fastgpt-init-exposure.yaml"
|
||||
const perplexica = "../../modules/recon/perplexica-config-exposure.yaml"
|
||||
const onyx = "../../modules/recon/onyx-auth-exposure.yaml"
|
||||
const verba = "../../modules/recon/verba-health-exposure.yaml"
|
||||
|
||||
fastgptInit := `{"code":200,"data":{"bufferId":"unAuth_x","feConfigs":{"systemTitle":"FastGPT",` +
|
||||
`"docUrl":"https://doc.fastgpt.io","show_git":true},"modelProviders":[{"provider":"openai"}],` +
|
||||
`"aiproxyChannels":[]}}`
|
||||
|
||||
perplexicaConfig := `{"values":{"general":{"theme":"dark","measureUnit":"Metric","autoMediaSearch":true,` +
|
||||
`"showWeatherWidget":true},"modelProviders":[{"id":"openai","name":"OpenAI",` +
|
||||
`"config":{"apiKey":"sk-secret","baseURL":""},"chatModels":[],"embeddingModels":[]}],` +
|
||||
`"search":{"searxngURL":"http://searxng:8080"}},"fields":[{"key":"measureUnit"},{"key":"autoMediaSearch"},` +
|
||||
`{"key":"searxngURL"}]}`
|
||||
|
||||
onyxAuth := `{"auth_type":"basic","requires_verification":false,"anonymous_user_enabled":false,` +
|
||||
`"password_min_length":8,"has_users":true,"oauth_enabled":false}`
|
||||
|
||||
verbaHealth := `{"message":"Alive!","production":"Local","gtag":"",` +
|
||||
`"deployments":{"WEAVIATE_URL_VERBA":"https://my-cluster.weaviate.network",` +
|
||||
`"WEAVIATE_API_KEY_VERBA":"sk-weaviate-AbC123secret"},"default_deployment":"Weaviate"}`
|
||||
|
||||
t.Run("a fastgpt init is flagged with its system title", func(t *testing.T) {
|
||||
res := runRAGUIModule(t, fastgpt, 200, fastgptInit)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a fastgpt finding")
|
||||
}
|
||||
if v := ragUIExtract(res, "fastgpt_title"); v != "FastGPT" {
|
||||
t.Errorf("fastgpt_title=%q, want FastGPT", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without feConfigs is not flagged as fastgpt", func(t *testing.T) {
|
||||
body := `{"data":{"modelProviders":[],"aiproxyChannels":[]}}`
|
||||
if res := runRAGUIModule(t, fastgpt, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without feConfigs should not match fastgpt, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without aiproxyChannels is not flagged as fastgpt", func(t *testing.T) {
|
||||
body := `{"data":{"feConfigs":{},"modelProviders":[]}}`
|
||||
if res := runRAGUIModule(t, fastgpt, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without aiproxyChannels should not match fastgpt, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a perplexica config is flagged with its searxng url", func(t *testing.T) {
|
||||
res := runRAGUIModule(t, perplexica, 200, perplexicaConfig)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a perplexica finding")
|
||||
}
|
||||
if v := ragUIExtract(res, "perplexica_searxng"); v != "http://searxng:8080" {
|
||||
t.Errorf("perplexica_searxng=%q, want http://searxng:8080", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without searxngURL is not flagged as perplexica", func(t *testing.T) {
|
||||
body := `{"values":{"general":{"measureUnit":"Metric","autoMediaSearch":true}}}`
|
||||
if res := runRAGUIModule(t, perplexica, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without searxngURL should not match perplexica, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without autoMediaSearch is not flagged as perplexica", func(t *testing.T) {
|
||||
body := `{"values":{"general":{"measureUnit":"Metric"},"search":{"searxngURL":"http://x"}}}`
|
||||
if res := runRAGUIModule(t, perplexica, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without autoMediaSearch should not match perplexica, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an onyx auth type is flagged with its auth scheme", func(t *testing.T) {
|
||||
res := runRAGUIModule(t, onyx, 200, onyxAuth)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an onyx finding")
|
||||
}
|
||||
if v := ragUIExtract(res, "onyx_auth_type"); v != "basic" {
|
||||
t.Errorf("onyx_auth_type=%q, want basic", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without anonymous_user_enabled is not flagged as onyx", func(t *testing.T) {
|
||||
body := `{"auth_type":"basic","requires_verification":false,"password_min_length":8}`
|
||||
if res := runRAGUIModule(t, onyx, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without anonymous_user_enabled should not match onyx, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without password_min_length is not flagged as onyx", func(t *testing.T) {
|
||||
body := `{"auth_type":"basic","requires_verification":false,"anonymous_user_enabled":false}`
|
||||
if res := runRAGUIModule(t, onyx, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without password_min_length should not match onyx, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a verba health is flagged and leaks the weaviate url", func(t *testing.T) {
|
||||
res := runRAGUIModule(t, verba, 200, verbaHealth)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a verba finding")
|
||||
}
|
||||
if v := ragUIExtract(res, "verba_weaviate_url"); v != "https://my-cluster.weaviate.network" {
|
||||
t.Errorf("verba_weaviate_url=%q, want https://my-cluster.weaviate.network", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without WEAVIATE_API_KEY_VERBA is not flagged as verba", func(t *testing.T) {
|
||||
body := `{"message":"Alive!","deployments":{"WEAVIATE_URL_VERBA":"http://x"},"default_deployment":""}`
|
||||
if res := runRAGUIModule(t, verba, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without WEAVIATE_API_KEY_VERBA should not match verba, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without WEAVIATE_URL_VERBA is not flagged as verba", func(t *testing.T) {
|
||||
body := `{"message":"Alive!","deployments":{"WEAVIATE_API_KEY_VERBA":"k"},"default_deployment":""}`
|
||||
if res := runRAGUIModule(t, verba, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without WEAVIATE_URL_VERBA should not match verba, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without default_deployment is not flagged as verba", func(t *testing.T) {
|
||||
body := `{"message":"Alive!","deployments":{"WEAVIATE_URL_VERBA":"http://x","WEAVIATE_API_KEY_VERBA":"k"}}`
|
||||
if res := runRAGUIModule(t, verba, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without default_deployment should not match verba, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a verba health with an empty weaviate url is not flagged", func(t *testing.T) {
|
||||
body := `{"message":"Alive!","production":"Local","deployments":{"WEAVIATE_URL_VERBA":"",` +
|
||||
`"WEAVIATE_API_KEY_VERBA":""},"default_deployment":""}`
|
||||
if res := runRAGUIModule(t, verba, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an embedded verba that leaks no backend url should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic alive health is not flagged as verba", func(t *testing.T) {
|
||||
body := `{"message":"Alive!","status":"ok","uptime":1234}`
|
||||
if res := runRAGUIModule(t, verba, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic health should not match verba, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{perplexica, verba, onyx, fastgpt} {
|
||||
if res := runRAGUIModule(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{perplexica, verba, onyx, fastgpt} {
|
||||
if res := runRAGUIModule(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,204 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runUIExposureModule(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 uiExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestLLMUIExposureModules(t *testing.T) {
|
||||
const openWebUI = "../../modules/recon/open-webui-exposure.yaml"
|
||||
const librechat = "../../modules/recon/librechat-exposure.yaml"
|
||||
const nextchat = "../../modules/recon/nextchat-config-exposure.yaml"
|
||||
const anythingllm = "../../modules/recon/anythingllm-exposure.yaml"
|
||||
|
||||
openWebUIConfig := `{"status":true,"name":"Open WebUI","version":"0.6.15","default_locale":"",` +
|
||||
`"oauth":{"providers":{}},"features":{"auth":false,"auth_trusted_header":false,` +
|
||||
`"enable_ldap":false,"enable_signup":true,"enable_login_form":true,"enable_websocket":true}}`
|
||||
|
||||
librechatConfig := `{"appTitle":"LibreChat","serverDomain":"https://chat.example.com",` +
|
||||
`"emailLoginEnabled":true,"registrationEnabled":true,"socialLogins":["google","github"]}`
|
||||
|
||||
nextchatConfig := `{"needCode":false,"hideUserApiKey":false,"disableGPT4":false,` +
|
||||
`"hideBalanceQuery":true,"disableFastLink":false,"customModels":"","defaultModel":"gpt-4o-mini",` +
|
||||
`"visionModels":""}`
|
||||
|
||||
anythingllmSetup := `{"results":{"RequiresAuth":false,"MultiUserMode":false,"EmbeddingEngine":"native",` +
|
||||
`"VectorDB":"lancedb","LLMProvider":"openai","LLMModel":"gpt-4o","WhisperProvider":"local"}}`
|
||||
|
||||
t.Run("an open webui with auth disabled is flagged with its version", func(t *testing.T) {
|
||||
res := runUIExposureModule(t, openWebUI, 200, openWebUIConfig)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an open webui finding")
|
||||
}
|
||||
if v := uiExtract(res, "open_webui_version"); v != "0.6.15" {
|
||||
t.Errorf("open_webui_version=%q, want 0.6.15", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a rebranded open webui with auth disabled is still flagged", func(t *testing.T) {
|
||||
body := `{"status":true,"name":"Acme AI Portal","version":"0.6.15",` +
|
||||
`"features":{"auth":false,"auth_trusted_header":false,"enable_signup":true}}`
|
||||
res := runUIExposureModule(t, openWebUI, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a finding for a rebranded open webui")
|
||||
}
|
||||
if v := uiExtract(res, "open_webui_version"); v != "0.6.15" {
|
||||
t.Errorf("open_webui_version=%q, want 0.6.15", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a librechat with open registration is flagged with its title", func(t *testing.T) {
|
||||
res := runUIExposureModule(t, librechat, 200, librechatConfig)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a librechat finding")
|
||||
}
|
||||
if v := uiExtract(res, "librechat_title"); v != "LibreChat" {
|
||||
t.Errorf("librechat_title=%q, want LibreChat", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open webui with auth enabled is not flagged", func(t *testing.T) {
|
||||
body := `{"status":true,"name":"Open WebUI","version":"0.6.15",` +
|
||||
`"features":{"auth":true,"auth_trusted_header":false,"enable_signup":true}}`
|
||||
if res := runUIExposureModule(t, openWebUI, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an auth-enabled open webui should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open webui auth_trusted_header false does not satisfy the auth regex", func(t *testing.T) {
|
||||
body := `{"status":true,"name":"Open WebUI","version":"0.6.15",` +
|
||||
`"features":{"auth":true,"auth_trusted_header":false}}`
|
||||
if res := runUIExposureModule(t, openWebUI, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("auth_trusted_header false should not match the auth regex, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a librechat with registration disabled is not flagged", func(t *testing.T) {
|
||||
body := `{"appTitle":"LibreChat","emailLoginEnabled":true,"registrationEnabled":false,"socialLogins":[]}`
|
||||
if res := runUIExposureModule(t, librechat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a closed-registration librechat should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an unrelated app with open registration is not flagged as librechat", func(t *testing.T) {
|
||||
body := `{"name":"otherapp","registrationEnabled":true}`
|
||||
if res := runUIExposureModule(t, librechat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an unrelated app should not match librechat, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a nextchat config is flagged and reports its access-code gate", func(t *testing.T) {
|
||||
res := runUIExposureModule(t, nextchat, 200, nextchatConfig)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a nextchat finding")
|
||||
}
|
||||
if v := uiExtract(res, "nextchat_needcode"); v != "false" {
|
||||
t.Errorf("nextchat_needcode=%q, want false", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without needCode is not flagged as nextchat", func(t *testing.T) {
|
||||
body := `{"hideUserApiKey":false,"hideBalanceQuery":true}`
|
||||
if res := runUIExposureModule(t, nextchat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without needCode should not match nextchat, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without hideUserApiKey is not flagged as nextchat", func(t *testing.T) {
|
||||
body := `{"needCode":false,"hideBalanceQuery":true}`
|
||||
if res := runUIExposureModule(t, nextchat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without hideUserApiKey should not match nextchat, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without hideBalanceQuery is not flagged as nextchat", func(t *testing.T) {
|
||||
body := `{"needCode":false,"hideUserApiKey":false}`
|
||||
if res := runUIExposureModule(t, nextchat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without hideBalanceQuery should not match nextchat, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a code-gated nextchat is not flagged", func(t *testing.T) {
|
||||
body := `{"needCode":true,"hideUserApiKey":false,"hideBalanceQuery":true,"disableGPT4":false}`
|
||||
if res := runUIExposureModule(t, nextchat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a needCode:true nextchat is access-code gated and should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an anythingllm setup is flagged with its model", func(t *testing.T) {
|
||||
res := runUIExposureModule(t, anythingllm, 200, anythingllmSetup)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an anythingllm finding")
|
||||
}
|
||||
if v := uiExtract(res, "anythingllm_model"); v != "gpt-4o" {
|
||||
t.Errorf("anythingllm_model=%q, want gpt-4o", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without LLMProvider is not flagged as anythingllm", func(t *testing.T) {
|
||||
body := `{"results":{"VectorDB":"lancedb","EmbeddingEngine":"native"}}`
|
||||
if res := runUIExposureModule(t, anythingllm, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without LLMProvider should not match anythingllm, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without a VectorDB is not flagged as anythingllm", func(t *testing.T) {
|
||||
body := `{"results":{"LLMProvider":"openai","EmbeddingEngine":"native"}}`
|
||||
if res := runUIExposureModule(t, anythingllm, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without VectorDB should not match anythingllm, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{openWebUI, librechat, anythingllm, nextchat} {
|
||||
if res := runUIExposureModule(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{openWebUI, librechat, anythingllm, nextchat} {
|
||||
if res := runUIExposureModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"runtime"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/vmfunc/sif/internal/output"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// Loader handles module discovery and loading.
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runLoginModule(t *testing.T, file string, status int, headers map[string]string, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runMgmtModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/vmfunc/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
func TestCheckMatchersCondition(t *testing.T) {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runMetricsModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runMLPlatformModule(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 mlPlatformExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestMLPlatformExposureModules(t *testing.T) {
|
||||
const labelStudio = "../../modules/recon/label-studio-exposure.yaml"
|
||||
const cvat = "../../modules/recon/cvat-server-exposure.yaml"
|
||||
|
||||
labelStudioVersion := `{"release":"1.13.1","label-studio-os-package":{"version":"1.13.1",` +
|
||||
`"short_version":"1.13","description":"Label Studio"},"label-studio-os-backend":{"message":"release",` +
|
||||
`"commit":"abc1234","date":"2024-06-01"}}`
|
||||
|
||||
cvatAbout := `{"name":"Computer Vision Annotation Tool","description":"CVAT is a re-designed annotation tool",` +
|
||||
`"version":"2.20.0","logo_url":"http://host/static/logo.png","subtitle":""}`
|
||||
|
||||
t.Run("a label studio version api is flagged with its release", func(t *testing.T) {
|
||||
res := runMLPlatformModule(t, labelStudio, 200, labelStudioVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a label studio finding")
|
||||
}
|
||||
if v := mlPlatformExtract(res, "label_studio_release"); v != "1.13.1" {
|
||||
t.Errorf("label_studio_release=%q, want 1.13.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic release version is not flagged as label studio", func(t *testing.T) {
|
||||
body := `{"release":"1.0","name":"some-app"}`
|
||||
if res := runMLPlatformModule(t, labelStudio, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic release should not match label studio, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a label studio package without the backend key is not flagged", func(t *testing.T) {
|
||||
body := `{"release":"1.13.1","label-studio-os-package":{"version":"1.13.1"}}`
|
||||
if res := runMLPlatformModule(t, labelStudio, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a package-only body should not match label studio, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a label studio backend without the package key is not flagged", func(t *testing.T) {
|
||||
body := `{"release":"1.13.1","label-studio-os-backend":{"commit":"abc"}}`
|
||||
if res := runMLPlatformModule(t, labelStudio, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a backend-only body should not match label studio, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a cvat about is flagged with its version", func(t *testing.T) {
|
||||
res := runMLPlatformModule(t, cvat, 200, cvatAbout)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a cvat finding")
|
||||
}
|
||||
if v := mlPlatformExtract(res, "cvat_version"); v != "2.20.0" {
|
||||
t.Errorf("cvat_version=%q, want 2.20.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("another annotation tool is not flagged as cvat", func(t *testing.T) {
|
||||
body := `{"name":"Some Other Tool","version":"1.0","logo_url":"http://x/l.png"}`
|
||||
if res := runMLPlatformModule(t, cvat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-cvat name should not match cvat, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an html page mentioning cvat is not flagged", func(t *testing.T) {
|
||||
body := `<html><body><h1>Computer Vision Annotation Tool</h1> version 2.20.0 logo_url</body></html>`
|
||||
if res := runMLPlatformModule(t, cvat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("prose mentioning cvat should not match the structured response, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a cvat about without a logo_url is not flagged", func(t *testing.T) {
|
||||
body := `{"name":"Computer Vision Annotation Tool","version":"2.20.0"}`
|
||||
if res := runMLPlatformModule(t, cvat, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without logo_url should not match cvat, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{cvat, labelStudio} {
|
||||
if res := runMLPlatformModule(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{cvat, labelStudio} {
|
||||
if res := runMLPlatformModule(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,161 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runMLPlatformServerModule(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 mlPlatformServerExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestMLPlatformServerExposureModules(t *testing.T) {
|
||||
const h2o = "../../modules/recon/h2o-cluster-exposure.yaml"
|
||||
const mindsdb = "../../modules/recon/mindsdb-api-exposure.yaml"
|
||||
const zenml = "../../modules/recon/zenml-server-exposure.yaml"
|
||||
|
||||
h2oCloud := `{"__meta":{"schema_name":"CloudV3"},"version":"3.46.0.6","branch_name":"rel-x",` +
|
||||
`"cloud_name":"H2O_started_from_python","cloud_size":1,"cloud_uptime_millis":123456,` +
|
||||
`"cloud_healthy":true,"build_too_old":false}`
|
||||
|
||||
mindsdbStatus := `{"environment":"local","mindsdb_version":"25.13.1",` +
|
||||
`"auth":{"confirmed":false,"required":false,"provider":"disabled"}}`
|
||||
|
||||
zenmlInfo := `{"id":"abc","version":"0.70.0","deployment_type":"docker","database_type":"sqlite",` +
|
||||
`"secrets_store_type":"sql","auth_scheme":"OAUTH2_PASSWORD_BEARER","analytics_enabled":true}`
|
||||
|
||||
t.Run("an h2o cloud is flagged with its version", func(t *testing.T) {
|
||||
res := runMLPlatformServerModule(t, h2o, 200, h2oCloud)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an h2o finding")
|
||||
}
|
||||
if v := mlPlatformServerExtract(res, "h2o_version"); v != "3.46.0.6" {
|
||||
t.Errorf("h2o_version=%q, want 3.46.0.6", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without cloud_name is not flagged as h2o", func(t *testing.T) {
|
||||
body := `{"cloud_uptime_millis":1,"build_too_old":false,"version":"3.46"}`
|
||||
if res := runMLPlatformServerModule(t, h2o, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without cloud_name should not match h2o, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without build_too_old is not flagged as h2o", func(t *testing.T) {
|
||||
body := `{"cloud_name":"x","cloud_uptime_millis":1}`
|
||||
if res := runMLPlatformServerModule(t, h2o, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without build_too_old should not match h2o, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic cloud status is not flagged as h2o", func(t *testing.T) {
|
||||
body := `{"cloud_name":"x","build_too_old":false}`
|
||||
if res := runMLPlatformServerModule(t, h2o, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without cloud_uptime_millis should not match h2o, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a mindsdb status is flagged with its version", func(t *testing.T) {
|
||||
res := runMLPlatformServerModule(t, mindsdb, 200, mindsdbStatus)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a mindsdb finding")
|
||||
}
|
||||
if v := mlPlatformServerExtract(res, "mindsdb_version"); v != "25.13.1" {
|
||||
t.Errorf("mindsdb_version=%q, want 25.13.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without mindsdb_version is not flagged", func(t *testing.T) {
|
||||
body := `{"environment":"local","auth":{"provider":"disabled"}}`
|
||||
if res := runMLPlatformServerModule(t, mindsdb, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without mindsdb_version should not match mindsdb, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without an auth provider is not flagged as mindsdb", func(t *testing.T) {
|
||||
body := `{"environment":"local","mindsdb_version":"25.13.1","auth":{"required":false}}`
|
||||
if res := runMLPlatformServerModule(t, mindsdb, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without provider should not match mindsdb, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a zenml info is flagged with its version", func(t *testing.T) {
|
||||
res := runMLPlatformServerModule(t, zenml, 200, zenmlInfo)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a zenml finding")
|
||||
}
|
||||
if v := mlPlatformServerExtract(res, "zenml_version"); v != "0.70.0" {
|
||||
t.Errorf("zenml_version=%q, want 0.70.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without deployment_type is not flagged as zenml", func(t *testing.T) {
|
||||
body := `{"secrets_store_type":"sql","auth_scheme":"OAUTH2","version":"0.70.0"}`
|
||||
if res := runMLPlatformServerModule(t, zenml, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without deployment_type should not match zenml, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without secrets_store_type is not flagged as zenml", func(t *testing.T) {
|
||||
body := `{"deployment_type":"docker","auth_scheme":"OAUTH2"}`
|
||||
if res := runMLPlatformServerModule(t, zenml, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without secrets_store_type should not match zenml, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without auth_scheme is not flagged as zenml", func(t *testing.T) {
|
||||
body := `{"deployment_type":"docker","secrets_store_type":"sql"}`
|
||||
if res := runMLPlatformServerModule(t, zenml, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without auth_scheme should not match zenml, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{h2o, mindsdb, zenml} {
|
||||
if res := runMLPlatformServerModule(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{h2o, mindsdb, zenml} {
|
||||
if res := runMLPlatformServerModule(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,223 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runTrackingModule(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 trackingExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestMLTrackingExposureModules(t *testing.T) {
|
||||
const mlflow = "../../modules/recon/mlflow-api-exposure.yaml"
|
||||
const tensorboard = "../../modules/recon/tensorboard-exposure.yaml"
|
||||
const aim = "../../modules/recon/aim-exposure.yaml"
|
||||
const determined = "../../modules/recon/determined-master-exposure.yaml"
|
||||
|
||||
mlflowExperiment := `{"experiment":{"experiment_id":"0","name":"Default",` +
|
||||
`"artifact_location":"file:///mlflow/mlruns/0","lifecycle_stage":"active",` +
|
||||
`"creation_time":1700000000000,"last_update_time":1700000000000,"tags":[]}}`
|
||||
|
||||
tensorboardEnv := `{"data_location":"/home/ml/runs/exp-2024","window_title":"",` +
|
||||
`"experiment_name":"","experiment_description":"","creation_time":0,"version":"2.16.2"}`
|
||||
|
||||
aimProject := `{"name":"my-aim-repo","path":"/home/ml/.aim","description":"",` +
|
||||
`"telemetry_enabled":0,"warn_index":false,"warn_runs":false}`
|
||||
|
||||
determinedMaster := `{"version":"0.27.1","master_id":"6f1f2a9c","cluster_id":"a1b2c3d4-e5f6-7890",` +
|
||||
`"cluster_name":"prod-cluster","telemetry_enabled":true,"rbac_enabled":false,` +
|
||||
`"strict_job_queue_control":false,"has_custom_logo":false,"branding":"determined"}`
|
||||
|
||||
t.Run("an mlflow experiment is flagged with its artifact store", func(t *testing.T) {
|
||||
res := runTrackingModule(t, mlflow, 200, mlflowExperiment)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an mlflow finding")
|
||||
}
|
||||
if v := trackingExtract(res, "mlflow_artifact_location"); v != "file:///mlflow/mlruns/0" {
|
||||
t.Errorf("mlflow_artifact_location=%q, want file:///mlflow/mlruns/0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an experiment_id without lifecycle_stage is not flagged as mlflow", func(t *testing.T) {
|
||||
body := `{"experiment_id":"5","artifact_location":"s3://bucket/x"}`
|
||||
if res := runTrackingModule(t, mlflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a partial body should not match mlflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an experiment without an artifact_location is not flagged as mlflow", func(t *testing.T) {
|
||||
body := `{"experiment":{"experiment_id":"0","name":"Default","lifecycle_stage":"active"}}`
|
||||
if res := runTrackingModule(t, mlflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an artifactless experiment should not match mlflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an experiment without an experiment_id is not flagged as mlflow", func(t *testing.T) {
|
||||
body := `{"experiment":{"name":"Default","artifact_location":"file:///x","lifecycle_stage":"active"}}`
|
||||
if res := runTrackingModule(t, mlflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an idless experiment should not match mlflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic lifecycle body is not flagged as mlflow", func(t *testing.T) {
|
||||
body := `{"lifecycle_stage":"production","name":"some-service"}`
|
||||
if res := runTrackingModule(t, mlflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic lifecycle body should not match mlflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a model checkpoint list is not flagged as mlflow", func(t *testing.T) {
|
||||
body := `[{"title":"x","model_name":"y","filename":"z"}]`
|
||||
if res := runTrackingModule(t, mlflow, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a model list should not match mlflow, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tensorboard environment is flagged with its version and run path", func(t *testing.T) {
|
||||
res := runTrackingModule(t, tensorboard, 200, tensorboardEnv)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a tensorboard finding")
|
||||
}
|
||||
if v := trackingExtract(res, "tensorboard_version"); v != "2.16.2" {
|
||||
t.Errorf("tensorboard_version=%q, want 2.16.2", v)
|
||||
}
|
||||
if v := trackingExtract(res, "tensorboard_data_location"); v != "/home/ml/runs/exp-2024" {
|
||||
t.Errorf("tensorboard_data_location=%q, want /home/ml/runs/exp-2024", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without data_location is not flagged as tensorboard", func(t *testing.T) {
|
||||
body := `{"window_title":"","version":"2.16.2"}`
|
||||
if res := runTrackingModule(t, tensorboard, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without data_location should not match tensorboard, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without window_title is not flagged as tensorboard", func(t *testing.T) {
|
||||
body := `{"data_location":"/runs","version":"2.16.2"}`
|
||||
if res := runTrackingModule(t, tensorboard, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without window_title should not match tensorboard, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without a version is not flagged as tensorboard", func(t *testing.T) {
|
||||
body := `{"data_location":"/runs","window_title":"my board"}`
|
||||
if res := runTrackingModule(t, tensorboard, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without version should not match tensorboard, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an aim project is flagged with its repo path", func(t *testing.T) {
|
||||
res := runTrackingModule(t, aim, 200, aimProject)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an aim finding")
|
||||
}
|
||||
if v := trackingExtract(res, "aim_project_path"); v != "/home/ml/.aim" {
|
||||
t.Errorf("aim_project_path=%q, want /home/ml/.aim", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without telemetry_enabled is not flagged as aim", func(t *testing.T) {
|
||||
body := `{"name":"x","path":"/y","warn_index":false,"warn_runs":false}`
|
||||
if res := runTrackingModule(t, aim, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without telemetry_enabled should not match aim, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without warn_index is not flagged as aim", func(t *testing.T) {
|
||||
body := `{"telemetry_enabled":0,"warn_runs":false,"name":"x"}`
|
||||
if res := runTrackingModule(t, aim, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without warn_index should not match aim, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without warn_runs is not flagged as aim", func(t *testing.T) {
|
||||
body := `{"telemetry_enabled":0,"warn_index":false,"name":"x"}`
|
||||
if res := runTrackingModule(t, aim, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without warn_runs should not match aim, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a determined master is flagged with its version and cluster id", func(t *testing.T) {
|
||||
res := runTrackingModule(t, determined, 200, determinedMaster)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a determined finding")
|
||||
}
|
||||
if v := trackingExtract(res, "determined_version"); v != "0.27.1" {
|
||||
t.Errorf("determined_version=%q, want 0.27.1", v)
|
||||
}
|
||||
if v := trackingExtract(res, "determined_cluster_id"); v != "a1b2c3d4-e5f6-7890" {
|
||||
t.Errorf("determined_cluster_id=%q, want a1b2c3d4-e5f6-7890", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a cluster info without a master_id is not flagged as determined", func(t *testing.T) {
|
||||
body := `{"cluster_id":"x","cluster_name":"y","version":"1.0"}`
|
||||
if res := runTrackingModule(t, determined, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a masterless cluster info should not match determined, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a master without a cluster_id is not flagged as determined", func(t *testing.T) {
|
||||
body := `{"master_id":"x","cluster_name":"y","version":"1.0"}`
|
||||
if res := runTrackingModule(t, determined, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without cluster_id should not match determined, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a master without a cluster_name is not flagged as determined", func(t *testing.T) {
|
||||
body := `{"master_id":"x","cluster_id":"y","version":"1.0"}`
|
||||
if res := runTrackingModule(t, determined, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without cluster_name should not match determined, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{mlflow, tensorboard, aim, determined} {
|
||||
if res := runTrackingModule(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{mlflow, tensorboard, aim, determined} {
|
||||
if res := runTrackingModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -93,7 +93,7 @@ type Matcher struct {
|
||||
Status []int `yaml:"status,omitempty"`
|
||||
Size []int `yaml:"size,omitempty"`
|
||||
Hash []int64 `yaml:"hash,omitempty"` // favicon: shodan mmh3 hashes (signed or unsigned)
|
||||
Condition string `yaml:"condition"` // and, or
|
||||
Condition string `yaml:"condition"` // and, or
|
||||
Negative bool `yaml:"negative"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runN8nModule(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 n8nExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestN8nSettingsExposureModule(t *testing.T) {
|
||||
const n8n = "../../modules/recon/n8n-settings-exposure.yaml"
|
||||
|
||||
t.Run("an n8n settings response is flagged with the version", func(t *testing.T) {
|
||||
body := `{"data":{"endpointWebhook":"webhook","endpointWebhookTest":"webhook-test",` +
|
||||
`"urlBaseWebhook":"https://n8n.example.com/","urlBaseEditor":"https://n8n.example.com/",` +
|
||||
`"versionCli":"1.45.1","releaseChannel":"stable","instanceId":"abc123def","n8nMetadata":{},` +
|
||||
`"userManagement":{"showSetupOnFirstLoad":false,"smtpSetup":true,"authenticationMethod":"email"}}}`
|
||||
res := runN8nModule(t, n8n, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an n8n finding")
|
||||
}
|
||||
if v := n8nExtract(res, "n8n_version"); v != "1.45.1" {
|
||||
t.Errorf("n8n_version=%q, want 1.45.1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a settings blob without instanceId is not flagged", func(t *testing.T) {
|
||||
if res := runN8nModule(t, n8n, 200, `{"data":{"endpointWebhook":"webhook","versionCli":"1.45.1"}}`); len(res.Findings) > 0 {
|
||||
t.Errorf("an instanceless settings blob should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a settings blob without endpointWebhook is not flagged", func(t *testing.T) {
|
||||
if res := runN8nModule(t, n8n, 200, `{"data":{"versionCli":"1.45.1","instanceId":"abc"}}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a webhookless settings blob should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
if res := runN8nModule(t, n8n, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 body should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
if res := runN8nModule(t, n8n, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runNodeRedModule(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 noderedExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestNodeREDFlowExposureModule(t *testing.T) {
|
||||
const nodered = "../../modules/recon/nodered-flow-exposure.yaml"
|
||||
|
||||
t.Run("an open node-red flows export is flagged with its tab label", func(t *testing.T) {
|
||||
body := `[{"id":"396c2376.c693dc","type":"tab","label":"Sheet 1"},` +
|
||||
`{"id":"a1","type":"inject","z":"396c2376.c693dc","wires":[["b2"]]},` +
|
||||
`{"id":"b2","type":"function","z":"396c2376.c693dc","func":"return msg;","wires":[[]]}]`
|
||||
res := runNodeRedModule(t, nodered, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a node-red finding")
|
||||
}
|
||||
if v := noderedExtract(res, "nodered_flow_label"); v != "Sheet 1" {
|
||||
t.Errorf("nodered_flow_label=%q, want Sheet 1", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an adminAuth node-red returns 401 and is not flagged", func(t *testing.T) {
|
||||
if res := runNodeRedModule(t, nodered, 401, `{"message":"Unauthorized"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 from a secured node-red should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a tabs-only flow without wires is not flagged", func(t *testing.T) {
|
||||
if res := runNodeRedModule(t, nodered, 200, `[{"id":"x","type":"tab","label":"Home"}]`); len(res.Findings) > 0 {
|
||||
t.Errorf("a tabs-only flow should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a wired graph without a tab is not flagged as node-red", func(t *testing.T) {
|
||||
if res := runNodeRedModule(t, nodered, 200, `[{"id":"x","type":"section","wires":[["y"]]}]`); len(res.Findings) > 0 {
|
||||
t.Errorf("a wired graph without a tab should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
if res := runNodeRedModule(t, nodered, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 body should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a 404 is not a leak", func(t *testing.T) {
|
||||
if res := runNodeRedModule(t, nodered, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runObservabilityModule(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 observExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestObservabilityExposureModules(t *testing.T) {
|
||||
const loki = "../../modules/recon/loki-api-exposure.yaml"
|
||||
const jaeger = "../../modules/recon/jaeger-query-exposure.yaml"
|
||||
const zipkin = "../../modules/recon/zipkin-exposure.yaml"
|
||||
|
||||
t.Run("an open loki labels response is flagged with a label", func(t *testing.T) {
|
||||
body := `{"status":"success","data":["app","filename","job","namespace","pod"]}`
|
||||
res := runObservabilityModule(t, loki, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a loki finding")
|
||||
}
|
||||
if v := observExtract(res, "loki_label"); v != "app" {
|
||||
t.Errorf("loki_label=%q, want app", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open loki with no ingested labels is still flagged", func(t *testing.T) {
|
||||
if res := runObservabilityModule(t, loki, 200, `{"status":"success","data":[]}`); len(res.Findings) == 0 {
|
||||
t.Error("expected a loki finding for an empty-but-open instance")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a multi-tenant loki returns 401 and is not flagged", func(t *testing.T) {
|
||||
if res := runObservabilityModule(t, loki, 401, `no org id\n`); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 from a secured loki should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a non-loki success envelope is not flagged as loki", func(t *testing.T) {
|
||||
if res := runObservabilityModule(t, loki, 200, `{"ok":true,"items":[]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without the loki shape should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a jaeger service list is flagged with a service name", func(t *testing.T) {
|
||||
body := `{"data":["customer","driver","frontend","route"],"total":0,"limit":0,"offset":0,"errors":null}`
|
||||
res := runObservabilityModule(t, jaeger, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a jaeger finding")
|
||||
}
|
||||
if v := observExtract(res, "jaeger_service"); v != "customer" {
|
||||
t.Errorf("jaeger_service=%q, want customer", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic pagination envelope without errors is not flagged as jaeger", func(t *testing.T) {
|
||||
body := `{"data":["a","b"],"total":2,"limit":10,"offset":0}`
|
||||
if res := runObservabilityModule(t, jaeger, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an envelope without errors should not match jaeger, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a bare data array is not flagged as jaeger", func(t *testing.T) {
|
||||
if res := runObservabilityModule(t, jaeger, 200, `{"data":["x"]}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare data array should not match jaeger, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a zipkin config is flagged with its environment", func(t *testing.T) {
|
||||
body := `{"environment":"prod","queryLimit":10,"defaultLookback":900000,"searchEnabled":true,` +
|
||||
`"dependency":{"enabled":true,"lowErrorRate":0.5,"highErrorRate":0.75}}`
|
||||
res := runObservabilityModule(t, zipkin, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a zipkin finding")
|
||||
}
|
||||
if v := observExtract(res, "zipkin_environment"); v != "prod" {
|
||||
t.Errorf("zipkin_environment=%q, want prod", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a config with searchEnabled alone is not flagged as zipkin", func(t *testing.T) {
|
||||
if res := runObservabilityModule(t, zipkin, 200, `{"searchEnabled":true,"foo":1}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a partial config should not match zipkin, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{loki, jaeger, zipkin} {
|
||||
if res := runObservabilityModule(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{loki, jaeger, zipkin} {
|
||||
if res := runObservabilityModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
// runOpsModule runs a shipped module end to end against a server that returns
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runOrchModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runRailsModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runRegistryModule(t *testing.T, file string, status int, headers map[string]string, body string) *modules.Result {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runRuntimeModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runSecretModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runServerAdminModule(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 serverAdminExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestServerAdminExposureModules(t *testing.T) {
|
||||
const caddy = "../../modules/recon/caddy-admin-exposure.yaml"
|
||||
const envoy = "../../modules/recon/envoy-admin-exposure.yaml"
|
||||
|
||||
t.Run("a caddy config dump is flagged with a handler", func(t *testing.T) {
|
||||
body := `{"apps":{"http":{"servers":{"srv0":{"listen":[":443"],"routes":[{"match":[{"host":` +
|
||||
`["example.com"]}],"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"localhost:8080"}]}]}]}}},` +
|
||||
`"tls":{"automation":{"policies":[{"issuers":[{"module":"acme"}]}]}}},"admin":{"listen":"0.0.0.0:2019"}}`
|
||||
res := runServerAdminModule(t, caddy, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a caddy finding")
|
||||
}
|
||||
if v := serverAdminExtract(res, "caddy_handler"); v != "reverse_proxy" {
|
||||
t.Errorf("caddy_handler=%q, want reverse_proxy", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an apps block without servers or handler is not flagged as caddy", func(t *testing.T) {
|
||||
if res := runServerAdminModule(t, caddy, 200, `{"apps":{"tls":{"automation":{}}}}`); len(res.Findings) > 0 {
|
||||
t.Errorf("an apps-only tls block should not match caddy, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an envoy server_info is flagged with its version", func(t *testing.T) {
|
||||
body := `{"version":"1.28.0/abcd/Clean/RELEASE/BoringSSL","state":"LIVE","uptime_current_epoch":"3600s",` +
|
||||
`"uptime_all_epochs":"3600s","hot_restart_version":"11.104","command_line_options":{"base_id":"0",` +
|
||||
`"concurrency":4,"config_path":"/etc/envoy/envoy.yaml"},"node":{"id":"node-1","cluster":"prod"}}`
|
||||
res := runServerAdminModule(t, envoy, 200, body)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an envoy finding")
|
||||
}
|
||||
if v := serverAdminExtract(res, "envoy_version"); v != "1.28.0/abcd/Clean/RELEASE/BoringSSL" {
|
||||
t.Errorf("envoy_version=%q, want the build string", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a bare version+state body is not flagged as envoy", func(t *testing.T) {
|
||||
if res := runServerAdminModule(t, envoy, 200, `{"version":"1.0","state":"LIVE"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a bare version+state should not match envoy, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{caddy, envoy} {
|
||||
if res := runServerAdminModule(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{caddy, envoy} {
|
||||
if res := runServerAdminModule(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,150 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
const eurekaAppsXML = `<applications><versions__delta>1</versions__delta>` +
|
||||
`<apps__hashcode>UP_5_</apps__hashcode><application><name>PAYMENT-SERVICE</name>` +
|
||||
`<instance><instanceId>10.0.0.5:payment-service:8443</instanceId>` +
|
||||
`<hostName>payment-1.svc.internal</hostName><app>PAYMENT-SERVICE</app>` +
|
||||
`<ipAddr>10.0.0.5</ipAddr><status>UP</status><port enabled="true">8443</port>` +
|
||||
`<vipAddress>payment-service</vipAddress></instance></application></applications>`
|
||||
|
||||
const eurekaAppsJSON = `{"applications":{"versions__delta":"1","apps__hashcode":"UP_5_",` +
|
||||
`"application":[{"name":"PAYMENT-SERVICE","instance":[{"instanceId":"p1",` +
|
||||
`"hostName":"payment-1.svc.internal","ipAddr":"10.0.0.5","status":"UP",` +
|
||||
`"vipAddress":"payment-service"}]}]}}`
|
||||
|
||||
const sbaInstancesJSON = `[{"id":"a1b2c3","version":12,"registration":{"name":"order-service",` +
|
||||
`"managementUrl":"http://order-1.internal:8080/actuator",` +
|
||||
`"healthUrl":"http://order-1.internal:8080/actuator/health",` +
|
||||
`"serviceUrl":"http://order-1.internal:8080/","source":"http-api","metadata":{}},` +
|
||||
`"registered":true,"statusInfo":{"status":"UP","timestamp":"2026-06-25T20:00:00Z","details":{}},` +
|
||||
`"buildVersion":"1.0.0","tags":{}}]`
|
||||
|
||||
func runServiceRegistryModule(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 svcRegExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestServiceRegistryExposureModules(t *testing.T) {
|
||||
const eureka = "../../modules/recon/eureka-registry-exposure.yaml"
|
||||
const sba = "../../modules/recon/spring-boot-admin-exposure.yaml"
|
||||
|
||||
t.Run("an open eureka registry (xml) is flagged with the instance ip", func(t *testing.T) {
|
||||
res := runServiceRegistryModule(t, eureka, 200, eurekaAppsXML)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a eureka finding")
|
||||
}
|
||||
if v := svcRegExtract(res, "eureka_instance_ip"); v != "10.0.0.5" {
|
||||
t.Errorf("eureka_instance_ip=%q, want 10.0.0.5", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open eureka registry (json) is also flagged with the instance ip", func(t *testing.T) {
|
||||
res := runServiceRegistryModule(t, eureka, 200, eurekaAppsJSON)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a eureka finding for the json form")
|
||||
}
|
||||
if v := svcRegExtract(res, "eureka_instance_ip"); v != "10.0.0.5" {
|
||||
t.Errorf("eureka_instance_ip=%q, want 10.0.0.5", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an envelope without apps__hashcode is not flagged", func(t *testing.T) {
|
||||
body := `<applications><versions__delta>1</versions__delta></applications>`
|
||||
if res := runServiceRegistryModule(t, eureka, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an apps__hashcode-less envelope should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a page that merely mentions applications is not flagged", func(t *testing.T) {
|
||||
body := `<html><body>Our applications support multiple versions of the eureka client.</body></html>`
|
||||
if res := runServiceRegistryModule(t, eureka, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a prose page should not match eureka, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a secured eureka is not flagged", func(t *testing.T) {
|
||||
if res := runServiceRegistryModule(t, eureka, 401, "Unauthorized"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 eureka should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a spring boot admin body does not match the eureka module", func(t *testing.T) {
|
||||
if res := runServiceRegistryModule(t, eureka, 200, sbaInstancesJSON); len(res.Findings) > 0 {
|
||||
t.Errorf("an sba body should not match eureka, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an open spring boot admin is flagged with the health url", func(t *testing.T) {
|
||||
res := runServiceRegistryModule(t, sba, 200, sbaInstancesJSON)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an sba finding")
|
||||
}
|
||||
if v := svcRegExtract(res, "sba_health_url"); v != "http://order-1.internal:8080/actuator/health" {
|
||||
t.Errorf("sba_health_url=%q, want the internal actuator health url", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a registration without statusInfo is not flagged", func(t *testing.T) {
|
||||
body := `[{"registration":{"name":"x","healthUrl":"http://h:8080/health"}}]`
|
||||
if res := runServiceRegistryModule(t, sba, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a statusInfo-less body should not match sba, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a secured spring boot admin is not flagged", func(t *testing.T) {
|
||||
if res := runServiceRegistryModule(t, sba, 401, `{"error":"Unauthorized"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a 401 sba should not match, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a eureka body does not match the spring boot admin module", func(t *testing.T) {
|
||||
if res := runServiceRegistryModule(t, sba, 200, eurekaAppsJSON); len(res.Findings) > 0 {
|
||||
t.Errorf("a eureka body should not match sba, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plain 200 bodies are not leaks", func(t *testing.T) {
|
||||
if res := runServiceRegistryModule(t, eureka, 200, "ok"); len(res.Findings) > 0 {
|
||||
t.Errorf("a plain 200 should not match eureka, got %d findings", len(res.Findings))
|
||||
}
|
||||
if res := runServiceRegistryModule(t, sba, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("a 404 should not match sba, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runSpeechModule(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 speechExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestSpeechAudioExposureModules(t *testing.T) {
|
||||
const speaches = "../../modules/recon/speaches-api-exposure.yaml"
|
||||
const xtts = "../../modules/recon/xtts-api-server-exposure.yaml"
|
||||
|
||||
speachesSTT := `{"data":[{"id":"Systran/faster-whisper-small","created":0,"object":"model",` +
|
||||
`"owned_by":"Systran","language":["en"],"task":"automatic-speech-recognition"}],"object":"list"}`
|
||||
|
||||
speachesTTS := `{"data":[{"id":"speaches-ai/Kokoro-82M-v1.0-ONNX","created":0,"object":"model",` +
|
||||
`"owned_by":"speaches-ai","language":["en"],"task":"text-to-speech"}],"object":"list"}`
|
||||
|
||||
xttsFolders := `{"speaker_folder":"/app/speakers","output_folder":"/app/output",` +
|
||||
`"model_folder":"/app/xtts_models"}`
|
||||
|
||||
t.Run("a speaches stt model list is flagged with its model id", func(t *testing.T) {
|
||||
res := runSpeechModule(t, speaches, 200, speachesSTT)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a speaches finding")
|
||||
}
|
||||
if v := speechExtract(res, "speaches_model"); v != "Systran/faster-whisper-small" {
|
||||
t.Errorf("speaches_model=%q, want Systran/faster-whisper-small", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a speaches tts-only model list is flagged", func(t *testing.T) {
|
||||
if res := runSpeechModule(t, speaches, 200, speachesTTS); len(res.Findings) == 0 {
|
||||
t.Error("expected a speaches finding for a text-to-speech model list")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic openai-compatible model list is not flagged as speaches", func(t *testing.T) {
|
||||
body := `{"data":[{"id":"meta-llama/Llama-3","created":1234,"object":"model","owned_by":"vllm"}],"object":"list"}`
|
||||
if res := runSpeechModule(t, speaches, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a taskless model list should not match speaches, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a model list with a non-speech task is not flagged as speaches", func(t *testing.T) {
|
||||
body := `{"data":[{"id":"bert-base","object":"model","owned_by":"hf","task":"fill-mask"}],"object":"list"}`
|
||||
if res := runSpeechModule(t, speaches, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a non-speech task should not match speaches, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an empty speaches model list is not flagged", func(t *testing.T) {
|
||||
// A fresh speaches server with no models downloaded returns an empty data
|
||||
// array with nothing speaches-specific to anchor on; this known miss is
|
||||
// preferable to firing on every empty OpenAI-shaped list.
|
||||
body := `{"data":[],"object":"list"}`
|
||||
if res := runSpeechModule(t, speaches, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("an empty model list should not match speaches, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an xtts get-folders is flagged with its model folder", func(t *testing.T) {
|
||||
res := runSpeechModule(t, xtts, 200, xttsFolders)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected an xtts finding")
|
||||
}
|
||||
if v := speechExtract(res, "xtts_model_folder"); v != "/app/xtts_models" {
|
||||
t.Errorf("xtts_model_folder=%q, want /app/xtts_models", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without speaker_folder is not flagged as xtts", func(t *testing.T) {
|
||||
body := `{"output_folder":"/app/output","model_folder":"/app/xtts_models"}`
|
||||
if res := runSpeechModule(t, xtts, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without speaker_folder should not match xtts, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without model_folder is not flagged as xtts", func(t *testing.T) {
|
||||
body := `{"speaker_folder":"/app/speakers","output_folder":"/app/output"}`
|
||||
if res := runSpeechModule(t, xtts, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without model_folder should not match xtts, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without output_folder is not flagged as xtts", func(t *testing.T) {
|
||||
body := `{"speaker_folder":"/app/speakers","model_folder":"/app/xtts_models"}`
|
||||
if res := runSpeechModule(t, xtts, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without output_folder should not match xtts, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{speaches, xtts} {
|
||||
if res := runSpeechModule(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{speaches, xtts} {
|
||||
if res := runSpeechModule(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,7 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runVCSModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runVectorDBModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
package modules_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runVectorSearchModule(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 vectorSearchExtract(res *modules.Result, key string) string {
|
||||
for _, f := range res.Findings {
|
||||
if v := f.Extracted[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestVectorSearchExposureModules(t *testing.T) {
|
||||
const marqo = "../../modules/recon/marqo-exposure.yaml"
|
||||
const vespa = "../../modules/recon/vespa-status-exposure.yaml"
|
||||
const meilisearch = "../../modules/recon/meilisearch-exposure.yaml"
|
||||
|
||||
marqoRoot := `{"message":"Welcome to Marqo","version":"2.11.0"}`
|
||||
|
||||
vespaStatus := `{"application":{"vespa":{"version":"8.43.64"},"meta":{"name":"default","generation":11}},` +
|
||||
`"abstractComponents":[],"handlers":[],"clients":[],"servers":[],"httpRequestFilters":[],` +
|
||||
`"httpResponseFilters":[],"processingChains":[]}`
|
||||
|
||||
meiliVersion := `{"commitSha":"b46889b5","commitDate":"2026-01-15T00:00:00Z","pkgVersion":"1.12.0"}`
|
||||
|
||||
t.Run("a marqo root is flagged with its version", func(t *testing.T) {
|
||||
res := runVectorSearchModule(t, marqo, 200, marqoRoot)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a marqo finding")
|
||||
}
|
||||
if v := vectorSearchExtract(res, "marqo_version"); v != "2.11.0" {
|
||||
t.Errorf("marqo_version=%q, want 2.11.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a generic root with a version is not flagged as marqo", func(t *testing.T) {
|
||||
body := `{"message":"Welcome","version":"2.11.0","service":"something-else"}`
|
||||
if res := runVectorSearchModule(t, marqo, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a generic welcome should not match marqo, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a marqo welcome without a version is not flagged", func(t *testing.T) {
|
||||
if res := runVectorSearchModule(t, marqo, 200, `{"message":"Welcome to Marqo"}`); len(res.Findings) > 0 {
|
||||
t.Errorf("a versionless welcome should not match marqo, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("an html page mentioning marqo is not flagged", func(t *testing.T) {
|
||||
body := `<html><body><h1>Welcome to Marqo</h1><p>version 2.11.0 docs</p></body></html>`
|
||||
if res := runVectorSearchModule(t, marqo, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("prose mentioning marqo should not match the structured response, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a vespa status is flagged with its version", func(t *testing.T) {
|
||||
res := runVectorSearchModule(t, vespa, 200, vespaStatus)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a vespa finding")
|
||||
}
|
||||
if v := vectorSearchExtract(res, "vespa_version"); v != "8.43.64" {
|
||||
t.Errorf("vespa_version=%q, want 8.43.64", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without abstractComponents is not flagged as vespa", func(t *testing.T) {
|
||||
body := `{"handlers":[],"processingChains":[],"httpRequestFilters":[]}`
|
||||
if res := runVectorSearchModule(t, vespa, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without abstractComponents should not match vespa, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without processingChains is not flagged as vespa", func(t *testing.T) {
|
||||
body := `{"abstractComponents":[],"httpRequestFilters":[]}`
|
||||
if res := runVectorSearchModule(t, vespa, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without processingChains should not match vespa, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a meilisearch version is flagged", func(t *testing.T) {
|
||||
res := runVectorSearchModule(t, meilisearch, 200, meiliVersion)
|
||||
if len(res.Findings) == 0 {
|
||||
t.Fatal("expected a meilisearch finding")
|
||||
}
|
||||
if v := vectorSearchExtract(res, "meilisearch_version"); v != "1.12.0" {
|
||||
t.Errorf("meilisearch_version=%q, want 1.12.0", v)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without commitSha is not flagged as meilisearch", func(t *testing.T) {
|
||||
body := `{"commitDate":"2026","pkgVersion":"1.12.0"}`
|
||||
if res := runVectorSearchModule(t, meilisearch, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without commitSha should not match meilisearch, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a body without pkgVersion is not flagged as meilisearch", func(t *testing.T) {
|
||||
body := `{"commitSha":"abc","commitDate":"2026"}`
|
||||
if res := runVectorSearchModule(t, meilisearch, 200, body); len(res.Findings) > 0 {
|
||||
t.Errorf("a body without pkgVersion should not match meilisearch, got %d findings", len(res.Findings))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("a plain 200 body is not a leak", func(t *testing.T) {
|
||||
for _, file := range []string{marqo, vespa, meilisearch} {
|
||||
if res := runVectorSearchModule(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{marqo, vespa, meilisearch} {
|
||||
if res := runVectorSearchModule(t, file, 404, "not found"); len(res.Findings) > 0 {
|
||||
t.Errorf("%s: a 404 should not match, got %d findings", file, len(res.Findings))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
)
|
||||
|
||||
func runWebSrvModule(t *testing.T, file string, status int, body string) *modules.Result {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/vmfunc/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// discordProvider posts to a discord webhook. discord's incoming-webhook body
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/vmfunc/sif/internal/finding"
|
||||
"github.com/vmfunc/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
)
|
||||
|
||||
// contentTypeJSON is the body type every provider POSTs; all four speak json.
|
||||
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/finding"
|
||||
"github.com/vmfunc/sif/internal/httpx"
|
||||
"github.com/vmfunc/sif/internal/output"
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/httpx"
|
||||
"github.com/dropalldatabases/sif/internal/output"
|
||||
)
|
||||
|
||||
// Options carries the runtime knobs Send needs. Timeout bounds each provider's
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/vmfunc/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// sampleFindings returns a small mixed-severity batch for payload assertions.
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/vmfunc/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// slackProvider posts to a slack incoming webhook. the webhook url already pins
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/vmfunc/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// telegramAPIBase is the bot api root. it's a var so tests can repoint it at an
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/vmfunc/sif/internal/finding"
|
||||
"github.com/dropalldatabases/sif/internal/finding"
|
||||
)
|
||||
|
||||
// webhookProvider posts a structured json payload to an arbitrary endpoint. unlike
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
package format
|
||||
|
||||
import (
|
||||
"github.com/dropalldatabases/sif/internal/styles"
|
||||
nucleiout "github.com/projectdiscovery/nuclei/v3/pkg/output"
|
||||
"github.com/vmfunc/sif/internal/styles"
|
||||
)
|
||||
|
||||
func FormatLine(event *nucleiout.ResultEvent) string {
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/vmfunc/sif/internal/scan/frameworks"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan/frameworks"
|
||||
)
|
||||
|
||||
type FrameworksModule struct{}
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/vmfunc/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
type NucleiModule struct{}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
package builtin
|
||||
|
||||
import "github.com/vmfunc/sif/internal/modules"
|
||||
import "github.com/dropalldatabases/sif/internal/modules"
|
||||
|
||||
// Register registers all Go-based built-in scans as modules.
|
||||
// Allows complex Go scans to participate in the module system
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/vmfunc/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
type SecurityTrailsModule struct{}
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/vmfunc/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
type ShodanModule struct{}
|
||||
|
||||
@@ -15,8 +15,8 @@ package builtin
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/vmfunc/sif/internal/modules"
|
||||
"github.com/vmfunc/sif/internal/scan"
|
||||
"github.com/dropalldatabases/sif/internal/modules"
|
||||
"github.com/dropalldatabases/sif/internal/scan"
|
||||
)
|
||||
|
||||
type WhoisModule struct{}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user