Skip to content

Commit 0a3887c

Browse files
authored
fix(misconf): handle heredocs in dockerfile instructions (#8284)
Signed-off-by: nikpivkin <[email protected]>
1 parent 846498d commit 0a3887c

File tree

2 files changed

+114
-3
lines changed

2 files changed

+114
-3
lines changed

pkg/iac/scanners/dockerfile/parser/parser.go

+57-3
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func Parse(_ context.Context, r io.Reader, path string) (any, error) {
3030

3131
instr, err := instructions.ParseInstruction(child)
3232
if err != nil {
33-
return nil, fmt.Errorf("process dockerfile instructions: %w", err)
33+
return nil, fmt.Errorf("parse dockerfile instruction: %w", err)
3434
}
3535

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

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

67+
// mark if the instruction is in exec form
68+
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#exec-form
6469
cmd.JSON = child.Attributes["json"]
65-
for n := child.Next; n != nil; n = n.Next {
66-
cmd.Value = append(cmd.Value, n.Value)
70+
71+
// heredoc may contain a script that will be executed in the shell, so we need to process it
72+
// https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#here-documents
73+
if len(child.Heredocs) > 0 && child.Next != nil {
74+
cmd.Original = originalFromHeredoc(child)
75+
cmd.Value = []string{processHeredoc(child)}
76+
} else {
77+
for n := child.Next; n != nil; n = n.Next {
78+
cmd.Value = append(cmd.Value, n.Value)
79+
}
6780
}
6881

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

7689
return &parsedFile, nil
7790
}
91+
92+
func originalFromHeredoc(node *parser.Node) string {
93+
var sb strings.Builder
94+
sb.WriteString(node.Original)
95+
sb.WriteRune('\n')
96+
for i, heredoc := range node.Heredocs {
97+
sb.WriteString(heredoc.Content)
98+
sb.WriteString(heredoc.Name)
99+
if i != len(node.Heredocs)-1 {
100+
sb.WriteRune('\n')
101+
}
102+
}
103+
104+
return sb.String()
105+
}
106+
107+
// heredoc processing taken from here
108+
// https://github.com/moby/buildkit/blob/9a39e2c112b7c98353c27e64602bc08f31fe356e/frontend/dockerfile/dockerfile2llb/convert.go#L1200
109+
func processHeredoc(node *parser.Node) string {
110+
if parser.MustParseHeredoc(node.Next.Value) == nil || strings.HasPrefix(node.Heredocs[0].Content, "#!") {
111+
// more complex heredoc is passed to the shell as is
112+
var sb strings.Builder
113+
sb.WriteString(node.Next.Value)
114+
for _, heredoc := range node.Heredocs {
115+
sb.WriteRune('\n')
116+
sb.WriteString(heredoc.Content)
117+
sb.WriteString(heredoc.Name)
118+
}
119+
return sb.String()
120+
}
121+
122+
// simple heredoc and the content is run in a shell
123+
content := node.Heredocs[0].Content
124+
if node.Heredocs[0].Chomp {
125+
content = parser.ChompHeredocContent(content)
126+
}
127+
128+
content = strings.ReplaceAll(content, "\r\n", "\n")
129+
cmds := strings.Split(strings.TrimSuffix(content, "\n"), "\n")
130+
return strings.Join(cmds, " ; ")
131+
}

pkg/iac/scanners/dockerfile/parser/parser_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,60 @@ CMD python /app/app.py
5959
assert.Equal(t, 4, commands[3].StartLine)
6060
assert.Equal(t, 4, commands[3].EndLine)
6161
}
62+
63+
func Test_ParseHeredocs(t *testing.T) {
64+
tests := []struct {
65+
name string
66+
src string
67+
expected string
68+
}{
69+
{
70+
name: "multi-line script",
71+
src: `RUN <<EOF
72+
apk add curl
73+
apk add git
74+
EOF`,
75+
expected: "apk add curl ; apk add git",
76+
},
77+
{
78+
name: "file redirection and chained command",
79+
src: `RUN cat <<EOF > /tmp/output && echo 'done'
80+
hello
81+
mr
82+
potato
83+
EOF`,
84+
expected: "cat <<EOF > /tmp/output && echo 'done'\nhello\nmr\npotato\nEOF",
85+
},
86+
{
87+
name: "redirect to file",
88+
src: `RUN <<EOF > /etc/config.yaml
89+
key1: value1
90+
key2: value2
91+
EOF`,
92+
expected: "<<EOF > /etc/config.yaml\nkey1: value1\nkey2: value2\nEOF",
93+
},
94+
{
95+
name: "with a shebang",
96+
src: `RUN <<EOF
97+
#!/usr/bin/env python
98+
print("hello world")
99+
EOF`,
100+
expected: "<<EOF\n#!/usr/bin/env python\nprint(\"hello world\")\nEOF",
101+
},
102+
}
103+
104+
for _, tt := range tests {
105+
t.Run(tt.name, func(t *testing.T) {
106+
res, err := parser.Parse(context.TODO(), strings.NewReader(tt.src), "Dockerfile")
107+
require.NoError(t, err)
108+
109+
df, ok := res.(*dockerfile.Dockerfile)
110+
require.True(t, ok)
111+
112+
cmd := df.Stages[0].Commands[0]
113+
114+
assert.Equal(t, tt.src, cmd.Original)
115+
assert.Equal(t, []string{tt.expected}, cmd.Value)
116+
})
117+
}
118+
}

0 commit comments

Comments
 (0)