mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
feat(misconf): include map key in manifest snippet for diagnostics (#9681)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
28
integration/testdata/helm.json.golden
vendored
28
integration/testdata/helm.json.golden
vendored
@@ -986,10 +986,20 @@
|
||||
"CauseMetadata": {
|
||||
"Provider": "Kubernetes",
|
||||
"Service": "general",
|
||||
"StartLine": 5,
|
||||
"StartLine": 4,
|
||||
"EndLine": 7,
|
||||
"Code": {
|
||||
"Lines": [
|
||||
{
|
||||
"Number": 4,
|
||||
"Content": "metadata:",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": "\u001b[38;5;33mmetadata\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 5,
|
||||
"Content": " name: nginx-deployment",
|
||||
@@ -997,7 +1007,7 @@
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mname\u001b[0m: nginx-deployment",
|
||||
"FirstCause": true,
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
@@ -1135,10 +1145,20 @@
|
||||
"CauseMetadata": {
|
||||
"Provider": "Kubernetes",
|
||||
"Service": "general",
|
||||
"StartLine": 18,
|
||||
"StartLine": 17,
|
||||
"EndLine": 22,
|
||||
"Code": {
|
||||
"Lines": [
|
||||
{
|
||||
"Number": 17,
|
||||
"Content": " spec:",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mspec\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 18,
|
||||
"Content": " containers:",
|
||||
@@ -1146,7 +1166,7 @@
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mcontainers\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
|
||||
40
integration/testdata/helm_testchart.json.golden
vendored
40
integration/testdata/helm_testchart.json.golden
vendored
@@ -415,10 +415,20 @@
|
||||
"CauseMetadata": {
|
||||
"Provider": "Kubernetes",
|
||||
"Service": "general",
|
||||
"StartLine": 5,
|
||||
"StartLine": 4,
|
||||
"EndLine": 11,
|
||||
"Code": {
|
||||
"Lines": [
|
||||
{
|
||||
"Number": 4,
|
||||
"Content": "metadata:",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": "\u001b[38;5;33mmetadata\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 5,
|
||||
"Content": " name: testchart",
|
||||
@@ -426,7 +436,7 @@
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mname\u001b[0m: testchart",
|
||||
"FirstCause": true,
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
@@ -536,10 +546,20 @@
|
||||
"CauseMetadata": {
|
||||
"Provider": "Kubernetes",
|
||||
"Service": "general",
|
||||
"StartLine": 24,
|
||||
"StartLine": 23,
|
||||
"EndLine": 57,
|
||||
"Code": {
|
||||
"Lines": [
|
||||
{
|
||||
"Number": 23,
|
||||
"Content": " spec:",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mspec\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 24,
|
||||
"Content": " serviceAccountName: testchart",
|
||||
@@ -547,7 +567,7 @@
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mserviceAccountName\u001b[0m: testchart",
|
||||
"FirstCause": true,
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
@@ -618,20 +638,10 @@
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mdrop\u001b[0m:",
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 32,
|
||||
"Content": " - ALL",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " - ALL",
|
||||
"FirstCause": false,
|
||||
"LastCause": true
|
||||
},
|
||||
{
|
||||
"Number": 33,
|
||||
"Number": 32,
|
||||
"Content": "",
|
||||
"IsCause": false,
|
||||
"Annotation": "",
|
||||
|
||||
@@ -542,10 +542,20 @@
|
||||
"CauseMetadata": {
|
||||
"Provider": "Kubernetes",
|
||||
"Service": "general",
|
||||
"StartLine": 30,
|
||||
"StartLine": 29,
|
||||
"EndLine": 36,
|
||||
"Code": {
|
||||
"Lines": [
|
||||
{
|
||||
"Number": 29,
|
||||
"Content": " securityContext:",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 30,
|
||||
"Content": " capabilities:",
|
||||
@@ -553,7 +563,7 @@
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
@@ -640,10 +650,20 @@
|
||||
"CauseMetadata": {
|
||||
"Provider": "Kubernetes",
|
||||
"Service": "general",
|
||||
"StartLine": 5,
|
||||
"StartLine": 4,
|
||||
"EndLine": 11,
|
||||
"Code": {
|
||||
"Lines": [
|
||||
{
|
||||
"Number": 4,
|
||||
"Content": "metadata:",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": "\u001b[38;5;33mmetadata\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 5,
|
||||
"Content": " name: testchart",
|
||||
@@ -651,7 +671,7 @@
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mname\u001b[0m: testchart",
|
||||
"FirstCause": true,
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
@@ -761,10 +781,20 @@
|
||||
"CauseMetadata": {
|
||||
"Provider": "Kubernetes",
|
||||
"Service": "general",
|
||||
"StartLine": 24,
|
||||
"StartLine": 23,
|
||||
"EndLine": 57,
|
||||
"Code": {
|
||||
"Lines": [
|
||||
{
|
||||
"Number": 23,
|
||||
"Content": " spec:",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mspec\u001b[0m:",
|
||||
"FirstCause": true,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 24,
|
||||
"Content": " serviceAccountName: testchart",
|
||||
@@ -772,7 +802,7 @@
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mserviceAccountName\u001b[0m: testchart",
|
||||
"FirstCause": true,
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
@@ -843,20 +873,10 @@
|
||||
"Truncated": false,
|
||||
"Highlighted": " \u001b[38;5;33mdrop\u001b[0m:",
|
||||
"FirstCause": false,
|
||||
"LastCause": false
|
||||
},
|
||||
{
|
||||
"Number": 32,
|
||||
"Content": " - ALL",
|
||||
"IsCause": true,
|
||||
"Annotation": "",
|
||||
"Truncated": false,
|
||||
"Highlighted": " - ALL",
|
||||
"FirstCause": false,
|
||||
"LastCause": true
|
||||
},
|
||||
{
|
||||
"Number": 33,
|
||||
"Number": 32,
|
||||
"Content": "",
|
||||
"IsCause": false,
|
||||
"Annotation": "",
|
||||
|
||||
@@ -130,17 +130,14 @@ func (n *ManifestNode) UnmarshalYAML(node *yaml.Node) error {
|
||||
}
|
||||
|
||||
func (n *ManifestNode) handleSliceTag(node *yaml.Node) error {
|
||||
var nodes []*ManifestNode
|
||||
nodes := make([]*ManifestNode, 0, len(node.Content))
|
||||
maxLine := node.Line
|
||||
for _, contentNode := range node.Content {
|
||||
newNode := new(ManifestNode)
|
||||
newNode.FilePath = n.FilePath
|
||||
if err := contentNode.Decode(newNode); err != nil {
|
||||
newNode, err := newManifestNodeFromYaml(n.FilePath, contentNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if newNode.EndLine > maxLine {
|
||||
maxLine = newNode.EndLine
|
||||
}
|
||||
maxLine = max(maxLine, newNode.EndLine)
|
||||
nodes = append(nodes, newNode)
|
||||
}
|
||||
n.EndLine = maxLine
|
||||
@@ -149,29 +146,54 @@ func (n *ManifestNode) handleSliceTag(node *yaml.Node) error {
|
||||
}
|
||||
|
||||
func (n *ManifestNode) handleMapTag(node *yaml.Node) error {
|
||||
output := make(map[string]*ManifestNode)
|
||||
var key string
|
||||
if len(node.Content)%2 != 0 {
|
||||
return fmt.Errorf("invalid map node at line %d: uneven number of children", node.Line)
|
||||
}
|
||||
|
||||
output := make(map[string]*ManifestNode, len(node.Content)/2)
|
||||
maxLine := node.Line
|
||||
for i, contentNode := range node.Content {
|
||||
if i == 0 || i%2 == 0 {
|
||||
key = contentNode.Value
|
||||
} else {
|
||||
newNode := new(ManifestNode)
|
||||
newNode.FilePath = n.FilePath
|
||||
if err := contentNode.Decode(newNode); err != nil {
|
||||
return err
|
||||
}
|
||||
output[key] = newNode
|
||||
if newNode.EndLine > maxLine {
|
||||
maxLine = newNode.EndLine
|
||||
}
|
||||
for i := 0; i < len(node.Content); i += 2 {
|
||||
keyNode := node.Content[i]
|
||||
valueNode := node.Content[i+1]
|
||||
|
||||
newNode, err := newManifestNodeFromYaml(n.FilePath, valueNode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if newNode.Type == TagMap {
|
||||
// Set StartLine to the key's line so that the map node snippet
|
||||
// correctly starts at the key in the YAML file
|
||||
newNode.StartLine = keyNode.Line
|
||||
}
|
||||
output[keyNode.Value] = newNode
|
||||
|
||||
maxLine = max(maxLine, newNode.EndLine)
|
||||
}
|
||||
n.EndLine = maxLine
|
||||
n.Value = output
|
||||
return nil
|
||||
}
|
||||
|
||||
func newManifestNodeFromYaml(filePath string, yamlNode *yaml.Node) (*ManifestNode, error) {
|
||||
newNode := &ManifestNode{
|
||||
FilePath: filePath,
|
||||
}
|
||||
|
||||
if yamlNode.Tag == "!!null" {
|
||||
// UnmarshalYAML is not called for null nodes
|
||||
newNode.StartLine = yamlNode.Line
|
||||
newNode.EndLine = yamlNode.Line
|
||||
return newNode, nil
|
||||
}
|
||||
|
||||
if err := yamlNode.Decode(newNode); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newNode, nil
|
||||
}
|
||||
|
||||
func (n *ManifestNode) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
||||
var valPtr any
|
||||
var nodeType TagType
|
||||
|
||||
@@ -84,6 +84,76 @@ func TestJsonManifestToRego(t *testing.T) {
|
||||
assert.Equal(t, expected, manifest.ToRego())
|
||||
}
|
||||
|
||||
func TestYamlManifestToRego(t *testing.T) {
|
||||
content := `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: hello-cpu-limit
|
||||
foo: null
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- sh
|
||||
- -c
|
||||
- echo 'Hello' && sleep 1h
|
||||
- null
|
||||
image: busybox
|
||||
name: hello
|
||||
`
|
||||
|
||||
const filePath = "pod.yaml"
|
||||
manifest, err := parser.ManifestFromYAML(filePath, []byte(content))
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := map[string]any{
|
||||
"__defsec_metadata": map[string]any{
|
||||
"filepath": filePath,
|
||||
"offset": 0,
|
||||
"startline": 1,
|
||||
"endline": 14,
|
||||
},
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": map[string]any{
|
||||
"__defsec_metadata": map[string]any{
|
||||
"filepath": filePath,
|
||||
"offset": 0,
|
||||
"startline": 3,
|
||||
"endline": 5,
|
||||
},
|
||||
"name": "hello-cpu-limit",
|
||||
"foo": nil, // YAML null preserved
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"__defsec_metadata": map[string]any{
|
||||
"filepath": filePath,
|
||||
"offset": 0,
|
||||
"startline": 6,
|
||||
"endline": 14,
|
||||
},
|
||||
"containers": []any{
|
||||
map[string]any{
|
||||
"__defsec_metadata": map[string]any{
|
||||
"filepath": filePath,
|
||||
"offset": 0,
|
||||
"startline": 8,
|
||||
"endline": 14,
|
||||
},
|
||||
"command": []any{
|
||||
"sh",
|
||||
"-c",
|
||||
"echo 'Hello' && sleep 1h",
|
||||
nil, // YAML null preserved here too
|
||||
},
|
||||
"image": "busybox",
|
||||
"name": "hello",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, manifest.ToRego())
|
||||
}
|
||||
|
||||
func TestManifestToRego(t *testing.T) {
|
||||
const filePath = "pod.json"
|
||||
tests := []struct {
|
||||
|
||||
@@ -41,8 +41,20 @@ func Parse(_ context.Context, r io.Reader, path string) ([]any, error) {
|
||||
manifests = append(manifests, manifest.ToRego())
|
||||
}
|
||||
|
||||
offset += len(strings.Split(partial, "\n"))
|
||||
offset += countLines(partial)
|
||||
}
|
||||
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
func countLines(s string) int {
|
||||
if s == "" {
|
||||
return 1
|
||||
}
|
||||
|
||||
count := strings.Count(s, "\n")
|
||||
if s[len(s)-1] != '\n' {
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
78
pkg/iac/scanners/kubernetes/parser/parser_test.go
Normal file
78
pkg/iac/scanners/kubernetes/parser/parser_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package parser_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes/parser"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
const filePath = "test.yaml"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
src string
|
||||
expectedOffsets []int
|
||||
}{
|
||||
{
|
||||
name: "empty file",
|
||||
src: "",
|
||||
expectedOffsets: nil,
|
||||
},
|
||||
{
|
||||
name: "single YAML without separator",
|
||||
src: `
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
`,
|
||||
expectedOffsets: []int{0},
|
||||
},
|
||||
{
|
||||
name: "multiple YAML documents",
|
||||
src: `---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
`,
|
||||
expectedOffsets: []int{1, 3},
|
||||
},
|
||||
{
|
||||
name: "YAML with multiple empty blocks",
|
||||
src: `---
|
||||
|
||||
---
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
`,
|
||||
expectedOffsets: []int{3},
|
||||
},
|
||||
{
|
||||
name: "Windows line endings",
|
||||
src: "---\r\napiVersion: v1\r\nkind: Pod\r\n",
|
||||
expectedOffsets: []int{1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
manifests, err := parser.Parse(t.Context(), strings.NewReader(tt.src), filePath)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, manifests, len(tt.expectedOffsets))
|
||||
|
||||
for i, m := range manifests {
|
||||
manifest := m.(map[string]any)
|
||||
metadata, ok := manifest["__defsec_metadata"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
offset, ok := metadata["offset"].(int)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tt.expectedOffsets[i], offset)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user