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
5 changes: 5 additions & 0 deletions grype/db/v6/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ type PackageQualifiers struct {
// RootIO indicates that the vulnerability applies only to Root IO packages (packages with Root IO fixes).
// When true, standard packages will not match this vulnerability (NAK pattern).
RootIO *bool `json:"rootio,omitempty"`

// Echo indicates that the vulnerability applies only to Echo-patched language packages
// (identified by a "+echo.N" version suffix). When true, non-Echo packages will not match
// this vulnerability (NAK pattern).
Echo *bool `json:"echo,omitempty"`
}

// Range defines a specific range of package versions pertaining to a vulnerability.
Expand Down
51 changes: 51 additions & 0 deletions grype/db/v6/build/transformers/osv/testdata/ECHO-maven-0001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"schema_version": "1.6.1",
"id": "ECHO-maven-0001",
"modified": "2024-12-15T14:30:00Z",
"published": "2024-12-10T09:00:00Z",
"aliases": [
"CVE-2024-22259"
],
"summary": "spring-web URL parsing open redirect / SSRF",
"details": "spring-web before 5.3.32+echo.1 is vulnerable to open redirect and SSRF via crafted input.",
"affected": [
{
"package": {
"ecosystem": "Echo:Maven",
"name": "org.springframework:spring-web",
"purl": "pkg:maven/org.springframework/spring-web"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "0"
},
{
"fixed": "5.3.32+echo.1"
}
]
}
]
}
],
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-22259"
}
],
"severity": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N"
}
],
"database_specific": {
"source": "Echo",
"anchore": {
"record_type": "advisory"
}
}
}
51 changes: 51 additions & 0 deletions grype/db/v6/build/transformers/osv/testdata/ECHO-npm-0001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"schema_version": "1.6.1",
"id": "ECHO-npm-0001",
"modified": "2024-12-15T14:30:00Z",
"published": "2024-12-10T09:00:00Z",
"aliases": [
"CVE-2022-29078"
],
"summary": "ejs template injection",
"details": "The ejs library before 3.1.10+echo.1 is vulnerable to server-side template injection.",
"affected": [
{
"package": {
"ecosystem": "Echo:npm",
"name": "ejs",
"purl": "pkg:npm/ejs"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.10+echo.1"
}
]
}
]
}
],
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2022-29078"
}
],
"severity": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H"
}
],
"database_specific": {
"source": "Echo",
"anchore": {
"record_type": "advisory"
}
}
}
51 changes: 51 additions & 0 deletions grype/db/v6/build/transformers/osv/testdata/ECHO-pypi-0001.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"schema_version": "1.6.1",
"id": "ECHO-pypi-0001",
"modified": "2024-12-15T14:30:00Z",
"published": "2024-12-10T09:00:00Z",
"aliases": [
"CVE-2023-32681"
],
"summary": "requests leaks Proxy-Authorization header",
"details": "The requests library before 2.14.2+echo.1 leaks the Proxy-Authorization header on redirect.",
"affected": [
{
"package": {
"ecosystem": "Echo:PyPi",
"name": "requests",
"purl": "pkg:pypi/requests"
},
"ranges": [
{
"type": "ECOSYSTEM",
"events": [
{
"introduced": "0"
},
{
"fixed": "2.14.2+echo.1"
}
]
}
]
}
],
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2023-32681"
}
],
"severity": [
{
"type": "CVSS_V3",
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N"
}
],
"database_specific": {
"source": "Echo",
"anchore": {
"record_type": "advisory"
}
}
}
1 change: 1 addition & 0 deletions grype/db/v6/build/transformers/osv/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type Strategy interface {
var strategies = []Strategy{
almaStrategy{},
bitnamiStrategy{},
echoStrategy{},
govulndbStrategy{},
rootioStrategy{},
}
159 changes: 159 additions & 0 deletions grype/db/v6/build/transformers/osv/transform_echo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package osv

import (
"fmt"
"sort"
"strings"

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

// echoStrategy handles ECHO-* records from Echo's OSV feed. Echo ships patched
// builds of upstream language packages (PyPI/npm/Maven) identified by a
// "+echo.N" version suffix. These records are *advisories* (NAK semantics):
// they describe the version range carrying Echo's fix so the upstream disclosure
// is suppressed on the patched build.
type echoStrategy struct{}

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

func (echoStrategy) 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)
}

