/* ·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· : : : █▀ █ █▀▀ · 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" "runtime" "github.com/charmbracelet/log" "github.com/vmfunc/sif/internal/output" ) // 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) { home, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("get home dir: %w", err) } 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") } 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 }