|
6 | 6 | "fmt"
|
7 | 7 | "strings"
|
8 | 8 |
|
| 9 | + v1 "github.com/google/go-containerregistry/pkg/v1" |
9 | 10 | "golang.org/x/xerrors"
|
10 | 11 |
|
11 | 12 | "github.com/aquasecurity/trivy/pkg/fanal/analyzer"
|
@@ -49,76 +50,98 @@ func (a *historyAnalyzer) Analyze(ctx context.Context, input analyzer.ConfigAnal
|
49 | 50 | if input.Config == nil {
|
50 | 51 | return nil, nil
|
51 | 52 | }
|
| 53 | + |
| 54 | + fsys := mapfs.New() |
| 55 | + if err := fsys.WriteVirtualFile( |
| 56 | + "Dockerfile", imageConfigToDockerfile(input.Config), 0600); err != nil { |
| 57 | + return nil, xerrors.Errorf("mapfs write error: %w", err) |
| 58 | + } |
| 59 | + |
| 60 | + misconfs, err := a.scanner.Scan(ctx, fsys) |
| 61 | + if err != nil { |
| 62 | + return nil, xerrors.Errorf("history scan error: %w", err) |
| 63 | + } |
| 64 | + // The result should be a single element as it passes one Dockerfile. |
| 65 | + if len(misconfs) != 1 { |
| 66 | + return nil, nil |
| 67 | + } |
| 68 | + |
| 69 | + return &analyzer.ConfigAnalysisResult{ |
| 70 | + Misconfiguration: &misconfs[0], |
| 71 | + }, nil |
| 72 | +} |
| 73 | + |
| 74 | +func imageConfigToDockerfile(cfg *v1.ConfigFile) []byte { |
52 | 75 | dockerfile := new(bytes.Buffer)
|
53 | 76 | var userFound bool
|
54 |
| - baseLayerIndex := image.GuessBaseImageIndex(input.Config.History) |
55 |
| - for i := baseLayerIndex + 1; i < len(input.Config.History); i++ { |
56 |
| - h := input.Config.History[i] |
| 77 | + baseLayerIndex := image.GuessBaseImageIndex(cfg.History) |
| 78 | + for i := baseLayerIndex + 1; i < len(cfg.History); i++ { |
| 79 | + h := cfg.History[i] |
57 | 80 | var createdBy string
|
58 | 81 | switch {
|
59 | 82 | case strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop)"):
|
60 | 83 | // Instruction other than RUN
|
61 | 84 | createdBy = strings.TrimPrefix(h.CreatedBy, "/bin/sh -c #(nop)")
|
62 | 85 | case strings.HasPrefix(h.CreatedBy, "/bin/sh -c"):
|
63 | 86 | // RUN instruction
|
64 |
| - createdBy = strings.ReplaceAll(h.CreatedBy, "/bin/sh -c", "RUN") |
| 87 | + createdBy = buildRunInstruction(createdBy) |
65 | 88 | case strings.HasSuffix(h.CreatedBy, "# buildkit"):
|
66 | 89 | // buildkit instructions
|
67 | 90 | // COPY ./foo /foo # buildkit
|
68 | 91 | // ADD ./foo.txt /foo.txt # buildkit
|
69 | 92 | // RUN /bin/sh -c ls -hl /foo # buildkit
|
70 | 93 | createdBy = strings.TrimSuffix(h.CreatedBy, "# buildkit")
|
71 |
| - if strings.HasPrefix(h.CreatedBy, "RUN /bin/sh -c") { |
72 |
| - createdBy = strings.ReplaceAll(createdBy, "RUN /bin/sh -c", "RUN") |
73 |
| - } |
| 94 | + createdBy = buildRunInstruction(createdBy) |
74 | 95 | case strings.HasPrefix(h.CreatedBy, "USER"):
|
75 | 96 | // USER instruction
|
76 | 97 | createdBy = h.CreatedBy
|
77 | 98 | userFound = true
|
78 | 99 | case strings.HasPrefix(h.CreatedBy, "HEALTHCHECK"):
|
79 | 100 | // HEALTHCHECK instruction
|
80 |
| - var interval, timeout, startPeriod, retries, command string |
81 |
| - if input.Config.Config.Healthcheck.Interval != 0 { |
82 |
| - interval = fmt.Sprintf("--interval=%s ", input.Config.Config.Healthcheck.Interval) |
| 101 | + createdBy = buildHealthcheckInstruction(cfg.Config.Healthcheck) |
| 102 | + default: |
| 103 | + for _, prefix := range []string{"ARG", "ENV", "ENTRYPOINT"} { |
| 104 | + strings.HasPrefix(h.CreatedBy, prefix) |
| 105 | + createdBy = h.CreatedBy |
| 106 | + break |
83 | 107 | }
|
84 |
| - if input.Config.Config.Healthcheck.Timeout != 0 { |
85 |
| - timeout = fmt.Sprintf("--timeout=%s ", input.Config.Config.Healthcheck.Timeout) |
86 |
| - } |
87 |
| - if input.Config.Config.Healthcheck.StartPeriod != 0 { |
88 |
| - startPeriod = fmt.Sprintf("--startPeriod=%s ", input.Config.Config.Healthcheck.StartPeriod) |
89 |
| - } |
90 |
| - if input.Config.Config.Healthcheck.Retries != 0 { |
91 |
| - retries = fmt.Sprintf("--retries=%d ", input.Config.Config.Healthcheck.Retries) |
92 |
| - } |
93 |
| - command = strings.Join(input.Config.Config.Healthcheck.Test, " ") |
94 |
| - command = strings.ReplaceAll(command, "CMD-SHELL", "CMD") |
95 |
| - createdBy = fmt.Sprintf("HEALTHCHECK %s%s%s%s%s", interval, timeout, startPeriod, retries, command) |
96 | 108 | }
|
97 | 109 | dockerfile.WriteString(strings.TrimSpace(createdBy) + "\n")
|
98 | 110 | }
|
99 | 111 |
|
100 |
| - if !userFound && input.Config.Config.User != "" { |
101 |
| - user := fmt.Sprintf("USER %s", input.Config.Config.User) |
| 112 | + if !userFound && cfg.Config.User != "" { |
| 113 | + user := fmt.Sprintf("USER %s", cfg.Config.User) |
102 | 114 | dockerfile.WriteString(user)
|
103 | 115 | }
|
104 | 116 |
|
105 |
| - fsys := mapfs.New() |
106 |
| - if err := fsys.WriteVirtualFile("Dockerfile", dockerfile.Bytes(), 0600); err != nil { |
107 |
| - return nil, xerrors.Errorf("mapfs write error: %w", err) |
| 117 | + return dockerfile.Bytes() |
| 118 | +} |
| 119 | + |
| 120 | +func buildRunInstruction(s string) string { |
| 121 | + pos := strings.Index(s, "/bin/sh -c") |
| 122 | + if pos == -1 { |
| 123 | + return s |
108 | 124 | }
|
| 125 | + return "RUN" + s[pos+len("/bin/sh -c"):] |
| 126 | +} |
109 | 127 |
|
110 |
| - misconfs, err := a.scanner.Scan(ctx, fsys) |
111 |
| - if err != nil { |
112 |
| - return nil, xerrors.Errorf("history scan error: %w", err) |
| 128 | +func buildHealthcheckInstruction(health *v1.HealthConfig) string { |
| 129 | + var interval, timeout, startPeriod, retries, command string |
| 130 | + if health.Interval != 0 { |
| 131 | + interval = fmt.Sprintf("--interval=%s ", health.Interval) |
113 | 132 | }
|
114 |
| - // The result should be a single element as it passes one Dockerfile. |
115 |
| - if len(misconfs) != 1 { |
116 |
| - return nil, nil |
| 133 | + if health.Timeout != 0 { |
| 134 | + timeout = fmt.Sprintf("--timeout=%s ", health.Timeout) |
117 | 135 | }
|
118 |
| - |
119 |
| - return &analyzer.ConfigAnalysisResult{ |
120 |
| - Misconfiguration: &misconfs[0], |
121 |
| - }, nil |
| 136 | + if health.StartPeriod != 0 { |
| 137 | + startPeriod = fmt.Sprintf("--startPeriod=%s ", health.StartPeriod) |
| 138 | + } |
| 139 | + if health.Retries != 0 { |
| 140 | + retries = fmt.Sprintf("--retries=%d ", health.Retries) |
| 141 | + } |
| 142 | + command = strings.Join(health.Test, " ") |
| 143 | + command = strings.ReplaceAll(command, "CMD-SHELL", "CMD") |
| 144 | + return fmt.Sprintf("HEALTHCHECK %s%s%s%s%s", interval, timeout, startPeriod, retries, command) |
122 | 145 | }
|
123 | 146 |
|
124 | 147 | func (a *historyAnalyzer) Required(_ types.OS) bool {
|
|
0 commit comments