diff --git a/pkg/model/model.go b/pkg/model/model.go index d74186b1039..d7cab19e637 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -62,7 +62,7 @@ var ( var ( // KICSCommentRgxp is the regexp to identify if a comment is a KICS comment - KICSCommentRgxp = regexp.MustCompile(`^((/{2})|#)\s*kics\s*`) + KICSCommentRgxp = regexp.MustCompile(`^((/{2})|#)*\s*kics\s*`) ) // Version - is the model for the version response diff --git a/pkg/parser/docker/comments.go b/pkg/parser/docker/comments.go new file mode 100644 index 00000000000..536c0685d57 --- /dev/null +++ b/pkg/parser/docker/comments.go @@ -0,0 +1,137 @@ +package docker + +import ( + "strings" + + "github.com/Checkmarx/kics/pkg/model" + "github.com/moby/buildkit/frontend/dockerfile/parser" +) + +// ignoreLine is a structure that contains information if the line/multi-line should be ignored. +type ignoreLine struct { + shouldIgnore bool + shouldIgnoreBlock bool + line []int +} + +// ignore is a structure that contains information about the lines that are being ignored. +type ignore struct { + from map[string][]int + lines []int +} + +// newIgnore returns a new ignore struct. +func newIgnore() *ignore { + return &ignore{ + from: make(map[string][]int), + lines: make([]int, 0), + } +} + +// setIgnore adds a new entry to the ignore struct for the 'FROM' block to be ignored +func (i *ignore) setIgnore(from string, shouldIgnoreBlock bool) { + if !shouldIgnoreBlock { + return + } + i.from[from] = make([]int, 0) +} + +// ignoreLine adds lines to be ignored to the ignore struct. +func (i *ignore) ignoreLine(lines []int) { + i.lines = append(i.lines, lines...) +} + +// ignoreBlock adds block lines to be ignored to the ignore struct. +func (i *ignore) ignoreBlock(node *parser.Node, from string) { + if _, ok := i.from[from]; ok { + i.from[from] = append(i.from[from], commentRange(node.StartLine, node.EndLine)...) + } +} + +// getIgnoreLines returns the lines that are being ignored. +func (i *ignore) getIgnoreLines() []int { + for _, value := range i.from { + i.lines = append(i.lines, value...) + } + return removeDups(i.lines) +} + +// removeDups removes duplicates from a slice of ints. +func removeDups(lines []int) []int { + seen := make(map[int]bool) + var result []int + for _, line := range lines { + if !seen[line] { + result = append(result, line) + seen[line] = true + } + } + return result +} + +// getIgnoreComments returns lines to be ignored for each node of the dockerfile +func getIgnoreComments(node *parser.Node) (ignore ignoreLine) { + if len(node.PrevComment) == 0 { + return ignoreLine{ + shouldIgnore: false, + } + } + + ignore = ignoreLine{ + shouldIgnore: true, + shouldIgnoreBlock: false, + line: make([]int, 0), + } + + for idx, comment := range node.PrevComment { + switch processComment(comment) { + case model.IgnoreLine: + ignore.line = append(ignore.line, commentRange(node.StartLine-(idx+1), node.EndLine)...) + case model.IgnoreBlock: + ignore.line = append(ignore.line, node.StartLine-(idx+1)) + ignore.shouldIgnoreBlock = true + default: + ignore.line = append(ignore.line, node.StartLine-(idx+1)) + } + } + + return +} + +// processComment returns the type of comment given. +func processComment(comment string) (value model.CommentCommand) { + commentLower := strings.ToLower(comment) + + if model.KICSCommentRgxp.MatchString(commentLower) { + commentLower = model.KICSCommentRgxp.ReplaceAllString(commentLower, "") + commands := strings.Split(strings.Trim(commentLower, "\n"), " ") + value = processCommands(commands) + return + } + return model.CommentCommand(comment) +} + +// processCommands goes over kics commands in a line and returns the type of command given +func processCommands(commands []string) model.CommentCommand { + for _, command := range commands { + switch com := model.CommentCommand(command); com { + case model.IgnoreLine: + return model.IgnoreLine + case model.IgnoreBlock: + return model.IgnoreBlock + default: + continue + } + } + + return model.CommentCommand(commands[0]) +} + +// commentRange returns the range of the comment between the start and end lines. +func commentRange(start, end int) (lines []int) { + lines = make([]int, end-start+1) + for i := range lines { + lines[i] = start + i + } + return +} diff --git a/pkg/parser/docker/comments_test.go b/pkg/parser/docker/comments_test.go new file mode 100644 index 00000000000..aef23fa5c70 --- /dev/null +++ b/pkg/parser/docker/comments_test.go @@ -0,0 +1,311 @@ +package docker + +import ( + "reflect" + "testing" + + "github.com/moby/buildkit/frontend/dockerfile/parser" + "github.com/stretchr/testify/require" +) + +// Test_newIgnore tests the newIgnore function +func Test_newIgnore(t *testing.T) { + tests := []struct { + name string + want *ignore + }{ + { + name: "new ignore: test01", + want: &ignore{ + from: make(map[string][]int), + lines: make([]int, 0), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := newIgnore(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("newIgnore() = %v, want %v", got, tt.want) + } + }) + } +} + +// Test_ignore_setIgnore tests the setIgnore function +func Test_ignore_setIgnore(t *testing.T) { + type fields struct { + from map[string][]int + lines []int + } + type args struct { + from string + shouldIgnoreBlock bool + } + tests := []struct { + name string + fields fields + args args + want bool + }{ + { + name: "setIgnore: should not ignore", + fields: fields{ + from: make(map[string][]int), + lines: make([]int, 0), + }, + args: args{ + from: "test_from", + shouldIgnoreBlock: false, + }, + want: false, + }, + { + name: "setIgnore: should ignore", + fields: fields{ + from: make(map[string][]int), + lines: make([]int, 0), + }, + args: args{ + from: "test_from", + shouldIgnoreBlock: true, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &ignore{ + from: tt.fields.from, + lines: tt.fields.lines, + } + i.setIgnore(tt.args.from, tt.args.shouldIgnoreBlock) + _, ok := i.from[tt.args.from] + require.Equal(t, tt.want, ok) + }) + } +} + +// Test_ignore_ignoreLine tests the ignoreLine function +func Test_ignore_ignoreLine(t *testing.T) { + type fields struct { + from map[string][]int + lines []int + } + type args struct { + lines []int + } + tests := []struct { + name string + fields fields + args args + }{ + { + name: "ignoreLine: should ignore line", + fields: fields{ + from: make(map[string][]int), + lines: make([]int, 0), + }, + args: args{ + lines: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &ignore{ + from: tt.fields.from, + lines: tt.fields.lines, + } + i.ignoreLine(tt.args.lines) + require.Equal(t, tt.args.lines, i.lines) + }) + } +} + +// Test_ignore_ignoreBlock tests the ignoreBlock function +func Test_ignore_ignoreBlock(t *testing.T) { + type fields struct { + from map[string][]int + lines []int + } + type args struct { + node *parser.Node + from string + } + tests := []struct { + name string + fields fields + args args + want []int + }{ + { + name: "ignoreBlock: should ignore block", + fields: fields{ + from: map[string][]int{ + "test_ignore": make([]int, 0), + }, + lines: make([]int, 0), + }, + args: args{ + node: &parser.Node{ + StartLine: 1, + EndLine: 5, + }, + from: "test_ignore", + }, + want: []int{1, 2, 3, 4, 5}, + }, + { + name: "ignoreBlock: should not ignore block", + fields: fields{ + from: map[string][]int{ + "test_ignore": make([]int, 0), + }, + lines: make([]int, 0), + }, + args: args{ + node: &parser.Node{ + StartLine: 1, + EndLine: 5, + }, + from: "test_no_ignore", + }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := &ignore{ + from: tt.fields.from, + lines: tt.fields.lines, + } + i.ignoreBlock(tt.args.node, tt.args.from) + require.Equal(t, tt.want, i.getIgnoreLines()) + }) + } +} + +// Test_getIgnoreComments tests the getIgnoreComments function +func Test_getIgnoreComments(t *testing.T) { + type args struct { + node *parser.Node + } + tests := []struct { + name string + args args + wantIgnore ignoreLine + }{ + { + name: "getIgnoreComments: should ignore single line comments", + args: args{ + node: &parser.Node{ + PrevComment: []string{"kics ignore-line"}, + StartLine: 2, + EndLine: 2, + }, + }, + wantIgnore: ignoreLine{ + shouldIgnore: true, + shouldIgnoreBlock: false, + line: []int{1, 2}, + }, + }, + { + name: "getIgnoreComments: should ignore default comments", + args: args{ + node: &parser.Node{ + PrevComment: []string{"this is a simple comment"}, + StartLine: 2, + EndLine: 2, + }, + }, + wantIgnore: ignoreLine{ + shouldIgnore: true, + shouldIgnoreBlock: false, + line: []int{1}, + }, + }, + { + name: "getIgnoreComments: should ignore block comments", + args: args{ + node: &parser.Node{ + PrevComment: []string{"kics ignore-block"}, + StartLine: 2, + EndLine: 2, + }, + }, + wantIgnore: ignoreLine{ + shouldIgnore: true, + shouldIgnoreBlock: true, + line: []int{1}, + }, + }, + { + name: "getIgnoreComments: should ignore multi line comments", + args: args{ + node: &parser.Node{ + PrevComment: []string{"kics ignore-line"}, + StartLine: 2, + EndLine: 5, + }, + }, + wantIgnore: ignoreLine{ + shouldIgnore: true, + shouldIgnoreBlock: false, + line: []int{1, 2, 3, 4, 5}, + }, + }, + { + name: "getIgnoreComments: should ignore kics not ignore command comment", + args: args{ + node: &parser.Node{ + PrevComment: []string{"kics command"}, + StartLine: 2, + EndLine: 5, + }, + }, + wantIgnore: ignoreLine{ + shouldIgnore: true, + shouldIgnoreBlock: false, + line: []int{1}, + }, + }, + { + name: "getIgnoreComments: should ignore multi line comments", + args: args{ + node: &parser.Node{ + PrevComment: []string{"this is a comment", "this is the second comment"}, + StartLine: 3, + EndLine: 3, + }, + }, + wantIgnore: ignoreLine{ + shouldIgnore: true, + shouldIgnoreBlock: false, + line: []int{2, 1}, + }, + }, + { + name: "getIgnoreComments: should not ignore", + args: args{ + node: &parser.Node{ + PrevComment: []string{}, + StartLine: 3, + EndLine: 3, + }, + }, + wantIgnore: ignoreLine{ + shouldIgnore: false, + shouldIgnoreBlock: false, + line: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if gotIgnore := getIgnoreComments(tt.args.node); !reflect.DeepEqual(gotIgnore, tt.wantIgnore) { + t.Errorf("getIgnoreComments() = %v, want %v", gotIgnore, tt.wantIgnore) + } + }) + } +} diff --git a/pkg/parser/docker/parser.go b/pkg/parser/docker/parser.go index 046a0e7a946..a4129eb650b 100644 --- a/pkg/parser/docker/parser.go +++ b/pkg/parser/docker/parser.go @@ -48,6 +48,7 @@ func (p *Parser) Parse(_ string, fileContent []byte) ([]model.Document, []int, e fromValue := "args" from := make(map[string][]Command) + ignoreStruct := newIgnore() for _, child := range parsed.AST.Children { child.Value = strings.ToLower(child.Value) @@ -55,6 +56,13 @@ func (p *Parser) Parse(_ string, fileContent []byte) ([]model.Document, []int, e fromValue = strings.TrimPrefix(child.Original, "FROM ") } + if comment := getIgnoreComments(child); comment.shouldIgnore { + ignoreStruct.ignoreLine(comment.line) + ignoreStruct.setIgnore(fromValue, comment.shouldIgnoreBlock) + } + + ignoreStruct.ignoreBlock(child, fromValue) + cmd := Command{ Cmd: child.Value, Original: child.Original, @@ -91,6 +99,8 @@ func (p *Parser) Parse(_ string, fileContent []byte) ([]model.Document, []int, e documents = append(documents, *doc) + // ignoreLines := ignoreStruct.getIgnoreLines() nolint + return documents, []int{}, nil } diff --git a/pkg/parser/docker/parser_test.go b/pkg/parser/docker/parser_test.go index 85d53406f28..dc4ec6dfa88 100644 --- a/pkg/parser/docker/parser_test.go +++ b/pkg/parser/docker/parser_test.go @@ -43,6 +43,7 @@ func TestParser_Parse(t *testing.T) { ` FROM ubuntu:xenial RUN echo hi > /etc/hi.conf + # kics ignore-line CMD ["echo"] HEALTHCHECK --retries=5 CMD echo hi ONBUILD ADD foo bar @@ -53,6 +54,7 @@ func TestParser_Parse(t *testing.T) { CMD ["echo"] `, ` + # kics ignore-block FROM golang:alpine ENV CGO_ENABLED=0 WORKDIR /app