Compare commits

..

190 Commits

Author SHA1 Message Date
vmfunc 1d2bc64dbc ci(release): hoist build ldflags into one env var
the 7 cross-compile steps each repeated the same ldflags string, easy to
drift; write it once in the extract-version step and reference $LDFLAGS
2026-06-09 16:03:56 -07:00
celeste 83ac92a4b8 Merge pull request #113 from vmfunc/fix/frameworks-confidence
fix(frameworks): require a real signature match + fix cve version matching
2026-06-09 15:02:40 -07:00
celeste 7f0e4cd128 Merge pull request #112 from vmfunc/test/e2e-integration
test: hermetic e2e integration suite
2026-06-09 14:50:54 -07:00
vmfunc 29d94e5352 fix(frameworks): require a real signature match, fix cve version matching
- recenter the detection confidence (sigmoid centered at 0.3) so a single weak
  signature match no longer clears the 0.5 threshold. before, sigmoid(0) was 0.5
  so *any* match counted as a detection - that's the magento-on-a-plain-page
  false positive from the live run. real detections match ~50%+ of signature
  weight, so the existing detector tests are unaffected
- getVulnerabilities matched affected versions with a raw string prefix, so "4.2"
  also matched "4.20"; match only on dotted boundaries now
- break confidence ties on name so the picked framework is deterministic
- add regression tests for the confidence floor and the version boundary
2026-06-09 14:46:10 -07:00
celeste 05fa35d945 Merge pull request #111 from vmfunc/feat/version
feat: stamp and surface the build version
2026-06-09 14:34:25 -07:00
vmfunc ce3075ad91 test: hermetic e2e integration suite
- make the four wordlist base urls (dirlist/dnslist/git/ports) package vars
  instead of consts so tests can repoint them at a local fixture; the default
  values are byte-for-byte unchanged
- add internal/scan/integration_test.go behind a //go:build integration tag: it
  stands up a local "vulnerable app" httptest server with planted artifacts and
  runs git/dirlist/cms/headers/sql/lfi/ports against it, asserting real findings
- go.yml runs them via `go test -tags=integration`; the default test run is
  untouched (the tag keeps them out)
- document the integration run in docs/development.md
2026-06-09 14:32:26 -07:00
vmfunc 661480a56d feat: stamp and surface the build version
- add internal/version: resolve from the release ldflag, else the go build
  info (module tag / vcs revision), else "dev"
- show the version on the boot banner and for `sif version`
- Makefile now stamps `make` builds via git describe (matching the release ci),
  so local/go-install builds report a real version instead of "dev"
- patchnotes.ShowOnce skips pseudo/dev versions so non-release builds dont make
  a doomed github call
- document sif version / sif patchnote / SIF_NO_PATCHNOTES in the readme + usage
2026-06-09 14:18:28 -07:00
celeste 76e8893ee2 Merge pull request #110 from vmfunc/fix/supabase-timeout
fix(js): give supabase requests a real timeout
2026-06-09 14:11:53 -07:00
celeste 1231ca3179 Merge pull request #109 from vmfunc/refactor/url-helper
refactor: dedupe url scheme stripping
2026-06-09 14:11:49 -07:00
vmfunc eb33321102 fix(js): give supabase requests a real timeout
doSupabaseRequest and the signup call used a bare http.Client{} with no
timeout, so a slow supabase project could hang the whole js scan. thread the
scan's --timeout down through ScanSupabase into every supabase request.
2026-06-09 13:56:01 -07:00
vmfunc 133224c348 refactor: dedupe url scheme stripping
`strings.Split(url, "://")[1]` was copy-pasted in 18 spots and panics on a
schemeless target (index out of range). add a small stripScheme helper in the
scan package - and a guarded equivalent in logger, which cant import scan - so
a bare host degrades gracefully instead of crashing the scan.
2026-06-09 13:51:04 -07:00
celeste 4c650e23e3 Merge pull request #108 from vmfunc/feat/manpage
feat: ship a man page
2026-06-09 13:47:08 -07:00
celeste 75e953cda7 Merge pull request #107 from vmfunc/feat/patchnotes
feat: show release notes via patch notes
2026-06-09 13:47:04 -07:00
vmfunc f7ef71e835 feat: ship a man page
adds man/sif.1 covering the targets/scans/options/modules flags, the
patchnote and version commands, env vars (incl SIF_NO_PATCHNOTES), and a
few examples. the install/uninstall make targets now drop it in
$(PREFIX)/share/man/man1.
2026-06-08 19:13:50 -07:00
vmfunc 5e10c1857b feat: show release notes via patch notes
- `sif patchnote` (also `-pn`) fetches the latest github release and renders
  its notes with glamour
- on the first run of a new version those notes are shown once, then recorded
  so they dont show again - best-effort, so dev builds, the SIF_NO_PATCHNOTES
  opt-out, and any network failure stay quiet
- wire up `var version` so the release `-X main.version` ldflag actually lands,
  and add `sif version`
2026-06-08 19:13:03 -07:00
celeste 3c070a621d Merge pull request #106 from vmfunc/feat/security-headers
feat: security-headers scan + scanner fixes and cleanup
2026-06-08 19:12:40 -07:00
vmfunc 94b99ade5a docs: fix broken -all example and document -sh
- the readme headline used -all, which isn't a real flag (goflags fatals
  on unknown flags), so the three -all examples now use actual flags
- document the new -sh security-header scan in the readme table, usage.md
  and scans.md, and fix the -headers section (it dumps headers; -sh grades
  them)
- bump the documented go version 1.23 -> 1.25 to match go.mod
2026-06-08 18:53:06 -07:00
vmfunc 9326465a46 chore: drop decorative emoji and redundant comments
strips the emoji from the subdomaintakeover/cloudstorage/supabase log
prefixes and trims comments that just restate the code (the
parameters/returns block on SubdomainTakeover, a couple of "this provides
type safety" notes) so the scanners read like whois.go/headers.go.
2026-06-08 18:53:06 -07:00
vmfunc 50c9933812 chore: drop the unused worker pool
internal/worker.Pool[T,R] (run/runwithfilter/foreach) was added with tests
but never wired into anything - the scanners all hand-roll their own
waitgroup loops. dead weight, so remove it.
2026-06-08 18:53:06 -07:00
vmfunc af0167859a fix: data races and a progress divide-by-zero
- git.go and dork.go appended to a shared results slice from every worker
  goroutine with no mutex - a real race that -race never caught since
  neither has a test. guard the appends like dirlist/dnslist already do.
- progress.go's non-tty milestone path divided by total with no guard, so
  a zero-total bar panicked when output was piped/redirected. bail early
  on total <= 0 to match the tty branch, and add an output test for it.
2026-06-08 18:53:06 -07:00
vmfunc 7efd62c804 feat: add security header analysis scan
adds a -sh/--security-headers scan that flags missing or weak response
headers (hsts, csp, x-frame-options, x-content-type-options,
referrer-policy, permissions-policy, coop) and headers that leak server
internals (server, x-powered-by, ...). hsts is only graded over https
where it actually applies. wired into App.Run and the module results.
2026-06-08 18:53:06 -07:00
celeste 1a1ff446d8 Merge pull request #105 from vmfunc/chore/copyright-2026
chore: bump copyright headers to 2026
2026-06-08 18:46:55 -07:00
vmfunc 648fa8d2c8 chore: bump copyright headers to 2026
rolls the (c) 2022-2025 banner to 2022-2026 across all go files, the
startup banner in sif.go, and the header-check workflow's expected
format. comment-only, nothing else changes.
2026-06-08 18:30:48 -07:00
celeste 8918be4797 Merge pull request #100 from vmfunc/cleanup/lint-exclusions-post-98
chore: resolve lint exclusions added in #98
2026-06-08 17:53:06 -07:00
vmfunc 4fc0df5a01 fix(templates): guard tar extraction against path traversal
The nuclei-templates tarball is fetched over the network and its entry
names flowed directly into os.Mkdir/os.Create, so a malicious or
compromised archive could write outside the extraction directory
("Zip Slip", CWE-22). Resolve each entry against the working directory
and reject any path that escapes it before touching the filesystem.

CodeQL flagged this as a high-severity alert on the lines this branch
already touched. gosec's G305 fires on filepath.Join with archive data
regardless of the traversal guard, so it's excluded with a note.
2026-06-08 17:35:05 -07:00
Claude ece5b2b0b0 chore: clean up lint exclusions deferred in #98
Address pre-existing code issues that were suppressed in #98 to keep that
PR scoped to the Go 1.25 / golangci-lint v2 toolchain bump.

https://claude.ai/code/session_01S433Zq3Xzm3ZethsqkyaZF
2026-06-08 16:56:49 -07:00
celeste 79f6b95eaf Merge pull request #101 from vmfunc/dependabot/go_modules/go_modules-d7145cbce0
chore(deps): bump github.com/go-git/go-git/v5 from 5.18.0 to 5.19.1 in the go_modules group across 1 directory
2026-06-07 11:05:53 -07:00
celeste fe2ab240a4 Merge pull request #103 from vmfunc/dependabot/github_actions/actions/dependency-review-action-5
chore(deps): bump actions/dependency-review-action from 4 to 5
2026-06-07 11:05:50 -07:00
dependabot[bot] 4c6cebf4de chore(deps): bump github.com/go-git/go-git/v5
Bumps the go_modules group with 1 update in the / directory: [github.com/go-git/go-git/v5](https://github.com/go-git/go-git).


Updates `github.com/go-git/go-git/v5` from 5.18.0 to 5.19.1
- [Release notes](https://github.com/go-git/go-git/releases)
- [Changelog](https://github.com/go-git/go-git/blob/main/HISTORY.md)
- [Commits](https://github.com/go-git/go-git/compare/v5.18.0...v5.19.1)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.19.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-07 17:48:20 +00:00
dependabot[bot] c7a244ed2f chore(deps): bump actions/dependency-review-action from 4 to 5
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4 to 5.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-07 17:47:57 +00:00
celeste 4d3b87a2cb chore: fix Discord link in README
Updated Discord invite link in README.md
2026-06-07 10:42:02 -07:00
celeste bf802a7c0b Merge pull request #99 from vmfunc/dependabot/go_modules/go_modules-c9a791322e
chore(deps): bump go.opentelemetry.io/otel from 1.38.0 to 1.41.0 in the go_modules group across 1 directory
2026-04-24 13:54:05 -07:00
dependabot[bot] c6143f7f39 chore(deps): bump go.opentelemetry.io/otel
Bumps the go_modules group with 1 update in the / directory: [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go).


Updates `go.opentelemetry.io/otel` from 1.38.0 to 1.41.0
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.38.0...v1.41.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.41.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-24 20:33:16 +00:00
celeste fbcf96d557 Merge pull request #96 from vmfunc/dependabot/go_modules/go_modules-c1d9254ec5
chore(deps): bump the go_modules group across 1 directory with 5 updates
2026-04-24 09:24:47 -07:00
celeste cecaa8aa3b Merge pull request #97 from vmfunc/dependabot/go_modules/github.com/projectdiscovery/nuclei/v3-3.8.0
chore(deps): bump github.com/projectdiscovery/nuclei/v3 from 3.7.1 to 3.8.0
2026-04-24 09:24:44 -07:00
dependabot[bot] 571711ff6b chore(deps): bump github.com/projectdiscovery/nuclei/v3
Bumps [github.com/projectdiscovery/nuclei/v3](https://github.com/projectdiscovery/nuclei) from 3.7.1 to 3.8.0.
- [Release notes](https://github.com/projectdiscovery/nuclei/releases)
- [Commits](https://github.com/projectdiscovery/nuclei/compare/v3.7.1...v3.8.0)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/nuclei/v3
  dependency-version: 3.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-24 16:21:28 +00:00
celeste e86a917364 Merge pull request #98 from vmfunc/ci/bump-go-1.25
ci: bump go to 1.25 and migrate golangci-lint to v2
2026-04-24 09:19:55 -07:00
vmfunc 09314edf5c ci: suppress v2-only staticcheck rules and 3 stray errcheck cases
second pass after v2 surfaced more old-code noise:
  * staticcheck ST1000/ST1003 were the v1 'stylecheck' linter
    (never enabled here); disabled to preserve prior parity
  * errcheck can't match (io.Closer).Close against concrete
    types (tarball, tcp, logger); added narrow file+text rules

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:04:50 -07:00
vmfunc 9863a252d8 ci: appease yamllint and relax v2 linters to match v1 parity
yamllint wanted a '---' document start and lines <=80 chars.

golangci-lint v2 surfaces 63 latent issues that v1.64.8 missed:
  * staticcheck QF1003/QF1012 (new quickfix checks) -- disabled
  * errcheck on idiomatic Body.Close / fmt.Fprint -- excluded
  * gocritic importShadow (nuclei output pkg alias) -- excluded
  * gocritic rangeValCopy (nuclei value-type iteration) -- excluded
  * gosec G301/G302 (log perms, intentional) -- excluded
  * noctx in 3 scan files -- excluded by path until refactored

real fixes for the noctx/perms cases are a separate follow-up;
this PR is strictly the toolchain + lint-action bump.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:01:33 -07:00
vmfunc 84b0b81996 ci: bump go toolchain to 1.25 and migrate golangci-lint to v2
nuclei/v3 v3.8.0 (PR #97) requires go >= 1.25.7 in go.mod.
this breaks the existing CI pinned to go 1.24:

  - build: "go.mod requires go >= 1.25.7 (running go 1.24.13;
    GOTOOLCHAIN=local)"
  - lint:  "the Go language version (go1.24) used to build
    golangci-lint is lower than the targeted Go version (1.25.7)"

bumps setup-go to 1.25 across all workflows and moves the lint
job to golangci-lint-action v8 with golangci-lint v2.11.4 (built
with go 1.25). migrates .golangci.yml to the v2 schema:

  - version: "2"
  - linters-settings -> linters.settings
  - issues.exclude-rules -> linters.exclusions.rules
  - drop gosimple (merged into staticcheck in v2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 08:53:43 -07:00
dependabot[bot] 7123e392c9 chore(deps): bump the go_modules group across 1 directory with 5 updates
Bumps the go_modules group with 3 updates in the / directory: [github.com/projectdiscovery/nuclei/v3](https://github.com/projectdiscovery/nuclei), [github.com/Azure/go-ntlmssp](https://github.com/Azure/go-ntlmssp) and [github.com/go-git/go-git/v5](https://github.com/go-git/go-git).


Updates `github.com/projectdiscovery/nuclei/v3` from 3.7.1 to 3.8.0
- [Release notes](https://github.com/projectdiscovery/nuclei/releases)
- [Commits](https://github.com/projectdiscovery/nuclei/compare/v3.7.1...v3.8.0)

Updates `github.com/Azure/go-ntlmssp` from 0.1.0 to 0.1.1
- [Release notes](https://github.com/Azure/go-ntlmssp/releases)
- [Commits](https://github.com/Azure/go-ntlmssp/compare/v0.1.0...v0.1.1)

Updates `github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream` from 1.6.11 to 1.7.8
- [Release notes](https://github.com/aws/aws-sdk-go-v2/releases)
- [Commits](https://github.com/aws/aws-sdk-go-v2/compare/service/rum/v1.6.11...service/m2/v1.7.8)

Updates `github.com/buger/jsonparser` from 1.1.1 to 1.1.2
- [Release notes](https://github.com/buger/jsonparser/releases)
- [Commits](https://github.com/buger/jsonparser/compare/v1.1.1...v1.1.2)

Updates `github.com/go-git/go-git/v5` from 5.17.1 to 5.18.0
- [Release notes](https://github.com/go-git/go-git/releases)
- [Commits](https://github.com/go-git/go-git/compare/v5.17.1...v5.18.0)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/nuclei/v3
  dependency-version: 3.8.0
  dependency-type: direct:production
  dependency-group: go_modules
- dependency-name: github.com/Azure/go-ntlmssp
  dependency-version: 0.1.1
  dependency-type: indirect
  dependency-group: go_modules
- dependency-name: github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream
  dependency-version: 1.7.8
  dependency-type: indirect
  dependency-group: go_modules
- dependency-name: github.com/buger/jsonparser
  dependency-version: 1.1.2
  dependency-type: indirect
  dependency-group: go_modules
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.18.0
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-24 07:40:32 +00:00
celeste 383e645d85 Merge pull request #91 from vmfunc/dependabot/github_actions/codecov/codecov-action-6
chore(deps): bump codecov/codecov-action from 5 to 6
2026-04-24 00:37:55 -07:00
celeste 30bf148768 Merge pull request #92 from vmfunc/dependabot/go_modules/go_modules-f67f74747b
chore(deps): bump github.com/go-git/go-git/v5 from 5.16.5 to 5.17.1 in the go_modules group across 1 directory
2026-04-24 00:37:42 -07:00
celeste 3d04a61b27 Merge pull request #89 from vmfunc/dependabot/go_modules/github.com/charmbracelet/log-1.0.0
chore(deps): bump github.com/charmbracelet/log from 0.4.2 to 1.0.0
2026-04-24 00:37:22 -07:00
celeste 9bd1d8cd14 Merge pull request #93 from vmfunc/dependabot/github_actions/actions/github-script-9
chore(deps): bump actions/github-script from 8 to 9
2026-04-24 00:37:01 -07:00
celeste 68fed81eee Merge pull request #94 from vmfunc/dependabot/go_modules/github.com/projectdiscovery/utils-0.10.1
chore(deps): bump github.com/projectdiscovery/utils from 0.9.0 to 0.10.1
2026-04-24 00:36:47 -07:00
celeste cf72dfff0e Merge pull request #95 from vmfunc/dependabot/github_actions/softprops/action-gh-release-3
chore(deps): bump softprops/action-gh-release from 2 to 3
2026-04-24 00:36:20 -07:00
dependabot[bot] a469463c19 chore(deps): bump softprops/action-gh-release from 2 to 3
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 12:45:52 +00:00
dependabot[bot] c527668c60 chore(deps): bump github.com/projectdiscovery/utils from 0.9.0 to 0.10.1
Bumps [github.com/projectdiscovery/utils](https://github.com/projectdiscovery/utils) from 0.9.0 to 0.10.1.
- [Release notes](https://github.com/projectdiscovery/utils/releases)
- [Changelog](https://github.com/projectdiscovery/utils/blob/main/CHANGELOG.md)
- [Commits](https://github.com/projectdiscovery/utils/compare/v0.9.0...v0.10.1)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/utils
  dependency-version: 0.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-17 12:43:54 +00:00
dependabot[bot] 4917eaf7e7 chore(deps): bump actions/github-script from 8 to 9
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-10 12:45:52 +00:00
dependabot[bot] 4152e74ade chore(deps): bump github.com/go-git/go-git/v5
Bumps the go_modules group with 1 update in the / directory: [github.com/go-git/go-git/v5](https://github.com/go-git/go-git).


Updates `github.com/go-git/go-git/v5` from 5.16.5 to 5.17.1
- [Release notes](https://github.com/go-git/go-git/releases)
- [Commits](https://github.com/go-git/go-git/compare/v5.16.5...v5.17.1)

---
updated-dependencies:
- dependency-name: github.com/go-git/go-git/v5
  dependency-version: 5.17.1
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-30 17:15:28 +00:00
dependabot[bot] 3e0cbbc5dd chore(deps): bump codecov/codecov-action from 5 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 12:45:10 +00:00
dependabot[bot] abe8bac165 chore(deps): bump github.com/charmbracelet/log from 0.4.2 to 1.0.0
Bumps [github.com/charmbracelet/log](https://github.com/charmbracelet/log) from 0.4.2 to 1.0.0.
- [Release notes](https://github.com/charmbracelet/log/releases)
- [Commits](https://github.com/charmbracelet/log/compare/v0.4.2...v1.0.0)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/log
  dependency-version: 1.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-13 12:44:04 +00:00
celeste d6c52d3dd8 Merge pull request #87 from vmfunc/dependabot/go_modules/github.com/projectdiscovery/nuclei/v3-3.7.1
chore(deps): bump github.com/projectdiscovery/nuclei/v3 from 3.7.0 to 3.7.1
2026-03-06 22:01:46 +01:00
celeste 439e829c1b Merge pull request #88 from vmfunc/dependabot/go_modules/github.com/antchfx/htmlquery-1.3.6
chore(deps): bump github.com/antchfx/htmlquery from 1.3.5 to 1.3.6
2026-03-06 22:01:27 +01:00
dependabot[bot] d5067d08b2 chore(deps): bump github.com/antchfx/htmlquery from 1.3.5 to 1.3.6
Bumps [github.com/antchfx/htmlquery](https://github.com/antchfx/htmlquery) from 1.3.5 to 1.3.6.
- [Release notes](https://github.com/antchfx/htmlquery/releases)
- [Commits](https://github.com/antchfx/htmlquery/compare/v1.3.5...v1.3.6)

---
updated-dependencies:
- dependency-name: github.com/antchfx/htmlquery
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 12:44:13 +00:00
dependabot[bot] 98c987bfbb chore(deps): bump github.com/projectdiscovery/nuclei/v3
Bumps [github.com/projectdiscovery/nuclei/v3](https://github.com/projectdiscovery/nuclei) from 3.7.0 to 3.7.1.
- [Release notes](https://github.com/projectdiscovery/nuclei/releases)
- [Commits](https://github.com/projectdiscovery/nuclei/compare/v3.7.0...v3.7.1)

---
updated-dependencies:
- dependency-name: github.com/projectdiscovery/nuclei/v3
  dependency-version: 3.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-06 12:44:06 +00:00
celeste 237dfde4d1 Merge pull request #86 from vmfunc/chore/bump-setup-go-v6
chore: bump setup-go to v6, drop go 1.23 from CI matrix
2026-03-01 05:13:54 +01:00
vmfunc c69bbe1232 chore: bump setup-go to v6, drop go 1.23 from CI matrix
go.mod requires >= 1.24.2 so the 1.23 matrix entry was already dead.
setup-go v6 sets GOTOOLCHAIN=local which makes it fail explicitly.
2026-03-01 05:07:23 +01:00
celeste d52bcfc736 Merge pull request #84 from vmfunc/dependabot/go_modules/go_modules-9655125a49
chore(deps): bump the go_modules group across 1 directory with 2 updates
2026-03-01 05:05:02 +01:00
celeste fcffe18ba5 Merge pull request #85 from vmfunc/dependabot/github_actions/github/codeql-action-4
chore(deps): bump github/codeql-action from 3 to 4
2026-03-01 05:04:59 +01:00
dependabot[bot] 7749b50d25 chore(deps): bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 12:45:54 +00:00
dependabot[bot] 93783d8bd3 chore(deps): bump the go_modules group across 1 directory with 2 updates
Bumps the go_modules group with 2 updates in the / directory: [filippo.io/edwards25519](https://github.com/FiloSottile/edwards25519) and [github.com/cloudflare/circl](https://github.com/cloudflare/circl).


Updates `filippo.io/edwards25519` from 1.1.0 to 1.1.1
- [Commits](https://github.com/FiloSottile/edwards25519/compare/v1.1.0...v1.1.1)

Updates `github.com/cloudflare/circl` from 1.6.1 to 1.6.3
- [Release notes](https://github.com/cloudflare/circl/releases)
- [Commits](https://github.com/cloudflare/circl/compare/v1.6.1...v1.6.3)

---
updated-dependencies:
- dependency-name: filippo.io/edwards25519
  dependency-version: 1.1.1
  dependency-type: indirect
  dependency-group: go_modules
- dependency-name: github.com/cloudflare/circl
  dependency-version: 1.6.3
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-25 19:34:14 +00:00
dependabot[bot] fef7806ac2 chore(deps): bump github.com/refraction-networking/utls (#78)
Bumps the go_modules group with 1 update in the / directory: [github.com/refraction-networking/utls](https://github.com/refraction-networking/utls).


Updates `github.com/refraction-networking/utls` from 1.8.1 to 1.8.2
- [Release notes](https://github.com/refraction-networking/utls/releases)
- [Commits](https://github.com/refraction-networking/utls/compare/v1.8.1...v1.8.2)

---
updated-dependencies:
- dependency-name: github.com/refraction-networking/utls
  dependency-version: 1.8.2
  dependency-type: indirect
  dependency-group: go_modules
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 02:11:40 +01:00
dependabot[bot] 422245fe7f chore(deps): bump actions/labeler from 5 to 6 (#80)
Bumps [actions/labeler](https://github.com/actions/labeler) from 5 to 6.
- [Release notes](https://github.com/actions/labeler/releases)
- [Commits](https://github.com/actions/labeler/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/labeler
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 02:11:25 +01:00
dependabot[bot] 24a9f4411f chore(deps): bump reviewdog/action-misspell from 1.26.0 to 1.27.0 (#82)
Bumps [reviewdog/action-misspell](https://github.com/reviewdog/action-misspell) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/reviewdog/action-misspell/releases)
- [Commits](https://github.com/reviewdog/action-misspell/compare/v1.26.0...v1.27.0)

---
updated-dependencies:
- dependency-name: reviewdog/action-misspell
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 02:10:52 +01:00
dependabot[bot] 22936a3281 chore(deps): bump reviewdog/action-yamllint from 1.19.0 to 1.21.0 (#83)
Bumps [reviewdog/action-yamllint](https://github.com/reviewdog/action-yamllint) from 1.19.0 to 1.21.0.
- [Release notes](https://github.com/reviewdog/action-yamllint/releases)
- [Commits](https://github.com/reviewdog/action-yamllint/compare/v1.19.0...v1.21.0)

---
updated-dependencies:
- dependency-name: reviewdog/action-yamllint
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-23 02:10:40 +01:00
vmfunc 495d2c5496 feat: add securitytrails integration for domain discovery + target expansion 2026-02-17 13:38:07 +01:00
dependabot[bot] 5ddfbc6204 chore(deps): bump github.com/likexian/whois from 1.15.1 to 1.15.7 (#67)
Bumps [github.com/likexian/whois](https://github.com/likexian/whois) from 1.15.1 to 1.15.7.
- [Release notes](https://github.com/likexian/whois/releases)
- [Commits](https://github.com/likexian/whois/compare/v1.15.1...v1.15.7)

---
updated-dependencies:
- dependency-name: github.com/likexian/whois
  dependency-version: 1.15.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 15:32:55 +01:00
dependabot[bot] b522aa3206 chore(deps): bump github.com/charmbracelet/log from 0.2.4 to 0.4.2 (#74)
Bumps [github.com/charmbracelet/log](https://github.com/charmbracelet/log) from 0.2.4 to 0.4.2.
- [Release notes](https://github.com/charmbracelet/log/releases)
- [Commits](https://github.com/charmbracelet/log/compare/v0.2.4...v0.4.2)

---
updated-dependencies:
- dependency-name: github.com/charmbracelet/log
  dependency-version: 0.4.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 15:11:38 +01:00
dependabot[bot] efd089a9b6 chore(deps): bump ossf/scorecard-action from 2.4.0 to 2.4.3 (#66)
Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.0 to 2.4.3.
- [Release notes](https://github.com/ossf/scorecard-action/releases)
- [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md)
- [Commits](https://github.com/ossf/scorecard-action/compare/v2.4.0...v2.4.3)

---
updated-dependencies:
- dependency-name: ossf/scorecard-action
  dependency-version: 2.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 15:11:30 +01:00
dependabot[bot] dd9db0dfd6 chore(deps): bump reviewdog/action-shellcheck from 1.27.0 to 1.32.0 (#70)
Bumps [reviewdog/action-shellcheck](https://github.com/reviewdog/action-shellcheck) from 1.27.0 to 1.32.0.
- [Release notes](https://github.com/reviewdog/action-shellcheck/releases)
- [Commits](https://github.com/reviewdog/action-shellcheck/compare/v1.27.0...v1.32.0)

---
updated-dependencies:
- dependency-name: reviewdog/action-shellcheck
  dependency-version: 1.32.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 15:11:26 +01:00
dependabot[bot] 1eab6143bb chore(deps): bump reviewdog/action-markdownlint from 0.24.0 to 0.26.2 (#73)
Bumps [reviewdog/action-markdownlint](https://github.com/reviewdog/action-markdownlint) from 0.24.0 to 0.26.2.
- [Release notes](https://github.com/reviewdog/action-markdownlint/releases)
- [Commits](https://github.com/reviewdog/action-markdownlint/compare/v0.24.0...v0.26.2)

---
updated-dependencies:
- dependency-name: reviewdog/action-markdownlint
  dependency-version: 0.26.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 15:11:23 +01:00
dependabot[bot] 418180a124 chore(deps): bump actions/github-script from 7 to 8 (#77)
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 15:05:54 +01:00
dependabot[bot] 6f4144efe1 chore(deps): bump actions/checkout from 4 to 6 (#68)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 14:47:32 +01:00
vmfunc a05d6ada56 ci: add pr bot for auto-labeling + rewrite release workflow for semver tags
pr-bot labels PRs by area (scan, nuclei, modules, ci, deps, etc) and size
(xs/s/m/l/xl), posts a summary comment with file stats breakdown.

release workflow now triggers on v* tags instead of every push to main -
extracts version from tag, injects via ldflags, auto-generates changelog
from commits since last release, includes install instructions in the
release body. prerelease detection for rc/beta tags.

Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 02:19:19 +01:00
vmfunc 75da3e3131 fix: resolve all golangci-lint issues across codebase
- noctx: use http.NewRequestWithContext instead of http.Get/client.Get
- bodyclose: close response bodies on all code paths
- httpNoBody: use http.NoBody instead of nil for GET request bodies
- ifElseChain: convert if/else chains to switch in sif.go
- sloppyReassign: use := in logger.go where possible
- nilnil: annotate intentional nil,nil returns in lfi.go, sql.go
- errcheck: handle template install error in nuclei.go
- govet copylock: pass mutex by pointer in executor.go
- log.Fatalf: replace with log.Errorf+continue in api mode
2026-02-13 02:11:17 +01:00
vmfunc f5251d0c44 chore: strengthen golangci-lint config - add gosec, errorlint, nilnil, wastedassign, usetesting linters
adds security and correctness linters, suppresses noisy checks
(fieldalignment, shadow, unusedwrite, nestingReduce), excludes
logger.Write from errcheck since log writes are best-effort
2026-02-13 02:11:03 +01:00
vmfunc e2198e932b ci: replace qodana with codeql - no external tokens needed
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 02:08:32 +01:00
vmfunc bad5b598c9 test: add fuzz tests for LFI detection, SQL patterns, version parsing
fuzz targets: DetectLFIFromResponse, isAdminPanel, databaseErrorPatterns,
isValidVersionString, ExtractVersionOptimized - should bump the scorecard
fuzzing check.

Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:57:46 +01:00
vmfunc c85201b1ed ci: pin govulncheck to v1.1.4 - fixes scorecard pinned-dependencies
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:57:36 +01:00
vmfunc 45a384bdc9 add SECURITY.md - fixes scorecard security-policy check
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:57:31 +01:00
vmfunc fcf9291653 ci: add explicit permissions to all workflows - fixes scorecard token-permissions
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:40:22 +01:00
vmfunc e94fda0acf deps: bump go-git to v5.16.5 - fixes CVE-2026-25934
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:39:00 +01:00
vmfunc 03a9488b65 internal/scan: migrate nuclei integration to v3 SDK
replace ~100 lines of manual nuclei v2 plumbing (catalog, loader, core,
protocolstate, protocolinit, hosterrorscache, interactsh, reporting,
ratelimit, testutils) with the v3 lib SDK - NewNucleiEngineCtx +
functional options.

drops direct ratelimit dep, mholt/archiver and nwaples/rardecode
(resolves dependabot CVE alerts for path traversal + DoS).

Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:22:25 +01:00
vmfunc 83702e9a41 ci: overhaul workflows - lint, security scanning, release hardening
- add golangci-lint job to go.yml (parallel with build+test)
- add Go 1.23/1.24 version matrix, coverage only on 1.24
- upgrade setup-go@v4 to v5, codecov@v4 to v5 across all workflows
- fix check-large-files bug (find|while never exits 1), exclude .git/
- add concurrency groups to push+PR workflows (no duplicate runs)
- lowercase all workflow names to match project voice
- add gosec, errorlint, gocognit, nilnil, wastedassign, usetesting linters
- remove deprecated exportloopref (Go 1.22 fixed loop var capture)
- new: govulncheck.yml - Go vuln scanner with call-graph analysis
- new: scorecard.yml - OpenSSF supply chain scorecard
- new: dependabot.yml - auto-update Go deps + Actions versions
- release: SHA256 checksums + SBOM generation for all artifacts
- add CODEOWNERS
2026-02-13 01:09:57 +01:00
vmfunc 426a301182 deps: bump projectdiscovery/utils to v0.9.0
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:03:47 +01:00
vmfunc 953ef299c9 deps: bump goflags to v0.1.74
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:03:46 +01:00
vmfunc 548c2110d4 update funding.yml with proper sponsor info 2026-02-13 00:26:04 +01:00
vmfunc 4f42c52964 add nixpkgs install instructions and badge to readme 2026-02-13 00:12:28 +01:00
vmfunc 5a557eb20a Merge pull request #62 from 0x4bs3nt/feat/builtin-shodan
feat(modules): builtin shodan scan as module
2026-02-08 21:31:07 +01:00
vmfunc f50f1b933a Merge branch 'main' into feat/builtin-shodan 2026-02-08 19:22:32 +01:00
vmfunc 6f460425be Merge pull request #63 from 0x4bs3nt/feat/builtin-whois
feat(modules): builtin whois scan as module
2026-02-08 14:12:27 +01:00
vmfunc 261dbea356 Merge pull request #64 from 0x4bs3nt/feat/builtin-frameworks
feat(modules): builtin framework detection as module
2026-02-08 14:11:56 +01:00
vmfunc 16ea9047f0 Merge branch 'main' into feat/builtin-frameworks 2026-01-12 11:22:56 +01:00
vmfunc 39bd115d3c Merge branch 'main' into feat/builtin-shodan 2026-01-12 11:22:36 +01:00
vmfunc ccf093b7e9 fix: rename to snakecase 2026-01-12 11:19:54 +01:00
vmfunc b5398ec687 fix: renamed whois module file
Renamed whois scan module file to differentiate from legacy whois scan
file.
2026-01-12 11:19:54 +01:00
vmfunc b298e2ec2c fix(conflicts): fix PR conflicts on 2026-01-12 11:19:48 +01:00
vmfunc 812d0b3e53 Merge pull request #61 from 0x4bs3nt/feat/builtin-nuclei
feat(modules): builtin nuclei scan as module
2026-01-11 16:39:18 -08:00
vmfunc 95cebab47f fix: rename to snakecase 2026-01-07 22:39:56 +01:00
vmfunc 579f5aff4b fix: rename to snakecase 2026-01-07 22:39:35 +01:00
vmfunc 6df46b635a fix: rename to snakecase 2026-01-07 22:39:19 +01:00
vmfunc 4a6364aba9 fix: shodan module file rename
Renamed shodan module file to differentiate from legacy shodan lookup
files.
2026-01-07 22:35:51 +01:00
vmfunc e7db0777c2 fix: frameworks module file rename
Renamed frameworks module file to differentiate from legacy framework
scans.
2026-01-07 22:34:53 +01:00
vmfunc 9767a6b189 fix: renamed nuclei module file
Renamed the nuclei module file to differentiate from the nuclei legacy
scan file.
2026-01-07 22:33:49 +01:00
vmfunc abb992aefd fix: colorizer exception
Fixed Nuclei giving off exception for missing Colorizer on the executor
options.
2026-01-07 19:06:51 +01:00
vmfunc 3c160de165 fix(nuclei): logdir, headless option and hosterrorscache
Set the HostErrorsCache executor option, cache is created but not passed
as option.
Headless initialization is required even without browser templates.
Nuclei expects project file to be set
2026-01-07 17:01:22 +01:00
vmfunc 66a752d604 fix: nuclei scan nil pointer dereference
Fixed nil pointer dereference issues in the nuclei scan running as a
module
2026-01-07 15:09:49 +01:00
vmfunc 45f341c97f feat(modules): legacy nuclei scan
Converted nuclei scan to be able to run as module.
2026-01-07 13:07:35 +01:00
vmfunc 0383c49bbd feat(modules): legacy shodan scan
Converted legacy Go shodan scan to be able to run as module.
2026-01-07 13:05:08 +01:00
vmfunc e5e831541f feat(modules): legacy framework scan
Converted legacy framework scan to be able to run as module.
2026-01-07 13:02:40 +01:00
vmfunc f309198f69 feat(modules): infra for builtin modules
Infrastructure preparation for builtin complex Go module registration.
2026-01-07 12:56:17 +01:00
vmfunc 689d575282 Merge pull request #56 from 0x4bs3nt/feat/astro-framework-detection
feat(frameworks): add Astro framework detection
2026-01-06 12:10:34 -08:00
vmfunc 75014e244b fix: adjust generator meta weight
Adjusted generator meta weight to remain consistent with other meta-framework detectors.

Co-authored-by: vmfunc <celeste@router.sex>
2026-01-06 14:45:03 +01:00
vmfunc 9c5220ec57 Merge pull request #55 from 0x4bs3nt/docs/contributing-update
docs: update CONTRIBUTING.md
2026-01-05 23:50:02 -08:00
vmfunc 0297bf3975 fix: discord invite
Fixed discord invite to official server invite url.
2026-01-06 06:35:32 +01:00
vmfunc 8eb7e84090 fix: use dynamic versioning for debian packages 2026-01-05 20:55:30 -08:00
vmfunc 4e0c45fa58 docs: update CONTRIBUTING.md
Update CONTRIBUTING.md docs with up to date data:
 - Discord invite to new sif server
 - Update URL-s to new vmfunc/sif repository
 - Update guidelines on contributing framework detection patterns
2026-01-06 05:30:34 +01:00
vmfunc 6467a2ca58 docs: add apt/cloudsmith installation instructions and badge 2026-01-05 20:28:30 -08:00
vmfunc 844affaed4 ci: push debian packages to cloudsmith 2026-01-05 20:28:07 -08:00
vmfunc 56895899ff ci: add debian package builds to releases 2026-01-05 20:13:18 -08:00
vmfunc 2e99e5072f docs: add 0xatrilla to contributors for AUR packaging 2026-01-05 19:51:50 -08:00
vmfunc 37925c6c99 docs: add AUR and Homebrew badges to readme 2026-01-05 19:48:51 -08:00
vmfunc d3216ca4a6 Merge pull request #53 from 0xatrilla/add-aur-install-instructions
docs: add AUR installation instructions
2026-01-05 19:44:43 -08:00
vmfunc 277f516ce9 chore: revise arch linux installation section in README
Updated Arch Linux installation instructions in README.md.
2026-01-05 19:44:15 -08:00
vmfunc ee1f9d7f31 feat(frameworks): add Astro framework detection
Add detection support for the Astro meta framework.

Includes signature detection, version extraction and tests with full
signature coverage.
2026-01-06 04:40:15 +01:00
acxtrilla 9705d95067 docs: add AUR installation instructions
Added Arch Linux (AUR) installation section to README with instructions
for installing via AUR helpers (yay/paru) or manually with makepkg.

Package available at: https://aur.archlinux.org/packages/sif
2026-01-06 01:56:40 +00:00
vmfunc 8c60e255dc docs: add homebrew installation instructions 2026-01-05 16:53:26 -08:00
vmfunc 7438dfb2ca chore: readme inconsistency 2026-01-03 06:14:40 -08:00
vmfunc 60c38e29cf ci: upgrade to go 1.24 in all workflows 2026-01-03 06:04:33 -08:00
vmfunc 7268374333 chore: add license headers to missing files 2026-01-03 06:01:00 -08:00
vmfunc 00a66adf27 feat(output): add styled console output with module loggers
- Add output package with colored prefixes and module loggers
- Each module gets unique background color based on name hash
- Add spinner for indeterminate operations
- Add progress bar for known-count operations
- Update all scan files to use ModuleLogger pattern
- Add clean PrintSummary for scan completion
2026-01-03 05:57:10 -08:00
vmfunc ab17191c31 docs: add comprehensive documentation and fix github actions
- add docs/ with installation, usage, modules, scans, and api docs
- add docs link to main readme
- fix release.yml to bundle modules directory with releases
- add module system tests to runtest.yml
- standardize go version to 1.23 across workflows
2026-01-03 05:57:10 -08:00
vmfunc cd1a56bd14 docs: update readme and add module documentation 2026-01-03 05:57:10 -08:00
vmfunc 36a0e473e3 feat: show module loading and execution logs by default 2026-01-03 05:57:10 -08:00
vmfunc 29b1b804af feat: add debug logging for module execution 2026-01-03 05:57:10 -08:00
vmfunc d2537dae1b refactor: move pkg/scan to internal/scan 2026-01-03 05:57:10 -08:00
vmfunc 6d8319dfa8 fix: add io.LimitReader and proper error handling to shodan.go
Add io.LimitReader with 5MB limit to prevent memory exhaustion and
fix ignored error in queryShodanHost. The error from io.ReadAll was
previously being discarded with _, which could mask read failures.
2026-01-03 05:57:10 -08:00
vmfunc 7ec8c6fb70 fix: add io.LimitReader to prevent memory exhaustion
Add io.LimitReader with 5MB limit to all HTTP response body reads
to prevent potential memory exhaustion from maliciously large responses.

Affected files:
- pkg/scan/cms.go
- pkg/scan/subdomaintakeover.go
- pkg/scan/js/scan.go
- pkg/scan/js/supabase.go
2026-01-03 05:57:10 -08:00
vmfunc 3e4fd67588 fix: regex compilation performance
Move regex compilation from inside functions to package level to avoid
recompiling on every function call. This improves performance by
compiling the regex patterns once at package initialization.

- Move jwtRegex to package level in supabase.go
- Move nextPagesRegex to package level in next.go
- Use strings.Builder instead of string concatenation in next.go
2026-01-03 05:57:10 -08:00
vmfunc 2d306fcf1d feat: implement loadYAML in module loader 2026-01-03 05:57:10 -08:00
vmfunc 82c8667e63 feat: integrate module system into sif.go
Add module system integration allowing users to run YAML-defined security
modules via CLI flags. Implements --list-modules to display available modules,
and supports running modules by ID, tags, or all at once.
2026-01-03 05:57:10 -08:00
vmfunc dc537a02f2 feat: add module cli flags 2026-01-03 05:57:10 -08:00
vmfunc a5ea29b88d feat: add built-in yaml modules for security scanning 2026-01-03 05:57:10 -08:00
vmfunc 01a10c6a2f feat: add yaml module parser and http executor 2026-01-03 05:57:10 -08:00
vmfunc 9154f8e77a feat: add module system infrastructure 2026-01-03 05:57:10 -08:00
vmfunc 539122ac4e refactor: move config to internal 2026-01-03 05:57:10 -08:00
vmfunc 28588fe37c refactor: move logger to internal 2026-01-03 05:57:10 -08:00
vmfunc a6abadd0d4 refactor: rewrite framework detection with modular detector architecture
- create detector interface and registry for extensibility
- extract detectors to separate files: backend.go, frontend.go, cms.go, meta.go
- reduce detect.go from 785 lines to 178 lines (pure orchestrator)
- export VersionMatch and ExtractVersionOptimized for detector use
- create result.go with NewFrameworkResult and WithVulnerabilities helpers
- add url validation to New() for early error detection
- add sif_test.go with main package tests
- update detect_test.go to use external test package pattern
2026-01-03 05:57:09 -08:00
vmfunc 1b27250b05 feat: add generic types and type-safe result handling
introduce ScanResult interface and generic NewModuleResult constructor
for compile-time type safety when creating module results.

- add pkg/scan/result.go with ScanResult interface and named slice types
- add typed shodanMetadata struct to replace map[string]interface{}
- refactor supabase.go with typed response structs and json.RawMessage
- add ResultType() methods to all scan result types
- update sif.go to use NewModuleResult generic constructor

this provides type safety without breaking JSON serialization.
2026-01-03 05:57:09 -08:00
vmfunc 2002509ab5 refactor: extract cve database to separate file
move CVEEntry struct and knownCVEs map to cve.go for better
organization. this reduces detect.go by another 170 lines and makes
the CVE database easier to maintain and extend.
2026-01-03 05:57:09 -08:00
vmfunc 7223a2eb66 perf: precompile framework version regex patterns
move version extraction patterns to version.go and compile them at init
time instead of recompiling on every check. this significantly improves
framework detection performance.

- add version.go with pre-compiled regex patterns for all frameworks
- update detect.go to use extractVersionOptimized
- remove duplicate extractVersionWithConfidence and isValidVersion functions
- add io.LimitReader to prevent memory exhaustion on large responses
- update tests to use the optimized version extraction
2026-01-03 05:57:09 -08:00
vmfunc 314783dba3 fix: response body leaks in cms.go and sql.go
close response bodies immediately after reading instead of deferring
inside loops, which delays closure until function exit
2026-01-03 05:57:09 -08:00
vmfunc bad1af5fc6 fix: response body leak in scan.go robots processing
move resp.body.close() inside the loop after use instead of deferring,
which would only run when the outer function exits
2026-01-03 05:57:09 -08:00
vmfunc 7ab5cfc18c feat: add generic worker pool for concurrent task processing
implement channel-based work distribution with generics for type-safe
concurrent processing, includes run, runwithfilter, and foreach methods
with comprehensive test coverage
2026-01-03 05:57:09 -08:00
vmfunc aba8c410a6 perf: optimize deduplication with map-based o(1) lookups in lfi and sql
replace o(n) slice iteration with map lookups for checking duplicates,
preallocate result slices, reduce lock hold time by separating map check
from result append
2026-01-03 05:57:09 -08:00
vmfunc 582baf2d33 fix: data races and slice preallocation in dirlist and dnslist
add mutex protection for concurrent slice appends, preallocate result
slices with reasonable capacity, use logger instead of direct file i/o
2026-01-03 05:57:09 -08:00
vmfunc ecb0124688 fix: error patterns and string building in sif.go and js/scan.go
replace errors.new(fmt.sprintf()) with fmt.errorf, use strings.builder
instead of string concatenation in loop, fix defer in loop issue,
preallocate slices where size is estimable
2026-01-03 05:57:09 -08:00
vmfunc 088a5bebeb test: add logger tests for buffered write functionality
covers initialization, write, flush, close, concurrent writes, and
file creation with proper cleanup verification
2026-01-03 05:57:09 -08:00
vmfunc 17d8e664d6 refactor: logger to use buffered file handles
replace per-write file open/close with cached file handles and buffered
writers for significantly reduced i/o overhead. adds flush and close
methods for proper cleanup at program exit.
2026-01-03 05:57:09 -08:00
vmfunc 86539cd06e chore: remove unused utils package
the returnApiOutput function was never used and contained only
hardcoded test data
2026-01-03 05:57:09 -08:00
vmfunc 046a5bc7d7 ci: add test coverage reporting to workflow
run tests with race detector and coverage profiling, upload results
to codecov for visibility into test coverage metrics
2026-01-03 05:57:09 -08:00
vmfunc 295d684054 ci: enhance golangci-lint with additional linters
add gocritic, revive, unconvert, prealloc, bodyclose, noctx, and
exportloopref for better code quality detection
2026-01-03 05:57:09 -08:00
vmfunc 8cb4a85a99 Merge pull request #51 from andrewgazelka/chore/modernize-nix-flake
chore(nix): modernize flake to use buildGoModule
2026-01-03 00:38:59 -08:00
Andrew Gazelka f2c8cc71b2 chore(nix): modernize flake to use buildGoModule
- Remove flake-utils dependency (use local forAllSystems helper)
- Remove gomod2nix dependency (use native buildGoModule)
- Add overlay export for easy consumption
- Update nixpkgs to latest unstable
- Disable tests in nix build (require network access)
2026-01-03 00:25:37 -08:00
vmfunc 01fa28555b docs: update contributor name and add vxfemboy 2026-01-02 19:56:44 -08:00
vmfunc 29478f087a chore: fix contributorrc 2026-01-02 19:55:31 -08:00
vmfunc 7b3f4a4f2f chore: fix contributorrc 2026-01-02 19:51:03 -08:00
vmfunc 563f3817ce Merge pull request #40 from vmfunc/feat/framework-detection
feat: framework detection module
2026-01-02 19:15:07 -08:00
vmfunc 20ea60cd70 fix: adjust sif logo alignment 2026-01-02 19:12:28 -08:00
vmfunc 5eac60f696 fix: improve version detection and add documentation
- fix version detection to validate reasonable version numbers (major < 100)
- remove overly permissive patterns that caused false positives
- add comprehensive framework contribution documentation to CONTRIBUTING.md
- document signature patterns, version detection, and CVE data format
- add configuration documentation for flags and env vars
- outline future enhancements for community contributions
2026-01-02 19:04:37 -08:00
vmfunc e02e1554ea docs: add framework detection to readme 2026-01-02 18:54:24 -08:00
vmfunc 816b89328c feat: expand framework detection with cvs, version confidence, concurrency
- add 20+ new framework signatures (vue, angular, react, svelte, sveltekit,
  remix, gatsby, joomla, magento, shopify, ghost, ember, backbone, meteor,
  strapi, adonisjs, cakephp, codeigniter, asp.net core, spring boot)
- add version confidence scoring with multiple detection sources
- add concurrent framework scanning for better performance
- expand cve database with 15+ known vulnerabilities (spring4shell, etc.)
- add risk level assessment based on cve severity
- add comprehensive security recommendations
- add new tests for all features
2026-01-02 18:52:15 -08:00
vmfunc af9d05f6b2 chore: add license header to detect.go 2026-01-02 18:52:15 -08:00
vmfunc 138bdf35aa feat: improve framework detection with more signatures and tests
- use math.Exp instead of custom exp implementation
- add more framework signatures: next.js, nuxt.js, wordpress, drupal,
  symfony, fastapi, gin, phoenix
- fix header detection to check both header names and values
- simplify version detection (remove unnecessary padding)
- add comprehensive test suite for framework detection
- fix formatting in dork.go
2026-01-02 18:52:15 -08:00
vmfunc 494a84e338 chore(actions): add framework to CI 2026-01-02 18:52:15 -08:00
vmfunc 3bc7a2463d feat(framework-detection): weighted bayesian detection algorithm
- weighted signature matching for more accurate framework detection
- sigmoid normalization for confidence scores
- version detection with semantic versioning support
- header-only pattern
2026-01-02 18:52:15 -08:00
vmfunc a08239bb1c feat: framework detection module 2026-01-02 18:52:15 -08:00
vmfunc ecf71be01f fix: use static discord badge instead of server id 2026-01-02 18:45:07 -08:00
vmfunc 29709103be docs: update readme with new modules and discord link 2026-01-02 18:42:45 -08:00
vmfunc db24c59498 feat: add lfi reconnaissance module (#49)
adds a new --lfi flag for local file inclusion vulnerability scanning:
- tests common lfi parameters with directory traversal payloads
- detects /etc/passwd, /etc/shadow, windows system files
- identifies php wrappers and encoded content
- supports various bypass techniques (null bytes, encoding)

closes #4
2026-01-02 18:41:30 -08:00
vmfunc 4392e33179 feat: add sql reconnaissance module (#48)
adds a new --sql flag that performs sql reconnaissance on target urls:
- detects common database admin panels (phpmyadmin, adminer, pgadmin, etc.)
- identifies database error disclosure (mysql, postgresql, mssql, oracle, sqlite)
- scans common paths for sql injection indicators

closes #3
2026-01-02 18:40:06 -08:00
vmfunc 080ab10f56 fix: remove duplicate subdomain takeover call and add config tests (#46)
- remove duplicate SubdomainTakeover call that ran twice when both
  dns scan and --st flag were enabled
- add comprehensive tests for config settings defaults and behavior
- fix formatting in dork.go

closes #1
2026-01-02 18:38:47 -08:00
vmfunc 4a77307acf Merge pull request #47 from vmfunc/feat/shodan-integration
feat: add shodan integration for host reconnaissance
2026-01-02 18:35:56 -08:00
vmfunc d44dbb7f48 feat: add shodan integration for host reconnaissance
adds a new --shodan flag that queries the shodan api for information
about the target host. requires SHODAN_API_KEY environment variable.

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

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

also fixes go vet warnings for non-constant format strings
2026-01-02 18:03:27 -08:00
vmfunc 0e3e43a1f3 fix: update readme badges and use banner image
- update badges to point to vmfunc/sif
- replace ascii art with banner image
- fix header check action to check first 5 lines
- remove obsolete LICENSE.md
2026-01-02 17:54:17 -08:00
vmfunc 8230edf30b chore: delete old license 2026-01-02 17:45:14 -08:00
vmfunc d30c7f56a3 license: switch to bsd 3-clause, update headers and readme
- replace proprietary license with bsd 3-clause
- update all go file headers with new retro terminal style
- add header-check github action to enforce license headers
- completely rewrite readme to be modern, sleek, and lowercase
- fix broken badges
2026-01-02 17:41:18 -08:00
125 changed files with 12128 additions and 3123 deletions
+15 -9
View File
@@ -10,7 +10,7 @@
"contributors": [
{
"login": "vmfunc",
"name": "mel",
"name": "vmfunc",
"avatar_url": "https://avatars.githubusercontent.com/u/59031302?v=4",
"profile": "https://vmfunc.re",
"contributions": [
@@ -18,12 +18,7 @@
"mentoring",
"projectManagement",
"security",
"test",
"business",
"code",
"design",
"financial",
"ideas"
"code"
]
},
{
@@ -41,6 +36,7 @@
"avatar_url": "https://avatars.githubusercontent.com/u/127897805?v=4",
"profile": "https://github.com/macdoos",
"contributions": [
"code"
]
},
{
@@ -52,7 +48,7 @@
"ideas"
]
},
{
{
"login": "tessa-u-k",
"name": "tessa ",
"avatar_url": "https://avatars.githubusercontent.com/u/109355732?v=4",
@@ -76,6 +72,16 @@
"test",
"code"
]
},
{
"login": "vxfemboy",
"name": "Zoa Hickenlooper",
"avatar_url": "https://avatars.githubusercontent.com/u/79362520?v=4",
"profile": "https://github.com/vxfemboy",
"contributions": [
"code"
]
}
]
],
"repoType": "github"
}
+1
View File
@@ -0,0 +1 @@
* @vmfunc
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: vmfunc
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+17
View File
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- deps
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
labels:
- deps
+44
View File
@@ -0,0 +1,44 @@
ci:
- changed-files:
- any-glob-to-any-file: ".github/**"
deps:
- changed-files:
- any-glob-to-any-file:
- "go.mod"
- "go.sum"
- "flake.nix"
- "flake.lock"
scan:
- changed-files:
- any-glob-to-any-file: "internal/scan/**"
nuclei:
- changed-files:
- any-glob-to-any-file: "internal/nuclei/**"
modules:
- changed-files:
- any-glob-to-any-file:
- "internal/modules/**"
- "internal/scan/builtin/**"
- "internal/scan/js/**"
- "modules/**"
docs:
- changed-files:
- any-glob-to-any-file:
- "**/*.md"
- "docs/**"
tests:
- changed-files:
- any-glob-to-any-file: "**/*_test.go"
config:
- changed-files:
- any-glob-to-any-file:
- "internal/config/**"
- ".golangci.yml"
- ".editorconfig"
+8 -3
View File
@@ -1,7 +1,12 @@
name: Automatic Rebase
name: automatic rebase
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
jobs:
rebase:
name: Rebase
@@ -9,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Automatic Rebase
- name: automatic rebase
uses: cirrus-actions/rebase@1.8
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+18 -7
View File
@@ -1,18 +1,29 @@
name: Check Large Files
name: check large files
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
check-large-files:
name: Check for large files
name: check for large files
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for large files
- uses: actions/checkout@v6
- name: check for large files
run: |
find . -type f -size +5M | while read file; do
echo "::error file=${file}::File ${file} is larger than 5MB"
done
large_files=$(find . -path ./.git -prune -o -type f -size +5M -print)
if [ -n "$large_files" ]; then
echo "$large_files" | while read -r file; do
echo "::error file=${file}::File ${file} is larger than 5MB"
done
exit 1
fi
+27 -12
View File
@@ -1,24 +1,39 @@
name: Qodana
name: code quality
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
schedule:
- cron: "0 6 * * 1" # monday 06:00 UTC
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
qodana:
codeql:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
checks: write
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: 'Qodana Scan'
uses: JetBrains/qodana-action@v2024.3
env:
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
go-version: "1.25"
- name: initialize codeql
uses: github/codeql-action/init@v4
with:
languages: go
- name: build
run: go build ./...
- name: perform codeql analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:go"
+10 -6
View File
@@ -1,4 +1,4 @@
name: "Dependency Review"
name: dependency review
on:
pull_request:
push:
@@ -7,16 +7,20 @@ on:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: "Checkout Repository"
uses: actions/checkout@v4
- name: "Dependency Review"
uses: actions/dependency-review-action@v4
- name: checkout repository
uses: actions/checkout@v6
- name: dependency review
uses: actions/dependency-review-action@v5
continue-on-error: ${{ github.event_name == 'push' }}
- name: "Check Dependency Review Outcome"
- name: check dependency review outcome
if: github.event_name == 'push' && failure()
run: |
echo "::warning::Dependency review failed. Please check the dependencies for potential issues."
+41 -7
View File
@@ -1,17 +1,51 @@
name: Go
name: go
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v6
with:
go-version: "1.23"
- name: Build
go-version: "1.25"
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:
version: v2.11.4
build:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.25"]
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go-version }}
- name: build
run: make
- name: run tests with coverage
run: go test -race -coverprofile=coverage.out -covermode=atomic ./...
- name: upload coverage to codecov
uses: codecov/codecov-action@v6
with:
files: ./coverage.out
fail_ci_if_error: false
- name: run integration tests
run: go test -tags=integration -race ./internal/scan/...
+27
View File
@@ -0,0 +1,27 @@
name: govulncheck
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1" # monday 06:00 UTC
permissions:
contents: read
jobs:
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- name: run govulncheck
run: govulncheck ./...
continue-on-error: true
+5 -2
View File
@@ -8,11 +8,14 @@ on:
paths:
- '**.go'
permissions:
contents: read
jobs:
check-headers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: check license headers
run: |
@@ -41,7 +44,7 @@ jobs:
echo ': █▀ █ █▀▀ · Blazing-fast pentesting suite :'
echo ': ▄█ █ █▀ · BSD 3-Clause License :'
echo ': :'
echo ': (c) 2022-2025 vmfunc (vmfunc), xyzeva, :'
echo ': (c) 2022-2026 vmfunc, xyzeva, :'
echo ': lunchcat alumni & contributors :'
echo ': :'
echo '·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·'
+8 -2
View File
@@ -1,4 +1,4 @@
name: Mind your language
name: mind your language
on:
issues:
types:
@@ -12,13 +12,19 @@ on:
types:
- created
- edited
permissions:
contents: read
issues: write
pull-requests: write
jobs:
echo_issue_comment:
runs-on: ubuntu-latest
name: profanity check
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Profanity check step
uses: tailaiw/mind-your-language-action@v1.0.3
env:
+7 -3
View File
@@ -1,18 +1,22 @@
name: Markdown Lint
name: markdown lint
on:
pull_request:
paths:
- "**/*.md"
permissions:
contents: read
pull-requests: write
jobs:
markdownlint:
name: runner / markdownlint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: markdownlint
uses: reviewdog/action-markdownlint@v0.24.0
uses: reviewdog/action-markdownlint@v0.26.2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
+11 -3
View File
@@ -1,18 +1,26 @@
name: Misspell Check
name: misspell check
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
misspell:
name: runner / misspell
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: misspell
uses: reviewdog/action-misspell@v1.26.0
uses: reviewdog/action-misspell@v1.27.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
+139
View File
@@ -0,0 +1,139 @@
name: pr bot
on:
pull_request:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v6
with:
configuration-path: .github/labeler.yml
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: label pr size
uses: actions/github-script@v9
with:
script: |
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.payload.pull_request.number,
per_page: 100,
});
const changes = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
let size;
if (changes < 10) size = "size/xs";
else if (changes < 50) size = "size/s";
else if (changes < 200) size = "size/m";
else if (changes < 500) size = "size/l";
else size = "size/xl";
const sizeLabels = ["size/xs", "size/s", "size/m", "size/l", "size/xl"];
const currentLabels = context.payload.pull_request.labels.map(l => l.name);
const toRemove = currentLabels.filter(l => sizeLabels.includes(l) && l !== size);
for (const label of toRemove) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
name: label,
}).catch(() => {});
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: [size],
});
ci-summary:
runs-on: ubuntu-latest
needs: [label, size]
if: always()
steps:
- uses: actions/github-script@v9
with:
script: |
const pr = context.payload.pull_request;
const { data: checks } = await github.rest.checks.listForRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: pr.head.sha,
per_page: 100,
});
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});
const additions = files.reduce((sum, f) => sum + f.additions, 0);
const deletions = files.reduce((sum, f) => sum + f.deletions, 0);
const fileCount = files.length;
let body = `### pr summary\n\n`;
body += `**${fileCount}** files changed (+${additions} -${deletions})\n\n`;
const goFiles = files.filter(f => f.filename.endsWith('.go')).length;
const testFiles = files.filter(f => f.filename.endsWith('_test.go')).length;
const ciFiles = files.filter(f => f.filename.startsWith('.github/')).length;
const modFiles = files.filter(f => f.filename === 'go.mod' || f.filename === 'go.sum').length;
if (goFiles > 0 || testFiles > 0 || ciFiles > 0 || modFiles > 0) {
body += `| category | files |\n|----------|-------|\n`;
if (goFiles > 0) body += `| go source | ${goFiles} |\n`;
if (testFiles > 0) body += `| tests | ${testFiles} |\n`;
if (ciFiles > 0) body += `| ci/workflows | ${ciFiles} |\n`;
if (modFiles > 0) body += `| deps | ${modFiles} |\n`;
body += `\n`;
}
// find existing bot comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
});
const marker = '<!-- sif-pr-bot -->';
body = marker + '\n' + body;
const existing = comments.find(c =>
c.user.type === 'Bot' && c.body.includes(marker)
);
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body,
});
}
+175 -40
View File
@@ -1,8 +1,9 @@
name: Release
name: release
on:
push:
branches: [main]
tags:
- "v*"
permissions:
contents: write
@@ -18,58 +19,192 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version: "1.25"
- name: Build for Windows
- name: extract version
run: |
GOOS=windows GOARCH=amd64 go build -o sif-windows-amd64.exe ./cmd/sif
GOOS=windows GOARCH=386 go build -o sif-windows-386.exe ./cmd/sif
echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
# single source of truth so the cross-compile steps can't drift
echo "LDFLAGS=-s -w -X main.version=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
- name: Build for macOS
- name: build for windows
run: |
GOOS=darwin GOARCH=amd64 go build -o sif-macos-amd64 ./cmd/sif
GOOS=darwin GOARCH=arm64 go build -o sif-macos-arm64 ./cmd/sif
GOOS=windows GOARCH=amd64 go build -ldflags="${{ env.LDFLAGS }}" -o sif-windows-amd64.exe ./cmd/sif
GOOS=windows GOARCH=386 go build -ldflags="${{ env.LDFLAGS }}" -o sif-windows-386.exe ./cmd/sif
- name: Build for Linux
- name: build for macOS
run: |
GOOS=linux GOARCH=amd64 go build -o sif-linux-amd64 ./cmd/sif
GOOS=linux GOARCH=386 go build -o sif-linux-386 ./cmd/sif
GOOS=linux GOARCH=arm64 go build -o sif-linux-arm64 ./cmd/sif
GOOS=darwin GOARCH=amd64 go build -ldflags="${{ env.LDFLAGS }}" -o sif-macos-amd64 ./cmd/sif
GOOS=darwin GOARCH=arm64 go build -ldflags="${{ env.LDFLAGS }}" -o sif-macos-arm64 ./cmd/sif
- name: Set release version
run: echo "RELEASE_VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: build for linux
run: |
GOOS=linux GOARCH=amd64 go build -ldflags="${{ env.LDFLAGS }}" -o sif-linux-amd64 ./cmd/sif
GOOS=linux GOARCH=386 go build -ldflags="${{ env.LDFLAGS }}" -o sif-linux-386 ./cmd/sif
GOOS=linux GOARCH=arm64 go build -ldflags="${{ env.LDFLAGS }}" -o sif-linux-arm64 ./cmd/sif
- name: Create Release and Upload Assets
uses: softprops/action-gh-release@v2
- name: package releases with modules
run: |
for binary in sif-linux-amd64 sif-linux-386 sif-linux-arm64 sif-macos-amd64 sif-macos-arm64; do
mkdir -p "dist/${binary}"
cp "${binary}" "dist/${binary}/sif"
cp -r modules "dist/${binary}/"
tar -czf "${binary}.tar.gz" -C dist "${binary}"
done
for binary in sif-windows-amd64 sif-windows-386; do
mkdir -p "dist/${binary}"
cp "${binary}.exe" "dist/${binary}/sif.exe"
cp -r modules "dist/${binary}/"
cd dist && zip -r "../${binary}.zip" "${binary}" && cd ..
done
- name: build debian packages
run: |
declare -A arch_map=(
["sif-linux-amd64"]="amd64"
["sif-linux-386"]="i386"
["sif-linux-arm64"]="arm64"
)
for binary in sif-linux-amd64 sif-linux-386 sif-linux-arm64; do
arch="${arch_map[$binary]}"
pkg_dir="sif_${{ env.VERSION }}_${arch}"
mkdir -p "${pkg_dir}/DEBIAN"
mkdir -p "${pkg_dir}/usr/bin"
mkdir -p "${pkg_dir}/usr/share/sif/modules"
cp "${binary}" "${pkg_dir}/usr/bin/sif"
chmod 755 "${pkg_dir}/usr/bin/sif"
cp -r modules/* "${pkg_dir}/usr/share/sif/modules/"
cat > "${pkg_dir}/DEBIAN/control" << EOF
Package: sif
Version: ${{ env.VERSION }}
Section: security
Priority: optional
Architecture: ${arch}
Maintainer: vmfunc <celeste@linux.com>
Homepage: https://github.com/vmfunc/sif
Description: Modular pentesting toolkit
sif is a fast, concurrent, and extensible pentesting toolkit written in Go.
It supports multiple scan types including directory fuzzing, subdomain
enumeration, port scanning, and vulnerability detection.
EOF
dpkg-deb --build "${pkg_dir}"
done
- name: generate checksums
run: |
sha256sum \
sif-windows-amd64.zip \
sif-windows-386.zip \
sif-macos-amd64.tar.gz \
sif-macos-arm64.tar.gz \
sif-linux-amd64.tar.gz \
sif-linux-386.tar.gz \
sif-linux-arm64.tar.gz \
sif_*.deb \
> checksums-sha256.txt
- name: generate SBOM
uses: anchore/sbom-action@v0
with:
tag_name: automated-release-${{ env.RELEASE_VERSION }}
name: Release ${{ env.RELEASE_VERSION }}
artifact-name: sbom.spdx.json
output-file: sbom.spdx.json
- name: generate changelog
id: changelog
uses: actions/github-script@v9
with:
result-encoding: string
script: |
const { data: releases } = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 1,
});
const prev = releases.length > 0 ? releases[0].tag_name : '';
const range = prev ? `${prev}...${context.ref}` : '';
const { data: commits } = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: prev ? `${prev}...${{ github.ref_name }}` : `${{ github.sha }}~10...${{ github.sha }}`,
}).catch(() => ({ data: { commits: [] } }));
let log = '';
for (const c of commits.commits || []) {
const msg = c.commit.message.split('\n')[0];
const sha = c.sha.substring(0, 7);
log += `- ${msg} (${sha})\n`;
}
return log || 'initial release';
- name: create release
uses: softprops/action-gh-release@v3
with:
name: sif v${{ env.VERSION }}
body: |
Automated release v${{ env.RELEASE_VERSION }}
## what's changed
## Assets
- Windows (64-bit): `sif-windows-amd64.exe`
- Windows (32-bit): `sif-windows-386.exe`
- macOS (64-bit Intel): `sif-macos-amd64`
- macOS (64-bit ARM): `sif-macos-arm64`
- Linux (64-bit): `sif-linux-amd64`
- Linux (32-bit): `sif-linux-386`
- Linux (64-bit ARM): `sif-linux-arm64`
${{ steps.changelog.outputs.result }}
For more details, check the [commit history](https://github.com/${{ github.repository }}/commits/main).
## install
**homebrew / linuxbrew**
```bash
# coming soon
```
**debian / ubuntu**
```bash
sudo dpkg -i sif_${{ env.VERSION }}_amd64.deb
```
**go install**
```bash
go install github.com/dropalldatabases/sif/cmd/sif@v${{ env.VERSION }}
```
**binary download** - grab the right archive from below.
## verification
```bash
sha256sum -c checksums-sha256.txt
```
draft: false
prerelease: false
prerelease: ${{ contains(github.ref_name, '-') }}
files: |
sif-windows-amd64.exe
sif-windows-386.exe
sif-macos-amd64
sif-macos-arm64
sif-linux-amd64
sif-linux-386
sif-linux-arm64
sif-windows-amd64.zip
sif-windows-386.zip
sif-macos-amd64.tar.gz
sif-macos-arm64.tar.gz
sif-linux-amd64.tar.gz
sif-linux-386.tar.gz
sif-linux-arm64.tar.gz
sif_*_amd64.deb
sif_*_i386.deb
sif_*_arm64.deb
checksums-sha256.txt
sbom.spdx.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: push to cloudsmith
if: ${{ !contains(github.ref_name, '-') }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
pip install cloudsmith-cli
for deb in sif_*.deb; do
cloudsmith push deb sif/deb/any-distro/any-version "$deb" -k "$CLOUDSMITH_API_KEY"
done
+10 -3
View File
@@ -1,4 +1,4 @@
name: Update Report Card
name: update report card
on:
push:
@@ -7,10 +7,17 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
update-report-card:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update Go Report Card
- uses: actions/checkout@v6
- name: update go report card
uses: creekorful/goreportcard-action@v1.0
+23 -7
View File
@@ -1,4 +1,4 @@
name: Functional Test
name: functional test
on:
push:
@@ -7,18 +7,21 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
- uses: actions/checkout@v6
- name: set up go
uses: actions/setup-go@v5
with:
go-version: "1.23"
- name: Build Sif
go-version: "1.25"
- name: build sif
run: make
- name: Run Sif with features
- name: run sif with features
run: |
./sif -u https://example.com -dnslist small -dirlist small -dork -git -whois -cms -framework
if [ $? -eq 0 ]; then
@@ -27,3 +30,16 @@ jobs:
echo "Sif exited with an error"
exit 1
fi
- name: test module system
run: |
echo "Listing modules..."
./sif -lm
echo "Running all modules..."
./sif -u https://example.com -am
if [ $? -eq 0 ]; then
echo "Module system working"
else
echo "Module system failed"
exit 1
fi
+30
View File
@@ -0,0 +1,30 @@
name: scorecard
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1" # monday 06:00 UTC
permissions: read-all
jobs:
analysis:
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: run scorecard
uses: ossf/scorecard-action@v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: true
- name: upload sarif results
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: results.sarif
+7 -3
View File
@@ -1,18 +1,22 @@
name: Shell Check
name: shell check
on:
pull_request:
paths:
- "**/*.sh"
permissions:
contents: read
pull-requests: write
jobs:
shellcheck:
name: runner / shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: shellcheck
uses: reviewdog/action-shellcheck@v1.27.0
uses: reviewdog/action-shellcheck@v1.32.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
+7 -3
View File
@@ -1,4 +1,4 @@
name: YAML Lint
name: yaml lint
on:
pull_request:
@@ -6,14 +6,18 @@ on:
- "**/*.yml"
- "**/*.yaml"
permissions:
contents: read
pull-requests: write
jobs:
yamllint:
name: runner / yamllint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: yamllint
uses: reviewdog/action-yamllint@v1.19.0
uses: reviewdog/action-yamllint@v1.21.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
+87 -16
View File
@@ -1,23 +1,94 @@
linters:
enable:
- errcheck # check error returns
- govet # suspicious constructs
- staticcheck # advanced static analysis
- unused # unused code
- gosimple # simplifications
- ineffassign # useless assignments
- misspell # spelling mistakes
linters-settings:
govet:
enable-all: true
errcheck:
check-blank: false
---
version: "2"
run:
timeout: 5m
issues-exit-code: 1
linters:
enable:
- errcheck # check error returns
- govet # suspicious constructs
- staticcheck # advanced static analysis (absorbs gosimple in v2)
- unused # unused code
- ineffassign # useless assignments
- misspell # spelling mistakes
- gocritic # opinionated lints
- revive # replacement for golint
- unconvert # unnecessary type conversions
- bodyclose # http response body not closed
- noctx # http requests without context
- gosec # security issues
- errorlint # error wrapping and comparison
- nilnil # return nil, nil
- wastedassign # assignments to variables never read
- usetesting # os.Setenv in tests instead of t.Setenv, etc.
settings:
govet:
enable-all: true
disable:
# too many structs to reorder, risks breaking serialization
- fieldalignment
- shadow # common Go pattern, too noisy
- unusedwrite # false positives on test data structs
errcheck:
check-blank: false
exclude-functions:
# log writes are best-effort
- github.com/dropalldatabases/sif/internal/logger.Write
# Close on io.Closer is idiomatic best-effort
- (io.Closer).Close
- (*os.File).Close
- (*net/http.Response).Body.Close
# fmt.Fprint* returns are rarely actionable
- fmt.Fprint
- fmt.Fprintf
- fmt.Fprintln
staticcheck:
# QF1003/QF1012 are v2 quickfix suggestions, not bugs.
# ST1000/ST1003 were the stylecheck linter in v1
# (not previously enabled); skipping to match prior parity.
checks:
- all
- -QF1003
- -QF1012
- -ST1000
- -ST1003
revive:
rules:
# stuttering names (scan.ScanResult) need breaking API changes
- name: exported
disabled: true
gocritic:
enabled-tags:
- diagnostic
- style
- performance
disabled-checks:
- commentedOutCode # too opinionated for a project with TODOs
- paramTypeCombine # style-only, not worth churn
- unnamedResult # style-only
- unnecessaryDefer # common pattern in tests
# inverting conditions in scan logic hurts readability
- nestingReduce
gosec:
excludes:
- G104 # errcheck covers this
- G107 # pentesting tool -- variable URLs are the whole point
- G110 # nuclei template decompression, acceptable context
- G304 # sif reads user-supplied wordlist paths -- intentional
- G305 # tar extraction is traversal-guarded (HasPrefix on the
# cleaned target); gosec flags filepath.Join regardless
exclusions:
rules:
# test files get some slack
- path: _test\.go
linters:
- errcheck
- noctx
issues:
max-issues-per-linter: 50
max-same-issues: 3
max-same-issues: 50
+52 -24
View File
@@ -4,17 +4,19 @@ Thank you for taking the time to contribute to sif! All contributions are valued
If you want to contribute but don't know where to start, worry not; there is no shortage of things to do.
Even if you don't know any Go, don't let that stop you from trying to contribute! We're here to help.
*By contributing to this repository, you agree to adhere to the sif [Code of Conduct](https://github.com/dropalldatabases/sif/blob/main/CODE_OF_CONDUCT.md). Not doing so may result in a ban.*
_By contributing to this repository, you agree to adhere to the sif [Code of Conduct](https://github.com/vmfunc/sif/blob/main/CODE_OF_CONDUCT.md). Not doing so may result in a ban._
## How can I help?
Here are some ways to get started:
- Have a look at our [issue tracker](https://github.com/dropalldatabases/sif/issues).
- Have a look at our [issue tracker](https://github.com/vmfunc/sif/issues).
- If you've encountered a bug, discuss it with us, [report it](#reporting-issues).
- Once you've found a bug you believe you can fix, open a [pull request](#contributing-code) for it.
- Alternatively, consider [packaging sif for your distribution](#packaging).
If you like the project, but don't have time to contribute, that's okay too! Here are other ways to show your appreciation for the project:
- Use sif (seriously, that's enough)
- Star the repository
- Share sif with your friends
@@ -22,7 +24,7 @@ If you like the project, but don't have time to contribute, that's okay too! Her
## Reporting issues
If you believe you've found a bug, or you have a new feature to request, please hop on the [Discord server](https://discord.gg/dropalldatabases) first to discuss it.
If you believe you've found a bug, or you have a new feature to request, please hop on the [Discord server](https://discord.com/invite/sifcli) first to discuss it.
This way, if it's an easy fix, we could help you solve it more quickly, and if it's a feature request we could workshop it together into something more mature.
When opening an issue, please use the search tool and make sure that the issue has not been discussed before. In the case of a bug report, run sif with the `-d/-debug` flag for full debug logs.
@@ -33,7 +35,7 @@ When opening an issue, please use the search tool and make sure that the issue h
To develop sif, you'll need version 1.23 or later of the Go toolchain. After making your changes, run the program using `go run ./cmd/sif` to make sure it compiles and runs properly.
*Nix users:* the repository provides a flake that can be used to develop and run sif. Use `nix run`, `nix develop`, `nix build`, etc. Make sure to run `gomod2nix` if `go.mod` is changed.
_Nix users:_ the repository provides a flake that can be used to develop and run sif. Use `nix run`, `nix develop`, `nix build`, etc. Make sure to run `gomod2nix` if `go.mod` is changed.
### Submitting a pull request
@@ -55,21 +57,40 @@ If you have any questions, feel free to ask around on the IRC channel.
## Contributing Framework Detection Patterns
The framework detection module (`pkg/scan/frameworks/detect.go`) identifies web frameworks by analyzing HTTP headers and response bodies. To add support for a new framework:
The framework detection module (`internal/scan/frameworks/`) identifies web frameworks by analyzing HTTP headers and response bodies. Detectors are organized by category in the `detectors/` subdirectory:
### Adding a New Framework Signature
### Adding a New Framework Detector
1. Add your framework to the `frameworkSignatures` map:
1. Create a detector struct in the appropriate file in `detectors/`:
```go
"MyFramework": {
{Pattern: `unique-identifier`, Weight: 0.5},
{Pattern: `header-signature`, Weight: 0.4, HeaderOnly: true},
{Pattern: `body-signature`, Weight: 0.3},
},
// myframeworkDetector detects MyFramework.
type myframeworkDetector struct{}
func (d *myframeworkDetector) Name() string { return "MyFramework" }
func (d *myframeworkDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "unique-identifier", Weight: 0.5},
{Pattern: "header-signature", Weight: 0.4, HeaderOnly: true},
{Pattern: "body-signature", Weight: 0.3},
}
}
...
```
2. Register the detector in the `init()` function of the same file:
```go
func init() {
fw.Register(&myframeworkDetector{})
}
```
**Pattern Guidelines:**
- `Weight`: How much this signature contributes to detection (0.0-1.0)
- `HeaderOnly`: Set to `true` for HTTP header patterns
- Use unique identifiers that won't false-positive on other frameworks
@@ -77,10 +98,11 @@ The framework detection module (`pkg/scan/frameworks/detect.go`) identifies web
### Adding Version Detection
Add version patterns to `extractVersionWithConfidence()`:
Add version patterns to `version.go` in the `rawPatterns` map inside `init()`:
```go
"MyFramework": {
{`<meta name="generator" content="MyFramework v?(\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
{`MyFramework[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"myframework":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
@@ -88,7 +110,7 @@ Add version patterns to `extractVersionWithConfidence()`:
### Adding CVE Data
Add known vulnerabilities to the `knownCVEs` map:
Add known vulnerabilities to `cve.go` in the `knownCVEs` map:
```go
"MyFramework": {
@@ -115,11 +137,17 @@ func TestDetectFramework_MyFramework(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
// assertions...
}
```
Also add your framework to the registry test in `TestDetectorRegistry`:
```go
expectedDetectors := []string{"Laravel", "Django", ..., "MyFramework"}
```
### Future Enhancements (Help Wanted)
- **Custom Signature Support**: Allow users to define signatures via config file
@@ -131,18 +159,18 @@ func TestDetectFramework_MyFramework(t *testing.T) {
### Framework Detection Flags
| Flag | Description |
|------|-------------|
| `-framework` | Enable framework detection |
| `-timeout` | HTTP request timeout (affects all modules) |
| `-threads` | Number of concurrent workers |
| `-log` | Directory to save scan results |
| `-debug` | Enable debug logging for verbose output |
| Flag | Description |
| ------------ | ------------------------------------------ |
| `-framework` | Enable framework detection |
| `-timeout` | HTTP request timeout (affects all modules) |
| `-threads` | Number of concurrent workers |
| `-log` | Directory to save scan results |
| `-debug` | Enable debug logging for verbose output |
### Environment Variables
| Variable | Description |
|----------|-------------|
| Variable | Description |
| ---------------- | ------------------------------------ |
| `SHODAN_API_KEY` | API key for Shodan host intelligence |
## Packaging
+1 -1
View File
@@ -1,6 +1,6 @@
BSD 3-Clause License
Copyright (c) 2022-2025 vmfunc (vmfunc), xyzeva, lunchcat alumni,
Copyright (c) 2022-2025 vmfunc, xyzeva, lunchcat alumni,
and other sif contributors.
Redistribution and use in source and binary forms, with or without
+11 -1
View File
@@ -9,6 +9,12 @@ RM ?= rm
GOFLAGS ?=
PREFIX ?= /usr/local
BINDIR ?= bin
MANDIR ?= share/man/man1
# stamp local builds with the nearest v* tag (or short sha), matching the
# release ci. --match keeps the automated-release-* tags out of the version.
VERSION ?= $(shell git describe --tags --match 'v*' --always --dirty 2>/dev/null | sed 's/^v//')
GO_LDFLAGS = -X main.version=$(VERSION)
define COPYRIGHT_ASCII
@@ -56,7 +62,7 @@ sif: check_go_version
@echo "📁 Current directory: $$(pwd)"
@echo "🔧 Go flags: $(GOFLAGS)"
@echo "📦 Building package: ./cmd/sif"
$(GO) build -v $(GOFLAGS) ./cmd/sif
$(GO) build -v $(GOFLAGS) -ldflags "$(GO_LDFLAGS)" ./cmd/sif
@echo "📊 Build info:"
@$(GO) version -m sif
@echo "✅ sif built successfully! 🚀"
@@ -76,6 +82,9 @@ install: check_go_version
fi
@mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo mkdir -p $(DESTDIR)$(PREFIX)/$(BINDIR))
@cp -f sif $(DESTDIR)$(PREFIX)/$(BINDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo cp -f sif $(DESTDIR)$(PREFIX)/$(BINDIR))
@echo "📖 Installing man page..."
@mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo mkdir -p $(DESTDIR)$(PREFIX)/$(MANDIR))
@cp -f man/sif.1 $(DESTDIR)$(PREFIX)/$(MANDIR) || (echo "🔒 Permission denied. Trying with sudo..." && sudo cp -f man/sif.1 $(DESTDIR)$(PREFIX)/$(MANDIR))
@echo "✅ sif installed successfully! 🎊"
uninstall:
@@ -86,6 +95,7 @@ uninstall:
exit 1; \
fi
@$(RM) $(DESTDIR)$(PREFIX)/$(BINDIR)/sif || (echo "🔒 Permission denied. Trying with sudo..." && sudo $(RM) $(DESTDIR)$(PREFIX)/$(BINDIR)/sif)
@$(RM) $(DESTDIR)$(PREFIX)/$(MANDIR)/sif.1 || (echo "🔒 Permission denied. Trying with sudo..." && sudo $(RM) $(DESTDIR)$(PREFIX)/$(MANDIR)/sif.1)
@echo "✅ sif uninstalled successfully!"
.PHONY: all check_go_version sif clean install uninstall
+167 -27
View File
@@ -7,9 +7,13 @@
[![go version](https://img.shields.io/github/go-mod/go-version/vmfunc/sif?style=flat-square&color=00ADD8)](https://go.dev/)
[![build](https://img.shields.io/github/actions/workflow/status/vmfunc/sif/go.yml?style=flat-square)](https://github.com/vmfunc/sif/actions)
[![license](https://img.shields.io/badge/license-BSD--3--Clause-blue?style=flat-square)](LICENSE)
[![discord](https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/sifcli)
[![aur](https://img.shields.io/aur/version/sif?style=flat-square&logo=archlinux&logoColor=white&color=1793D1)](https://aur.archlinux.org/packages/sif)
[![nixpkgs](https://img.shields.io/badge/nixpkgs-sif-5277C3?style=flat-square&logo=nixos&logoColor=white)](https://search.nixos.org/packages?query=sif)
[![homebrew](https://img.shields.io/badge/homebrew-tap-FBB040?style=flat-square&logo=homebrew&logoColor=white)](https://github.com/vmfunc/homebrew-sif)
[![apt](https://img.shields.io/badge/apt-cloudsmith-2A5ADF?style=flat-square&logo=debian&logoColor=white)](https://cloudsmith.io/~sif/repos/deb/packages/)
[![discord](https://img.shields.io/badge/discord-join-5865F2?style=flat-square&logo=discord&logoColor=white)](https://discord.gg/Yksy9J2BvE)
**[install](#install) · [usage](#usage) · [modules](#modules) · [contribute](#contribute)**
**[install](#install) · [usage](#usage) · [modules](#modules) · [docs](docs/) · [contribute](#contribute)**
</div>
@@ -20,11 +24,54 @@
sif is a modular pentesting toolkit written in go. it's designed to be fast, concurrent, and extensible. run multiple scan types against targets with a single command.
```bash
./sif -u https://example.com -all
./sif -u https://example.com -headers -sh -cms -framework -git
```
## install
### homebrew (macos)
```bash
brew tap vmfunc/sif
brew install sif
```
### arch linux (aur)
install using your preferred aur helper:
```bash
yay -S sif
# or
paru -S sif
```
### nix
```bash
# nixpkgs (declarative — add to configuration.nix or home-manager)
environment.systemPackages = [ pkgs.sif ];
# or imperatively
nix profile install nixpkgs#sif
# or just run it without installing
nix run nixpkgs#sif -- -u https://example.com -headers -sh -framework
```
the repo also ships a flake if you want to build from source:
```bash
nix run github:vmfunc/sif
```
### debian/ubuntu (apt)
```bash
curl -1sLf 'https://dl.cloudsmith.io/public/sif/deb/setup.deb.sh' | sudo -E bash
sudo apt-get install sif
```
### from releases
grab the latest binary from [releases](https://github.com/vmfunc/sif/releases).
@@ -32,13 +79,21 @@ grab the latest binary from [releases](https://github.com/vmfunc/sif/releases).
### from source
```bash
git clone https://github.com/dropalldatabases/sif.git
git clone https://github.com/vmfunc/sif.git
cd sif
make
```
requires go 1.23+
### aur (manual install)
```bash
git clone https://aur.archlinux.org/sif.git
cd sif
makepkg -si
```
## usage
```bash
@@ -60,38 +115,119 @@ requires go 1.23+
# shodan host intelligence (requires SHODAN_API_KEY env var)
./sif -u https://example.com -shodan
# securitytrails domain discovery (requires SECURITYTRAILS_API_KEY env var)
# discovers subdomains + associated domains, then scans all of them
./sif -u https://example.com -securitytrails -headers
# sql recon + lfi scanning
./sif -u https://example.com -sql -lfi
# framework detection (with cve lookup)
./sif -u https://example.com -framework
# everything
./sif -u https://example.com -all
# a broad sweep
./sif -u https://example.com -dirlist small -dnslist small -ports common -headers -sh -cms -framework -git -whois
```
run `./sif -h` for all options.
## commands
a couple of subcommands run without scanning:
```bash
# print the version (release builds are stamped; local builds use git describe)
./sif version
# show the latest release notes (also -pn)
./sif patchnote
```
the first time you run a new release, sif prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to turn that off.
## modules
| module | description |
|--------|-------------|
| `dirlist` | directory and file fuzzing |
| `dnslist` | subdomain enumeration |
| `ports` | port and service scanning |
| `nuclei` | vulnerability scanning with nuclei templates |
| `dork` | automated google dorking |
| `js` | javascript framework detection (next.js, supabase) |
| `c3` | cloud storage misconfiguration scanning |
| `headers` | http header analysis |
| `takeover` | subdomain takeover detection |
| `cms` | cms detection |
| `whois` | whois lookups |
| `git` | exposed git repository detection |
| `shodan` | shodan host intelligence (requires SHODAN_API_KEY) |
| `sql` | sql admin panel and error disclosure detection |
| `lfi` | local file inclusion vulnerability scanning |
| `framework` | web framework detection with version + cve lookup |
sif has a modular architecture. modules are defined in yaml and can be extended by users.
### built-in scan flags
| flag | description |
|------|-------------|
| `-dirlist` | directory and file fuzzing (small/medium/large) |
| `-dnslist` | subdomain enumeration (small/medium/large) |
| `-ports` | port scanning (common/full) |
| `-nuclei` | vulnerability scanning with nuclei templates |
| `-dork` | automated google dorking |
| `-js` | javascript analysis |
| `-c3` | cloud storage misconfiguration |
| `-headers` | http header analysis |
| `-sh` | security header analysis (missing/weak headers) |
| `-st` | subdomain takeover detection |
| `-cms` | cms detection |
| `-whois` | whois lookups |
| `-git` | exposed git repository detection |
| `-shodan` | shodan lookup (requires SHODAN_API_KEY) |
| `-securitytrails` | domain discovery + target expansion (requires SECURITYTRAILS_API_KEY) |
| `-sql` | sql recon |
| `-lfi` | local file inclusion |
| `-framework` | framework detection with cve lookup |
### yaml modules
list available modules:
```bash
./sif -lm
```
run specific modules:
```bash
# run by id
./sif -u https://example.com -m sqli-error-based,xss-reflected
# run by tag
./sif -u https://example.com -mt owasp-top10
# run all modules
./sif -u https://example.com -am
```
### custom modules
create your own modules in `~/.config/sif/modules/`. modules use a yaml format similar to nuclei templates:
```yaml
id: my-custom-check
info:
name: my custom security check
author: you
severity: medium
description: checks for something specific
tags: [custom, recon]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/admin"
- "{{BaseURL}}/login"
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "admin panel"
- "login"
condition: or
```
see [docs/modules.md](docs/modules.md) for the full module format.
## contribute
@@ -122,12 +258,16 @@ join our discord for support, feature discussions, and pentesting tips:
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://vmfunc.re"><img src="https://avatars.githubusercontent.com/u/59031302?v=4?s=100" width="100px;" alt="mel"/><br /><sub><b>mel</b></sub></a><br /><a href="#maintenance-vmfunc" title="Maintenance">🚧</a> <a href="#mentoring-vmfunc" title="Mentoring">🧑‍🏫</a> <a href="#projectManagement-vmfunc" title="Project Management">📆</a> <a href="#security-vmfunc" title="Security">🛡️</a> <a href="#test-vmfunc" title="Tests">⚠️</a> <a href="#business-vmfunc" title="Business development">💼</a> <a href="#code-vmfunc" title="Code">💻</a> <a href="#design-vmfunc" title="Design">🎨</a> <a href="#financial-vmfunc" title="Financial">💵</a> <a href="#ideas-vmfunc" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://vmfunc.re"><img src="https://avatars.githubusercontent.com/u/59031302?v=4?s=100" width="100px;" alt="vmfunc"/><br /><sub><b>vmfunc</b></sub></a><br /><a href="#maintenance-vmfunc" title="Maintenance">🚧</a> <a href="#mentoring-vmfunc" title="Mentoring">🧑‍🏫</a> <a href="#projectManagement-vmfunc" title="Project Management">📆</a> <a href="#security-vmfunc" title="Security">🛡️</a> <a href="https://github.com/lunchcat/sif/commits?author=vmfunc" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://projectdiscovery.io"><img src="https://avatars.githubusercontent.com/u/50994705?v=4?s=100" width="100px;" alt="ProjectDiscovery"/><br /><sub><b>ProjectDiscovery</b></sub></a><br /><a href="#platform-projectdiscovery" title="Packaging/porting to new platform">📦</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/macdoos"><img src="https://avatars.githubusercontent.com/u/127897805?v=4?s=100" width="100px;" alt="macdoos"/><br /><sub><b>macdoos</b></sub></a><br /><a href="#code-macdoos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/macdoos"><img src="https://avatars.githubusercontent.com/u/127897805?v=4?s=100" width="100px;" alt="macdoos"/><br /><sub><b>macdoos</b></sub></a><br /><a href="https://github.com/lunchcat/sif/commits?author=macdoos" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://epitech.eu"><img src="https://avatars.githubusercontent.com/u/75166283?v=4?s=100" width="100px;" alt="Matthieu Witrowiez"/><br /><sub><b>Matthieu Witrowiez</b></sub></a><br /><a href="#ideas-D3adPlays" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tessa-u-k"><img src="https://avatars.githubusercontent.com/u/109355732?v=4?s=100" width="100px;" alt="tessa "/><br /><sub><b>tessa </b></sub></a><br /><a href="#infra-tessa-u-k" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#question-tessa-u-k" title="Answering Questions">💬</a> <a href="#userTesting-tessa-u-k" title="User Testing">📓</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xyzeva"><img src="https://avatars.githubusercontent.com/u/133499694?v=4?s=100" width="100px;" alt="Eva"/><br /><sub><b>Eva</b></sub></a><br /><a href="#blog-xyzeva" title="Blogposts">📝</a> <a href="#content-xyzeva" title="Content">🖋</a> <a href="#research-xyzeva" title="Research">🔬</a> <a href="#security-xyzeva" title="Security">🛡️</a> <a href="#test-xyzeva" title="Tests">⚠️</a> <a href="#code-xyzeva" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xyzeva"><img src="https://avatars.githubusercontent.com/u/133499694?v=4?s=100" width="100px;" alt="Eva"/><br /><sub><b>Eva</b></sub></a><br /><a href="#blog-xyzeva" title="Blogposts">📝</a> <a href="#content-xyzeva" title="Content">🖋</a> <a href="#research-xyzeva" title="Research">🔬</a> <a href="#security-xyzeva" title="Security">🛡️</a> <a href="https://github.com/lunchcat/sif/commits?author=xyzeva" title="Tests">⚠️</a> <a href="https://github.com/lunchcat/sif/commits?author=xyzeva" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/vxfemboy"><img src="https://avatars.githubusercontent.com/u/79362520?v=4?s=100" width="100px;" alt="Zoa Hickenlooper"/><br /><sub><b>Zoa Hickenlooper</b></sub></a><br /><a href="https://github.com/lunchcat/sif/commits?author=vxfemboy" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0xatrilla"><img src="https://avatars.githubusercontent.com/u/107285362?v=4?s=100" width="100px;" alt="acxtrilla"/><br /><sub><b>acxtrilla</b></sub></a><br /><a href="#platform-0xatrilla" title="Packaging/porting to new platform">📦</a></td>
</tr>
</tbody>
</table>
+15
View File
@@ -0,0 +1,15 @@
# security policy
## reporting a vulnerability
if you find a security issue in sif, email celeste@linux.com directly.
don't open a public issue.
expect a response within 48 hours. if it's confirmed, i'll push a fix
and credit you in the release notes (unless you'd rather stay anonymous).
## scope
sif is a pentesting tool — "it can scan things" is not a vulnerability.
actual bugs: command injection in user input handling, path traversal in
template extraction, credential leaks, that kind of thing.
+32 -2
View File
@@ -4,7 +4,7 @@
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
@@ -13,12 +13,38 @@
package main
import (
"fmt"
"os"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif"
"github.com/dropalldatabases/sif/pkg/config"
"github.com/dropalldatabases/sif/internal/config"
"github.com/dropalldatabases/sif/internal/patchnotes"
ver "github.com/dropalldatabases/sif/internal/version"
// Register framework detectors
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
)
// version is stamped at release time via -ldflags "-X main.version=...";
// ver.Resolve falls back to the build info or "dev" for other builds.
var version = "dev"
func main() {
version = ver.Resolve(version)
sif.Version = version
if len(os.Args) > 1 {
switch os.Args[1] {
case "patchnote", "patchnotes", "-pn", "--patchnotes":
patchnotes.Print("")
return
case "version", "-version", "--version":
fmt.Printf("sif %s\n", version)
return
}
}
settings := config.Parse()
app, err := sif.New(settings)
@@ -26,6 +52,10 @@ func main() {
log.Fatal(err)
}
if !settings.ApiMode {
patchnotes.ShowOnce(version)
}
err = app.Run()
if err != nil {
log.Fatal(err)
+52
View File
@@ -0,0 +1,52 @@
# sif documentation
welcome to the sif documentation. sif is a modular pentesting toolkit designed to be fast, concurrent, and extensible.
## table of contents
### getting started
- [installation](installation.md) - how to install sif
- [quickstart](quickstart.md) - get up and running in minutes
- [usage](usage.md) - command line options and examples
### features
- [scans](scans.md) - built-in security scans
- [modules](modules.md) - yaml module system and custom modules
### reference
- [configuration](configuration.md) - runtime configuration options
- [api mode](api-mode.md) - json output for automation
### contributing
- [development](development.md) - setting up a dev environment
- [writing modules](modules.md#writing-modules) - create your own modules
---
## quick links
```bash
# install
git clone https://github.com/dropalldatabases/sif.git && cd sif && make
# basic scan
./sif -u https://example.com
# list modules
./sif -lm
# run all modules
./sif -u https://example.com -am
# help
./sif -h
```
## support
- [github issues](https://github.com/vmfunc/sif/issues) - bug reports and feature requests
- [discord](https://discord.gg/sifcli) - community chat
+160
View File
@@ -0,0 +1,160 @@
# api mode
use sif's json output for automation and integration.
## enabling api mode
```bash
./sif -u https://example.com -api
```
## output format
api mode outputs json to stdout:
```json
{
"url": "https://example.com",
"results": [
{
"id": "module-id",
"data": {
"module_id": "module-id",
"target": "https://example.com",
"findings": [
{
"url": "https://example.com/.git/HEAD",
"severity": "high",
"evidence": "ref: refs/heads/main",
"extracted": {
"branch": "main"
}
}
]
}
}
]
}
```
## fields
### url
the target url that was scanned.
### results
array of module results.
### results[].id
module identifier.
### results[].data.findings
array of security findings from the module.
### findings[].url
the specific url where the finding was detected.
### findings[].severity
severity level: `info`, `low`, `medium`, `high`, `critical`
### findings[].evidence
evidence that triggered the finding (matched content, etc).
### findings[].extracted
extracted data from the response (versions, keys, etc).
## examples
### save to file
```bash
./sif -u https://example.com -api -am > results.json
```
### pipe to jq
```bash
./sif -u https://example.com -api -am | jq '.results[].data.findings[]'
```
### filter high severity
```bash
./sif -u https://example.com -api -am | jq '.results[].data.findings[] | select(.severity == "high")'
```
### extract urls
```bash
./sif -u https://example.com -api -am | jq -r '.results[].data.findings[].url'
```
## ci/cd integration
### github actions
```yaml
- name: run sif scan
run: |
./sif -u ${{ env.TARGET_URL }} -api -am > sif-results.json
- name: check for high severity findings
run: |
HIGH_COUNT=$(jq '[.results[].data.findings[] | select(.severity == "high" or .severity == "critical")] | length' sif-results.json)
if [ "$HIGH_COUNT" -gt 0 ]; then
echo "Found $HIGH_COUNT high/critical severity findings"
exit 1
fi
```
### gitlab ci
```yaml
security_scan:
script:
- ./sif -u $TARGET_URL -api -am > sif-results.json
- |
if jq -e '.results[].data.findings[] | select(.severity == "critical")' sif-results.json > /dev/null; then
echo "Critical findings detected"
exit 1
fi
artifacts:
paths:
- sif-results.json
```
## multiple targets
when scanning multiple urls, each target outputs a separate json object:
```bash
./sif -u https://site1.com,https://site2.com -api
```
outputs:
```json
{"url":"https://site1.com","results":[...]}
{"url":"https://site2.com","results":[...]}
```
use `jq -s` to combine into an array:
```bash
./sif -u https://site1.com,https://site2.com -api | jq -s '.'
```
## notes
- api mode suppresses banner and interactive output
- all output goes to stdout
- errors and warnings still go to stderr
- combine with `-l` flag to also save detailed logs
+162
View File
@@ -0,0 +1,162 @@
# configuration
runtime configuration options for sif.
## environment variables
### SHODAN_API_KEY
required for shodan lookups.
```bash
export SHODAN_API_KEY=your-api-key-here
./sif -u https://example.com -shodan
```
## command line options
### timeout
default request timeout is 10 seconds.
```bash
# increase for slow targets
./sif -u https://example.com -t 30s
# decrease for fast scans
./sif -u https://example.com -t 5s
```
### threads
default is 10 concurrent threads.
```bash
# more threads for faster scanning
./sif -u https://example.com --threads 50
# fewer threads to reduce load
./sif -u https://example.com --threads 5
```
### logging
save output to files:
```bash
./sif -u https://example.com -l ./logs
```
creates timestamped log files in the specified directory.
### debug mode
enable verbose logging:
```bash
./sif -u https://example.com -d
```
## user modules
place custom modules in:
- linux/macos: `~/.config/sif/modules/`
- windows: `%LOCALAPPDATA%\sif\modules\`
### directory structure
```
~/.config/sif/
├── modules/
│ ├── http/
│ │ └── my-sqli-check.yaml
│ ├── recon/
│ │ └── custom-paths.yaml
│ └── my-module.yaml
```
modules can be organized in subdirectories or placed directly in the modules folder.
### overriding built-in modules
user modules with the same id as built-in modules will override them:
```yaml
# ~/.config/sif/modules/sqli-error-based.yaml
# this overrides the built-in sqli-error-based module
id: sqli-error-based
info:
name: my custom sqli check
# ...
```
## performance tuning
### fast scans
```bash
./sif -u https://example.com \
--threads 50 \
-t 5s \
-dirlist small \
-dnslist small
```
### thorough scans
```bash
./sif -u https://example.com \
--threads 10 \
-t 30s \
-dirlist large \
-dnslist large \
-ports full
```
### low-impact scans
reduce load on target:
```bash
./sif -u https://example.com \
--threads 2 \
-t 10s
```
## output formats
### console (default)
human-readable output with colors and formatting.
### json (api mode)
```bash
./sif -u https://example.com -api
```
returns structured json:
```json
{
"url": "https://example.com",
"results": [
{
"id": "sqli-error-based",
"data": {
"findings": [...]
}
}
]
}
```
### log files
```bash
./sif -u https://example.com -l ./logs
```
creates separate log files for each scan type.
+193
View File
@@ -0,0 +1,193 @@
# development
setting up a development environment for sif.
## prerequisites
- go 1.25 or later
- git
- make
## clone and build
```bash
git clone https://github.com/dropalldatabases/sif.git
cd sif
make
```
## project structure
```
sif/
├── cmd/sif/ # entry point
│ └── main.go
├── sif.go # main application logic
├── internal/ # private packages
│ ├── config/ # configuration parsing
│ ├── logger/ # logging utilities
│ ├── modules/ # module system
│ ├── scan/ # built-in scans
│ └── styles/ # terminal styling
├── modules/ # built-in yaml modules
│ ├── http/ # http-based modules
│ ├── info/ # information gathering
│ └── recon/ # reconnaissance modules
├── docs/ # documentation
└── assets/ # images, etc
```
## running locally
```bash
# build
make
# run
./sif -u https://example.com
# run with debug
./sif -u https://example.com -d
```
## code quality
### format
```bash
gofmt -w .
```
### lint
```bash
golangci-lint run
```
### test
```bash
go test ./...
```
### race detection
```bash
go test -race ./...
```
## adding a new scan
1. create a new file in `internal/scan/`
2. implement the scan function
3. add flag to `internal/config/config.go`
4. integrate in `sif.go`
see existing scans for examples.
## adding a new module
create a yaml file in `modules/`:
```yaml
id: my-new-module
info:
name: my new security check
author: your-name
severity: medium
description: what this checks for
tags: [custom, security]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/path"
matchers:
- type: status
status:
- 200
```
see [modules.md](modules.md) for the full format.
## module system internals
the module system is in `internal/modules/`:
- `module.go` - core interface and types
- `registry.go` - module registration
- `loader.go` - discovery and loading
- `yaml.go` - yaml parsing
- `executor.go` - http execution
### adding a new module type
1. add type constant to `module.go`
2. implement executor in new file
3. update loader to handle new extension/type
## testing
### unit tests
```bash
go test ./internal/...
```
### integration tests
run the scanners against a local testbed that plants the artifacts each one
should find (network-free, behind a build tag):
```bash
go test -tags=integration ./internal/scan/...
```
### functional test
```bash
./sif -u https://example.com -am
```
### test modules
```bash
./sif -lm # list modules
./sif -u https://example.com -m my-module -d # test specific module
```
## pull requests
1. fork the repository
2. create a feature branch
3. make changes
4. run `gofmt -w .` and `golangci-lint run`
5. submit pr
### commit messages
use lowercase, present tense:
```
add sql injection module
fix timeout handling in http executor
update readme with new flags
```
## release process
releases are automated via github actions on push to main.
binaries are built for:
- linux (amd64, 386, arm64)
- macos (amd64, arm64)
- windows (amd64, 386)
## resources
- [go documentation](https://golang.org/doc/)
- [goflags](https://github.com/projectdiscovery/goflags) - cli parsing
- [nuclei templates](https://github.com/projectdiscovery/nuclei-templates) - module format inspiration
+93
View File
@@ -0,0 +1,93 @@
# installation
## from releases
download the latest binary for your platform from [releases](https://github.com/vmfunc/sif/releases).
### linux
```bash
# download
curl -LO https://github.com/vmfunc/sif/releases/latest/download/sif-linux-amd64
# make executable
chmod +x sif-linux-amd64
# move to path (optional)
sudo mv sif-linux-amd64 /usr/local/bin/sif
```
### macos
```bash
# intel
curl -LO https://github.com/vmfunc/sif/releases/latest/download/sif-macos-amd64
# apple silicon
curl -LO https://github.com/vmfunc/sif/releases/latest/download/sif-macos-arm64
chmod +x sif-macos-*
sudo mv sif-macos-* /usr/local/bin/sif
```
### windows
download `sif-windows-amd64.exe` from releases and add to your PATH.
## from source
requires go 1.25+
```bash
git clone https://github.com/dropalldatabases/sif.git
cd sif
make
```
the binary will be created in the current directory.
### install to system
```bash
sudo make install
```
this installs to `/usr/local/bin/sif`.
### uninstall
```bash
sudo make uninstall
```
## verify installation
```bash
./sif -h
```
you should see the help output with available flags.
## updating
### from releases
download the new binary and replace the old one.
### from source
```bash
cd sif
git pull
make clean
make
```
## modules directory
sif looks for modules in these locations:
- **built-in**: `modules/` directory next to the sif binary
- **user modules**: `~/.config/sif/modules/` (linux/macos) or `%LOCALAPPDATA%\sif\modules\` (windows)
user modules override built-in modules with the same id.
+387
View File
@@ -0,0 +1,387 @@
# writing sif modules
sif modules are yaml files that define security checks. they're similar to nuclei templates but designed specifically for sif.
## module locations
- **built-in**: `modules/` directory in the sif installation
- **user-defined**: `~/.config/sif/modules/` (linux/macos) or `%LOCALAPPDATA%\sif\modules\` (windows)
user modules can override built-in modules with the same id.
## basic structure
```yaml
id: unique-module-id
info:
name: human readable name
author: your-name
severity: low|medium|high|critical|info
description: what this module checks for
tags: [tag1, tag2, tag3]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/path"
matchers:
- type: status
status:
- 200
```
## fields
### id (required)
unique identifier for the module. use lowercase with hyphens.
```yaml
id: sqli-error-based
```
### info (required)
metadata about the module.
```yaml
info:
name: SQL Injection Detection
author: sif
severity: high
description: detects sql injection via error messages
tags: [sqli, injection, owasp-top10]
```
**severity levels:**
- `info` - informational finding
- `low` - minor issue
- `medium` - moderate security concern
- `high` - serious vulnerability
- `critical` - critical security flaw
### type (required)
module type. currently only `http` is supported.
```yaml
type: http
```
### http
http request configuration.
#### method
http method to use.
```yaml
http:
method: GET
```
supported: `GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `OPTIONS`
#### paths
urls to check. use `{{BaseURL}}` as placeholder for the target.
```yaml
http:
paths:
- "{{BaseURL}}/.git/HEAD"
- "{{BaseURL}}/.git/config"
- "{{BaseURL}}/admin"
```
#### payloads
values to inject into paths. use `{{payload}}` as placeholder.
```yaml
http:
paths:
- "{{BaseURL}}/?id={{payload}}"
payloads:
- "'"
- "1' OR '1'='1"
- "1; DROP TABLE--"
```
each payload creates a separate request for each path.
#### headers
custom headers to send.
```yaml
http:
headers:
User-Agent: "Mozilla/5.0"
X-Custom-Header: "value"
```
#### body
request body for POST/PUT requests.
```yaml
http:
method: POST
body: '{"username": "admin", "password": "{{payload}}"}'
```
#### threads
concurrent requests (default: 10).
```yaml
http:
threads: 5
```
## matchers
matchers determine if a response indicates a finding.
### status matcher
match http status codes.
```yaml
matchers:
- type: status
status:
- 200
- 301
- 302
```
### word matcher
match words in response.
```yaml
matchers:
- type: word
part: body
words:
- "admin"
- "login"
condition: or
```
**parts:**
- `body` - response body
- `header` - response headers
**conditions:**
- `or` - match any word (default)
- `and` - match all words
### regex matcher
match regex patterns.
```yaml
matchers:
- type: regex
part: body
regex:
- "SQL syntax.*MySQL"
- "ORA-[0-9]+"
- "PostgreSQL.*ERROR"
condition: or
```
### combining matchers
multiple matchers are combined with AND logic by default.
```yaml
matchers:
- type: status
status:
- 200
- type: word
part: body
words:
- "ref: refs/"
condition: or
```
this matches responses with status 200 AND containing "ref: refs/".
## extractors
extractors pull data from responses.
### regex extractor
```yaml
extractors:
- type: regex
name: version
part: body
regex:
- "version[\"']?\\s*[:=]\\s*[\"']?([0-9.]+)"
group: 1
```
**group**: capture group to extract (0 = full match, 1+ = groups)
### kv extractor
extract key-value pairs.
```yaml
extractors:
- type: kv
name: headers
part: header
```
## examples
### exposed git repository
```yaml
id: git-exposed
info:
name: exposed git repository
author: sif
severity: high
description: detects exposed .git directories
tags: [git, exposure, source-code]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/.git/HEAD"
- "{{BaseURL}}/.git/config"
matchers:
- type: word
part: body
words:
- "ref: refs/"
- "[core]"
condition: or
- type: status
status:
- 200
extractors:
- type: regex
name: branch
part: body
regex:
- "ref: refs/heads/(.+)"
group: 1
```
### sql injection detection
```yaml
id: sqli-error-based
info:
name: sql injection (error-based)
author: sif
severity: high
description: detects sql injection via database errors
tags: [sqli, injection, database]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/?id={{payload}}"
- "{{BaseURL}}/search?q={{payload}}"
payloads:
- "'"
- "1' OR '1'='1"
- "1; SELECT * FROM--"
threads: 10
matchers:
- type: regex
part: body
regex:
- "SQL syntax.*MySQL"
- "ORA-[0-9]+"
- "PostgreSQL.*ERROR"
- "Microsoft SQL Server"
condition: or
```
### security headers check
```yaml
id: security-headers
info:
name: security headers analysis
author: sif
severity: info
description: checks for missing security headers
tags: [headers, security, info]
type: http
http:
method: GET
paths:
- "{{BaseURL}}/"
matchers:
- type: status
status:
- 200
extractors:
- type: kv
name: headers
part: header
```
## tips
1. **use specific paths** - don't just check `/`, be specific about what you're looking for
2. **combine matchers** - use status + content matchers together to reduce false positives
3. **limit payloads** - too many payloads slow down scans, pick the most effective ones
4. **tag properly** - use consistent tags so modules can be filtered with `-mt`
5. **test locally** - run your module against a test target before sharing
## running modules
```bash
# list all modules
./sif -lm
# run specific module
./sif -u https://example.com -m git-exposed
# run multiple modules
./sif -u https://example.com -m git-exposed,sqli-error-based
# run by tag
./sif -u https://example.com -mt owasp-top10
# run all modules
./sif -u https://example.com -am
```
+102
View File
@@ -0,0 +1,102 @@
# quickstart
get up and running with sif in minutes.
## basic scan
run a basic scan against a target:
```bash
./sif -u https://example.com
```
this performs a base scan checking robots.txt, common files, and basic reconnaissance.
## add more scans
enable additional scan types with flags:
```bash
# directory fuzzing
./sif -u https://example.com -dirlist medium
# subdomain enumeration
./sif -u https://example.com -dnslist small
# port scanning
./sif -u https://example.com -ports common
# framework detection
./sif -u https://example.com -framework
```
## run modules
sif has a modular architecture with yaml-based security checks:
```bash
# list available modules
./sif -lm
# run all modules
./sif -u https://example.com -am
# run specific modules
./sif -u https://example.com -m sqli-error-based,xss-reflected
# run by tag
./sif -u https://example.com -mt owasp-top10
```
## multiple targets
scan multiple urls:
```bash
./sif -u https://site1.com,https://site2.com
```
or from a file:
```bash
./sif -f targets.txt
```
## save output
save results to a log directory:
```bash
./sif -u https://example.com -l ./logs
```
## json output
for automation, use api mode:
```bash
./sif -u https://example.com -api
```
## full scan example
run everything:
```bash
./sif -u https://example.com \
-dirlist medium \
-dnslist small \
-ports common \
-framework \
-js \
-headers \
-git \
-am \
-l ./logs
```
## next steps
- [usage](usage.md) - all command line options
- [scans](scans.md) - detailed scan descriptions
- [modules](modules.md) - write custom modules
+239
View File
@@ -0,0 +1,239 @@
# scans
detailed information about sif's built-in security scans.
## base scan
runs automatically unless `-noscan` is specified.
checks:
- robots.txt parsing
- common files (sitemap.xml, security.txt, etc)
- basic reconnaissance
## directory fuzzing (-dirlist)
brute-forces directories and files using wordlists.
### sizes
| size | entries | use case |
|------|---------|----------|
| small | ~1k | quick scan, low noise |
| medium | ~10k | balanced coverage |
| large | ~100k | thorough, takes longer |
### what it finds
- hidden directories (/admin, /backup, /config)
- backup files (.bak, .old, .zip)
- configuration files
- development artifacts
## subdomain enumeration (-dnslist)
discovers subdomains via dns brute-forcing.
### sizes
| size | entries | use case |
|------|---------|----------|
| small | ~1k | quick discovery |
| medium | ~10k | common subdomains |
| large | ~100k | comprehensive |
### what it finds
- dev/staging environments
- internal services
- forgotten subdomains
- api endpoints
## port scanning (-ports)
scans for open ports and identifies services.
### scopes
| scope | ports | description |
|-------|-------|-------------|
| common | top 1000 | most common services |
| full | 1-65535 | all ports, slow |
### what it finds
- web servers (80, 443, 8080)
- databases (3306, 5432, 27017)
- admin interfaces (8443, 9090)
- development servers
## framework detection (-framework)
identifies web frameworks and their versions.
### detects
- react, vue, angular, next.js
- django, flask, rails
- laravel, symfony, express
- wordpress, drupal, joomla
### features
- version detection
- cve lookup for known vulnerabilities
- confidence scoring
## javascript analysis (-js)
analyzes javascript files for security issues.
### finds
- api endpoints and keys
- hardcoded credentials
- internal urls
- framework configurations
- source maps
## http headers (-headers)
dumps the target's response headers.
## security headers (-sh)
flags missing or weak security headers and headers that leak server internals.
### checks
- strict-transport-security (https only)
- content-security-policy
- x-frame-options
- x-content-type-options (expects nosniff)
- referrer-policy
- permissions-policy
- cross-origin-opener-policy
### flagged as disclosure
- server
- x-powered-by
- x-aspnet-version / x-aspnetmvc-version
## cms detection (-cms)
identifies content management systems.
### detects
- wordpress (with version)
- drupal
- joomla
- magento
- shopify
- ghost
## git repository (-git)
checks for exposed git repositories.
### finds
- .git/HEAD
- .git/config
- .git/index
- source code exposure risk
## cloud storage (-c3)
checks for cloud storage misconfigurations.
### checks
- s3 bucket access
- azure blob storage
- gcp storage buckets
- open bucket policies
## subdomain takeover (-st)
detects subdomain takeover vulnerabilities.
requires `-dnslist` to enumerate subdomains first.
### checks
- dangling cname records
- unclaimed cloud services
- expired third-party services
## shodan lookup (-shodan)
queries shodan for host intelligence.
requires `SHODAN_API_KEY` environment variable.
### returns
- open ports
- services and versions
- known vulnerabilities
- ssl/tls info
- organization data
## sql reconnaissance (-sql)
detects sql-related exposures.
### finds
- admin panels (/phpmyadmin, /adminer)
- database error messages
- sql injection indicators
## lfi scanning (-lfi)
checks for local file inclusion vulnerabilities.
### tests
- path traversal (../)
- null byte injection
- common lfi payloads
- sensitive file disclosure
## whois lookup (-whois)
performs whois lookups on target domains.
### returns
- registrar info
- creation/expiration dates
- nameservers
- registrant info (if available)
## google dorking (-dork)
automated google dorking for target.
### searches
- indexed sensitive files
- exposed admin panels
- configuration files
- backup files
- error pages
## nuclei scanning (-nuclei)
runs nuclei vulnerability templates.
requires nuclei to be installed.
### templates
- cve detection
- misconfigurations
- exposures
- default credentials
+323
View File
@@ -0,0 +1,323 @@
# usage
complete guide to sif command line options.
## target options
### -u, --urls
specify target urls (comma-separated):
```bash
./sif -u https://example.com
./sif -u https://site1.com,https://site2.com
```
### -f, --file
read targets from a file (one url per line):
```bash
./sif -f targets.txt
```
## scan options
### directory fuzzing
`-dirlist <size>` - fuzz for directories and files
sizes: `small`, `medium`, `large`
```bash
./sif -u https://example.com -dirlist medium
```
### subdomain enumeration
`-dnslist <size>` - enumerate subdomains
sizes: `small`, `medium`, `large`
```bash
./sif -u https://example.com -dnslist small
```
### port scanning
`-ports <scope>` - scan for open ports
scopes: `common` (top ports), `full` (all ports)
```bash
./sif -u https://example.com -ports common
```
### google dorking
`-dork` - automated google dorking
```bash
./sif -u https://example.com -dork
```
### git repository detection
`-git` - check for exposed git repositories
```bash
./sif -u https://example.com -git
```
### nuclei scanning
`-nuclei` - run nuclei vulnerability templates
```bash
./sif -u https://example.com -nuclei
```
### javascript analysis
`-js` - analyze javascript files
```bash
./sif -u https://example.com -js
```
### cms detection
`-cms` - detect content management systems
```bash
./sif -u https://example.com -cms
```
### http headers
`-headers` - dump the target's response headers
```bash
./sif -u https://example.com -headers
```
### security headers
`-sh` - flag missing/weak security headers (hsts, csp, x-frame-options, ...) and headers that leak server internals
```bash
./sif -u https://example.com -sh
```
### cloud storage
`-c3` - check for cloud storage misconfigurations
```bash
./sif -u https://example.com -c3
```
### subdomain takeover
`-st` - check for subdomain takeover vulnerabilities
requires `-dnslist` to be enabled
```bash
./sif -u https://example.com -dnslist small -st
```
### shodan lookup
`-shodan` - query shodan for host intelligence
requires `SHODAN_API_KEY` environment variable
```bash
export SHODAN_API_KEY=your-api-key
./sif -u https://example.com -shodan
```
### sql reconnaissance
`-sql` - detect sql admin panels and error disclosure
```bash
./sif -u https://example.com -sql
```
### lfi scanning
`-lfi` - local file inclusion vulnerability checks
```bash
./sif -u https://example.com -lfi
```
### framework detection
`-framework` - detect web frameworks with version and cve lookup
```bash
./sif -u https://example.com -framework
```
### whois lookup
`-whois` - perform whois lookups
```bash
./sif -u https://example.com -whois
```
### skip base scan
`-noscan` - skip the base url scan (robots.txt, etc)
```bash
./sif -u https://example.com -noscan -dirlist medium
```
## module options
### -lm, --list-modules
list all available modules:
```bash
./sif -lm
```
### -m, --modules
run specific modules by id (comma-separated):
```bash
./sif -u https://example.com -m sqli-error-based,xss-reflected
```
### -mt, --module-tags
run modules matching tags:
```bash
./sif -u https://example.com -mt owasp-top10
./sif -u https://example.com -mt injection
```
### -am, --all-modules
run all available modules:
```bash
./sif -u https://example.com -am
```
## runtime options
### -t, --timeout
http request timeout (default: 10s):
```bash
./sif -u https://example.com -t 30s
```
### --threads
number of concurrent threads (default: 10):
```bash
./sif -u https://example.com --threads 20
```
### -l, --log
directory to save log files:
```bash
./sif -u https://example.com -l ./logs
```
### -d, --debug
enable debug logging:
```bash
./sif -u https://example.com -d
```
## api options
### -api
enable api mode for json output:
```bash
./sif -u https://example.com -api
```
output is a json object with scan results.
## commands
these run without scanning a target.
### version
print the sif version. release builds are stamped via ldflags, local `make` builds derive it from `git describe`, and `go install`ed builds read it from the module build info:
```bash
./sif version
```
### patchnote
show the latest release's notes, fetched from github (also `-pn`):
```bash
./sif patchnote
```
the first time you run a new release sif also prints that release's notes once. set `SIF_NO_PATCHNOTES=1` to disable that.
## examples
### quick recon
```bash
./sif -u https://example.com -framework -headers -git
```
### full scan
```bash
./sif -u https://example.com \
-dirlist large \
-dnslist medium \
-ports full \
-framework \
-js \
-headers \
-cms \
-git \
-sql \
-lfi \
-am
```
### ci/cd pipeline
```bash
./sif -u https://staging.example.com -api -am > results.json
```
### batch scanning
```bash
echo "https://site1.com
https://site2.com
https://site3.com" > targets.txt
./sif -f targets.txt -am -l ./logs
```
Generated
+4 -62
View File
@@ -1,35 +1,12 @@
{
"nodes": {
"gomod2nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"utils": [
"utils"
]
},
"locked": {
"lastModified": 1677459247,
"narHash": "sha256-JbakfAiPYmCCV224yAMq/XO0udN5coWv/oazblMKdoY=",
"owner": "tweag",
"repo": "gomod2nix",
"rev": "3cbf3a51fe32e2f57af4c52744e7228bab22983d",
"type": "github"
},
"original": {
"owner": "tweag",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1693844670,
"narHash": "sha256-t69F2nBB8DNQUWHD809oJZJVE+23XBrth4QZuVd6IE0=",
"lastModified": 1767364772,
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "3c15feef7770eb5500a4b8792623e2d6f598c9c1",
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
"type": "github"
},
"original": {
@@ -41,42 +18,7 @@
},
"root": {
"inputs": {
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
"nixpkgs": "nixpkgs"
}
}
},
+49 -28
View File
@@ -1,36 +1,57 @@
{
description = "a blazing-fast pentesting (recon/exploitation) suite";
description = "A blazing-fast pentesting (recon/exploitation) suite";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
gomod2nix = {
url = "github:tweag/gomod2nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.utils.follows = "utils";
};
};
outputs = { self, nixpkgs, utils, gomod2nix }:
utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs {
inherit system;
overlays = [ gomod2nix.overlays.default ];
outputs = { self, nixpkgs }:
let
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
in
{
packages = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
default = pkgs.buildGoModule {
pname = "sif";
version = "unstable-${self.shortRev or self.dirtyShortRev or "dev"}";
src = ./.;
vendorHash = "sha256-ztKXnOjZS/jMxsRjtF0rIZ3lKv4YjMdZd6oQFRuAtR4=";
# Tests require network access (httptest)
doCheck = false;
ldflags = [ "-s" "-w" ];
meta = with pkgs.lib; {
description = "Modular pentesting toolkit written in Go";
homepage = "https://github.com/vmfunc/sif";
license = licenses.bsd3;
mainProgram = "sif";
maintainers = [ ];
};
};
sif = self.packages.${system}.default;
});
overlays.default = final: prev: {
sif = self.packages.${final.system}.default;
};
in
{
packages.default = pkgs.buildGoApplication {
pname = "sif";
version = "0.1.0";
src = ./.;
modules = ./gomod2nix.toml;
};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
go
gomod2nix.packages.${system}.default
];
};
});
devShells = forAllSystems (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [ go gopls ];
};
});
};
}
+312 -141
View File
@@ -1,225 +1,396 @@
module github.com/dropalldatabases/sif
go 1.24.0
toolchain go1.25.5
go 1.25.7
require (
github.com/antchfx/htmlquery v1.3.0
github.com/charmbracelet/lipgloss v0.8.0
github.com/charmbracelet/log v0.2.4
github.com/likexian/whois v1.15.1
github.com/projectdiscovery/goflags v0.1.54
github.com/projectdiscovery/nuclei/v2 v2.9.14
github.com/projectdiscovery/ratelimit v0.0.9
github.com/projectdiscovery/utils v0.1.1
github.com/antchfx/htmlquery v1.3.6
github.com/charmbracelet/glamour v0.10.0
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
github.com/charmbracelet/log v1.0.0
github.com/likexian/whois v1.15.7
github.com/projectdiscovery/goflags v0.1.74
github.com/projectdiscovery/nuclei/v3 v3.8.0
github.com/projectdiscovery/utils v0.10.1
github.com/rocketlaunchr/google-search v1.1.6
gopkg.in/yaml.v3 v3.0.1
)
require (
aead.dev/minisign v0.2.0 // indirect
aead.dev/minisign v0.3.0 // indirect
carvel.dev/ytt v0.52.0 // indirect
code.gitea.io/sdk/gitea v0.17.0 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
git.mills.io/prologic/smtpd v0.0.0-20210710122116-a525b76c287a // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.1.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Mzack9999/gcache v0.0.0-20230410081825-519e28eab057 // indirect
github.com/Mzack9999/go-http-digest-auth-client v0.6.1-0.20220414142836-eb8883508809 // indirect
github.com/PuerkitoBio/goquery v1.8.1 // indirect
github.com/Mzack9999/go-rsync v0.0.0-20250821180103-81ffa574ef4d // indirect
github.com/Mzack9999/goja v0.0.0-20250507184235-e46100e9c697 // indirect
github.com/Mzack9999/goja_nodejs v0.0.0-20250507184139-66bcbf65c883 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/PuerkitoBio/goquery v1.11.0 // indirect
github.com/STARRY-S/zip v0.2.3 // indirect
github.com/VividCortex/ewma v1.2.0 // indirect
github.com/akrylysov/pogreb v0.10.1 // indirect
github.com/akrylysov/pogreb v0.10.2 // indirect
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/alecthomas/jsonschema v0.0.0-20211022214203-8b29eab41725 // indirect
github.com/alecthomas/chroma/v2 v2.20.0 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/andygrunwald/go-jira v1.16.0 // indirect
github.com/antchfx/xmlquery v1.3.15 // indirect
github.com/antchfx/xpath v1.2.4 // indirect
github.com/alexsnet/go-vnc v0.1.0 // indirect
github.com/alitto/pond v1.9.2 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/andygrunwald/go-jira v1.16.1 // indirect
github.com/antchfx/xmlquery v1.4.4 // indirect
github.com/antchfx/xpath v1.3.6 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aws/aws-sdk-go-v2 v1.19.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.18.28 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.13.27 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3 // indirect
github.com/aws/smithy-go v1.13.5 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
github.com/aws/aws-sdk-go-v2/config v1.29.17 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/bits-and-blooms/bitset v1.22.0 // indirect
github.com/bits-and-blooms/bloom/v3 v3.5.0 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/caddyserver/certmagic v0.19.2 // indirect
github.com/charmbracelet/glamour v0.6.0 // indirect
github.com/cheggaaa/pb/v3 v3.1.4 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.6.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/brianvoe/gofakeit/v7 v7.2.1 // indirect
github.com/buger/jsonparser v1.1.2 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/caddyserver/certmagic v0.25.0 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/censys/censys-sdk-go v0.19.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.3.2 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250908092851-c2208eb08494 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cheggaaa/pb/v3 v3.1.7 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/cloudflare/cfssl v1.6.4 // indirect
github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect
github.com/corpix/uarand v0.2.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cnf/structhash v0.0.0-20250313080605-df4c6cc74a9a // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/ditashi/jsbeautifier-go v0.0.0-20141206144643-2520a8026a9c // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/docker v28.3.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/ericlagergren/decimal v0.0.0-20240411145413-00de7ca16731 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/free5gc/util v1.0.5-0.20230511064842-2e120956883b // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gaissmai/bart v0.26.1 // indirect
github.com/geoffgarside/ber v1.1.0 // indirect
github.com/getkin/kin-openapi v0.132.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.9.1 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-git/go-git/v5 v5.19.1 // indirect
github.com/go-ldap/ldap/v3 v3.4.11 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-pdf/fpdf v0.9.0 // indirect
github.com/go-pg/pg/v10 v10.15.0 // indirect
github.com/go-pg/zerochecker v0.2.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.1 // indirect
github.com/go-rod/rod v0.114.0 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/go-rod/rod v0.116.2 // indirect
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/goburrow/cache v0.1.4 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gocolly/colly/v2 v2.1.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/certificate-transparency-go v1.1.4 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/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
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/gosimple/slug v1.15.0 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.8.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hbakhtiyor/strsim v0.0.0-20190107154042-4d2bbb273edf // indirect
github.com/hdm/jarm-go v0.0.7 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/itchyny/gojq v0.12.13 // indirect
github.com/itchyny/timefmt-go v0.1.5 // indirect
github.com/iangcarroll/cookiemonster v1.6.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/invopop/yaml v0.3.1 // indirect
github.com/itchyny/gojq v0.12.17 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jcmturner/aescts/v2 v2.0.0 // indirect
github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect
github.com/jcmturner/gofork v1.7.6 // indirect
github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect
github.com/jcmturner/rpc/v2 v2.0.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kataras/jwt v0.1.8 // indirect
github.com/k14s/starlark-go v0.0.0-20200720175618-3a5c849cc368 // indirect
github.com/kaiakz/ubuffer v0.0.0-20200803053910-dd1083087166 // indirect
github.com/kataras/jwt v0.1.10 // indirect
github.com/kennygrant/sanitize v1.2.4 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/libdns/libdns v0.2.1 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kitabisa/go-ci v1.0.3 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/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
github.com/libdns/libdns v1.1.1 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/lor00x/goldap v0.0.0-20180618054307-a546dffdd1a3 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mackerelio/go-osstat v0.2.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/logrusorgru/aurora/v4 v4.0.0 // indirect
github.com/lor00x/goldap v0.0.0-20240304151906-8d785c64d1c8 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/mackerelio/go-osstat v0.2.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.14 // indirect
github.com/mholt/acmez v1.2.0 // indirect
github.com/mholt/archiver v3.1.1+incompatible // indirect
github.com/mholt/archiver/v3 v3.5.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
github.com/miekg/dns v1.1.56 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/maypok86/otter/v2 v2.2.1 // indirect
github.com/mholt/acmez/v3 v3.1.3 // indirect
github.com/mholt/archives v0.1.5 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/microsoft/go-mssqldb v1.9.2 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mikelolasagasti/xz v1.0.1 // indirect
github.com/minio/minlz v1.0.1 // indirect
github.com/minio/selfupdate v0.6.1-0.20230907112617-f11e74f84ca7 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/nwaples/rardecode v1.1.3 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
github.com/pierrec/lz4/v4 v4.1.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.0.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.23 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/projectdiscovery/asnmap v1.1.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/praetorian-inc/fingerprintx v1.1.15 // indirect
github.com/projectdiscovery/asnmap v1.1.1 // indirect
github.com/projectdiscovery/blackrock v0.0.1 // indirect
github.com/projectdiscovery/cdncheck v1.0.9 // indirect
github.com/projectdiscovery/clistats v0.0.19 // indirect
github.com/projectdiscovery/dsl v0.0.20 // indirect
github.com/projectdiscovery/fastdialer v0.1.1 // indirect
github.com/projectdiscovery/cdncheck v1.2.31 // indirect
github.com/projectdiscovery/clistats v0.1.1 // indirect
github.com/projectdiscovery/dsl v0.8.14 // indirect
github.com/projectdiscovery/fastdialer v0.5.6 // indirect
github.com/projectdiscovery/fasttemplate v0.0.2 // indirect
github.com/projectdiscovery/freeport v0.0.5 // indirect
github.com/projectdiscovery/gologger v1.1.12 // indirect
github.com/projectdiscovery/gostruct v0.0.1 // indirect
github.com/projectdiscovery/hmap v0.0.45 // indirect
github.com/projectdiscovery/httpx v1.3.4 // indirect
github.com/projectdiscovery/interactsh v1.2.0 // indirect
github.com/projectdiscovery/freeport v0.0.7 // indirect
github.com/projectdiscovery/gcache v0.0.0-20241015120333-12546c6e3f4c // indirect
github.com/projectdiscovery/go-smb2 v0.0.0-20240129202741-052cc450c6cb // indirect
github.com/projectdiscovery/gologger v1.1.68 // indirect
github.com/projectdiscovery/gostruct v0.0.2 // indirect
github.com/projectdiscovery/gozero v0.1.1-0.20251027191944-a4ea43320b81 // indirect
github.com/projectdiscovery/hmap v0.0.100 // indirect
github.com/projectdiscovery/httpx v1.9.0 // indirect
github.com/projectdiscovery/interactsh v1.3.1 // indirect
github.com/projectdiscovery/ldapserver v1.0.2-0.20240219154113-dcc758ebc0cb // indirect
github.com/projectdiscovery/machineid v0.0.0-20240226150047-2e2c51e35983 // indirect
github.com/projectdiscovery/mapcidr v1.1.34 // indirect
github.com/projectdiscovery/networkpolicy v0.0.8 // indirect
github.com/projectdiscovery/rawhttp v0.1.18 // indirect
github.com/projectdiscovery/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.36 // indirect
github.com/projectdiscovery/ratelimit v0.0.85 // indirect
github.com/projectdiscovery/rawhttp v0.1.90 // indirect
github.com/projectdiscovery/rdap v0.9.1-0.20221108103045-9865884d1917 // indirect
github.com/projectdiscovery/retryabledns v1.0.62 // indirect
github.com/projectdiscovery/retryablehttp-go v1.0.63 // indirect
github.com/projectdiscovery/retryabledns v1.0.114 // indirect
github.com/projectdiscovery/retryablehttp-go v1.3.8 // indirect
github.com/projectdiscovery/sarif v0.0.1 // indirect
github.com/projectdiscovery/tlsx v1.1.4 // indirect
github.com/projectdiscovery/yamldoc-go v1.0.4 // indirect
github.com/refraction-networking/utls v1.8.1 // indirect
github.com/projectdiscovery/tlsx v1.2.2 // indirect
github.com/projectdiscovery/uncover v1.2.0 // indirect
github.com/projectdiscovery/useragent v0.0.107 // indirect
github.com/projectdiscovery/wappalyzergo v0.2.76 // indirect
github.com/projectdiscovery/yamldoc-go v1.0.6 // indirect
github.com/redis/go-redis/v9 v9.11.0 // indirect
github.com/refraction-networking/utls v1.8.2 // indirect
github.com/remeh/sizedwaitgroup v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
github.com/sashabaranov/go-openai v1.14.2 // indirect
github.com/sashabaranov/go-openai v1.37.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shirou/gopsutil/v3 v3.23.7 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
github.com/shirou/gopsutil/v4 v4.26.3 // indirect
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect
github.com/sijms/go-ora/v2 v2.9.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/sorairolake/lzip-go v0.3.8 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/cast v1.5.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.9.2 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/temoto/robotstxt v1.1.2 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/tidwall/buntdb v1.3.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/btree v1.8.1 // indirect
github.com/tidwall/buntdb v1.3.2 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/grect v0.1.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/rtred v0.1.2 // indirect
github.com/tidwall/tinyqueue v0.1.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/tim-ywliu/nested-logrus-formatter v1.3.2 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/trivago/tgo v1.0.7 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
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/weppos/publicsuffix-go v0.30.1-0.20230422193905-8fecedd899db // indirect
github.com/xanzy/go-gitlab v0.84.0 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
github.com/vmihailenco/bufpool v0.1.11 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/vulncheck-oss/go-exploit v1.51.0 // indirect
github.com/weppos/publicsuffix-go v0.50.3 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yassinebenaid/godump v0.11.1 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/ysmood/fetchup v0.2.3 // indirect
github.com/ysmood/goob v0.4.0 // indirect
github.com/ysmood/got v0.34.1 // indirect
github.com/ysmood/got v0.40.0 // indirect
github.com/ysmood/gson v0.7.3 // indirect
github.com/ysmood/leakless v0.8.0 // indirect
github.com/yuin/goldmark v1.5.4 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
github.com/ysmood/leakless v0.9.0 // indirect
github.com/yuin/goldmark v1.7.13 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zcalusic/sysinfo v1.0.2 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
github.com/zcalusic/sysinfo v1.1.3 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248 // indirect
github.com/zmap/zcrypto v0.0.0-20230422215203-9a665e1e9968 // indirect
go.etcd.io/bbolt v1.3.7 // indirect
github.com/zmap/zcrypto v0.0.0-20240803002437-3a861682ac77 // indirect
github.com/zmap/zgrab2 v0.1.8 // indirect
gitlab.com/gitlab-org/api/client-go v0.130.1 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
go.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.25.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
goftp.io/server/v2 v2.0.1 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.39.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect
gopkg.in/corvus-ch/zbase32.v1 v1.0.0 // indirect
gopkg.in/djherbis/times.v1 v1.3.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
mellium.im/sasl v0.3.2 // indirect
moul.io/http2curl v1.0.0 // indirect
)
+1191 -309
View File
File diff suppressed because it is too large Load Diff
-624
View File
@@ -1,624 +0,0 @@
schema = 3
[mod]
[mod."aead.dev/minisign"]
version = "v0.2.0"
hash = "sha256-2a05wSk811IdX9WSfMsrAvjPe6XVXEd4cvojrV+zqJ4="
[mod."git.mills.io/prologic/smtpd"]
version = "v0.0.0-20210710122116-a525b76c287a"
hash = "sha256-tbfKCLDJKAoZE3BvimQQLPn1cou2eA2wyMB0y1zPJEc="
[mod."github.com/Knetic/govaluate"]
version = "v3.0.1-0.20171022003610-9aa49832a739+incompatible"
hash = "sha256-Qs7qeK+Mrlm4ToAEYvN+OY6X7SRFV808frvKNr6gNhE="
[mod."github.com/Masterminds/semver/v3"]
version = "v3.2.1"
hash = "sha256-VKHIquwriyOL8A0qgtmap/3cGEOpDokOLtPg1w4xjMA="
[mod."github.com/Mzack9999/gcache"]
version = "v0.0.0-20230410081825-519e28eab057"
hash = "sha256-ofR592gukVdlEqA5ny9BPRDL4q2DrDTZeh4x1lrEmnQ="
[mod."github.com/Mzack9999/go-http-digest-auth-client"]
version = "v0.6.1-0.20220414142836-eb8883508809"
hash = "sha256-N4W589FOd0Oej0hpWsH0FaOBFxrYmAyX+L6eFW5sXDA="
[mod."github.com/Mzack9999/ldapserver"]
version = "v1.0.2-0.20211229000134-b44a0d6ad0dd"
hash = "sha256-s7X5Zd9Py8mKjJ/xWfgtrmYXl6ynpETwf0KXlnj3rRc="
[mod."github.com/PuerkitoBio/goquery"]
version = "v1.8.1"
hash = "sha256-z2RaB8PVPEzSJdMUfkfNjT616yXWTjW2gkhNOh989ZU="
[mod."github.com/VividCortex/ewma"]
version = "v1.2.0"
hash = "sha256-mHprIVRUOgs1qyYpiMO3bh6fCzDrqasDsaTaRE0oHXI="
[mod."github.com/akrylysov/pogreb"]
version = "v0.10.1"
hash = "sha256-f1BoPiR4KghX68eDPYQVuv1AVj97X1a+biip4vCrQ/s="
[mod."github.com/alecthomas/chroma"]
version = "v0.10.0"
hash = "sha256-p721vddVTv4iv1O0/dqpdk5xF6x9iLIHcrfh8JEVnqQ="
[mod."github.com/alecthomas/jsonschema"]
version = "v0.0.0-20211022214203-8b29eab41725"
hash = "sha256-l0OFXpa2E/t839tJGLY6jJUCuQC0SLCseYKsfM5o2vI="
[mod."github.com/alecthomas/template"]
version = "v0.0.0-20190718012654-fb15b899a751"
hash = "sha256-RsS4qxdRQ3q+GejA8D9Iu31A/mZNms4LbJ7518jWiu4="
[mod."github.com/alecthomas/units"]
version = "v0.0.0-20211218093645-b94a6e3cc137"
hash = "sha256-uriYmwxT69xbmWKO/5OAyeMa2lFBOJDrU2KtQh/+ZjY="
[mod."github.com/andybalholm/brotli"]
version = "v1.0.5"
hash = "sha256-/qS8wU8yZQJ+uTOg66rEl9s7spxq9VIXF5L1BcaEClc="
[mod."github.com/andybalholm/cascadia"]
version = "v1.3.2"
hash = "sha256-Nc9SkqJO/ecincVcUBFITy24TMmMGj5o0Q8EgdNhrEk="
[mod."github.com/andygrunwald/go-jira"]
version = "v1.16.0"
hash = "sha256-veyWp65T9uYYmw9o0g4w6tqn5Svq5++WFXNfy4vI+HA="
[mod."github.com/antchfx/htmlquery"]
version = "v1.3.0"
hash = "sha256-tldRSQPTmUodUepZkOnISWjfWPY37MzNN2Pd2/zmvoo="
[mod."github.com/antchfx/xmlquery"]
version = "v1.3.15"
hash = "sha256-uenaH5HiVcIswTjfwm2qqOA0ljY5la0BI4NiH4LjFD4="
[mod."github.com/antchfx/xpath"]
version = "v1.2.4"
hash = "sha256-rT5AtOv49/iGdR6X42Ho+ZEw6+YGQqfNUcYkSp1CU/g="
[mod."github.com/asaskevich/govalidator"]
version = "v0.0.0-20230301143203-a9d515a09cc2"
hash = "sha256-UCENzt1c1tFgsAzK2TNq5s2g0tQMQ5PxFaQKe8hTL/A="
[mod."github.com/aws/aws-sdk-go-v2"]
version = "v1.19.0"
hash = "sha256-z4UJRyk3eLx0yQ3kTl3zKH6bEM7MK1sqPQKvbP8d2Ec="
[mod."github.com/aws/aws-sdk-go-v2/config"]
version = "v1.18.28"
hash = "sha256-zFNtrknzaJ0zQr8EOT/3Y1qqZ/YcRMizRUZHxt9QY0I="
[mod."github.com/aws/aws-sdk-go-v2/credentials"]
version = "v1.13.27"
hash = "sha256-so4NK+rlyZnBtxgUNLld/G7vQKP/wp1A6wRJtaZT2pU="
[mod."github.com/aws/aws-sdk-go-v2/feature/ec2/imds"]
version = "v1.13.5"
hash = "sha256-zseMGwUW3NjzhD5IixiTiwp7x9hRAvpMbADEaYIB6Ig="
[mod."github.com/aws/aws-sdk-go-v2/internal/configsources"]
version = "v1.1.35"
hash = "sha256-TuDsdVuVbqUQbV4Y2E9Exmlu2an0yrfMGgdTHhXY85E="
[mod."github.com/aws/aws-sdk-go-v2/internal/endpoints/v2"]
version = "v2.4.29"
hash = "sha256-P+9wAU5sbBn1tQqS1nFwisaoa3999czJilowwO2rO3Y="
[mod."github.com/aws/aws-sdk-go-v2/internal/ini"]
version = "v1.3.36"
hash = "sha256-9VmY8oidPMnAfpt2AyiCSSascqBZGGLtIizTydlK8k8="
[mod."github.com/aws/aws-sdk-go-v2/service/internal/presigned-url"]
version = "v1.9.29"
hash = "sha256-mXNOY17gXxhS2NV7azA0mxrARkROGrrpeN0Lgg7KQSw="
[mod."github.com/aws/aws-sdk-go-v2/service/sso"]
version = "v1.12.13"
hash = "sha256-F4tTYdgFvDImOQNuKQFFsLwd6bX1CO50Ab3KYqY32Lc="
[mod."github.com/aws/aws-sdk-go-v2/service/ssooidc"]
version = "v1.14.13"
hash = "sha256-XGj/ccaj00wNN32J3JTuuqthCbxrTfmxfSYJLf/hK8Y="
[mod."github.com/aws/aws-sdk-go-v2/service/sts"]
version = "v1.19.3"
hash = "sha256-Q8NFgFRjNUFldTmr/Ya9DyAUNfsC9AuWPkSFMrVF/jg="
[mod."github.com/aws/smithy-go"]
version = "v1.13.5"
hash = "sha256-lu1UnvPnLzXjDPBk2FJ4ZImKRQf7aj43mLbuolFdE64="
[mod."github.com/aymanbagabas/go-osc52/v2"]
version = "v2.0.1"
hash = "sha256-6Bp0jBZ6npvsYcKZGHHIUSVSTAMEyieweAX2YAKDjjg="
[mod."github.com/aymerick/douceur"]
version = "v0.2.0"
hash = "sha256-NiBX8EfOvLXNiK3pJaZX4N73YgfzdrzRXdiBFe3X3sE="
[mod."github.com/bluele/gcache"]
version = "v0.0.2"
hash = "sha256-gU44V3jqw6K3Mjgh6DG9f7DU+ft3wA9GDmH4AgMTjxE="
[mod."github.com/caddyserver/certmagic"]
version = "v0.19.2"
hash = "sha256-ruz2oG5E834tMjyL/HdFPaWlNuwBg/fxqVitZX3sQR0="
[mod."github.com/charmbracelet/glamour"]
version = "v0.6.0"
hash = "sha256-L5V2P/9EPP80703KJUSMDiAPgjW1B4i1IbJADPmUCoY="
[mod."github.com/charmbracelet/lipgloss"]
version = "v0.8.0"
hash = "sha256-m+cRJCCJjNyxJKxCk1ftu32OMesYDIUw/EVUzTZjo9I="
[mod."github.com/charmbracelet/log"]
version = "v0.2.4"
hash = "sha256-LQe3fQHf/v6q8pegS5E54eSfU0Y5tnKXM+Mk6uzeWvU="
[mod."github.com/cheggaaa/pb/v3"]
version = "v3.1.4"
hash = "sha256-Fl0bM8ag8sKr8C/hj5qaxN+VjmRA403xXcQoTdQ19LU="
[mod."github.com/cloudflare/cfssl"]
version = "v1.6.4"
hash = "sha256-dAUHPutZ+bpDgJ0mWrALLIbQqNF2d1OkgSAWzQkxXWY="
[mod."github.com/cloudflare/circl"]
version = "v1.3.3"
hash = "sha256-ItdVkU53Ep01553/tJ4MdAwoTpPljRxiBW9sAd7p0xI="
[mod."github.com/cnf/structhash"]
version = "v0.0.0-20201127153200-e1b16c1ebc08"
hash = "sha256-hvJSTpbaPHgWnJ16B9a4cFVblplAgCw5OkGSUFmJBvg="
[mod."github.com/corpix/uarand"]
version = "v0.2.0"
hash = "sha256-/2ZqTtYPEbfn5adf5tIU9p8jwHFRkBYzi4WE5h2AwkI="
[mod."github.com/dimchansky/utfbom"]
version = "v1.1.1"
hash = "sha256-w8KEprK54zJkMat78T6zldjDwvhbc/O8s6pVFzfmg1I="
[mod."github.com/dlclark/regexp2"]
version = "v1.8.1"
hash = "sha256-Xm4I+Qrpwn21QsWcUMden00zWapbloa6K1yJ83tTOVE="
[mod."github.com/docker/go-units"]
version = "v0.5.0"
hash = "sha256-iK/V/jJc+borzqMeqLY+38Qcts2KhywpsTk95++hImE="
[mod."github.com/dsnet/compress"]
version = "v0.0.1"
hash = "sha256-HCqu3cKayMvx1YIUPkJ+u4UM6WN8nrsNIhdvGJIJgwg="
[mod."github.com/fatih/color"]
version = "v1.15.0"
hash = "sha256-7b+scFVQeEUoXfeCDd8X2gS8GMoWA+HxjK8wfbypa5s="
[mod."github.com/fatih/structs"]
version = "v1.1.0"
hash = "sha256-OCmubTLF1anwNnkvFZDYHnF6hFlX0WDoe/9+dDlaMPM="
[mod."github.com/gabriel-vasile/mimetype"]
version = "v1.4.2"
hash = "sha256-laV+IkgbnEG07h1eFfPISqp0ctnLXfzchz/CLR1lftk="
[mod."github.com/gaukas/godicttls"]
version = "v0.0.4"
hash = "sha256-Tok6mN6P7rnqK+VCiI6LOV9DBnOTjGyGrgfzZdMCMVk="
[mod."github.com/go-logfmt/logfmt"]
version = "v0.6.0"
hash = "sha256-RtIG2qARd5sT10WQ7F3LR8YJhS8exs+KiuUiVf75bWg="
[mod."github.com/go-ole/go-ole"]
version = "v1.2.6"
hash = "sha256-+oxitLeJxYF19Z6g+6CgmCHJ1Y5D8raMi2Cb3M6nXCs="
[mod."github.com/go-playground/locales"]
version = "v0.14.1"
hash = "sha256-BMJGAexq96waZn60DJXZfByRHb8zA/JP/i6f/YrW9oQ="
[mod."github.com/go-playground/universal-translator"]
version = "v0.18.1"
hash = "sha256-2/B2qP51zfiY+k8G0w0D03KXUc7XpWj6wKY7NjNP/9E="
[mod."github.com/go-playground/validator/v10"]
version = "v10.14.1"
hash = "sha256-13J8JqIuhI7lbBagaR7INykFRXqRbB7tjXtMZI3PNvA="
[mod."github.com/go-rod/rod"]
version = "v0.114.0"
hash = "sha256-YQwPbgeBPziMTmFg8kulEQkdTi3OTUutlX+8CmCdQ94="
[mod."github.com/goburrow/cache"]
version = "v0.1.4"
hash = "sha256-3imkv1DlePYg0aBswzxqOn1EzZFwMXW+D3Dq0u0GEEQ="
[mod."github.com/gobwas/glob"]
version = "v0.2.3"
hash = "sha256-hYHMUdwxVkMOjSKjR7UWO0D0juHdI4wL8JEy5plu/Jc="
[mod."github.com/gobwas/httphead"]
version = "v0.1.0"
hash = "sha256-6wFni/JkK2GqtVs3IW+GxHRNoSu4EJfzaBRGX2hF1IA="
[mod."github.com/gobwas/pool"]
version = "v0.2.1"
hash = "sha256-py8/+Wo5Q83EbYMUKK5U/4scRcyMo2MjOoxqi5y+sUY="
[mod."github.com/gobwas/ws"]
version = "v1.2.1"
hash = "sha256-5kWY244Vuyj01BzgTJuaJUJJwTXaKZ0UzPruKATByEg="
[mod."github.com/gocolly/colly/v2"]
version = "v2.1.0"
hash = "sha256-yWhPcNwGj31wWJrnHWOa3jBO1qZXfqOWuHDlmpSPuyg="
[mod."github.com/golang-jwt/jwt/v4"]
version = "v4.5.0"
hash = "sha256-dyKL8wQRApkdCkKxJ1knllvixsrBLw+BtRS0SjlN7NQ="
[mod."github.com/golang/groupcache"]
version = "v0.0.0-20210331224755-41bb18bfe9da"
hash = "sha256-7Gs7CS9gEYZkbu5P4hqPGBpeGZWC64VDwraSKFF+VR0="
[mod."github.com/golang/protobuf"]
version = "v1.5.3"
hash = "sha256-svogITcP4orUIsJFjMtp+Uv1+fKJv2Q5Zwf2dMqnpOQ="
[mod."github.com/golang/snappy"]
version = "v0.0.4"
hash = "sha256-Umx+5xHAQCN/Gi4HbtMhnDCSPFAXSsjVbXd8n5LhjAA="
[mod."github.com/google/certificate-transparency-go"]
version = "v1.1.4"
hash = "sha256-/V18IcVehgvhkT+w7y8vpXaVAtdV3BAsxOnbRBromGw="
[mod."github.com/google/go-github"]
version = "v17.0.0+incompatible"
hash = "sha256-5EGZnkefwLCEODLICIgaq39UoOzBJqpeLraoc2hJfM8="
[mod."github.com/google/go-github/v30"]
version = "v30.1.0"
hash = "sha256-u6m+wWJl440UI64Q2tpX0qFF3LyEH3hPww82hIEf6/Q="
[mod."github.com/google/go-querystring"]
version = "v1.1.0"
hash = "sha256-itsKgKghuX26czU79cK6C2n+lc27jm5Dw1XbIRgwZJY="
[mod."github.com/google/uuid"]
version = "v1.3.1"
hash = "sha256-JxAEAB2bFlGPShFreyOWjUahjaGV3xYS5TpfUOikod0="
[mod."github.com/gorilla/css"]
version = "v1.0.0"
hash = "sha256-Mmt/IqHpgrtWpbr/AKcJyf/USQTqEuv1HVivY4eHzoQ="
[mod."github.com/h2non/filetype"]
version = "v1.1.3"
hash = "sha256-lSX/fSbT3MVlNK7d1U6Q/lBHtGXXAQ/HY4zW6Bppqhc="
[mod."github.com/hashicorp/go-cleanhttp"]
version = "v0.5.2"
hash = "sha256-N9GOKYo7tK6XQUFhvhImtL7PZW/mr4C4Manx/yPVvcQ="
[mod."github.com/hashicorp/go-retryablehttp"]
version = "v0.7.2"
hash = "sha256-PcLyolWF7G409rs7j3tnwgQK6xhgWYk9/iK2bO13TGQ="
[mod."github.com/hashicorp/go-version"]
version = "v1.6.0"
hash = "sha256-UV0equpmW6BiJnp4W3TZlSJ+PTHuTA+CdOs2JTeHhjs="
[mod."github.com/hbakhtiyor/strsim"]
version = "v0.0.0-20190107154042-4d2bbb273edf"
hash = "sha256-vK4ghGQy9IGvAq0/3roEDiE/ybNOePULr4s/V8ZHLj8="
[mod."github.com/hdm/jarm-go"]
version = "v0.0.7"
hash = "sha256-4SnBXV+O7iWPO0Yt9/D1BhaF7MEvNUrwBj116uMt5j0="
[mod."github.com/iancoleman/orderedmap"]
version = "v0.0.0-20190318233801-ac98e3ecb4b0"
hash = "sha256-IIm0P6GnYSBGHzOYc7ljp+5LPoWBmmqXt1Yi4vBRdsQ="
[mod."github.com/itchyny/gojq"]
version = "v0.12.13"
hash = "sha256-tlnj0CCsPZRQjIZCvNPjN0JD6oqRDvdWOCYR3tYMPUA="
[mod."github.com/itchyny/timefmt-go"]
version = "v0.1.5"
hash = "sha256-FvgqEW8fnZsfbHpV+X4FQvDzzneNOpdQtQLXovh1YmI="
[mod."github.com/json-iterator/go"]
version = "v1.1.12"
hash = "sha256-To8A0h+lbfZ/6zM+2PpRpY3+L6725OPC66lffq6fUoM="
[mod."github.com/julienschmidt/httprouter"]
version = "v1.3.0"
hash = "sha256-YVbnyFLVZX1mtqcwM1SStQdhcQsPHyi1ltpOrD3w2qg="
[mod."github.com/kataras/jwt"]
version = "v0.1.8"
hash = "sha256-3AKX8wmQ6RaRMAyhe1JirEl1P0ZiMNRJZ3D1yzBRuCU="
[mod."github.com/kennygrant/sanitize"]
version = "v1.2.4"
hash = "sha256-PRNblaLosaB7tvUVgAOZORMZGUo+7Wy7h1Z1mpJLd5c="
[mod."github.com/klauspost/compress"]
version = "v1.16.7"
hash = "sha256-8miX/lnXyNLPSqhhn5BesLauaIAxETpQpWtr1cu2f+0="
[mod."github.com/klauspost/cpuid/v2"]
version = "v2.2.5"
hash = "sha256-/M8CHNah2/EPr0va44r1Sx+3H6E+jN8bGFi5jQkLBrM="
[mod."github.com/leodido/go-urn"]
version = "v1.2.4"
hash = "sha256-N2HO7ChScxI79KGvXI9LxoIlr+lkBNdDZP9OPGwPRK0="
[mod."github.com/libdns/libdns"]
version = "v0.2.1"
hash = "sha256-bxEY0wYu4Um0t7sakLyMwMPDXfv2x07gjckKSyAypsc="
[mod."github.com/logrusorgru/aurora"]
version = "v2.0.3+incompatible"
hash = "sha256-7o5Fh4jscdYKgXfnNMbcD68Kjw8Z4LcPgHcr4ZyQYrI="
[mod."github.com/lor00x/goldap"]
version = "v0.0.0-20180618054307-a546dffdd1a3"
hash = "sha256-wE3bDMJqd+drbrYK0QPF3GMQOzgB8u9uN2T0uUX9xow="
[mod."github.com/lucasb-eyer/go-colorful"]
version = "v1.2.0"
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
[mod."github.com/lufia/plan9stats"]
version = "v0.0.0-20211012122336-39d0f177ccd0"
hash = "sha256-thb+rkDx5IeWMgw5/5jgu5gZ+6RjJAUXeMgSkJHhRlA="
[mod."github.com/mackerelio/go-osstat"]
version = "v0.2.4"
hash = "sha256-WW5VbvDedsNRxclUjI/pvlf4vB4VyDKEGlpvcLqiAyo="
[mod."github.com/mattn/go-colorable"]
version = "v0.1.13"
hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8="
[mod."github.com/mattn/go-isatty"]
version = "v0.0.19"
hash = "sha256-wYQqGxeqV3Elkmn26Md8mKZ/viw598R4Ych3vtt72YE="
[mod."github.com/mattn/go-runewidth"]
version = "v0.0.14"
hash = "sha256-O3QdxqAcJgQ+HL1v8oBA4iKBwJ2AlDN+F464027hWMU="
[mod."github.com/mholt/acmez"]
version = "v1.2.0"
hash = "sha256-zfj14WFQr1/AO64gYsbFk4a4T0dsMEs+W3uIa9968/M="
[mod."github.com/mholt/archiver"]
version = "v3.1.1+incompatible"
hash = "sha256-+XCbzKmuqktmYveDdJCNWB8B6Ya8yJM8H7uugYxrhhA="
[mod."github.com/microcosm-cc/bluemonday"]
version = "v1.0.25"
hash = "sha256-/crG5s6cDrJ55nkDBwugLUpY7U+vQuHpCkKm7nnN8Zc="
[mod."github.com/miekg/dns"]
version = "v1.1.55"
hash = "sha256-Jbii9veDSpqF7yIkdrzb/bEUM3wZG41mNEAYV3VEAJo="
[mod."github.com/minio/selfupdate"]
version = "v0.6.0"
hash = "sha256-CupJKkF1MNaOEMBPjfCxF+k/k3yNWXfWShmJfezg3O4="
[mod."github.com/mitchellh/go-homedir"]
version = "v1.1.0"
hash = "sha256-oduBKXHAQG8X6aqLEpqZHs5DOKe84u6WkBwi4W6cv3k="
[mod."github.com/modern-go/concurrent"]
version = "v0.0.0-20180306012644-bacd9c7ef1dd"
hash = "sha256-OTySieAgPWR4oJnlohaFTeK1tRaVp/b0d1rYY8xKMzo="
[mod."github.com/modern-go/reflect2"]
version = "v1.0.2"
hash = "sha256-+W9EIW7okXIXjWEgOaMh58eLvBZ7OshW2EhaIpNLSBU="
[mod."github.com/muesli/reflow"]
version = "v0.3.0"
hash = "sha256-Pou2ybE9SFSZG6YfZLVV1Eyfm+X4FuVpDPLxhpn47Cc="
[mod."github.com/muesli/termenv"]
version = "v0.15.2"
hash = "sha256-Eum/SpyytcNIchANPkG4bYGBgcezLgej7j/+6IhqoMU="
[mod."github.com/nwaples/rardecode"]
version = "v1.1.3"
hash = "sha256-X7Cg0kEygyy6Xw6sxRF9HirgefkH9tn9UPPelxRaAGg="
[mod."github.com/olekukonko/tablewriter"]
version = "v0.0.5"
hash = "sha256-/5i70IkH/qSW5KjGzv8aQNKh9tHoz98tqtL0K2DMFn4="
[mod."github.com/pierrec/lz4"]
version = "v2.6.1+incompatible"
hash = "sha256-5+4i5SN97wG71knAF9eUgEEG5k03HW4wPnAdPd6JSfE="
[mod."github.com/pkg/errors"]
version = "v0.9.1"
hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw="
[mod."github.com/power-devops/perfstat"]
version = "v0.0.0-20210106213030-5aafc221ea8c"
hash = "sha256-ywykDYuqcMt0TvZOz1l9Z6Z2JMTYQw8cP2fT8AtpmX4="
[mod."github.com/projectdiscovery/asnmap"]
version = "v1.0.4"
hash = "sha256-J5Dn5eDzwj+ApwQ3ibTsMbwCobRAb1Cli+hbf74I9VQ="
[mod."github.com/projectdiscovery/blackrock"]
version = "v0.0.1"
hash = "sha256-E66IuBQ3meaGTVk26YzlUDwdUV4kP7VLhrhLnQShkHA="
[mod."github.com/projectdiscovery/cdncheck"]
version = "v1.0.9"
hash = "sha256-fJngwA9mAYB2awhEhS1gWXhOlmKeLrNV8WQj0r5y7Q0="
[mod."github.com/projectdiscovery/clistats"]
version = "v0.0.19"
hash = "sha256-vW7h0Eqm578jI/REU48rexVXGAeZt7JThRSeFm3gUt4="
[mod."github.com/projectdiscovery/dsl"]
version = "v0.0.20"
hash = "sha256-wkDZVgSU6EK5t6tH+g6EsEaTZ9bDNqIdix3I2MnQXOE="
[mod."github.com/projectdiscovery/fastdialer"]
version = "v0.0.37"
hash = "sha256-XxUFV6yfbH3Qw+Euogk/YFlHDxJtB4AIpOoFDK7poBY="
[mod."github.com/projectdiscovery/fasttemplate"]
version = "v0.0.2"
hash = "sha256-kl0lxr7Zhubs3b8Xgt5DRHVj6XxM/WtEAiVkecy62O4="
[mod."github.com/projectdiscovery/freeport"]
version = "v0.0.5"
hash = "sha256-14FrV/9ImnzdH8Pgl8VmgNhtEoqJtJGMO4QoYHdEZig="
[mod."github.com/projectdiscovery/goflags"]
version = "v0.1.19"
hash = "sha256-x72o/EiV2cTf9BW2XRwDGxW7rYFuXnmVc4MJyjoNvIg="
[mod."github.com/projectdiscovery/gologger"]
version = "v1.1.11"
hash = "sha256-ujoMwz77PRSqwE7Dr+MCm8144trX4le8z3l5yVNhMVs="
[mod."github.com/projectdiscovery/gostruct"]
version = "v0.0.1"
hash = "sha256-OhglrSmIVlNBWkY9WrIQB4SL4P47H/uqX9l+LjNZhSQ="
[mod."github.com/projectdiscovery/hmap"]
version = "v0.0.16"
hash = "sha256-mgnvUmgvTm7S71t5rK87eIxRHXZKsR7dUxAOuputtsE="
[mod."github.com/projectdiscovery/httpx"]
version = "v1.3.4"
hash = "sha256-Ye5xYjMaZamigmumgFzo8f3suXRJMOfJQa1S4OV2Gks="
[mod."github.com/projectdiscovery/interactsh"]
version = "v1.1.6"
hash = "sha256-kkUiuODfQwGesZi5w+t6f2BAIe9PLBDb24ltpbOqzp0="
[mod."github.com/projectdiscovery/mapcidr"]
version = "v1.1.2"
hash = "sha256-MXY4WRzRZ7OwuUxq5pCFgipHNakCB9U0UaNjYA5xnm8="
[mod."github.com/projectdiscovery/networkpolicy"]
version = "v0.0.6"
hash = "sha256-TEuxI6vJly0Sh1vkYhrr+EHZdFNZKOvNaU3q3cNyIlA="
[mod."github.com/projectdiscovery/nuclei/v2"]
version = "v2.9.14"
hash = "sha256-mTx6QCs0sTEHQX9/frJ6J1F+sJgmc4TqeoXR1esuTMY="
[mod."github.com/projectdiscovery/ratelimit"]
version = "v0.0.9"
hash = "sha256-/puvEIORXvDGDzotR0DhQnRXQramZYNtjaxjV0KgrN8="
[mod."github.com/projectdiscovery/rawhttp"]
version = "v0.1.18"
hash = "sha256-RkXxq/MAkPLTPzFvG90JgGtOeH/5oOPhCb42HCBweqs="
[mod."github.com/projectdiscovery/rdap"]
version = "v0.9.1-0.20221108103045-9865884d1917"
hash = "sha256-BEZDRPZPjhkNoyj/8Tk21UM98plLNitZ1W52GktJvMs="
[mod."github.com/projectdiscovery/retryabledns"]
version = "v1.0.35"
hash = "sha256-pGq+ZSETmt10PzBBY7ePnq+JW9YBJa9xq9+r1TmJY1E="
[mod."github.com/projectdiscovery/retryablehttp-go"]
version = "v1.0.25"
hash = "sha256-O2OksMSebG5fyiKlkTqC/draHa4g4ERYwuOmsZLPqec="
[mod."github.com/projectdiscovery/sarif"]
version = "v0.0.1"
hash = "sha256-m1s98hDVLAYbXgB0AEqHktZw2N89QeojqPZ7ConL4OE="
[mod."github.com/projectdiscovery/tlsx"]
version = "v1.1.4"
hash = "sha256-EMTNd5NOvaFbVxv31j3pBU//mWQQpThswCT8bMNx5Qw="
[mod."github.com/projectdiscovery/utils"]
version = "v0.0.52"
hash = "sha256-TOUCrtkO976RqBy6w4mQXJ8n/5klkg9tWuEMHdMooHg="
[mod."github.com/projectdiscovery/yamldoc-go"]
version = "v1.0.4"
hash = "sha256-ufjSaGHdRzyusbg5XKG6NVX/UyrUu2PBvGBl0Bour6I="
[mod."github.com/quic-go/quic-go"]
version = "v0.37.4"
hash = "sha256-EXsOITb0kh48+Wy2bIZyyNeGVuJmiL6xB0mtPOBUY/Y="
[mod."github.com/refraction-networking/utls"]
version = "v1.5.2"
hash = "sha256-QwYwEFkpo82NP4l6n6/+5HXzcFt6bEYqy4jFomushkw="
[mod."github.com/remeh/sizedwaitgroup"]
version = "v1.0.0"
hash = "sha256-CtjNoNeep0TnfkuRN/rc48diAo0jUog1fOz3I/z6jfc="
[mod."github.com/rivo/uniseg"]
version = "v0.4.4"
hash = "sha256-B8tbL9K6ICLdm0lEhs9+h4cpjAfvFtNiFMGvQZmw0bM="
[mod."github.com/rocketlaunchr/google-search"]
version = "v1.1.6"
hash = "sha256-2BMD4RXtrxMKC8AaxyeU/p1i92MvGIQjv4KOA4giXfk="
[mod."github.com/rs/xid"]
version = "v1.5.0"
hash = "sha256-u0QLm2YFMJqEjUhpWcLwfoS9lNHUxc2A79MObsqVbVU="
[mod."github.com/saintfish/chardet"]
version = "v0.0.0-20230101081208-5e3ef4b5456d"
hash = "sha256-JXlHMCbXB8iRQ9wQBGCeTjDSfgaBwUVOpvcjj0iVn5A="
[mod."github.com/sashabaranov/go-openai"]
version = "v1.14.2"
hash = "sha256-dc1SL5n3sOZPL018JDnqM6W/8pTwg7xUtxEnON4v+lM="
[mod."github.com/segmentio/ksuid"]
version = "v1.0.4"
hash = "sha256-50molk1vt8/n4Y+ruayW/EAn9NeeQ8ApmLJQVePhieE="
[mod."github.com/shirou/gopsutil/v3"]
version = "v3.23.7"
hash = "sha256-UppGryc5MO0sY3PuOC4H3hYsSomVTaXhgEprOsNFqe4="
[mod."github.com/shoenig/go-m1cpu"]
version = "v0.1.6"
hash = "sha256-hT+JP30BBllsXosK/lo89HV/uxxPLsUyO3dRaDiLnCg="
[mod."github.com/spaolacci/murmur3"]
version = "v1.1.0"
hash = "sha256-RWD4PPrlAsZZ8Xy356MBxpj+/NZI7w2XOU14Ob7/Y9M="
[mod."github.com/spf13/cast"]
version = "v1.5.1"
hash = "sha256-/tQNGGQv+Osp+2jepQaQe6GlncZbqdxzSR82FieiUBU="
[mod."github.com/syndtr/goleveldb"]
version = "v1.0.0"
hash = "sha256-rW7SW6nehede0oMZo4NBatM6Eizbnlb7xYoX/dcDUxA="
[mod."github.com/temoto/robotstxt"]
version = "v1.1.2"
hash = "sha256-/0zXEWCnvefGjU2RNxoyZu15KU6WYe9C4m58kyLU6zo="
[mod."github.com/tidwall/btree"]
version = "v1.6.0"
hash = "sha256-H4S46Yk3tVfOtrEhVWUrF4S1yWYmzU43W80HlzS9rcY="
[mod."github.com/tidwall/buntdb"]
version = "v1.3.0"
hash = "sha256-tXp+wcPYogh/Thubk4baFLpbwrCGVf0URvlBXwGg3eQ="
[mod."github.com/tidwall/gjson"]
version = "v1.14.4"
hash = "sha256-3DS2YNL95wG0qSajgRtIABD32J+oblaKVk8LIw+KSOc="
[mod."github.com/tidwall/grect"]
version = "v0.1.4"
hash = "sha256-iSS8YjTqtmlzK9T3PFXoLx5xF/vC8864yNzGw0KYwKs="
[mod."github.com/tidwall/match"]
version = "v1.1.1"
hash = "sha256-M2klhPId3Q3T3VGkSbOkYl/2nLHnsG+yMbXkPkyrRdg="
[mod."github.com/tidwall/pretty"]
version = "v1.2.1"
hash = "sha256-S0uTDDGD8qr415Ut7QinyXljCp0TkL4zOIrlJ+9OMl8="
[mod."github.com/tidwall/rtred"]
version = "v0.1.2"
hash = "sha256-C4p3rZWRLuNgbfVVPr83PZjbD8rZNN3a3YGQJQJlSQU="
[mod."github.com/tidwall/tinyqueue"]
version = "v0.1.1"
hash = "sha256-vsVVA0dAkYtX/C/pk0nDUiu6kURZrK+rxVBRB4wY78Q="
[mod."github.com/tklauser/go-sysconf"]
version = "v0.3.11"
hash = "sha256-io8s7PJi4OX+wXkCm+v5pKy4yiqA/RE/I4ksy6mKX30="
[mod."github.com/tklauser/numcpus"]
version = "v0.6.0"
hash = "sha256-6jssTsP5L6yVl43tXfqDdgeI+tEkBp3BpiWwKXLTHAM="
[mod."github.com/trivago/tgo"]
version = "v1.0.7"
hash = "sha256-VzCbopX6wKWVWmcr/qnKf4ruMicwyEeNfCEWc0UxoxI="
[mod."github.com/ulikunitz/xz"]
version = "v0.5.11"
hash = "sha256-SUyrjc2wyN3cTGKe5JdBEXjtZC1rJySRxJHVUZ59row="
[mod."github.com/ulule/deepcopier"]
version = "v0.0.0-20200430083143-45decc6639b6"
hash = "sha256-zyn5rHS5bU/4KajCVg+6pex42KVdXLZS8DFqRDUpn0E="
[mod."github.com/valyala/bytebufferpool"]
version = "v1.0.0"
hash = "sha256-I9FPZ3kCNRB+o0dpMwBnwZ35Fj9+ThvITn8a3Jr8mAY="
[mod."github.com/valyala/fasttemplate"]
version = "v1.2.2"
hash = "sha256-gp+lNXE8zjO+qJDM/YbS6V43HFsYP6PKn4ux1qa5lZ0="
[mod."github.com/weppos/publicsuffix-go"]
version = "v0.30.1-0.20230422193905-8fecedd899db"
hash = "sha256-Hw5S8ACINl+z/qZmLhcQcXjrXHzYM9HsqQF91RbDoB4="
[mod."github.com/xanzy/go-gitlab"]
version = "v0.84.0"
hash = "sha256-1Se9LHWGnmvAm0QHrb8Zw2jkyaKH2o3j0wvdMp289IQ="
[mod."github.com/xi2/xz"]
version = "v0.0.0-20171230120015-48954b6210f8"
hash = "sha256-2J4cb9KUnGHn1WZ2+g/S+yiHGLDt6KU0cP3fJpQDGZ0="
[mod."github.com/yl2chen/cidranger"]
version = "v1.0.2"
hash = "sha256-rPZApwakcZ1D3lmZnFds79+TFr9IlYkovTA7o52N9h0="
[mod."github.com/ysmood/fetchup"]
version = "v0.2.3"
hash = "sha256-sJ9PBMJ/PH3Es/ngAJkrxTPNAXr7AFjdsblF67mP2Hc="
[mod."github.com/ysmood/goob"]
version = "v0.4.0"
hash = "sha256-o0yVrxQRbN1dSjBH359VHADzPmkyrYOp7jn1GqIYhvw="
[mod."github.com/ysmood/got"]
version = "v0.34.1"
hash = "sha256-dCLb+1Yt/HAZhfQlVkEQoVG9Uv7iBGSqhxdunoakLTU="
[mod."github.com/ysmood/gson"]
version = "v0.7.3"
hash = "sha256-Dn5cTopPKtKCjQ7G6nlvPW2d7G4c5NfIdLVM9eLgR0E="
[mod."github.com/ysmood/leakless"]
version = "v0.8.0"
hash = "sha256-+D41mvLU29dPR4Lf9iWYq3oATgKHpRnUKahO0hTiCDc="
[mod."github.com/yuin/goldmark"]
version = "v1.5.4"
hash = "sha256-4he5sGi0uj1LogdqvgpvN8b7p6qlKMGuWXRFzh+FK8s="
[mod."github.com/yuin/goldmark-emoji"]
version = "v1.0.1"
hash = "sha256-liYCi6/EYG4obl51CzCaOmXf3fdzrU43J9VBZyHggEo="
[mod."github.com/yusufpapurcu/wmi"]
version = "v1.2.3"
hash = "sha256-HOLI8i58AMWeTotvYtdZessgrLwUG2aiS37eeHgsneY="
[mod."github.com/zeebo/blake3"]
version = "v0.2.3"
hash = "sha256-ZepnzkvOyicTGL078O1F84q0TzBAouJlB5AMmfsiOIg="
[mod."github.com/zmap/rc2"]
version = "v0.0.0-20190804163417-abaa70531248"
hash = "sha256-yMyZfFjcLynxiNXmUdfSfUlWekdtlXV3jGIoJMxMDz4="
[mod."github.com/zmap/zcrypto"]
version = "v0.0.0-20230422215203-9a665e1e9968"
hash = "sha256-nDBTEGDBv764XaC3KEwMtKGim0dEy4cjgo8XwnvyLh4="
[mod."go.etcd.io/bbolt"]
version = "v1.3.7"
hash = "sha256-poZk8tPLDWwW95oCOkTJcQtEvOJTD9UXAZ2TqGJutwk="
[mod."go.uber.org/multierr"]
version = "v1.11.0"
hash = "sha256-Lb6rHHfR62Ozg2j2JZy3MKOMKdsfzd1IYTR57r3Mhp0="
[mod."go.uber.org/zap"]
version = "v1.25.0"
hash = "sha256-aU270ds5r37xtfFFDVrvjOTTOv1aZNd7ffvHZJB6VIQ="
[mod."goftp.io/server/v2"]
version = "v2.0.1"
hash = "sha256-lI1UZVC9zQnyarOK6AR3Llw4exPqvNn3BZqwKlAOYbQ="
[mod."golang.org/x/crypto"]
version = "v0.12.0"
hash = "sha256-Wes72EA9ICTG8o0nEYWZk9xjpqlniorFeY6o26GExns="
[mod."golang.org/x/exp"]
version = "v0.0.0-20230626212559-97b1e661b5df"
hash = "sha256-aoesDZqls2sBtDmZ/ZSLzIudLuD8GDtGEEucyiqbCjY="
[mod."golang.org/x/mod"]
version = "v0.12.0"
hash = "sha256-M/oXnzm7odpJdQzEnG6W0pNYtl0uhOM/l7qgfGVpU2M="
[mod."golang.org/x/net"]
version = "v0.14.0"
hash = "sha256-QScKgO7lBWOsd0Y31wLRzFETv3tjqdB/eRQWW5q7aV4="
[mod."golang.org/x/oauth2"]
version = "v0.11.0"
hash = "sha256-ztz1lRVZXq6lTN/q4b4Y+P6L1EkP8ZJuhUbSJ0QvCw4="
[mod."golang.org/x/sys"]
version = "v0.11.0"
hash = "sha256-g/LjhABK2c/u6v7M2aAIrHvZjmx/ikGHkef86775N38="
[mod."golang.org/x/text"]
version = "v0.12.0"
hash = "sha256-aNQaW3EgCK9ehpnBzIAkZX6TmiUU1S175YlJUH7P5Qg="
[mod."golang.org/x/time"]
version = "v0.3.0"
hash = "sha256-/hmc9skIswMYbivxNS7R8A6vCTUF9k2/7tr/ACkcEaM="
[mod."golang.org/x/tools"]
version = "v0.11.0"
hash = "sha256-3fNsrCbUnbI5kwZRTx/olHLxR2DJhfvEQ3x0yeeZ8JY="
[mod."google.golang.org/appengine"]
version = "v1.6.7"
hash = "sha256-zIxGRHiq4QBvRqkrhMGMGCaVL4iM4TtlYpAi/hrivS4="
[mod."google.golang.org/protobuf"]
version = "v1.31.0"
hash = "sha256-UdIk+xRaMfdhVICvKRk1THe3R1VU+lWD8hqoW/y8jT0="
[mod."gopkg.in/alecthomas/kingpin.v2"]
version = "v2.2.6"
hash = "sha256-uViE2kPj7tMrGYVjjdLOl2jFDmmu+3P7GvnZBse2zVY="
[mod."gopkg.in/corvus-ch/zbase32.v1"]
version = "v1.0.0"
hash = "sha256-T6PzD4SJv6ipfCkr8CVHXjmKvYRGcLOypHTa238GGlw="
[mod."gopkg.in/djherbis/times.v1"]
version = "v1.3.0"
hash = "sha256-0ZIFWjtY4KyTPIRjUVIGKMXSXe++6vxBckckluhBYLY="
[mod."gopkg.in/yaml.v2"]
version = "v2.4.0"
hash = "sha256-uVEGglIedjOIGZzHW4YwN1VoRSTK8o0eGZqzd+TNdd0="
[mod."gopkg.in/yaml.v3"]
version = "v3.0.1"
hash = "sha256-FqL9TKYJ0XkNwJFnq9j0VvJ5ZUU1RvH/52h/f5bkYAU="
[mod."moul.io/http2curl"]
version = "v1.0.0"
hash = "sha256-1ZP4V71g1K3oTvz5nGWUBD5h84hXga/RUQwWTpSnphM="
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -39,12 +39,18 @@ type Settings struct {
Template string
CMS bool
Headers bool
SecurityHeaders bool
CloudStorage bool
SubdomainTakeover bool
Shodan bool
SecurityTrails bool
SQL bool
LFI bool
Framework bool
Modules string // Comma-separated list of module IDs to run
ModuleTags string // Run modules matching these tags
AllModules bool // Run all loaded modules
ListModules bool // List available modules and exit
}
const (
@@ -85,9 +91,11 @@ func Parse() *Settings {
flagSet.BoolVar(&settings.JavaScript, "js", false, "Enable JavaScript scans"),
flagSet.BoolVar(&settings.CMS, "cms", false, "Enable CMS detection"),
flagSet.BoolVar(&settings.Headers, "headers", false, "Enable HTTP Header Analysis"),
flagSet.BoolVarP(&settings.SecurityHeaders, "security-headers", "sh", false, "Enable security header analysis (missing/weak headers)"),
flagSet.BoolVar(&settings.CloudStorage, "c3", false, "Enable C3 Misconfiguration Scan"),
flagSet.BoolVar(&settings.SubdomainTakeover, "st", false, "Enable Subdomain Takeover Check"),
flagSet.BoolVar(&settings.Shodan, "shodan", false, "Enable Shodan lookup (requires SHODAN_API_KEY env var)"),
flagSet.BoolVar(&settings.SecurityTrails, "securitytrails", false, "Enable SecurityTrails domain discovery (requires SECURITYTRAILS_API_KEY env var)"),
flagSet.BoolVar(&settings.SQL, "sql", false, "Enable SQL reconnaissance (admin panels, error disclosure)"),
flagSet.BoolVar(&settings.LFI, "lfi", false, "Enable LFI (Local File Inclusion) reconnaissance"),
flagSet.BoolVar(&settings.Framework, "framework", false, "Enable framework detection"),
@@ -105,6 +113,13 @@ func Parse() *Settings {
flagSet.BoolVar(&settings.ApiMode, "api", false, "Enable API mode. Only useful for internal lunchcat usage"),
)
flagSet.CreateGroup("modules", "Modules",
flagSet.StringVarP(&settings.Modules, "modules", "m", "", "Comma-separated list of module IDs to run"),
flagSet.StringVarP(&settings.ModuleTags, "module-tags", "mt", "", "Run modules matching these tags"),
flagSet.BoolVarP(&settings.AllModules, "all-modules", "am", false, "Run all loaded modules"),
flagSet.BoolVarP(&settings.ListModules, "list-modules", "lm", false, "List available modules and exit"),
)
if err := flagSet.Parse(); err != nil {
log.Fatalf("Could not parse flags: %s", err)
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
+162
View File
@@ -0,0 +1,162 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package logger
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
)
// Logger manages buffered file writers for efficient logging.
// File handles are kept open and writes are buffered to minimize I/O overhead.
type Logger struct {
mu sync.RWMutex
writers map[string]*bufio.Writer
files map[string]*os.File
}
var defaultLogger = &Logger{
writers: make(map[string]*bufio.Writer),
files: make(map[string]*os.File),
}
// Init creates the log directory if it doesn't exist.
func Init(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.Mkdir(dir, 0o750); err != nil {
return err
}
}
return nil
}
// getWriter returns a buffered writer for the given file path, creating it if needed.
func (l *Logger) getWriter(path string) (*bufio.Writer, error) {
l.mu.RLock()
w, exists := l.writers[path]
l.mu.RUnlock()
if exists {
return w, nil
}
l.mu.Lock()
defer l.mu.Unlock()
// Double-check after acquiring write lock
if w, exists = l.writers[path]; exists {
return w, nil
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600)
if err != nil {
return nil, err
}
w = bufio.NewWriter(f)
l.writers[path] = w
l.files[path] = f
return w, nil
}
// write writes text to the specified log file using buffered I/O.
func (l *Logger) write(path, text string) error {
w, err := l.getWriter(path)
if err != nil {
return err
}
l.mu.Lock()
_, err = w.WriteString(text)
l.mu.Unlock()
return err
}
// Flush flushes all buffered writers to disk.
func (l *Logger) Flush() error {
l.mu.Lock()
defer l.mu.Unlock()
for _, w := range l.writers {
if err := w.Flush(); err != nil {
return err
}
}
return nil
}
// Close flushes and closes all open file handles.
func (l *Logger) Close() error {
l.mu.Lock()
defer l.mu.Unlock()
var firstErr error
for path, w := range l.writers {
if err := w.Flush(); err != nil && firstErr == nil {
firstErr = err
}
if err := l.files[path].Close(); err != nil && firstErr == nil {
firstErr = err
}
}
l.writers = make(map[string]*bufio.Writer)
l.files = make(map[string]*os.File)
return firstErr
}
// CreateFile initializes a log file for the given URL and writes the header.
func CreateFile(logFiles *[]string, url string, dir string) error {
sanitizedURL := url
if _, after, ok := strings.Cut(url, "://"); ok {
sanitizedURL = after
}
path := filepath.Join(dir, sanitizedURL+".log")
header := fmt.Sprintf(" _____________\n__________(_)__ __/\n__ ___/_ /__ /_ \n_(__ )_ / _ __/ \n/____/ /_/ /_/ \n\nsif log file for %s\nhttps://sif.sh\n\n", url)
if err := defaultLogger.write(path, header); err != nil {
return err
}
*logFiles = append(*logFiles, path)
return nil
}
// Write appends text to the log file for the given URL.
func Write(url string, dir string, text string) error {
path := filepath.Join(dir, url+".log")
return defaultLogger.write(path, text)
}
// WriteHeader writes a section header to the log file.
func WriteHeader(url string, dir string, scan string) error {
return Write(url, dir, fmt.Sprintf("\n\n--------------\nStarting %s\n--------------\n", scan))
}
// Flush flushes all buffered log data to disk.
func Flush() error {
return defaultLogger.Flush()
}
// Close flushes and closes all log files. Should be called before program exit.
func Close() error {
return defaultLogger.Close()
}
+196
View File
@@ -0,0 +1,196 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package logger
import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
)
func TestInit(t *testing.T) {
tmpDir := t.TempDir()
logDir := filepath.Join(tmpDir, "logs")
if err := Init(logDir); err != nil {
t.Fatalf("Init failed: %v", err)
}
if _, err := os.Stat(logDir); os.IsNotExist(err) {
t.Fatal("Init did not create log directory")
}
// Second call should be a no-op
if err := Init(logDir); err != nil {
t.Fatalf("Init failed on existing directory: %v", err)
}
}
func TestWriteAndFlush(t *testing.T) {
tmpDir := t.TempDir()
// Write some data
if err := Write("test", tmpDir, "hello world\n"); err != nil {
t.Fatalf("Write failed: %v", err)
}
// Flush to ensure data is written
if err := Flush(); err != nil {
t.Fatalf("Flush failed: %v", err)
}
// Read back and verify
content, err := os.ReadFile(filepath.Join(tmpDir, "test.log"))
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if string(content) != "hello world\n" {
t.Errorf("Expected 'hello world\\n', got %q", content)
}
// Cleanup
Close()
}
func TestWriteHeader(t *testing.T) {
tmpDir := t.TempDir()
if err := WriteHeader("test", tmpDir, "TestScan"); err != nil {
t.Fatalf("WriteHeader failed: %v", err)
}
if err := Flush(); err != nil {
t.Fatalf("Flush failed: %v", err)
}
content, err := os.ReadFile(filepath.Join(tmpDir, "test.log"))
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if !strings.Contains(string(content), "Starting TestScan") {
t.Errorf("Expected header to contain 'Starting TestScan', got %q", content)
}
Close()
}
func TestCreateFile(t *testing.T) {
tmpDir := t.TempDir()
var logFiles []string
if err := CreateFile(&logFiles, "https://example.com", tmpDir); err != nil {
t.Fatalf("CreateFile failed: %v", err)
}
if err := Flush(); err != nil {
t.Fatalf("Flush failed: %v", err)
}
if len(logFiles) != 1 {
t.Fatalf("Expected 1 log file, got %d", len(logFiles))
}
content, err := os.ReadFile(logFiles[0])
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if !strings.Contains(string(content), "sif log file for https://example.com") {
t.Errorf("Expected header content, got %q", content)
}
Close()
}
func TestConcurrentWrites(t *testing.T) {
tmpDir := t.TempDir()
var wg sync.WaitGroup
numWriters := 10
writesPerWriter := 100
for i := 0; i < numWriters; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < writesPerWriter; j++ {
if err := Write("concurrent", tmpDir, "data\n"); err != nil {
t.Errorf("Write failed: %v", err)
}
}
}(i)
}
wg.Wait()
if err := Flush(); err != nil {
t.Fatalf("Flush failed: %v", err)
}
content, err := os.ReadFile(filepath.Join(tmpDir, "concurrent.log"))
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
lines := strings.Count(string(content), "data\n")
expected := numWriters * writesPerWriter
if lines != expected {
t.Errorf("Expected %d lines, got %d", expected, lines)
}
Close()
}
func TestClose(t *testing.T) {
tmpDir := t.TempDir()
if err := Write("close_test", tmpDir, "before close\n"); err != nil {
t.Fatalf("Write failed: %v", err)
}
if err := Close(); err != nil {
t.Fatalf("Close failed: %v", err)
}
// Verify data was flushed on close
content, err := os.ReadFile(filepath.Join(tmpDir, "close_test.log"))
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if string(content) != "before close\n" {
t.Errorf("Expected 'before close\\n', got %q", content)
}
// Write after close should create new file handle
if err := Write("close_test", tmpDir, "after close\n"); err != nil {
t.Fatalf("Write after close failed: %v", err)
}
if err := Close(); err != nil {
t.Fatalf("Second close failed: %v", err)
}
content, err = os.ReadFile(filepath.Join(tmpDir, "close_test.log"))
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
if string(content) != "before close\nafter close\n" {
t.Errorf("Expected both writes, got %q", content)
}
}
+400
View File
@@ -0,0 +1,400 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package modules
import (
"context"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"sync"
"time"
)
// MaxBodySize limits response body to prevent memory exhaustion.
const MaxBodySize = 5 * 1024 * 1024
// httpRequest represents a generated HTTP request.
type httpRequest struct {
Method string
URL string
Headers map[string]string
Body string
Payload string
Original string // Original path template
}
// ExecuteHTTPModule runs an HTTP-based module.
func ExecuteHTTPModule(ctx context.Context, target string, def *YAMLModule, opts Options) (*Result, error) {
if def.HTTP == nil {
return nil, fmt.Errorf("no HTTP configuration")
}
cfg := def.HTTP
result := &Result{
ModuleID: def.ID,
Target: target,
Findings: make([]Finding, 0),
}
// Create HTTP client
client := opts.Client
if client == nil {
client = &http.Client{
Timeout: opts.Timeout,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
}
// Generate requests based on paths and payloads
requests := generateHTTPRequests(target, cfg)
// Determine thread count
threads := cfg.Threads
if threads == 0 {
threads = opts.Threads
}
if threads == 0 {
threads = 10
}
// Execute requests concurrently
var wg sync.WaitGroup
var mu sync.Mutex
resultsChan := make(chan Finding, len(requests))
// Limit concurrency
sem := make(chan struct{}, threads)
for _, req := range requests {
select {
case <-ctx.Done():
return result, ctx.Err()
case sem <- struct{}{}:
}
wg.Add(1)
go func(r *httpRequest) {
defer wg.Done()
defer func() { <-sem }()
finding, ok := executeHTTPRequest(ctx, client, r, cfg, def.Info.Severity)
if ok {
resultsChan <- finding
}
}(req)
}
// Collect results
go func() {
wg.Wait()
close(resultsChan)
}()
for finding := range resultsChan {
mu.Lock()
result.Findings = append(result.Findings, finding)
mu.Unlock()
}
return result, nil
}
// generateHTTPRequests creates all requests based on paths and payloads.
func generateHTTPRequests(target string, cfg *HTTPConfig) []*httpRequest {
var requests []*httpRequest
// Ensure target has no trailing slash
target = strings.TrimSuffix(target, "/")
method := cfg.Method
if method == "" {
method = "GET"
}
// If no payloads, just use paths directly
if len(cfg.Payloads) == 0 {
for _, path := range cfg.Paths {
url := substituteVariables(path, target, "")
requests = append(requests, &httpRequest{
Method: method,
URL: url,
Headers: cfg.Headers,
Body: cfg.Body,
Original: path,
})
}
return requests
}
// Generate requests with payloads
for _, path := range cfg.Paths {
for _, payload := range cfg.Payloads {
url := substituteVariables(path, target, payload)
body := substituteVariables(cfg.Body, target, payload)
requests = append(requests, &httpRequest{
Method: method,
URL: url,
Headers: cfg.Headers,
Body: body,
Payload: payload,
Original: path,
})
}
}
return requests
}
// substituteVariables replaces template variables in a string.
func substituteVariables(template, baseURL, payload string) string {
result := template
result = strings.ReplaceAll(result, "{{BaseURL}}", baseURL)
result = strings.ReplaceAll(result, "{{baseurl}}", baseURL)
result = strings.ReplaceAll(result, "{{payload}}", payload)
result = strings.ReplaceAll(result, "{{Payload}}", payload)
return result
}
// executeHTTPRequest executes a single HTTP request and checks matchers.
func executeHTTPRequest(ctx context.Context, client *http.Client, r *httpRequest, cfg *HTTPConfig, severity string) (Finding, bool) {
var body io.Reader
if r.Body != "" {
body = strings.NewReader(r.Body)
}
req, err := http.NewRequestWithContext(ctx, r.Method, r.URL, body)
if err != nil {
return Finding{}, false
}
// Set headers
for k, v := range r.Headers {
req.Header.Set(k, v)
}
if req.Header.Get("User-Agent") == "" {
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; sif/1.0)")
}
resp, err := client.Do(req)
if err != nil {
return Finding{}, false
}
defer resp.Body.Close()
// Read body with limit
respBody, err := io.ReadAll(io.LimitReader(resp.Body, MaxBodySize))
if err != nil {
return Finding{}, false
}
bodyStr := string(respBody)
// Check matchers
if !checkMatchers(cfg.Matchers, resp, bodyStr) {
return Finding{}, false
}
// Extract data
extracted := runExtractors(cfg.Extractors, resp, bodyStr)
return Finding{
URL: r.URL,
Severity: severity,
Evidence: truncateEvidence(bodyStr),
Extracted: extracted,
}, true
}
// checkMatchers evaluates all matchers against the response.
func checkMatchers(matchers []Matcher, resp *http.Response, body string) bool {
if len(matchers) == 0 {
return false
}
// Default to AND condition across matchers
for i := range matchers {
matched := checkMatcher(&matchers[i], resp, body)
if matchers[i].Negative {
matched = !matched
}
if !matched {
return false // AND logic
}
}
return true
}
// checkMatcher evaluates a single matcher.
func checkMatcher(m *Matcher, resp *http.Response, body string) bool {
part := getPart(m.Part, resp, body)
switch m.Type {
case "status":
for _, status := range m.Status {
if resp.StatusCode == status {
return true
}
}
return false
case "word":
return checkWords(part, m.Words, m.Condition)
case "regex":
return checkRegex(part, m.Regex, m.Condition)
default:
return false
}
}
// getPart extracts the relevant part of the response.
func getPart(part string, resp *http.Response, body string) string {
switch part {
case "header", "headers":
var sb strings.Builder
for k, v := range resp.Header {
sb.WriteString(k)
sb.WriteString(": ")
sb.WriteString(strings.Join(v, ", "))
sb.WriteString("\n")
}
return sb.String()
case "body":
return body
case "all", "":
var sb strings.Builder
for k, v := range resp.Header {
sb.WriteString(k)
sb.WriteString(": ")
sb.WriteString(strings.Join(v, ", "))
sb.WriteString("\n")
}
sb.WriteString("\n")
sb.WriteString(body)
return sb.String()
default:
return body
}
}
// checkWords checks if any/all words are found.
func checkWords(content string, words []string, condition string) bool {
if condition == "or" {
for _, word := range words {
if strings.Contains(content, word) {
return true
}
}
return false
}
// Default to AND
for _, word := range words {
if !strings.Contains(content, word) {
return false
}
}
return true
}
// checkRegex checks if any/all regex patterns match.
func checkRegex(content string, patterns []string, condition string) bool {
if condition == "or" {
for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
continue
}
if re.MatchString(content) {
return true
}
}
return false
}
// Default to AND
for _, pattern := range patterns {
re, err := regexp.Compile(pattern)
if err != nil {
return false
}
if !re.MatchString(content) {
return false
}
}
return true
}
// runExtractors extracts data from the response.
func runExtractors(extractors []Extractor, resp *http.Response, body string) map[string]string {
if len(extractors) == 0 {
return nil
}
result := make(map[string]string)
for _, e := range extractors {
part := getPart(e.Part, resp, body)
if e.Type == "regex" {
for _, pattern := range e.Regex {
re, err := regexp.Compile(pattern)
if err != nil {
continue
}
matches := re.FindStringSubmatch(part)
if len(matches) > e.Group {
result[e.Name] = matches[e.Group]
break
}
}
}
}
return result
}
// truncateEvidence limits evidence length for storage.
func truncateEvidence(s string) string {
const maxLen = 500
if len(s) > maxLen {
return s[:maxLen] + "..."
}
return s
}
// ExecuteDNSModule runs a DNS-based module (stub for now).
func ExecuteDNSModule(ctx context.Context, target string, def *YAMLModule, opts Options) (*Result, error) {
// TODO: Implement DNS module execution
return &Result{
ModuleID: def.ID,
Target: target,
Findings: []Finding{},
}, nil
}
// ExecuteTCPModule runs a TCP-based module (stub for now).
func ExecuteTCPModule(ctx context.Context, target string, def *YAMLModule, opts Options) (*Result, error) {
// TODO: Implement TCP module execution
return &Result{
ModuleID: def.ID,
Target: target,
Findings: []Finding{},
}, nil
}
+153
View File
@@ -0,0 +1,153 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package modules
import (
"fmt"
"os"
"path/filepath"
"runtime"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/output"
)
// Loader handles module discovery and loading.
type Loader struct {
builtinDir string
userDir string
loaded int
}
// NewLoader creates a new module loader.
// It automatically detects the built-in modules directory and sets up
// the user modules directory based on the operating system.
func NewLoader() (*Loader, error) {
home, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("get home dir: %w", err)
}
// Find built-in modules relative to executable
execPath, err := os.Executable()
if err != nil {
execPath = "."
}
builtinDir := filepath.Join(filepath.Dir(execPath), "modules")
// Also check current working directory for development
if _, err := os.Stat(builtinDir); os.IsNotExist(err) {
builtinDir = "modules"
}
// User modules directory based on OS
var userDir string
switch runtime.GOOS {
case "windows":
userDir = filepath.Join(home, "AppData", "Local", "sif", "modules")
default:
userDir = filepath.Join(home, ".config", "sif", "modules")
}
return &Loader{
builtinDir: builtinDir,
userDir: userDir,
}, nil
}
// LoadAll discovers and loads all modules from both built-in
// and user directories.
func (l *Loader) LoadAll() error {
// Load built-in modules first
if err := l.loadDir(l.builtinDir, false); err != nil {
log.Debugf("No built-in modules found: %v", err)
}
// Load user modules (can override built-in)
if err := l.loadDir(l.userDir, true); err != nil {
// User dir might not exist, that's OK
if !os.IsNotExist(err) {
log.Debugf("No user modules found: %v", err)
}
}
if l.loaded > 0 {
modLog := output.Module("MODULES")
modLog.Info("Loaded %d modules", l.loaded)
}
return nil
}
// loadDir loads modules from a directory.
func (l *Loader) loadDir(dir string, userDefined bool) error {
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
switch filepath.Ext(path) {
case ".yaml", ".yml":
if err := l.loadYAML(path); err != nil {
log.Warnf("Failed to load module %s: %v", path, err)
} else {
l.loaded++
}
case ".go":
if err := l.loadScript(path); err != nil {
log.Debugf("Failed to load script %s: %v", path, err)
} else {
l.loaded++
}
}
return nil
})
}
// loadYAML loads a YAML module definition.
func (l *Loader) loadYAML(path string) error {
def, err := ParseYAMLModule(path)
if err != nil {
return err
}
module := newYAMLModuleWrapper(def, path)
Register(module)
return nil
}
// loadScript loads a Go script module.
// Implementation will be provided in script.go.
func (l *Loader) loadScript(path string) error {
// Will be implemented in script.go
return nil
}
// BuiltinDir returns the built-in modules directory path.
func (l *Loader) BuiltinDir() string {
return l.builtinDir
}
// UserDir returns the user modules directory path.
func (l *Loader) UserDir() string {
return l.userDir
}
// Loaded returns the number of loaded modules.
func (l *Loader) Loaded() int {
return l.loaded
}
+106
View File
@@ -0,0 +1,106 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
// Package modules provides the module system infrastructure for SIF.
// It defines the core interfaces, types, and utilities for building
// and executing security scanning modules.
package modules
import (
"context"
"net/http"
"time"
)
// ModuleType represents the type of module.
type ModuleType string
const (
TypeHTTP ModuleType = "http"
TypeDNS ModuleType = "dns"
TypeTCP ModuleType = "tcp"
TypeScript ModuleType = "script"
)
// Module is the interface all modules implement.
// Each module must provide metadata, specify its type, and implement
// an Execute method for running the scan against a target.
type Module interface {
// Info returns the module metadata.
Info() Info
// Type returns the module type (http, dns, tcp, script).
Type() ModuleType
// Execute runs the module against the specified target.
Execute(ctx context.Context, target string, opts Options) (*Result, error)
}
// Info contains module metadata.
type Info struct {
ID string `yaml:"id" json:"id"`
Name string `yaml:"name" json:"name"`
Author string `yaml:"author" json:"author"`
Severity string `yaml:"severity" json:"severity"`
Description string `yaml:"description" json:"description"`
Tags []string `yaml:"tags" json:"tags"`
}
// Options for module execution.
type Options struct {
Timeout time.Duration
Threads int
LogDir string
Client *http.Client
}
// Result from module execution.
type Result struct {
ModuleID string `json:"module_id"`
Target string `json:"target"`
Findings []Finding `json:"findings,omitempty"`
}
// ResultType implements the ScanResult interface from pkg/scan.
func (r *Result) ResultType() string {
return r.ModuleID
}
// Finding represents a discovered issue.
type Finding struct {
URL string `json:"url,omitempty"`
Severity string `json:"severity"`
Evidence string `json:"evidence,omitempty"`
Extracted map[string]string `json:"extracted,omitempty"`
}
// Matcher defines matching logic for module responses.
// Matchers are used to determine if a response indicates a vulnerability.
type Matcher struct {
Type string `yaml:"type"` // regex, status, word, size
Part string `yaml:"part"` // body, header, all
Regex []string `yaml:"regex,omitempty"`
Words []string `yaml:"words,omitempty"`
Status []int `yaml:"status,omitempty"`
Condition string `yaml:"condition"` // and, or
Negative bool `yaml:"negative"`
}
// Extractor defines data extraction from responses.
// Extractors pull specific data from matched responses for reporting.
type Extractor struct {
Type string `yaml:"type"` // regex, kval, json
Name string `yaml:"name"`
Part string `yaml:"part"`
Regex []string `yaml:"regex,omitempty"`
Group int `yaml:"group"`
}
+92
View File
@@ -0,0 +1,92 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package modules
import "sync"
var (
registry = make(map[string]Module)
mu sync.RWMutex
)
// Register adds a module to the registry.
// If a module with the same ID already exists, it will be overwritten.
func Register(m Module) {
mu.Lock()
defer mu.Unlock()
registry[m.Info().ID] = m
}
// Get returns a module by ID.
// The second return value indicates whether the module was found.
func Get(id string) (Module, bool) {
mu.RLock()
defer mu.RUnlock()
m, ok := registry[id]
return m, ok
}
// All returns all registered modules.
func All() []Module {
mu.RLock()
defer mu.RUnlock()
result := make([]Module, 0, len(registry))
for _, m := range registry {
result = append(result, m)
}
return result
}
// ByTag returns modules matching a tag.
func ByTag(tag string) []Module {
mu.RLock()
defer mu.RUnlock()
var result []Module
for _, m := range registry {
for _, t := range m.Info().Tags {
if t == tag {
result = append(result, m)
break
}
}
}
return result
}
// ByType returns modules of a specific type.
func ByType(t ModuleType) []Module {
mu.RLock()
defer mu.RUnlock()
var result []Module
for _, m := range registry {
if m.Type() == t {
result = append(result, m)
}
}
return result
}
// Count returns the number of registered modules.
func Count() int {
mu.RLock()
defer mu.RUnlock()
return len(registry)
}
// Clear removes all modules from the registry.
// This is primarily useful for testing.
func Clear() {
mu.Lock()
defer mu.Unlock()
registry = make(map[string]Module)
}
+155
View File
@@ -0,0 +1,155 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
-------------------------------------------------------------------------------------------------
: :
: SIF - Blazing-fast pentesting suite :
: Blaze - BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
-------------------------------------------------------------------------------------------------
*/
package modules
import (
"context"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
// YAMLModule represents a parsed YAML module file
type YAMLModule struct {
ID string `yaml:"id"`
Info YAMLModuleInfo `yaml:"info"`
Type ModuleType `yaml:"type"`
HTTP *HTTPConfig `yaml:"http,omitempty"`
DNS *DNSConfig `yaml:"dns,omitempty"`
TCP *TCPConfig `yaml:"tcp,omitempty"`
}
// YAMLModuleInfo contains module metadata
type YAMLModuleInfo struct {
Name string `yaml:"name"`
Author string `yaml:"author"`
Severity string `yaml:"severity"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
}
// HTTPConfig defines HTTP module settings
type HTTPConfig struct {
Method string `yaml:"method"`
Paths []string `yaml:"paths"`
Payloads []string `yaml:"payloads,omitempty"`
Headers map[string]string `yaml:"headers,omitempty"`
Body string `yaml:"body,omitempty"`
Attack string `yaml:"attack,omitempty"` // sniper, pitchfork, clusterbomb
Threads int `yaml:"threads,omitempty"`
Matchers []Matcher `yaml:"matchers"`
Extractors []Extractor `yaml:"extractors,omitempty"`
}
// DNSConfig defines DNS module settings
type DNSConfig struct {
Type string `yaml:"type"` // A, AAAA, MX, TXT, NS, etc.
Name string `yaml:"name"`
Matchers []Matcher `yaml:"matchers"`
Extractors []Extractor `yaml:"extractors,omitempty"`
}
// TCPConfig defines TCP module settings
type TCPConfig struct {
Port int `yaml:"port"`
Data string `yaml:"data,omitempty"`
Matchers []Matcher `yaml:"matchers"`
Extractors []Extractor `yaml:"extractors,omitempty"`
}
// ParseYAMLModule parses a YAML file into a module definition
func ParseYAMLModule(path string) (*YAMLModule, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read module file: %w", err)
}
var ym YAMLModule
if err := yaml.Unmarshal(data, &ym); err != nil {
return nil, fmt.Errorf("parse yaml: %w", err)
}
if ym.ID == "" {
return nil, fmt.Errorf("module missing required field: id")
}
if ym.Type == "" {
return nil, fmt.Errorf("module missing required field: type")
}
return &ym, nil
}
// yamlModuleWrapper wraps YAMLModule to implement the Module interface
type yamlModuleWrapper struct {
def *YAMLModule
path string
}
// newYAMLModuleWrapper creates a Module from a YAMLModule definition
func newYAMLModuleWrapper(def *YAMLModule, path string) *yamlModuleWrapper {
return &yamlModuleWrapper{def: def, path: path}
}
// Info returns the module metadata
func (m *yamlModuleWrapper) Info() Info {
return Info{
ID: m.def.ID,
Name: m.def.Info.Name,
Author: m.def.Info.Author,
Severity: m.def.Info.Severity,
Description: m.def.Info.Description,
Tags: m.def.Info.Tags,
}
}
// Type returns the module type
func (m *yamlModuleWrapper) Type() ModuleType {
return m.def.Type
}
// Execute runs the module (delegates to appropriate executor)
func (m *yamlModuleWrapper) Execute(ctx context.Context, target string, opts Options) (*Result, error) {
switch m.def.Type {
case TypeHTTP:
if m.def.HTTP == nil {
return nil, fmt.Errorf("HTTP module missing http configuration")
}
return ExecuteHTTPModule(ctx, target, m.def, opts)
case TypeDNS:
if m.def.DNS == nil {
return nil, fmt.Errorf("DNS module missing dns configuration")
}
return ExecuteDNSModule(ctx, target, m.def, opts)
case TypeTCP:
if m.def.TCP == nil {
return nil, fmt.Errorf("TCP module missing tcp configuration")
}
return ExecuteTCPModule(ctx, target, m.def, opts)
default:
return nil, fmt.Errorf("unsupported module type: %s", m.def.Type)
}
}
+9 -11
View File
@@ -4,7 +4,7 @@
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
@@ -13,25 +13,23 @@
package format
import (
"fmt"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/projectdiscovery/nuclei/v2/pkg/output"
nucleiout "github.com/projectdiscovery/nuclei/v3/pkg/output"
)
func FormatLine(event *output.ResultEvent) string {
output := event.TemplateID
func FormatLine(event *nucleiout.ResultEvent) string {
line := event.TemplateID
if event.MatcherName != "" {
output += ":" + styles.Highlight.Render(event.MatcherName)
line += ":" + styles.Highlight.Render(event.MatcherName)
} else if event.ExtractorName != "" {
output += ":" + styles.Highlight.Render(event.ExtractorName)
line += ":" + styles.Highlight.Render(event.ExtractorName)
}
output += " [" + event.Type + "]"
output += " [" + formatSeverity(fmt.Sprintf("%s", event.Info.SeverityHolder.Severity)) + "]"
line += " [" + event.Type + "]"
line += " [" + formatSeverity(event.Info.SeverityHolder.Severity.String()) + "]"
return output
return line
}
func formatSeverity(severity string) string {
+33 -7
View File
@@ -4,7 +4,7 @@
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
@@ -15,11 +15,14 @@ package templates
import (
"archive/tar"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/charmbracelet/log"
)
@@ -37,7 +40,12 @@ func Install(logger *log.Logger) error {
logger.Infof("nuclei-templates directory not found. Installing...")
resp, err := http.Get(fmt.Sprintf(archive, ref))
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf(archive, ref), http.NoBody)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
@@ -47,37 +55,55 @@ func Install(logger *log.Logger) error {
if err != nil {
return err
}
defer tarball.Close()
defer func() {
if cerr := tarball.Close(); cerr != nil {
logger.Warnf("closing gzip reader: %v", cerr)
}
}()
data := tar.NewReader(tarball)
dest, err := os.Getwd()
if err != nil {
return err
}
cleanDest := filepath.Clean(dest)
for {
header, err := data.Next()
if errors.Is(io.EOF, err) {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err
}
// guard against path traversal ("Zip Slip"): the resolved path must
// stay within the extraction directory before any filesystem op.
target := filepath.Join(cleanDest, header.Name)
if !strings.HasPrefix(target, cleanDest+string(os.PathSeparator)) {
return fmt.Errorf("invalid archive entry %q: escapes extraction directory", header.Name)
}
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(header.Name, 0755); err != nil {
if err := os.Mkdir(target, 0o750); err != nil {
return err
}
case tar.TypeReg:
file, err := os.Create(header.Name)
file, err := os.Create(target)
if err != nil {
return err
}
if _, err := io.Copy(file, data); err != nil {
file.Close()
return err
}
file.Close()
}
}
if err = os.Rename(fmt.Sprintf("nuclei-templates-%s", ref), "nuclei-templates"); err != nil {
if err := os.Rename(fmt.Sprintf("nuclei-templates-%s", ref), "nuclei-templates"); err != nil {
return err
}
+295
View File
@@ -0,0 +1,295 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package output
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/lipgloss"
)
// Clean, subtle color palette
var (
ColorGreen = lipgloss.Color("#22c55e") // success green
ColorBlue = lipgloss.Color("#3b82f6") // info blue
ColorYellow = lipgloss.Color("#eab308") // warning yellow
ColorRed = lipgloss.Color("#ef4444") // error red
ColorGray = lipgloss.Color("#6b7280") // muted gray
ColorWhite = lipgloss.Color("#f3f4f6") // bright text
)
// Prefix styles
var (
prefixInfo = lipgloss.NewStyle().Foreground(ColorBlue).Bold(true)
prefixSuccess = lipgloss.NewStyle().Foreground(ColorGreen).Bold(true)
prefixWarning = lipgloss.NewStyle().Foreground(ColorYellow).Bold(true)
prefixError = lipgloss.NewStyle().Foreground(ColorRed).Bold(true)
)
// Text styles
var (
Highlight = lipgloss.NewStyle().Bold(true).Foreground(ColorWhite)
Muted = lipgloss.NewStyle().Foreground(ColorGray)
Status = lipgloss.NewStyle().Bold(true).Foreground(ColorGreen)
)
// Box style for banners
var Box = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWhite).
BorderStyle(lipgloss.RoundedBorder()).
BorderForeground(ColorGray).
Align(lipgloss.Center).
PaddingRight(15).
PaddingLeft(15).
Width(60)
// Subheading style
var Subheading = lipgloss.NewStyle().
Foreground(ColorGray).
Align(lipgloss.Center).
PaddingRight(15).
PaddingLeft(15).
Width(60)
// Severity styles
var (
SeverityLow = lipgloss.NewStyle().Foreground(ColorGreen)
SeverityMedium = lipgloss.NewStyle().Foreground(ColorYellow)
SeverityHigh = lipgloss.NewStyle().Foreground(lipgloss.Color("#f97316")) // orange
SeverityCritical = lipgloss.NewStyle().Foreground(ColorRed).Bold(true)
)
// Module color palette - visually distinct, nice colors
var moduleColors = []lipgloss.Color{
lipgloss.Color("#6366f1"), // indigo
lipgloss.Color("#8b5cf6"), // violet
lipgloss.Color("#ec4899"), // pink
lipgloss.Color("#f97316"), // orange
lipgloss.Color("#14b8a6"), // teal
lipgloss.Color("#06b6d4"), // cyan
lipgloss.Color("#84cc16"), // lime
lipgloss.Color("#a855f7"), // purple
lipgloss.Color("#f43f5e"), // rose
lipgloss.Color("#0ea5e9"), // sky
}
// getModuleColor returns a consistent color for a module name
func getModuleColor(name string) lipgloss.Color {
// Simple hash to pick a color
hash := 0
for _, c := range name {
hash = hash*31 + int(c)
}
if hash < 0 {
hash = -hash
}
return moduleColors[hash%len(moduleColors)]
}
// moduleStyleFor returns a styled prefix for a module
func moduleStyleFor(name string) lipgloss.Style {
return lipgloss.NewStyle().
Background(getModuleColor(name)).
Foreground(lipgloss.Color("#ffffff")).
Bold(true).
Padding(0, 1)
}
// IsTTY returns true if stdout is a terminal
var IsTTY = checkTTY()
func checkTTY() bool {
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
return true
}
return false
}
// apiMode disables visual output when true
var apiMode bool
// SetAPIMode enables or disables API mode
func SetAPIMode(enabled bool) {
apiMode = enabled
}
// Info prints an informational message with [*] prefix
func Info(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixInfo.Render("[*]"), msg)
}
// Success prints a success message with [+] prefix
func Success(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixSuccess.Render("[+]"), msg)
}
// Warn prints a warning message with [!] prefix
func Warn(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixWarning.Render("[!]"), msg)
}
// Error prints an error message with [-] prefix
func Error(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", prefixError.Render("[-]"), msg)
}
// ScanStart prints a styled scan start message
func ScanStart(scanName string) {
if apiMode {
return
}
fmt.Printf("%s starting %s\n", prefixInfo.Render("[*]"), scanName)
}
// ScanComplete prints a styled scan completion message
func ScanComplete(scanName string, resultCount int, resultType string) {
if apiMode {
return
}
fmt.Printf("%s %s complete (%d %s)\n", prefixInfo.Render("[*]"), scanName, resultCount, resultType)
}
// Module creates a prefixed logger for a specific module/tool
func Module(name string) *ModuleLogger {
return &ModuleLogger{
name: name,
style: moduleStyleFor(name),
}
}
// ModuleLogger provides prefixed logging for a specific module
type ModuleLogger struct {
name string
style lipgloss.Style
}
func (m *ModuleLogger) prefix() string {
return m.style.Render(m.name)
}
// Info prints an info message with module prefix
func (m *ModuleLogger) Info(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s\n", m.prefix(), msg)
}
// Success prints a success message with module prefix
func (m *ModuleLogger) Success(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s %s\n", m.prefix(), prefixSuccess.Render("✓"), msg)
}
// Warn prints a warning message with module prefix
func (m *ModuleLogger) Warn(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s %s\n", m.prefix(), prefixWarning.Render("!"), msg)
}
// Error prints an error message with module prefix
func (m *ModuleLogger) Error(format string, args ...interface{}) {
if apiMode {
return
}
msg := fmt.Sprintf(format, args...)
fmt.Printf("%s %s %s\n", m.prefix(), prefixError.Render("✗"), msg)
}
// Start prints a scan start message with module prefix (adds newline before for separation)
func (m *ModuleLogger) Start() {
if apiMode {
return
}
fmt.Printf("\n%s starting scan\n", m.prefix())
}
// Complete prints a scan complete message with module prefix
func (m *ModuleLogger) Complete(resultCount int, resultType string) {
if apiMode {
return
}
fmt.Printf("%s complete (%d %s)\n", m.prefix(), resultCount, resultType)
}
// ClearLine clears the current line (for progress bar updates)
func ClearLine() {
if !IsTTY {
return
}
fmt.Print("\033[2K\r")
}
// Summary styles
var (
summaryHeader = lipgloss.NewStyle().
Bold(true).
Foreground(ColorWhite).
Background(lipgloss.Color("#22c55e")).
Padding(0, 2)
summaryLine = lipgloss.NewStyle().
Foreground(ColorGray)
)
// PrintSummary prints a clean scan completion summary
func PrintSummary(scans []string, logFiles []string) {
if apiMode {
return
}
fmt.Println()
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
fmt.Println()
fmt.Printf(" %s\n", summaryHeader.Render("SCAN COMPLETE"))
fmt.Println()
// Print scans
scanList := strings.Join(scans, ", ")
fmt.Printf(" %s %s\n", Muted.Render("Scans:"), scanList)
// Print log files if any
if len(logFiles) > 0 {
fmt.Printf(" %s %s\n", Muted.Render("Output:"), strings.Join(logFiles, ", "))
}
fmt.Println()
fmt.Println(summaryLine.Render("────────────────────────────────────────────────────────────"))
fmt.Println()
}
+171
View File
@@ -0,0 +1,171 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package output
import (
"fmt"
"sync"
"sync/atomic"
)
// Progress bar configuration
const (
progressWidth = 30
progressFilled = "="
progressCurrent = ">"
progressEmpty = " "
)
// Progress displays a progress bar for operations with known counts
type Progress struct {
total int64
current int64
message string
lastItem string
mu sync.Mutex
paused bool
}
// NewProgress creates a new progress bar
func NewProgress(total int, message string) *Progress {
return &Progress{
total: int64(total),
message: message,
}
}
// Increment advances the progress by 1 and optionally updates the current item
func (p *Progress) Increment(item string) {
atomic.AddInt64(&p.current, 1)
p.mu.Lock()
p.lastItem = item
paused := p.paused
p.mu.Unlock()
if !paused {
p.render()
}
}
// Set sets the progress to a specific value
func (p *Progress) Set(current int, item string) {
atomic.StoreInt64(&p.current, int64(current))
p.mu.Lock()
p.lastItem = item
paused := p.paused
p.mu.Unlock()
if !paused {
p.render()
}
}
// Pause temporarily stops rendering (use before printing other output)
func (p *Progress) Pause() {
p.mu.Lock()
p.paused = true
p.mu.Unlock()
ClearLine()
}
// Resume resumes rendering after a pause
func (p *Progress) Resume() {
p.mu.Lock()
p.paused = false
p.mu.Unlock()
p.render()
}
// Done clears the progress bar line
func (p *Progress) Done() {
if apiMode || !IsTTY {
return
}
ClearLine()
}
func (p *Progress) render() {
if apiMode {
return
}
// In non-TTY mode, print progress at milestones only
if !IsTTY {
current := atomic.LoadInt64(&p.current)
total := p.total
if total <= 0 {
return
}
percent := int(current * 100 / total)
// Print at 0%, 25%, 50%, 75%, 100%
if current == 1 || percent == 25 || percent == 50 || percent == 75 || current == total {
fmt.Printf(" [%d%%] %d/%d\n", percent, current, total)
}
return
}
current := atomic.LoadInt64(&p.current)
total := p.total
p.mu.Lock()
lastItem := p.lastItem
p.mu.Unlock()
// Calculate percentage
percent := 0
if total > 0 {
percent = int(current * 100 / total)
}
// Build progress bar
filled := 0
if total > 0 {
filled = int(progressWidth * current / total)
}
if filled > progressWidth {
filled = progressWidth
}
bar := ""
for i := 0; i < progressWidth; i++ {
switch {
case i < filled:
bar += progressFilled
case i == filled && current < total:
bar += progressCurrent
default:
bar += progressEmpty
}
}
// Truncate item if too long
maxItemLen := 30
if len(lastItem) > maxItemLen {
lastItem = lastItem[:maxItemLen-3] + "..."
}
// Format: [========> ] 45% (4500/10000) /admin
line := fmt.Sprintf(" [%s] %3d%% (%d/%d) %s",
prefixInfo.Render(bar),
percent,
current,
total,
Muted.Render(lastItem),
)
ClearLine()
fmt.Print(line)
}
+34
View File
@@ -0,0 +1,34 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package output
import "testing"
// the non-tty milestone path divides current*100/total, so a zero-total bar
// used to panic with integer divide-by-zero when piped or redirected.
func TestProgressZeroTotalNoPanic(t *testing.T) {
p := NewProgress(0, "scanning")
p.Increment("item")
p.Set(0, "item")
p.Done()
}
func TestProgressCounts(t *testing.T) {
p := NewProgress(4, "scanning")
for i := 0; i < 4; i++ {
p.Increment("x")
}
if p.current != 4 {
t.Errorf("current = %d, want 4", p.current)
}
}
+121
View File
@@ -0,0 +1,121 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package output
import (
"fmt"
"os"
"sync"
"time"
)
// Spinner frames using simple ASCII
var spinnerFrames = []string{"|", "/", "-", "\\"}
// Spinner displays an animated spinner for indeterminate operations
type Spinner struct {
message string
running bool
done chan struct{}
mu sync.Mutex
interval time.Duration
}
// NewSpinner creates a new spinner with the given message
func NewSpinner(message string) *Spinner {
return &Spinner{
message: message,
interval: 100 * time.Millisecond,
done: make(chan struct{}),
}
}
// Start begins the spinner animation
func (s *Spinner) Start() {
if apiMode {
return
}
s.mu.Lock()
if s.running {
s.mu.Unlock()
return
}
s.running = true
s.done = make(chan struct{})
s.mu.Unlock()
// In non-TTY mode, just print the message once
if !IsTTY {
fmt.Printf(" %s...\n", s.message)
return
}
go s.animate()
}
// Stop halts the spinner and clears the line
func (s *Spinner) Stop() {
if apiMode {
return
}
s.mu.Lock()
if !s.running {
s.mu.Unlock()
return
}
s.running = false
close(s.done)
s.mu.Unlock()
// Give animation goroutine time to exit
time.Sleep(s.interval)
// Clear the spinner line
if IsTTY {
ClearLine()
}
}
// Update changes the spinner message while running
func (s *Spinner) Update(message string) {
s.mu.Lock()
s.message = message
s.mu.Unlock()
}
func (s *Spinner) animate() {
frame := 0
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-s.done:
return
case <-ticker.C:
s.mu.Lock()
msg := s.message
s.mu.Unlock()
spinnerChar := prefixInfo.Render(spinnerFrames[frame])
line := fmt.Sprintf("\r %s %s", spinnerChar, msg)
fmt.Fprint(os.Stdout, "\033[2K") // Clear line
fmt.Fprint(os.Stdout, line)
frame = (frame + 1) % len(spinnerFrames)
}
}
}
+145
View File
@@ -0,0 +1,145 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
// Package patchnotes shows release notes pulled from the github releases.
package patchnotes
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/charmbracelet/glamour"
)
const releasesAPI = "https://api.github.com/repos/vmfunc/sif/releases"
type release struct {
TagName string `json:"tag_name"`
Name string `json:"name"`
Body string `json:"body"`
URL string `json:"html_url"`
}
func fetch(ctx context.Context, path string) (*release, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, releasesAPI+path, http.NoBody)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/vnd.github+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("github returned %s", resp.Status)
}
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
if err != nil {
return nil, err
}
var r release
if err := json.Unmarshal(body, &r); err != nil {
return nil, err
}
return &r, nil
}
// render turns a release's markdown body into styled terminal output, falling
// back to the raw body if glamour can't render it.
func render(r *release) string {
out, err := glamour.Render(r.Body, "dark")
if err != nil {
return r.Body
}
return fmt.Sprintf("%s\n%s", r.TagName, out)
}
// Print fetches the latest release and writes its notes to stdout. tag may be
// empty for the latest release, or a "vX" tag for a specific one.
func Print(tag string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
path := "/latest"
if tag != "" {
path = "/tags/" + tag
}
r, err := fetch(ctx, path)
if err != nil {
fmt.Printf("couldn't fetch patch notes: %v\n", err)
return
}
fmt.Print(render(r))
}
// ShowOnce prints the running version's notes the first time that version runs,
// then records it so it isn't shown again. best-effort: dev builds, the
// SIF_NO_PATCHNOTES opt-out, and any network failure stay silent.
func ShowOnce(version string) {
// only clean release tags (e.g. 2026.6.7) map to a github release; skip dev
// and pseudo-versions (a commit/dirty build) so we don't make a doomed call.
if version == "" || version == "dev" || strings.ContainsAny(version, "-+") || os.Getenv("SIF_NO_PATCHNOTES") != "" {
return
}
path, err := statePath()
if err != nil || hasSeen(path, version) {
return
}
// record before fetching so a flaky network doesn't nag on every run
recordSeen(path, version)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
r, err := fetch(ctx, "/tags/v"+version)
if err != nil {
return
}
fmt.Printf("\nwhat's new in this release:\n%s", render(r))
}
func statePath() (string, error) {
dir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "sif", "seen_version"), nil
}
func hasSeen(path, version string) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
return strings.TrimSpace(string(data)) == version
}
func recordSeen(path, version string) {
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
return
}
_ = os.WriteFile(path, []byte(version), 0o600)
}
+42
View File
@@ -0,0 +1,42 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package patchnotes
import (
"path/filepath"
"strings"
"testing"
)
func TestSeenRoundTrip(t *testing.T) {
path := filepath.Join(t.TempDir(), "sif", "seen_version")
if hasSeen(path, "2026.6.7") {
t.Fatal("nothing recorded yet, hasSeen should be false")
}
recordSeen(path, "2026.6.7")
if !hasSeen(path, "2026.6.7") {
t.Error("recorded version should read back as seen")
}
if hasSeen(path, "2026.6.8") {
t.Error("a different version should not be seen")
}
}
func TestRenderIncludesTag(t *testing.T) {
out := render(&release{TagName: "v2026.6.7", Body: "## what's changed\n- a thing"})
if !strings.Contains(out, "v2026.6.7") {
t.Errorf("rendered notes should include the tag, got %q", out)
}
}
@@ -0,0 +1,95 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package builtin
import (
"context"
"fmt"
"strings"
"github.com/dropalldatabases/sif/internal/modules"
"github.com/dropalldatabases/sif/internal/scan/frameworks"
)
type FrameworksModule struct{}
func (m *FrameworksModule) Info() modules.Info {
return modules.Info{
ID: "framework-detection",
Name: "Web Framework Detection",
Author: "sif",
Severity: "info",
Description: "Detects web frameworks with version and CVE mapping",
Tags: []string{"recon", "framework", "cve"},
}
}
func (m *FrameworksModule) Type() modules.ModuleType {
return modules.TypeHTTP
}
func (m *FrameworksModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
// Call existing legacy frameworks.DetectFramework function
frameworkResult, err := frameworks.DetectFramework(target, opts.Timeout, opts.LogDir)
if err != nil {
return nil, err
}
result := &modules.Result{
ModuleID: m.Info().ID,
Target: target,
Findings: []modules.Finding{},
}
// Return empty if no framework detected
if frameworkResult == nil {
return result, nil
}
// Construct finding
evidence := fmt.Sprintf("Detected %s framework (version: %s, confidence: %.2f)",
frameworkResult.Name, frameworkResult.Version, frameworkResult.Confidence)
severity := "info"
if frameworkResult.RiskLevel != "" && frameworkResult.RiskLevel != "low" {
severity = frameworkResult.RiskLevel
}
finding := modules.Finding{
URL: target,
Severity: severity,
Evidence: evidence,
Extracted: map[string]string{
"framework": frameworkResult.Name,
"version": frameworkResult.Version,
"confidence": fmt.Sprintf("%.2f", frameworkResult.Confidence),
"version_confidence": fmt.Sprintf("%.2f", frameworkResult.VersionConfidence),
},
}
// Add CVE information
if len(frameworkResult.CVEs) > 0 {
finding.Extracted["cves"] = strings.Join(frameworkResult.CVEs, ", ")
finding.Extracted["risk_level"] = frameworkResult.RiskLevel
}
// Add recommendations
if len(frameworkResult.Suggestions) > 0 {
finding.Extracted["recommendations"] = strings.Join(frameworkResult.Suggestions, "; ")
}
result.Findings = append(result.Findings, finding)
return result, nil
}
+143
View File
@@ -0,0 +1,143 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package builtin
import (
"context"
"fmt"
"github.com/dropalldatabases/sif/internal/modules"
"github.com/dropalldatabases/sif/internal/scan"
)
type NucleiModule struct{}
func (m *NucleiModule) Info() modules.Info {
return modules.Info{
ID: "nuclei-scan",
Name: "Nuclei Vulnerability Scanner",
Author: "sif",
Severity: "high",
Description: "Runs Nuclei vulnerability scanning templates against target",
Tags: []string{"vuln", "nuclei", "cve"},
}
}
func (m *NucleiModule) Type() modules.ModuleType {
return modules.TypeScript
}
func (m *NucleiModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
// Call existing legacy scan.Nuclei function
nucleiResults, err := scan.Nuclei(target, opts.Timeout, opts.Threads, opts.LogDir)
if err != nil {
return nil, err
}
result := &modules.Result{
ModuleID: m.Info().ID,
Target: target,
Findings: make([]modules.Finding, 0, len(nucleiResults)),
}
// Process nuclei results into module findings
for i := range nucleiResults {
event := &nucleiResults[i]
severity := "info"
switch event.Info.SeverityHolder.Severity.String() {
case "critical":
severity = "critical"
case "high":
severity = "high"
case "medium":
severity = "medium"
case "low":
severity = "low"
}
evidence := fmt.Sprintf("[%s] %s", event.TemplateID, event.Info.Name)
if event.Matched != "" {
evidence = fmt.Sprintf("[%s] %s - matched: %s", event.TemplateID, event.Info.Name, event.Matched)
}
finding := modules.Finding{
URL: event.Host,
Severity: severity,
Evidence: evidence,
Extracted: map[string]string{
"template_id": event.TemplateID,
"template_name": event.Info.Name,
"severity": event.Info.SeverityHolder.Severity.String(),
},
}
// Template info
if event.Type != "" {
finding.Extracted["type"] = event.Type
}
// Matcher name
if event.MatcherName != "" {
finding.Extracted["matcher_name"] = event.MatcherName
}
// Extractor name
if event.ExtractorName != "" {
finding.Extracted["extractor_name"] = event.ExtractorName
}
// Matched line/data
if event.Matched != "" {
finding.Extracted["matched"] = event.Matched
}
// Metadata
if len(event.Info.Metadata) > 0 {
for key, value := range event.Info.Metadata {
finding.Extracted[fmt.Sprintf("metadata_%s", key)] = fmt.Sprintf("%v", value)
}
}
// Tags
if !event.Info.Tags.IsEmpty() {
tagStr := ""
for _, tag := range event.Info.Tags.ToSlice() {
if tagStr != "" {
tagStr += ", "
}
tagStr += tag
}
finding.Extracted["tags"] = tagStr
}
// Reference
if event.Info.Reference != nil && !event.Info.Reference.IsEmpty() {
refStr := ""
for _, ref := range event.Info.Reference.ToSlice() {
if refStr != "" {
refStr += "; "
}
refStr += ref
}
finding.Extracted["references"] = refStr
}
result.Findings = append(result.Findings, finding)
}
return result, nil
}
@@ -4,19 +4,22 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package utils
package builtin
import (
"fmt"
)
import "github.com/dropalldatabases/sif/internal/modules"
func ReturnApiOutput() {
const data = `{"key": "value"}`
fmt.Println(data)
// Register registers all Go-based built-in scans as modules.
// Allows complex Go scans to participate in the module system
func Register() {
modules.Register(&ShodanModule{})
modules.Register(&FrameworksModule{})
modules.Register(&NucleiModule{})
modules.Register(&WhoisModule{})
modules.Register(&SecurityTrailsModule{})
}
@@ -0,0 +1,79 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package builtin
import (
"context"
"fmt"
"strings"
"github.com/dropalldatabases/sif/internal/modules"
"github.com/dropalldatabases/sif/internal/scan"
)
type SecurityTrailsModule struct{}
func (m *SecurityTrailsModule) Info() modules.Info {
return modules.Info{
ID: "securitytrails-lookup",
Name: "SecurityTrails Domain Discovery",
Author: "sif",
Severity: "info",
Description: "Queries SecurityTrails API for subdomains and associated domains (requires SECURITYTRAILS_API_KEY)",
Tags: []string{"recon", "osint", "dns", "subdomains"},
}
}
func (m *SecurityTrailsModule) Type() modules.ModuleType {
return modules.TypeScript
}
func (m *SecurityTrailsModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
stResult, err := scan.SecurityTrails(target, opts.Timeout, opts.LogDir)
if err != nil {
return nil, err
}
result := &modules.Result{
ModuleID: m.Info().ID,
Target: target,
Findings: []modules.Finding{},
}
if stResult == nil {
return result, nil
}
finding := modules.Finding{
URL: target,
Severity: "info",
Evidence: fmt.Sprintf("discovered %d subdomains and %d associated domains",
len(stResult.Subdomains), len(stResult.AssociatedDomains)),
Extracted: map[string]string{
"domain": stResult.Domain,
"subdomain_count": fmt.Sprintf("%d", len(stResult.Subdomains)),
"associated_count": fmt.Sprintf("%d", len(stResult.AssociatedDomains)),
},
}
if len(stResult.Subdomains) > 0 {
finding.Extracted["subdomains"] = strings.Join(stResult.Subdomains, ", ")
}
if len(stResult.AssociatedDomains) > 0 {
finding.Extracted["associated_domains"] = strings.Join(stResult.AssociatedDomains, ", ")
}
result.Findings = append(result.Findings, finding)
return result, nil
}
+158
View File
@@ -0,0 +1,158 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package builtin
import (
"context"
"fmt"
"strings"
"github.com/dropalldatabases/sif/internal/modules"
"github.com/dropalldatabases/sif/internal/scan"
)
type ShodanModule struct{}
func (m *ShodanModule) Info() modules.Info {
return modules.Info{
ID: "shodan-lookup",
Name: "Shodan Host Intelligence",
Author: "sif",
Severity: "info",
Description: "Queries Shodan API for host information, open ports, and vulnerabilities (requires SHODAN_API_KEY)",
Tags: []string{"recon", "osint", "shodan", "vulns"},
}
}
func (m *ShodanModule) Type() modules.ModuleType {
return modules.TypeScript
}
func (m *ShodanModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
// Call existing legacy scan.Shodan function
shodanResult, err := scan.Shodan(target, opts.Timeout, opts.LogDir)
if err != nil {
return nil, err
}
result := &modules.Result{
ModuleID: m.Info().ID,
Target: target,
Findings: []modules.Finding{},
}
// If nothing returned, return empty result
if shodanResult == nil || shodanResult.IP == "" {
return result, nil
}
// Create main finding
evidence := fmt.Sprintf("Shodan data found for %s", shodanResult.IP)
severity := "info"
if len(shodanResult.Vulns) > 0 {
severity = "high"
evidence = fmt.Sprintf("Host %s has %d known vulnerabilities", shodanResult.IP, len(shodanResult.Vulns))
}
finding := modules.Finding{
URL: target,
Severity: severity,
Evidence: evidence,
Extracted: map[string]string{
"ip": shodanResult.IP,
},
}
// Add hostnames
if len(shodanResult.Hostnames) > 0 {
finding.Extracted["hostnames"] = strings.Join(shodanResult.Hostnames, ", ")
}
// Add organization info
if shodanResult.Organization != "" {
finding.Extracted["organization"] = shodanResult.Organization
}
// Add ISP info
if shodanResult.ISP != "" {
finding.Extracted["isp"] = shodanResult.ISP
}
// Add ASN
if shodanResult.ASN != "" {
finding.Extracted["asn"] = shodanResult.ASN
}
// Add location
if shodanResult.Country != "" {
location := shodanResult.Country
if shodanResult.City != "" {
location = shodanResult.City + ", " + shodanResult.Country
}
finding.Extracted["location"] = location
}
// Add OS
if shodanResult.OS != "" {
finding.Extracted["os"] = shodanResult.OS
}
// Add open ports
if len(shodanResult.Ports) > 0 {
portStrs := make([]string, len(shodanResult.Ports))
for i, port := range shodanResult.Ports {
portStrs[i] = fmt.Sprintf("%d", port)
}
finding.Extracted["open_ports"] = strings.Join(portStrs, ", ")
finding.Extracted["port_count"] = fmt.Sprintf("%d", len(shodanResult.Ports))
}
// Add vulnerabilities
if len(shodanResult.Vulns) > 0 {
finding.Extracted["vulnerabilities"] = strings.Join(shodanResult.Vulns, ", ")
finding.Extracted["vuln_count"] = fmt.Sprintf("%d", len(shodanResult.Vulns))
}
// Add last update
if shodanResult.LastUpdate != "" {
finding.Extracted["last_update"] = shodanResult.LastUpdate
}
// Add service count
if len(shodanResult.Services) > 0 {
finding.Extracted["service_count"] = fmt.Sprintf("%d", len(shodanResult.Services))
// Add service details
serviceDetails := make([]string, 0, len(shodanResult.Services))
for _, service := range shodanResult.Services {
detail := fmt.Sprintf("%d/%s", service.Port, service.Protocol)
if service.Product != "" {
detail += " " + service.Product
if service.Version != "" {
detail += " " + service.Version
}
}
serviceDetails = append(serviceDetails, detail)
}
finding.Extracted["services"] = strings.Join(serviceDetails, "; ")
}
result.Findings = append(result.Findings, finding)
return result, nil
}
+57
View File
@@ -0,0 +1,57 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package builtin
import (
"context"
"github.com/dropalldatabases/sif/internal/modules"
"github.com/dropalldatabases/sif/internal/scan"
)
type WhoisModule struct{}
func (m *WhoisModule) Info() modules.Info {
return modules.Info{
ID: "whois-lookup",
Name: "WHOIS Domain Information",
Author: "sif",
Severity: "info",
Description: "Performs WHOIS lookup for domain registration information",
Tags: []string{"recon", "whois", "osint"},
}
}
func (m *WhoisModule) Type() modules.ModuleType {
return modules.TypeScript
}
func (m *WhoisModule) Execute(ctx context.Context, target string, opts modules.Options) (*modules.Result, error) {
// Call existing legacy scan.Whois function
scan.Whois(target, opts.LogDir)
// Return that scan was executed, since no data is returned from scan.Whois
result := &modules.Result{
ModuleID: m.Info().ID,
Target: target,
Findings: []modules.Finding{
{
URL: target,
Severity: "info",
Evidence: "WHOIS lookup completed",
},
},
}
return result, nil
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -13,6 +13,7 @@
package scan
import (
"context"
"fmt"
"net/http"
"os"
@@ -20,8 +21,8 @@ import (
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
)
type CloudStorageResult struct {
@@ -30,9 +31,9 @@ type CloudStorageResult struct {
}
func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStorageResult, error) {
fmt.Println(styles.Separator.Render("☁️ Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
fmt.Println(styles.Separator.Render("Starting " + styles.Status.Render("Cloud Storage Misconfiguration Scan") + "..."))
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "Cloud Storage Misconfiguration Scan"); err != nil {
@@ -42,7 +43,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
}
cloudlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "C3 ☁️",
Prefix: "C3",
}).With("url", url)
client := &http.Client{
@@ -54,7 +55,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
var results []CloudStorageResult
for _, bucket := range potentialBuckets {
isPublic, err := checkS3Bucket(bucket, client)
isPublic, err := checkS3Bucket(context.TODO(), bucket, client)
if err != nil {
cloudlog.Errorf("Error checking S3 bucket %s: %v", bucket, err)
continue
@@ -69,7 +70,7 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
if isPublic {
cloudlog.Warnf("Public S3 bucket found: %s", styles.Highlight.Render(bucket))
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("Public S3 bucket found: %s\n", bucket))
_ = logger.Write(sanitizedURL, logdir, fmt.Sprintf("Public S3 bucket found: %s\n", bucket))
}
} else {
cloudlog.Infof("S3 bucket is not public/found: %s", bucket)
@@ -80,27 +81,27 @@ func CloudStorage(url string, timeout time.Duration, logdir string) ([]CloudStor
}
func extractPotentialBuckets(url string) []string {
// This is a simple implementation.
// TODO: add more cases
// TODO: handle non-adjacent label combos and strip the tld
parts := strings.Split(url, ".")
var buckets []string
for i, part := range parts {
buckets = append(buckets, part)
buckets = append(buckets, part+"-s3")
buckets = append(buckets, "s3-"+part)
buckets = append(buckets, part, part+"-s3", "s3-"+part)
if i < len(parts)-1 {
domainExtension := part + "-" + parts[i+1]
buckets = append(buckets, domainExtension)
buckets = append(buckets, parts[i+1]+"-"+part)
buckets = append(buckets, domainExtension, parts[i+1]+"-"+part)
}
}
return buckets
}
func checkS3Bucket(bucket string, client *http.Client) (bool, error) {
func checkS3Bucket(ctx context.Context, bucket string, client *http.Client) (bool, error) {
url := fmt.Sprintf("https://%s.s3.amazonaws.com", bucket)
resp, err := client.Get(url)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return false, err
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
+43 -23
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -13,16 +13,14 @@
package scan
import (
"fmt"
"context"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
type CMSResult struct {
@@ -31,60 +29,77 @@ type CMSResult struct {
}
func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("CMS detection") + "..."))
log := output.Module("CMS")
log.Start()
sanitizedURL := strings.Split(url, "://")[1]
spin := output.NewSpinner("Detecting content management system")
spin.Start()
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "CMS detection"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
cmslog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "CMS 🔍",
}).With("url", url)
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Get(url)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody)
if err != nil {
spin.Stop()
return nil, err
}
resp, err := client.Do(req)
if err != nil {
spin.Stop()
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
if err != nil {
spin.Stop()
return nil, err
}
bodyString := string(body)
// WordPress
if detectWordPress(url, client, bodyString) {
spin.Stop()
result := &CMSResult{Name: "WordPress", Version: "Unknown"}
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
return result, nil
}
// Drupal
if strings.Contains(resp.Header.Get("X-Drupal-Cache"), "HIT") || strings.Contains(bodyString, "Drupal.settings") {
spin.Stop()
result := &CMSResult{Name: "Drupal", Version: "Unknown"}
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
return result, nil
}
// Joomla
if strings.Contains(bodyString, "joomla") || strings.Contains(bodyString, "/media/system/js/core.js") {
spin.Stop()
result := &CMSResult{Name: "Joomla", Version: "Unknown"}
cmslog.Infof("Detected CMS: %s", styles.Highlight.Render(result.Name))
log.Success("Detected CMS: %s", output.Highlight.Render(result.Name))
log.Complete(1, "detected")
return result, nil
}
cmslog.Info("No CMS detected")
return nil, nil
spin.Stop()
log.Info("No CMS detected")
log.Complete(0, "detected")
return nil, nil //nolint:nilnil // no CMS found is not an error
}
func detectWordPress(url string, client *http.Client, bodyString string) bool {
@@ -110,10 +125,15 @@ func detectWordPress(url string, client *http.Client, bodyString string) bool {
}
for _, file := range wpFiles {
resp, err := client.Get(url + file)
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url+file, http.NoBody)
if err != nil {
continue
}
resp, err := client.Do(req)
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound {
found := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound
resp.Body.Close()
if found {
return true
}
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,24 +14,25 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
// directoryURL is a var so integration tests can repoint it at a fixture.
var directoryURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dirlist/"
const (
directoryURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dirlist/"
smallFile = "directory-list-2.3-small.txt"
mediumFile = "directory-list-2.3-medium.txt"
bigFile = "directory-list-2.3-big.txt"
smallFile = "directory-list-2.3-small.txt"
mediumFile = "directory-list-2.3-medium.txt"
bigFile = "directory-list-2.3-big.txt"
)
type DirectoryResult struct {
@@ -40,36 +41,20 @@ type DirectoryResult struct {
}
// Dirlist performs directory fuzzing on the target URL.
//
// Parameters:
// - size: determines the size of the directory list to use ("small", "medium", or "large")
// - url: the target URL to scan
// - timeout: maximum duration for each request
// - threads: number of concurrent threads to use
// - logdir: directory to store log files (empty string for no logging)
//
// Returns:
// - []DirectoryResult: a slice of discovered directories and their status codes
// - error: any error encountered during the scan
func Dirlist(size string, url string, timeout time.Duration, threads int, logdir string) ([]DirectoryResult, error) {
log := output.Module("DIRLIST")
log.Start()
fmt.Println(styles.Separator.Render("📂 Starting " + styles.Status.Render("directory fuzzing") + "..."))
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, size+" directory fuzzing"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
dirlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Dirlist 📂",
}).With("url", url)
var list string
switch size {
case "small":
list = directoryURL + smallFile
@@ -79,14 +64,18 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
list = directoryURL + bigFile
}
dirlog.Infof("Starting %s directory listing", size)
resp, err := http.Get(list)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, list, http.NoBody)
if err != nil {
log.Errorf("Error downloading directory list: %s", err)
log.Error("Error creating directory list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Error downloading directory list: %s", err)
return nil, err
}
defer resp.Body.Close()
var directories []string
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -98,10 +87,13 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
Timeout: timeout,
}
progress := output.NewProgress(len(directories), "fuzzing")
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
results := []DirectoryResult{}
results := make([]DirectoryResult, 0, 64)
for thread := 0; thread < threads; thread++ {
go func(thread int) {
defer wg.Done()
@@ -111,30 +103,45 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
continue
}
log.Debugf("%s", directory)
resp, err := client.Get(url + "/" + directory)
progress.Increment(directory)
charmlog.Debugf("%s", directory)
dirReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+directory, http.NoBody)
if err != nil {
log.Debugf("Error %s: %s", directory, err)
return
charmlog.Debugf("Error creating request for %s: %s", directory, err)
continue
}
resp, err := client.Do(dirReq)
if err != nil {
charmlog.Debugf("Error %s: %s", directory, err)
continue
}
if resp.StatusCode != 404 && resp.StatusCode != 403 {
// log url, directory, and status code
dirlog.Infof("%s [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(directory))
progress.Pause()
log.Success("found: %s [%s]", output.Highlight.Render(directory), output.Status.Render(strconv.Itoa(resp.StatusCode)))
progress.Resume()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s [%s]\n", strconv.Itoa(resp.StatusCode), directory))
_ = logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s [%s]\n", strconv.Itoa(resp.StatusCode), directory))
}
result := DirectoryResult{
Url: resp.Request.URL.String(),
StatusCode: resp.StatusCode,
}
mu.Lock()
results = append(results, result)
mu.Unlock()
}
resp.Body.Close()
}
}(thread)
}
wg.Wait()
progress.Done()
log.Complete(len(results), "found")
return results, nil
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,47 +14,32 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
// dnsURL is a var so integration tests can repoint it at a fixture.
var dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
const (
dnsURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/dnslist/"
dnsSmallFile = "subdomains-100.txt"
dnsMediumFile = "subdomains-1000.txt"
dnsBigFile = "subdomains-10000.txt"
)
// Dnslist performs DNS subdomain enumeration on the target domain.
//
// Parameters:
// - size: determines the size of the subdomain list to use ("small", "medium", or "large")
// - url: the target URL to scan
// - timeout: maximum duration for each DNS lookup
// - threads: number of concurrent threads to use
// - logdir: directory to store log files (empty string for no logging)
//
// Returns:
// - []string: a slice of discovered subdomains
// - error: any error encountered during the enumeration
func Dnslist(size string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
fmt.Println(styles.Separator.Render("📡 Starting " + styles.Status.Render("DNS fuzzing") + "..."))
dnslog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Dnslist 📡",
}).With("url", url)
log := output.Module("DNS")
log.Start()
var list string
switch size {
case "small":
list = dnsURL + dnsSmallFile
@@ -64,14 +49,18 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
list = dnsURL + dnsBigFile
}
dnslog.Infof("Starting %s DNS listing", size)
resp, err := http.Get(list)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, list, http.NoBody)
if err != nil {
log.Errorf("Error downloading DNS list: %s", err)
log.Error("Error creating request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Error downloading DNS list: %s", err)
return nil, err
}
defer resp.Body.Close()
var dns []string
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
@@ -79,11 +68,11 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
dns = append(dns, scanner.Text())
}
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, size+" subdomain fuzzing"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
@@ -92,10 +81,13 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
Timeout: timeout,
}
progress := output.NewProgress(len(dns), "enumerating")
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
urls := []string{}
urls := make([]string, 0, 64)
for thread := 0; thread < threads; thread++ {
go func(thread int) {
defer wg.Done()
@@ -105,39 +97,64 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
continue
}
log.Debugf("Looking up: %s", domain)
resp, err := client.Get("http://" + domain + "." + sanitizedURL)
progress.Increment(domain)
charmlog.Debugf("Looking up: %s", domain)
// Check HTTP
httpReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "http://"+domain+"."+sanitizedURL, http.NoBody)
if err != nil {
log.Debugf("Error %s: %s", domain, err)
charmlog.Debugf("Error %s: %s", domain, err)
continue
}
resp, err := client.Do(httpReq)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
dnslog.Infof("%s %s.%s", styles.Status.Render("[http]"), styles.Highlight.Render(domain), sanitizedURL)
mu.Unlock()
resp.Body.Close()
progress.Pause()
log.Success("found: %s.%s [http]", output.Highlight.Render(domain), sanitizedURL)
progress.Resume()
if logdir != "" {
f, err := os.OpenFile(logdir+"/"+sanitizedURL+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Errorf("Error creating log file: %s", err)
return
}
defer f.Close()
f.WriteString(fmt.Sprintf("[http] %s.%s\n", domain, sanitizedURL))
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[http] %s.%s\n", domain, sanitizedURL))
}
}
resp, err = client.Get("https://" + domain + "." + sanitizedURL)
// Check HTTPS
httpsReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "https://"+domain+"."+sanitizedURL, http.NoBody)
if err != nil {
log.Debugf("Error %s: %s", domain, err)
charmlog.Debugf("Error %s: %s", domain, err)
continue
}
resp, err = client.Do(httpsReq)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
dnslog.Infof("%s %s.%s", styles.Status.Render("[https]"), styles.Highlight.Render(domain), sanitizedURL)
mu.Unlock()
resp.Body.Close()
progress.Pause()
log.Success("found: %s.%s [https]", output.Highlight.Render(domain), sanitizedURL)
progress.Resume()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("[https] %s.%s\n", domain, sanitizedURL))
_ = logger.Write(sanitizedURL, logdir, fmt.Sprintf("[https] %s.%s\n", domain, sanitizedURL))
}
}
}
}(thread)
}
wg.Wait()
progress.Done()
log.Complete(len(urls), "found")
return urls, nil
}
+31 -20
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -17,17 +17,16 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
googlesearch "github.com/rocketlaunchr/google-search"
)
@@ -55,27 +54,32 @@ type DorkResult struct {
// - []DorkResult: A slice of results from the dorking operation
// - error: Any error encountered during the dorking process
func Dork(url string, timeout time.Duration, threads int, logdir string) ([]DorkResult, error) {
output.ScanStart("URL dorking")
fmt.Println(styles.Separator.Render("🤓 Starting " + styles.Status.Render("URL Dorking") + "..."))
spin := output.NewSpinner("Running Google dorks")
spin.Start()
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "URL dorking"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
output.Error("Error creating log file: %v", err)
return nil, err
}
}
dorklog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Dorking 🤓",
}).With("url", url)
dorklog.Infof("Starting URL dorking...")
resp, err := http.Get(dorkURL + dorkFile)
ctx := context.TODO()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, dorkURL+dorkFile, http.NoBody)
if err != nil {
log.Errorf("Error downloading dork list: %s", err)
spin.Stop()
output.Error("Error creating dork list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
output.Error("Error downloading dork list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -88,6 +92,7 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
// util.InitProgressBar()
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
dorkResults := []DorkResult{}
@@ -101,15 +106,17 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
continue
}
results, err := googlesearch.Search(nil, fmt.Sprintf("%s %s", dork, sanitizedURL))
results, err := googlesearch.Search(context.TODO(), fmt.Sprintf("%s %s", dork, sanitizedURL))
if err != nil {
dorklog.Debugf("error searching for dork %s: %v", dork, err)
log.Debugf("error searching for dork %s: %v", dork, err)
continue
}
if len(results) > 0 {
dorklog.Infof("%s dork results found for dork [%s]", styles.Status.Render(strconv.Itoa(len(results))), styles.Highlight.Render(dork))
spin.Stop()
output.Success("%s dork results found for dork %s", output.Status.Render(strconv.Itoa(len(results))), output.Highlight.Render(dork))
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s dork results found for dork [%s]\n", strconv.Itoa(len(results)), dork))
_ = logger.Write(sanitizedURL, logdir, strconv.Itoa(len(results))+" dork results found for dork ["+dork+"]\n")
}
result := DorkResult{
@@ -117,12 +124,16 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
Count: len(results),
}
mu.Lock()
dorkResults = append(dorkResults, result)
mu.Unlock()
}
}
}(thread)
}
wg.Wait()
spin.Stop()
output.ScanComplete("URL dorking", len(dorkResults), "found")
return dorkResults, nil
}
+184
View File
@@ -0,0 +1,184 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
// CVEEntry represents a known vulnerability for a framework version
type CVEEntry struct {
CVE string
AffectedVersions []string // versions affected (use semver ranges in future)
FixedVersion string
Severity string // critical, high, medium, low
Description string
Recommendations []string
}
// knownCVEs contains known vulnerabilities for popular frameworks.
// This database can be extended or loaded from an external source.
var knownCVEs = map[string][]CVEEntry{
"Laravel": {
{
CVE: "CVE-2021-3129",
AffectedVersions: []string{"8.0.0", "8.0.1", "8.0.2", "8.1.0", "8.2.0", "8.3.0", "8.4.0", "8.4.1"},
FixedVersion: "8.4.2",
Severity: "critical",
Description: "Ignition debug mode RCE vulnerability",
Recommendations: []string{"Update to Laravel 8.4.2 or later", "Disable debug mode in production"},
},
{
CVE: "CVE-2021-21263",
AffectedVersions: []string{"8.0.0", "8.1.0", "8.2.0", "8.3.0", "8.4.0"},
FixedVersion: "8.5.0",
Severity: "high",
Description: "SQL injection via request validation",
Recommendations: []string{"Update to Laravel 8.5.0 or later", "Use parameterized queries"},
},
},
"Django": {
{
CVE: "CVE-2023-36053",
AffectedVersions: []string{"3.2.0", "3.2.1", "3.2.2", "4.0.0", "4.1.0"},
FixedVersion: "4.2.3",
Severity: "high",
Description: "Potential ReDoS in EmailValidator and URLValidator",
Recommendations: []string{"Update to Django 4.2.3 or later"},
},
{
CVE: "CVE-2023-31047",
AffectedVersions: []string{"3.2.0", "4.0.0", "4.1.0"},
FixedVersion: "4.1.9",
Severity: "medium",
Description: "File upload validation bypass",
Recommendations: []string{"Update to Django 4.1.9 or later", "Implement additional file validation"},
},
},
"WordPress": {
{
CVE: "CVE-2023-2745",
AffectedVersions: []string{"5.0", "5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8", "5.9", "6.0", "6.1"},
FixedVersion: "6.2",
Severity: "medium",
Description: "Directory traversal vulnerability",
Recommendations: []string{"Update to WordPress 6.2 or later"},
},
},
"Drupal": {
{
CVE: "CVE-2023-44487",
AffectedVersions: []string{"9.0", "9.1", "9.2", "9.3", "9.4", "9.5", "10.0"},
FixedVersion: "10.1.4",
Severity: "high",
Description: "HTTP/2 rapid reset attack (DoS)",
Recommendations: []string{"Update to Drupal 10.1.4 or later", "Configure HTTP/2 rate limiting"},
},
},
"Next.js": {
{
CVE: "CVE-2023-46298",
AffectedVersions: []string{"13.0.0", "13.1.0", "13.2.0", "13.3.0", "13.4.0"},
FixedVersion: "13.5.0",
Severity: "medium",
Description: "Server-side request forgery vulnerability",
Recommendations: []string{"Update to Next.js 13.5.0 or later"},
},
},
"Angular": {
{
CVE: "CVE-2023-26117",
AffectedVersions: []string{"14.0.0", "14.1.0", "14.2.0", "15.0.0"},
FixedVersion: "15.2.0",
Severity: "medium",
Description: "Regular expression denial of service",
Recommendations: []string{"Update to Angular 15.2.0 or later"},
},
},
"Vue.js": {
{
CVE: "CVE-2024-5987",
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
FixedVersion: "2.7.16",
Severity: "medium",
Description: "XSS vulnerability in certain configurations",
Recommendations: []string{"Update to Vue.js 2.7.16 or 3.x"},
},
},
"Express.js": {
{
CVE: "CVE-2024-29041",
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0", "4.3.0", "4.4.0"},
FixedVersion: "4.19.2",
Severity: "medium",
Description: "Open redirect vulnerability",
Recommendations: []string{"Update to Express.js 4.19.2 or later"},
},
},
"Ruby on Rails": {
{
CVE: "CVE-2023-22795",
AffectedVersions: []string{"6.0.0", "6.1.0", "7.0.0"},
FixedVersion: "7.0.4.1",
Severity: "high",
Description: "ReDoS vulnerability in Action Dispatch",
Recommendations: []string{"Update to Rails 7.0.4.1 or later"},
},
},
"Spring": {
{
CVE: "CVE-2022-22965",
AffectedVersions: []string{"5.0.0", "5.1.0", "5.2.0", "5.3.0"},
FixedVersion: "5.3.18",
Severity: "critical",
Description: "Spring4Shell RCE vulnerability",
Recommendations: []string{"Update to Spring 5.3.18 or later", "Disable class binding on user input"},
},
},
"Spring Boot": {
{
CVE: "CVE-2022-22963",
AffectedVersions: []string{"2.0.0", "2.1.0", "2.2.0", "2.3.0", "2.4.0", "2.5.0", "2.6.0"},
FixedVersion: "2.6.6",
Severity: "critical",
Description: "RCE via Spring Cloud Function",
Recommendations: []string{"Update to Spring Boot 2.6.6 or later"},
},
},
"ASP.NET": {
{
CVE: "CVE-2023-36899",
AffectedVersions: []string{"4.0", "4.5", "4.6", "4.7", "4.8"},
FixedVersion: "latest security patches",
Severity: "high",
Description: "Elevation of privilege vulnerability",
Recommendations: []string{"Apply latest security patches", "Ensure proper request validation"},
},
},
"Joomla": {
{
CVE: "CVE-2023-23752",
AffectedVersions: []string{"4.0.0", "4.1.0", "4.2.0"},
FixedVersion: "4.2.8",
Severity: "critical",
Description: "Improper access check allowing unauthorized access to webservice endpoints",
Recommendations: []string{"Update to Joomla 4.2.8 or later"},
},
},
"Magento": {
{
CVE: "CVE-2022-24086",
AffectedVersions: []string{"2.3.0", "2.3.1", "2.3.2", "2.4.0", "2.4.1", "2.4.2"},
FixedVersion: "2.4.3-p1",
Severity: "critical",
Description: "Improper input validation leading to arbitrary code execution",
Recommendations: []string{"Update to Magento 2.4.3-p1 or later"},
},
},
}
@@ -0,0 +1,36 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import "testing"
func TestVersionAffected(t *testing.T) {
tests := []struct {
version string
affected string
want bool
}{
{"4.2", "4.2", true},
{"4.2.1", "4.2", true},
{"4.2.13", "4.2", true},
{"4.20", "4.2", false}, // the boundary bug: 4.20 is not a 4.2.x release
{"4.20.0", "4.2", false},
{"5.0", "4.2", false},
}
for _, tt := range tests {
if got := versionAffected(tt.version, tt.affected); got != tt.want {
t.Errorf("versionAffected(%q, %q) = %v, want %v", tt.version, tt.affected, got, tt.want)
}
}
}
+196
View File
@@ -0,0 +1,196 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
// detectionThreshold is the minimum confidence for a detection to be reported.
const detectionThreshold = 0.5
// maxBodySize limits response body to prevent memory exhaustion.
const maxBodySize = 5 * 1024 * 1024
// detectionResult holds the result from a single detector.
type detectionResult struct {
name string
confidence float32
version string
}
// DetectFramework runs all registered detectors against the target URL.
func DetectFramework(url string, timeout time.Duration, logdir string) (*FrameworkResult, error) {
log := output.Module("FRAMEWORK")
log.Start()
spin := output.NewSpinner("Detecting frameworks")
spin.Start()
client := &http.Client{Timeout: timeout}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
spin.Stop()
return nil, err
}
resp, err := client.Do(req) //nolint:bodyclose // closed via defer below
if err != nil {
spin.Stop()
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
if err != nil {
spin.Stop()
return nil, err
}
bodyStr := string(body)
// Get all registered detectors
detectors := GetDetectors()
if len(detectors) == 0 {
spin.Stop()
log.Warn("No framework detectors registered")
return nil, nil //nolint:nilnil // no detectors registered is not an error
}
// Run all detectors concurrently
results := make(chan detectionResult, len(detectors))
var wg sync.WaitGroup
for _, detector := range detectors {
wg.Add(1)
go func(d Detector) {
defer wg.Done()
confidence, version := d.Detect(bodyStr, resp.Header)
results <- detectionResult{
name: d.Name(),
confidence: confidence,
version: version,
}
}(detector)
}
// Close results channel when all goroutines complete
go func() {
wg.Wait()
close(results)
}()
// Find the best match
// results arrive in goroutine-completion order; tie-break on name so the
// winner is deterministic when two detectors land on the same confidence.
var best detectionResult
for r := range results {
if r.confidence > best.confidence || (r.confidence == best.confidence && r.name < best.name) {
best = r
}
}
spin.Stop()
if best.confidence <= detectionThreshold {
log.Info("No framework detected with sufficient confidence")
log.Complete(0, "detected")
return nil, nil //nolint:nilnil // no framework detected is not an error
}
// Get version match details
versionMatch := ExtractVersionOptimized(bodyStr, best.name)
cves, suggestions := getVulnerabilities(best.name, best.version)
result := NewFrameworkResult(best.name, best.version, best.confidence, versionMatch.Confidence)
result.WithVulnerabilities(cves, suggestions)
// Log results
if logdir != "" {
logEntry := fmt.Sprintf("Detected framework: %s (version: %s, confidence: %.2f, version_confidence: %.2f)\n",
best.name, best.version, best.confidence, versionMatch.Confidence)
if len(cves) > 0 {
logEntry += fmt.Sprintf(" Risk Level: %s\n", result.RiskLevel)
logEntry += fmt.Sprintf(" CVEs: %v\n", cves)
logEntry += fmt.Sprintf(" Recommendations: %v\n", suggestions)
}
_ = logger.Write(url, logdir, logEntry)
}
log.Success("Detected %s framework (version: %s, confidence: %.2f)",
output.Highlight.Render(best.name), best.version, best.confidence)
if versionMatch.Confidence > 0 {
charmlog.Debugf("Version detected from: %s (confidence: %.2f)",
versionMatch.Source, versionMatch.Confidence)
}
if len(cves) > 0 {
log.Warn("Risk level: %s", output.SeverityHigh.Render(result.RiskLevel))
for _, cve := range cves {
log.Warn("Found potential vulnerability: %s", output.Highlight.Render(cve))
}
for _, suggestion := range suggestions {
log.Info("Recommendation: %s", suggestion)
}
}
log.Complete(1, "detected")
return result, nil
}
// getVulnerabilities returns CVEs and recommendations for a framework version.
func getVulnerabilities(framework, version string) ([]string, []string) {
entries, exists := knownCVEs[framework]
if !exists {
return nil, nil
}
var cves []string
var recommendations []string
seenRecs := make(map[string]bool)
for _, entry := range entries {
for _, affectedVer := range entry.AffectedVersions {
if versionAffected(version, affectedVer) {
cves = append(cves, fmt.Sprintf("%s (%s)", entry.CVE, entry.Severity))
for _, rec := range entry.Recommendations {
if !seenRecs[rec] {
recommendations = append(recommendations, rec)
seenRecs[rec] = true
}
}
break
}
}
}
return cves, recommendations
}
// versionAffected reports whether version falls under an affected-version
// entry. the entry is a version prefix, matched only on dotted boundaries, so
// "4.2" covers 4.2 and 4.2.1 but not 4.20.
func versionAffected(version, affected string) bool {
return version == affected || strings.HasPrefix(version, affected+".")
}
@@ -4,59 +4,25 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
*/
package frameworks
package frameworks_test
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/dropalldatabases/sif/internal/scan/frameworks"
// Import detectors to register them via init()
_ "github.com/dropalldatabases/sif/internal/scan/frameworks/detectors"
)
func TestContainsHeader_HeaderName(t *testing.T) {
headers := http.Header{
"X-Powered-By": []string{"Express"},
"Content-Type": []string{"text/html"},
}
if !containsHeader(headers, "x-powered-by") {
t.Error("expected to find x-powered-by in header names")
}
if !containsHeader(headers, "X-POWERED-BY") {
t.Error("expected case-insensitive match for header names")
}
}
func TestContainsHeader_HeaderValue(t *testing.T) {
headers := http.Header{
"X-Powered-By": []string{"Express"},
"Set-Cookie": []string{"laravel_session=abc123"},
}
if !containsHeader(headers, "express") {
t.Error("expected to find 'express' in header values")
}
if !containsHeader(headers, "laravel_session") {
t.Error("expected to find 'laravel_session' in header values")
}
}
func TestContainsHeader_NotFound(t *testing.T) {
headers := http.Header{
"Content-Type": []string{"text/html"},
}
if containsHeader(headers, "django") {
t.Error("expected not to find 'django' in headers")
}
}
func TestExtractVersion_Laravel(t *testing.T) {
tests := []struct {
body string
@@ -69,9 +35,9 @@ func TestExtractVersion_Laravel(t *testing.T) {
}
for _, tt := range tests {
result := extractVersion(tt.body, "Laravel")
result := frameworks.ExtractVersionOptimized(tt.body, "Laravel").Version
if result != tt.expected {
t.Errorf("extractVersion(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected)
t.Errorf("ExtractVersionOptimized(%q, 'Laravel') = %q, want %q", tt.body, result, tt.expected)
}
}
}
@@ -87,9 +53,9 @@ func TestExtractVersion_Django(t *testing.T) {
}
for _, tt := range tests {
result := extractVersion(tt.body, "Django")
result := frameworks.ExtractVersionOptimized(tt.body, "Django").Version
if result != tt.expected {
t.Errorf("extractVersion(%q, 'Django') = %q, want %q", tt.body, result, tt.expected)
t.Errorf("ExtractVersionOptimized(%q, 'Django') = %q, want %q", tt.body, result, tt.expected)
}
}
}
@@ -105,9 +71,9 @@ func TestExtractVersion_NextJS(t *testing.T) {
}
for _, tt := range tests {
result := extractVersion(tt.body, "Next.js")
result := frameworks.ExtractVersionOptimized(tt.body, "Next.js").Version
if result != tt.expected {
t.Errorf("extractVersion(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected)
t.Errorf("ExtractVersionOptimized(%q, 'Next.js') = %q, want %q", tt.body, result, tt.expected)
}
}
}
@@ -129,7 +95,7 @@ func TestDetectFramework_NextJS(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -152,7 +118,7 @@ func TestDetectFramework_Express(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -180,7 +146,7 @@ func TestDetectFramework_WordPress(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -208,7 +174,7 @@ func TestDetectFramework_ASPNET(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -227,7 +193,7 @@ func TestDetectFramework_NoMatch(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -237,36 +203,9 @@ func TestDetectFramework_NoMatch(t *testing.T) {
}
}
func TestGetVulnerabilities_Laravel(t *testing.T) {
cves, suggestions := getVulnerabilities("Laravel", "8.0.0")
if len(cves) == 0 {
t.Error("expected CVEs for Laravel 8.0.0")
}
if len(suggestions) == 0 {
t.Error("expected suggestions for Laravel 8.0.0")
}
}
func TestGetVulnerabilities_NoMatch(t *testing.T) {
cves, suggestions := getVulnerabilities("Unknown", "1.0.0")
if len(cves) != 0 {
t.Error("expected no CVEs for unknown framework")
}
if len(suggestions) != 0 {
t.Error("expected no suggestions for unknown framework")
}
}
func TestFrameworkResult_Fields(t *testing.T) {
result := FrameworkResult{
Name: "Laravel",
Version: "9.0.0",
Confidence: 0.85,
VersionConfidence: 0.9,
CVEs: []string{"CVE-2021-3129"},
Suggestions: []string{"Update to latest version"},
RiskLevel: "critical",
}
result := frameworks.NewFrameworkResult("Laravel", "9.0.0", 0.85, 0.9)
result.WithVulnerabilities([]string{"CVE-2021-3129"}, []string{"Update to latest version"})
if result.Name != "Laravel" {
t.Errorf("expected Name 'Laravel', got '%s'", result.Name)
@@ -286,9 +225,6 @@ func TestFrameworkResult_Fields(t *testing.T) {
if len(result.Suggestions) != 1 {
t.Errorf("expected 1 suggestion, got %d", len(result.Suggestions))
}
if result.RiskLevel != "critical" {
t.Errorf("expected RiskLevel 'critical', got '%s'", result.RiskLevel)
}
}
func TestExtractVersionWithConfidence(t *testing.T) {
@@ -308,18 +244,18 @@ func TestExtractVersionWithConfidence(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractVersionWithConfidence(tt.body, tt.framework)
result := frameworks.ExtractVersionOptimized(tt.body, tt.framework)
if result.Version != tt.wantVer {
t.Errorf("extractVersionWithConfidence() version = %q, want %q", result.Version, tt.wantVer)
t.Errorf("ExtractVersionOptimized() version = %q, want %q", result.Version, tt.wantVer)
}
if result.Confidence < tt.minConf {
t.Errorf("extractVersionWithConfidence() confidence = %f, want >= %f", result.Confidence, tt.minConf)
t.Errorf("ExtractVersionOptimized() confidence = %f, want >= %f", result.Confidence, tt.minConf)
}
})
}
}
func TestGetRiskLevel(t *testing.T) {
func TestDetermineRiskLevel(t *testing.T) {
tests := []struct {
name string
cves []string
@@ -334,44 +270,16 @@ func TestGetRiskLevel(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getRiskLevel(tt.cves)
if result != tt.expected {
t.Errorf("getRiskLevel() = %q, want %q", result, tt.expected)
// Test via WithVulnerabilities which uses determineRiskLevel internally
result := frameworks.NewFrameworkResult("Test", "1.0", 0.5, 0.5)
result.WithVulnerabilities(tt.cves, nil)
if result.RiskLevel != tt.expected {
t.Errorf("determineRiskLevel() = %q, want %q", result.RiskLevel, tt.expected)
}
})
}
}
func TestGetVulnerabilities_Django(t *testing.T) {
cves, suggestions := getVulnerabilities("Django", "3.2.0")
if len(cves) == 0 {
t.Error("expected CVEs for Django 3.2.0")
}
if len(suggestions) == 0 {
t.Error("expected suggestions for Django 3.2.0")
}
}
func TestGetVulnerabilities_Spring(t *testing.T) {
cves, suggestions := getVulnerabilities("Spring", "5.3.0")
if len(cves) == 0 {
t.Error("expected CVEs for Spring 5.3.0 (Spring4Shell)")
}
found := false
for _, cve := range cves {
if cve == "CVE-2022-22965 (critical)" {
found = true
break
}
}
if !found {
t.Error("expected Spring4Shell CVE-2022-22965")
}
if len(suggestions) == 0 {
t.Error("expected suggestions for Spring 5.3.0")
}
}
func TestDetectFramework_Vue(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
@@ -390,7 +298,7 @@ func TestDetectFramework_Vue(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -417,7 +325,7 @@ func TestDetectFramework_Angular(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -445,7 +353,7 @@ func TestDetectFramework_React(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -474,7 +382,7 @@ func TestDetectFramework_Svelte(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -504,7 +412,7 @@ func TestDetectFramework_Joomla(t *testing.T) {
}))
defer server.Close()
result, err := DetectFramework(server.URL, 5*time.Second, "")
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@@ -516,8 +424,67 @@ func TestDetectFramework_Joomla(t *testing.T) {
}
}
func TestDetectFramework_Astro(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`
<!DOCTYPE html>
<html data-astro-transition="forward">
<head>
<meta name="generator" content="Astro v5.16.6">
<link rel="stylesheet" href="/_astro/index.abc123.css">
</head>
<body>
<astro-island data-astro-cid-xyz789 data-astro-source-file="src/components/Counter.astro">
<div>Content</div>
</astro-island>
<nav>
<a href="/about" data-astro-history="push">About</a>
<a href="/external" data-astro-reload>External</a>
</nav>
<script src="/_astro/hoisted.def456.js"></script>
</body>
</html>
`))
}))
defer server.Close()
result, err := frameworks.DetectFramework(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result == nil {
t.Fatal("expected result, got nil")
}
if result.Name != "Astro" {
t.Errorf("expected framework 'Astro', got '%s'", result.Name)
}
}
func TestExtractVersion_Astro(t *testing.T) {
tests := []struct {
body string
expected string
}{
{`<meta name="generator" content="Astro v4.2.0">`, "4.2.0"},
{`<meta name="generator" content="Astro 3.5.1">`, "3.5.1"},
{"Astro 4.0.0", "4.0.0"},
{"Astro/3.2.1", "3.2.1"},
{`"astro": "^4.1.0"`, "4.1.0"},
{`"astro": "~3.0.5"`, "3.0.5"},
{"no version", "unknown"},
}
for _, tt := range tests {
result := frameworks.ExtractVersionOptimized(tt.body, "Astro").Version
if result != tt.expected {
t.Errorf("ExtractVersionOptimized(%q, 'Astro') = %q, want %q", tt.body, result, tt.expected)
}
}
}
func TestCVEEntry_Fields(t *testing.T) {
entry := CVEEntry{
entry := frameworks.CVEEntry{
CVE: "CVE-2021-3129",
AffectedVersions: []string{"8.0.0", "8.0.1"},
FixedVersion: "8.4.2",
@@ -536,3 +503,18 @@ func TestCVEEntry_Fields(t *testing.T) {
t.Errorf("expected Severity 'critical', got '%s'", entry.Severity)
}
}
func TestDetectorRegistry(t *testing.T) {
detectors := frameworks.GetDetectors()
if len(detectors) == 0 {
t.Fatal("expected registered detectors, got none")
}
// Check that some expected detectors are registered
expectedDetectors := []string{"Laravel", "Django", "React", "Vue.js", "Angular", "Next.js", "WordPress", "Astro"}
for _, name := range expectedDetectors {
if _, ok := frameworks.GetDetector(name); !ok {
t.Errorf("expected detector %q to be registered", name)
}
}
}
+145
View File
@@ -0,0 +1,145 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package frameworks
import (
"net/http"
"strings"
"sync"
)
// Signature represents a pattern to match for framework detection.
type Signature struct {
Pattern string
Weight float32
HeaderOnly bool
}
// Detector is the interface for framework detection plugins.
type Detector interface {
// Name returns the unique framework name.
Name() string
// Signatures returns patterns to search for this framework.
Signatures() []Signature
// Detect performs detection and returns confidence (0.0-1.0) and version.
// The version can be empty if not detectable.
Detect(body string, headers http.Header) (confidence float32, version string)
}
// registry holds all registered detectors.
var (
registryMu sync.RWMutex
registry = make(map[string]Detector)
)
// Register adds a detector to the registry. Should be called from init().
func Register(d Detector) {
registryMu.Lock()
defer registryMu.Unlock()
registry[d.Name()] = d
}
// GetDetectors returns all registered detectors.
func GetDetectors() map[string]Detector {
registryMu.RLock()
defer registryMu.RUnlock()
// Return a copy to prevent mutation
result := make(map[string]Detector, len(registry))
for k, v := range registry {
result[k] = v
}
return result
}
// GetDetector returns a specific detector by name.
func GetDetector(name string) (Detector, bool) {
registryMu.RLock()
defer registryMu.RUnlock()
d, ok := registry[name]
return d, ok
}
// BaseDetector provides common functionality for detector implementations.
type BaseDetector struct {
name string
signatures []Signature
}
// NewBaseDetector creates a new base detector.
func NewBaseDetector(name string, signatures []Signature) BaseDetector {
return BaseDetector{name: name, signatures: signatures}
}
// Name returns the framework name.
func (b BaseDetector) Name() string {
return b.name
}
// Signatures returns the detection signatures.
func (b BaseDetector) Signatures() []Signature {
return b.signatures
}
// MatchSignatures checks body and headers against signatures and returns a weighted score.
func (b BaseDetector) MatchSignatures(body string, headers http.Header) float32 {
var weightedScore float32
var totalWeight float32
for _, sig := range b.signatures {
totalWeight += sig.Weight
if sig.HeaderOnly {
if containsHeader(headers, sig.Pattern) {
weightedScore += sig.Weight
}
} else if strings.Contains(body, sig.Pattern) {
weightedScore += sig.Weight
}
}
if totalWeight == 0 {
return 0
}
return weightedScore / totalWeight
}
// containsHeader checks if a signature pattern exists in headers.
func containsHeader(headers http.Header, signature string) bool {
sigLower := strings.ToLower(signature)
// Check header names
for name := range headers {
if strings.Contains(strings.ToLower(name), sigLower) {
return true
}
}
// Check header values
for _, values := range headers {
for _, value := range values {
if strings.Contains(strings.ToLower(value), sigLower) {
return true
}
}
}
return false
}
@@ -0,0 +1,490 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package detectors
import (
"math"
"net/http"
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
)
func init() {
// Register all backend detectors
fw.Register(&laravelDetector{})
fw.Register(&djangoDetector{})
fw.Register(&railsDetector{})
fw.Register(&expressDetector{})
fw.Register(&aspnetDetector{})
fw.Register(&aspnetCoreDetector{})
fw.Register(&springDetector{})
fw.Register(&springBootDetector{})
fw.Register(&flaskDetector{})
fw.Register(&symfonyDetector{})
fw.Register(&fastapiDetector{})
fw.Register(&ginDetector{})
fw.Register(&phoenixDetector{})
fw.Register(&strapiDetector{})
fw.Register(&adonisDetector{})
fw.Register(&cakephpDetector{})
fw.Register(&codeigniterDetector{})
}
// sigmoidConfidence maps the matched-weight fraction to a 0-1 confidence,
// centered at 0.3 so a single weak signature match no longer clears the 0.5
// detection threshold (it used to: sigmoid(0) was 0.5, so any match "detected").
func sigmoidConfidence(score float32) float32 {
return float32(1.0 / (1.0 + math.Exp(-(float64(score)-0.3)*10.0)))
}
// laravelDetector detects Laravel framework.
type laravelDetector struct {
fw.BaseDetector
}
func (d *laravelDetector) Name() string { return "Laravel" }
func (d *laravelDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "laravel_session", Weight: 0.4, HeaderOnly: true},
{Pattern: "XSRF-TOKEN", Weight: 0.3, HeaderOnly: true},
{Pattern: `<meta name="csrf-token"`, Weight: 0.3},
}
}
func (d *laravelDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// djangoDetector detects Django framework.
type djangoDetector struct{}
func (d *djangoDetector) Name() string { return "Django" }
func (d *djangoDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "csrfmiddlewaretoken", Weight: 0.4, HeaderOnly: true},
{Pattern: "csrftoken", Weight: 0.3, HeaderOnly: true},
{Pattern: "django.contrib", Weight: 0.3},
{Pattern: "django.core", Weight: 0.3},
{Pattern: "__admin_media_prefix__", Weight: 0.3},
}
}
func (d *djangoDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// railsDetector detects Ruby on Rails framework.
type railsDetector struct{}
func (d *railsDetector) Name() string { return "Ruby on Rails" }
func (d *railsDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "csrf-param", Weight: 0.4, HeaderOnly: true},
{Pattern: "csrf-token", Weight: 0.3, HeaderOnly: true},
{Pattern: "_rails_session", Weight: 0.3, HeaderOnly: true},
{Pattern: "ruby-on-rails", Weight: 0.3},
{Pattern: "rails-env", Weight: 0.3},
{Pattern: "data-turbo", Weight: 0.2},
}
}
func (d *railsDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// expressDetector detects Express.js framework.
type expressDetector struct{}
func (d *expressDetector) Name() string { return "Express.js" }
func (d *expressDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "Express", Weight: 0.5, HeaderOnly: true},
{Pattern: "connect.sid", Weight: 0.3, HeaderOnly: true},
}
}
func (d *expressDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// aspnetDetector detects ASP.NET framework.
type aspnetDetector struct{}
func (d *aspnetDetector) Name() string { return "ASP.NET" }
func (d *aspnetDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "X-AspNet-Version", Weight: 0.5, HeaderOnly: true},
{Pattern: "X-AspNetMvc-Version", Weight: 0.5, HeaderOnly: true},
{Pattern: "ASP.NET", Weight: 0.4, HeaderOnly: true},
{Pattern: "__VIEWSTATE", Weight: 0.4},
{Pattern: "__EVENTVALIDATION", Weight: 0.3},
{Pattern: "__VIEWSTATEGENERATOR", Weight: 0.3},
{Pattern: ".aspx", Weight: 0.2},
{Pattern: ".ashx", Weight: 0.2},
{Pattern: ".asmx", Weight: 0.2},
{Pattern: "asp.net_sessionid", Weight: 0.4, HeaderOnly: true},
{Pattern: "X-Powered-By: ASP.NET", Weight: 0.4, HeaderOnly: true},
}
}
func (d *aspnetDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// aspnetCoreDetector detects ASP.NET Core framework.
type aspnetCoreDetector struct{}
func (d *aspnetCoreDetector) Name() string { return "ASP.NET Core" }
func (d *aspnetCoreDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: ".AspNetCore.", Weight: 0.5, HeaderOnly: true},
{Pattern: "blazor", Weight: 0.4},
{Pattern: "_blazor", Weight: 0.4},
{Pattern: "dotnet", Weight: 0.2, HeaderOnly: true},
}
}
func (d *aspnetCoreDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// springDetector detects Spring framework.
type springDetector struct{}
func (d *springDetector) Name() string { return "Spring" }
func (d *springDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "org.springframework", Weight: 0.4, HeaderOnly: true},
{Pattern: "spring-security", Weight: 0.3, HeaderOnly: true},
{Pattern: "JSESSIONID", Weight: 0.3, HeaderOnly: true},
{Pattern: "X-Application-Context", Weight: 0.3, HeaderOnly: true},
}
}
func (d *springDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// springBootDetector detects Spring Boot framework.
type springBootDetector struct{}
func (d *springBootDetector) Name() string { return "Spring Boot" }
func (d *springBootDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "spring-boot", Weight: 0.5},
{Pattern: "actuator", Weight: 0.3},
{Pattern: "whitelabel", Weight: 0.2},
}
}
func (d *springBootDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// flaskDetector detects Flask framework.
type flaskDetector struct{}
func (d *flaskDetector) Name() string { return "Flask" }
func (d *flaskDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "Werkzeug", Weight: 0.4, HeaderOnly: true},
{Pattern: "flask", Weight: 0.3, HeaderOnly: true},
{Pattern: "jinja2", Weight: 0.3},
}
}
func (d *flaskDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// symfonyDetector detects Symfony framework.
type symfonyDetector struct{}
func (d *symfonyDetector) Name() string { return "Symfony" }
func (d *symfonyDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "symfony", Weight: 0.4, HeaderOnly: true},
{Pattern: "sf_", Weight: 0.3, HeaderOnly: true},
{Pattern: "_sf2_", Weight: 0.3, HeaderOnly: true},
}
}
func (d *symfonyDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// fastapiDetector detects FastAPI framework.
type fastapiDetector struct{}
func (d *fastapiDetector) Name() string { return "FastAPI" }
func (d *fastapiDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "fastapi", Weight: 0.4, HeaderOnly: true},
{Pattern: "starlette", Weight: 0.3, HeaderOnly: true},
}
}
func (d *fastapiDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// ginDetector detects Gin framework.
type ginDetector struct{}
func (d *ginDetector) Name() string { return "Gin" }
func (d *ginDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "gin-gonic", Weight: 0.4},
{Pattern: "gin", Weight: 0.2, HeaderOnly: true},
}
}
func (d *ginDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// phoenixDetector detects Phoenix framework.
type phoenixDetector struct{}
func (d *phoenixDetector) Name() string { return "Phoenix" }
func (d *phoenixDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "_csrf_token", Weight: 0.4, HeaderOnly: true},
{Pattern: "phx-", Weight: 0.3},
{Pattern: "phoenix", Weight: 0.2},
}
}
func (d *phoenixDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// strapiDetector detects Strapi framework.
type strapiDetector struct{}
func (d *strapiDetector) Name() string { return "Strapi" }
func (d *strapiDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "strapi", Weight: 0.4},
{Pattern: "/api/", Weight: 0.2},
}
}
func (d *strapiDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// adonisDetector detects AdonisJS framework.
type adonisDetector struct{}
func (d *adonisDetector) Name() string { return "AdonisJS" }
func (d *adonisDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "adonis", Weight: 0.4},
{Pattern: "_csrf", Weight: 0.2, HeaderOnly: true},
}
}
func (d *adonisDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// cakephpDetector detects CakePHP framework.
type cakephpDetector struct{}
func (d *cakephpDetector) Name() string { return "CakePHP" }
func (d *cakephpDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "cakephp", Weight: 0.4},
{Pattern: "cake", Weight: 0.2},
}
}
func (d *cakephpDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// codeigniterDetector detects CodeIgniter framework.
type codeigniterDetector struct{}
func (d *codeigniterDetector) Name() string { return "CodeIgniter" }
func (d *codeigniterDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "codeigniter", Weight: 0.4},
{Pattern: "ci_session", Weight: 0.4, HeaderOnly: true},
}
}
func (d *codeigniterDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
+192
View File
@@ -0,0 +1,192 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package detectors
import (
"net/http"
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
)
func init() {
// Register all CMS detectors
fw.Register(&wordpressDetector{})
fw.Register(&drupalDetector{})
fw.Register(&joomlaDetector{})
fw.Register(&magentoDetector{})
fw.Register(&shopifyDetector{})
fw.Register(&ghostDetector{})
}
// wordpressDetector detects WordPress CMS.
type wordpressDetector struct{}
func (d *wordpressDetector) Name() string { return "WordPress" }
func (d *wordpressDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "wp-content", Weight: 0.4},
{Pattern: "wp-includes", Weight: 0.4},
{Pattern: "wp-json", Weight: 0.3},
{Pattern: "wordpress", Weight: 0.3},
{Pattern: "wp-emoji", Weight: 0.2},
}
}
func (d *wordpressDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// drupalDetector detects Drupal CMS.
type drupalDetector struct{}
func (d *drupalDetector) Name() string { return "Drupal" }
func (d *drupalDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "Drupal", Weight: 0.4, HeaderOnly: true},
{Pattern: "drupal.js", Weight: 0.4},
{Pattern: "/sites/default/files", Weight: 0.3},
{Pattern: "Drupal.settings", Weight: 0.3},
}
}
func (d *drupalDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// joomlaDetector detects Joomla CMS.
type joomlaDetector struct{}
func (d *joomlaDetector) Name() string { return "Joomla" }
func (d *joomlaDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "Joomla", Weight: 0.4},
{Pattern: "/media/jui/", Weight: 0.4},
{Pattern: "/components/com_", Weight: 0.3},
{Pattern: "joomla.javascript", Weight: 0.3},
}
}
func (d *joomlaDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// magentoDetector detects Magento CMS.
type magentoDetector struct{}
func (d *magentoDetector) Name() string { return "Magento" }
func (d *magentoDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "Magento", Weight: 0.4},
{Pattern: "/static/frontend/", Weight: 0.4},
{Pattern: "mage/", Weight: 0.3},
{Pattern: "Mage.Cookies", Weight: 0.3},
}
}
func (d *magentoDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// shopifyDetector detects Shopify platform.
type shopifyDetector struct{}
func (d *shopifyDetector) Name() string { return "Shopify" }
func (d *shopifyDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "Shopify", Weight: 0.5},
{Pattern: "cdn.shopify.com", Weight: 0.4},
{Pattern: "shopify-section", Weight: 0.4},
{Pattern: "myshopify.com", Weight: 0.3},
}
}
func (d *shopifyDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// ghostDetector detects Ghost CMS.
type ghostDetector struct{}
func (d *ghostDetector) Name() string { return "Ghost" }
func (d *ghostDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "ghost-", Weight: 0.4},
{Pattern: "Ghost", Weight: 0.3, HeaderOnly: true},
{Pattern: "/ghost/api/", Weight: 0.4},
}
}
func (d *ghostDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
@@ -0,0 +1,30 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package detectors
import "testing"
func TestSigmoidConfidence(t *testing.T) {
// a weak match (small matched-weight fraction) must stay below the 0.5
// detection threshold; a strong match must clear it. the old curve put any
// match above 0.5, which is what false-detected magento on a plain page.
if c := sigmoidConfidence(0); c >= 0.5 {
t.Errorf("no match conf = %.3f, want < 0.5", c)
}
if c := sigmoidConfidence(0.2); c >= 0.5 {
t.Errorf("weak match conf = %.3f, want < 0.5", c)
}
if c := sigmoidConfidence(0.5); c <= 0.5 {
t.Errorf("strong match conf = %.3f, want > 0.5", c)
}
}
@@ -0,0 +1,220 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package detectors
import (
"net/http"
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
)
func init() {
// Register all frontend detectors
fw.Register(&reactDetector{})
fw.Register(&vueDetector{})
fw.Register(&angularDetector{})
fw.Register(&svelteDetector{})
fw.Register(&emberDetector{})
fw.Register(&backboneDetector{})
fw.Register(&meteorDetector{})
}
// reactDetector detects React framework.
type reactDetector struct{}
func (d *reactDetector) Name() string { return "React" }
func (d *reactDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "data-reactroot", Weight: 0.5},
{Pattern: "react-dom", Weight: 0.4},
{Pattern: "__REACT_DEVTOOLS", Weight: 0.4},
{Pattern: "react.production", Weight: 0.4},
{Pattern: "_reactRootContainer", Weight: 0.3},
}
}
func (d *reactDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// vueDetector detects Vue.js framework.
type vueDetector struct{}
func (d *vueDetector) Name() string { return "Vue.js" }
func (d *vueDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "data-v-", Weight: 0.5},
{Pattern: "Vue.js", Weight: 0.4},
{Pattern: "vue.runtime", Weight: 0.4},
{Pattern: "vue.min.js", Weight: 0.4},
{Pattern: "__vue__", Weight: 0.3},
{Pattern: "v-cloak", Weight: 0.3},
}
}
func (d *vueDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// angularDetector detects Angular framework.
type angularDetector struct{}
func (d *angularDetector) Name() string { return "Angular" }
func (d *angularDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "ng-version", Weight: 0.5},
{Pattern: "ng-app", Weight: 0.4},
{Pattern: "ng-controller", Weight: 0.4},
{Pattern: "angular.js", Weight: 0.4},
{Pattern: "angular.min.js", Weight: 0.4},
{Pattern: "ng-binding", Weight: 0.3},
{Pattern: "_nghost", Weight: 0.3},
{Pattern: "_ngcontent", Weight: 0.3},
}
}
func (d *angularDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// svelteDetector detects Svelte framework.
type svelteDetector struct{}
func (d *svelteDetector) Name() string { return "Svelte" }
func (d *svelteDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "svelte", Weight: 0.4},
{Pattern: "__svelte", Weight: 0.5},
{Pattern: "svelte-", Weight: 0.3},
}
}
func (d *svelteDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// emberDetector detects Ember.js framework.
type emberDetector struct{}
func (d *emberDetector) Name() string { return "Ember.js" }
func (d *emberDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "ember", Weight: 0.4},
{Pattern: "ember-cli", Weight: 0.4},
{Pattern: "data-ember", Weight: 0.3},
}
}
func (d *emberDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// backboneDetector detects Backbone.js framework.
type backboneDetector struct{}
func (d *backboneDetector) Name() string { return "Backbone.js" }
func (d *backboneDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "backbone", Weight: 0.4},
{Pattern: "Backbone.", Weight: 0.4},
}
}
func (d *backboneDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// meteorDetector detects Meteor framework.
type meteorDetector struct{}
func (d *meteorDetector) Name() string { return "Meteor" }
func (d *meteorDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "__meteor_runtime_config__", Weight: 0.5},
{Pattern: "meteor", Weight: 0.3},
}
}
func (d *meteorDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
+191
View File
@@ -0,0 +1,191 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package detectors
import (
"net/http"
fw "github.com/dropalldatabases/sif/internal/scan/frameworks"
)
func init() {
// Register all meta-framework detectors
fw.Register(&nextjsDetector{})
fw.Register(&nuxtDetector{})
fw.Register(&sveltekitDetector{})
fw.Register(&gatsbyDetector{})
fw.Register(&remixDetector{})
fw.Register(&astroDetector{})
}
// nextjsDetector detects Next.js framework.
type nextjsDetector struct{}
func (d *nextjsDetector) Name() string { return "Next.js" }
func (d *nextjsDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "__NEXT_DATA__", Weight: 0.5},
{Pattern: "_next/static", Weight: 0.4},
{Pattern: "__next", Weight: 0.3},
{Pattern: "x-nextjs", Weight: 0.3, HeaderOnly: true},
}
}
func (d *nextjsDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// nuxtDetector detects Nuxt.js framework.
type nuxtDetector struct{}
func (d *nuxtDetector) Name() string { return "Nuxt.js" }
func (d *nuxtDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "__NUXT__", Weight: 0.5},
{Pattern: "_nuxt/", Weight: 0.4},
{Pattern: "nuxt", Weight: 0.2},
}
}
func (d *nuxtDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// sveltekitDetector detects SvelteKit framework.
type sveltekitDetector struct{}
func (d *sveltekitDetector) Name() string { return "SvelteKit" }
func (d *sveltekitDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "__sveltekit", Weight: 0.5},
{Pattern: "_app/immutable", Weight: 0.4},
{Pattern: "sveltekit", Weight: 0.3},
}
}
func (d *sveltekitDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// gatsbyDetector detects Gatsby framework.
type gatsbyDetector struct{}
func (d *gatsbyDetector) Name() string { return "Gatsby" }
func (d *gatsbyDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "___gatsby", Weight: 0.5},
{Pattern: "gatsby-", Weight: 0.4},
{Pattern: "page-data.json", Weight: 0.3},
}
}
func (d *gatsbyDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// remixDetector detects Remix framework.
type remixDetector struct{}
func (d *remixDetector) Name() string { return "Remix" }
func (d *remixDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: "__remixContext", Weight: 0.5},
{Pattern: "remix", Weight: 0.3},
{Pattern: "_remix", Weight: 0.4},
}
}
func (d *remixDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
// astroDetector detects Astro framework.
type astroDetector struct{}
func (d *astroDetector) Name() string { return "Astro" }
func (d *astroDetector) Signatures() []fw.Signature {
return []fw.Signature{
{Pattern: `<meta name="generator" content="Astro`, Weight: 0.5},
{Pattern: "astro-island", Weight: 0.5},
{Pattern: "data-astro-cid-", Weight: 0.4},
{Pattern: "/_astro/", Weight: 0.4},
{Pattern: "data-astro-transition", Weight: 0.3},
{Pattern: "data-astro-reload", Weight: 0.3},
{Pattern: "data-astro-history", Weight: 0.3},
}
}
func (d *astroDetector) Detect(body string, headers http.Header) (float32, string) {
base := fw.NewBaseDetector(d.Name(), d.Signatures())
score := base.MatchSignatures(body, headers)
confidence := sigmoidConfidence(score)
var version string
if confidence > 0.5 {
version = fw.ExtractVersionOptimized(body, d.Name()).Version
}
return confidence, version
}
+46
View File
@@ -0,0 +1,46 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import "testing"
func FuzzIsValidVersionString(f *testing.F) {
f.Add("1.0")
f.Add("1.0.0")
f.Add("10.2.3.4")
f.Add("999.999.999")
f.Add("")
f.Add("abc")
f.Add("1.")
f.Add(".1")
f.Add("1.2.3.4.5")
f.Add("aaaaaaaaaaaaaaaaaaaaaaaaa")
f.Fuzz(func(t *testing.T, v string) {
// should never panic
isValidVersionString(v)
})
}
func FuzzExtractVersionOptimized(f *testing.F) {
f.Add("<meta name=\"generator\" content=\"WordPress 6.4.2\">", "WordPress")
f.Add("Laravel v10.0.1", "Laravel")
f.Add("<html>nothing</html>", "Django")
f.Add("", "unknown")
f.Add("X-Powered-By: Express/4.18.2", "Express")
f.Fuzz(func(t *testing.T, body string, framework string) {
// should never panic
ExtractVersionOptimized(body, framework)
})
}
+95
View File
@@ -0,0 +1,95 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
/*
BSD 3-Clause License
(c) 2022-2026 vmfunc, xyzeva & contributors
*/
package frameworks
// FrameworkResult represents the result of framework detection.
type FrameworkResult struct {
Name string `json:"name"`
Version string `json:"version"`
Confidence float32 `json:"confidence"`
VersionConfidence float32 `json:"version_confidence"`
CVEs []string `json:"cves,omitempty"`
Suggestions []string `json:"suggestions,omitempty"`
RiskLevel string `json:"risk_level,omitempty"`
}
// ResultType implements the ScanResult interface.
func (r *FrameworkResult) ResultType() string { return "framework" }
// NewFrameworkResult creates a new FrameworkResult with the given parameters.
func NewFrameworkResult(name, version string, confidence, versionConfidence float32) *FrameworkResult {
return &FrameworkResult{
Name: name,
Version: version,
Confidence: confidence,
VersionConfidence: versionConfidence,
}
}
// WithVulnerabilities adds CVE information to the result.
func (r *FrameworkResult) WithVulnerabilities(cves, suggestions []string) *FrameworkResult {
r.CVEs = cves
r.Suggestions = suggestions
r.RiskLevel = determineRiskLevel(cves)
return r
}
// determineRiskLevel calculates the risk level based on CVE severities.
func determineRiskLevel(cves []string) string {
if len(cves) == 0 {
return "low"
}
for _, cve := range cves {
if containsSeverity(cve, "critical") {
return "critical"
}
}
for _, cve := range cves {
if containsSeverity(cve, "high") {
return "high"
}
}
return "medium"
}
func containsSeverity(cve, severity string) bool {
// Simple substring match for now - could be more sophisticated
for i := 0; i+len(severity) <= len(cve); i++ {
match := true
for j := 0; j < len(severity); j++ {
c := cve[i+j]
// Case-insensitive comparison
if c >= 'A' && c <= 'Z' {
c += 'a' - 'A'
}
if c != severity[j] {
match = false
break
}
}
if match {
return true
}
}
return false
}
+221
View File
@@ -0,0 +1,221 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package frameworks
import (
"regexp"
"unicode"
)
// VersionMatch represents a version detection result with confidence.
type VersionMatch struct {
Version string
Confidence float32
Source string // where the version was found
}
// compiledVersionPattern holds a pre-compiled regex for version extraction
type compiledVersionPattern struct {
re *regexp.Regexp
confidence float32
source string
}
// frameworkVersionPatterns maps framework names to their pre-compiled version patterns.
// Patterns are compiled once at package initialization for optimal performance.
var frameworkVersionPatterns map[string][]compiledVersionPattern
func init() {
// Raw patterns to be compiled
rawPatterns := map[string][]struct {
pattern string
confidence float32
source string
}{
"Laravel": {
{`Laravel\s+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`laravel/framework.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "composer.json"},
},
"Django": {
{`Django[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`django.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "package reference"},
},
"Ruby on Rails": {
{`Rails[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`rails.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "gem reference"},
},
"Express.js": {
{`Express[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"express":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"ASP.NET": {
{`X-AspNet-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.95, "header"},
{`ASP\.NET[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`X-AspNetMvc-Version:\s*(\d+\.\d+(?:\.\d+)?)`, 0.9, "MVC header"},
},
"ASP.NET Core": {
{`\.NET\s*(\d+\.\d+(?:\.\d+)?)`, 0.8, "dotnet version"},
},
"Spring": {
{`Spring[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`spring-core.*?(\d+\.\d+(?:\.\d+)?)`, 0.8, "maven"},
},
"Spring Boot": {
{`spring-boot.*?(\d+\.\d+(?:\.\d+)?)`, 0.9, "maven"},
},
"Flask": {
{`Flask[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`Werkzeug[/\s]+(\d+\.\d+(?:\.\d+)?)`, 0.7, "werkzeug version"},
},
"Next.js": {
{`Next\.js[/\s]+[Vv]?(\d{1,2}\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"next":\s*"[~^]?(\d{1,2}\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"Nuxt.js": {
{`Nuxt[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"nuxt":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"Vue.js": {
{`Vue\.js[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"vue":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
{`vue@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
},
"Angular": {
{`ng-version="(\d+\.\d+(?:\.\d+)?)"`, 0.95, "ng-version attribute"},
{`Angular[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"@angular/core":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"React": {
{`React[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"react":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
{`react@(\d+\.\d+(?:\.\d+)?)`, 0.8, "CDN reference"},
},
"Svelte": {
{`Svelte[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"svelte":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"SvelteKit": {
{`"@sveltejs/kit":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
"WordPress": {
{`<meta name="generator" content="WordPress (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
{`WordPress (\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Drupal": {
{`Drupal[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`<meta name="Generator" content="Drupal (\d+)`, 0.9, "generator meta"},
},
"Joomla": {
{`Joomla[!/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`<meta name="generator" content="Joomla! - Open Source Content Management - Version (\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
},
"Magento": {
{`Magento[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Shopify": {
{`Shopify\.theme.*?(\d+\.\d+(?:\.\d+)?)`, 0.7, "theme version"},
},
"Symfony": {
{`Symfony[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"FastAPI": {
{`FastAPI[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Gin": {
{`Gin[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Phoenix": {
{`Phoenix[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Ember.js": {
{`Ember[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Backbone.js": {
{`Backbone[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Meteor": {
{`Meteor[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Ghost": {
{`Ghost[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
},
"Astro": {
{`<meta name="generator" content="Astro v?(\d+\.\d+(?:\.\d+)?)"`, 0.95, "generator meta"},
{`Astro[/\s]+[Vv]?(\d+\.\d+(?:\.\d+)?)`, 0.9, "explicit version"},
{`"astro":\s*"[~^]?(\d+\.\d+(?:\.\d+)?)"`, 0.85, "package.json"},
},
}
// Compile all patterns
frameworkVersionPatterns = make(map[string][]compiledVersionPattern, len(rawPatterns))
for framework, patterns := range rawPatterns {
compiled := make([]compiledVersionPattern, len(patterns))
for i, p := range patterns {
compiled[i] = compiledVersionPattern{
re: regexp.MustCompile(p.pattern),
confidence: p.confidence,
source: p.source,
}
}
frameworkVersionPatterns[framework] = compiled
}
}
// ExtractVersionOptimized extracts version using pre-compiled patterns.
// This is exported for use by individual detector implementations.
func ExtractVersionOptimized(body string, framework string) VersionMatch {
patterns, exists := frameworkVersionPatterns[framework]
if !exists {
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
}
var bestMatch VersionMatch
for _, p := range patterns {
matches := p.re.FindStringSubmatch(body)
if len(matches) > 1 && p.confidence > bestMatch.Confidence {
candidate := matches[1]
if isValidVersionString(candidate) {
bestMatch = VersionMatch{
Version: candidate,
Confidence: p.confidence,
Source: p.source,
}
}
}
}
if bestMatch.Version == "" {
return VersionMatch{Version: "unknown", Confidence: 0, Source: ""}
}
return bestMatch
}
// isValidVersionString checks if a version string looks like a valid semver
func isValidVersionString(v string) bool {
if v == "" || len(v) > 20 {
return false
}
dotCount := 0
for _, c := range v {
if c == '.' {
dotCount++
if dotCount > 3 {
return false
}
} else if !unicode.IsDigit(c) {
return false
}
}
return dotCount >= 1
}
+63
View File
@@ -0,0 +1,63 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import "testing"
func FuzzDetectLFIFromResponse(f *testing.F) {
f.Add("root:x:0:0:root:/root:/bin/bash")
f.Add("<html><body>Hello World</body></html>")
f.Add("[boot loader]\ntimeout=30")
f.Add("DOCUMENT_ROOT=/var/www/html")
f.Add("<?php echo 'hello'; ?>")
f.Add("127.0.0.1 localhost")
f.Add("")
f.Add("PD9waHAgZWNobyAnaGVsbG8nOyA/Pg==")
f.Fuzz(func(t *testing.T, body string) {
// should never panic
DetectLFIFromResponse(body)
})
}
func FuzzIsAdminPanel(f *testing.F) {
f.Add("<html>phpMyAdmin</html>", "phpMyAdmin")
f.Add("<html>adminer</html>", "Adminer")
f.Add("<html>pgadmin</html>", "pgAdmin")
f.Add("<html>nothing here</html>", "phpMyAdmin")
f.Add("", "unknown")
f.Add("<html>database query mysql</html>", "generic")
f.Fuzz(func(t *testing.T, body string, panelType string) {
// should never panic
isAdminPanel(body, panelType)
})
}
func FuzzDatabaseErrorPatterns(f *testing.F) {
f.Add("you have an error in your sql syntax")
f.Add("Warning: mysql_fetch_array()")
f.Add("postgresql error at character 42")
f.Add("ORA-12345: some oracle error")
f.Add("sqlite3_prepare_v2 failed")
f.Add("document bson error in mongodb")
f.Add("<html>normal page</html>")
f.Add("")
f.Fuzz(func(t *testing.T, body string) {
// should never panic on any input
for _, pattern := range databaseErrorPatterns {
pattern.pattern.MatchString(body)
}
})
}
+45 -27
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,46 +14,50 @@ package scan
import (
"bufio"
"fmt"
"context"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
const (
gitURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/git/"
gitFile = "git.txt"
)
// gitURL is a var so integration tests can repoint it at a fixture.
var gitURL = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/git/"
const gitFile = "git.txt"
func Git(url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
log := output.Module("GIT")
log.Start()
fmt.Println(styles.Separator.Render("🌿 Starting " + styles.Status.Render("git repository scanning") + "..."))
spin := output.NewSpinner("Scanning for exposed git repositories")
spin.Start()
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "git directory fuzzing"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
gitlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Git 🌿",
}).With("url", url)
gitlog.Infof("Starting repository scanning")
resp, err := http.Get(gitURL + gitFile)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, gitURL+gitFile, http.NoBody)
if err != nil {
log.Errorf("Error downloading git list: %s", err)
spin.Stop()
log.Error("Error creating git list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
log.Error("Error downloading git list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -70,6 +74,7 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
}
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(threads)
foundUrls := []string{}
@@ -82,25 +87,38 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
continue
}
log.Debugf("%s", repourl)
resp, err := client.Get(url + "/" + repourl)
charmlog.Debugf("%s", repourl)
gitReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+repourl, http.NoBody)
if err != nil {
log.Debugf("Error %s: %s", repourl, err)
charmlog.Debugf("Error creating request for %s: %s", repourl, err)
continue
}
resp, err := client.Do(gitReq)
if err != nil {
charmlog.Debugf("Error %s: %s", repourl, err)
continue
}
if resp.StatusCode == 200 && !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
// log url, directory, and status code
gitlog.Infof("%s git found at [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(repourl))
spin.Stop()
log.Success("Git found at %s [%s]", output.Highlight.Render(repourl), output.Status.Render(strconv.Itoa(resp.StatusCode)))
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s git found at [%s]\n", strconv.Itoa(resp.StatusCode), repourl))
logger.Write(sanitizedURL, logdir, strconv.Itoa(resp.StatusCode)+" git found at ["+repourl+"]\n")
}
mu.Lock()
foundUrls = append(foundUrls, resp.Request.URL.String())
mu.Unlock()
}
resp.Body.Close()
}
}(thread)
}
wg.Wait()
spin.Stop()
log.Complete(len(foundUrls), "found")
return foundUrls, nil
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -13,15 +13,12 @@
package scan
import (
"fmt"
"context"
"net/http"
"os"
"strings"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
type HeaderResult struct {
@@ -30,26 +27,27 @@ type HeaderResult struct {
}
func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult, error) {
fmt.Println(styles.Separator.Render("🔍 Starting " + styles.Status.Render("HTTP Header Analysis") + "..."))
log := output.Module("HEADERS")
log.Start()
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "HTTP Header Analysis"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
headerlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Headers 🔍",
}).With("url", url)
client := &http.Client{
Timeout: timeout,
}
resp, err := client.Get(url)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
@@ -60,12 +58,13 @@ func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult,
for name, values := range resp.Header {
for _, value := range values {
results = append(results, HeaderResult{Name: name, Value: value})
headerlog.Infof("%s: %s", styles.Highlight.Render(name), value)
log.Info("%s: %s", output.Highlight.Render(name), value)
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s: %s\n", name, value))
logger.Write(sanitizedURL, logdir, name+": "+value+"\n")
}
}
}
log.Complete(len(results), "found")
return results, nil
}
+219
View File
@@ -0,0 +1,219 @@
//go:build integration
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
// These tests run the real scanners against a local server standing in for a
// deliberately-vulnerable app, asserting the findings each one should produce.
// They're behind the `integration` build tag so the default `go test` stays
// network-free; run with `go test -tags=integration ./internal/scan/...`.
package scan
import (
"context"
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
)
// newVulnApp serves the planted artifacts each scanner is meant to find, plus
// the wordlists the remote-list scanners fetch.
func newVulnApp() *httptest.Server {
mux := http.NewServeMux()
// wordlists the remote-list scanners download
mux.HandleFunc("/git.txt", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(".git/HEAD\n.git/config\n"))
})
mux.HandleFunc("/directory-list-2.3-small.txt", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("admin\nlogin\nnope\n"))
})
// an exposed git repo: HEAD is a real find, config is html so it's excluded
mux.HandleFunc("/.git/HEAD", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
w.Write([]byte("ref: refs/heads/main\n"))
})
mux.HandleFunc("/.git/config", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<html>nope</html>"))
})
// live directories for dirlist
mux.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
// an exposed db admin panel for sql recon
mux.HandleFunc("/phpmyadmin/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("<title>phpMyAdmin</title>"))
})
// homepage doubles as the cms fingerprint and the lfi sink
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
if strings.Contains(r.URL.RawQuery, "passwd") || strings.Contains(r.URL.RawQuery, "etc") {
w.Write([]byte("root:x:0:0:root:/root:/bin/bash\n"))
return
}
w.Header().Set("X-Powered-By", "PHP/8.1.0")
w.Write([]byte(`<html><head><link href="/wp-content/themes/x/style.css"></head><body>hi</body></html>`))
})
return httptest.NewServer(mux)
}
func TestIntegrationGit(t *testing.T) {
srv := newVulnApp()
defer srv.Close()
orig := gitURL
gitURL = srv.URL + "/"
defer func() { gitURL = orig }()
found, err := Git(srv.URL, 5*time.Second, 2, "")
if err != nil {
t.Fatalf("Git: %v", err)
}
if len(found) != 1 {
t.Fatalf("expected 1 git find (HEAD, not the html config), got %d: %v", len(found), found)
}
if !strings.HasSuffix(found[0], ".git/HEAD") {
t.Errorf("expected .git/HEAD, got %s", found[0])
}
}
func TestIntegrationDirlist(t *testing.T) {
srv := newVulnApp()
defer srv.Close()
orig := directoryURL
directoryURL = srv.URL + "/"
defer func() { directoryURL = orig }()
results, err := Dirlist("small", srv.URL, 5*time.Second, 3, "")
if err != nil {
t.Fatalf("Dirlist: %v", err)
}
got := map[string]bool{}
for _, r := range results {
got[r.Url] = true
}
if !hasSuffixIn(got, "/admin") || !hasSuffixIn(got, "/login") {
t.Errorf("expected admin and login to be found, got %v", results)
}
if hasSuffixIn(got, "/nope") {
t.Errorf("404 path nope should not be reported, got %v", results)
}
}
func TestIntegrationCMS(t *testing.T) {
srv := newVulnApp()
defer srv.Close()
result, err := CMS(srv.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("CMS: %v", err)
}
if result == nil || result.Name != "WordPress" {
t.Errorf("expected WordPress, got %+v", result)
}
}
func TestIntegrationHeaders(t *testing.T) {
srv := newVulnApp()
defer srv.Close()
results, err := Headers(srv.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("Headers: %v", err)
}
if len(results) == 0 {
t.Error("expected at least one header back")
}
}
func TestIntegrationSQL(t *testing.T) {
srv := newVulnApp()
defer srv.Close()
result, err := SQL(srv.URL, 5*time.Second, 5, "")
if err != nil {
t.Fatalf("SQL: %v", err)
}
if result == nil || len(result.AdminPanels) == 0 {
t.Fatalf("expected an admin panel finding, got %+v", result)
}
if result.AdminPanels[0].Type != "phpMyAdmin" {
t.Errorf("expected phpMyAdmin, got %s", result.AdminPanels[0].Type)
}
}
func TestIntegrationLFI(t *testing.T) {
srv := newVulnApp()
defer srv.Close()
result, err := LFI(srv.URL, 5*time.Second, 5, "")
if err != nil {
t.Fatalf("LFI: %v", err)
}
if result == nil || len(result.Vulnerabilities) == 0 {
t.Errorf("expected an lfi finding from the passwd sink, got %+v", result)
}
}
func TestIntegrationPorts(t *testing.T) {
// a real listener stands in for an open port; a tiny server hands its number
// to Ports via the commonPorts wordlist.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
defer ln.Close()
port := ln.Addr().(*net.TCPAddr).Port
list := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(strconv.Itoa(port) + "\n"))
}))
defer list.Close()
orig := commonPorts
commonPorts = list.URL
defer func() { commonPorts = orig }()
open, err := Ports(context.Background(), "common", "tcp://127.0.0.1", 2*time.Second, 1, "")
if err != nil {
t.Fatalf("Ports: %v", err)
}
found := false
for _, p := range open {
if p == strconv.Itoa(port) {
found = true
}
}
if !found {
t.Errorf("expected open port %d in %v", port, open)
}
}
func hasSuffixIn(set map[string]bool, suffix string) bool {
for k := range set {
if strings.HasSuffix(k, suffix) {
return true
}
}
return false
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -24,6 +24,7 @@ package frameworks
import (
"bufio"
"context"
"fmt"
"net/http"
"regexp"
@@ -32,33 +33,37 @@ import (
urlutil "github.com/projectdiscovery/utils/url"
)
// nextPagesRegex matches JavaScript file references in Next.js build manifest.
var nextPagesRegex = regexp.MustCompile(`\[("([^"]+\.js)"(,?))`)
func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
baseUrl, err := urlutil.Parse(scriptUrl)
if err != nil {
return nil, err
}
resp, err := http.Get(scriptUrl)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, scriptUrl, http.NoBody)
if err != nil {
fmt.Println(err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println(err)
return nil, err
}
defer resp.Body.Close()
var manifestText string
var sb strings.Builder
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
manifestText += scanner.Text()
sb.WriteString(scanner.Text())
}
manifestText := sb.String()
regex, err := regexp.Compile("\\[(\"([^\"]+.js)\"(,?))")
if err != nil {
return nil, err
}
list := regex.FindAllStringSubmatch(manifestText, -1)
list := nextPagesRegex.FindAllStringSubmatch(manifestText, -1)
var scripts []string
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,17 +14,17 @@ package js
import (
"bufio"
"fmt"
"context"
"io"
"net/http"
"os"
"slices"
"strings"
"time"
"github.com/antchfx/htmlquery"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/pkg/scan/js/frameworks"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/output"
"github.com/dropalldatabases/sif/internal/scan/js/frameworks"
urlutil "github.com/projectdiscovery/utils/url"
)
@@ -33,28 +33,40 @@ type JavascriptScanResult struct {
FoundEnvironmentVars map[string]string `json:"environment_variables"`
}
// ResultType implements the ScanResult interface.
func (r *JavascriptScanResult) ResultType() string { return "js" }
func JavascriptScan(url string, timeout time.Duration, threads int, logdir string) (*JavascriptScanResult, error) {
jslog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "🚧 JavaScript",
}).With("url", url)
log := output.Module("JS")
log.Start()
spin := output.NewSpinner("Scanning JavaScript files")
spin.Start()
baseUrl, err := urlutil.Parse(url)
if err != nil {
spin.Stop()
return nil, err
}
resp, err := http.Get(url)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
fmt.Println(err)
spin.Stop()
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
return nil, err
}
defer resp.Body.Close()
var html string
var sb strings.Builder
scanner := bufio.NewScanner(resp.Body)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
html += scanner.Text()
sb.WriteString(scanner.Text())
}
html := sb.String()
doc, err := htmlquery.Parse(strings.NewReader(html))
if err != nil {
@@ -82,9 +94,10 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
for _, script := range scripts {
if strings.Contains(script, "/_buildManifest.js") {
jslog.Infof("Detected Next.JS pages router! Getting all scripts from %s", script)
log.Info("Detected Next.JS pages router! Getting all scripts from %s", script)
nextScripts, err := frameworks.GetPagesRouterScripts(script)
if err != nil {
spin.Stop()
return nil, err
}
@@ -97,29 +110,35 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
}
}
jslog.Infof("Got %d scripts, now running scans on them", len(scripts))
log.Info("Got %d scripts, now running scans on them", len(scripts))
var supabaseResults []supabaseScanResult
supabaseResults := make([]supabaseScanResult, 0, len(scripts))
for _, script := range scripts {
jslog.Infof("Scanning %s", script)
resp, err := http.Get(script)
charmlog.Debugf("Scanning %s", script)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, script, http.NoBody)
if err != nil {
fmt.Println(err)
charmlog.Warnf("Failed to create request: %s", err)
continue
}
defer resp.Body.Close()
bodyBytes, err := io.ReadAll(resp.Body)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
charmlog.Warnf("Failed to fetch script: %s", err)
continue
}
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
resp.Body.Close()
if err != nil {
charmlog.Errorf("Failed to read script body: %s", err)
continue
}
content := string(bodyBytes)
jslog.Infof("Running supabase scanner on %s", script)
scriptSupabaseResults, err := ScanSupabase(content, script)
charmlog.Debugf("Running supabase scanner on %s", script)
scriptSupabaseResults, err := ScanSupabase(content, script, timeout)
if err != nil {
jslog.Errorf("Error while scanning supabase: %s", err)
charmlog.Errorf("Error while scanning supabase: %s", err)
}
if scriptSupabaseResults != nil {
@@ -127,10 +146,14 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
}
}
spin.Stop()
result := JavascriptScanResult{
SupabaseResults: supabaseResults,
FoundEnvironmentVars: map[string]string{},
}
log.Complete(len(supabaseResults), "found")
return &result, nil
}
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -16,11 +16,11 @@ package js
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"math"
"net/http"
"os"
"regexp"
@@ -32,30 +32,91 @@ import (
"github.com/charmbracelet/log"
)
// jwtRegex matches JWT tokens in JavaScript content.
var jwtRegex = regexp.MustCompile(`["'\x60](ey[A-Za-z0-9_-]{2,}(?:\.[A-Za-z0-9_-]{2,}){2})["'\x60]`)
type supabaseJwtBody struct {
ProjectId *string `json:"ref"`
Role *string `json:"role"`
}
type supabaseScanResult struct {
ProjectId string `json:"project_id"`
ApiKey string `json:"api_key"`
Role string `json:"role"` // note: if this isnt anon its bad
Collections []supabaseCollection `json:"collections"`
}
type supabaseCollection struct {
Name string `json:"name"`
Sample []interface{} `json:"sample"`
Count int `json:"count"`
Name string `json:"name"`
Sample []json.RawMessage `json:"sample"` // raw JSON for deferred parsing
Count int `json:"count"`
}
func GetSupabaseJsonResponse(projectId string, path string, apikey string, auth *string) (map[string]interface{}, error) {
client := http.Client{}
// supabaseArrayResponse represents a response that is an array with count header.
type supabaseArrayResponse struct {
Array []json.RawMessage
Count int
}
req, err := http.NewRequest("GET", "https://"+projectId+".supabase.co"+path, nil)
// supabaseAuthResponse represents the auth response from Supabase.
type supabaseAuthResponse struct {
AccessToken string `json:"access_token"`
}
// supabaseOpenAPIResponse represents the OpenAPI spec response.
type supabaseOpenAPIResponse struct {
Paths map[string]json.RawMessage `json:"paths"`
}
// getSupabaseArrayResponse fetches a Supabase endpoint that returns an array.
func getSupabaseArrayResponse(projectId, path, apikey string, auth *string, timeout time.Duration) (*supabaseArrayResponse, error) {
body, resp, err := doSupabaseRequest(projectId, path, apikey, auth, timeout) //nolint:bodyclose // closed in doSupabaseRequest
if err != nil {
return nil, err
}
var arr []json.RawMessage
if err := json.Unmarshal(body, &arr); err != nil {
return nil, err
}
contentRange := resp.Header.Get("Content-Range")
parts := strings.Split(contentRange, "/")
if len(parts) < 2 {
return nil, errors.New("invalid Content-Range header")
}
count, err := strconv.Atoi(parts[1])
if err != nil {
return nil, err
}
return &supabaseArrayResponse{Array: arr, Count: count}, nil
}
// getSupabaseOpenAPI fetches the OpenAPI spec from Supabase.
func getSupabaseOpenAPI(projectId, apikey string, auth *string, timeout time.Duration) (*supabaseOpenAPIResponse, error) {
body, _, err := doSupabaseRequest(projectId, "/rest/v1/", apikey, auth, timeout) //nolint:bodyclose // closed in doSupabaseRequest
if err != nil {
return nil, err
}
var spec supabaseOpenAPIResponse
if err := json.Unmarshal(body, &spec); err != nil {
return nil, err
}
return &spec, nil
}
// doSupabaseRequest performs a GET request to the Supabase API.
func doSupabaseRequest(projectId, path, apikey string, auth *string, timeout time.Duration) ([]byte, *http.Response, error) {
client := http.Client{Timeout: timeout}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "https://"+projectId+".supabase.co"+path, http.NoBody)
if err != nil {
return nil, nil, err
}
log.Debugf("Sending request to %s", req.URL.String())
req.Header.Set("apikey", apikey)
req.Header.Set("Prefer", "count=exact")
@@ -65,57 +126,27 @@ func GetSupabaseJsonResponse(projectId string, path string, apikey string, auth
resp, err := client.Do(req)
if err != nil {
return nil, err
return nil, nil, err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.New("Request to " + resp.Request.URL.String() + " failed with status code " + strconv.Itoa(resp.StatusCode))
return nil, nil, errors.New("request to " + resp.Request.URL.String() + " failed with status code " + strconv.Itoa(resp.StatusCode))
}
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
if err != nil {
return nil, err
}
content := string(body)
var data interface{}
err = json.Unmarshal([]byte(content), &data)
if err != nil {
return nil, err
return nil, nil, err
}
arr, ok := data.([]interface{})
if ok {
wrappedData := map[string]interface{}{}
contentRange := resp.Header.Get("Content-Range")
count, err := strconv.Atoi(strings.Split(contentRange, "/")[1])
if err != nil {
return nil, err
}
wrappedData["count"] = count
wrappedData["array"] = arr
return wrappedData, nil
}
return data.(map[string]interface{}), nil
return body, resp, nil
}
func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error) {
func ScanSupabase(jsContent string, jsUrl string, timeout time.Duration) ([]supabaseScanResult, error) {
supabaselog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "🚧 JavaScript > Supabase ⚡️",
Prefix: "JavaScript > Supabase",
}).With("url", jsUrl)
jwtRegex, err := regexp.Compile("[\"|'|`](ey[A-Za-z0-9_-]{2,}(?:\\.[A-Za-z0-9_-]{2,}){2})[\"|'|`]")
if err != nil {
return nil, err
}
var results = []supabaseScanResult{}
jwtGroups := jwtRegex.FindAllStringSubmatch(jsContent, -1)
@@ -140,7 +171,7 @@ func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error)
supabaselog.Debugf("JWT body: %s", decoded)
var supabaseJwt *supabaseJwtBody
err = json.Unmarshal([]byte(decoded), &supabaseJwt)
err = json.Unmarshal(decoded, &supabaseJwt)
if err != nil {
supabaselog.Debugf("Failed to json parse JWT %s: %s", jwt, err)
continue
@@ -151,9 +182,9 @@ func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error)
}
supabaselog.Infof("Found valid supabase project %s with role %s", *supabaseJwt.ProjectId, *supabaseJwt.Role)
client := http.Client{}
client := http.Client{Timeout: timeout}
req, err := http.NewRequest("POST", "https://"+*supabaseJwt.ProjectId+".supabase.co/auth/v1/signup", bytes.NewBufferString(`{"email":"automated`+strconv.Itoa(int(time.Now().Unix()))+`@sif.sh","password":"automatedacct"}`))
req, err := http.NewRequestWithContext(context.TODO(), http.MethodPost, "https://"+*supabaseJwt.ProjectId+".supabase.co/auth/v1/signup", bytes.NewBufferString(`{"email":"automated`+strconv.Itoa(int(time.Now().Unix()))+`@sif.sh","password":"automatedacct"}`))
if err != nil {
supabaselog.Errorf("Error while creating HTTP req for creating user: %s", err)
continue
@@ -168,66 +199,67 @@ func ScanSupabase(jsContent string, jsUrl string) ([]supabaseScanResult, error)
var auth string
if resp.StatusCode == http.StatusOK {
body, err := io.ReadAll(resp.Body)
body, err := io.ReadAll(io.LimitReader(resp.Body, 5*1024*1024))
if err != nil {
resp.Body.Close()
return nil, err
}
content := string(body)
resp.Body.Close()
var data map[string]interface{}
err = json.Unmarshal([]byte(content), &data)
if err != nil {
var authResp supabaseAuthResponse
if err := json.Unmarshal(body, &authResp); err != nil {
return nil, err
}
auth = data["access_token"].(string)
auth = authResp.AccessToken
supabaselog.Infof("Created account with JWT %s", auth)
} else {
resp.Body.Close()
}
var collections = []supabaseCollection{}
res, err := GetSupabaseJsonResponse(*supabaseJwt.ProjectId, "/rest/v1/", jwt, &auth)
openAPI, err := getSupabaseOpenAPI(*supabaseJwt.ProjectId, jwt, &auth, timeout)
if err != nil {
return nil, err
}
index := res
if index["paths"] == nil {
if openAPI.Paths == nil {
return nil, errors.New("paths not found in supabase openapi")
}
var paths = index["paths"].(map[string]interface{})
for k := range paths {
if k == "/" {
for path := range openAPI.Paths {
if path == "/" {
continue
}
// todo: support for scanning rpc calls
if strings.HasPrefix(k, "/rpc/") {
if strings.HasPrefix(path, "/rpc/") {
continue
}
sampleObj, err := GetSupabaseJsonResponse(*supabaseJwt.ProjectId, "/rest/v1"+k, jwt, &auth)
sampleResp, err := getSupabaseArrayResponse(*supabaseJwt.ProjectId, "/rest/v1"+path, jwt, &auth, timeout)
if err != nil {
continue
}
samples := sampleObj["array"].([]interface{})
marshalled, err := json.Marshal(samples)
marshalled, err := json.Marshal(sampleResp.Array)
if err != nil {
supabaselog.Errorf("Failed to marshal sample data for %s: %s", k, err)
supabaselog.Errorf("Failed to marshal sample data for %s: %s", path, err)
}
supabaselog.Infof("Got sample (1000 entries) for collection %s: %s", k, string(marshalled))
supabaselog.Infof("Got sample (1000 entries) for collection %s: %s", path, string(marshalled))
limitedSample := samples[0:int(math.Min(float64(len(samples)), 10))]
// limit to first 10 samples
sampleLimit := len(sampleResp.Array)
if sampleLimit > 10 {
sampleLimit = 10
}
collection := supabaseCollection{
Name: strings.TrimPrefix(k, "/"),
Sample: limitedSample, // passed to local LLM for scope
Count: sampleObj["count"].(int),
Name: strings.TrimPrefix(path, "/"),
Sample: sampleResp.Array[:sampleLimit], // passed to local LLM for scope
Count: sampleResp.Count,
}
if collection.Count > 1 /* one entry may just be for the user */ {
+58 -51
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -13,19 +13,18 @@
package scan
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
// LFIResult represents the results of LFI reconnaissance
@@ -113,26 +112,26 @@ var commonLFIParams = []string{
// LFI performs LFI (Local File Inclusion) reconnaissance on the target URL
func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*LFIResult, error) {
fmt.Println(styles.Separator.Render("📁 Starting " + styles.Status.Render("LFI reconnaissance") + "..."))
log := output.Module("LFI")
log.Start()
sanitizedURL := strings.Split(targetURL, "://")[1]
spin := output.NewSpinner("Scanning for LFI vulnerabilities")
spin.Start()
sanitizedURL := stripScheme(targetURL)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "LFI reconnaissance"); err != nil {
log.Errorf("Error creating log file: %v", err)
spin.Stop()
log.Error("Error creating log file: %v", err)
return nil, err
}
}
lfilog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "LFI 📁",
}).With("url", targetURL)
lfilog.Infof("Starting LFI reconnaissance...")
result := &LFIResult{
Vulnerabilities: []LFIVulnerability{},
Vulnerabilities: make([]LFIVulnerability, 0, 16),
}
seen := make(map[string]bool)
var mu sync.Mutex
var wg sync.WaitGroup
@@ -169,7 +168,7 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
result.TestedParams = len(paramsToTest)
result.TestedPayloads = len(lfiPayloads)
lfilog.Infof("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
log.Info("Testing %d parameters with %d payloads", len(paramsToTest), len(lfiPayloads))
// create work items
type workItem struct {
@@ -215,9 +214,14 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
parsedURL.Path,
testParams.Encode())
resp, err := client.Get(testURL)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, testURL, http.NoBody)
if err != nil {
log.Debugf("Error testing %s: %v", testURL, err)
charmlog.Debugf("Error creating request for %s: %v", testURL, err)
continue
}
resp, err := client.Do(req)
if err != nil {
charmlog.Debugf("Error testing %s: %v", testURL, err)
continue
}
@@ -232,38 +236,37 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
// check for evidence patterns
for _, evidence := range lfiEvidencePatterns {
if evidence.pattern.MatchString(bodyStr) {
key := item.param + "|" + item.payload.payload
mu.Lock()
// check for duplicates
duplicate := false
for _, v := range result.Vulnerabilities {
if v.Parameter == item.param && v.Payload == item.payload.payload {
duplicate = true
break
}
if seen[key] {
mu.Unlock()
break
}
if !duplicate {
vuln := LFIVulnerability{
URL: testURL,
Parameter: item.param,
Payload: item.payload.payload,
Evidence: evidence.description,
Severity: item.payload.severity,
FileIncluded: item.payload.target,
}
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
seen[key] = true
lfilog.Warnf("LFI vulnerability found: %s in param [%s] - %s",
styles.SeverityHigh.Render(evidence.description),
styles.Highlight.Render(item.param),
styles.Status.Render(item.payload.target))
if logdir != "" {
logger.Write(sanitizedURL, logdir,
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
evidence.description, item.param, item.payload.payload))
}
vuln := LFIVulnerability{
URL: testURL,
Parameter: item.param,
Payload: item.payload.payload,
Evidence: evidence.description,
Severity: item.payload.severity,
FileIncluded: item.payload.target,
}
result.Vulnerabilities = append(result.Vulnerabilities, vuln)
mu.Unlock()
spin.Stop()
log.Warn("LFI vulnerability found: %s in param %s - %s",
output.SeverityHigh.Render(evidence.description),
output.Highlight.Render(item.param),
output.Status.Render(item.payload.target))
spin.Start()
if logdir != "" {
logger.Write(sanitizedURL, logdir,
fmt.Sprintf("LFI: %s in param [%s] via payload [%s]\n",
evidence.description, item.param, item.payload.payload))
}
break
}
}
@@ -272,9 +275,11 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
}
wg.Wait()
spin.Stop()
// summary
if len(result.Vulnerabilities) > 0 {
lfilog.Warnf("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
log.Warn("Found %d LFI vulnerabilities", len(result.Vulnerabilities))
criticalCount := 0
highCount := 0
for _, v := range result.Vulnerabilities {
@@ -285,14 +290,16 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
}
}
if criticalCount > 0 {
lfilog.Errorf("%d CRITICAL vulnerabilities found!", criticalCount)
log.Error("%d CRITICAL vulnerabilities found!", criticalCount)
}
if highCount > 0 {
lfilog.Warnf("%d HIGH severity vulnerabilities found", highCount)
log.Warn("%d HIGH severity vulnerabilities found", highCount)
}
log.Complete(len(result.Vulnerabilities), "found")
} else {
lfilog.Infof("No LFI vulnerabilities detected")
return nil, nil
log.Info("No LFI vulnerabilities detected")
log.Complete(0, "found")
return nil, nil //nolint:nilnil // no LFI found is not an error
}
return result, nil
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
+90
View File
@@ -0,0 +1,90 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"context"
"os"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/nuclei/format"
"github.com/dropalldatabases/sif/internal/nuclei/templates"
sifoutput "github.com/dropalldatabases/sif/internal/output"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
)
func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]output.ResultEvent, error) {
sifoutput.ScanStart("nuclei template scanning")
spin := sifoutput.NewSpinner("Running nuclei templates")
spin.Start()
nucleilog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "nuclei",
}).With("url", url)
_ = templates.Install(nucleilog)
pwd, err := os.Getwd()
if err != nil {
spin.Stop()
return nil, err
}
ctx := context.Background()
ne, err := nuclei.NewNucleiEngineCtx(ctx,
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
Templates: []string{pwd + "/nuclei-templates"},
}),
nuclei.WithConcurrency(nuclei.Concurrency{
TemplateConcurrency: threads,
HostConcurrency: 1,
HeadlessHostConcurrency: 1,
HeadlessTemplateConcurrency: 1,
JavascriptTemplateConcurrency: 1,
TemplatePayloadConcurrency: 25,
ProbeConcurrency: 50,
}),
nuclei.WithNetworkConfig(nuclei.NetworkConfig{
Timeout: int(timeout.Seconds()),
}),
nuclei.DisableUpdateCheck(),
)
if err != nil {
spin.Stop()
return nil, err
}
defer ne.Close()
sanitizedURL := stripScheme(url)
ne.LoadTargets([]string{sanitizedURL}, false)
var results []output.ResultEvent
var mu sync.Mutex
err = ne.ExecuteCallbackWithCtx(ctx, func(event *output.ResultEvent) {
if event.Matched != "" {
nucleilog.Infof("%s", format.FormatLine(event))
mu.Lock()
results = append(results, *event)
mu.Unlock()
}
})
spin.Stop()
sifoutput.ScanComplete("nuclei template scanning", len(results), "found")
return results, err
}
+38 -29
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -14,45 +14,45 @@ package scan
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
charmlog "github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
const commonPorts = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/ports/top-ports.txt"
// commonPorts is a var so integration tests can repoint it at a fixture.
var commonPorts = "https://raw.githubusercontent.com/dropalldatabases/sif-runtime/main/ports/top-ports.txt"
func Ports(scope string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
log.Printf("%s", styles.Separator.Render("🚪 Starting "+styles.Status.Render("port scanning")+"..."))
func Ports(ctx context.Context, scope string, url string, timeout time.Duration, threads int, logdir string) ([]string, error) {
log := output.Module("PORTS")
log.Start()
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, scope+" port scanning"); err != nil {
log.Errorf("Error creating log file: %v", err)
log.Error("Error creating log file: %v", err)
return nil, err
}
}
portlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Ports 🚪",
})
portlog.Infof("Starting %s port scanning", scope)
var ports []int
switch scope {
case "common":
resp, err := http.Get(commonPorts)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, commonPorts, http.NoBody)
if err != nil {
log.Errorf("Error downloading ports list: %s", err)
log.Error("Error creating request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Error downloading ports list: %s", err)
return nil, err
}
defer resp.Body.Close()
@@ -70,9 +70,13 @@ func Ports(scope string, url string, timeout time.Duration, threads int, logdir
}
}
progress := output.NewProgress(len(ports), "scanning")
var openPorts []string
var mu sync.Mutex
var wg sync.WaitGroup
wg.Add(threads)
for thread := 0; thread < threads; thread++ {
go func(thread int) {
defer wg.Done()
@@ -82,25 +86,30 @@ func Ports(scope string, url string, timeout time.Duration, threads int, logdir
continue
}
log.Debugf("Looking up: %d", port)
tcp, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", sanitizedURL, port), timeout)
progress.Increment(strconv.Itoa(port))
charmlog.Debugf("Looking up: %d", port)
addr := fmt.Sprintf("%s:%d", sanitizedURL, port)
tcp, err := (&net.Dialer{Timeout: timeout}).DialContext(ctx, "tcp", addr)
if err != nil {
log.Debugf("Error %d: %v", port, err)
charmlog.Debugf("Error %d: %v", port, err)
} else {
progress.Pause()
log.Success("open: %s:%s [tcp]", sanitizedURL, output.Highlight.Render(strconv.Itoa(port)))
progress.Resume()
mu.Lock()
openPorts = append(openPorts, strconv.Itoa(port))
portlog.Infof("%s %s:%s", styles.Status.Render("[tcp]"), sanitizedURL, styles.Highlight.Render(strconv.Itoa(port)))
tcp.Close()
mu.Unlock()
_ = tcp.Close()
}
}
}(thread)
}
wg.Wait()
progress.Done()
if len(openPorts) > 0 {
portlog.Infof("Found %d open ports: %s", len(openPorts), strings.Join(openPorts, ", "))
} else {
portlog.Error("Found no open ports")
}
log.Complete(len(openPorts), "open")
return openPorts, nil
}
+62
View File
@@ -0,0 +1,62 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
// Named slice types for scan results.
// These provide better type safety and allow method implementations.
type (
HeaderResults []HeaderResult
SecurityHeaderResults []SecurityHeaderResult
DirectoryResults []DirectoryResult
CloudStorageResults []CloudStorageResult
DorkResults []DorkResult
SubdomainTakeoverResults []SubdomainTakeoverResult
)
// ScanResult is the interface that all scan result types implement.
type ScanResult interface {
// ResultType returns the unique identifier for this result type.
ResultType() string
}
// ResultType implementations for pointer result types.
func (r *ShodanResult) ResultType() string { return "shodan" }
func (r *SQLResult) ResultType() string { return "sql" }
func (r *LFIResult) ResultType() string { return "lfi" }
func (r *CMSResult) ResultType() string { return "cms" }
func (r *SecurityTrailsResult) ResultType() string { return "securitytrails" }
// ResultType implementations for slice result types.
func (r HeaderResults) ResultType() string { return "headers" }
func (r SecurityHeaderResults) ResultType() string { return "security_headers" }
func (r DirectoryResults) ResultType() string { return "dirlist" }
func (r CloudStorageResults) ResultType() string { return "cloudstorage" }
func (r DorkResults) ResultType() string { return "dork" }
func (r SubdomainTakeoverResults) ResultType() string { return "subdomain_takeover" }
// Compile-time interface satisfaction checks.
var (
_ ScanResult = (*ShodanResult)(nil)
_ ScanResult = (*SQLResult)(nil)
_ ScanResult = (*LFIResult)(nil)
_ ScanResult = (*CMSResult)(nil)
_ ScanResult = (*SecurityTrailsResult)(nil)
_ ScanResult = HeaderResults(nil)
_ ScanResult = SecurityHeaderResults(nil)
_ ScanResult = DirectoryResults(nil)
_ ScanResult = CloudStorageResults(nil)
_ ScanResult = DorkResults(nil)
_ ScanResult = SubdomainTakeoverResults(nil)
)
+34 -20
View File
@@ -4,7 +4,7 @@
: · Blazing-fast pentesting suite :
: · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (vmfunc), xyzeva, :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
··
@@ -19,21 +19,34 @@ package scan
import (
"bufio"
"fmt"
"context"
"net/http"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/styles"
"github.com/dropalldatabases/sif/pkg/logger"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
// stripScheme drops the scheme:// prefix from url, or returns it unchanged when
// there's no scheme (so a bare host doesn't panic).
func stripScheme(url string) string {
if _, rest, ok := strings.Cut(url, "://"); ok {
return rest
}
return url
}
func fetchRobotsTXT(url string, client *http.Client) *http.Response {
resp, err := client.Get(url)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
log.Debugf("Error creating request for robots.txt: %s", err)
return nil
}
resp, err := client.Do(req)
if err != nil {
log.Debugf("Error fetching robots.txt: %s", err)
return nil
@@ -61,21 +74,17 @@ func fetchRobotsTXT(url string, client *http.Client) *http.Response {
// - threads: number of concurrent threads to use
// - logdir: directory to store log files (empty string for no logging)
func Scan(url string, timeout time.Duration, threads int, logdir string) {
fmt.Println(styles.Separator.Render("🐾 Starting " + styles.Status.Render("base url scanning") + "..."))
output.ScanStart("base URL scanning")
sanitizedURL := strings.Split(url, "://")[1]
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "URL scanning"); err != nil {
log.Errorf("Error creating log file: %v", err)
output.Error("Error creating log file: %v", err)
return
}
}
scanlog := log.NewWithOptions(os.Stderr, log.Options{
Prefix: "Scan 👁️‍🗨️",
}).With("url", url)
client := &http.Client{
Timeout: timeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
@@ -90,7 +99,7 @@ func Scan(url string, timeout time.Duration, threads int, logdir string) {
defer resp.Body.Close()
if resp.StatusCode != 404 && resp.StatusCode != 301 && resp.StatusCode != 302 && resp.StatusCode != 307 {
scanlog.Infof("file [%s] found", styles.Status.Render("robots.txt"))
output.Success("File %s found", output.Status.Render("robots.txt"))
var robotsData []string
scanner := bufio.NewScanner(resp.Body)
@@ -115,20 +124,25 @@ func Scan(url string, timeout time.Duration, threads int, logdir string) {
}
_, sanitizedRobot, _ := strings.Cut(robot, ": ")
scanlog.Debugf("%s", robot)
resp, err := client.Get(url + "/" + sanitizedRobot)
log.Debugf("%s", robot)
robotReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+sanitizedRobot, http.NoBody)
if err != nil {
scanlog.Debugf("Error %s: %s", sanitizedRobot, err)
log.Debugf("Error creating request for %s: %s", sanitizedRobot, err)
continue
}
resp, err := client.Do(robotReq)
if err != nil {
log.Debugf("Error %s: %s", sanitizedRobot, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != 404 {
scanlog.Infof("%s from robots: [%s]", styles.Status.Render(strconv.Itoa(resp.StatusCode)), styles.Highlight.Render(sanitizedRobot))
output.Success("%s from robots: %s", output.Status.Render(strconv.Itoa(resp.StatusCode)), output.Highlight.Render(sanitizedRobot))
if logdir != "" {
logger.Write(sanitizedURL, logdir, fmt.Sprintf("%s from robots: [%s]\n", strconv.Itoa(resp.StatusCode), sanitizedRobot))
logger.Write(sanitizedURL, logdir, strconv.Itoa(resp.StatusCode)+" from robots: ["+sanitizedRobot+"]\n")
}
}
resp.Body.Close()
}
}(thread)
@@ -200,3 +200,24 @@ func TestHeaderResult(t *testing.T) {
t.Errorf("expected value 'application/json', got '%s'", result.Value)
}
}
func TestStripScheme(t *testing.T) {
tests := []struct {
name string
url string
want string
}{
{"https with path", "https://example.com/path", "example.com/path"},
{"http", "http://example.com", "example.com"},
{"no scheme stays put", "example.com", "example.com"},
{"empty", "", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := stripScheme(tt.url); got != tt.want {
t.Errorf("stripScheme(%q) = %q, want %q", tt.url, got, tt.want)
}
})
}
}
+162
View File
@@ -0,0 +1,162 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/output"
)
type SecurityHeaderResult struct {
Header string `json:"header"`
Present bool `json:"present"`
Value string `json:"value,omitempty"`
Severity string `json:"severity"`
Note string `json:"note"`
}
type recommendedHeader struct {
name string
severity string
}
var recommendedHeaders = []recommendedHeader{
{"Strict-Transport-Security", "high"},
{"Content-Security-Policy", "medium"},
{"X-Frame-Options", "medium"},
{"X-Content-Type-Options", "low"},
{"Referrer-Policy", "low"},
{"Permissions-Policy", "low"},
{"Cross-Origin-Opener-Policy", "low"},
}
// headers that leak server/framework details when present.
var disclosureHeaders = []string{"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version"}
const hstsMinMaxAge = 31536000 // a year, in seconds
func SecurityHeaders(url string, timeout time.Duration, logdir string) (SecurityHeaderResults, error) {
log := output.Module("SECHEADERS")
log.Start()
sanitizedURL := stripScheme(url)
if logdir != "" {
if err := logger.WriteHeader(sanitizedURL, logdir, "Security Header Analysis"); err != nil {
log.Error("Error creating log file: %v", err)
return nil, err
}
}
client := &http.Client{
Timeout: timeout,
}
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
results := gradeSecurityHeaders(resp.Header, strings.HasPrefix(url, "https://"))
for _, r := range results {
line := r.Header + " " + r.Note
log.Warn("%s [%s]", line, r.Severity)
if logdir != "" {
_ = logger.Write(sanitizedURL, logdir, line+" ["+r.Severity+"]\n")
}
}
if len(results) == 0 {
log.Success("all recommended security headers present")
}
log.Complete(len(results), "issues")
return results, nil
}
func gradeSecurityHeaders(header http.Header, https bool) SecurityHeaderResults {
var results SecurityHeaderResults
for _, h := range recommendedHeaders {
// hsts does nothing over plain http, so don't flag its absence there
if h.name == "Strict-Transport-Security" && !https {
continue
}
value := header.Get(h.name)
switch {
case value == "":
results = append(results, SecurityHeaderResult{
Header: h.name,
Severity: h.severity,
Note: "missing",
})
case h.name == "Strict-Transport-Security" && hstsMaxAge(value) < hstsMinMaxAge:
results = append(results, SecurityHeaderResult{
Header: h.name,
Present: true,
Value: value,
Severity: h.severity,
Note: "max-age too short",
})
case h.name == "X-Content-Type-Options" && !strings.EqualFold(value, "nosniff"):
results = append(results, SecurityHeaderResult{
Header: h.name,
Present: true,
Value: value,
Severity: "low",
Note: "should be nosniff",
})
}
}
for _, name := range disclosureHeaders {
if value := header.Get(name); value != "" {
results = append(results, SecurityHeaderResult{
Header: name,
Present: true,
Value: value,
Severity: "low",
Note: "discloses " + value,
})
}
}
return results
}
// hstsMaxAge returns the max-age seconds from an hsts value, or 0 if absent.
func hstsMaxAge(value string) int {
for _, part := range strings.Split(value, ";") {
if age, ok := strings.CutPrefix(strings.ToLower(strings.TrimSpace(part)), "max-age="); ok {
n, err := strconv.Atoi(strings.TrimSpace(age))
if err != nil {
return 0
}
return n
}
}
return 0
}
+154
View File
@@ -0,0 +1,154 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package scan
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func buildHeader(kv map[string]string) http.Header {
h := http.Header{}
for k, v := range kv {
h.Set(k, v)
}
return h
}
func findFinding(results SecurityHeaderResults, name string) (SecurityHeaderResult, bool) {
for _, r := range results {
if r.Header == name {
return r, true
}
}
return SecurityHeaderResult{}, false
}
func TestGradeSecurityHeaders_MissingOverHTTPS(t *testing.T) {
results := gradeSecurityHeaders(http.Header{}, true)
for _, h := range recommendedHeaders {
f, ok := findFinding(results, h.name)
if !ok {
t.Errorf("expected %s to be flagged", h.name)
continue
}
if f.Present {
t.Errorf("%s should not be marked present", h.name)
}
if f.Severity != h.severity {
t.Errorf("%s severity = %q, want %q", h.name, f.Severity, h.severity)
}
}
}
func TestGradeSecurityHeaders_HSTSSkippedOverHTTP(t *testing.T) {
results := gradeSecurityHeaders(http.Header{}, false)
if _, ok := findFinding(results, "Strict-Transport-Security"); ok {
t.Error("HSTS should only be graded for https targets")
}
}
func TestGradeSecurityHeaders_AllPresent(t *testing.T) {
h := buildHeader(map[string]string{
"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
"Content-Security-Policy": "default-src 'self'",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
"Referrer-Policy": "no-referrer",
"Permissions-Policy": "geolocation=()",
"Cross-Origin-Opener-Policy": "same-origin",
})
if results := gradeSecurityHeaders(h, true); len(results) != 0 {
t.Errorf("expected no findings, got %d: %+v", len(results), results)
}
}
func TestGradeSecurityHeaders_ContentTypeNotNosniff(t *testing.T) {
h := buildHeader(map[string]string{
"Strict-Transport-Security": "max-age=63072000",
"Content-Security-Policy": "default-src 'self'",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "sniff",
"Referrer-Policy": "no-referrer",
"Permissions-Policy": "geolocation=()",
"Cross-Origin-Opener-Policy": "same-origin",
})
f, ok := findFinding(gradeSecurityHeaders(h, true), "X-Content-Type-Options")
if !ok {
t.Fatal("expected X-Content-Type-Options to be flagged when not nosniff")
}
if !f.Present || f.Value != "sniff" {
t.Errorf("finding = %+v, want present with value sniff", f)
}
}
func TestGradeSecurityHeaders_WeakHSTS(t *testing.T) {
// max-age=0 actively disables hsts, so a present header still has to be flagged
h := buildHeader(map[string]string{"Strict-Transport-Security": "max-age=0"})
f, ok := findFinding(gradeSecurityHeaders(h, true), "Strict-Transport-Security")
if !ok {
t.Fatal("expected a short-lived hsts header to be flagged")
}
if !f.Present || f.Severity != "high" {
t.Errorf("finding = %+v, want present high", f)
}
}
func TestGradeSecurityHeaders_Disclosure(t *testing.T) {
h := buildHeader(map[string]string{
"Server": "Apache/2.4.1 (Ubuntu)",
"X-Powered-By": "PHP/8.1.2",
})
results := gradeSecurityHeaders(h, false)
for _, name := range []string{"Server", "X-Powered-By"} {
f, ok := findFinding(results, name)
if !ok {
t.Errorf("expected disclosure finding for %s", name)
continue
}
if !f.Present || f.Severity != "low" {
t.Errorf("%s finding = %+v, want present low", name, f)
}
}
}
func TestSecurityHeaders_LiveResponse(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
w.Header().Set("Server", "nginx/1.25.3")
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
results, err := SecurityHeaders(server.URL, 5*time.Second, "")
if err != nil {
t.Fatalf("SecurityHeaders returned error: %v", err)
}
if _, ok := findFinding(results, "X-Frame-Options"); ok {
t.Error("X-Frame-Options was set, should not be flagged")
}
if _, ok := findFinding(results, "Content-Security-Policy"); !ok {
t.Error("expected missing Content-Security-Policy to be flagged")
}
if _, ok := findFinding(results, "Server"); !ok {
t.Error("expected Server disclosure to be flagged")
}
}

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