Skip to content

Commit ccb35a1

Browse files
authored
feat: add support for custom Trivy ignore file (#2750)
* fix(helm): improve `trivy.ignoreFile` rendering * feat: add support a custom trivyignore name * test: add test to fix the previous behavior for .trivyignore * chore: clean up unused variable * chore: use a correct path joining * chore: use a default vuln image * test: fix CVE ignoring * chore(test): remove redundant logic
1 parent 3e530e1 commit ccb35a1

File tree

10 files changed

+166
-11
lines changed

10 files changed

+166
-11
lines changed

deploy/helm/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ Keeps security report resources updated
147147
| trivy.httpProxy | string | `nil` | httpProxy is the HTTP proxy used by Trivy to download the vulnerabilities database from GitHub. |
148148
| trivy.httpsProxy | string | `nil` | httpsProxy is the HTTPS proxy used by Trivy to download the vulnerabilities database from GitHub. |
149149
| trivy.ignoreFile | string | `nil` | ignoreFile can be used to tell Trivy to ignore vulnerabilities by ID (one per line) |
150+
| trivy.ignoreFileName | string | `""` | ignoreFileName sets the file name that will be mounted inside the scanner container. Defaults to ".trivyignore" if not set. |
150151
| trivy.ignoreUnfixed | bool | `false` | ignoreUnfixed is the flag to show only fixed vulnerabilities in vulnerabilities reported by Trivy. Set to true to enable it. |
151152
| trivy.image.imagePullSecret | string | `nil` | imagePullSecret is the secret name to be used when pulling trivy image from private registries example : reg-secret It is the user responsibility to create the secret for the private registry in `trivy-operator` namespace |
152153
| trivy.image.pullPolicy | string | `"IfNotPresent"` | pullPolicy is the imge pull policy used for trivy image , valid values are (Always, Never, IfNotPresent) |

deploy/helm/templates/configmaps/trivy.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,10 @@ data:
8686
{{- end }}
8787
{{- if .Values.trivy.ignoreFile }}
8888
trivy.ignoreFile: |
89-
{{- range .Values.trivy.ignoreFile }}
90-
{{ . | indent 4 }}
89+
{{- .Values.trivy.ignoreFile | toYaml | nindent 4 }}
9190
{{- end }}
91+
{{- with .Values.trivy.ignoreFileName }}
92+
trivy.ignoreFileName: {{ . | quote }}
9293
{{- end }}
9394
{{- range $k, $v := .Values.trivy }}
9495
{{- if hasPrefix "ignorePolicy" $k }}

deploy/helm/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,10 @@ trivy:
458458
# - CVE-1970-0001
459459
# - CVE-1970-0002
460460

461+
# -- ignoreFileName sets the file name that will be mounted inside the scanner container.
462+
# Defaults to ".trivyignore" if not set.
463+
ignoreFileName: ""
464+
461465
# -- configFile can be used to tell Trivy to use specific options available only in the config file (e.g. Mirror registries).
462466
configFile: ~
463467
# configFile:

docs/docs/vulnerability-scanning/trivy.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ EOF
110110
| `trivy.offlineScan` | N/A | Whether to enable the offline scan mode of Trivy preventing outgoing calls, e.g. to <search.maven.org> for additional vulnerability information. Set to `"true"` to enable it. |
111111
| `trivy.skipFiles` | N/A | A comma separated list of file paths for Trivy to skip traversal. |
112112
| `trivy.skipDirs` | N/A | A comma separated list of directories for Trivy to skip traversal. |
113-
| `trivy.ignoreFile` | N/A | It specifies the `.trivyignore` file which contains a list of vulnerability IDs to be ignored from vulnerabilities reported by Trivy. |
113+
| `trivy.ignoreFile` | N/A | It specifies the contents of the ignore file which contains a list of vulnerability IDs to be ignored from vulnerabilities reported by Trivy (one per line). |
114+
| `trivy.ignoreFileName` | `.trivyignore` | The file name to mount inside the scanner container for the ignore list. |
114115
| `trivy.ignorePolicy` | N/A | It specifies a fallback [policy](https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/#by-rego) file which allows to customize which vulnerabilities are reported by Trivy. |
115116
| `trivy.ignorePolicy.{ns}` | N/A | It specifies a namespace specific [policy](https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/#by-rego) file which allows to customize which vulnerabilities are reported by Trivy. |
116117
| `trivy.ignorePolicy.{ns}.{wl}` | N/A | It specifies a namespace/workload specific [policy](https://aquasecurity.github.io/trivy/latest/docs/configuration/filtering/#by-rego) file which allows to customize which vulnerabilities are reported by Trivy. |

pkg/plugins/trivy/config.go

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

33
import (
44
"fmt"
5+
"path"
56
"path/filepath"
67
"strconv"
78
"strings"
@@ -31,6 +32,7 @@ const (
3132
keyTrivyOfflineScan = "trivy.offlineScan"
3233
keyTrivyTimeout = "trivy.timeout"
3334
keyTrivyIgnoreFile = "trivy.ignoreFile"
35+
keyTrivyIgnoreFileName = "trivy.ignoreFileName"
3436
keyTrivyIgnorePolicy = "trivy.ignorePolicy"
3537
keyTrivyInsecureRegistryPrefix = "trivy.insecureRegistry."
3638
keyTrivyNonSslRegistryPrefix = "trivy.nonSslRegistry."
@@ -343,6 +345,23 @@ func (c Config) IgnoreFileExists() bool {
343345
_, ok := c.Data[keyTrivyIgnoreFile]
344346
return ok
345347
}
348+
349+
// GetIgnoreFileName returns the ignore file name to be mounted inside the scanner container.
350+
// Defaults to the package-level default (".trivyignore") when not explicitly set.
351+
func (c Config) GetIgnoreFileName() string {
352+
if v, ok := c.Data[keyTrivyIgnoreFileName]; ok {
353+
v = strings.TrimSpace(v)
354+
if v != "" {
355+
return v
356+
}
357+
}
358+
return ignoreFileName
359+
}
360+
361+
// IgnoreFileMountPath returns full mount path for the ignore file.
362+
func (c Config) IgnoreFileMountPath() string {
363+
return path.Join("/etc", "trivy", c.GetIgnoreFileName())
364+
}
346365
func (c Config) ConfigFileExists() bool {
347366
_, ok := c.Data[keyTrivyConfigFile]
348367
return ok
@@ -386,16 +405,16 @@ func (c Config) GenerateIgnoreFileVolumeIfAvailable(trivyConfigName string) (*co
386405
Items: []corev1.KeyToPath{
387406
{
388407
Key: keyTrivyIgnoreFile,
389-
Path: ignoreFileName,
408+
Path: c.GetIgnoreFileName(),
390409
},
391410
},
392411
},
393412
},
394413
}
395414
volumeMount := corev1.VolumeMount{
396415
Name: ignoreFileVolumeName,
397-
MountPath: ignoreFileMountPath,
398-
SubPath: ignoreFileName,
416+
MountPath: c.IgnoreFileMountPath(),
417+
SubPath: c.GetIgnoreFileName(),
399418
}
400419
return &volume, &volumeMount
401420
}

pkg/plugins/trivy/config_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,44 @@ func TestPlugin_FindIgnorePolicyKey(t *testing.T) {
922922
}
923923
}
924924

925+
func TestConfig_IgnoreFileMountPath(t *testing.T) {
926+
tests := []struct {
927+
name string
928+
value string
929+
setKey bool
930+
expected string
931+
}{
932+
{
933+
name: "default name when unset",
934+
setKey: false,
935+
expected: "/etc/trivy/.trivyignore",
936+
},
937+
{
938+
name: "custom file name",
939+
setKey: true,
940+
value: "custom.ignore",
941+
expected: "/etc/trivy/custom.ignore",
942+
},
943+
{
944+
name: "whitespace falls back to default",
945+
setKey: true,
946+
value: " ",
947+
expected: "/etc/trivy/.trivyignore",
948+
},
949+
}
950+
951+
for _, tt := range tests {
952+
t.Run(tt.name, func(t *testing.T) {
953+
cfg := Config{PluginConfig: trivyoperator.PluginConfig{Data: make(map[string]string)}}
954+
if tt.setKey {
955+
cfg.Data[keyTrivyIgnoreFileName] = tt.value
956+
}
957+
got := cfg.IgnoreFileMountPath()
958+
assert.Equal(t, tt.expected, got)
959+
})
960+
}
961+
}
962+
925963
func TestPlugin_GetIncludeDevDeps(t *testing.T) {
926964

927965
testCases := []struct {

pkg/plugins/trivy/filesystem.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ func GetPodSpecForStandaloneFSMode(ctx trivyoperator.PluginContext, config Confi
176176
if config.IgnoreFileExists() {
177177
env = append(env, corev1.EnvVar{
178178
Name: "TRIVY_IGNOREFILE",
179-
Value: ignoreFileMountPath,
179+
Value: config.IgnoreFileMountPath(),
180180
})
181181
}
182182
if config.FindIgnorePolicyKey(workload) != "" {
@@ -406,7 +406,7 @@ func GetPodSpecForClientServerFSMode(ctx trivyoperator.PluginContext, config Con
406406
if config.IgnoreFileExists() {
407407
env = append(env, corev1.EnvVar{
408408
Name: "TRIVY_IGNOREFILE",
409-
Value: ignoreFileMountPath,
409+
Value: config.IgnoreFileMountPath(),
410410
})
411411
}
412412
if config.FindIgnorePolicyKey(workload) != "" {

pkg/plugins/trivy/image.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func GetPodSpecForStandaloneMode(ctx trivyoperator.PluginContext,
225225
if config.IgnoreFileExists() {
226226
env = append(env, corev1.EnvVar{
227227
Name: "TRIVY_IGNOREFILE",
228-
Value: ignoreFileMountPath,
228+
Value: config.IgnoreFileMountPath(),
229229
})
230230
}
231231
if config.FindIgnorePolicyKey(workload) != "" {
@@ -460,7 +460,7 @@ func GetPodSpecForClientServerMode(ctx trivyoperator.PluginContext, config Confi
460460
if config.IgnoreFileExists() {
461461
env = append(env, corev1.EnvVar{
462462
Name: "TRIVY_IGNOREFILE",
463-
Value: ignoreFileMountPath,
463+
Value: config.IgnoreFileMountPath(),
464464
})
465465
}
466466
if config.FindIgnorePolicyKey(workload) != "" {

pkg/plugins/trivy/plugin.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ const (
119119
ignoreFileName = ".trivyignore"
120120
configFileName = "trivy-config.yaml"
121121
configFileMountPath = "/etc/trivy/" + configFileName
122-
ignoreFileMountPath = "/etc/trivy/" + ignoreFileName
123122
ignorePolicyVolumeName = "ignorepolicy"
124123
ignorePolicyName = "policy.rego"
125124
ignorePolicyMountPath = "/etc/trivy/" + ignorePolicyName
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package trivy_operator
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
corev1 "k8s.io/api/core/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/labels"
11+
"k8s.io/apimachinery/pkg/types"
12+
crclient "sigs.k8s.io/controller-runtime/pkg/client"
13+
14+
v1alpha1 "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
15+
"github.com/aquasecurity/trivy-operator/pkg/operator/etc"
16+
"github.com/aquasecurity/trivy-operator/pkg/trivyoperator"
17+
"github.com/aquasecurity/trivy-operator/tests/itest/helper"
18+
19+
. "github.com/onsi/ginkgo/v2"
20+
. "github.com/onsi/gomega"
21+
)
22+
23+
var _ = Describe("Trivy ignoreFile integration", func() {
24+
var ctx context.Context
25+
26+
BeforeEach(func() {
27+
ctx = context.Background()
28+
})
29+
30+
It("hides CVE when set in .trivyignore", func() {
31+
const IgnoredCVE = "CVE-2018-14618"
32+
33+
// Patch trivy-operator-trivy-config ConfigMap to include CVEToIgnore in trivy.ignoreFile
34+
operatorConfig, err := etc.GetOperatorConfig()
35+
Expect(err).ToNot(HaveOccurred())
36+
operatorNamespace, err := operatorConfig.GetOperatorNamespace()
37+
Expect(err).ToNot(HaveOccurred())
38+
39+
cm := &corev1.ConfigMap{}
40+
Expect(kubeClient.Get(ctx, clientObjectKey(operatorNamespace, trivyoperator.TrivyConfigMapName), cm)).To(Succeed())
41+
42+
cm.Data["trivy.ignoreFile"] = fmt.Sprintf("%s\n", IgnoredCVE)
43+
44+
By("Updating trivy.ignoreFile in ConfigMap")
45+
Expect(kubeClient.Update(ctx, cm)).To(Succeed())
46+
47+
// Brief wait to reduce races where a scan could start before CM is observed by the controller
48+
time.Sleep(5 * time.Second)
49+
50+
// Create an unmanaged Pod using kube-bench image and assert the ignored CVE is not reported
51+
pod := helper.NewPod().
52+
WithRandomName("trivyignore-kubebench").
53+
WithNamespace(inputs.PrimaryNamespace).
54+
WithContainer("kube-bench", "mirror.gcr.io/knqyf263/vuln-image:1.2.3", []string{"/bin/sh", "-c", "--"}, []string{"while true; do sleep 30; done;"}).
55+
Build()
56+
57+
By("Creating kube-bench Pod")
58+
Expect(inputs.Create(ctx, pod)).To(Succeed())
59+
DeferCleanup(func() {
60+
_ = inputs.Delete(ctx, pod)
61+
})
62+
63+
By("Waiting for VulnerabilityReport")
64+
Eventually(inputs.HasVulnerabilityReportOwnedBy(ctx, pod), inputs.AssertTimeout, inputs.PollingInterval).Should(BeTrue())
65+
66+
vrList := &v1alpha1.VulnerabilityReportList{}
67+
Expect(kubeClient.List(ctx, vrList, clientListOptionsForOwner(pod.ObjectMeta, "Pod"))).To(Succeed())
68+
Expect(vrList.Items).ToNot(BeEmpty(), "expected at least one VulnerabilityReport for the pod")
69+
70+
// The report must not include the ignored CVE ID
71+
for _, vr := range vrList.Items {
72+
for _, v := range vr.Report.Vulnerabilities {
73+
Expect(v.VulnerabilityID).ToNot(Equal(IgnoredCVE), fmt.Sprintf("unexpectedly found ignored CVE %s", IgnoredCVE))
74+
}
75+
}
76+
})
77+
})
78+
79+
// clientListOptionsForOwner builds ListOptions that match a VulnerabilityReport owned by the given object.
80+
func clientListOptionsForOwner(obj metav1.ObjectMeta, kind string) crclient.ListOption {
81+
sel := labels.Set{
82+
trivyoperator.LabelResourceNamespace: obj.Namespace,
83+
trivyoperator.LabelResourceKind: kind,
84+
trivyoperator.LabelResourceName: obj.Name,
85+
}
86+
return crclient.MatchingLabels(sel)
87+
}
88+
89+
// clientObjectKey returns a NamespacedName tuple for use with client.Get.
90+
func clientObjectKey(namespace, name string) types.NamespacedName {
91+
return types.NamespacedName{Namespace: namespace, Name: name}
92+
}

0 commit comments

Comments
 (0)