From 6294d9d5267e0f42c5dc5748ce63d7092e393cf8 Mon Sep 17 00:00:00 2001 From: Starttoaster Date: Tue, 30 Apr 2024 23:39:12 -0700 Subject: [PATCH] Add filters to multiple dashboards (#26) * Add namespace and kind filters to roles and configaudits dashboards * Fix up readme * Add images filter for vulnerabilities with fixes --- README.md | 15 ++------- internal/web/server.go | 40 ++++++++++++++++++++++-- internal/web/views/configaudits/audit.go | 25 +++++++++++++-- internal/web/views/images/images.go | 14 ++++++++- internal/web/views/roles/roles.go | 16 ++++++++-- static/configaudits.html | 6 ++-- static/css/output.css | 24 ++++++++++++++ static/images.html | 3 ++ static/roles.html | 2 +- 9 files changed, 119 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 4352616..c6d9007 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ trivy-operator-explorer ## Filters -Some dashboards have filters available. These filters are only set-able from the URL in query parameters for now, as there are no forms on the explorer yet, though it is planned. +Some dashboards have filters that can be set from clicking elements on the page, but the following can only be set manually from the URL query parameters for now. UI elements might be added for these over time. ### Image filter @@ -39,18 +39,7 @@ Some dashboards have filters available. These filters are only set-able from the Example URL: `http://your.explorer.install/image?hasfix=true&severity=Critical` -There are query parameters for image and digest as well, but it's not expected for them to be changed manually. These filters currently do not have a graphical toggle but there's a TODO item below to add some form elements to the page for them. - -### ClusterRole/Role/Exposed Secrets filter - -| Parameters | Description | Example | -|--------------|-----------------------------------------------------------------------|---------------------------| -| severity | Filter by level of vulnerability severity. | severity=Critical | - -Example URL: `http://your.explorer.install/role?severity=Critical` - ## TODO - Make a home page that displays useful graphs for each of the report types. -- Graphical elements for setting filters, currently they're just URL query parameters. -- Testing. Pretty sure by law no new product can have tests. +- Graphical elements for setting the filters on the /image page diff --git a/internal/web/server.go b/internal/web/server.go index 4a5a3f4..26cec2c 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -51,13 +51,29 @@ func imagesHandler(w http.ResponseWriter, r *http.Request) { return } + // Parse URL query params + q := r.URL.Query() + + // Check query params + hasFix := q.Get("hasfix") + var hasFixBool bool + if hasFix != "" { + var err error + hasFixBool, err = strconv.ParseBool(hasFix) + if err != nil { + log.Logger.Warn("could not parse hasfix query parameter to bool type, ignoring filter", "raw", hasFix, "error", err.Error()) + } + } + // Get vulnerability reports data, err := kube.GetVulnerabilityReportList() if err != nil { log.Logger.Error("error getting VulnerabilityReports", "error", err.Error()) return } - imageData := imagesview.GetView(data) + imageData := imagesview.GetView(data, imagesview.Filters{ + HasFix: hasFixBool, + }) err = tmpl.Execute(w, imageData) if err != nil { @@ -146,13 +162,21 @@ func rolesHandler(w http.ResponseWriter, r *http.Request) { return } + // Parse URL query params + q := r.URL.Query() + + // Check query params + namespace := q.Get("namespace") + // Get role reports reports, err := kube.GetRbacAssessmentReportList() if err != nil { log.Logger.Error("error getting VulnerabilityReports", "error", err.Error()) return } - roles := rolesview.GetView(reports) + roles := rolesview.GetView(reports, rolesview.Filters{ + Namespace: namespace, + }) err = tmpl.Execute(w, roles) if err != nil { @@ -293,13 +317,23 @@ func configauditsHandler(w http.ResponseWriter, r *http.Request) { return } + // Parse URL query params + q := r.URL.Query() + + // Check query params + namespace := q.Get("namespace") + kind := q.Get("kind") + // Get reports reports, err := kube.GetConfigAuditReportList() if err != nil { log.Logger.Error("error getting configauditreports", "error", err.Error()) return } - audits := configauditsview.GetView(reports) + audits := configauditsview.GetView(reports, configauditsview.Filters{ + Namespace: namespace, + Kind: kind, + }) err = tmpl.Execute(w, audits) if err != nil { diff --git a/internal/web/views/configaudits/audit.go b/internal/web/views/configaudits/audit.go index 9de60dd..cf09e23 100644 --- a/internal/web/views/configaudits/audit.go +++ b/internal/web/views/configaudits/audit.go @@ -7,8 +7,14 @@ import ( "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" ) +// Filters represents the available optional filters to the config audit view +type Filters struct { + Namespace string + Kind string +} + // GetView converts some report data to the /roles view -func GetView(data *v1alpha1.ConfigAuditReportList) View { +func GetView(data *v1alpha1.ConfigAuditReportList, filters Filters) View { var view View for _, item := range data.Items { @@ -20,11 +26,24 @@ func GetView(data *v1alpha1.ConfigAuditReportList) View { } else { name = item.Name } + kind := item.ObjectMeta.Labels["trivy-operator.resource.kind"] + namespace := item.ObjectMeta.Labels["trivy-operator.resource.namespace"] + + if filters.Kind != "" { + if kind != filters.Kind { + continue + } + } + if filters.Namespace != "" { + if namespace != filters.Namespace { + continue + } + } audit := Data{ - Kind: item.ObjectMeta.Labels["trivy-operator.resource.kind"], + Kind: kind, Name: name, - Namespace: item.ObjectMeta.Labels["trivy-operator.resource.namespace"], + Namespace: namespace, } index, unique := view.isUnique(audit.Name, audit.Namespace, audit.Kind) diff --git a/internal/web/views/images/images.go b/internal/web/views/images/images.go index 0780ae5..2d13f9e 100644 --- a/internal/web/views/images/images.go +++ b/internal/web/views/images/images.go @@ -8,8 +8,13 @@ import ( "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" ) +// Filters represents the available optional filters to the images view +type Filters struct { + HasFix bool +} + // GetView converts some report data to the /images view -func GetView(data *v1alpha1.VulnerabilityReportList) View { +func GetView(data *v1alpha1.VulnerabilityReportList, filters Filters) View { var i View for _, item := range data.Items { @@ -62,6 +67,13 @@ func GetView(data *v1alpha1.VulnerabilityReportList) View { FixedVersion: v.FixedVersion, } + // Filter by hasfix + if filters.HasFix { + if strings.TrimSpace(vuln.FixedVersion) == "" { + continue + } + } + // If the image is not unique, we need to check if the vulnerability is unique too // Seems rare, but Trivy Operator sometimes gives duplicate CVE data for an image uniqueVuln := i[imageIndex].isUniqueImageVulnerability(vuln.ID, vuln.Severity) diff --git a/internal/web/views/roles/roles.go b/internal/web/views/roles/roles.go index 0243665..85a8d33 100644 --- a/internal/web/views/roles/roles.go +++ b/internal/web/views/roles/roles.go @@ -7,8 +7,13 @@ import ( "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" ) +// Filters represents the available optional filters to the roles audit view +type Filters struct { + Namespace string +} + // GetView converts some report data to the /roles view -func GetView(data *v1alpha1.RbacAssessmentReportList) View { +func GetView(data *v1alpha1.RbacAssessmentReportList, filters Filters) View { var r View for _, item := range data.Items { @@ -21,10 +26,17 @@ func GetView(data *v1alpha1.RbacAssessmentReportList) View { name = item.Name } + namespace := item.ObjectMeta.Labels["trivy-operator.resource.namespace"] + if filters.Namespace != "" { + if namespace != filters.Namespace { + continue + } + } + role := Data{ Kind: item.ObjectMeta.Labels["trivy-operator.resource.kind"], Name: name, - Namespace: item.ObjectMeta.Labels["trivy-operator.resource.namespace"], + Namespace: namespace, } index, unique := r.isUniqueRole(role.Name, role.Namespace, role.Kind) diff --git a/static/configaudits.html b/static/configaudits.html index 8c003d2..df9bc81 100644 --- a/static/configaudits.html +++ b/static/configaudits.html @@ -1,6 +1,6 @@ - Explorer: configaudits + Explorer: Config Audits @@ -38,7 +38,7 @@ - + {{ $data.Namespace }} @@ -48,7 +48,7 @@ - + {{ $data.Kind }} diff --git a/static/css/output.css b/static/css/output.css index 603be9c..8fd37bc 100644 --- a/static/css/output.css +++ b/static/css/output.css @@ -767,6 +767,11 @@ video { background-color: rgb(249 250 251 / var(--tw-bg-opacity)); } +.bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(220 252 231 / var(--tw-bg-opacity)); +} + .bg-orange-200 { --tw-bg-opacity: 1; background-color: rgb(254 215 170 / var(--tw-bg-opacity)); @@ -905,6 +910,10 @@ video { font-weight: 500; } +.font-normal { + font-weight: 400; +} + .font-semibold { font-weight: 600; } @@ -938,6 +947,11 @@ video { color: rgb(17 24 39 / var(--tw-text-opacity)); } +.text-green-800 { + --tw-text-opacity: 1; + color: rgb(22 101 52 / var(--tw-text-opacity)); +} + .text-orange-800 { --tw-text-opacity: 1; color: rgb(154 52 18 / var(--tw-text-opacity)); @@ -1107,6 +1121,11 @@ video { background-color: rgb(17 24 39 / var(--tw-bg-opacity)); } + .dark\:bg-green-900 { + --tw-bg-opacity: 1; + background-color: rgb(20 83 45 / var(--tw-bg-opacity)); + } + .dark\:bg-indigo-800 { --tw-bg-opacity: 1; background-color: rgb(55 48 163 / var(--tw-bg-opacity)); @@ -1152,6 +1171,11 @@ video { color: rgb(156 163 175 / var(--tw-text-opacity)); } + .dark\:text-green-300 { + --tw-text-opacity: 1; + color: rgb(134 239 172 / var(--tw-text-opacity)); + } + .dark\:text-orange-100 { --tw-text-opacity: 1; color: rgb(255 237 213 / var(--tw-text-opacity)); diff --git a/static/images.html b/static/images.html index d2e9000..bd9c0ad 100644 --- a/static/images.html +++ b/static/images.html @@ -29,6 +29,9 @@ Vulnerabilities + + Has fix? + diff --git a/static/roles.html b/static/roles.html index 1615f3a..af21eec 100644 --- a/static/roles.html +++ b/static/roles.html @@ -38,7 +38,7 @@ - + {{ $data.Namespace }}