Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion grype/db/internal/provider/unmarshal/osv_vulnerability.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,16 @@ import (
"github.com/google/osv-scanner/pkg/models"
)

type OSVVulnerability = models.Vulnerability
// OSVVulnerability extends the standard OSV Vulnerability model with additional
// fields used by Chainguard/Wolfi advisories that aren't yet in the osv-scanner library.
type OSVVulnerability struct {
models.Vulnerability

// Upstream contains IDs of upstream vulnerabilities that this advisory addresses.
// Per OSV spec, this is semantically correct for distro advisories that reference
// upstream CVEs. Included alongside "related" for backwards compatibility.
Upstream []string `json:"upstream,omitempty"`
}

func OSVVulnerabilityEntries(reader io.Reader) ([]OSVVulnerability, error) {
return unmarshalSingleOrMulti[OSVVulnerability](reader)
Expand Down
4 changes: 4 additions & 0 deletions grype/db/v6/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ type PackageQualifiers struct {

// PlatformCPEs lists Common Platform Enumeration (CPE) identifiers for affected platforms.
PlatformCPEs []string `json:"platform_cpes,omitempty"`

// Architecture specifies the CPU architecture this vulnerability applies to (e.g., "aarch64", "x86_64").
// If nil, the vulnerability applies to all architectures.
Architecture *string `json:"architecture,omitempty"`
}

// Range defines a specific range of package versions pertaining to a vulnerability.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"modified": "2026-01-07T00:00:00Z",
"published": "2026-01-07T00:00:00Z",
"id": "CGA-224q-ccj5-2p53",
"related": [
"CVE-2025-32464",
"GHSA-frg5-h47x-75j9"
],
"affected": [
{
"package": {
"ecosystem": "Chainguard",
"name": "haproxy-2.8",
"purl": "pkg:apk/chainguard/haproxy-2.8"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "0"
},
{
"fixed": "2.8.18-r0"
}
]
}
]
},
{
"package": {
"ecosystem": "Wolfi",
"name": "haproxy-3.1",
"purl": "pkg:apk/wolfi/haproxy-3.1"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.7-r0"
}
]
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"schema_version": "1.6.0",
"id": "CGA-test-arch-upstream",
"published": "2026-03-01T10:00:00Z",
"modified": "2026-03-05T10:00:00Z",
"summary": "Test vulnerability with arch in PURL and upstream field",
"severity": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
}
],
"upstream": [
"CVE-2024-12345",
"GHSA-xxxx-yyyy-zzzz"
],
"related": [
"CVE-2024-12345",
"GHSA-xxxx-yyyy-zzzz"
],
"references": [
{"type": "ADVISORY", "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-12345"}
],
"affected": [
{
"package": {
"ecosystem": "Chainguard",
"name": "openssl",
"purl": "pkg:apk/chainguard/openssl?arch=aarch64"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{ "introduced": "0" },
{ "fixed": "3.2.1-r5" }
]
}
]
},
{
"package": {
"ecosystem": "Chainguard",
"name": "openssl",
"purl": "pkg:apk/chainguard/openssl?arch=x86_64"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{ "introduced": "0" },
{ "fixed": "3.2.1-r5" }
]
}
]
},
{
"package": {
"ecosystem": "Chainguard",
"name": "openssl-dev",
"purl": "pkg:apk/chainguard/openssl-dev"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{ "introduced": "0" },
{ "fixed": "3.2.1-r5" }
]
}
]
}
]
}
99 changes: 71 additions & 28 deletions grype/db/v6/build/transformers/osv/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/anchore/grype/grype/db/v6/build/transformers"
"github.com/anchore/grype/grype/db/v6/build/transformers/internal"
"github.com/anchore/grype/grype/db/v6/name"
"github.com/anchore/grype/internal/stringutil"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/pkg"
)

