Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/httpx/httpx.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ const (
// conn, so we cap the read and let the conn be discarded instead.
const drainCap = 16 << 10

// MaxBodySize is the shared ceiling on how much of a response body the scanners
// read into memory, so a hostile or accidental multi-gigabyte response can't
// exhaust the process.
const MaxBodySize = 5 << 20

// Options carries the runtime knobs that apply to every outbound request.
// RateLimit is requests/sec (0 = unlimited); Headers are "Key: Value" strings.
type Options struct {
Expand Down Expand Up @@ -213,6 +218,13 @@ func DrainClose(resp *http.Response) {
resp.Body.Close()
}

// ReadCappedBody reads resp.Body up to MaxBodySize, the shared cap every scanner
// uses so one runaway response can't exhaust memory. It does not close the body;
// pair it with DrainClose or an explicit Close.
func ReadCappedBody(resp *http.Response) ([]byte, error) {
return io.ReadAll(io.LimitReader(resp.Body, MaxBodySize))
}

// parseHeaders splits each "Key: Value" entry on the first ": ". Entries
// without the separator are rejected so a typo fails loud instead of silently.
// The returned map is always non-nil so callers can range it unconditionally.
Expand Down
6 changes: 2 additions & 4 deletions internal/modules/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,9 @@ import (
"time"

"github.com/tidwall/gjson"
"github.com/vmfunc/sif/internal/httpx"
)

// MaxBodySize limits response body to prevent memory exhaustion.
const MaxBodySize = 5 * 1024 * 1024

// ErrUnsupportedModuleType signals an executor for a module type that is not
// yet implemented. Returning it (rather than an empty result) keeps callers
// from mistaking "not implemented" for "scanned, found nothing".
Expand Down Expand Up @@ -297,7 +295,7 @@ func executeHTTPRequest(ctx context.Context, client *http.Client, r *httpRequest
defer resp.Body.Close()

// Read body with limit
respBody, err := io.ReadAll(io.LimitReader(resp.Body, MaxBodySize))
respBody, err := httpx.ReadCappedBody(resp)
if err != nil {
return Finding{}, false
}
Expand Down
18 changes: 5 additions & 13 deletions internal/modules/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"

"github.com/charmbracelet/log"
"github.com/vmfunc/sif/internal/output"
"github.com/vmfunc/sif/internal/sifpath"
)

// Loader handles module discovery and loading.
Expand All @@ -33,11 +33,6 @@ type Loader struct {
// 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 {
Expand All @@ -50,13 +45,10 @@ func NewLoader() (*Loader, error) {
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")
// User modules directory (can override built-ins)
userDir, err := sifpath.UserSubdir("modules")
if err != nil {
return nil, fmt.Errorf("resolve user modules dir: %w", err)
}

return &Loader{
Expand Down
11 changes: 2 additions & 9 deletions internal/scan/frameworks/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"strings"

charmlog "github.com/charmbracelet/log"
"github.com/vmfunc/sif/internal/output"
"github.com/vmfunc/sif/internal/sifpath"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -129,14 +129,7 @@ func parseCustomDetector(path string) (Detector, error) {
// customSignaturesDir is the per-user directory that holds yaml-defined
// detectors, alongside the user modules directory.
func customSignaturesDir() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
if runtime.GOOS == "windows" {
return filepath.Join(home, "AppData", "Local", "sif", "signatures"), nil
}
return filepath.Join(home, ".config", "sif", "signatures"), nil
return sifpath.UserSubdir("signatures")
}

// loadCustomDetectors registers every signature file under the user directory.
Expand Down
6 changes: 1 addition & 5 deletions internal/scan/frameworks/detect.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ package frameworks
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"sync"
Expand All @@ -30,9 +29,6 @@ import (
// 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
Expand Down Expand Up @@ -68,7 +64,7 @@ func DetectFramework(url string, timeout time.Duration, logdir string) (*Framewo
}
defer resp.Body.Close()

body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
body, err := httpx.ReadCappedBody(resp)
if err != nil {
spin.Stop()
return nil, err
Expand Down
36 changes: 36 additions & 0 deletions internal/sifpath/sifpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

// Package sifpath resolves the per-user sif directories so every subsystem
// agrees on where user-supplied files live.
package sifpath

import (
"os"
"path/filepath"
"runtime"
)

// UserSubdir returns the per-user sif configuration subdirectory for name (for
// example "modules" or "signatures"). It preserves sif's historical layout:
// ~/.config/sif/<name> on unix-like systems and %LOCALAPPDATA%\sif\<name> on
// windows.
func UserSubdir(name string) (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
if runtime.GOOS == "windows" {
return filepath.Join(home, "AppData", "Local", "sif", name), nil
}
return filepath.Join(home, ".config", "sif", name), nil
}
48 changes: 48 additions & 0 deletions internal/sifpath/sifpath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/

package sifpath

import (
"path/filepath"
"testing"
)

func TestUserSubdirLayout(t *testing.T) {
got, err := UserSubdir("modules")
if err != nil {
t.Fatalf("UserSubdir returned error: %v", err)
}
if !filepath.IsAbs(got) {
t.Errorf("UserSubdir(%q) = %q, want an absolute path", "modules", got)
}
if base := filepath.Base(got); base != "modules" {
t.Errorf("UserSubdir(%q) base = %q, want %q", "modules", base, "modules")
}
if parent := filepath.Base(filepath.Dir(got)); parent != "sif" {
t.Errorf("UserSubdir(%q) parent = %q, want %q", "modules", parent, "sif")
}
}

func TestUserSubdirSiblings(t *testing.T) {
mods, err := UserSubdir("modules")
if err != nil {
t.Fatalf("UserSubdir(modules): %v", err)
}
sigs, err := UserSubdir("signatures")
if err != nil {
t.Fatalf("UserSubdir(signatures): %v", err)
}
if filepath.Dir(mods) != filepath.Dir(sigs) {
t.Errorf("modules dir %q and signatures dir %q should share a parent", mods, sigs)
}
}
Loading