Files
nuclei/pkg/model/model_test.go
Arthur LoiselleandClaude Opus 5.5 3ae6ca8e3d fix(output): encode nested template maps in JSON output
Template YAML decodes nested maps as `map[interface{}]interface{}`, which
the JSON encoder rejects, so a result whose `info.metadata` (or javascript
args carried in `meta`) held a map was dropped from `-j`/`-jsonl` output.
`model.Info` and `output.ResultEvent` now encode their metadata with
string-keyed maps.

Closes #7971

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 09:21:08 -04:00

184 lines
5.7 KiB
Go

package model
import (
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
"github.com/projectdiscovery/nuclei/v3/pkg/utils/yaml"
"github.com/stretchr/testify/require"
)
func TestInfoJsonMarshal(t *testing.T) {
info := Info{
Name: "Test Template Name",
Authors: stringslice.StringSlice{Value: []string{"forgedhallpass", "ice3man"}},
Description: "Test description",
SeverityHolder: severity.Holder{Severity: severity.High},
Tags: stringslice.StringSlice{Value: []string{"cve", "misc"}},
Reference: stringslice.NewRawStringSlice("Reference1"),
Metadata: map[string]interface{}{
"string_key": "string_value",
"array_key": []string{"array_value1", "array_value2"},
"map_key": map[string]string{
"key1": "val1",
},
},
}
result, err := json.Marshal(&info)
require.Nil(t, err)
expected := `{"name":"Test Template Name","author":["forgedhallpass","ice3man"],"tags":["cve","misc"],"description":"Test description","reference":"Reference1","severity":"high","metadata":{"array_key":["array_value1","array_value2"],"map_key":{"key1":"val1"},"string_key":"string_value"}}`
require.Equal(t, expected, string(result))
}
func TestInfoJsonMarshalNestedYAMLMetadata(t *testing.T) {
var info Info
err := yaml.Unmarshal([]byte(`
name: Nested metadata
author: pdteam
tags: misc
severity: info
metadata:
labels:
en: Hello
1: one
items:
- name: first
tags:
fr: Bonjour
plain: value
`), &info)
require.NoError(t, err)
require.IsType(t, map[interface{}]interface{}{}, info.Metadata["labels"])
for _, value := range []interface{}{info, &info} {
result, err := json.Marshal(value)
require.NoError(t, err)
require.JSONEq(t, `{"name":"Nested metadata","author":["pdteam"],"tags":["misc"],"severity":"info","metadata":{"labels":{"en":"Hello","1":"one"},"items":[{"name":"first","tags":{"fr":"Bonjour"}}],"plain":"value"}}`, string(result))
}
require.IsType(t, map[interface{}]interface{}{}, info.Metadata["labels"], "marshaling must not modify the info")
}
func TestInfoYamlMarshal(t *testing.T) {
info := Info{
Name: "Test Template Name",
Authors: stringslice.StringSlice{Value: []string{"forgedhallpass", "ice3man"}},
Description: "Test description",
SeverityHolder: severity.Holder{Severity: severity.High},
Tags: stringslice.StringSlice{Value: []string{"cve", "misc"}},
Reference: stringslice.NewRawStringSlice("Reference1"),
Metadata: map[string]interface{}{
"string_key": "string_value",
"array_key": []string{"array_value1", "array_value2"},
"map_key": map[string]string{
"key1": "val1",
},
},
}
result, err := yaml.Marshal(&info)
require.Nil(t, err)
expected := `name: Test Template Name
author:
- forgedhallpass
- ice3man
tags:
- cve
- misc
description: Test description
reference: Reference1
severity: high
metadata:
array_key:
- array_value1
- array_value2
map_key:
key1: val1
string_key: string_value
`
require.Equal(t, expected, string(result))
}
func TestUnmarshal(t *testing.T) {
templateName := "Test Template"
authors := []string{"forgedhallpass", "ice3man"}
tags := []string{"cve", "misc"}
references := []string{"http://test.com", "http://Domain.com"}
dynamicKey1 := "customDynamicKey1"
dynamicKey2 := "customDynamicKey2"
dynamicKeysMap := map[string]interface{}{
dynamicKey1: "customDynamicValue1",
dynamicKey2: "customDynamicValue2",
}
assertUnmarshalledTemplateInfo := func(t *testing.T, yamlPayload string) Info {
t.Helper()
info := Info{}
err := yaml.Unmarshal([]byte(yamlPayload), &info)
require.Nil(t, err)
require.Equal(t, info.Name, templateName)
require.Equal(t, info.Authors.ToSlice(), authors)
require.Equal(t, info.Tags.ToSlice(), tags)
require.Equal(t, info.SeverityHolder.Severity, severity.Critical)
require.Equal(t, info.Reference.ToSlice(), references)
require.Equal(t, info.Metadata, dynamicKeysMap)
return info
}
yamlPayload1 := `
name: ` + templateName + `
author: ` + strings.Join(authors, ", ") + `
tags: ` + strings.Join(tags, ", ") + `
severity: critical
reference: ` + strings.Join(references, ",") + `
metadata:
` + dynamicKey1 + `: ` + dynamicKeysMap[dynamicKey1].(string) + `
` + dynamicKey2 + `: ` + dynamicKeysMap[dynamicKey2].(string) + `
`
yamlPayload2 := `
name: ` + templateName + `
author:
- ` + authors[0] + `
- ` + authors[1] + `
tags:
- ` + tags[0] + `
- ` + tags[1] + `
severity: critical
reference:
- ` + references[0] + ` # comments are not unmarshalled
- ` + references[1] + `
metadata:
` + dynamicKey1 + `: ` + dynamicKeysMap[dynamicKey1].(string) + `
` + dynamicKey2 + `: ` + dynamicKeysMap[dynamicKey2].(string) + `
`
info1 := assertUnmarshalledTemplateInfo(t, yamlPayload1)
info2 := assertUnmarshalledTemplateInfo(t, yamlPayload2)
require.Equal(t, info1, info2)
}
func TestInfoFilledFields(t *testing.T) {
written := Info{Description: "{{host}} is exposed", Impact: "Data loss"}
filled := written
filled.Description = "example.com is exposed"
require.Nil(t, filled.FilledFields(nil))
fields := filled.FilledFields(&written)
require.Len(t, fields, 1)
require.Equal(t, "Description", fields[0].Label)
require.Equal(t, "example.com is exposed", *fields[0].Value)
}
func TestInfoHasPlaceholders(t *testing.T) {
require.False(t, (&Info{Description: "Redis is exposed"}).HasPlaceholders())
require.True(t, (&Info{Impact: "{{host}} is exposed"}).HasPlaceholders())
require.True(t, (&Info{Remediation: "rotate §password§"}).HasPlaceholders())
}