Compare commits

...

3 Commits

Author SHA1 Message Date
anchore-actions-token-generator[bot]
2e100f33f3
chore(deps): update tools to latest versions (#4358)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com>
2025-11-12 13:27:47 -05:00
dependabot[bot]
b444f0c2ed
chore(deps): bump golang.org/x/mod from 0.29.0 to 0.30.0 (#4359)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.29.0 to 0.30.0.
- [Commits](https://github.com/golang/mod/compare/v0.29.0...v0.30.0)

---
updated-dependencies:
- dependency-name: golang.org/x/mod
  dependency-version: 0.30.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-12 13:27:33 -05:00
Adam Chovanec
102d362daf
feat: CPEs format decoder (#4207)
Signed-off-by: Adam Chovanec <git@adamchovanec.cz>
2025-11-12 10:45:09 -05:00
9 changed files with 363 additions and 5 deletions

View File

@ -90,7 +90,7 @@ tools:
# used for running all local and CI tasks # used for running all local and CI tasks
- name: task - name: task
version: version:
want: v3.45.4 want: v3.45.5
method: github-release method: github-release
with: with:
repo: go-task/task repo: go-task/task

2
go.mod
View File

@ -90,7 +90,7 @@ require (
go.uber.org/goleak v1.3.0 go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
golang.org/x/mod v0.29.0 golang.org/x/mod v0.30.0
golang.org/x/net v0.46.0 golang.org/x/net v0.46.0
modernc.org/sqlite v1.40.0 modernc.org/sqlite v1.40.0
) )

4
go.sum
View File

@ -1070,8 +1070,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -0,0 +1,95 @@
package cpes
import (
"bufio"
"errors"
"fmt"
"io"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/format/internal"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
const ID sbom.FormatID = "cpes"
const version = "1"
var _ sbom.FormatDecoder = (*decoder)(nil)
type decoder struct{}
func NewFormatDecoder() sbom.FormatDecoder {
return decoder{}
}
func (d decoder) Decode(r io.Reader) (*sbom.SBOM, sbom.FormatID, string, error) {
if r == nil {
return nil, "", "", fmt.Errorf("no reader provided")
}
s, err := toSyftModel(r)
return s, ID, version, err
}
func (d decoder) Identify(r io.Reader) (sbom.FormatID, string) {
if r == nil {
return "", ""
}
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
// skip whitespace only lines
continue
}
err := cpe.ValidateString(line)
if err != nil {
return "", ""
}
return ID, version
}
return "", ""
}
func toSyftModel(r io.Reader) (*sbom.SBOM, error) {
var errs []error
pkgs := pkg.NewCollection()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// skip invalid CPEs
c, err := cpe.New(line, "")
if err != nil {
log.WithFields("error", err, "line", line).Debug("unable to parse cpe")
continue
}
p := pkg.Package{
Name: c.Attributes.Product,
Version: c.Attributes.Version,
CPEs: []cpe.CPE{c},
}
internal.Backfill(&p)
p.SetID()
pkgs.Add(p)
}
return &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkgs,
},
}, errors.Join(errs...)
}

View File

