feat: add module system infrastructure

This commit is contained in:
Celeste Hickenlooper
2026-01-03 00:48:29 -08:00
parent 3ae61080fe
commit 04da73b79c
3 changed files with 342 additions and 0 deletions

144
internal/modules/loader.go Normal file
View File

@@ -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
}

106
internal/modules/module.go Normal file
View File

@@ -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"`
}

View File

@@ -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)
}