From 356b16fc63d11f7f86b697e49759be5ffdbbd614 Mon Sep 17 00:00:00 2001 From: Tigah <88289044+TBX3D@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:51:20 -0700 Subject: [PATCH] feat(modules): embed built-in modules in the binary (#316) modules loaded only from an exe-adjacent modules/ dir or ~/.config, so a bare `go install`ed binary shipped zero modules. embed the modules/ tree and fall back to it when no filesystem builtin dir is found. the on-disk builtin dir and the user-override dir still take precedence, so a dev tree and a release that ships the folder alongside the binary keep working unchanged. the embed directive lives at the repo root because go:embed cannot reach a parent directory, and it registers the filesystem with the loader through a package hook to avoid an import cycle back into the root package. --- embed_modules.go | 40 +++++++++++++++++++++++++++ internal/modules/loader.go | 55 +++++++++++++++++++++++++++++++++++++- internal/modules/yaml.go | 5 ++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 embed_modules.go diff --git a/embed_modules.go b/embed_modules.go new file mode 100644 index 0000000..10b1340 --- /dev/null +++ b/embed_modules.go @@ -0,0 +1,40 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2026 vmfunc, xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package sif + +import ( + "embed" + "io/fs" + + "github.com/charmbracelet/log" + "github.com/vmfunc/sif/internal/modules" +) + +// builtinModules embeds the module tree into the binary. this file lives at the +// repo root because go:embed cannot reference a parent directory, and modules/ +// sits above internal/modules where the loader lives. +// +//go:embed modules +var builtinModules embed.FS + +// register the embedded modules with the loader once, rooted at the modules +// directory so paths match the on-disk layout. the loader only uses this as a +// fallback when no filesystem modules/ dir is present. +func init() { + sub, err := fs.Sub(builtinModules, "modules") + if err != nil { + log.Debugf("embedded modules unavailable: %v", err) + return + } + modules.SetBuiltinFS(sub) +} diff --git a/internal/modules/loader.go b/internal/modules/loader.go index 42906a6..24e5352 100644 --- a/internal/modules/loader.go +++ b/internal/modules/loader.go @@ -14,6 +14,7 @@ package modules import ( "fmt" + "io/fs" "os" "path/filepath" "runtime" @@ -22,10 +23,21 @@ import ( "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 } @@ -62,17 +74,28 @@ func NewLoader() (*Loader, error) { return &Loader{ builtinDir: builtinDir, userDir: userDir, + embedded: builtinFS, }, nil } // LoadAll discovers and loads all modules from both built-in // and user directories. func (l *Loader) LoadAll() error { - // Load built-in modules first + // 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 @@ -118,6 +141,36 @@ func (l *Loader) loadDir(dir string, userDefined bool) error { }) } +// 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) diff --git a/internal/modules/yaml.go b/internal/modules/yaml.go index f7f6170..7b8943f 100644 --- a/internal/modules/yaml.go +++ b/internal/modules/yaml.go @@ -90,7 +90,12 @@ func ParseYAMLModule(path string) (*YAMLModule, error) { if err != nil { return nil, fmt.Errorf("read module file: %w", err) } + return ParseYAMLModuleBytes(data) +} +// ParseYAMLModuleBytes parses and validates a module definition from raw bytes, +// so the loader can read modules from an embedded fs.FS as well as from disk. +func ParseYAMLModuleBytes(data []byte) (*YAMLModule, error) { var ym YAMLModule if err := yaml.Unmarshal(data, &ym); err != nil { return nil, fmt.Errorf("parse yaml: %w", err)