@ -0,0 +1,171 @@
package cpes
import (
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
func Test_CPEProvider(t *testing.T) {
tests := []struct {
name string
userInput string
sbom *sbom.SBOM
}{
{
name: "takes a single cpe",
userInput: "cpe:/a:apache:log4j:2.14.1",
sbom: &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "log4j",
Version: "2.14.1",
CPEs: []cpe.CPE{
cpe.Must("cpe:/a:apache:log4j:2.14.1", ""),
},
}),
},
},
},
{
name: "takes multiple cpes",
userInput: `cpe:/a:apache:log4j:2.14.1
cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*;
cpe:2.3:a:f5:nginx:0.5.2:*:*:*:*:*:*:*;
cpe:2.3:a:f5:nginx:0.5.3:*:*:*:*:*:*:*;`,
sbom: &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(
pkg.Package{
Name: "log4j",
Version: "2.14.1",
CPEs: []cpe.CPE{
cpe.Must("cpe:/a:apache:log4j:2.14.1", ""),
},
},
pkg.Package{
Name: "nginx",
Version: "",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:f5:nginx:*:*:*:*:*:*:*:*;", ""),
},
},
pkg.Package{
Name: "nginx",
Version: "0.5.2",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:f5:nginx:0.5.2:*:*:*:*:*:*:*;", ""),
},
},
pkg.Package{
Name: "nginx",
Version: "0.5.3",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:f5:nginx:0.5.3:*:*:*:*:*:*:*;", ""),
},
},
),
},
},
},
{
name: "takes cpe with no version",
userInput: "cpe:/a:apache:log4j",
sbom: &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "log4j",
CPEs: []cpe.CPE{
cpe.Must("cpe:/a:apache:log4j", ""),
},
}),
},
},
},
{
name: "takes CPE 2.3 format",
userInput: "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*",
sbom: &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "log4j",
Version: "2.14.1",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*", ""),
},
}),
},
},
},
{
name: "deduces target SW from CPE - known target_sw",
userInput: "cpe:2.3:a:amazon:opensearch:*:*:*:*:*:ruby:*:*",
sbom: &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "opensearch",
Type: pkg.GemPkg,
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:amazon:opensearch:*:*:*:*:*:ruby:*:*", ""),
},
}),
},
},
},
{
name: "handles unknown target_sw CPE field",
userInput: "cpe:2.3:a:amazon:opensearch:*:*:*:*:*:loremipsum:*:*",
sbom: &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(pkg.Package{
Name: "opensearch",
Type: "",
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:amazon:opensearch:*:*:*:*:*:loremipsum:*:*", ""),
},
}),
},
},
},
{
name: "invalid prefix",
userInput: "dir:test-fixtures/cpe",
sbom: &sbom.SBOM{
Artifacts: sbom.Artifacts{
Packages: pkg.NewCollection(),
},
},
},
}
syftPkgOpts := []cmp.Option{
cmpopts.IgnoreFields(pkg.Package{}, "id", "Language"),
cmpopts.IgnoreUnexported(pkg.Package{}, file.LocationSet{}, pkg.LicenseSet{}),
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
dec := NewFormatDecoder()
decodedSBOM, _, _, err := dec.Decode(strings.NewReader(tc.userInput))
require.NoError(t, err)
gotSyftPkgs := decodedSBOM.Artifacts.Packages.Sorted()
wantSyftPkgs := tc.sbom.Artifacts.Packages.Sorted()
require.Equal(t, len(gotSyftPkgs), len(wantSyftPkgs))
for idx, wantPkg := range wantSyftPkgs {
if d := cmp.Diff(wantPkg, gotSyftPkgs[idx], syftPkgOpts...); d != "" {
t.Errorf("unexpected Syft Package (-want +got):\n%s", d)
}
}
})
}
}

View File

@ -3,6 +3,7 @@ package format
import ( import (
"io" "io"
"github.com/anchore/syft/syft/format/cpes"
"github.com/anchore/syft/syft/format/cyclonedxjson" "github.com/anchore/syft/syft/format/cyclonedxjson"
"github.com/anchore/syft/syft/format/cyclonedxxml" "github.com/anchore/syft/syft/format/cyclonedxxml"
"github.com/anchore/syft/syft/format/purls" "github.com/anchore/syft/syft/format/purls"
@ -26,6 +27,7 @@ func Decoders() []sbom.FormatDecoder {
spdxtagvalue.NewFormatDecoder(), spdxtagvalue.NewFormatDecoder(),
spdxjson.NewFormatDecoder(), spdxjson.NewFormatDecoder(),
purls.NewFormatDecoder(), purls.NewFormatDecoder(),
cpes.NewFormatDecoder(),
} }
} }

View File

