Skip to content

Commit

Permalink
feat: Export kube-bench reports to HTML (#422)
Browse files Browse the repository at this point in the history
Resolve: #396
  • Loading branch information
krol3 committed Mar 10, 2021
1 parent d5278c2 commit 38285f1
Show file tree
Hide file tree
Showing 8 changed files with 642 additions and 16 deletions.
3 changes: 3 additions & 0 deletions pkg/cmd/get_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ NAME is the name of a particular Kubernetes workload.
case kube.KindNamespace:
reporter := report.NewNamespaceReporter(clock, kubeClient)
return reporter.Generate(workload, outWriter)
case kube.KindNode:
reporter := report.NewNodeReporter(clock, kubeClientset, kubeClient)
return reporter.Generate(workload, outWriter)
default:
return fmt.Errorf("HTML report is not supported for %q", workload.Kind)
}
Expand Down
24 changes: 24 additions & 0 deletions pkg/kubebench/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package kubebench

import (
"io"

"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
corev1 "k8s.io/api/core/v1"
)

// Plugin defines the interface between Starboard and Kubernetes configuration
// checker with CIS Kubernetes Benchmarks.
type Plugin interface {

// GetScanJobSpec describes the pod that will be created by Starboard when
// it schedules a Kubernetes job to audit the configuration of the specified
// node.
GetScanJobSpec(node corev1.Node) (corev1.PodSpec, error)

// ParseCISKubeBenchOutput is a callback to parse and convert logs of
// the pod controlled by the scan job to v1alpha1.CISKubeBenchOutput.
ParseCISKubeBenchOutput(logsStream io.ReadCloser) (v1alpha1.CISKubeBenchOutput, error)

GetContainerName() string
}
16 changes: 0 additions & 16 deletions pkg/kubebench/scanner.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// Plugin defines the interface between Starboard and Kubernetes configuration
// checker with CIS Kubernetes Benchmarks.
type Plugin interface {

// GetScanJobSpec describes the pod that will be created by Starboard when
// it schedules a Kubernetes job to audit the configuration of the specified
// node.
GetScanJobSpec(node corev1.Node) (corev1.PodSpec, error)

// ParseCISKubeBenchOutput is a callback to parse and convert logs of
// the pod controlled by the scan job to v1alpha1.CISKubeBenchOutput.
ParseCISKubeBenchOutput(logsStream io.ReadCloser) (v1alpha1.CISKubeBenchOutput, error)

GetContainerName() string
}

type Scanner struct {
scheme *runtime.Scheme
opts kube.ScannerOpts
Expand Down
41 changes: 41 additions & 0 deletions pkg/report/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"github.com/aquasecurity/starboard/pkg/configauditreport"
"github.com/aquasecurity/starboard/pkg/ext"
"github.com/aquasecurity/starboard/pkg/kube"
"github.com/aquasecurity/starboard/pkg/kubebench"
"github.com/aquasecurity/starboard/pkg/report/templates"
"github.com/aquasecurity/starboard/pkg/vulnerabilityreport"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
)
Expand Down Expand Up @@ -117,3 +119,42 @@ func (r *namespaceReport) Generate(namespace kube.Object, out io.Writer) error {
templates.WritePageTemplate(out, &data)
return nil
}

type nodeReport struct {
clock ext.Clock
client client.Client
kubebenchReportsReader kubebench.ReadWriter
}

// NewNodeReporter generate the html reporter
func NewNodeReporter(clock ext.Clock, kubeClientset kubernetes.Interface, client client.Client) NodeReporter {
return &nodeReport{
clock: clock,
client: client,
kubebenchReportsReader: kubebench.NewReadWriter(client),
}
}

func (r *nodeReport) Generate(node kube.Object, out io.Writer) error {
data, err := r.RetrieveData(node)
if err != nil {
return err
}
templates.WritePageTemplate(out, &data)
return nil
}

func (r *nodeReport) RetrieveData(node kube.Object) (templates.NodeReport, error) {

found := &v1alpha1.CISKubeBenchReport{}
err := r.client.Get(context.Background(), types.NamespacedName{Name: node.Name}, found)
if err != nil {
return templates.NodeReport{}, err
}

return templates.NodeReport{
GeneratedAt: r.clock.Now(),
Node: node,
CisKubeBenchReport: found,
}, nil
}
5 changes: 5 additions & 0 deletions pkg/report/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ type NamespaceReporter interface {
RetrieveData(namespace kube.Object) (templates.NamespaceReport, error)
Generate(namespace kube.Object, out io.Writer) error
}

type NodeReporter interface {
RetrieveData(node kube.Object) (templates.NodeReport, error)
Generate(node kube.Object, out io.Writer) error
}
149 changes: 149 additions & 0 deletions pkg/report/templates/node_report.qtpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{% import "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1" %}

