Skip to content

Commit

Permalink
feat(cli): Show top 5 failed workload configuration checks in html re…
Browse files Browse the repository at this point in the history
…port for namespace (#462)

Resolves: #410
  • Loading branch information
xyoxo committed Mar 29, 2021
1 parent 20182e2 commit 55b37f7
Show file tree
Hide file tree
Showing 6 changed files with 391 additions and 69 deletions.
69 changes: 69 additions & 0 deletions pkg/report/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,17 @@ func (r *namespaceReporter) RetrieveData(namespace kube.Object) (templates.Names
return templates.NamespaceReport{}, err
}

var configAuditReportList v1alpha1.ConfigAuditReportList
err = r.client.List(context.Background(), &configAuditReportList, client.InNamespace(namespace.Name))
if err != nil {
return templates.NamespaceReport{}, err
}

return templates.NamespaceReport{
Namespace: namespace,
GeneratedAt: r.clock.Now(),
Top5VulnerableImages: r.topNImagesBySeverityCount(vulnerabilityReportList.Items, 5),
Top5FailedChecks: r.topNFailedChecksByAffectedWorkloadsCount(configAuditReportList.Items, 5),
}, nil
}

Expand All @@ -110,6 +117,68 @@ func (r *namespaceReporter) topNImagesBySeverityCount(reports []v1alpha1.Vulnera
return b[:ext.MinInt(N, len(b))]
}

func (r *namespaceReporter) topNFailedChecksByAffectedWorkloadsCount(reports []v1alpha1.ConfigAuditReport, N int) []templates.CheckWithCount {
checksMap := make(map[string]templates.CheckWithCount)

for _, report := range reports {
for _, podCheck := range report.Report.PodChecks {
if podCheck.Success {
continue
}
configId := podCheck.ID
_, ok := checksMap[configId]
if ok {
config := checksMap[configId]
config.AffectedWorkloads++
checksMap[configId] = config
} else {
checksMap[configId] = templates.CheckWithCount{
Check: podCheck,
AffectedWorkloads: 1,
}
}
}

alreadyCheckedForWorkload := make(map[string]bool)
for _, container := range report.Report.ContainerChecks {
for _, containerCheck := range container {
if containerCheck.Success {
continue
}

configId := containerCheck.ID
if alreadyCheckedForWorkload[configId] {
continue
}

alreadyCheckedForWorkload[configId] = true
_, ok := checksMap[configId]
if ok {
config := checksMap[configId]
config.AffectedWorkloads++
checksMap[configId] = config
} else {
checksMap[configId] = templates.CheckWithCount{
Check: containerCheck,
AffectedWorkloads: 1,
}
}
}
}
}

failedChecks := make([]templates.CheckWithCount, len(checksMap))
i := 0
for _, check := range checksMap {
failedChecks[i] = check
i++
}

OrderedBy(checkCompareFunc...).SortDesc(failedChecks)

return failedChecks[:ext.MinInt(N, len(failedChecks))]
}

