Skip to content

Commit 09db1d4

Browse files
authored
feat(image): add logic to guess base layer for docker-cis scan (#4344)
1 parent 3f0721f commit 09db1d4

File tree

4 files changed

+127
-54
lines changed

4 files changed

+127
-54
lines changed

pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"golang.org/x/xerrors"
1010

1111
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
12+
"github.com/aquasecurity/trivy/pkg/fanal/image"
1213
"github.com/aquasecurity/trivy/pkg/fanal/types"
1314
"github.com/aquasecurity/trivy/pkg/mapfs"
1415
"github.com/aquasecurity/trivy/pkg/misconf"
@@ -40,7 +41,9 @@ func (a *historyAnalyzer) Analyze(ctx context.Context, input analyzer.ConfigAnal
4041
return nil, nil
4142
}
4243
dockerfile := new(bytes.Buffer)
43-
for _, h := range input.Config.History {
44+
baseLayerIndex := image.GuessBaseImageIndex(input.Config.History)
45+
for i := baseLayerIndex + 1; i < len(input.Config.History); i++ {
46+
h := input.Config.History[i]
4447
var createdBy string
4548
switch {
4649
case strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop)"):

pkg/fanal/analyzer/imgconf/dockerfile/dockerfile_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,68 @@ func Test_historyAnalyzer_Analyze(t *testing.T) {
9494
},
9595
},
9696
},
97+
{
98+
name: "happy path. Base layer is found",
99+
input: analyzer.ConfigAnalysisInput{
100+
Config: &v1.ConfigFile{
101+
Config: v1.Config{
102+
Healthcheck: &v1.HealthConfig{
103+
Test: []string{"CMD-SHELL", "curl --fail http://localhost:3000 || exit 1"},
104+
Interval: time.Second * 10,
105+
Timeout: time.Second * 3,
106+
},
107+
},
108+
History: []v1.History{
109+
{
110+
CreatedBy: "/bin/sh -c #(nop) ADD file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in /",
111+
EmptyLayer: false,
112+
},
113+
{
114+
CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/sh\"]`,
115+
EmptyLayer: true,
116+
},
117+
{
118+
CreatedBy: `HEALTHCHECK &{["CMD-SHELL" "curl --fail http://localhost:3000 || exit 1"] "10s" "3s" "0s" '\x00'}`,
119+
EmptyLayer: false,
120+
},
121+
{
122+
CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/sh\"]`,
123+
EmptyLayer: true,
124+
},
125+
},
126+
},
127+
},
128+
want: &analyzer.ConfigAnalysisResult{
129+
Misconfiguration: &types.Misconfiguration{
130+
FileType: "dockerfile",
131+
FilePath: "Dockerfile",
132+
Failures: types.MisconfResults{
133+
types.MisconfResult{
134+
Namespace: "builtin.dockerfile.DS002",
135+
Query: "data.builtin.dockerfile.DS002.deny",
136+
Message: "Specify at least 1 USER command in Dockerfile with non-root user as argument",
137+
PolicyMetadata: types.PolicyMetadata{
138+
ID: "DS002",
139+
AVDID: "AVD-DS-0002",
140+
Type: "Dockerfile Security Check",
141+
Title: "Image user should not be 'root'",
142+
Description: "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.",
143+
Severity: "HIGH",
144+
RecommendedActions: "Add 'USER <non root user name>' line to the Dockerfile",
145+
References: []string{
146+
"https://docs.docker." +
147+
"com/develop/develop-images/dockerfile_best-practices/",
148+
},
149+
},
150+
CauseMetadata: types.CauseMetadata{
151+
Provider: "Dockerfile",
152+
Service: "general",
153+
},
154+
},
155+
},
156+
},
157+
},
158+
},
97159
{
98160
name: "nil config",
99161
input: analyzer.ConfigAnalysisInput{

pkg/fanal/artifact/image/image.go

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/aquasecurity/trivy/pkg/fanal/artifact"
2121
"github.com/aquasecurity/trivy/pkg/fanal/cache"
2222
"github.com/aquasecurity/trivy/pkg/fanal/handler"
23+
"github.com/aquasecurity/trivy/pkg/fanal/image"
2324
"github.com/aquasecurity/trivy/pkg/fanal/log"
2425
"github.com/aquasecurity/trivy/pkg/fanal/types"
2526
"github.com/aquasecurity/trivy/pkg/fanal/walker"
@@ -450,64 +451,13 @@ func (a Artifact) inspectConfig(ctx context.Context, imageID string, osFound typ
450451
return nil
451452
}
452453

453-
// Guess layers in base image (call base layers).
454-
//
455-
// e.g. In the following example, we should detect layers in debian:8.
456-
//
457-
// FROM debian:8
458-
// RUN apt-get update
459-
// COPY mysecret /
460-
// ENTRYPOINT ["entrypoint.sh"]
461-
// CMD ["somecmd"]
462-
//
463-
// debian:8 may be like
464-
//
465-
// ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in /
466-
// CMD ["/bin/sh"]
467-
//
468-
// In total, it would be like:
469-
//
470-
// ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in /
471-
// CMD ["/bin/sh"] # empty layer (detected)
472-
// RUN apt-get update
473-
// COPY mysecret /
474-
// ENTRYPOINT ["entrypoint.sh"] # empty layer (skipped)
475-
// CMD ["somecmd"] # empty layer (skipped)
476-
//
477-
// This method tries to detect CMD in the second line and assume the first line is a base layer.
478-
// 1. Iterate histories from the bottom.
479-
// 2. Skip all the empty layers at the bottom. In the above example, "entrypoint.sh" and "somecmd" will be skipped
480-
// 3. If it finds CMD, it assumes that it is the end of base layers.
481-
// 4. It gets all the layers as base layers above the CMD found in #3.
454+
// guessBaseLayers guesses layers in base image (call base layers).
482455
func (a Artifact) guessBaseLayers(diffIDs []string, configFile *v1.ConfigFile) []string {
483456
if configFile == nil {
484457
return nil
485458
}
486459

487-
baseImageIndex := -1
488-
var foundNonEmpty bool
489-
for i := len(configFile.History) - 1; i >= 0; i-- {
490-
h := configFile.History[i]
491-
492-
// Skip the last CMD, ENTRYPOINT, etc.
493-
if !foundNonEmpty {
494-
if h.EmptyLayer {
495-
continue
496-
}
497-
foundNonEmpty = true
498-
}
499-
500-
if !h.EmptyLayer {
501-
continue
502-
}
503-
504-
// Detect CMD instruction in base image
505-
if strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop) CMD") ||
506-
strings.HasPrefix(h.CreatedBy, "CMD") { // BuildKit
507-
baseImageIndex = i
508-
break
509-
}
510-
}
460+
baseImageIndex := image.GuessBaseImageIndex(configFile.History)
511461

512462
// Diff IDs don't include empty layers, so the index is different from histories
513463
var diffIDIndex int

pkg/fanal/image/image.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package image
22

33
import (
44
"context"
5+
"strings"
56

67
"github.com/google/go-containerregistry/pkg/name"
78
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -75,3 +76,60 @@ func LayerIDs(img v1.Image) ([]string, error) {
7576
}
7677
return layerIDs, nil
7778
}
79+
80+
// GuessBaseImageIndex tries to guess index of base layer
81+
//
82+
// e.g. In the following example, we should detect layers in debian:8.
83+
//
84+
// FROM debian:8
85+
// RUN apt-get update
86+
// COPY mysecret /
87+
// ENTRYPOINT ["entrypoint.sh"]
88+
// CMD ["somecmd"]
89+
//
90+
// debian:8 may be like
91+
//
92+
// ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in /
93+
// CMD ["/bin/sh"]
94+
//
95+
// In total, it would be like:
96+
//
97+
// ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in /
98+
// CMD ["/bin/sh"] # empty layer (detected)
99+
// RUN apt-get update
100+
// COPY mysecret /
101+
// ENTRYPOINT ["entrypoint.sh"] # empty layer (skipped)
102+
// CMD ["somecmd"] # empty layer (skipped)
103+
//
104+
// This method tries to detect CMD in the second line and assume the first line is a base layer.
105+
// 1. Iterate histories from the bottom.
106+
// 2. Skip all the empty layers at the bottom. In the above example, "entrypoint.sh" and "somecmd" will be skipped
107+
// 3. If it finds CMD, it assumes that it is the end of base layers.
108+
// 4. It gets all the layers as base layers above the CMD found in #3.
109+
func GuessBaseImageIndex(histories []v1.History) int {
110+
baseImageIndex := -1
111+
var foundNonEmpty bool
112+
for i := len(histories) - 1; i >= 0; i-- {
113+
h := histories[i]
114+
115+
// Skip the last CMD, ENTRYPOINT, etc.
116+
if !foundNonEmpty {
117+
if h.EmptyLayer {
118+
continue
119+
}
120+
foundNonEmpty = true
121+
}
122+
123+
if !h.EmptyLayer {
124+
continue
125+
}
126+
127+
// Detect CMD instruction in base image
128+
if strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop) CMD") ||
129+
strings.HasPrefix(h.CreatedBy, "CMD") { // BuildKit
130+
baseImageIndex = i
131+
break
132+
}
133+
}
134+
return baseImageIndex
135+
}

0 commit comments

Comments
 (0)