Skip to content

Commit

Permalink
fix(misconf): handle heredocs in dockerfile instructions (#8284)
Browse files Browse the repository at this point in the history
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
  • Loading branch information
nikpivkin authored Jan 29, 2025
1 parent 846498d commit 0a3887c
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 3 deletions.
60 changes: 57 additions & 3 deletions pkg/iac/scanners/dockerfile/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func Parse(_ context.Context, r io.Reader, path string) (any, error) {

instr, err := instructions.ParseInstruction(child)
if err != nil {
return nil, fmt.Errorf("process dockerfile instructions: %w", err)
return nil, fmt.Errorf("parse dockerfile instruction: %w", err)
}

if _, ok := instr.(*instructions.Stage); ok {
Expand All @@ -56,14 +56,27 @@ func Parse(_ context.Context, r io.Reader, path string) (any, error) {
EndLine: child.EndLine,
}

// processing statement with sub-statement
// example: ONBUILD RUN foo bar
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#onbuild
if child.Next != nil && len(child.Next.Children) > 0 {
cmd.SubCmd = child.Next.Children[0].Value
child = child.Next.Children[0]
}

// mark if the instruction is in exec form
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#exec-form
cmd.JSON = child.Attributes["json"]
for n := child.Next; n != nil; n = n.Next {
cmd.Value = append(cmd.Value, n.Value)

// heredoc may contain a script that will be executed in the shell, so we need to process it
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#here-documents
if len(child.Heredocs) > 0 && child.Next != nil {
cmd.Original = originalFromHeredoc(child)
cmd.Value = []string{processHeredoc(child)}
} else {
for n := child.Next; n != nil; n = n.Next {
cmd.Value = append(cmd.Value, n.Value)
}
}

stage.Commands = append(stage.Commands, cmd)
Expand All @@ -75,3 +88,44 @@ func Parse(_ context.Context, r io.Reader, path string) (any, error) {

return &parsedFile, nil
}

func originalFromHeredoc(node *parser.Node) string {
var sb strings.Builder
sb.WriteString(node.Original)
sb.WriteRune('\n')
for i, heredoc := range node.Heredocs {
sb.WriteString(heredoc.Content)
sb.WriteString(heredoc.Name)
if i != len(node.Heredocs)-1 {
sb.WriteRune('\n')
}
}

return sb.String()
}

// heredoc processing taken from here
// https://github.com/moby/buildkit/blob/9a39e2c112b7c98353c27e64602bc08f31fe356e/frontend/dockerfile/dockerfile2llb/convert.go#L1200
func processHeredoc(node *parser.Node) string {
if parser.MustParseHeredoc(node.Next.Value) == nil || strings.HasPrefix(node.Heredocs[0].Content, "#!") {
// more complex heredoc is passed to the shell as is
var sb strings.Builder
sb.WriteString(node.Next.Value)
for _, heredoc := range node.Heredocs {
sb.WriteRune('\n')
sb.WriteString(heredoc.Content)
sb.WriteString(heredoc.Name)
}
return sb.String()
}

// simple heredoc and the content is run in a shell
content := node.Heredocs[0].Content
if node.Heredocs[0].Chomp {
content = parser.ChompHeredocContent(content)
}

content = strings.ReplaceAll(content, "\r\n", "\n")
cmds := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
return strings.Join(cmds, " ; ")
}
57 changes: 57 additions & 0 deletions pkg/iac/scanners/dockerfile/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,60 @@ CMD python /app/app.py
assert.Equal(t, 4, commands[3].StartLine)
assert.Equal(t, 4, commands[3].EndLine)
}

func Test_ParseHeredocs(t *testing.T) {
tests := []struct {
name string
src string
expected string
}{
{
name: "multi-line script",
src: `RUN <<EOF
apk add curl
apk add git
EOF`,
expected: "apk add curl ; apk add git",
},
{
name: "file redirection and chained command",
src: `RUN cat <<EOF > /tmp/output && echo 'done'
hello
mr
potato
EOF`,
expected: "cat <<EOF > /tmp/output && echo 'done'\nhello\nmr\npotato\nEOF",
},
{
name: "redirect to file",
src: `RUN <<EOF > /etc/config.yaml
key1: value1
key2: value2
EOF`,
expected: "<<EOF > /etc/config.yaml\nkey1: value1\nkey2: value2\nEOF",
},
{
name: "with a shebang",
src: `RUN <<EOF
#!/usr/bin/env python
print("hello world")
EOF`,
expected: "<<EOF\n#!/usr/bin/env python\nprint(\"hello world\")\nEOF",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := parser.Parse(context.TODO(), strings.NewReader(tt.src), "Dockerfile")
require.NoError(t, err)

df, ok := res.(*dockerfile.Dockerfile)
require.True(t, ok)

cmd := df.Stages[0].Commands[0]

assert.Equal(t, tt.src, cmd.Original)
assert.Equal(t, []string{tt.expected}, cmd.Value)
})
}
}

0 comments on commit 0a3887c

Please sign in to comment.