Severity of container image from Trivy Scan on Harbor

調查 trivy image severity

這篇文章將紀錄 Harbor 搭配 vulnerabilities Scanning 功能後,在 UI 呈現 Image 的 Severity 與 Score 的關係。

事件發生

客戶環境的 Harbor 有使用 Vulnerability Scanning 功能,並搭配 Trivy,因此可以在 Harbor UI 上看到每個 image 的 CVE,如下圖所式:

harbor

由於 UI 可以看到 CVSS3Severity,但是常常發生分數和 Severity 無法匹配的情況,例如上圖:

CVSS3: https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator

CVE CVSS3 Severity
CVE-2024-20945 redhat: 4.7 Medium
CVE-2022-43552 nvd: 5.9 redhat: 5.9 Low

客戶希望我們能向他們說明,上面的嚴重等級,是如何計算的,

調查經過

Harbor UI 是前後端分離的架構,透過 chrome 的 inspect tool 可以看到所 invoke 的 API: /api/v2.0/projects/library/repositories/openjdk/artifacts/<sha>/additions/vulnerabilities,他的 resource json 如下:

{
    "application/vnd.security.vulnerability.report; version=1.1": {
        "generated_at": "2024-04-10T06:32:48.449316778Z",
        "scanner": {
            "name": "Trivy",
            "vendor": "Aqua Security",
            "version": "v0.47.0"
        },
        "severity": "High",
        "vulnerabilities": [
            {
                "id": "CVE-2022-43552",
                "package": "curl",
                "version": "7.29.0-59.el7_9.1",
                "fix_version": "7.29.0-59.el7_9.2",
                "severity": "Low",
                "description": "A use after free vulnerability exists in curl \u003c7.87.0. Curl can be asked to *tunnel* virtually all protocols it supports through an HTTP proxy. HTTP proxies can (and often do) deny such tunnel operations. When getting denied to tunnel the specific protocols SMB or TELNET, curl would use a heap-allocated struct after it had been freed, in its transfer shutdown code path.",
                "links": [
                    "https://avd.aquasec.com/nvd/cve-2022-43552"
                ],
                "artifact_digests": [
                    "sha256:f1d3fb84b984705a72ca7f4017bb2884c84db90651819aad87052d722aa14226"
                ],
                "preferred_cvss": {
                    "score_v3": 5.9,
                    "score_v2": null,
                    "vector_v3": "",
                    "vector_v2": ""
                },
                "cwe_ids": [
                    "CWE-416"
                ],
                "vendor_attributes": {
                    "CVSS": {
                        "nvd": {
                            "V3Score": 5.9,
                            "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"
                        },
                        "redhat": {
                            "V3Score": 5.9,
                            "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"
                        }
                    }
                }
            },
        ...
        ]
    }
}

我從 Harbor 的 source code 找到以下內容,job.go 會將 vulnerability data 寫入 db,並且將:

https://github.com/goharbor/harbor/blob/main/src/pkg/scan/job.go#L301C30-L301C38

...
reportData, err := handler.PostScan(ctx, req, rp, rawReports[i], startTime, robotAccount)
if err != nil {
    myLogger.Errorf("Failed to convert vulnerability data to new schema for report %s, error %v", rp.UUID, err)
    return err
}
...

而 API /additions/vulnerabilities 則會讀取 db 的內容,並且直接回傳,其中就包含 Severity:

https://github.com/goharbor/harbor/blob/main/src/pkg/securityhub/dao/security.go#L310

...
func (d *dao) ListVulnerabilities(ctx context.Context, registrationUUID string, _ int64, query *q.Query) ([]*model.VulnerabilityItem, error) {
	o, err := orm.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	sqlStr := vulnerabilitySQL
	params := []interface{}{registrationUUID}
	if err := checkQFilter(query, filterMap); err != nil {
		return nil, err
	}
	sqlStr, params = applyVulFilter(ctx, sqlStr, query, params)
	sqlStr, params = applyVulPagination(sqlStr, query, params)
	vulnRecs := make([]*model.VulnerabilityItem, 0)
	_, err = o.Raw(sqlStr, params).QueryRows(&vulnRecs)
	return vulnRecs, err
}
...

查看 vulnerability data 的來源,也就是 Trivy 的 source code,在FillInfo() 的內容上,一開始會透過 getVendorSeverity() 取得 severity,接下來判斷 datasource 是否有提供自己的 severity,如果有,則覆蓋 severity 並直接回傳:

https://github.com/aquasecurity/trivy/blob/main/pkg/vulnerability/vulnerability.go#L86

...
// Select the severity according to the detected source.
severity, severitySource := c.getVendorSeverity(vulnID, &vuln, source)

// The vendor might provide package-specific severity like Debian.
// For example, CVE-2015-2328 in Debian has "unimportant" for mongodb and "low" for pcre3.
// In that case, we keep the severity as is.
if vulns[i].SeveritySource != "" {
    severity = vulns[i].Severity
    severitySource = vulns[i].SeveritySource

    // Store package-specific severity in vendor severities
    if vuln.VendorSeverity == nil {
        vuln.VendorSeverity = make(dbTypes.VendorSeverity)
    }
    s, _ := dbTypes.NewSeverity(severity) // skip error handling because `SeverityUnknown` will be returned in case of error
    vuln.VendorSeverity[severitySource] = s
}

// Add the vulnerability detail
vulns[i].Vulnerability = vuln

vulns[i].Severity = severity
vulns[i].SeveritySource = severitySource
vulns[i].PrimaryURL = c.getPrimaryURL(vulnID, vuln.References, source)
...

從 source code 上也可以得知,Severity 與 CVSS 的 Score 沒有關係。

結論

以 CVE: CVE-2022-43552 為例,在 NVD 上的 Severity 是 5.9 MEDIUM,在 RedHat 上的 Severity 是 Low,根據 Trivy 的規則,會顯示 Low:

result

Trivy 的官方文件上有寫描述 Severity 的邏輯,會優先選擇 vendor 所提供的資訊,如果沒有提供,則根據 CVSS Score 計算:

https://aquasecurity.github.io/trivy/v0.50/docs/scanner/vulnerability/

score