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
3 changes: 2 additions & 1 deletion grype/db/v6/build/transformers/osv/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ type Strategy interface {
var strategies = []Strategy{
almaStrategy{},
bitnamiStrategy{},
}
cleanstartStrategy{},
}
176 changes: 176 additions & 0 deletions grype/db/v6/build/transformers/osv/transform_cleanstart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package osv

import (
"fmt"
"sort"
"strconv"
"strings"

"github.com/google/osv-scanner/pkg/models"

"github.com/anchore/grype/grype/db/data"
"github.com/anchore/grype/grype/db/internal/codename"
"github.com/anchore/grype/grype/db/internal/provider/unmarshal"
"github.com/anchore/grype/grype/db/provider"
db "github.com/anchore/grype/grype/db/v6"
"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/syft/syft/pkg"
)

const cleanStart = "cleanstart"

// cleanstartStrategy handles CleanStart Security Advisory (CLEANSTART) records.
// CleanStart OSV records describe affected version ranges for APK packages.
//
// CleanStart-specific decisions:
// - Ecosystem is "CleanStart" or "CleanStart:<version>"; package type is
// always APK; OS metadata is extracted from the ecosystem string.
// - CleanStart is a rolling distro — no version suffix means rolling.
// - OSV ranges (introduced/fixed events) are converted directly to
// AffectedPackageHandle records with "< fixVersion" constraints, matching
// how Alpine and Wolfi vulnerability data is stored.
// - ADVISORY-type references get their refID set to the record ID.
type cleanstartStrategy struct{}

func (cleanstartStrategy) Matches(id string) bool {
return strings.HasPrefix(id, "CLEANSTART-")
}

func (cleanstartStrategy) Transform(vuln unmarshal.OSVVulnerability, state provider.State) ([]data.Entry, error) {
severities, err := getSeverities(vuln)
if err != nil {
return nil, fmt.Errorf("unable to obtain severities: %w", err)
}

aliases := append([]string{}, vuln.Aliases...)

in := []any{
db.VulnerabilityHandle{
Name: vuln.ID,
ProviderID: state.Provider,
Provider: provider.Model(state),
Status: db.VulnerabilityActive,
ModifiedDate: &vuln.Modified,
PublishedDate: &vuln.Published,
BlobValue: &db.VulnerabilityBlob{
ID: vuln.ID,
Description: vuln.Details,
References: cleanstartReferences(vuln),
Aliases: aliases,
Severities: severities,
},
},
}

for _, aph := range cleanstartAffectedPackages(vuln) {
in = append(in, aph)
}
return transformers.NewEntries(in...), nil
}

func cleanstartReferences(vuln unmarshal.OSVVulnerability) []db.Reference {
var refs []db.Reference
for _, ref := range vuln.References {
refID := ""
if ref.Type == models.ReferenceAdvisory {
refID = vuln.ID
}
refs = append(refs, db.Reference{
ID: refID,
URL: ref.URL,
Tags: []string{string(ref.Type)},
})
}
return refs
}

func cleanstartAffectedPackages(vuln unmarshal.OSVVulnerability) []db.AffectedPackageHandle {
if len(vuln.Affected) == 0 {
return nil
}
var aphs []db.AffectedPackageHandle
for _, affected := range vuln.Affected {
aphs = append(aphs, db.AffectedPackageHandle{
Package: cleanstartPackage(affected.Package),
OperatingSystem: cleanstartOSFromEcosystem(string(affected.Package.Ecosystem)),
BlobValue: cleanstartAffectedBlob(vuln, affected),
})
}
sort.Sort(internal.ByAffectedPackage(aphs))
return aphs
}

func cleanstartAffectedBlob(vuln unmarshal.OSVVulnerability, affected models.Affected) *db.PackageBlob {
var ranges []db.Range
for _, r := range affected.Ranges {
ranges = append(ranges, getGrypeRangesFromRange(r, cleanstartRangeType(r.Type))...)
}
return &db.PackageBlob{
CVEs: vuln.Aliases,
Ranges: ranges,
}
}

func cleanstartPackage(p models.Package) *db.Package {
return &db.Package{
Ecosystem: pkg.ApkPkg.String(),
Name: name.Normalize(p.Name, pkg.ApkPkg),
}
}

func cleanstartRangeType(t models.RangeType) string {
if t == models.RangeEcosystem {
return pkg.ApkPkg.String()
}
return defaultRangeType(t)
}