@ -10,13 +10,31 @@ import (
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
cataloger "github.com/anchore/syft/syft/pkg/cataloger/common/cpe"
) )
// Backfill takes all information present in the package and attempts to fill in any missing information // Backfill takes all information present in the package and attempts to fill in any missing information
// from any available sources, such as the Metadata and PURL. // from any available sources, such as the Metadata, PURL, or CPEs.
// //
// Backfill does not call p.SetID(), but this needs to be called later to ensure it's up to date // Backfill does not call p.SetID(), but this needs to be called later to ensure it's up to date
func Backfill(p *pkg.Package) { func Backfill(p *pkg.Package) {
backfillFromPurl(p)
backfillFromCPE(p)
}
func backfillFromCPE(p *pkg.Package) {
if len(p.CPEs) == 0 {
return
}
c := p.CPEs[0]
if p.Type == "" {
p.Type = cataloger.TargetSoftwareToPackageType(c.Attributes.TargetSW)
}
}
func backfillFromPurl(p *pkg.Package) {
if p.PURL == "" { if p.PURL == "" {
return return
} }

View File

@ -121,6 +121,20 @@ func Test_Backfill(t *testing.T) {
Metadata: pkg.JavaArchive{}, Metadata: pkg.JavaArchive{},
}, },
}, },
{
name: "target-sw from CPE",
in: pkg.Package{
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:amazon:opensearch:*:*:*:*:*:ruby:*:*", ""),
},
},
expected: pkg.Package{
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:amazon:opensearch:*:*:*:*:*:ruby:*:*", ""),
},
Type: pkg.GemPkg,
},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@ -0,0 +1,58 @@
package cpe
import (
"strings"
"github.com/anchore/syft/syft/pkg"
)
// TargetSoftwareToPackageType is derived from looking at target_software attributes in the NVD dataset
// TODO: ideally this would be driven from the store, where we can resolve ecosystem aliases directly
func TargetSoftwareToPackageType(tsw string) pkg.Type {
tsw = strings.NewReplacer("-", "_", " ", "_").Replace(strings.ToLower(tsw))
switch tsw {
case "alpine", "apk":
return pkg.ApkPkg
case "debian", "dpkg":
return pkg.DebPkg
case "java", "maven", "ant", "gradle", "jenkins", "jenkins_ci", "kafka", "logstash", "mule", "nifi", "solr", "spark", "storm", "struts", "tomcat", "zookeeper", "log4j":
return pkg.JavaPkg
case "javascript", "node", "nodejs", "node.js", "npm", "yarn", "apache", "jquery", "next.js", "prismjs":
return pkg.NpmPkg
case "c", "c++", "c/c++", "conan", "gnu_c++", "qt":
return pkg.ConanPkg
case "dart":
return pkg.DartPubPkg
case "redhat", "rpm", "redhat_enterprise_linux", "rhel", "suse", "suse_linux", "opensuse", "opensuse_linux", "fedora", "centos", "oracle_linux", "ol":
return pkg.RpmPkg
case "elixir", "hex":
return pkg.HexPkg
case "erlang":
return pkg.ErlangOTPPkg
case ".net", ".net_framework", "asp", "asp.net", "dotnet", "dotnet_framework", "c#", "csharp", "nuget":
return pkg.DotnetPkg
case "ruby", "gem", "nokogiri", "ruby_on_rails":
return pkg.GemPkg
case "rust", "cargo", "crates":
return pkg.RustPkg
case "python", "pip", "pypi", "flask":
return pkg.PythonPkg
case "kb", "knowledgebase", "msrc", "mskb", "microsoft":
return pkg.KbPkg
case "portage", "gentoo":
return pkg.PortagePkg
case "go", "golang", "gomodule":
return pkg.GoModulePkg
case "linux_kernel", "linux", "z/linux":
return pkg.LinuxKernelPkg
case "php":
return pkg.PhpComposerPkg
case "swift":
return pkg.SwiftPkg
case "wordpress", "wordpress_plugin", "wordpress_":
return pkg.WordpressPluginPkg
case "lua", "luarocks":
return pkg.LuaRocksPkg
}
return ""
}