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 }