diff --git a/pkg/iac/scanners/dockerfile/parser/parser.go b/pkg/iac/scanners/dockerfile/parser/parser.go index 59881a2feb..3b64b0060d 100644 --- a/pkg/iac/scanners/dockerfile/parser/parser.go +++ b/pkg/iac/scanners/dockerfile/parser/parser.go @@ -2,12 +2,13 @@ package parser import ( "context" - "fmt" "io" + "slices" "strings" "github.com/moby/buildkit/frontend/dockerfile/instructions" "github.com/moby/buildkit/frontend/dockerfile/parser" + "golang.org/x/xerrors" "github.com/aquasecurity/trivy/pkg/iac/providers/dockerfile" ) @@ -15,7 +16,7 @@ import ( func Parse(_ context.Context, r io.Reader, path string) ([]*dockerfile.Dockerfile, error) { parsed, err := parser.Parse(r) if err != nil { - return nil, fmt.Errorf("dockerfile parse error: %w", err) + return nil, xerrors.Errorf("dockerfile parse error: %w", err) } var ( @@ -28,9 +29,9 @@ func Parse(_ context.Context, r io.Reader, path string) ([]*dockerfile.Dockerfil for _, child := range parsed.AST.Children { child.Value = strings.ToLower(child.Value) - instr, err := instructions.ParseInstruction(child) + instr, err := parseInstruction(child) if err != nil { - return nil, fmt.Errorf("parse dockerfile instruction: %w", err) + return nil, xerrors.Errorf("parse dockerfile instruction: %w", err) } if _, ok := instr.(*instructions.Stage); ok { @@ -89,6 +90,39 @@ func Parse(_ context.Context, r io.Reader, path string) ([]*dockerfile.Dockerfil return []*dockerfile.Dockerfile{&parsedFile}, nil } +func parseInstruction(child *parser.Node) (any, error) { + for { + instr, err := instructions.ParseInstruction(child) + if err == nil { + return instr, nil + } + + flagName := extractUnknownFlag(err.Error()) + if flagName == "" { + return nil, xerrors.Errorf("parse instruction %q: %w", child.Value, err) + } + + filtered := slices.DeleteFunc(child.Flags, func(flag string) bool { + return strings.HasPrefix(flag, flagName) + }) + + if len(filtered) == len(child.Flags) { + return nil, xerrors.Errorf("cannot remove unknown flag %q from flags %v", flagName, child.Flags) + } + child.Flags = filtered + } +} + +func extractUnknownFlag(errMsg string) string { + after, ok := strings.CutPrefix(errMsg, "unknown flag: ") + if !ok { + return "" + } + + flagName, _, _ := strings.Cut(after, " ") + return flagName +} + func originalFromHeredoc(node *parser.Node) string { var sb strings.Builder sb.WriteString(node.Original) diff --git a/pkg/iac/scanners/dockerfile/parser/parser_test.go b/pkg/iac/scanners/dockerfile/parser/parser_test.go index 2464904248..20a041ad21 100644 --- a/pkg/iac/scanners/dockerfile/parser/parser_test.go +++ b/pkg/iac/scanners/dockerfile/parser/parser_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/aquasecurity/trivy/pkg/iac/providers/dockerfile" "github.com/aquasecurity/trivy/pkg/iac/scanners/dockerfile/parser" ) @@ -57,6 +58,75 @@ CMD python /app/app.py assert.Equal(t, 4, commands[3].EndLine) } +func TestParserUnknownFlags(t *testing.T) { + input := `FROM ubuntu:18.04 +COPY --foo --chown=1 --bar=./test . /app +RUN --baz --baz --network=host make /app +ONBUILD RUN --foo make /app +CMD python /app/app.py +` + dfs, err := parser.Parse(t.Context(), strings.NewReader(input), "Dockerfile") + require.NoError(t, err) + require.Len(t, dfs, 1) + + expected := &dockerfile.Dockerfile{ + Stages: []dockerfile.Stage{ + { + Name: "ubuntu:18.04", + Commands: []dockerfile.Command{ + { + Cmd: "from", + Value: []string{"ubuntu:18.04"}, + Flags: []string{}, + Original: "FROM ubuntu:18.04", + Path: "Dockerfile", + StartLine: 1, + EndLine: 1, + }, + { + Cmd: "copy", + Value: []string{".", "/app"}, + Flags: []string{"--chown=1"}, + Original: "COPY --foo --chown=1 --bar=./test . /app", + Path: "Dockerfile", + StartLine: 2, + EndLine: 2, + }, + { + Cmd: "run", + Value: []string{"make /app"}, + Flags: []string{"--network=host"}, + Original: "RUN --baz --baz --network=host make /app", + Path: "Dockerfile", + StartLine: 3, + EndLine: 3, + }, + { + Cmd: "onbuild", + SubCmd: "RUN", + Value: []string{"make /app"}, + Flags: []string{}, + Original: "ONBUILD RUN --foo make /app", + Path: "Dockerfile", + StartLine: 4, + EndLine: 4, + }, + { + Cmd: "cmd", + Value: []string{"python /app/app.py"}, + Flags: []string{}, + Original: "CMD python /app/app.py", + Path: "Dockerfile", + StartLine: 5, + EndLine: 5, + }, + }, + }, + }, + } + assert.Equal(t, expected, dfs[0]) +} + func Test_ParseHeredocs(t *testing.T) { tests := []struct { name string