mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 15:50:15 -08:00
fix: improve conversion of image config to Dockerfile (#8308)
Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
||||
@@ -49,61 +50,10 @@ func (a *historyAnalyzer) Analyze(ctx context.Context, input analyzer.ConfigAnal
|
||||
if input.Config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
dockerfile := new(bytes.Buffer)
|
||||
var userFound bool
|
||||
baseLayerIndex := image.GuessBaseImageIndex(input.Config.History)
|
||||
for i := baseLayerIndex + 1; i < len(input.Config.History); i++ {
|
||||
h := input.Config.History[i]
|
||||
var createdBy string
|
||||
switch {
|
||||
case strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop)"):
|
||||
// Instruction other than RUN
|
||||
createdBy = strings.TrimPrefix(h.CreatedBy, "/bin/sh -c #(nop)")
|
||||
case strings.HasPrefix(h.CreatedBy, "/bin/sh -c"):
|
||||
// RUN instruction
|
||||
createdBy = strings.ReplaceAll(h.CreatedBy, "/bin/sh -c", "RUN")
|
||||
case strings.HasSuffix(h.CreatedBy, "# buildkit"):
|
||||
// buildkit instructions
|
||||
// COPY ./foo /foo # buildkit
|
||||
// ADD ./foo.txt /foo.txt # buildkit
|
||||
// RUN /bin/sh -c ls -hl /foo # buildkit
|
||||
createdBy = strings.TrimSuffix(h.CreatedBy, "# buildkit")
|
||||
if strings.HasPrefix(h.CreatedBy, "RUN /bin/sh -c") {
|
||||
createdBy = strings.ReplaceAll(createdBy, "RUN /bin/sh -c", "RUN")
|
||||
}
|
||||
case strings.HasPrefix(h.CreatedBy, "USER"):
|
||||
// USER instruction
|
||||
createdBy = h.CreatedBy
|
||||
userFound = true
|
||||
case strings.HasPrefix(h.CreatedBy, "HEALTHCHECK"):
|
||||
// HEALTHCHECK instruction
|
||||
var interval, timeout, startPeriod, retries, command string
|
||||
if input.Config.Config.Healthcheck.Interval != 0 {
|
||||
interval = fmt.Sprintf("--interval=%s ", input.Config.Config.Healthcheck.Interval)
|
||||
}
|
||||
if input.Config.Config.Healthcheck.Timeout != 0 {
|
||||
timeout = fmt.Sprintf("--timeout=%s ", input.Config.Config.Healthcheck.Timeout)
|
||||
}
|
||||
if input.Config.Config.Healthcheck.StartPeriod != 0 {
|
||||
startPeriod = fmt.Sprintf("--startPeriod=%s ", input.Config.Config.Healthcheck.StartPeriod)
|
||||
}
|
||||
if input.Config.Config.Healthcheck.Retries != 0 {
|
||||
retries = fmt.Sprintf("--retries=%d ", input.Config.Config.Healthcheck.Retries)
|
||||
}
|
||||
command = strings.Join(input.Config.Config.Healthcheck.Test, " ")
|
||||
command = strings.ReplaceAll(command, "CMD-SHELL", "CMD")
|
||||
createdBy = fmt.Sprintf("HEALTHCHECK %s%s%s%s%s", interval, timeout, startPeriod, retries, command)
|
||||
}
|
||||
dockerfile.WriteString(strings.TrimSpace(createdBy) + "\n")
|
||||
}
|
||||
|
||||
if !userFound && input.Config.Config.User != "" {
|
||||
user := fmt.Sprintf("USER %s", input.Config.Config.User)
|
||||
dockerfile.WriteString(user)
|
||||
}
|
||||
|
||||
fsys := mapfs.New()
|
||||
if err := fsys.WriteVirtualFile("Dockerfile", dockerfile.Bytes(), 0600); err != nil {
|
||||
if err := fsys.WriteVirtualFile(
|
||||
"Dockerfile", imageConfigToDockerfile(input.Config), 0600); err != nil {
|
||||
return nil, xerrors.Errorf("mapfs write error: %w", err)
|
||||
}
|
||||
|
||||
@@ -121,6 +71,79 @@ func (a *historyAnalyzer) Analyze(ctx context.Context, input analyzer.ConfigAnal
|
||||
}, nil
|
||||
}
|
||||
|
||||
func imageConfigToDockerfile(cfg *v1.ConfigFile) []byte {
|
||||
dockerfile := new(bytes.Buffer)
|
||||
var userFound bool
|
||||
baseLayerIndex := image.GuessBaseImageIndex(cfg.History)
|
||||
for i := baseLayerIndex + 1; i < len(cfg.History); i++ {
|
||||
h := cfg.History[i]
|
||||
var createdBy string
|
||||
switch {
|
||||
case strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop)"):
|
||||
// Instruction other than RUN
|
||||
createdBy = strings.TrimPrefix(h.CreatedBy, "/bin/sh -c #(nop)")
|
||||
case strings.HasPrefix(h.CreatedBy, "/bin/sh -c"):
|
||||
// RUN instruction
|
||||
createdBy = buildRunInstruction(createdBy)
|
||||
case strings.HasSuffix(h.CreatedBy, "# buildkit"):
|
||||
// buildkit instructions
|
||||
// COPY ./foo /foo # buildkit
|
||||
// ADD ./foo.txt /foo.txt # buildkit
|
||||
// RUN /bin/sh -c ls -hl /foo # buildkit
|
||||
createdBy = strings.TrimSuffix(h.CreatedBy, "# buildkit")
|
||||
createdBy = buildRunInstruction(createdBy)
|
||||
case strings.HasPrefix(h.CreatedBy, "USER"):
|
||||
// USER instruction
|
||||
createdBy = h.CreatedBy
|
||||
userFound = true
|
||||
case strings.HasPrefix(h.CreatedBy, "HEALTHCHECK"):
|
||||
// HEALTHCHECK instruction
|
||||
createdBy = buildHealthcheckInstruction(cfg.Config.Healthcheck)
|
||||
default:
|
||||
for _, prefix := range []string{"ARG", "ENV", "ENTRYPOINT"} {
|
||||
strings.HasPrefix(h.CreatedBy, prefix)
|
||||
createdBy = h.CreatedBy
|
||||
break
|
||||
}
|
||||
}
|
||||
dockerfile.WriteString(strings.TrimSpace(createdBy) + "\n")
|
||||
}
|
||||
|
||||
if !userFound && cfg.Config.User != "" {
|
||||
user := fmt.Sprintf("USER %s", cfg.Config.User)
|
||||
dockerfile.WriteString(user)
|
||||
}
|
||||
|
||||
return dockerfile.Bytes()
|
||||
}
|
||||
|
||||
func buildRunInstruction(s string) string {
|
||||
pos := strings.Index(s, "/bin/sh -c")
|
||||
if pos == -1 {
|
||||
return s
|
||||
}
|
||||
return "RUN" + s[pos+len("/bin/sh -c"):]
|
||||
}
|
||||
|
||||
func buildHealthcheckInstruction(health *v1.HealthConfig) string {
|
||||
var interval, timeout, startPeriod, retries, command string
|
||||
if health.Interval != 0 {
|
||||
interval = fmt.Sprintf("--interval=%s ", health.Interval)
|
||||
}
|
||||
if health.Timeout != 0 {
|
||||
timeout = fmt.Sprintf("--timeout=%s ", health.Timeout)
|
||||
}
|
||||
if health.StartPeriod != 0 {
|
||||
startPeriod = fmt.Sprintf("--startPeriod=%s ", health.StartPeriod)
|
||||
}
|
||||
if health.Retries != 0 {
|
||||
retries = fmt.Sprintf("--retries=%d ", health.Retries)
|
||||
}
|
||||
command = strings.Join(health.Test, " ")
|
||||
command = strings.ReplaceAll(command, "CMD-SHELL", "CMD")
|
||||
return fmt.Sprintf("HEALTHCHECK %s%s%s%s%s", interval, timeout, startPeriod, retries, command)
|
||||
}
|
||||
|
||||
func (a *historyAnalyzer) Required(_ types.OS) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package dockerfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/moby/buildkit/frontend/dockerfile/parser"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -343,3 +345,101 @@ func Test_historyAnalyzer_Analyze(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ImageConfigToDockerfile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input *v1.ConfigFile
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "run instruction with build args",
|
||||
input: &v1.ConfigFile{
|
||||
History: []v1.History{
|
||||
{
|
||||
CreatedBy: "RUN |1 pkg=curl /bin/sh -c apk add $pkg # buildkit",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "RUN apk add $pkg\n",
|
||||
},
|
||||
{
|
||||
name: "healthcheck instruction with system's default shell",
|
||||
input: &v1.ConfigFile{
|
||||
History: []v1.History{
|
||||
{
|
||||
CreatedBy: "HEALTHCHECK &{[\"CMD-SHELL\" \"curl -f http://localhost/ || exit 1\"] \"5m0s\" \"3s\" \"1s\" \"5s\" '\\x03'}",
|
||||
},
|
||||
},
|
||||
Config: v1.Config{
|
||||
Healthcheck: &v1.HealthConfig{
|
||||
Test: []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"},
|
||||
Interval: time.Minute * 5,
|
||||
Timeout: time.Second * 3,
|
||||
StartPeriod: time.Second * 1,
|
||||
Retries: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "HEALTHCHECK --interval=5m0s --timeout=3s --startPeriod=1s --retries=3 CMD curl -f http://localhost/ || exit 1\n",
|
||||
},
|
||||
{
|
||||
name: "healthcheck instruction exec arguments directly",
|
||||
input: &v1.ConfigFile{
|
||||
History: []v1.History{
|
||||
{
|
||||
CreatedBy: "HEALTHCHECK &{[\"CMD\" \"curl\" \"-f\" \"http://localhost/\" \"||\" \"exit 1\"] \"0s\" \"0s\" \"0s\" \"0s\" '\x03'}",
|
||||
},
|
||||
},
|
||||
Config: v1.Config{
|
||||
Healthcheck: &v1.HealthConfig{
|
||||
Test: []string{"CMD", "curl", "-f", "http://localhost/", "||", "exit 1"},
|
||||
Retries: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "HEALTHCHECK --retries=3 CMD curl -f http://localhost/ || exit 1\n",
|
||||
},
|
||||
{
|
||||
name: "nop, no run instruction",
|
||||
input: &v1.ConfigFile{
|
||||
History: []v1.History{
|
||||
{
|
||||
CreatedBy: "/bin/sh -c #(nop) ARG TAG=latest",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "ARG TAG=latest\n",
|
||||
},
|
||||
{
|
||||
name: "buildkit metadata instructions",
|
||||
input: &v1.ConfigFile{
|
||||
History: []v1.History{
|
||||
{
|
||||
CreatedBy: "ARG TAG=latest",
|
||||
},
|
||||
{
|
||||
CreatedBy: "ENV TAG=latest",
|
||||
},
|
||||
{
|
||||
CreatedBy: "ENTRYPOINT [\"/bin/sh\" \"-c\" \"echo test\"]",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `ARG TAG=latest
|
||||
ENV TAG=latest
|
||||
ENTRYPOINT ["/bin/sh" "-c" "echo test"]
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := imageConfigToDockerfile(tt.input)
|
||||
_, err := parser.Parse(bytes.NewReader(got))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tt.expected, string(got))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user