mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-28 14:37:01 -07:00
* refactor(config): extract registerFlags from Parse split flag registration into a registerFlags helper so callers, including tests, can build and inspect the flag set without parsing os.Args. pure move, no behavior change. * feat(config): add scan templates via -template wire up the existing -template flag to load a batch of scan settings from a built-in preset or a local yaml file, so a run does not have to pass every flag by hand. a value is a named preset (minimal, recon, full) embedded in the binary, or a path to a local yaml file keyed by flag long-names. it merges as the goflags config before parsing, so command-line flags still win and it replaces the ambient config for that run. implements #5.
212 lines
6.5 KiB
Go
212 lines
6.5 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package config
|
|
|
|
import (
|
|
"flag"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/projectdiscovery/goflags"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// writeTemplate drops a yaml template in a temp dir and returns its path.
|
|
func writeTemplate(t *testing.T, body string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "tmpl.yaml")
|
|
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
|
|
t.Fatalf("write template: %s", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
// loadPreset registers the real flags, merges the named embedded preset, and
|
|
// returns the resulting settings (no cli scan flags set).
|
|
func loadPreset(t *testing.T, name string) *Settings {
|
|
t.Helper()
|
|
goflags.DisableAutoConfigMigration = true
|
|
path, cleanup, err := resolveTemplate(name)
|
|
if err != nil {
|
|
t.Fatalf("resolve %s: %s", name, err)
|
|
}
|
|
if cleanup != nil {
|
|
defer cleanup()
|
|
}
|
|
settings := &Settings{}
|
|
flagSet := registerFlags(settings)
|
|
flagSet.SetConfigFilePath(path)
|
|
if err := flagSet.Parse("-silent"); err != nil {
|
|
t.Fatalf("parse %s: %s", name, err)
|
|
}
|
|
return settings
|
|
}
|
|
|
|
// every key in an embedded preset must be a real flag long-name. goflags drops
|
|
// unknown config keys silently, so a typo would otherwise ship as a dead no-op.
|
|
func TestPresetKeysAreRegisteredFlags(t *testing.T) {
|
|
valid := map[string]bool{}
|
|
registerFlags(&Settings{}).CommandLine.VisitAll(func(f *flag.Flag) {
|
|
valid[f.Name] = true
|
|
})
|
|
|
|
for _, name := range presetNames {
|
|
data, ok := embeddedPreset(name)
|
|
if !ok {
|
|
t.Errorf("preset %q is not embedded", name)
|
|
continue
|
|
}
|
|
var keys map[string]any
|
|
if err := yaml.Unmarshal(data, &keys); err != nil {
|
|
t.Errorf("preset %q is not valid yaml: %s", name, err)
|
|
continue
|
|
}
|
|
for key := range keys {
|
|
if !valid[key] {
|
|
t.Errorf("preset %q references unknown flag %q", name, key)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPresetMinimal(t *testing.T) {
|
|
s := loadPreset(t, "minimal")
|
|
if !s.Probe || !s.Headers || !s.Favicon {
|
|
t.Errorf("minimal should enable probe/headers/favicon, got probe=%v headers=%v favicon=%v",
|
|
s.Probe, s.Headers, s.Favicon)
|
|
}
|
|
if s.XSS || s.SQL || s.Nuclei {
|
|
t.Error("minimal should not enable heavy or intrusive scans")
|
|
}
|
|
}
|
|
|
|
func TestPresetReconIsNonIntrusive(t *testing.T) {
|
|
s := loadPreset(t, "recon")
|
|
if !s.Passive || !s.Whois || !s.CMS || !s.Probe {
|
|
t.Errorf("recon should enable passive/whois/cms/probe, got %v/%v/%v/%v",
|
|
s.Passive, s.Whois, s.CMS, s.Probe)
|
|
}
|
|
if s.XSS || s.SQL || s.LFI || s.Redirect {
|
|
t.Errorf("recon must not enable payload-injecting scans: xss=%v sql=%v lfi=%v redirect=%v",
|
|
s.XSS, s.SQL, s.LFI, s.Redirect)
|
|
}
|
|
}
|
|
|
|
func TestPresetFull(t *testing.T) {
|
|
s := loadPreset(t, "full")
|
|
if !s.XSS || !s.SQL || !s.LFI || !s.Redirect {
|
|
t.Error("full should enable the intrusive scans")
|
|
}
|
|
if s.Dirlist != "large" || s.Ports != "full" {
|
|
t.Errorf("full should set dirlist=large ports=full, got dirlist=%q ports=%q",
|
|
s.Dirlist, s.Ports)
|
|
}
|
|
}
|
|
|
|
// the template merges as the goflags config: it fills flags left at their
|
|
// default, an explicit cli flag still wins, and an untouched flag stays put.
|
|
func TestTemplateConfigPrecedence(t *testing.T) {
|
|
goflags.DisableAutoConfigMigration = true
|
|
tmpl := writeTemplate(t, "cms: true\nthreads: 99\n")
|
|
|
|
var cms, sql bool
|
|
var threads int
|
|
flagSet := goflags.NewFlagSet()
|
|
flagSet.BoolVar(&cms, "cms", false, "")
|
|
flagSet.BoolVar(&sql, "sql", false, "")
|
|
flagSet.IntVar(&threads, "threads", 10, "")
|
|
|
|
flagSet.SetConfigFilePath(tmpl)
|
|
if err := flagSet.Parse("-threads", "5"); err != nil {
|
|
t.Fatalf("parse: %s", err)
|
|
}
|
|
|
|
if !cms {
|
|
t.Error("expected template to set cms=true")
|
|
}
|
|
if threads != 5 {
|
|
t.Errorf("expected cli threads 5 to win over template, got %d", threads)
|
|
}
|
|
if sql {
|
|
t.Error("expected sql left untouched to stay false")
|
|
}
|
|
}
|
|
|
|
func TestTemplateFlagValue(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{"long with space", []string{"-template", "a.yaml"}, "a.yaml"},
|
|
{"double dash with space", []string{"--template", "b.yaml"}, "b.yaml"},
|
|
{"long with equals", []string{"-template=c.yaml"}, "c.yaml"},
|
|
{"double dash with equals", []string{"--template=d.yaml"}, "d.yaml"},
|
|
{"absent", []string{"-u", "x"}, ""},
|
|
{"trailing without value", []string{"-u", "x", "-template"}, ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := templateFlagValue(tc.args); got != tc.want {
|
|
t.Errorf("expected %q, got %q", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateExistingFile(t *testing.T) {
|
|
path := writeTemplate(t, "cms: true\n")
|
|
got, cleanup, err := resolveTemplate(path)
|
|
if cleanup != nil {
|
|
defer cleanup()
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("resolveTemplate: %s", err)
|
|
}
|
|
if got != path {
|
|
t.Errorf("expected %q, got %q", path, got)
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateNamedPreset(t *testing.T) {
|
|
path, cleanup, err := resolveTemplate("recon")
|
|
if cleanup != nil {
|
|
defer cleanup()
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("recon preset should resolve: %s", err)
|
|
}
|
|
if path == "" {
|
|
t.Fatal("expected a materialized preset path")
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateMissingFile(t *testing.T) {
|
|
if _, _, err := resolveTemplate("./does-not-exist.yaml"); err == nil {
|
|
t.Fatal("expected an error for a missing template file")
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateDirectory(t *testing.T) {
|
|
if _, _, err := resolveTemplate(t.TempDir()); err == nil {
|
|
t.Fatal("expected an error for a directory")
|
|
}
|
|
}
|
|
|
|
func TestResolveTemplateUnknownName(t *testing.T) {
|
|
if _, _, err := resolveTemplate("bogus"); err == nil {
|
|
t.Fatal("expected an error for an unknown template name")
|
|
}
|
|
}
|