package nucleitpl import ( "os" "path/filepath" "testing" ) const sampleSupported = ` id: test-basic-word-match info: name: Test Basic Word Match author: vyntral severity: high description: Fires when response body contains 'phpinfo' tags: exposure,php reference: - https://example.com/advisory/CVE-2021-12345 requests: - method: GET path: - "{{BaseURL}}/phpinfo.php" matchers: - type: word part: body words: - "PHP Version" - type: status status: - 200 matchers-condition: and ` const sampleUnsupportedDNS = ` id: test-dns info: name: Test DNS severity: medium dns: - name: "{{FQDN}}" type: TXT matchers: - type: word words: ["v=spf"] ` const sampleUnsupportedPayloads = ` id: test-payloads info: name: Test Payloads severity: low requests: - method: GET path: - "{{BaseURL}}/{{word}}" payloads: word: - admin - backup matchers: - type: status status: [200] ` const sampleBadYAML = ` id: [unclosed info: name: ` func writeTmp(t *testing.T, name, content string) string { t.Helper() dir := t.TempDir() path := filepath.Join(dir, name) if err := os.WriteFile(path, []byte(content), 0o644); err != nil { t.Fatal(err) } return path } func TestLoad_Supported(t *testing.T) { path := writeTmp(t, "ok.yaml", sampleSupported) tpl, err := Load(path) if err != nil { t.Fatal(err) } if tpl.ID != "test-basic-word-match" { t.Errorf("ID = %q", tpl.ID) } if tpl.Info.Severity != "high" { t.Errorf("Severity = %q", tpl.Info.Severity) } if len(tpl.Requests) != 1 { t.Fatalf("Requests len = %d", len(tpl.Requests)) } r := tpl.Requests[0] if r.Path[0] != "{{BaseURL}}/phpinfo.php" { t.Errorf("Path[0] = %q", r.Path[0]) } if len(r.Matchers) != 2 { t.Errorf("Matchers len = %d", len(r.Matchers)) } if r.MatchersCondition != "and" { t.Errorf("MatchersCondition = %q", r.MatchersCondition) } if ok, reason := tpl.IsSupported(); !ok { t.Errorf("should be supported; reason=%q", reason) } if tags := tpl.Tags(); len(tags) != 2 || tags[0] != "exposure" { t.Errorf("Tags = %v", tags) } } func TestLoad_DNSUnsupported(t *testing.T) { path := writeTmp(t, "dns.yaml", sampleUnsupportedDNS) tpl, err := Load(path) if err != nil { t.Fatal(err) } ok, reason := tpl.IsSupported() if ok { t.Error("dns template should be unsupported") } if reason == "" { t.Error("expected non-empty reason") } } func TestLoad_PayloadsUnsupported(t *testing.T) { path := writeTmp(t, "payloads.yaml", sampleUnsupportedPayloads) tpl, err := Load(path) if err != nil { t.Fatal(err) } ok, reason := tpl.IsSupported() if ok { t.Error("payloads template should be unsupported") } if reason == "" { t.Error("expected non-empty reason") } } func TestLoad_BadYAML(t *testing.T) { path := writeTmp(t, "bad.yaml", sampleBadYAML) if _, err := Load(path); err == nil { t.Error("expected parse error") } } func TestLoad_MissingID(t *testing.T) { path := writeTmp(t, "noid.yaml", "info:\n severity: low\n") if _, err := Load(path); err == nil { t.Error("expected missing id error") } } func TestLoadDir(t *testing.T) { dir := t.TempDir() _ = os.WriteFile(filepath.Join(dir, "a.yaml"), []byte(sampleSupported), 0o644) _ = os.WriteFile(filepath.Join(dir, "b.yaml"), []byte(sampleUnsupportedDNS), 0o644) _ = os.WriteFile(filepath.Join(dir, "c.yml"), []byte(sampleSupported), 0o644) _ = os.WriteFile(filepath.Join(dir, "d.bad"), []byte("???"), 0o644) _ = os.WriteFile(filepath.Join(dir, "e.yaml"), []byte(sampleBadYAML), 0o644) sub := filepath.Join(dir, "nested") _ = os.MkdirAll(sub, 0o755) _ = os.WriteFile(filepath.Join(sub, "f.yaml"), []byte(sampleSupported), 0o644) tpls, diags, err := LoadDir(dir) if err != nil { t.Fatal(err) } // 3 supported (a, c, nested/f), 1 dns (b), 1 parse error (e). .bad ignored. if got := len(tpls); got != 4 { t.Errorf("loaded = %d, want 4 (3 supported + 1 dns)", got) } if len(diags) != 1 { t.Errorf("diags = %d, want 1", len(diags)) } } func TestSeverity_Default(t *testing.T) { tpl := &Template{Info: Info{Severity: "UNKNOWN"}} if sev := tpl.Severity(); sev != "info" { t.Errorf("got %q, want info", sev) } } func TestSeverity_Normalized(t *testing.T) { for input, want := range map[string]string{ "critical": "critical", "HIGH": "high", " Medium ": "medium", "LOW": "low", "info": "info", "": "info", } { tpl := &Template{Info: Info{Severity: input}} if got := tpl.Severity(); got != want { t.Errorf("Severity(%q) = %q, want %q", input, got, want) } } } func TestHTTPAlias(t *testing.T) { content := ` id: http-alias info: severity: low http: - method: GET path: ["{{BaseURL}}/"] matchers: - type: status status: [200] ` path := writeTmp(t, "http.yaml", content) tpl, err := Load(path) if err != nil { t.Fatal(err) } if len(tpl.Requests) != 1 { t.Errorf("expected http: to be aliased to Requests, got %d", len(tpl.Requests)) } if ok, _ := tpl.IsSupported(); !ok { t.Error("http alias template should be supported") } }