From 04da73b79c4751e01df8b6fa723f120203cee41b Mon Sep 17 00:00:00 2001 From: Celeste Hickenlooper Date: Sat, 3 Jan 2026 00:48:29 -0800 Subject: [PATCH] feat: add module system infrastructure --- internal/modules/loader.go | 144 +++++++++++++++++++++++++++++++++++ internal/modules/module.go | 106 ++++++++++++++++++++++++++ internal/modules/registry.go | 92 ++++++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 internal/modules/loader.go create mode 100644 internal/modules/module.go create mode 100644 internal/modules/registry.go diff --git a/internal/modules/loader.go b/internal/modules/loader.go new file mode 100644 index 0000000..a076ebe --- /dev/null +++ b/internal/modules/loader.go @@ -0,0 +1,144 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2025 vmfunc (Celeste Hickenlooper), xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package modules + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/charmbracelet/log" +) + +// Loader handles module discovery and loading. +type Loader struct { + builtinDir string + userDir string + 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) + } + + // Find built-in modules relative to executable + execPath, err := os.Executable() + if err != nil { + execPath = "." + } + builtinDir := filepath.Join(filepath.Dir(execPath), "modules") + + // Also check current working directory for development + if _, err := os.Stat(builtinDir); os.IsNotExist(err) { + 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") + } + + return &Loader{ + builtinDir: builtinDir, + userDir: userDir, + }, nil +} + +// LoadAll discovers and loads all modules from both built-in +// and user directories. +func (l *Loader) LoadAll() error { + // Load built-in modules first + if err := l.loadDir(l.builtinDir, false); err != nil { + log.Debugf("No built-in modules found: %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) + } + } + + log.Debugf("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 + }) +} + +// loadYAML loads a YAML module definition. +// Implementation will be provided in yaml.go. +func (l *Loader) loadYAML(path string) error { + // Will be implemented in yaml.go + 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 +} diff --git a/internal/modules/module.go b/internal/modules/module.go new file mode 100644 index 0000000..39be5c1 --- /dev/null +++ b/internal/modules/module.go @@ -0,0 +1,106 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2025 vmfunc (Celeste Hickenlooper), xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +// Package modules provides the module system infrastructure for SIF. +// It defines the core interfaces, types, and utilities for building +// and executing security scanning modules. +package modules + +import ( + "context" + "net/http" + "time" +) + +// ModuleType represents the type of module. +type ModuleType string + +const ( + TypeHTTP ModuleType = "http" + TypeDNS ModuleType = "dns" + TypeTCP ModuleType = "tcp" + TypeScript ModuleType = "script" +) + +// Module is the interface all modules implement. +// Each module must provide metadata, specify its type, and implement +// an Execute method for running the scan against a target. +type Module interface { + // Info returns the module metadata. + Info() Info + + // Type returns the module type (http, dns, tcp, script). + Type() ModuleType + + // Execute runs the module against the specified target. + Execute(ctx context.Context, target string, opts Options) (*Result, error) +} + +// Info contains module metadata. +type Info struct { + ID string `yaml:"id" json:"id"` + Name string `yaml:"name" json:"name"` + Author string `yaml:"author" json:"author"` + Severity string `yaml:"severity" json:"severity"` + Description string `yaml:"description" json:"description"` + Tags []string `yaml:"tags" json:"tags"` +} + +// Options for module execution. +type Options struct { + Timeout time.Duration + Threads int + LogDir string + Client *http.Client +} + +// Result from module execution. +type Result struct { + ModuleID string `json:"module_id"` + Target string `json:"target"` + Findings []Finding `json:"findings,omitempty"` +} + +// ResultType implements the ScanResult interface from pkg/scan. +func (r *Result) ResultType() string { + return r.ModuleID +} + +// Finding represents a discovered issue. +type Finding struct { + URL string `json:"url,omitempty"` + Severity string `json:"severity"` + Evidence string `json:"evidence,omitempty"` + Extracted map[string]string `json:"extracted,omitempty"` +} + +// Matcher defines matching logic for module responses. +// Matchers are used to determine if a response indicates a vulnerability. +type Matcher struct { + Type string `yaml:"type"` // regex, status, word, size + Part string `yaml:"part"` // body, header, all + Regex []string `yaml:"regex,omitempty"` + Words []string `yaml:"words,omitempty"` + Status []int `yaml:"status,omitempty"` + Condition string `yaml:"condition"` // and, or + Negative bool `yaml:"negative"` +} + +// Extractor defines data extraction from responses. +// Extractors pull specific data from matched responses for reporting. +type Extractor struct { + Type string `yaml:"type"` // regex, kval, json + Name string `yaml:"name"` + Part string `yaml:"part"` + Regex []string `yaml:"regex,omitempty"` + Group int `yaml:"group"` +} diff --git a/internal/modules/registry.go b/internal/modules/registry.go new file mode 100644 index 0000000..577b66d --- /dev/null +++ b/internal/modules/registry.go @@ -0,0 +1,92 @@ +/* +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +: : +: █▀ █ █▀▀ · Blazing-fast pentesting suite : +: ▄█ █ █▀ · BSD 3-Clause License : +: : +: (c) 2022-2025 vmfunc (Celeste Hickenlooper), xyzeva, : +: lunchcat alumni & contributors : +: : +·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━· +*/ + +package modules + +import "sync" + +var ( + registry = make(map[string]Module) + mu sync.RWMutex +) + +// Register adds a module to the registry. +// If a module with the same ID already exists, it will be overwritten. +func Register(m Module) { + mu.Lock() + defer mu.Unlock() + registry[m.Info().ID] = m +} + +// Get returns a module by ID. +// The second return value indicates whether the module was found. +func Get(id string) (Module, bool) { + mu.RLock() + defer mu.RUnlock() + m, ok := registry[id] + return m, ok +} + +// All returns all registered modules. +func All() []Module { + mu.RLock() + defer mu.RUnlock() + result := make([]Module, 0, len(registry)) + for _, m := range registry { + result = append(result, m) + } + return result +} + +// ByTag returns modules matching a tag. +func ByTag(tag string) []Module { + mu.RLock() + defer mu.RUnlock() + var result []Module + for _, m := range registry { + for _, t := range m.Info().Tags { + if t == tag { + result = append(result, m) + break + } + } + } + return result +} + +// ByType returns modules of a specific type. +func ByType(t ModuleType) []Module { + mu.RLock() + defer mu.RUnlock() + var result []Module + for _, m := range registry { + if m.Type() == t { + result = append(result, m) + } + } + return result +} + +// Count returns the number of registered modules. +func Count() int { + mu.RLock() + defer mu.RUnlock() + return len(registry) +} + +// Clear removes all modules from the registry. +// This is primarily useful for testing. +func Clear() { + mu.Lock() + defer mu.Unlock() + registry = make(map[string]Module) +}