Skip to content

Commit

Permalink
Add config audit dashboard (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Starttoaster authored Apr 29, 2024
1 parent 6b6f224 commit 346ef18
Show file tree
Hide file tree
Showing 12 changed files with 525 additions and 4 deletions.
24 changes: 24 additions & 0 deletions internal/kube/configaudit.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 configAuditReportsResource = "configauditreports"

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

return &list, nil
}
88 changes: 88 additions & 0 deletions internal/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/starttoaster/trivy-operator-explorer/internal/web/content"
clusterroleview "github.com/starttoaster/trivy-operator-explorer/internal/web/views/clusterrole"
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"
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,6 +29,8 @@ func Start(port string) error {
mux.HandleFunc("/role", roleHandler)
mux.HandleFunc("/clusterroles", clusterrolesHandler)
mux.HandleFunc("/clusterrole", clusterroleHandler)
mux.HandleFunc("/configaudits", configauditsHandler)
mux.HandleFunc("/configaudit", configauditHandler)
mux.Handle("/static/", http.FileServer(http.FS(content.Static)))
return http.ListenAndServe(fmt.Sprintf(":%s", port), mux)
}
Expand Down Expand Up @@ -272,3 +276,87 @@ func clusterroleHandler(w http.ResponseWriter, r *http.Request) {
return
}
}

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

// Get reports
reports, err := kube.GetConfigAuditReportList()
if err != nil {
log.Logger.Error("error getting configauditreports", "error", err.Error())
return
}
audits := configauditsview.GetView(reports)

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

