mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 11:01:24 -07:00
320fc3d4e7
the yaml module engine (the user-facing extensibility surface) had 0% test coverage. add table-driven tests for the matcher types (status/word/regex + and/or + negative), checkWords/checkRegex (incl invalid-pattern fail-closed under AND, skip under OR), runExtractors (regex capture groups, group-index bounds, part selection), substituteVariables and generateHTTPRequests (path x payload expansion), and ParseYAMLModule on valid + malformed yaml. drive ExecuteHTTPModule end-to-end against an httptest server through the shared httpx client so matcher hits and extractor captures are exercised for real. coverage 0% -> 93.7%. also: ExecuteDNSModule/ExecuteTCPModule were stubs returning an empty result with nil error, so a type:dns/type:tcp module silently reported "0 findings" - indistinguishable from a real clean scan. make them return ErrUnsupportedModuleType (sentinel, wrapped with the module id) so the existing caller logs a clear failure instead. a test pins the new behavior. bodyclose is excluded for test files in .golangci.yml: the synthetic *http.Response fixtures carry no socket, mirroring the existing _test.go slack for errcheck/noctx/gosec.
270 lines
7.6 KiB
Go
270 lines
7.6 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package modules
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// writeModule drops a yaml file into a temp dir and returns its path.
|
|
func writeModule(t *testing.T, dir, name, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("write module: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestParseYAMLModuleValid(t *testing.T) {
|
|
const doc = `id: example-http
|
|
type: http
|
|
info:
|
|
name: Example
|
|
author: azzie
|
|
severity: medium
|
|
description: a test module
|
|
tags: [test, demo]
|
|
http:
|
|
method: GET
|
|
paths:
|
|
- "{{BaseURL}}/admin"
|
|
matchers:
|
|
- type: status
|
|
status: [200]
|
|
- type: word
|
|
part: body
|
|
words: ["admin"]
|
|
condition: and
|
|
extractors:
|
|
- type: regex
|
|
name: token
|
|
part: body
|
|
regex: ["token=(\\w+)"]
|
|
group: 1
|
|
`
|
|
dir := t.TempDir()
|
|
path := writeModule(t, dir, "ok.yaml", doc)
|
|
|
|
def, err := ParseYAMLModule(path)
|
|
if err != nil {
|
|
t.Fatalf("ParseYAMLModule: %v", err)
|
|
}
|
|
if def.ID != "example-http" {
|
|
t.Errorf("id = %q, want example-http", def.ID)
|
|
}
|
|
if def.Type != TypeHTTP {
|
|
t.Errorf("type = %q, want http", def.Type)
|
|
}
|
|
if def.Info.Severity != "medium" {
|
|
t.Errorf("severity = %q, want medium", def.Info.Severity)
|
|
}
|
|
if def.HTTP == nil {
|
|
t.Fatal("http config not parsed")
|
|
}
|
|
if len(def.HTTP.Matchers) != 2 {
|
|
t.Errorf("got %d matchers, want 2", len(def.HTTP.Matchers))
|
|
}
|
|
if len(def.HTTP.Extractors) != 1 || def.HTTP.Extractors[0].Group != 1 {
|
|
t.Errorf("extractor not parsed correctly: %+v", def.HTTP.Extractors)
|
|
}
|
|
if len(def.Info.Tags) != 2 {
|
|
t.Errorf("got %d tags, want 2", len(def.Info.Tags))
|
|
}
|
|
}
|
|
|
|
func TestParseYAMLModuleErrors(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
}{
|
|
{
|
|
name: "missing id",
|
|
content: "type: http\nhttp:\n paths: [\"/\"]\n",
|
|
},
|
|
{
|
|
name: "missing type",
|
|
content: "id: no-type\nhttp:\n paths: [\"/\"]\n",
|
|
},
|
|
{
|
|
name: "malformed yaml",
|
|
content: "id: bad\ntype: http\n paths: [unbalanced\n : nope\n",
|
|
},
|
|
{
|
|
// a scalar where a mapping is expected must fail to unmarshal.
|
|
name: "type mismatch",
|
|
content: "id: bad-shape\ntype: http\nhttp: \"should-be-a-map\"\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
path := writeModule(t, dir, tt.name+".yaml", tt.content)
|
|
if _, err := ParseYAMLModule(path); err == nil {
|
|
t.Fatalf("expected error for %s", tt.name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseYAMLModuleMissingFile(t *testing.T) {
|
|
if _, err := ParseYAMLModule(filepath.Join(t.TempDir(), "does-not-exist.yaml")); err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
}
|
|
|
|
func TestYAMLModuleWrapperInfoAndType(t *testing.T) {
|
|
def := &YAMLModule{
|
|
ID: "wrap-test",
|
|
Type: TypeHTTP,
|
|
Info: YAMLModuleInfo{
|
|
Name: "Wrapped",
|
|
Author: "azzie",
|
|
Severity: "low",
|
|
Description: "desc",
|
|
Tags: []string{"a", "b"},
|
|
},
|
|
}
|
|
w := newYAMLModuleWrapper(def, "wrap.yaml")
|
|
|
|
if w.Type() != TypeHTTP {
|
|
t.Errorf("Type() = %q, want http", w.Type())
|
|
}
|
|
info := w.Info()
|
|
if info.ID != "wrap-test" || info.Name != "Wrapped" || info.Severity != "low" {
|
|
t.Errorf("Info() mismatch: %+v", info)
|
|
}
|
|
if len(info.Tags) != 2 {
|
|
t.Errorf("Info().Tags = %v, want 2 entries", info.Tags)
|
|
}
|
|
}
|
|
|
|
// TestLoaderLoadAll exercises the directory walk: a valid module registers, a
|
|
// malformed one is skipped without aborting the walk.
|
|
func TestLoaderLoadAll(t *testing.T) {
|
|
Clear()
|
|
t.Cleanup(Clear)
|
|
|
|
dir := t.TempDir()
|
|
writeModule(t, dir, "good.yaml", "id: good-mod\ntype: http\nhttp:\n paths: [\"{{BaseURL}}/\"]\n matchers:\n - type: status\n status: [200]\n")
|
|
writeModule(t, dir, "bad.yml", "id: bad-mod\n") // missing type -> skipped
|
|
writeModule(t, dir, "ignore.txt", "not a module")
|
|
|
|
l := &Loader{builtinDir: dir, userDir: filepath.Join(dir, "nonexistent-user")}
|
|
if err := l.LoadAll(); err != nil {
|
|
t.Fatalf("LoadAll: %v", err)
|
|
}
|
|
|
|
// only the good module loads; the malformed one is logged and skipped.
|
|
if l.Loaded() != 1 {
|
|
t.Errorf("Loaded() = %d, want 1", l.Loaded())
|
|
}
|
|
if _, ok := Get("good-mod"); !ok {
|
|
t.Error("good-mod not registered")
|
|
}
|
|
if _, ok := Get("bad-mod"); ok {
|
|
t.Error("bad-mod should not have registered")
|
|
}
|
|
}
|
|
|
|
func TestNewLoaderDirs(t *testing.T) {
|
|
l, err := NewLoader()
|
|
if err != nil {
|
|
t.Fatalf("NewLoader: %v", err)
|
|
}
|
|
if l.BuiltinDir() == "" {
|
|
t.Error("BuiltinDir is empty")
|
|
}
|
|
if l.UserDir() == "" {
|
|
t.Error("UserDir is empty")
|
|
}
|
|
}
|
|
|
|
// TestRegistry exercises the package-level registry: register, get, dedupe by
|
|
// id, filter by tag and type, count and clear.
|
|
func TestRegistry(t *testing.T) {
|
|
Clear()
|
|
t.Cleanup(Clear)
|
|
|
|
http1 := newYAMLModuleWrapper(&YAMLModule{ID: "h1", Type: TypeHTTP, Info: YAMLModuleInfo{Tags: []string{"web", "cve"}}}, "h1")
|
|
http2 := newYAMLModuleWrapper(&YAMLModule{ID: "h2", Type: TypeHTTP, Info: YAMLModuleInfo{Tags: []string{"web"}}}, "h2")
|
|
dns1 := newYAMLModuleWrapper(&YAMLModule{ID: "d1", Type: TypeDNS, Info: YAMLModuleInfo{Tags: []string{"dns"}}}, "d1")
|
|
|
|
Register(http1)
|
|
Register(http2)
|
|
Register(dns1)
|
|
|
|
if Count() != 3 {
|
|
t.Fatalf("Count() = %d, want 3", Count())
|
|
}
|
|
|
|
got, ok := Get("h1")
|
|
if !ok || got.Info().ID != "h1" {
|
|
t.Errorf("Get(h1) = %v, %v", got, ok)
|
|
}
|
|
if _, ok := Get("missing"); ok {
|
|
t.Error("Get(missing) should report not found")
|
|
}
|
|
|
|
if n := len(ByType(TypeHTTP)); n != 2 {
|
|
t.Errorf("ByType(http) = %d, want 2", n)
|
|
}
|
|
if n := len(ByType(TypeDNS)); n != 1 {
|
|
t.Errorf("ByType(dns) = %d, want 1", n)
|
|
}
|
|
if n := len(ByTag("web")); n != 2 {
|
|
t.Errorf("ByTag(web) = %d, want 2", n)
|
|
}
|
|
if n := len(ByTag("cve")); n != 1 {
|
|
t.Errorf("ByTag(cve) = %d, want 1", n)
|
|
}
|
|
if n := len(ByTag("none")); n != 0 {
|
|
t.Errorf("ByTag(none) = %d, want 0", n)
|
|
}
|
|
if n := len(All()); n != 3 {
|
|
t.Errorf("All() = %d, want 3", n)
|
|
}
|
|
|
|
// re-registering the same id overwrites rather than duplicating.
|
|
Register(newYAMLModuleWrapper(&YAMLModule{ID: "h1", Type: TypeHTTP}, "h1-v2"))
|
|
if Count() != 3 {
|
|
t.Errorf("Count() after re-register = %d, want 3", Count())
|
|
}
|
|
|
|
Clear()
|
|
if Count() != 0 {
|
|
t.Errorf("Count() after Clear = %d, want 0", Count())
|
|
}
|
|
}
|
|
|
|
// TestResultType pins the ScanResult interface bridge.
|
|
func TestResultType(t *testing.T) {
|
|
r := &Result{ModuleID: "abc"}
|
|
if r.ResultType() != "abc" {
|
|
t.Errorf("ResultType() = %q, want abc", r.ResultType())
|
|
}
|
|
}
|
|
|
|
// TestLoaderScriptStubNoop confirms the go-script loader is currently a no-op
|
|
// that registers nothing and returns no error.
|
|
func TestLoaderScriptStubNoop(t *testing.T) {
|
|
l := &Loader{}
|
|
if err := l.loadScript("anything.go"); err != nil {
|
|
t.Errorf("loadScript stub returned error: %v", err)
|
|
}
|
|
}
|