Files
kubescape/core/pkg/resultshandling/printer/v2/sarifprinter.go
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

352 lines
10 KiB
Go

package printer
import (
"context"
"fmt"
"net/url"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/anchore/grype/grype/presenter/models"
logger "github.com/kubescape/go-logger"
"github.com/kubescape/go-logger/helpers"
"github.com/kubescape/kubescape/v2/core/cautils"
"github.com/kubescape/kubescape/v2/core/pkg/fixhandler"
"github.com/kubescape/kubescape/v2/core/pkg/resultshandling/locationresolver"
"github.com/kubescape/kubescape/v2/core/pkg/resultshandling/printer"
"github.com/kubescape/opa-utils/objectsenvelopes/localworkload"
"github.com/kubescape/opa-utils/reporthandling/results/v1/reportsummary"
"github.com/kubescape/opa-utils/reporthandling/results/v1/resourcesresults"
v2 "github.com/kubescape/opa-utils/reporthandling/v2"
"github.com/owenrumney/go-sarif/v2/sarif"
"github.com/sergi/go-diff/diffmatchpatch"
)
const (
sarifOutputFile = "report"
sarifOutputExt = ".sarif"
toolName = "kubescape"
toolInfoURI = "https://armosec.io"
)
// sarifSeverityLevel is a SARIF-specific severity level for Rules and Results
type sarifSeverityLevel string
const (
sarifSeverityLevelNote sarifSeverityLevel = "note"
sarifSeverityLevelWarning sarifSeverityLevel = "warning"
sarifSeverityLevelError sarifSeverityLevel = "error"
)
// scoreFactorToSARIFSeverityLevel returns a SARIF severity level that matches
// a given Kubescape severity score
func scoreFactorToSARIFSeverityLevel(score float32) sarifSeverityLevel {
switch {
case score >= 9.0:
return sarifSeverityLevelError
case score >= 4.0:
return sarifSeverityLevelWarning
}
return sarifSeverityLevelNote
}
var _ printer.IPrinter = &SARIFPrinter{}
// SARIFPrinter is a printer that emits the report in the SARIF format
type SARIFPrinter struct {
// outputFile is the name of the output file
writer *os.File
}
// NewSARIFPrinter returns a new SARIF printer instance
func NewSARIFPrinter() *SARIFPrinter {
return &SARIFPrinter{}
}
func (sp *SARIFPrinter) Score(score float32) {
}
func (sp *SARIFPrinter) SetWriter(ctx context.Context, outputFile string) {
if strings.TrimSpace(outputFile) == "" {
outputFile = sarifOutputFile
}
if filepath.Ext(strings.TrimSpace(outputFile)) != sarifOutputExt {
outputFile = outputFile + sarifOutputExt
}
sp.writer = printer.GetWriter(ctx, outputFile)
}
// addRule adds a rule description to the scan run based on the given control summary
func (sp *SARIFPrinter) addRule(scanRun *sarif.Run, control reportsummary.IControlSummary) {
controlSARIFSeverity := string(scoreFactorToSARIFSeverityLevel(control.GetScoreFactor()))
configuration := sarif.NewReportingConfiguration().WithLevel(controlSARIFSeverity)
scanRun.AddRule(control.GetID()).
WithDefaultConfiguration(configuration).
WithShortDescription(sarif.NewMultiformatMessageString(control.GetName())).
WithFullDescription(sarif.NewMultiformatMessageString(control.GetDescription())).
WithHelp(sarif.NewMultiformatMessageString(sp.generateRemediationMessage(control)))
}
// addResult adds a result of checking a rule to the scan run based on the given control summary
func (sp *SARIFPrinter) addResult(scanRun *sarif.Run, ctl reportsummary.IControlSummary, filepath string, location locationresolver.Location) *sarif.Result {
return scanRun.CreateResultForRule(ctl.GetID()).
WithMessage(sarif.NewTextMessage(ctl.GetDescription())).
WithLocations([]*sarif.Location{
sarif.NewLocationWithPhysicalLocation(
sarif.NewPhysicalLocation().
WithArtifactLocation(
sarif.NewSimpleArtifactLocation(filepath),
).WithRegion(
sarif.NewRegion().WithStartLine(location.Line).WithStartColumn(location.Column),
),
),
})
}
func (sp *SARIFPrinter) PrintImageScan(context.Context, *models.PresenterConfig) {
}
func (sp *SARIFPrinter) PrintNextSteps() {
}
func (sp *SARIFPrinter) ActionPrint(ctx context.Context, opaSessionObj *cautils.OPASessionObj, imageScanData []cautils.ImageScanData) {
report, err := sarif.New(sarif.Version210)
if err != nil {
panic(err)
}
run := sarif.NewRunWithInformationURI(toolName, toolInfoURI)
basePath := getBasePathFromMetadata(*opaSessionObj)
for resourceID, result := range opaSessionObj.ResourcesResult {
if result.GetStatus(nil).IsFailed() {
resourceSource := opaSessionObj.ResourceSource[resourceID]
filepath := resourceSource.RelativePath
// Github Code Scanning considers results not associated to a file path meaningless and invalid when uploading
if filepath == "" || basePath == "" {
continue
}
rsrcAbsPath := path.Join(basePath, filepath)
locationResolver, err := locationresolver.NewFixPathLocationResolver(rsrcAbsPath)
if err != nil {
logger.L().Debug("failed to create location resolver", helpers.Error(err))
}
for _, toPin := range result.AssociatedControls {
ac := toPin
if ac.GetStatus(nil).IsFailed() {
ctl := opaSessionObj.Report.SummaryDetails.Controls.GetControl(reportsummary.EControlCriteriaID, ac.GetID())
location := sp.resolveFixLocation(opaSessionObj, locationResolver, &ac, resourceID)
sp.addRule(run, ctl)
result := sp.addResult(run, ctl, filepath, location)
collectFixes(ctx, result, ac, opaSessionObj, resourceID, filepath)
}
}
}
}
report.AddRun(run)
report.PrettyWrite(sp.writer)
printer.LogOutputFile(sp.writer.Name())
}
func (sp *SARIFPrinter) resolveFixLocation(opaSessionObj *cautils.OPASessionObj, locationResolver *locationresolver.FixPathLocationResolver, ac *resourcesresults.ResourceAssociatedControl, resourceID string) locationresolver.Location {
defaultLocation := locationresolver.Location{Line: 1, Column: 1}
if locationResolver == nil {
return defaultLocation
}
fixPaths := failedPathsToString(ac)
if len(fixPaths) == 0 {
fixPaths = fixPathsToString(ac)
}
var fixPath string
if len(fixPaths) > 0 {
fixPath = fixPaths[0]
}
var location locationresolver.Location
if fixPath == "" {
return defaultLocation
}
docIndex, ok := getDocIndex(opaSessionObj, resourceID)
if !ok {
return defaultLocation
}
location, _ = locationResolver.ResolveLocation(fixPath, docIndex)
if location.Line == 0 {
return defaultLocation
}
return location
}
func addFix(result *sarif.Result, filepath string, startLine int, startColumn int, endLine int, endColumn int, text string) {
result.AddFix(
sarif.NewFix().
WithArtifactChanges([]*sarif.ArtifactChange{
sarif.NewArtifactChange(
sarif.NewSimpleArtifactLocation(filepath),
).WithReplacement(
sarif.NewReplacement(sarif.NewRegion().
WithStartLine(startLine).
WithStartColumn(startColumn).
WithEndLine(endLine).
WithEndColumn(endColumn),
).WithInsertedContent(
sarif.NewArtifactContent().WithText(text),
),
),
}),
)
}
func calculateMove(str string, file []string, endColumn int, endLine int) (int, int, bool) {
num, err := strconv.Atoi(str)
if err != nil {
logger.L().Debug("failed to get move from string "+str, helpers.Error(err))
return 0, 0, false
}
for num+endColumn-1 > len(file[endLine-1]) {
num -= len(file[endLine-1]) - endColumn + 2
endLine++
endColumn = 1
}
endColumn += num
return endLine, endColumn, true
}
func collectDiffs(dmp *diffmatchpatch.DiffMatchPatch, diffs []diffmatchpatch.Diff, result *sarif.Result, filepath string, fileAsString string) {
file := strings.Split(fileAsString, "\n")
text := ""
startLine := 1
startColumn := 1
endLine := 1
endColumn := 1
delta := strings.Split(dmp.DiffToDelta(diffs), "\t")
for index, seg := range delta {
switch seg[0] {
case '+':
var err error
text, err = url.QueryUnescape(seg[1:])
if err != nil {
logger.L().Debug("failed to unescape string", helpers.Error(err))
continue
}
if index >= len(delta)-1 || delta[index+1][0] == '=' {
addFix(result, filepath, startLine, startColumn, endLine, endColumn, text)
}
case '-':
var ok bool
endLine, endColumn, ok = calculateMove(seg[1:], file, endColumn, endLine)
if !ok {
continue
}
if index >= len(delta)-1 || delta[index+1][0] == '=' {
addFix(result, filepath, startLine, startColumn, endLine, endColumn, text)
}
case '=':
var ok bool
endLine, endColumn, ok = calculateMove(seg[1:], file, endColumn, endLine)
if !ok {
continue
}
startLine = endLine
startColumn = endColumn
text = ""
}
}
}
func collectFixes(ctx context.Context, result *sarif.Result, ac resourcesresults.ResourceAssociatedControl, opaSessionObj *cautils.OPASessionObj, resourceID string, filepath string) {
for _, rule := range ac.ResourceAssociatedRules {
if !rule.GetStatus(nil).IsFailed() {
continue
}
for _, rulePaths := range rule.Paths {
if rulePaths.FixPath.Path == "" {
continue
}
// if strings.HasPrefix(rulePaths.FixPath.Value, fixhandler.UserValuePrefix) {
// continue
// }
documentIndex, ok := getDocIndex(opaSessionObj, resourceID)
if !ok {
continue
}
yamlExpression := fixhandler.FixPathToValidYamlExpression(rulePaths.FixPath.Path, rulePaths.FixPath.Value, documentIndex)
fileAsString, err := fixhandler.GetFileString(filepath)
if err != nil {
logger.L().Debug("failed to access "+filepath, helpers.Error(err))
continue
}
fixedYamlString, err := fixhandler.ApplyFixToContent(ctx, fileAsString, yamlExpression)
if err != nil {
logger.L().Debug("failed to fix "+filepath+" with "+yamlExpression, helpers.Error(err))
continue
}
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(fileAsString, fixedYamlString, false)
collectDiffs(dmp, diffs, result, filepath, fileAsString)
}
}
}
func getDocIndex(opaSessionObj *cautils.OPASessionObj, resourceID string) (int, bool) {
resource := opaSessionObj.AllResources[resourceID]
localworkload, ok := resource.(*localworkload.LocalWorkload)
if !ok {
return 0, false
}
splittedPath := strings.Split(localworkload.GetPath(), ":")
if len(splittedPath) <= 1 {
return 0, false
}
docIndex, err := strconv.Atoi(splittedPath[1])
if err != nil {
return 0, false
}
return docIndex, true
}
func getBasePathFromMetadata(opaSessionObj cautils.OPASessionObj) string {
switch opaSessionObj.Metadata.ScanMetadata.ScanningTarget {
case v2.GitLocal:
return opaSessionObj.Metadata.ContextMetadata.RepoContextMetadata.LocalRootPath
case v2.Directory:
return opaSessionObj.Metadata.ContextMetadata.DirectoryContextMetadata.BasePath
default:
return ""
}
}
// generateRemediationMessage generates a remediation message for the given control summary
func (sp *SARIFPrinter) generateRemediationMessage(control reportsummary.IControlSummary) string {
return fmt.Sprintf("Remediation: %s", control.GetRemediation())
}