Files
sif/internal/modules/loader.go
T
TigahandGitHub f6db61b0ef 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.
2026-07-22 15:48:49 -07:00

239 lines
7.2 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package modules
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"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
// repo-root package that can actually run the go:embed directive (go:embed can't
// reach a parent directory, and modules/ sits above this package). it stays nil
// in builds and tests that don't import that package, so the loader simply falls
// back to the filesystem as before.
var builtinFS fs.FS
// SetBuiltinFS registers the embedded module filesystem. see builtinFS.
func SetBuiltinFS(fsys fs.FS) { builtinFS = fsys }
// Loader handles module discovery and loading.
type Loader struct {
builtinDir string
userDir string
embedded fs.FS
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) {
// 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 (can override built-ins)
userDir, err := sifpath.UserSubdir("modules")
if err != nil {
return nil, fmt.Errorf("resolve user modules dir: %w", err)
}
return &Loader{
builtinDir: builtinDir,
userDir: userDir,
embedded: builtinFS,
}, nil
}
// resolveBuiltinDir picks the built-in modules directory: the first existing
// candidate, or the working-directory default when none are present (LoadAll
// then logs "no built-in modules found" as before).
func resolveBuiltinDir() string {
if dir := firstExistingDir(builtinDirCandidates()); dir != "" {
return dir
}
return "modules"
}
// builtinDirCandidates lists the directories to probe for built-in modules,
// most specific first: next to the executable, the working directory (for
// development), then the freedesktop system data dirs so packaged installs
// (modules under /usr/share/sif) are found too.
func builtinDirCandidates() []string {
candidates := make([]string, 0, 4)
if execPath, err := os.Executable(); err == nil {
candidates = append(candidates, filepath.Join(filepath.Dir(execPath), "modules"))
}
candidates = append(candidates, "modules")
for _, dir := range dataDirs() {
candidates = append(candidates, filepath.Join(dir, "sif", "modules"))
}
return candidates
}
// dataDirs returns the freedesktop base data directories, honoring
// $XDG_DATA_DIRS and falling back to the spec default when it is unset.
func dataDirs() []string {
if env := os.Getenv("XDG_DATA_DIRS"); env != "" {
return filepath.SplitList(env)
}
return []string{"/usr/local/share", "/usr/share"}
}
func firstExistingDir(candidates []string) string {
for _, dir := range candidates {
if info, err := os.Stat(dir); err == nil && info.IsDir() {
return dir
}
}
return ""
}
// LoadAll discovers and loads all modules from both built-in
// and user directories.
func (l *Loader) LoadAll() error {
// Load built-in modules first, preferring an on-disk modules/ dir (dev tree
// or a release that ships the folder alongside the binary).
before := l.loaded
if err := l.loadDir(l.builtinDir, false); err != nil {
log.Debugf("No built-in modules found: %v", err)
}
// nothing on disk: fall back to the modules embedded in the binary so a bare
// `go install`ed sif still ships its built-in modules.
if l.loaded == before && l.embedded != nil {
if err := l.loadFS(l.embedded); err != nil {
log.Debugf("No embedded modules loaded: %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
})
}
// loadFS loads yaml modules from an embedded filesystem. only yaml is embedded
// (the .go script path is a filesystem-only dev affordance), so this walks for
// yaml files and parses them from bytes.
func (l *Loader) loadFS(fsys fs.FS) error {
return fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
switch filepath.Ext(path) {
case ".yaml", ".yml":
data, rerr := fs.ReadFile(fsys, path)
if rerr != nil {
log.Warnf("Failed to read embedded module %s: %v", path, rerr)
return nil
}
def, perr := ParseYAMLModuleBytes(data)
if perr != nil {
log.Warnf("Failed to load embedded module %s: %v", path, perr)
return nil
}
Register(newYAMLModuleWrapper(def, path))
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
}