File: report.go

package info (click to toggle)
gitlab-agent 16.1.3-2
  • links: PTS, VCS
  • area: contrib
  • in suites: forky, sid, trixie
  • size: 6,324 kB
  • sloc: makefile: 175; sh: 52; ruby: 3
file content (226 lines) | stat: -rw-r--r-- 8,774 bytes parent folder | download
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 "", ""
}