mirror of
https://github.com/lunchcat/sif.git
synced 2026-01-13 05:16:44 -08:00
feat: add module system infrastructure
This commit is contained in:
144
internal/modules/loader.go
Normal file
144
internal/modules/loader.go
Normal 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
106
internal/modules/module.go
Normal 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"`
|
||||
}
|
||||
92
internal/modules/registry.go
Normal file
92
internal/modules/registry.go
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user