{% func (p *NodeReport) Title() %}
Aqua Starboard Node Security Report - Node: {%s p.Node.Name %}
{% endfunc %}

{% func (p *NodeReport) Body() %}
<div class="container">

<div class="col mt-5">
<div class="row text-center">{%= imgAquaLogo() %}</div>
<div class="row mt-4 text-center">
<h2 class="text-muted mx-auto">Aqua Starboard Node Security Report</h2>
</div>
<div class="row text-center">
<h3 class="text-muted mx-auto">{%s string(p.Node.Kind) %}: {%s p.Node.Name %}</h3>
</div>
<div class="row text-center">
<h3 class="text-muted mx-auto">Generated on {%s p.GeneratedAt.Format("2 Jan 2006 15:04:01") %}</h3>
</div>
</div>

<!-- Resume START -->
{% if p.CisKubeBenchReport != nil %}

<div class="row text-center border-bottom mt-4">
<h3 class="mx-auto " id="vuln_header" style="color: rgb(0, 160, 170);">CIS Benchmarks for Kubernetes </h3>
</div>
<!-- Cards -->
<div class="">
<div class="row my-5" style="font-size:small;">
<!-- Scanner -->
<div class="col-3 border rounded shadow px-3 py-2 ml-4 ">
<div class="row text-center">
<div class="col">
<p class="mb-2 pb-1 border-bottom">Scanner</p>
</div>
</div>
<div class="row">
<div class="col">
{% code
report := p.CisKubeBenchReport.Report
scanner_name := report.Scanner.Name
scanner_vendor := report.Scanner.Vendor
scanner_version := report.Scanner.Version
creation_timestamp := report.UpdateTimestamp.Format("2 Jan 2006 15:04:01")
%}
<p class="my-0">Name: {%s scanner_name %}</p>
<p class="my-0">Vendor: {%s scanner_vendor %}</p>
<p class="my-0">Version: {%s scanner_version %}</p>
</div>
</div>
</div>
<!-- summary -->
<div class="col-5 border rounded shadow py-2 mx-auto ">
<div class="row text-center">
<div class="col">
<p class="mb-2 pb-1 border-bottom">Summary</p>
</div>
</div>
<div class="row">
{% code
summary := report.Summary
%}
{% if summary.FailCount > 0 %}
<div class="col text-center p-0 text-danger font-weight-bold">
{% else %}
<div class="col text-center p-0">
{% endif %}
<p class="mx-auto mb-1">{%d summary.FailCount %}</p>
<p class="mx-auto ">FAIL</p>
</div>
{% if summary.WarnCount > 0 %}
<div class="col text-center p-0 text-warning font-weight-bold">
{% else %}
<div class="col text-center p-0">
{% endif %}
<p class="mx-auto mb-1">{%d summary.WarnCount %}</p>
<p class="mx-auto ">WARN</p>
</div>
<div class="col text-center p-0">
<p class="mx-auto mb-1">{%d summary.InfoCount %}</p>
<p class="mx-auto ">INFO</p>
</div>
<div class="col text-center p-0">
<p class="mx-auto mb-1">{%d summary.PassCount %}</p>
<p class="mx-auto ">PASS</p>
</div>
</div>
</div>
<!-- Metadata -->
<div class="col-3 border rounded shadow px-3 py-2 mr-4">
<div class="row text-center">
<div class="col">
<p class="mb-2 pb-1 border-bottom">Metadata</p>
</div>
</div>
<div class="row">
<div class="col">
<p class="my-0">
Generated at: {%s creation_timestamp %}
</p>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Resume END -->

<!-- Sections START -->
{% code
report := p.CisKubeBenchReport.Report
%}
<div class="row">
{% for _, section := range report.Sections %}
<table class="table table-sm table-bordered">
<thead>
<tr>
<th scope="col">Test No.</th>
<th scope="col">Status</th>
<th scope="col">Test Description</th>
<th scope="col">Remediation</th>
</tr>
</thead>
<tbody>
<h3> {%s section.Text %} </h3>
{% for _, test := range section.Tests %}
{% for _, result := range test.Results %}
<tr>
<td>{%s result.TestNumber %}</td>
<td>{%s result.Status %}</td>
<td>{%s result.TestDesc %}</td>
<td>{%s result.Remediation %}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
{% endfor %}
</div>
<!-- Sections END -->

</div>
{% endfunc %}

{% func nodeReference(section []v1alpha1.CISKubeBenchSection) %}
{%s section[0].ID %}/{%s section[0].Text %}:{%s section[0].NodeType %}/
{% endfunc %}

0 comments on commit 38285f1

Please sign in to comment.