Compare commits

..

10 Commits

Author SHA1 Message Date
vmfunc 0c9af6a23c 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 216b0cde3c 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 834879ec66 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 dd74fe1895 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 c8f189116d 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 0f72c5d744 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 68640a37e1 add SECURITY.md - fixes scorecard security-policy check
Signed-off-by: vmfunc <celeste@linux.com>
2026-02-13 01:57:31 +01:00
vmfunc 60431ce92d 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 dc1d33011d 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 7cc21d029b 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
46 changed files with 698 additions and 138 deletions
+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"
+5
View File
@@ -2,6 +2,11 @@ name: automatic rebase
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
jobs:
rebase:
name: Rebase
+3
View File
@@ -5,6 +5,9 @@ on:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+22 -11
View File
@@ -1,28 +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
- 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.24"
- name: initialize codeql
uses: github/codeql-action/init@v3
with:
languages: go
- name: build
run: go build ./...
- name: perform codeql analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:go"
+3
View File
@@ -6,6 +6,9 @@ on:
pull_request:
branches: ["main"]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+4 -1
View File
@@ -8,6 +8,9 @@ on:
schedule:
- cron: "0 6 * * 1" # monday 06:00 UTC
permissions:
contents: read
jobs:
govulncheck:
runs-on: ubuntu-latest
@@ -18,7 +21,7 @@ jobs:
with:
go-version: "1.24"
- name: install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- name: run govulncheck
run: govulncheck ./...
continue-on-error: true
+3
View File
@@ -8,6 +8,9 @@ on:
paths:
- '**.go'
permissions:
contents: read
jobs:
check-headers:
runs-on: ubuntu-latest
+6
View File
@@ -12,6 +12,12 @@ on:
types:
- created
- edited
permissions:
contents: read
issues: write
pull-requests: write
jobs:
echo_issue_comment:
runs-on: ubuntu-latest
+4
View File
@@ -5,6 +5,10 @@ on:
paths:
- "**/*.md"
permissions:
contents: read
pull-requests: write
jobs:
markdownlint:
name: runner / markdownlint
+4
View File
@@ -5,6 +5,10 @@ on:
push:
branches: [main]
permissions:
contents: read
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+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@v5
with:
configuration-path: .github/labeler.yml
size:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: label pr size
uses: actions/github-script@v7
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@v7
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,
});
}
+68 -35
View File
@@ -2,7 +2,8 @@ name: release
on:
push:
branches: [main]
tags:
- "v*"
permissions:
contents: write
@@ -24,21 +25,24 @@ jobs:
with:
go-version: "1.24"
- name: extract version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV
- name: build for windows
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
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-windows-amd64.exe ./cmd/sif
GOOS=windows GOARCH=386 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-windows-386.exe ./cmd/sif
- name: build for macOS
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=darwin GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-macos-amd64 ./cmd/sif
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-macos-arm64 ./cmd/sif
- name: build for linux
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=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-linux-amd64 ./cmd/sif
GOOS=linux GOARCH=386 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-linux-386 ./cmd/sif
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ env.VERSION }}" -o sif-linux-arm64 ./cmd/sif
- name: package releases with modules
run: |
@@ -57,8 +61,6 @@ jobs:
- name: build debian packages
run: |
VERSION="0.1.0-$(git rev-parse --short HEAD)"
declare -A arch_map=(
["sif-linux-amd64"]="amd64"
["sif-linux-386"]="i386"
@@ -67,7 +69,7 @@ jobs:
for binary in sif-linux-amd64 sif-linux-386 sif-linux-arm64; do
arch="${arch_map[$binary]}"
pkg_dir="sif_${VERSION}_${arch}"
pkg_dir="sif_${{ env.VERSION }}_${arch}"
mkdir -p "${pkg_dir}/DEBIAN"
mkdir -p "${pkg_dir}/usr/bin"
@@ -79,11 +81,11 @@ jobs:
cat > "${pkg_dir}/DEBIAN/control" << EOF
Package: sif
Version: ${VERSION}
Version: ${{ env.VERSION }}
Section: security
Priority: optional
Architecture: ${arch}
Maintainer: vmfunc <celeste@router.sex>
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.
@@ -113,41 +115,71 @@ jobs:
artifact-name: sbom.spdx.json
output-file: sbom.spdx.json
- name: set release version
run: echo "RELEASE_VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: generate changelog
id: changelog
uses: actions/github-script@v7
with:
result-encoding: string
script: |
const { data: releases } = await github.rest.repos.listReleases({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 1,
});
- name: create release and upload assets
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@v2
with:
tag_name: automated-release-${{ env.RELEASE_VERSION }}
name: Release ${{ env.RELEASE_VERSION }}
name: sif v${{ env.VERSION }}
body: |
Automated release v${{ env.RELEASE_VERSION }}
## what's changed
## Assets
${{ steps.changelog.outputs.result }}
Each archive contains the sif binary and built-in modules.
## install
- Windows (64-bit): `sif-windows-amd64.zip`
- Windows (32-bit): `sif-windows-386.zip`
- macOS (64-bit Intel): `sif-macos-amd64.tar.gz`
- macOS (64-bit ARM): `sif-macos-arm64.tar.gz`
- Linux (64-bit): `sif-linux-amd64.tar.gz`
- Linux (32-bit): `sif-linux-386.tar.gz`
- Linux (64-bit ARM): `sif-linux-arm64.tar.gz`
- Debian/Ubuntu (64-bit): `sif_*_amd64.deb`
- Debian/Ubuntu (32-bit): `sif_*_i386.deb`
- Debian/Ubuntu (64-bit ARM): `sif_*_arm64.deb`
**homebrew / linuxbrew**
```bash
# coming soon
```
## Verification
**debian / ubuntu**
```bash
sudo dpkg -i sif_${{ env.VERSION }}_amd64.deb
```
**go install**
```bash
go install github.com/dropalldatabases/sif/cmd/sif@v${{ env.VERSION }}
```
**binary download** - grab the right archive from below.
## verification
```bash
sha256sum -c checksums-sha256.txt
```
For more details, check the [commit history](https://github.com/${{ github.repository }}/commits/main).
draft: false
prerelease: false
prerelease: ${{ contains(github.ref_name, '-') }}
files: |
sif-windows-amd64.zip
sif-windows-386.zip
@@ -165,6 +197,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: push to cloudsmith
if: ${{ !contains(github.ref_name, '-') }}
env:
CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }}
run: |
+3
View File
@@ -7,6 +7,9 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
+3
View File
@@ -7,6 +7,9 @@ on:
branches: [main]
workflow_call:
permissions:
contents: read
jobs:
build-and-test:
runs-on: ubuntu-latest
+4
View File
@@ -5,6 +5,10 @@ on:
paths:
- "**/*.sh"
permissions:
contents: read
pull-requests: write
jobs:
shellcheck:
name: runner / shellcheck
+4
View File
@@ -6,6 +6,10 @@ on:
- "**/*.yml"
- "**/*.yaml"
permissions:
contents: read
pull-requests: write
jobs:
yamllint:
name: runner / yamllint
+23 -7
View File
@@ -10,12 +10,10 @@ linters:
- gocritic # opinionated lints
- revive # replacement for golint
- unconvert # unnecessary type conversions
- prealloc # slice preallocation hints
- bodyclose # http response body not closed
- noctx # http requests without context
- gosec # security issues
- errorlint # error wrapping and comparison
- gocognit # cognitive complexity
- nilnil # return nil, nil
- wastedassign # assignments to variables never read
- usetesting # os.Setenv in tests instead of t.Setenv, etc.
@@ -23,23 +21,35 @@ linters:
linters-settings:
govet:
enable-all: true
disable:
- fieldalignment # too many structs to reorder, risks breaking serialization
- shadow # common Go pattern, too noisy
- unusedwrite # false positives on test data structs
errcheck:
check-blank: false
exclude-functions:
- github.com/dropalldatabases/sif/internal/logger.Write # log writes are best-effort
revive:
rules:
- name: exported
arguments: [checkPrivateReceivers]
disabled: true # stuttering names (scan.ScanResult) require breaking API changes
gocritic:
enabled-tags:
- diagnostic
- style
- performance
disabled-checks:
- commentedOutCode # too opinionated for a project with TODO comments
- paramTypeCombine # style-only, not worth churn
- unnamedResult # style-only
- unnecessaryDefer # common pattern in tests
- nestingReduce # inverting conditions in scan logic hurts readability
gosec:
excludes:
- G104 # errcheck covers this
- G304 # sif reads user-supplied wordlist paths — intentional
gocognit:
min-complexity: 30
- G107 # pentesting tool -- variable URLs are the whole point
- G110 # nuclei template decompression, acceptable context
- G304 # sif reads user-supplied wordlist paths -- intentional
run:
timeout: 5m
@@ -47,4 +57,10 @@ run:
issues:
max-issues-per-linter: 50
max-same-issues: 3
max-same-issues: 50
exclude-rules:
# test files get some slack
- path: _test\.go
linters:
- errcheck
- noctx
+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.
+1 -1
View File
@@ -132,7 +132,7 @@ require (
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.16.2 // indirect
github.com/go-git/go-git/v5 v5.16.5 // indirect
github.com/go-ldap/ldap/v3 v3.4.11 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
+2 -2
View File
@@ -390,8 +390,8 @@ github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UN
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=
github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+2 -2
View File
@@ -37,7 +37,7 @@ var defaultLogger = &Logger{
// Init creates the log directory if it doesn't exist.
func Init(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err = os.Mkdir(dir, 0755); err != nil {
if err := os.Mkdir(dir, 0o755); err != nil {
return err
}
}
@@ -62,7 +62,7 @@ func (l *Logger) getWriter(path string) (*bufio.Writer, error) {
return w, nil
}
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil {
return nil, err
}
+5 -6
View File
@@ -228,9 +228,9 @@ func checkMatchers(matchers []Matcher, resp *http.Response, body string) bool {
}
// Default to AND condition across matchers
for _, m := range matchers {
matched := checkMatcher(m, resp, body)
if m.Negative {
for i := range matchers {
matched := checkMatcher(&matchers[i], resp, body)
if matchers[i].Negative {
matched = !matched
}
if !matched {
@@ -242,7 +242,7 @@ func checkMatchers(matchers []Matcher, resp *http.Response, body string) bool {
}
// checkMatcher evaluates a single matcher.
func checkMatcher(m Matcher, resp *http.Response, body string) bool {
func checkMatcher(m *Matcher, resp *http.Response, body string) bool {
part := getPart(m.Part, resp, body)
switch m.Type {
@@ -352,8 +352,7 @@ func runExtractors(extractors []Extractor, resp *http.Response, body string) map
for _, e := range extractors {
part := getPart(e.Part, resp, body)
switch e.Type {
case "regex":
if e.Type == "regex" {
for _, pattern := range e.Regex {
re, err := regexp.Compile(pattern)
if err != nil {
+10 -4
View File
@@ -15,6 +15,7 @@ package templates
import (
"archive/tar"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
@@ -37,7 +38,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
}
@@ -53,7 +59,7 @@ func Install(logger *log.Logger) error {
for {
header, err := data.Next()
if errors.Is(io.EOF, err) {
if errors.Is(err, io.EOF) {
break
}
if err != nil {
@@ -62,7 +68,7 @@ func Install(logger *log.Logger) error {
switch header.Typeflag {
case tar.TypeDir:
if err := os.Mkdir(header.Name, 0755); err != nil {
if err := os.Mkdir(header.Name, 0o755); err != nil {
return err
}
case tar.TypeReg:
@@ -77,7 +83,7 @@ func Install(logger *log.Logger) error {
}
}
if err = os.Rename(fmt.Sprintf("nuclei-templates-%s", ref), "nuclei-templates"); err != nil {
if err := os.Rename(fmt.Sprintf("nuclei-templates-%s", ref), "nuclei-templates"); err != nil {
return err
}
+4 -3
View File
@@ -138,11 +138,12 @@ func (p *Progress) render() {
bar := ""
for i := 0; i < progressWidth; i++ {
if i < filled {
switch {
case i < filled:
bar += progressFilled
} else if i == filled && current < total {
case i == filled && current < total:
bar += progressCurrent
} else {
default:
bar += progressEmpty
}
}
+11 -9
View File
@@ -13,6 +13,7 @@
package scan
import (
"context"
"fmt"
"net/http"
"os"
@@ -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)
@@ -85,22 +86,23 @@ func extractPotentialBuckets(url string) []string {
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
}
+14 -3
View File
@@ -13,6 +13,7 @@
package scan
import (
"context"
"io"
"net/http"
"strings"
@@ -48,7 +49,13 @@ func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
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
@@ -92,7 +99,7 @@ func CMS(url string, timeout time.Duration, logdir string) (*CMSResult, error) {
spin.Stop()
log.Info("No CMS detected")
log.Complete(0, "detected")
return nil, nil
return nil, nil //nolint:nilnil // no CMS found is not an error
}
func detectWordPress(url string, client *http.Client, bodyString string) bool {
@@ -118,7 +125,11 @@ 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 {
found := resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusFound
resp.Body.Close()
+15 -3
View File
@@ -14,6 +14,7 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"strconv"
@@ -62,7 +63,12 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
list = directoryURL + bigFile
}
resp, err := http.Get(list)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, list, http.NoBody)
if err != nil {
log.Error("Error creating directory list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Error downloading directory list: %s", err)
return nil, err
@@ -99,7 +105,12 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
progress.Increment(directory)
charmlog.Debugf("%s", directory)
resp, err := client.Get(url + "/" + directory)
dirReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+directory, http.NoBody)
if err != nil {
charmlog.Debugf("Error creating request for %s: %s", directory, err)
continue
}
resp, err := client.Do(dirReq)
if err != nil {
charmlog.Debugf("Error %s: %s", directory, err)
continue
@@ -111,7 +122,7 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
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{
@@ -122,6 +133,7 @@ func Dirlist(size string, url string, timeout time.Duration, threads int, logdir
results = append(results, result)
mu.Unlock()
}
resp.Body.Close()
}
}(thread)
}
+22 -4
View File
@@ -14,6 +14,7 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"strings"
@@ -47,7 +48,12 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
list = dnsURL + dnsBigFile
}
resp, err := http.Get(list)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, list, http.NoBody)
if err != nil {
log.Error("Error creating request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Error("Error downloading DNS list: %s", err)
return nil, err
@@ -95,13 +101,19 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
charmlog.Debugf("Looking up: %s", domain)
// Check HTTP
resp, err := client.Get("http://" + domain + "." + sanitizedURL)
httpReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "http://"+domain+"."+sanitizedURL, http.NoBody)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
continue
}
resp, err := client.Do(httpReq)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
mu.Unlock()
resp.Body.Close()
progress.Pause()
log.Success("found: %s.%s [http]", output.Highlight.Render(domain), sanitizedURL)
@@ -113,20 +125,26 @@ func Dnslist(size string, url string, timeout time.Duration, threads int, logdir
}
// Check HTTPS
resp, err = client.Get("https://" + domain + "." + sanitizedURL)
httpsReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "https://"+domain+"."+sanitizedURL, http.NoBody)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
continue
}
resp, err = client.Do(httpsReq)
if err != nil {
charmlog.Debugf("Error %s: %s", domain, err)
} else {
mu.Lock()
urls = append(urls, resp.Request.URL.String())
mu.Unlock()
resp.Body.Close()
progress.Pause()
log.Success("found: %s.%s [https]", output.Highlight.Render(domain), sanitizedURL)
progress.Resume()
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))
}
}
}
+11 -3
View File
@@ -17,6 +17,7 @@ package scan
import (
"bufio"
"context"
"fmt"
"net/http"
"strconv"
@@ -69,7 +70,14 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
}
}
resp, err := http.Get(dorkURL + dorkFile)
ctx := context.TODO()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, dorkURL+dorkFile, http.NoBody)
if err != nil {
spin.Stop()
output.Error("Error creating dork list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
output.Error("Error downloading dork list: %s", err)
@@ -98,7 +106,7 @@ 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 {
log.Debugf("error searching for dork %s: %v", dork, err)
continue
@@ -108,7 +116,7 @@ func Dork(url string, timeout time.Duration, threads int, logdir string) ([]Dork
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, strconv.Itoa(len(results))+" dork results found for dork ["+dork+"]\n")
_ = logger.Write(sanitizedURL, logdir, strconv.Itoa(len(results))+" dork results found for dork ["+dork+"]\n")
}
result := DorkResult{
+10 -4
View File
@@ -13,6 +13,7 @@
package frameworks
import (
"context"
"fmt"
"io"
"net/http"
@@ -47,7 +48,12 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
client := &http.Client{Timeout: timeout}
resp, err := client.Get(url)
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
@@ -66,7 +72,7 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
if len(detectors) == 0 {
spin.Stop()
log.Warn("No framework detectors registered")
return nil, nil
return nil, nil //nolint:nilnil // no detectors registered is not an error
}
// Run all detectors concurrently
@@ -105,7 +111,7 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
if best.confidence <= detectionThreshold {
log.Info("No framework detected with sufficient confidence")
log.Complete(0, "detected")
return nil, nil
return nil, nil //nolint:nilnil // no framework detected is not an error
}
// Get version match details
@@ -124,7 +130,7 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
logEntry += fmt.Sprintf(" CVEs: %v\n", cves)
logEntry += fmt.Sprintf(" Recommendations: %v\n", suggestions)
}
logger.Write(url, logdir, logEntry)
_ = logger.Write(url, logdir, logEntry)
}
log.Success("Detected %s framework (version: %s, confidence: %.2f)",
+46
View File
@@ -0,0 +1,46 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (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)
})
}
+1 -1
View File
@@ -202,7 +202,7 @@ func ExtractVersionOptimized(body string, framework string) VersionMatch {
// isValidVersionString checks if a version string looks like a valid semver
func isValidVersionString(v string) bool {
if len(v) == 0 || len(v) > 20 {
if v == "" || len(v) > 20 {
return false
}
+63
View File
@@ -0,0 +1,63 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2025 vmfunc (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)
}
})
}
+16 -2
View File
@@ -14,6 +14,7 @@ package scan
import (
"bufio"
"context"
"net/http"
"strconv"
"strings"
@@ -47,7 +48,13 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
}
}
resp, err := http.Get(gitURL + gitFile)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, gitURL+gitFile, http.NoBody)
if err != nil {
spin.Stop()
log.Error("Error creating git list request: %s", err)
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
log.Error("Error downloading git list: %s", err)
@@ -80,9 +87,15 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
}
charmlog.Debugf("%s", repourl)
resp, err := client.Get(url + "/" + repourl)
gitReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+repourl, http.NoBody)
if err != nil {
charmlog.Debugf("Error creating request for %s: %s", repourl, err)
continue
}
resp, err := client.Do(gitReq)
if err != nil {
charmlog.Debugf("Error %s: %s", repourl, err)
continue
}
if resp.StatusCode == 200 && !strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
@@ -95,6 +108,7 @@ func Git(url string, timeout time.Duration, threads int, logdir string) ([]strin
foundUrls = append(foundUrls, resp.Request.URL.String())
}
resp.Body.Close()
}
}(thread)
}
+6 -1
View File
@@ -13,6 +13,7 @@
package scan
import (
"context"
"net/http"
"strings"
"time"
@@ -43,7 +44,11 @@ func Headers(url string, timeout time.Duration, logdir string) ([]HeaderResult,
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
}
+8 -1
View File
@@ -24,6 +24,7 @@ package frameworks
import (
"bufio"
"context"
"fmt"
"net/http"
"regexp"
@@ -41,7 +42,13 @@ func GetPagesRouterScripts(scriptUrl string) ([]string, error) {
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
+13 -2
View File
@@ -14,6 +14,7 @@ package js
import (
"bufio"
"context"
"io"
"net/http"
"slices"
@@ -47,7 +48,12 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
spin.Stop()
return nil, err
}
resp, err := http.Get(url)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url, http.NoBody)
if err != nil {
spin.Stop()
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
spin.Stop()
return nil, err
@@ -109,7 +115,12 @@ func JavascriptScan(url string, timeout time.Duration, threads int, logdir strin
supabaseResults := make([]supabaseScanResult, 0, len(scripts))
for _, script := range scripts {
charmlog.Debugf("Scanning %s", script)
resp, err := http.Get(script)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, script, http.NoBody)
if err != nil {
charmlog.Warnf("Failed to create request: %s", err)
continue
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
charmlog.Warnf("Failed to fetch script: %s", err)
continue
+6 -5
View File
@@ -16,6 +16,7 @@ package js
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
@@ -70,7 +71,7 @@ type supabaseOpenAPIResponse struct {
// getSupabaseArrayResponse fetches a Supabase endpoint that returns an array.
func getSupabaseArrayResponse(projectId, path, apikey string, auth *string) (*supabaseArrayResponse, error) {
body, resp, err := doSupabaseRequest(projectId, path, apikey, auth)
body, resp, err := doSupabaseRequest(projectId, path, apikey, auth) //nolint:bodyclose // closed in doSupabaseRequest
if err != nil {
return nil, err
}
@@ -95,7 +96,7 @@ func getSupabaseArrayResponse(projectId, path, apikey string, auth *string) (*su
// getSupabaseOpenAPI fetches the OpenAPI spec from Supabase.
func getSupabaseOpenAPI(projectId, apikey string, auth *string) (*supabaseOpenAPIResponse, error) {
body, _, err := doSupabaseRequest(projectId, "/rest/v1/", apikey, auth)
body, _, err := doSupabaseRequest(projectId, "/rest/v1/", apikey, auth) //nolint:bodyclose // closed in doSupabaseRequest
if err != nil {
return nil, err
}
@@ -111,7 +112,7 @@ func getSupabaseOpenAPI(projectId, apikey string, auth *string) (*supabaseOpenAP
func doSupabaseRequest(projectId, path, apikey string, auth *string) ([]byte, *http.Response, error) {
client := http.Client{}
req, err := http.NewRequest("GET", "https://"+projectId+".supabase.co"+path, nil)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "https://"+projectId+".supabase.co"+path, http.NoBody)
if err != nil {
return nil, nil, err
}
@@ -170,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
@@ -183,7 +184,7 @@ 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{}
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
+8 -2
View File
@@ -13,6 +13,7 @@
package scan
import (
"context"
"fmt"
"io"
"net/http"
@@ -214,7 +215,12 @@ 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 {
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
@@ -294,7 +300,7 @@ func LFI(targetURL string, timeout time.Duration, threads int, logdir string) (*
} else {
log.Info("No LFI vulnerabilities detected")
log.Complete(0, "found")
return nil, nil
return nil, nil //nolint:nilnil // no LFI found is not an error
}
return result, nil
+1 -1
View File
@@ -37,7 +37,7 @@ func Nuclei(url string, timeout time.Duration, threads int, logdir string) ([]ou
Prefix: "nuclei",
}).With("url", url)
templates.Install(nucleilog)
_ = templates.Install(nucleilog)
pwd, err := os.Getwd()
if err != nil {
spin.Stop()
+7 -1
View File
@@ -14,6 +14,7 @@ package scan
import (
"bufio"
"context"
"fmt"
"net"
"net/http"
@@ -44,7 +45,12 @@ func Ports(scope string, url string, timeout time.Duration, threads int, logdir
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.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
+13 -2
View File
@@ -19,6 +19,7 @@ package scan
import (
"bufio"
"context"
"net/http"
"strconv"
"strings"
@@ -31,7 +32,12 @@ import (
)
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
@@ -110,7 +116,12 @@ func Scan(url string, timeout time.Duration, threads int, logdir string) {
_, sanitizedRobot, _ := strings.Cut(robot, ": ")
log.Debugf("%s", robot)
resp, err := client.Get(url + "/" + sanitizedRobot)
robotReq, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, url+"/"+sanitizedRobot, http.NoBody)
if err != nil {
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
+13 -8
View File
@@ -13,6 +13,7 @@
package scan
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -182,7 +183,11 @@ func queryShodanHost(ip string, apiKey string, timeout time.Duration) (*ShodanRe
client := &http.Client{Timeout: timeout}
reqURL := fmt.Sprintf("%s/shodan/host/%s?key=%s", shodanBaseURL, ip, apiKey)
resp, err := client.Get(reqURL)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, reqURL, http.NoBody)
if err != nil {
return nil, fmt.Errorf("failed to create Shodan request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to query Shodan: %w", err)
}
@@ -232,14 +237,14 @@ func queryShodanHost(ip string, apiKey string, timeout time.Duration) (*ShodanRe
Services: make([]ShodanService, 0, len(shodanResp.Data)),
}
for _, data := range shodanResp.Data {
for i := range shodanResp.Data {
service := ShodanService{
Port: data.Port,
Protocol: data.Transport,
Product: data.Product,
Version: data.Version,
Banner: truncateBanner(data.Data, 200),
Module: data.Shodan.Module,
Port: shodanResp.Data[i].Port,
Protocol: shodanResp.Data[i].Transport,
Product: shodanResp.Data[i].Product,
Version: shodanResp.Data[i].Version,
Banner: truncateBanner(shodanResp.Data[i].Data, 200),
Module: shodanResp.Data[i].Shodan.Module,
}
result.Services = append(result.Services, service)
}
+13 -3
View File
@@ -13,6 +13,7 @@
package scan
import (
"context"
"io"
"net/http"
"regexp"
@@ -164,7 +165,12 @@ func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*
adminPath := sqlAdminPaths[idx]
checkURL := strings.TrimSuffix(targetURL, "/") + adminPath.path
resp, err := client.Get(checkURL)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, checkURL, http.NoBody)
if err != nil {
charmlog.Debugf("Error creating request for %s: %v", checkURL, err)
continue
}
resp, err := client.Do(req)
if err != nil {
charmlog.Debugf("Error checking %s: %v", checkURL, err)
continue
@@ -243,7 +249,7 @@ func SQL(targetURL string, timeout time.Duration, threads int, logdir string) (*
if totalFindings == 0 {
log.Info("No SQL exposures found")
log.Complete(0, "found")
return nil, nil
return nil, nil //nolint:nilnil // no SQLi found is not an error
}
log.Complete(totalFindings, "found")
@@ -285,7 +291,11 @@ func isAdminPanel(body string, panelType string) bool {
}
func checkDatabaseErrors(client *http.Client, checkURL, sanitizedURL string, result *SQLResult, logdir string, mu *sync.Mutex, seen map[string]bool) {
resp, err := client.Get(checkURL)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, checkURL, http.NoBody)
if err != nil {
return
}
resp, err := client.Do(req)
if err != nil {
return
}
+10 -4
View File
@@ -13,10 +13,8 @@
package scan
import (
"context"
"fmt"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
"io"
"net"
"net/http"
@@ -24,6 +22,10 @@ import (
"strings"
"sync"
"time"
"github.com/charmbracelet/log"
"github.com/dropalldatabases/sif/internal/logger"
"github.com/dropalldatabases/sif/internal/styles"
)
// SubdomainTakeoverResult represents the outcome of a subdomain takeover vulnerability check.
@@ -114,7 +116,11 @@ func SubdomainTakeover(url string, dnsResults []string, timeout time.Duration, t
}
func checkSubdomainTakeover(subdomain string, client *http.Client) (bool, string) {
resp, err := client.Get("http://" + subdomain)
req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, "http://"+subdomain, http.NoBody)
if err != nil {
return false, ""
}
resp, err := client.Do(req)
if err != nil {
if strings.Contains(err.Error(), "no such host") {
// Check if CNAME exists
+10 -7
View File
@@ -86,9 +86,10 @@ func New(settings *config.Settings) (*App, error) {
return app, nil
}
if len(settings.URLs) > 0 {
switch {
case len(settings.URLs) > 0:
app.targets = settings.URLs
} else if settings.File != "" {
case settings.File != "":
if _, err := os.Stat(settings.File); err != nil {
return nil, err
}
@@ -104,7 +105,7 @@ func New(settings *config.Settings) (*App, error) {
for scanner.Scan() {
app.targets = append(app.targets, scanner.Text())
}
} else {
default:
return nil, fmt.Errorf("target(s) must be supplied with -u or -f\n\nSee 'sif -h' for more information")
}
@@ -362,13 +363,14 @@ func (app *App) Run() error {
// Determine which modules to run
var toRun []modules.Module
if app.settings.AllModules {
switch {
case app.settings.AllModules:
toRun = modules.All()
} else if app.settings.ModuleTags != "" {
case app.settings.ModuleTags != "":
for _, tag := range strings.Split(app.settings.ModuleTags, ",") {
toRun = append(toRun, modules.ByTag(strings.TrimSpace(tag))...)
}
} else if app.settings.Modules != "" {
case app.settings.Modules != "":
for _, id := range strings.Split(app.settings.Modules, ",") {
if m, ok := modules.Get(strings.TrimSpace(id)); ok {
toRun = append(toRun, m)
@@ -411,7 +413,8 @@ func (app *App) Run() error {
marshalled, err := json.Marshal(result)
if err != nil {
log.Fatalf("failed to marshal result: %s", err)
log.Errorf("failed to marshal result: %s", err)
continue
}
fmt.Println(string(marshalled))
}