Files
5379b9b0a6 New output (#1320)
* phase-1

Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>

* factory

Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>

* wip: feat(cli): add an image scanning command

Add a CLI command that launches an image scan. Does not scan images yet.

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* wip: feat: add image scanning service

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* chore: include dependencies

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* wip: adjust image scanning service

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* wip: feat: use scanning service in CLI

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* use iface

Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>

* touches

Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>

* continue

Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>

* add cmd

Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>

* support single workload scan

Signed-off-by: Amir Malka <amirm@armosec.io>

* fix conflict

Signed-off-by: Amir Malka <amirm@armosec.io>

* identifiers

* go mod

* feat(imagescan): add an image scanning command

This commit adds a CLI command and an associated package that scan
images for vulnerabilities.

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

feat(imagescan): fail on exceeding the severity threshold

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* chore(imagescan): include dependencies

This commit adds the dependencies necessary for image scanning.

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* chore(imagescan): add dependencies to httphandler

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* added unit tests

Signed-off-by: Amir Malka <amirm@armosec.io>

* merge

* more

* integrate img scan

* added unit tests

Signed-off-by: Amir Malka <amirm@armosec.io>

* more refactoring

Signed-off-by: Amir Malka <amirm@armosec.io>

* add scanned workload reference to opasessionobj

Signed-off-by: Amir Malka <amirm@armosec.io>

* fix GetWorkloadParentKind

Signed-off-by: Amir Malka <amirm@armosec.io>

* remove namespace argument from pullSingleResource, using field selector instead

Signed-off-by: Amir Malka <amirm@armosec.io>

* removed designators (unused) field from PolicyIdentifier, and designators argument from GetResources function

Signed-off-by: Amir Malka <amirm@armosec.io>

* changes

* changes

* fixes

* changes

* feat(imagescan): add an image scanning command

This commit adds a CLI command and an associated package that scan
images for vulnerabilities.

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

feat(imagescan): fail on exceeding the severity threshold

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* chore(imagescan): include dependencies

This commit adds the dependencies necessary for image scanning.

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* chore(imagescan): add dependencies to httphandler

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* chore(imagescan): create vuln db with dedicated function

Remove commented out code, too.

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* docs(imagescan): provide package-level docs

Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>

* finish merge

* image scan tests

* continue

* fixes

* refactor

* rm duplicate

* start fixes

* update gh actions

Signed-off-by: David Wertenteil <dwertent@armosec.io>

* pr fixes

* fix test

* improvements

---------

Signed-off-by: Daniel Grunberger <danielgrunberger@armosec.io>
Signed-off-by: Vlad Klokun <vklokun@protonmail.ch>
Signed-off-by: Amir Malka <amirm@armosec.io>
Signed-off-by: David Wertenteil <dwertent@armosec.io>
Co-authored-by: Daniel Grunberger <danielgrunberger@armosec.io>
Co-authored-by: Vlad Klokun <vklokun@protonmail.ch>
Co-authored-by: Amir Malka <amirm@armosec.io>
Co-authored-by: David Wertenteil <dwertent@armosec.io>
2023-08-03 12:09:33 +03:00

237 lines
8.7 KiB
Go

package printer
import (
"context"
"encoding/xml"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
logger "github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/k8s-interface/workloadinterface"
"github.com/kubescape/kubescape/v2/core/cautils"
"github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer"
"github.com/kubescape/opa-utils/reporthandling/apis"
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
"github.com/kubescape/opa-utils/shared"
)
const (
junitOutputFile = "report"
junitOutputExt = ".xml"
)
var _ printer.IPrinter = &JunitPrinter{}
type JunitPrinter struct {
writer *os.File
verbose bool
}
// https://llg.cubic.org/docs/junit/
type JUnitXML struct {
TestSuites JUnitTestSuites `xml:"testsuites"`
}
// JUnitTestSuites represents the test summary
type JUnitTestSuites struct {
XMLName xml.Name `xml:"testsuites"`
Suites []JUnitTestSuite `xml:"testsuite"` // list of controls
Errors int `xml:"errors,attr"` // total number of tests with error result from all testsuites
Failures int `xml:"failures,attr"` // total number of failed tests from all testsuites
Tests int `xml:"tests,attr"` // total number of tests from all testsuites. Some software may expect to only see the number of successful tests from all testsuites though
Time string `xml:"time,attr"` // time in seconds to execute all test suites
Name string `xml:"name,attr"` // ? Add framework names ?
}
// JUnitTestSuite represents a single control
type JUnitTestSuite struct {
XMLName xml.Name `xml:"testsuite"`
Tests int `xml:"tests,attr"` // total number of tests from this testsuite. Some software may expect to only see the number of successful tests though
Name string `xml:"name,attr"` // Full (class) name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents. Required
Errors int `xml:"errors,attr"` // The total number of tests in the suite that errors
Failures int `xml:"failures,attr"` // The total number of tests in the suite that failed
Hostname string `xml:"hostname,attr"` // Host on which the tests were executed ? cluster name ?
ID int `xml:"id,attr"` // Starts at 0 for the first testsuite and is incremented by 1 for each following testsuite
Skipped string `xml:"skipped,attr"` // The total number of skipped tests
Time string `xml:"time,attr"` // Time taken (in seconds) to execute the tests in the suite
Timestamp string `xml:"timestamp,attr"` // when the test was executed in ISO 8601 format (2014-01-21T16:17:18)
Properties []JUnitProperty `xml:"properties>property,omitempty"`
TestCases []JUnitTestCase `xml:"testcase"`
}
// JUnitTestCase represents a single resource
type JUnitTestCase struct {
XMLName xml.Name `xml:"testcase"`
Classname string `xml:"classname,attr"` // Full class name for the class the test method is in. required
Name string `xml:"name,attr"` // Name of the test method, required
Time string `xml:"time,attr"` // Time taken (in seconds) to execute the test. optional
SkipMessage *JUnitSkipMessage `xml:"skipped,omitempty"`
Failure *JUnitFailure `xml:"failure,omitempty"`
}
// JUnitSkipMessage contains the reason why a testcase was skipped.
type JUnitSkipMessage struct {
Message string `xml:"message,attr"`
}
// JUnitProperty represents a key/value pair used to define properties.
type JUnitProperty struct {
Name string `xml:"name,attr"`
Value string `xml:"value,attr"`
}
// JUnitFailure contains data related to a failed test.
type JUnitFailure struct {
Message string `xml:"message,attr"`
Type string `xml:"type,attr"`
Contents string `xml:",chardata"`
}
func NewJunitPrinter(verbose bool) *JunitPrinter {
return &JunitPrinter{
verbose: verbose,
}
}
func (jp *JunitPrinter) SetWriter(ctx context.Context, outputFile string) {
if strings.TrimSpace(outputFile) == "" {
outputFile = junitOutputFile
}
if filepath.Ext(strings.TrimSpace(outputFile)) != junitOutputExt {
outputFile = outputFile + junitOutputExt
}
jp.writer = printer.GetWriter(ctx, outputFile)
}
func (jp *JunitPrinter) Score(score float32) {
fmt.Fprintf(os.Stderr, "\nOverall compliance-score (100- Excellent, 0- All failed): %d\n", cautils.Float32ToInt(score))
}
func (jp *JunitPrinter) PrintNextSteps() {
}
func (jp *JunitPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.OPASessionObj, imageScanData []cautils.ImageScanData) {
junitResult := testsSuites(opaSessionObj)
postureReportStr, err := xml.Marshal(junitResult)
if err != nil {
logger.L().Ctx(ctx).Fatal("failed to Marshal xml result object", helpers.Error(err))
}
if _, err := jp.writer.Write(postureReportStr); err != nil {
logger.L().Ctx(ctx).Error("failed to write results", helpers.Error(err))
return
}
printer.LogOutputFile(jp.writer.Name())
}
func testsSuites(results *cautils.OPASessionObj) *JUnitTestSuites {
return &JUnitTestSuites{
Suites: listTestsSuite(results),
Tests: results.Report.SummaryDetails.NumberOfControls().All(),
Name: "Kubescape Scanning",
Failures: results.Report.SummaryDetails.NumberOfControls().Failed(),
}
}
func listTestsSuite(results *cautils.OPASessionObj) []JUnitTestSuite {
var testSuites []JUnitTestSuite
// control scan
if len(results.Report.SummaryDetails.ListFrameworks()) == 0 {
testSuite := JUnitTestSuite{}
testSuite.Tests = results.Report.SummaryDetails.NumberOfControls().All()
testSuite.Failures = results.Report.SummaryDetails.NumberOfControls().Failed()
testSuite.Timestamp = results.Report.ReportGenerationTime.String()
testSuite.ID = 0
testSuite.Name = "kubescape"
testSuite.Properties = properties(results.Report.SummaryDetails.Score)
testSuite.TestCases = testsCases(results, &results.Report.SummaryDetails.Controls, "Kubescape")
testSuites = append(testSuites, testSuite)
return testSuites
}
for i, f := range results.Report.SummaryDetails.Frameworks {
testSuite := JUnitTestSuite{}
testSuite.Tests = f.NumberOfControls().All()
testSuite.Failures = f.NumberOfControls().Failed()
testSuite.Timestamp = results.Report.ReportGenerationTime.String()
testSuite.ID = i
testSuite.Name = f.Name
testSuite.Properties = properties(f.Score)
testSuite.TestCases = testsCases(results, f.GetControls(), f.GetName())
testSuites = append(testSuites, testSuite)
}
return testSuites
}
func testsCases(results *cautils.OPASessionObj, controls reportsummary.IControlsSummaries, classname string) []JUnitTestCase {
var testCases []JUnitTestCase
for cID := range controls.ListControlsIDs(nil).All() {
testCase := JUnitTestCase{}
control := results.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, cID)
testCase.Name = control.GetName()
testCase.Classname = classname
if control.GetStatus().IsFailed() {
resources := map[string]interface{}{}
for rId, status := range control.ListResourcesIDs(nil).All() {
if status != apis.StatusFailed {
continue
}
resource := results.AllResources[rId]
sourcePath := ""
if ResourceSourcePath, ok := results.ResourceSource[rId]; ok {
sourcePath = ResourceSourcePath.RelativePath
}
resources[resourceToString(resource, sourcePath)] = nil
}
resourcesStr := shared.MapStringToSlice(resources)
sort.Strings(resourcesStr)
testCaseFailure := JUnitFailure{}
testCaseFailure.Type = "Control"
testCaseFailure.Message = fmt.Sprintf("Remediation: %s\nMore details: %s\n\n%s", control.GetRemediation(), cautils.GetControlLink(control.GetID()), strings.Join(resourcesStr, "\n"))
testCase.Failure = &testCaseFailure
} else if control.GetStatus().IsSkipped() {
testCase.SkipMessage = &JUnitSkipMessage{
Message: "", // TODO - fill after statusInfo is supported
}
}
testCases = append(testCases, testCase)
}
return testCases
}
func resourceToString(resource workloadinterface.IMetadata, sourcePath string) string {
sep := "; "
s := ""
s += fmt.Sprintf("apiVersion: %s", resource.GetApiVersion()) + sep
s += fmt.Sprintf("kind: %s", resource.GetKind()) + sep
if resource.GetNamespace() != "" {
s += fmt.Sprintf("namespace: %s", resource.GetNamespace()) + sep
}
s += fmt.Sprintf("name: %s", resource.GetName())
if sourcePath != "" {
s += sep + fmt.Sprintf("sourcePath: %s", sourcePath)
}
return s
}
func properties(complianceScore float32) []JUnitProperty {
return []JUnitProperty{
{
Name: "complianceScore",
Value: fmt.Sprintf("%.2f", complianceScore),
},
}
}