mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-05 20:40:16 -08:00
fix(misconf): handle heredocs in dockerfile instructions (#8284)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
@@ -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 {
|
||||
@@ -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)
|
||||
@@ -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, " ; ")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user