Skip to content
Open
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
58 changes: 57 additions & 1 deletion vulnfeeds/cmd/converters/alpine/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"time"

"github.com/google/osv/vulnfeeds/conversion"
"github.com/google/osv/vulnfeeds/conversion/writer"
"github.com/google/osv/vulnfeeds/models"
"github.com/google/osv/vulnfeeds/utility/logger"
Expand Down Expand Up @@ -195,10 +196,11 @@ func generateAlpineOSV(allAlpineSecDb map[string][]VersionAndPkg, allCVEs map[mo
}

for _, verPkg := range verPkgs {
introduced := findIntroducedVersion(cve.CVE, verPkg.Pkg)
pkgInfo := vulns.PackageInfo{
PkgName: verPkg.Pkg,
VersionInfo: models.VersionInfo{
AffectedVersions: []models.AffectedVersion{{Fixed: verPkg.Ver}},
AffectedVersions: []models.AffectedVersion{{Introduced: introduced, Fixed: verPkg.Ver}},
},
Ecosystem: "Alpine:" + verPkg.AlpineVer,
PURL: "pkg:apk/alpine/" + verPkg.Pkg + "?arch=source",
Expand All @@ -220,6 +222,60 @@ func generateAlpineOSV(allAlpineSecDb map[string][]VersionAndPkg, allCVEs map[mo
return osvVulnerabilities
}

// cpeMatchesAlpinePackage checks if a parsed CPE matches an Alpine package name
// using the same naming convention rules as the Alpine secfixes-tracker:
// language-specific prefixes (py3-, ruby-, perl-, lua-, vscode-) derived from
// the CPE target_sw field, plus direct name match for native packages.
func cpeMatchesAlpinePackage(parsed *models.CPEString, alpinePkg string) bool {
product := strings.ToLower(parsed.Product)
if product == alpinePkg {
return true
}
product = strings.ReplaceAll(product, "_", "-")
targetSW := strings.ToLower(parsed.TargetSW)
var rewritten string
switch {
case strings.Contains(targetSW, "python"):
rewritten = "py3-" + product
case strings.Contains(targetSW, "ruby"):
rewritten = "ruby-" + product
case strings.Contains(targetSW, "perl"):
rewritten = "perl-" + strings.ReplaceAll(product, "::", "-")
case strings.Contains(targetSW, "lua"):
rewritten = "lua-" + product
case strings.Contains(targetSW, "visual_studio_code"):
rewritten = "vscode-" + product
default:
return false
}

return rewritten == alpinePkg
}

// findIntroducedVersion searches NVD CPE configurations for the introduced
// version corresponding to the given Alpine package. Returns "0" if no
// VersionStartIncluding match is found.
func findIntroducedVersion(cve models.NVDCVE, alpinePkg string) string {
for _, config := range cve.Configurations {
for _, node := range config.Nodes {
for _, match := range node.CPEMatch {
if !match.Vulnerable || match.VersionStartIncluding == nil {
continue
}
parsed, err := conversion.ParseCPE(match.Criteria)
if err != nil {
continue
}
if cpeMatchesAlpinePackage(parsed, alpinePkg) {
return *match.VersionStartIncluding
}
}
}
}

return "0"
}

// downloadAlpine downloads Alpine SecDB data from their API
func downloadAlpine(version string) AlpineSecDB {
res, err := http.Get(fmt.Sprintf(alpineURLBase, version))
Expand Down
236 changes: 236 additions & 0 deletions vulnfeeds/cmd/converters/alpine/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package main

import (
"testing"

"github.com/google/osv/vulnfeeds/models"
)

func TestCpeMatchesAlpinePackage(t *testing.T) {
tests := []struct {
name string
product string
targetSW string
alpinePkg string
want bool
}{
{"direct match", "xz", "", "xz", true},
{"direct match case-insensitive", "OpenSSL", "", "openssl", true},
// Direct match on the full prefixed name takes priority over prefix rewriting.
{"direct match beats prefix logic", "py3-foo", "python", "py3-foo", true},
{"direct mismatch no targetSW", "openssl", "", "xz", false},

// Python
{"python prefix exact", "pillow", "python", "py3-pillow", true},
{"python prefix uppercase product", "Pillow", "python", "py3-pillow", true},
{"python prefix underscore product", "python_pillow", "python", "py3-python-pillow", true},
{"python prefix different package name", "pillow", "python", "py3-imaging", false},
{"python prefix case-insensitive targetSW", "certifi", "Python", "py3-certifi", true},
{"python prefix cpython in targetSW", "certifi", "cpython", "py3-certifi", true},

// Ruby
{"ruby prefix", "bigdecimal", "ruby", "ruby-bigdecimal", true},
{"ruby prefix underscore", "some_gem", "ruby", "ruby-some-gem", true},

// Perl
{"perl prefix simple", "json", "perl", "perl-json", true},
{"perl prefix double-colon namespace", "CGI::Session", "perl", "perl-cgi-session", true},

// Lua
{"lua prefix", "luaossl", "lua", "lua-luaossl", true},

// VSCode
{"vscode prefix", "python", "visual_studio_code", "vscode-python", true},

// Unknown / unhandled targetSW
{"java targetSW product mismatch", "log4j", "java", "log4j-core", false},
{"unknown targetSW product mismatch", "foo", "java", "bar", false},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
parsed := &models.CPEString{Product: tc.product, TargetSW: tc.targetSW}
got := cpeMatchesAlpinePackage(parsed, tc.alpinePkg)
if got != tc.want {
t.Errorf("cpeMatchesAlpinePackage({Product:%q, TargetSW:%q}, %q) = %v, want %v",
tc.product, tc.targetSW, tc.alpinePkg, got, tc.want)
}
})
}
}

func TestFindIntroducedVersion(t *testing.T) {
// A valid CPE 2.3 string whose product is "xz" with no target_sw.
const xzCPE = "cpe:2.3:a:tukaani:xz:*:*:*:*:*:*:*:*"
// A valid CPE 2.3 string whose product is "foo".
const fooCPE = "cpe:2.3:a:example:foo:*:*:*:*:*:*:*:*"
// A valid CPE 2.3 string for a python package.
const pillowCPE = "cpe:2.3:a:python-pillow:pillow:*:*:*:*:*:python:*:*"

tests := []struct {
name string
cve models.NVDCVE
alpinePkg string
want string
}{
{
name: "no configurations",
cve: models.NVDCVE{},
alpinePkg: "xz",
want: "0",
},
{
name: "match with VersionStartIncluding",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{{
CPEMatch: []models.CPEMatch{{
Criteria: xzCPE,
Vulnerable: true,
VersionStartIncluding: new("5.6.0"),
}},
}},
}},
},
alpinePkg: "xz",
want: "5.6.0",
},
{
name: "non-vulnerable CPE skipped",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{{
CPEMatch: []models.CPEMatch{{
Criteria: xzCPE,
Vulnerable: false,
VersionStartIncluding: new("5.6.0"),
}},
}},
}},
},
alpinePkg: "xz",
want: "0",
},
{
name: "VersionStartIncluding nil returns 0",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{{
CPEMatch: []models.CPEMatch{{
Criteria: xzCPE,
Vulnerable: true,
}},
}},
}},
},
alpinePkg: "xz",
want: "0",
},
{
// Known gap: VersionStartExcluding is not handled; falls back to "0".
name: "VersionStartExcluding only falls back to 0",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{{
CPEMatch: []models.CPEMatch{{
Criteria: xzCPE,
Vulnerable: true,
VersionStartExcluding: new("5.5.0"),
}},
}},
}},
},
alpinePkg: "xz",
want: "0",
},
{
name: "invalid CPE criteria skipped",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{{
CPEMatch: []models.CPEMatch{{
Criteria: "not-a-cpe",
Vulnerable: true,
VersionStartIncluding: new("1.0.0"),
}},
}},
}},
},
alpinePkg: "xz",
want: "0",
},
{
name: "match found in second node",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{
{
CPEMatch: []models.CPEMatch{{
Criteria: xzCPE,
Vulnerable: true,
VersionStartIncluding: new("0.9.0"),
}},
},
{
CPEMatch: []models.CPEMatch{{
Criteria: fooCPE,
Vulnerable: true,
VersionStartIncluding: new("1.0.0"),
}},
},
},
}},
},
alpinePkg: "foo",
want: "1.0.0",
},
{
name: "python package prefix match",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{{
CPEMatch: []models.CPEMatch{{
Criteria: pillowCPE,
Vulnerable: true,
VersionStartIncluding: new("9.0.0"),
}},
}},
}},
},
alpinePkg: "py3-pillow",
want: "9.0.0",
},
{
name: "first match wins when multiple CPEs match",
cve: models.NVDCVE{
Configurations: []models.Config{{
Nodes: []models.Node{{
CPEMatch: []models.CPEMatch{
{
Criteria: xzCPE,
Vulnerable: true,
VersionStartIncluding: new("5.0.0"),
},
{
Criteria: xzCPE,
Vulnerable: true,
VersionStartIncluding: new("5.6.0"),
},
},
}},
}},
},
alpinePkg: "xz",
want: "5.0.0",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := findIntroducedVersion(tc.cve, tc.alpinePkg)
if got != tc.want {
t.Errorf("findIntroducedVersion(..., %q) = %q, want %q", tc.alpinePkg, got, tc.want)
}
})
}
}
Loading