mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
refactor: share path-resolution and body-cap helpers (#355)
* refactor: share one helper for the per-user sif config directory the modules loader and the framework custom-signature loader each hand-rolled the same ~/.config/sif/<sub> (and %LocalAppData%\sif\<sub> on windows) path. extract internal/sifpath.UserSubdir and route both through it so the per-user layout is defined in one place. no behavior change: the helper reproduces the existing paths exactly. * refactor: read capped response bodies through one httpx helper the frameworks detector and the module executor each defined their own 5 MB body cap and ran the same io.ReadAll(io.LimitReader(...)) read. move the cap and the read into httpx.MaxBodySize / httpx.ReadCappedBody so both scanners share one ceiling instead of two consts that can drift. no behavior change: the cap value and read semantics are unchanged.
This commit is contained in:
@@ -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 {
|
||||
@@ -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.
|
||||
|
||||
@@ -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".
|
||||
@@ -422,7 +420,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
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/vmfunc/sif/internal/output"
|
||||
"github.com/vmfunc/sif/internal/sifpath"
|
||||
)
|
||||
|
||||
// builtinFS holds the modules embedded into the binary. it's set once, from the
|
||||
@@ -45,20 +45,15 @@ 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)
|
||||
}
|
||||
|
||||
// resolveBuiltinDir already probes the executable dir, the working
|
||||
// directory and the system data dirs, so the per-user lookup below is the
|
||||
// only thing sifpath needs to own here.
|
||||
builtinDir := resolveBuiltinDir()
|
||||
|
||||
// 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{
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -15,7 +15,6 @@ package frameworks
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -31,9 +30,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
|
||||
@@ -66,7 +62,7 @@ func gatherDetections(url string, timeout time.Duration) ([]detectionResult, str
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
|
||||
body, err := httpx.ReadCappedBody(resp)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user