// cleanstartOSFromEcosystem extracts OS metadata from a CleanStart ecosystem
// string. CleanStart is a rolling distro; a bare "CleanStart" ecosystem (no
// version suffix) maps to a rolling OS entry.
func cleanstartOSFromEcosystem(ecosystem string) *db.OperatingSystem {
if ecosystem == "" {
return nil
}

parts := strings.SplitN(ecosystem, ":", 2)
osName := strings.ToLower(parts[0])

if osName != cleanStart {
return nil
}

if len(parts) < 2 || parts[1] == "" {
return &db.OperatingSystem{
Name: cleanStart,
}
}

osVersion := parts[1]
versionFields := strings.Split(osVersion, ".")
if len(versionFields) == 0 || versionFields[0] == "" {
return nil
}

major := versionFields[0]
if _, err := strconv.Atoi(major[0:1]); err != nil {
return &db.OperatingSystem{
Name: cleanStart,
LabelVersion: osVersion,
Codename: codename.LookupOS(cleanStart, "", ""),
}
}

var minor string
if len(versionFields) > 1 {
minor = versionFields[1]
}
return &db.OperatingSystem{
Name: cleanStart,
MajorVersion: major,
MinorVersion: minor,
Codename: codename.LookupOS(cleanStart, major, minor),
}
}
2 changes: 2 additions & 0 deletions grype/db/v6/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ func KnownOperatingSystemSpecifierOverrides() []OperatingSystemSpecifierOverride
{Alias: "amazon", ReplacementName: strRef("amzn")}, // non-standard, but common
{Alias: "amazonlinux", ReplacementName: strRef("amzn")}, // non-standard, but common (dockerhub uses "amazonlinux")
{Alias: "echo", Rolling: true},
{Alias: "cleanstart", Rolling: true},
{Alias: "clnstrt", Rolling: true},
// TODO: forky is a placeholder for now, but should be updated to sid when the time comes
// this needs to be automated, but isn't clear how to do so since you'll see things like this:
//
Expand Down
10 changes: 10 additions & 0 deletions grype/distro/distro_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,16 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) {
Type: MinimOS,
Version: "20241031",
},
{
Name: "testdata/os/cleanstart",
Type: CleanStart,
Version: "3.20.3",
},
{
Name: "testdata/os/clnstrt",
Type: CleanStart,
Version: "3.20.3",
},
{
Name: "testdata/os/raspbian",
Type: Raspbian,
Expand Down
6 changes: 6 additions & 0 deletions grype/distro/testdata/os/cleanstart/etc/os-release
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ID=cleanstart
NAME="CleanStart"
PRETTY_NAME="CleanStart v3.20"
VERSION_ID="3.20.3"
HOME_URL="https://www.cleanstart.com/"
BUG_REPORT_URL="https://github.com/cleanstart-containers/cleanstart/issues"
6 changes: 6 additions & 0 deletions grype/distro/testdata/os/clnstrt/etc/os-release
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ID=clnstrt
NAME="Clnstrt Linux"
PRETTY_NAME="Clnstrt Linux v3.20"
VERSION_ID="3.20.3"
HOME_URL="https://clnstrt.dev"
BUG_REPORT_URL="https://clnstrt.dev"
6 changes: 5 additions & 1 deletion grype/distro/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
SecureOS Type = "secureos"
PostmarketOS Type = "postmarketos"
Hummingbird Type = "hummingbird"
CleanStart Type = "cleanstart"
)

// All contains all Linux distribution options
Expand Down Expand Up @@ -70,6 +71,7 @@ var All = []Type{
SecureOS,
PostmarketOS,
Hummingbird,
CleanStart,
}

// IDMapping maps a distro ID from the /etc/os-release (e.g. like "ubuntu") to a Distro type.
Expand Down Expand Up @@ -101,13 +103,15 @@ var IDMapping = map[string]Type{
"secureos": SecureOS,
"postmarketos": PostmarketOS,
"hummingbird": Hummingbird,
"cleanstart": CleanStart,
}

// aliasTypes maps common aliases to their corresponding Type.
var aliasTypes = map[string]Type{
"Alpine Linux": Alpine, // needed for CPE matching (see #2039)
"windows": Windows,
"scientific linux": Scientific, // Scientific linux prior to v7 didn't have an os-release file and syft raises up "scientific linux" as the release id as parsed from /etc/redhat-release
"clnstrt": CleanStart,
}

var typeToIDMapping = map[Type]string{}
Expand Down Expand Up @@ -156,4 +160,4 @@ func TypeFromRelease(release linux.Release) Type {
// String returns the string representation of the given Linux distribution.
func (t Type) String() string {
return string(t)
}
}
14 changes: 14 additions & 0 deletions grype/distro/type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ func TestTypeFromRelease(t *testing.T) {
},
want: Alpine,
},
{
name: "cleanstart ID mapping",
release: linux.Release{
ID: "cleanstart",
},
want: CleanStart,
},
{
name: "clnstrt ID mapping",
release: linux.Release{
ID: "clnstrt",
},
want: CleanStart,
},
{
name: "Scientific Linux 6",
release: linux.Release{
Expand Down