Expand All @@ -36,8 +38,17 @@ func Transform(vulnerability unmarshal.OSVVulnerability, state provider.State) (

if isAdvisory {
aliases = append(aliases, vulnerability.Related...)
} else if strings.HasPrefix(vulnerability.ID, "CGA-") {
// Chainguard CGA records put CVE/GHSA IDs in "upstream" and "related"
// rather than "aliases". Per OSV spec, "upstream" is semantically correct
// for distro advisories; "related" is kept for backwards compatibility.
aliases = append(aliases, vulnerability.Upstream...)
aliases = append(aliases, vulnerability.Related...)
}

// Deduplicate aliases (related and upstream may contain the same IDs)
aliases = stringutil.NewStringSetFromSlice(aliases).ToSlice()

in := []any{
db.VulnerabilityHandle{
Name: vulnerability.ID,
Expand Down Expand Up @@ -115,7 +126,7 @@ func getAffectedPackages(vuln unmarshal.OSVVulnerability) []db.AffectedPackageHa
}

// getPackageQualifiers extracts package qualifiers from affected package data
// including CPE information and RPM modularity
// including CPE information, RPM modularity, and architecture from PURL
func getPackageQualifiers(affected models.Affected, cpes any, withCPE bool) *db.PackageQualifiers {
var qualifiers *db.PackageQualifiers

Expand All @@ -135,9 +146,40 @@ func getPackageQualifiers(affected models.Affected, cpes any, withCPE bool) *db.
qualifiers.RpmModularity = &rpmModularity
}

// Extract architecture from PURL qualifier (e.g., "pkg:apk/chainguard/foo?arch=aarch64")
arch := extractArchFromPURL(affected.Package.Purl)
if arch != "" {
if qualifiers == nil {
qualifiers = &db.PackageQualifiers{}
}
qualifiers.Architecture = &arch
}

return qualifiers
}

// extractArchFromPURL extracts the "arch" qualifier from a PURL string.
// Returns empty string if no arch qualifier is present or if PURL is invalid.
// Example: "pkg:apk/chainguard/openssl?arch=aarch64" returns "aarch64"
func extractArchFromPURL(purlStr string) string {
if purlStr == "" {
return ""
}

purl, err := packageurl.FromString(purlStr)
if err != nil {
return ""
}

for _, qualifier := range purl.Qualifiers {
if qualifier.Key == pkg.PURLQualifierArch {
return qualifier.Value
}
}

return ""
}

// extractRpmModularity extracts RPM modularity information from affected package ecosystem_specific
func extractRpmModularity(affected models.Affected) string {
if affected.EcosystemSpecific == nil {
Expand Down Expand Up @@ -326,6 +368,12 @@ func normalizeRangeType(t models.RangeType, ecosystem string) string {
return "bitnami"
}

// APK distros (Chainguard, Wolfi, Alpine) use ECOSYSTEM ranges but require APK
// version semantics for correct constraint evaluation (e.g. epoch-release suffixes).
if t == models.RangeEcosystem && getPackageTypeFromEcosystem(ecosystem) == pkg.ApkPkg {
return "apk"
}

switch t {
case models.RangeSemVer, models.RangeEcosystem, models.RangeGit:
return strings.ToLower(string(t))
Expand Down Expand Up @@ -358,19 +406,19 @@ func getPackage(p models.Package) *db.Package {
}
}

// getPackageTypeFromEcosystem determines package type from OSV ecosystem
// Currently only supports AlmaLinux; other ecosystems use PURL-based detection
// getPackageTypeFromEcosystem determines package type from OSV ecosystem.
// This is used when no PURL is present; when a PURL is present it takes priority.
func getPackageTypeFromEcosystem(ecosystem string) pkg.Type {
if ecosystem == "" {
return ""
}

// Split ecosystem by colon to get OS name
parts := strings.Split(ecosystem, ":")
osName := strings.ToLower(parts[0])
osName := strings.ToLower(strings.Split(ecosystem, ":")[0])

// Only handle AlmaLinux
if osName == almaLinux {
switch osName {
case "chainguard", "wolfi", "alpine":
return pkg.ApkPkg
case almaLinux:
return pkg.RpmPkg
}

Expand Down Expand Up @@ -459,24 +507,28 @@ func getSeverities(vuln unmarshal.OSVVulnerability) ([]db.Severity, error) {
return severities, nil
}

// getOperatingSystemFromEcosystem extracts operating system information from OSV ecosystem field
// Currently only supports AlmaLinux ecosystems
// Example: "AlmaLinux:8" -> almalinux 8
// getOperatingSystemFromEcosystem extracts operating system information from OSV ecosystem field.
// Examples:
// - "Chainguard" -> chainguard rolling
// - "Wolfi" -> wolfi rolling
// - "AlmaLinux:8" -> almalinux 8
// - "Alpine:3.21" -> alpine 3.21
func getOperatingSystemFromEcosystem(ecosystem string) *db.OperatingSystem {
if ecosystem == "" {
return nil
}

// Split ecosystem by colon to get components
parts := strings.Split(ecosystem, ":")
if len(parts) < 2 {
return nil
}

osName := strings.ToLower(parts[0])

// Only handle AlmaLinux
if osName != almaLinux {
// Rolling APK distros — no version suffix in the ecosystem field
switch osName {
case "chainguard", "wolfi":
return &db.OperatingSystem{Name: osName, LabelVersion: "rolling"}
}

// Versioned ecosystems require a colon-separated version component
if len(parts) < 2 {
return nil
}

Expand Down Expand Up @@ -509,17 +561,8 @@ func getOperatingSystemFromEcosystem(ecosystem string) *db.OperatingSystem {
}
}

// normalizeOSName normalizes operating system names for consistency
// Currently only supports AlmaLinux
func normalizeOSName(osName string) string {
osName = strings.ToLower(osName)

// Only handle AlmaLinux
if osName == almaLinux {
return almaLinux
}

return osName
return strings.ToLower(osName)
}

// isAdvisoryRecord checks if the OSV record is marked as an advisory
Expand Down
Loading