1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
|
package agent
import (
"fmt"
"net/url"
"regexp"
"strconv"
"time"
report "gitlab.com/gitlab-org/security-products/analyzers/report/v3"
)
var (
// This regex captures 2 groups,
// First group being characters before: ` (`
// Second group being characters between `(` and `)`
// Example input: "nginx:1.16 (debian 10.3)"
// Expected output: 1st_group="nginx:1.16" 2nd_group="debian 10.3"
convertTargetRegexp = regexp.MustCompile(`^(.+)\s\((.+)\)$`)
)
// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/k8s/report/report.go?ref_type=tags#L51
type ConsolidatedReport struct {
Findings []Resource `json:"Findings"`
}
// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/k8s/report/report.go#L58
type Resource struct {
Namespace string `json:"Namespace"`
Kind string `json:"Kind"`
Name string `json:"Name"`
Results []Result `json:"Results"`
}
// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/types/report.go#L71
type Result struct {
Target string `json:"Target"`
Class string `json:"Class"`
Type string `json:"Type"`
Vulnerabilities []DetectedVulnerability `json:"Vulnerabilities"`
}
// Type referenced from Trivy https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/types/vulnerability.go#L9
type DetectedVulnerability struct {
VulnerabilityID string `json:"VulnerabilityID"`
PkgName string `json:"PkgName"`
InstalledVersion string `json:"InstalledVersion"`
FixedVersion string `json:"FixedVersion"`
PrimaryURL string `json:"PrimaryURL"`
// Embed vulnerability details
Vulnerability
}
// Type referenced from Trivy-db https://gitlab.com/gitlab-org/security-products/dependencies/trivy-db/-/blob/4bcdf1c414d0/pkg/types/types.go#L132 referenced by Trivy v0.38.3
type Vulnerability struct {
Title string `json:"Title"`
Description string `json:"Description"`
Severity string `json:"Severity"` // Selected from VendorSeverity, depending on a scan target
References []string `json:"References"`
PublishedDate *time.Time `json:"PublishedDate"` // Take from NVD
LastModifiedDate *time.Time `json:"LastModifiedDate"` // Take from NVD
}
var TrivyScanner = report.ScannerDetails{
ID: "starboard_trivy",
Name: "Trivy (via Starboard Operator)",
Vendor: report.Vendor{
Name: "GitLab",
},
}
// Convert turns a Trivy k8s vulnerability report into a slice of payloads which
// can be sent to the internal vulnerability API
func Convert(findings []Resource, agentID int64) ([]*Payload, error) {
payloads := make([]*Payload, 0)
for _, finding := range findings {
imageName, operatingSystem := findImageName(finding)
kubernetesResource := convertKubernetesResource(finding, agentID)
results := finding.Results
for _, result := range results {
vulns := result.Vulnerabilities
for _, vuln := range vulns {
payload := convert(vuln)
payload.Vulnerability.Location = convertLocation(imageName, operatingSystem, kubernetesResource, vuln)
payload.Scanner.Version = TrivyScannerVersion
payloads = append(payloads, payload)
}
}
}
return payloads, nil
}
type Payload struct {
Vulnerability *report.Vulnerability `json:"vulnerability"`
Scanner report.ScannerDetails `json:"scanner"`
}
func convert(vuln DetectedVulnerability) *Payload {
return &Payload{
Vulnerability: convertVulnerability(vuln),
Scanner: TrivyScanner,
}
}
func convertVulnerability(vuln DetectedVulnerability) *report.Vulnerability {
return &report.Vulnerability{
Name: vuln.VulnerabilityID,
Message: fmt.Sprintf("%s in %s", vuln.VulnerabilityID, vuln.PkgName),
Description: vuln.Description,
Solution: fmt.Sprintf("Upgrade %s from %s to %s", vuln.PkgName, vuln.InstalledVersion, vuln.FixedVersion),
Severity: convertSeverity(vuln.Severity),
Confidence: report.ConfidenceLevelUnknown,
Identifiers: convertIdentifiers(vuln),
Links: convertLinks(vuln),
}
}
// Adapted from severityNames in Trivy-db https://gitlab.com/gitlab-org/security-products/dependencies/trivy-db/-/blob/2bd1364579ec652f8f595c4a61595fd9575e8496/pkg/types/types.go#L35
const (
SeverityCritical = "CRITICAL"
SeverityHigh = "HIGH"
SeverityMedium = "MEDIUM"
SeverityLow = "LOW"
SeverityNone = "NONE" // Kept for legacy reasons since starboard contains this severity level
SeverityUnknown = "UNKNOWN"
)
var severityMapping = map[string]report.SeverityLevel{
SeverityCritical: report.SeverityLevelCritical,
SeverityHigh: report.SeverityLevelHigh,
SeverityMedium: report.SeverityLevelMedium,
SeverityLow: report.SeverityLevelLow,
SeverityNone: report.SeverityLevelInfo,
SeverityUnknown: report.SeverityLevelUnknown,
}
func convertSeverity(severity string) report.SeverityLevel {
sev, ok := severityMapping[severity]
if !ok {
return report.SeverityLevelUnknown
}
return sev
}
func convertLocation(image string, operatingSystem string, kubernetesResource *report.KubernetesResource, vuln DetectedVulnerability) report.Location {
return report.Location{
Dependency: &report.Dependency{
Package: report.Package{Name: vuln.PkgName},
Version: vuln.InstalledVersion,
},
KubernetesResource: kubernetesResource,
Image: image,
OperatingSystem: operatingSystem,
}
}
// Location is used to fingerprint(uniquely identify) the finding in gitlab. The fields used for fingerprinting are: agentID, k8sresource.namespace, k8sresource.kind, k8sresource.name, k8sresource.container, PkgName
// As defined here: https://gitlab.com/gitlab-org/gitlab/-/blob/f50075762cf33d3841b88bb191770776b07ede77/ee/app/services/vulnerabilities/starboard_vulnerability_create_service.rb#L62
// WARNING! Be extra careful when changing these fields as it could cause new findings to be flagged to the user when they might have been previously addressed.
func convertKubernetesResource(finding Resource, agentID int64) *report.KubernetesResource {
return &report.KubernetesResource{
Namespace: finding.Namespace,
Name: finding.Name,
Kind: finding.Kind,
AgentID: strconv.FormatInt(agentID, 10),
ContainerName: "", //NOTE In Trivy k8s, the ContainerName is not provided. https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/k8s/report/report.go#L58-L69.
// Leaving ContainerName as an empty string as such.
// This does not affect the fingerprint as the field referenced in gitlab is `container` while the one defined in KubernetesResource is `container_name`.
}
}
func convertIdentifiers(vuln DetectedVulnerability) []report.Identifier {
id := vuln.VulnerabilityID
return []report.Identifier{
{
Type: report.IdentifierTypeCVE,
Name: id,
Value: id,
URL: fmt.Sprintf("https://cve.mitre.org/cgi-bin/cvename.cgi?name=%s", url.QueryEscape(id)),
},
}
}
func convertLinks(vuln DetectedVulnerability) []report.Link {
var links []report.Link // nolint:prealloc
if vuln.PrimaryURL != "" {
links = append(links, report.Link{URL: vuln.PrimaryURL})
}
for _, r := range vuln.References {
links = append(links, report.Link{URL: r})
}
return links
}
// findImageName identifies the image associated with the finding.
// When transmitting report to Gitlab the image name is required for users to identify the source of the vulnerability.
// Trivy k8s scans for both OS and language vulnerabilities.
// For os-pkgs, result.Target is the image name that is being scanned.
// For lang-pkgs(language packages), result.Target is a directory eg `usr/local/bin/trivy` which is not useful to users in identifying the image that contains the vulnerability.
// This function serves to provide the image name for language package vulnerabilities.
func findImageName(finding Resource) (string, string) {
for _, result := range finding.Results {
if result.Class == "os-pkgs" {
imageName, operatingSystem := convertTarget(result.Target)
return imageName, operatingSystem
}
}
return "", ""
}
// convertTarget converts the target into imageName and OS strings
// Target example "nginx:1.16 (debian 10.3)" would output imageName=`nginx:1.16` OS=`debian 10.3`
// Target is defined here:
// https://gitlab.com/gitlab-org/security-products/dependencies/trivy/-/blob/v0.38.3/pkg/scanner/local/scan.go#L281
func convertTarget(target string) (string, string) {
match := convertTargetRegexp.FindStringSubmatch(target)
if len(match) == 3 {
return match[1], match[2]
}
return "", ""
}
|