github.com/devseccon/trivy@v0.47.1-0.20231123133102-bd902a0bd996/pkg/report/sarif.go (about)

     1  package report
     2  
     3  import (
     4  	"fmt"
     5  	"html"
     6  	"io"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strings"
    10  
    11  	containerName "github.com/google/go-containerregistry/pkg/name"
    12  	"github.com/owenrumney/go-sarif/v2/sarif"
    13  	"golang.org/x/xerrors"
    14  
    15  	ftypes "github.com/devseccon/trivy/pkg/fanal/types"
    16  	"github.com/devseccon/trivy/pkg/types"
    17  )
    18  
    19  const (
    20  	sarifOsPackageVulnerability        = "OsPackageVulnerability"
    21  	sarifLanguageSpecificVulnerability = "LanguageSpecificPackageVulnerability"
    22  	sarifConfigFiles                   = "Misconfiguration"
    23  	sarifSecretFiles                   = "Secret"
    24  	sarifLicenseFiles                  = "License"
    25  	sarifUnknownIssue                  = "UnknownIssue"
    26  
    27  	sarifError   = "error"
    28  	sarifWarning = "warning"
    29  	sarifNote    = "note"
    30  	sarifNone    = "none"
    31  
    32  	columnKind = "utf16CodeUnits"
    33  
    34  	builtinRulesUrl = "https://github.com/devseccon/trivy/blob/main/pkg/fanal/secret/builtin-rules.go" // list all secrets
    35  )
    36  
    37  var (
    38  	rootPath = "file:///"
    39  
    40  	// pathRegex to extract file path in case string includes (distro:version)
    41  	pathRegex = regexp.MustCompile(`(?P<path>.+?)(?:\s*\((?:.*?)\).*?)?$`)
    42  )
    43  
    44  // SarifWriter implements result Writer
    45  type SarifWriter struct {
    46  	Output        io.Writer
    47  	Version       string
    48  	run           *sarif.Run
    49  	locationCache map[string][]location
    50  	Target        string
    51  }
    52  
    53  type sarifData struct {
    54  	title            string
    55  	vulnerabilityId  string
    56  	shortDescription string
    57  	fullDescription  string
    58  	helpText         string
    59  	helpMarkdown     string
    60  	resourceClass    types.ResultClass
    61  	severity         string
    62  	url              string
    63  	resultIndex      int
    64  	artifactLocation string
    65  	locationMessage  string
    66  	message          string
    67  	cvssScore        string
    68  	locations        []location
    69  }
    70  
    71  type location struct {
    72  	startLine int
    73  	endLine   int
    74  }
    75  
    76  func (sw *SarifWriter) addSarifRule(data *sarifData) {
    77  	r := sw.run.AddRule(data.vulnerabilityId).
    78  		WithName(toSarifRuleName(data.resourceClass)).
    79  		WithDescription(data.vulnerabilityId).
    80  		WithShortDescription(&sarif.MultiformatMessageString{Text: &data.shortDescription}).
    81  		WithFullDescription(&sarif.MultiformatMessageString{Text: &data.fullDescription}).
    82  		WithHelp(&sarif.MultiformatMessageString{
    83  			Text:     &data.helpText,
    84  			Markdown: &data.helpMarkdown,
    85  		}).
    86  		WithDefaultConfiguration(&sarif.ReportingConfiguration{
    87  			Level: toSarifErrorLevel(data.severity),
    88  		}).
    89  		WithProperties(sarif.Properties{
    90  			"tags": []string{
    91  				data.title,
    92  				"security",
    93  				data.severity,
    94  			},
    95  			"precision":         "very-high",
    96  			"security-severity": data.cvssScore,
    97  		})
    98  	if data.url != "" {
    99  		r.WithHelpURI(data.url)
   100  	}
   101  }
   102  
   103  func (sw *SarifWriter) addSarifResult(data *sarifData) {
   104  	sw.addSarifRule(data)
   105  
   106  	result := sarif.NewRuleResult(data.vulnerabilityId).
   107  		WithRuleIndex(data.resultIndex).
   108  		WithMessage(sarif.NewTextMessage(data.message)).
   109  		WithLevel(toSarifErrorLevel(data.severity)).
   110  		WithLocations(toSarifLocations(data.locations, data.artifactLocation, data.locationMessage))
   111  	sw.run.AddResult(result)
   112  }
   113  
   114  func getRuleIndex(id string, indexes map[string]int) int {
   115  	if i, ok := indexes[id]; ok {
   116  		return i
   117  	} else {
   118  		l := len(indexes)
   119  		indexes[id] = l
   120  		return l
   121  	}
   122  }
   123  
   124  func (sw *SarifWriter) Write(report types.Report) error {
   125  	sarifReport, err := sarif.New(sarif.Version210)
   126  	if err != nil {
   127  		return xerrors.Errorf("error creating a new sarif template: %w", err)
   128  	}
   129  	sw.run = sarif.NewRunWithInformationURI("Trivy", "https://github.com/devseccon/trivy")
   130  	sw.run.Tool.Driver.WithVersion(sw.Version)
   131  	sw.run.Tool.Driver.WithFullName("Trivy Vulnerability Scanner")
   132  	sw.locationCache = make(map[string][]location)
   133  	if report.ArtifactType == ftypes.ArtifactContainerImage {
   134  		sw.run.Properties = sarif.Properties{
   135  			"imageName":   report.ArtifactName,
   136  			"repoTags":    report.Metadata.RepoTags,
   137  			"repoDigests": report.Metadata.RepoDigests,
   138  		}
   139  	}
   140  	if sw.Target != "" {
   141  		absPath, _ := filepath.Abs(sw.Target)
   142  		rootPath = fmt.Sprintf("file://%s/", absPath)
   143  	}
   144  
   145  	ruleIndexes := make(map[string]int)
   146  	for _, res := range report.Results {
   147  		target := ToPathUri(res.Target, res.Class)
   148  
   149  		for _, vuln := range res.Vulnerabilities {
   150  			fullDescription := vuln.Description
   151  			if fullDescription == "" {
   152  				fullDescription = vuln.Title
   153  			}
   154  			path := target
   155  			if vuln.PkgPath != "" {
   156  				path = ToPathUri(vuln.PkgPath, res.Class)
   157  			}
   158  			sw.addSarifResult(&sarifData{
   159  				title:            "vulnerability",
   160  				vulnerabilityId:  vuln.VulnerabilityID,
   161  				severity:         vuln.Severity,
   162  				cvssScore:        getCVSSScore(vuln),
   163  				url:              vuln.PrimaryURL,
   164  				resourceClass:    res.Class,
   165  				artifactLocation: path,
   166  				locationMessage:  fmt.Sprintf("%v: %v@%v", path, vuln.PkgName, vuln.InstalledVersion),
   167  				locations:        sw.getLocations(vuln.PkgName, vuln.InstalledVersion, path, res.Packages),
   168  				resultIndex:      getRuleIndex(vuln.VulnerabilityID, ruleIndexes),
   169  				shortDescription: html.EscapeString(vuln.Title),
   170  				fullDescription:  html.EscapeString(fullDescription),
   171  				helpText: fmt.Sprintf(`Vulnerability %v\nSeverity: %v\nPackage: %v\nFixed Version: %v\nLink: [%v](%v)\n%v`,
   172  					vuln.VulnerabilityID, vuln.Severity, vuln.PkgName, vuln.FixedVersion, vuln.VulnerabilityID, vuln.PrimaryURL, vuln.Description),
   173  				helpMarkdown: fmt.Sprintf(`**Vulnerability %v**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|%v|%v|%v|[%v](%v)|\n\n%v`,
   174  					vuln.VulnerabilityID, vuln.Severity, vuln.PkgName, vuln.FixedVersion, vuln.VulnerabilityID, vuln.PrimaryURL, vuln.Description),
   175  				message: fmt.Sprintf(`Package: %v\nInstalled Version: %v\nVulnerability %v\nSeverity: %v\nFixed Version: %v\nLink: [%v](%v)`,
   176  					vuln.PkgName, vuln.InstalledVersion, vuln.VulnerabilityID, vuln.Severity, vuln.FixedVersion, vuln.VulnerabilityID, vuln.PrimaryURL),
   177  			})
   178  		}
   179  		for _, misconf := range res.Misconfigurations {
   180  			sw.addSarifResult(&sarifData{
   181  				title:            "misconfiguration",
   182  				vulnerabilityId:  misconf.ID,
   183  				severity:         misconf.Severity,
   184  				cvssScore:        severityToScore(misconf.Severity),
   185  				url:              misconf.PrimaryURL,
   186  				resourceClass:    res.Class,
   187  				artifactLocation: target,
   188  				locationMessage:  target,
   189  				locations: []location{
   190  					{
   191  						startLine: misconf.CauseMetadata.StartLine,
   192  						endLine:   misconf.CauseMetadata.EndLine,
   193  					},
   194  				},
   195  				resultIndex:      getRuleIndex(misconf.ID, ruleIndexes),
   196  				shortDescription: html.EscapeString(misconf.Title),
   197  				fullDescription:  html.EscapeString(misconf.Description),
   198  				helpText: fmt.Sprintf(`Misconfiguration %v\nType: %s\nSeverity: %v\nCheck: %v\nMessage: %v\nLink: [%v](%v)\n%s`,
   199  					misconf.ID, misconf.Type, misconf.Severity, misconf.Title, misconf.Message, misconf.ID, misconf.PrimaryURL, misconf.Description),
   200  				helpMarkdown: fmt.Sprintf(`**Misconfiguration %v**\n| Type | Severity | Check | Message | Link |\n| --- | --- | --- | --- | --- |\n|%v|%v|%v|%s|[%v](%v)|\n\n%v`,
   201  					misconf.ID, misconf.Type, misconf.Severity, misconf.Title, misconf.Message, misconf.ID, misconf.PrimaryURL, misconf.Description),
   202  				message: fmt.Sprintf(`Artifact: %v\nType: %v\nVulnerability %v\nSeverity: %v\nMessage: %v\nLink: [%v](%v)`,
   203  					res.Target, res.Type, misconf.ID, misconf.Severity, misconf.Message, misconf.ID, misconf.PrimaryURL),
   204  			})
   205  		}
   206  		for _, secret := range res.Secrets {
   207  			sw.addSarifResult(&sarifData{
   208  				title:            "secret",
   209  				vulnerabilityId:  secret.RuleID,
   210  				severity:         secret.Severity,
   211  				cvssScore:        severityToScore(secret.Severity),
   212  				url:              builtinRulesUrl,
   213  				resourceClass:    res.Class,
   214  				artifactLocation: target,
   215  				locationMessage:  target,
   216  				locations: []location{
   217  					{
   218  						startLine: secret.StartLine,
   219  						endLine:   secret.EndLine,
   220  					},
   221  				},
   222  				resultIndex:      getRuleIndex(secret.RuleID, ruleIndexes),
   223  				shortDescription: html.EscapeString(secret.Title),
   224  				fullDescription:  html.EscapeString(secret.Match),
   225  				helpText: fmt.Sprintf(`Secret %v\nSeverity: %v\nMatch: %s`,
   226  					secret.Title, secret.Severity, secret.Match),
   227  				helpMarkdown: fmt.Sprintf(`**Secret %v**\n| Severity | Match |\n| --- | --- |\n|%v|%v|`,
   228  					secret.Title, secret.Severity, secret.Match),
   229  				message: fmt.Sprintf(`Artifact: %v\nType: %v\nSecret %v\nSeverity: %v\nMatch: %v`,
   230  					res.Target, res.Type, secret.Title, secret.Severity, secret.Match),
   231  			})
   232  		}
   233  		for _, license := range res.Licenses {
   234  			id := fmt.Sprintf("%s:%s", license.PkgName, license.Name)
   235  			desc := fmt.Sprintf("%s in %s", license.Name, license.PkgName)
   236  			sw.addSarifResult(&sarifData{
   237  				title:            "license",
   238  				vulnerabilityId:  id,
   239  				severity:         license.Severity,
   240  				cvssScore:        severityToScore(license.Severity),
   241  				url:              license.Link,
   242  				resourceClass:    res.Class,
   243  				artifactLocation: target,
   244  				resultIndex:      getRuleIndex(id, ruleIndexes),
   245  				shortDescription: desc,
   246  				fullDescription:  desc,
   247  				helpText: fmt.Sprintf(`License %s\nClassification: %s\nPkgName: %s\nPath: %s`,
   248  					license.Name, license.Category, license.PkgName, license.FilePath),
   249  				helpMarkdown: fmt.Sprintf(`**License %s**\n| PkgName | Classification | Path |\n| --- | --- | --- |\n|%s|%s|%s|`,
   250  					license.Name, license.PkgName, license.Category, license.FilePath),
   251  				message: fmt.Sprintf(`Artifact: %s\nLicense %s\nPkgName: %s\n Classification: %s\n Path: %s`,
   252  					res.Target, license.Name, license.Category, license.PkgName, license.FilePath),
   253  			})
   254  		}
   255  
   256  	}
   257  	sw.run.ColumnKind = columnKind
   258  	sw.run.OriginalUriBaseIDs = map[string]*sarif.ArtifactLocation{
   259  		"ROOTPATH": {URI: &rootPath},
   260  	}
   261  	sarifReport.AddRun(sw.run)
   262  	return sarifReport.PrettyWrite(sw.Output)
   263  }
   264  
   265  func toSarifLocations(locations []location, artifactLocation, locationMessage string) []*sarif.Location {
   266  	var sarifLocs []*sarif.Location
   267  	// add default (hardcoded) location for vulnerabilities that don't support locations
   268  	if len(locations) == 0 {
   269  		locations = append(locations, location{
   270  			startLine: 1,
   271  			endLine:   1,
   272  		})
   273  	}
   274  
   275  	// some dependencies can be placed in multiple places.
   276  	// e.g.https://github.com/aquasecurity/go-dep-parser/pull/134#discussion_r985353240
   277  	// create locations for each place.
   278  
   279  	for _, l := range locations {
   280  		// location is missed. Use default (hardcoded) value (misconfigurations have this case)
   281  		if l.startLine == 0 && l.endLine == 0 {
   282  			l.startLine = 1
   283  			l.endLine = 1
   284  		}
   285  		region := sarif.NewRegion().WithStartLine(l.startLine).WithEndLine(l.endLine).WithStartColumn(1).WithEndColumn(1)
   286  		loc := sarif.NewPhysicalLocation().
   287  			WithArtifactLocation(sarif.NewSimpleArtifactLocation(artifactLocation).WithUriBaseId("ROOTPATH")).
   288  			WithRegion(region)
   289  		sarifLocs = append(sarifLocs, sarif.NewLocation().WithMessage(sarif.NewTextMessage(locationMessage)).WithPhysicalLocation(loc))
   290  	}
   291  
   292  	return sarifLocs
   293  }
   294  
   295  func toSarifRuleName(class types.ResultClass) string {
   296  	switch class {
   297  	case types.ClassOSPkg:
   298  		return sarifOsPackageVulnerability
   299  	case types.ClassLangPkg:
   300  		return sarifLanguageSpecificVulnerability
   301  	case types.ClassConfig:
   302  		return sarifConfigFiles
   303  	case types.ClassSecret:
   304  		return sarifSecretFiles
   305  	case types.ClassLicense, types.ClassLicenseFile:
   306  		return sarifLicenseFiles
   307  	default:
   308  		return sarifUnknownIssue
   309  	}
   310  }
   311  
   312  func toSarifErrorLevel(severity string) string {
   313  	switch severity {
   314  	case "CRITICAL", "HIGH":
   315  		return sarifError
   316  	case "MEDIUM":
   317  		return sarifWarning
   318  	case "LOW", "UNKNOWN":
   319  		return sarifNote
   320  	default:
   321  		return sarifNone
   322  	}
   323  }
   324  
   325  func ToPathUri(input string, resultClass types.ResultClass) string {
   326  	// we only need to convert OS input
   327  	// e.g. image names, digests, etc...
   328  	if resultClass != types.ClassOSPkg {
   329  		return input
   330  	}
   331  	var matches = pathRegex.FindStringSubmatch(input)
   332  	if matches != nil {
   333  		input = matches[pathRegex.SubexpIndex("path")]
   334  	}
   335  	ref, err := containerName.ParseReference(input)
   336  	if err == nil {
   337  		input = ref.Context().RepositoryStr()
   338  	}
   339  
   340  	return strings.ReplaceAll(strings.ReplaceAll(input, "\\", "/"), "git::https:/", "")
   341  }
   342  
   343  func (sw *SarifWriter) getLocations(name, version, path string, pkgs []ftypes.Package) []location {
   344  	id := fmt.Sprintf("%s@%s@%s", path, name, version)
   345  	locs, ok := sw.locationCache[id]
   346  	if !ok {
   347  		for _, pkg := range pkgs {
   348  			if name == pkg.Name && version == pkg.Version {
   349  				for _, l := range pkg.Locations {
   350  					loc := location{
   351  						startLine: l.StartLine,
   352  						endLine:   l.EndLine,
   353  					}
   354  					locs = append(locs, loc)
   355  				}
   356  				sw.locationCache[id] = locs
   357  				return locs
   358  			}
   359  		}
   360  	}
   361  	return locs
   362  }
   363  
   364  func getCVSSScore(vuln types.DetectedVulnerability) string {
   365  	// Take the vendor score
   366  	if cvss, ok := vuln.CVSS[vuln.SeveritySource]; ok {
   367  		return fmt.Sprintf("%.1f", cvss.V3Score)
   368  	}
   369  
   370  	// Converts severity to score
   371  	return severityToScore(vuln.Severity)
   372  }
   373  
   374  func severityToScore(severity string) string {
   375  	switch severity {
   376  	case "CRITICAL":
   377  		return "9.5"
   378  	case "HIGH":
   379  		return "8.0"
   380  	case "MEDIUM":
   381  		return "5.5"
   382  	case "LOW":
   383  		return "2.0"
   384  	default:
   385  		return "0.0"
   386  	}
   387  }