// Echo records may carry the upstream CVE in either `aliases` or `related`;
// merge both so the full CVE set rides on the vulnerability blob and on each
// unaffected package handle (lets the language matcher cross-reference the
// Echo NAK to the upstream GHSA/NVD disclosure for the same CVE).
aliases := append([]string{}, vuln.Aliases...)
aliases = append(aliases, vuln.Related...)

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: echoReferences(vuln),
Aliases: aliases,
Severities: severities,
},
},
}

for _, uph := range echoUnaffectedPackages(vuln, aliases) {
in = append(in, uph)
}
return transformers.NewEntries(in...), nil
}

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

func echoUnaffectedPackages(vuln unmarshal.OSVVulnerability, aliases []string) []db.UnaffectedPackageHandle {
if len(vuln.Affected) == 0 {
return nil
}
echoOnly := true
var uphs []db.UnaffectedPackageHandle
for _, affected := range vuln.Affected {
ecosystem := affected.Package.Ecosystem
pkgType := echoPackageType(ecosystem)
if pkgType == "" {
// OS-level "Echo" entries (no language suffix) are owned by the
// echo OS provider; any other ecosystem is upstream drift. The
// vunnel echo-osv provider already filters to language ecosystems,
// so this is defensive — skip rather than emit an unusable entry.
log.WithFields("id", vuln.ID, "ecosystem", ecosystem, "package", affected.Package.Name).
Trace("echo record uses a non-language ecosystem; skipping (handled by the echo OS provider, or add a case to echoPackageType)")
continue
}

var ranges []db.Range
for _, r := range affected.Ranges {
ranges = append(ranges, getGrypeUnaffectedRangesFromRange(r, defaultRangeType(r.Type))...)
}

uphs = append(uphs, db.UnaffectedPackageHandle{
Package: echoPackage(affected.Package, pkgType),
BlobValue: &db.PackageBlob{
CVEs: aliases,
Ranges: ranges,
// Gate the NAK to actual Echo builds: the unaffected range is
// open-ended (">= X+echo.1"), which on an upstream-named package
// would otherwise leak onto plain higher versions (e.g. a plain
// "26.1" that is still vulnerable upstream). The echo runtime
// qualifier requires the scanned package to carry the "+echo.N"
// suffix, so non-Echo packages don't match this NAK.
Qualifiers: &db.PackageQualifiers{Echo: &echoOnly},
},
})
}
sort.Sort(internal.ByUnaffectedPackage(uphs))
return uphs
}

// echoPackageType resolves the grype package type from the OSV ecosystem
// string. Every Echo language ecosystem is prefixed "Echo:":
//
// "Echo:PyPi", "Echo:npm", "Echo:Maven"
//
// OS-level "Echo" entries (no language suffix) and any unrecognized ecosystem
// return "" and are skipped by the caller. The suffix match is case-insensitive
// (the feed uses "PyPi"; OSV/osv.dev use "PyPI").
func echoPackageType(ecosystem string) pkg.Type {
rest, ok := strings.CutPrefix(ecosystem, "Echo:")
if !ok || rest == "" {
return ""
}
switch strings.ToLower(rest) {
case "pypi", "python", "pip":
return pkg.PythonPkg
case "npm":
return pkg.NpmPkg
case "maven", "java":
return pkg.JavaPkg
}
return ""
}

// echoPackage builds the db.Package, keeping the upstream package name verbatim
// from the OSV record. Ecosystem is canonicalized to the grype package-type string
// and the name is normalized per package type (e.g. PEP 503 for PythonPkg).
func echoPackage(p osvmodel.Package, pkgType pkg.Type) *db.Package {
return &db.Package{
Ecosystem: pkgType.String(),
Name: name.Normalize(p.Name, pkgType),
}
}
Loading