Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add exposed secrets dashboard #22

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions internal/kube/exposedsecretreport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kube

import (
"context"

"github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
)

const exposedSecretReportsResource = "exposedsecretreports"

// GetExposedSecretReportList retrieves all resources of type exposedsecretreports in all namespaces.
func GetExposedSecretReportList() (*v1alpha1.ExposedSecretReportList, error) {
var list v1alpha1.ExposedSecretReportList
err := client.
Get().
Resource(exposedSecretReportsResource).
Do(context.TODO()).
Into(&list)
if err != nil {
return nil, err
}

return &list, nil
}
91 changes: 87 additions & 4 deletions internal/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
clusterrolesview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/clusterroles"
configauditview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/configaudit"
configauditsview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/configaudits"
exposedsecretview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/exposedsecret"
exposedsecretsview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/exposedsecrets"
imageview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/image"
imagesview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/images"
roleview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/role"
Expand All @@ -27,14 +29,16 @@ func Start(port string) error {
mux := http.NewServeMux()
mux.HandleFunc("/", imagesHandler)
mux.HandleFunc("/image", imageHandler)
mux.HandleFunc("/roles", rolesHandler)
mux.HandleFunc("/role", roleHandler)
mux.HandleFunc("/clusterroles", clusterrolesHandler)
mux.HandleFunc("/clusterrole", clusterroleHandler)
mux.HandleFunc("/configaudits", configauditsHandler)
mux.HandleFunc("/configaudit", configauditHandler)
mux.HandleFunc("/clusteraudits", clusterauditsHandler)
mux.HandleFunc("/clusteraudit", clusterauditHandler)
mux.HandleFunc("/clusterroles", clusterrolesHandler)
mux.HandleFunc("/clusterrole", clusterroleHandler)
mux.HandleFunc("/exposedsecrets", exposedsecretsHandler)
mux.HandleFunc("/exposedsecret", exposedsecretHandler)
mux.HandleFunc("/roles", rolesHandler)
mux.HandleFunc("/role", roleHandler)
mux.Handle("/static/", http.FileServer(http.FS(content.Static)))
return http.ListenAndServe(fmt.Sprintf(":%s", port), mux)
}
Expand Down Expand Up @@ -441,3 +445,82 @@ func clusterauditHandler(w http.ResponseWriter, r *http.Request) {
return
}
}

func exposedsecretsHandler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFS(content.Static, "static/exposedsecrets.html", "static/sidebar.html"))
if tmpl == nil {
log.Logger.Error("encountered error parsing exposed secrets html template")
http.Error(w, "Internal Server Error, check server logs", http.StatusInternalServerError)
return
}

data, err := kube.GetExposedSecretReportList()
if err != nil {
log.Logger.Error("error getting ExposedSecretReports", "error", err.Error())
return
}
imageData := exposedsecretsview.GetView(data)

err = tmpl.Execute(w, imageData)
if err != nil {
log.Logger.Error("encountered error executing exposed secrets html template", "error", err)
http.Error(w, "Internal Server Error, check server logs", http.StatusInternalServerError)
return
}
}

func exposedsecretHandler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFS(content.Static, "static/exposedsecret.html", "static/sidebar.html"))
if tmpl == nil {
log.Logger.Error("encountered error parsing exposed secret html template")
http.Error(w, "Internal Server Error, check server logs", http.StatusInternalServerError)
return
}

// Parse URL query params
q := r.URL.Query()

// Check query params -- 404 if required params not passed
imageName := q.Get("image")
if imageName == "" {
log.Logger.Error("image name query param missing from request")
http.NotFound(w, r)
return
}
imageDigest := q.Get("digest")
if imageDigest == "" {
log.Logger.Error("image digest query param missing from request")
http.NotFound(w, r)
return
}
severity := q.Get("severity")

// Get secret reports
data, err := kube.GetExposedSecretReportList()
if err != nil {
log.Logger.Error("error getting ExposedSecretReports", "error", err.Error())
return
}

// Get image view from reports
view, found := exposedsecretview.GetView(data, exposedsecretview.Filters{
Name: imageName,
Digest: imageDigest,
Severity: severity,
})

// If the selected image from query params was not found, 404
if !found {
log.Logger.Error("image name and digest query params did not produce a valid result from scraped data", "image", imageName, "digest", imageDigest)
http.NotFound(w, r)
return
}

// Execute html template
err = tmpl.Execute(w, view)
if err != nil {
log.Logger.Error("encountered error executing exposed secret html template", "error", err)
http.Error(w, "Internal Server Error, check server logs", http.StatusInternalServerError)
return
}
}
97 changes: 97 additions & 0 deletions internal/web/views/exposedsecret/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package image

import (
"fmt"
"sort"
"strings"

"github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
)

// Filters contains the supported filters for the image view
type Filters struct {
Name string
Digest string

// optional filters
Severity string
}

// GetView converts some report data to the /exposedsecret view
// returns view data and "true" if the image was found in the report list
func GetView(data *v1alpha1.ExposedSecretReportList, filters Filters) (View, bool) {
for _, item := range data.Items {
itemImageName := getImageNameFromLabels(item.Report.Registry.Server, item.Report.Artifact.Repository, item.Report.Artifact.Tag)
if filters.Name != itemImageName || filters.Digest != item.Report.Artifact.Digest {
continue
}

i := View{
Name: itemImageName,
Digest: item.Report.Artifact.Digest,
}

for _, v := range item.Report.Secrets {
secret := Secret{
Severity: string(v.Severity),
Title: v.Title,
Target: v.Target,
Match: v.Match,
}

uniqueSecret := i.isUniqueImageSecret(secret.Severity, secret.Title, secret.Target, secret.Match)
if uniqueSecret {
// Skip vulnerability if any filters don't match
// Filter severity
if filters.Severity != "" && !strings.EqualFold(secret.Severity, filters.Severity) {
continue
}

i.Secrets = append(i.Secrets, secret)
}
}

i = sortView(i)

return i, true
}

return View{}, false
}

func getImageNameFromLabels(registry, repo, tag string) string {
if registry == "index.docker.io" {
// If Docker Hub, trim the registry prefix for readability
// Also trims `library/` from the prefix of the image name, which is a hidden username for Docker Hub official images
return fmt.Sprintf("%s:%s", strings.TrimPrefix(repo, "library/"), tag)
}
return fmt.Sprintf("%s/%s:%s", registry, repo, tag)
}

func (i View) isUniqueImageSecret(severity, title, target, match string) bool {
for _, secret := range i.Secrets {
if severity == secret.Severity && title == secret.Title && target == secret.Target && match == secret.Match {
return false
}
}

return true
}

func sortView(v View) View {
// Create an order for severities to sort by
// Define custom priority order
severityOrder := map[string]int{
"CRITICAL": 3,
"HIGH": 2,
"MEDIUM": 1,
"LOW": 0,
}

// Sort the slice by severity in descending order
sort.Slice(v.Secrets, func(j, k int) bool {
return severityOrder[v.Secrets[j].Severity] > severityOrder[v.Secrets[k].Severity]
})

return v
}
19 changes: 19 additions & 0 deletions internal/web/views/exposedsecret/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package image

// View data about an image and their exposed secrets
type View Data

// Data contains data about an image and its exposed secrets
type Data struct {
Name string // name of the image
Digest string // sha digest of the image
Secrets []Secret
}

// Secret data related to an exposed secret
type Secret struct {
Severity string
Title string
Target string
Match string
}
Loading