func (r *namespaceReporter) Generate(namespace kube.Object, out io.Writer) error {
data, err := r.RetrieveData(namespace)
if err != nil {
Expand Down
77 changes: 77 additions & 0 deletions pkg/report/sort.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package report

import (
"sort"

"github.com/aquasecurity/starboard/pkg/report/templates"
)

type LessFunc func(p1, p2 *templates.CheckWithCount) bool

// multiSorter implements the Sort interface, sorting the reports within.
type multiSorter struct {
checks []templates.CheckWithCount
less []LessFunc
}

// SortDesc sorts the argument slice according to the LessFunc functions passed to OrderedBy.
func (ms *multiSorter) SortDesc(reports []templates.CheckWithCount) {
ms.checks = reports
sort.Stable(sort.Reverse(ms))
}

// OrderedBy returns a Sorter that sorts using the LessFunc functions, in order.
// Call its Sort method to sort the data.
func OrderedBy(less ...LessFunc) *multiSorter {
return &multiSorter{
less: less,
}
}

// Len is part of sort.Interface.
func (ms *multiSorter) Len() int {
return len(ms.checks)
}

// Swap is part of sort.Interface.
func (ms *multiSorter) Swap(i, j int) {
ms.checks[i], ms.checks[j] = ms.checks[j], ms.checks[i]
}

// Less is part of sort.Interface. It is implemented by looping along the
// less functions until it finds a comparison that discriminates between
// the two items (one is less than the other). Note that it can call the
// less functions twice per call. We could change the functions to return
// -1, 0, 1 and reduce the number of calls for greater efficiency: an
// exercise for the reader.
func (ms *multiSorter) Less(i, j int) bool {
p, q := &ms.checks[i], &ms.checks[j]
// Try all but the last comparison.
var k int
for k = 0; k < len(ms.less)-1; k++ {
less := ms.less[k]
switch {
case less(p, q):
// p < q, so we have a decision.
return true
case less(q, p):
// p > q, so we have a decision.
return false
}
// p == q; try the next comparison.
}
// All comparisons to here said "equal", so just return whatever
// the final comparison reports.
return ms.less[k](p, q)
}

var (
checkCompareFunc = []LessFunc{
func(r1, r2 *templates.CheckWithCount) bool {
return r1.AffectedWorkloads < r2.AffectedWorkloads
}, func(r1, r2 *templates.CheckWithCount) bool {
return r1.Severity > r2.Severity
}, func(r1, r2 *templates.CheckWithCount) bool {
return r1.ID > r2.ID
}}
)
98 changes: 98 additions & 0 deletions pkg/report/sort_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package report

import (
"testing"

"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
"github.com/aquasecurity/starboard/pkg/report/templates"
"github.com/stretchr/testify/assert"
)

func TestOrderedBy(t *testing.T) {
checks := []templates.CheckWithCount{
{
Check: v1alpha1.Check{
ID: "privilegeEscalationAllowed",
Severity: "danger",
Category: "Security",
},
AffectedWorkloads: 10,
},
{
Check: v1alpha1.Check{
ID: "cpuLimitsMissing",
Severity: "warning",
Category: "Efficiency",
},
AffectedWorkloads: 12,
},
{
Check: v1alpha1.Check{
ID: "cpuRequestsMissing",
Severity: "warning",
Category: "Efficiency",
},
AffectedWorkloads: 8,
},
{
Check: v1alpha1.Check{
ID: "livenessProbeMissing",
Severity: "warning",
Category: "Reliability",
},
AffectedWorkloads: 5,
},
{
Check: v1alpha1.Check{
ID: "insecureCapabilities",
Severity: "warning",
Category: "Security",
},
AffectedWorkloads: 5,
},
}

OrderedBy(checkCompareFunc...).SortDesc(checks)
assert.Equal(t, []templates.CheckWithCount{
{
Check: v1alpha1.Check{
ID: "cpuLimitsMissing",
Severity: "warning",
Category: "Efficiency",
},
AffectedWorkloads: 12,
},
{
Check: v1alpha1.Check{
ID: "privilegeEscalationAllowed",
Severity: "danger",
Category: "Security",
},
AffectedWorkloads: 10,
},
{
Check: v1alpha1.Check{
ID: "cpuRequestsMissing",
Severity: "warning",
Category: "Efficiency",
},
AffectedWorkloads: 8,
},
{
Check: v1alpha1.Check{
ID: "insecureCapabilities",
Severity: "warning",
Category: "Security",
},
AffectedWorkloads: 5,
},
{
Check: v1alpha1.Check{
ID: "livenessProbeMissing",
Severity: "warning",
Category: "Reliability",
},
AffectedWorkloads: 5,
},
}, checks)
}
24 changes: 24 additions & 0 deletions pkg/report/templates/namespace_report.qtpl
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,30 @@ Aqua Starboard Namespace Security Report - Namespace: {%s p.Namespace.Name %}
</table>
</div>

<h3>Top 5 failed configuration checks by affected workloads count</h3>
<div class="row">
<table class="table table-sm table-bordered">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Severity</th>
<th scope="col">Category</th>
<th scope="col">Affected Workloads</th>
</tr>
</thead>
<tbody>
{% for _, report := range p.Top5FailedChecks %}
<tr>
<td>{%s report.ID %}</td>
<td>{%s report.Severity %}</td>
<td>{%s report.Category %}</td>
<td>{%d report.AffectedWorkloads %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>

</div>
{% endfunc %}

Expand Down

0 comments on commit 55b37f7

Please sign in to comment.