func configauditHandler(w http.ResponseWriter, r *http.Request) {
tmpl := template.Must(template.ParseFS(content.Static, "static/configaudit.html", "static/sidebar.html"))
if tmpl == nil {
log.Logger.Error("encountered error parsing configaudit 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
name := q.Get("name")
if name == "" {
log.Logger.Error("config audit name query param missing from request")
http.NotFound(w, r)
return
}
namespace := q.Get("namespace")
if namespace == "" {
log.Logger.Error("config audit namespace query param missing from request")
http.NotFound(w, r)
return
}
kind := q.Get("kind")
if kind == "" {
log.Logger.Error("config audit kind query param missing from request")
http.NotFound(w, r)
return
}
severity := q.Get("severity")

// Get configaudit reports
reports, err := kube.GetConfigAuditReportList()
if err != nil {
log.Logger.Error("error getting configauditreports", "error", err.Error())
return
}
audit, found := configauditview.GetView(reports, configauditview.Filters{
Name: name,
Namespace: namespace,
Kind: kind,
Severity: severity,
})

// If the selected resource from query params was not found, 404
if !found {
log.Logger.Error("resource name and namespace query params did not produce a valid result from reports", "name", name, "namespace", namespace)
http.NotFound(w, r)
return
}

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

import (
"fmt"
"sort"
"strings"

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

// Filters contains the supported filters for the configaudit view
type Filters struct {
Name string
Namespace string
Kind string

// optional
Severity string
}

// GetView converts some report data to the /configaudit view
// returns view data and "true" if the image was found in the report list
func GetView(data *v1alpha1.ConfigAuditReportList, filters Filters) (View, bool) {
for _, item := range data.Items {
var name string
if val, ok := item.ObjectMeta.Labels["trivy-operator.resource.name"]; ok {
name = val
} else if val, ok := item.ObjectMeta.Annotations["trivy-operator.resource.name"]; ok {
name = val
} else {
name = item.Name
}

if filters.Name != name {
continue
}
if filters.Namespace != item.ObjectMeta.Labels["trivy-operator.resource.namespace"] {
continue
}
if filters.Kind != item.ObjectMeta.Labels["trivy-operator.resource.kind"] {
continue
}

view := View{
Kind: item.ObjectMeta.Labels["trivy-operator.resource.kind"],
Name: name,
Namespace: item.ObjectMeta.Labels["trivy-operator.resource.namespace"],
}

for _, v := range item.Report.Checks {
ksvNum := strings.TrimPrefix(v.ID, "KSV")
url := fmt.Sprintf("https://avd.aquasec.com/misconfig/kubernetes/general/avd-ksv-%04s/", ksvNum)

vuln := Vulnerability{
ID: v.ID,
URL: url,
Severity: string(v.Severity),
Title: v.Title,
Description: v.Description,
}

if filters.Severity != "" && !strings.EqualFold(vuln.Severity, filters.Severity) {
continue
}

view.Vulnerabilities = append(view.Vulnerabilities, vuln)
}

view = sortView(view)

return view, true
}

return View{}, false
}

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.Vulnerabilities, func(j, k int) bool {
return severityOrder[v.Vulnerabilities[j].Severity] > severityOrder[v.Vulnerabilities[k].Severity]
})

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

// View a list of data about role vulnerabilities
type View Data

// Data data about a role and its vulnerabilities
type Data struct {
Name string
Namespace string
Kind string
Vulnerabilities []Vulnerability
}

// Vulnerability data related to a role
type Vulnerability struct {
ID string
URL string
Severity string
Title string
Description string
}
91 changes: 91 additions & 0 deletions internal/web/views/configaudits/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package roles

import (
"sort"
"strings"

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

// GetView converts some report data to the /roles view
func GetView(data *v1alpha1.ConfigAuditReportList) View {
var view View

for _, item := range data.Items {
var name string
if val, ok := item.ObjectMeta.Labels["trivy-operator.resource.name"]; ok {
name = val
} else if val, ok := item.ObjectMeta.Annotations["trivy-operator.resource.name"]; ok {
name = val
} else {
name = item.Name
}

audit := Data{
Kind: item.ObjectMeta.Labels["trivy-operator.resource.kind"],
Name: name,
Namespace: item.ObjectMeta.Labels["trivy-operator.resource.namespace"],
}

index, unique := view.isUnique(audit.Name, audit.Namespace, audit.Kind)
if unique {
view = append(view, audit)
index = len(view) - 1
}

for _, v := range item.Report.Checks {
severity := v.Severity
vuln := Vulnerability{
ID: v.ID,
Title: v.Title,
Description: v.Description,
}

switch strings.ToLower(string(severity)) {
case "critical":
view[index].CriticalVulnerabilities = append(view[index].CriticalVulnerabilities, vuln)
case "high":
view[index].HighVulnerabilities = append(view[index].HighVulnerabilities, vuln)
case "medium":
view[index].MediumVulnerabilities = append(view[index].MediumVulnerabilities, vuln)
case "low":
view[index].LowVulnerabilities = append(view[index].LowVulnerabilities, vuln)
}
}
}

view = sortView(view)

return view
}

func (a View) isUnique(name, namespace, kind string) (int, bool) {
for i, audit := range a {
if name == audit.Name && kind == audit.Kind {
return i, false
}
}

return 0, true
}

func sortView(a View) View {
// Sort the slice by severity in descending order
sort.Slice(a, func(j, k int) bool {
if len(a[j].CriticalVulnerabilities) != len(a[k].CriticalVulnerabilities) {
return len(a[j].CriticalVulnerabilities) > len(a[k].CriticalVulnerabilities)
}

if len(a[j].HighVulnerabilities) != len(a[k].HighVulnerabilities) {
return len(a[j].HighVulnerabilities) > len(a[k].HighVulnerabilities)
}

if len(a[j].MediumVulnerabilities) != len(a[k].MediumVulnerabilities) {
return len(a[j].MediumVulnerabilities) > len(a[k].MediumVulnerabilities)
}

return len(a[j].LowVulnerabilities) > len(a[k].LowVulnerabilities)
})

return a
}
22 changes: 22 additions & 0 deletions internal/web/views/configaudits/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package roles

// View a list of data about role vulnerabilities
type View []Data

// Data data about a role and its vulnerabilities
type Data struct {
Name string
Namespace string
Kind string
CriticalVulnerabilities []Vulnerability
HighVulnerabilities []Vulnerability
MediumVulnerabilities []Vulnerability
LowVulnerabilities []Vulnerability
}

// Vulnerability data related to a role
type Vulnerability struct {
ID string
Title string
Description string
}
2 changes: 2 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
//go:generate tailwindcss build -i ./static/css/input.css -o ./static/css/output.css

//go:embed static/sidebar.html
//go:embed static/configaudits.html
//go:embed static/configaudit.html
//go:embed static/roles.html
//go:embed static/role.html
//go:embed static/clusterroles.html
Expand Down
4 changes: 2 additions & 2 deletions static/clusterroles.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@
</a>
{{ end }}
{{ if $data.MediumVulnerabilities }}
<a href="/role?name={{ $data.Name }}&severity=Medium" title="Medium" class="bg-yellow-100 text-yellow-800 text-xs font-medium me-1 px-2 py-2 rounded dark:bg-yellow-900 dark:text-yellow-100">
<a href="/clusterrole?name={{ $data.Name }}&severity=Medium" title="Medium" class="bg-yellow-100 text-yellow-800 text-xs font-medium me-1 px-2 py-2 rounded dark:bg-yellow-900 dark:text-yellow-100">
{{ len $data.MediumVulnerabilities }}
</a>
{{ end }}
{{ if $data.LowVulnerabilities }}
<a href="/role?name={{ $data.Name }}&severity=Low" title="Low" class="bg-blue-100 text-blue-800 text-xs font-medium me-1 px-2 py-2 rounded dark:bg-blue-900 dark:text-blue-100">
<a href="/clusterrole?name={{ $data.Name }}&severity=Low" title="Low" class="bg-blue-100 text-blue-800 text-xs font-medium me-1 px-2 py-2 rounded dark:bg-blue-900 dark:text-blue-100">
{{ len $data.LowVulnerabilities }}
</a>
{{ end }}
Expand Down
Loading

0 comments on commit 346ef18

Please sign in to comment.