mirror of
https://github.com/anchore/syft.git
synced 2026-11-01 23:16:30 +01:00
add PE binary cataloger (#3911)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -15,7 +15,7 @@ func newELFPackage(ctx context.Context, metadata elfBinaryPackageNotes, location
|
||||
Name: metadata.Name,
|
||||
Version: metadata.Version,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, metadata.License)),
|
||||
PURL: packageURL(metadata),
|
||||
PURL: elfPackageURL(metadata),
|
||||
Type: pkgType(metadata.Type),
|
||||
Locations: locations,
|
||||
Metadata: metadata.ELFBinaryPackageNoteJSONPayload,
|
||||
@@ -26,7 +26,7 @@ func newELFPackage(ctx context.Context, metadata elfBinaryPackageNotes, location
|
||||
return p
|
||||
}
|
||||
|
||||
func packageURL(metadata elfBinaryPackageNotes) string {
|
||||
func elfPackageURL(metadata elfBinaryPackageNotes) string {
|
||||
var qualifiers []packageurl.Qualifier
|
||||
|
||||
os, osVersion := osNameAndVersionFromMetadata(metadata)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_ELF_Package_Cataloger(t *testing.T) {
|
||||
func Test_ELFPackageCataloger(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
@@ -130,7 +130,7 @@ func Test_packageURL(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.want, packageURL(test.metadata))
|
||||
assert.Equal(t, test.want, elfPackageURL(test.metadata))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
var (
|
||||
// spaceRegex includes nbsp (#160) considered to be a space character
|
||||
spaceRegex = regexp.MustCompile(`[\s\xa0]+`)
|
||||
numberRegex = regexp.MustCompile(`\d`)
|
||||
)
|
||||
|
||||
func newPEPackage(versionResources map[string]string, f file.Location) pkg.Package {
|
||||
name := findNameFromVR(versionResources)
|
||||
|
||||
if name == "" {
|
||||
// it's possible that the version resources are empty, so we fall back to the file name
|
||||
name = strings.TrimSuffix(strings.TrimSuffix(path.Base(f.RealPath), ".exe"), ".dll")
|
||||
}
|
||||
|
||||
p := pkg.Package{
|
||||
Name: name,
|
||||
Version: findVersionFromVR(versionResources),
|
||||
Locations: file.NewLocationSet(f.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Type: pkg.BinaryPkg,
|
||||
Metadata: newPEBinaryVersionResourcesFromMap(versionResources),
|
||||
}
|
||||
|
||||
p.SetID()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func newPEBinaryVersionResourcesFromMap(vr map[string]string) pkg.PEBinary {
|
||||
var kvs pkg.KeyValues
|
||||
for k, v := range vr {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
kvs = append(kvs, pkg.KeyValue{
|
||||
Key: k,
|
||||
Value: spaceNormalize(v),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(kvs, func(i, j int) bool {
|
||||
return kvs[i].Key < kvs[j].Key
|
||||
})
|
||||
|
||||
return pkg.PEBinary{
|
||||
VersionResources: kvs,
|
||||
}
|
||||
}
|
||||
|
||||
func findNameFromVR(versionResources map[string]string) string {
|
||||
// PE files not authored by Microsoft tend to use ProductName as an identifier.
|
||||
nameFields := []string{"ProductName", "FileDescription", "InternalName", "OriginalFilename"}
|
||||
|
||||
if isMicrosoftVR(versionResources) {
|
||||
// for Microsoft files, prioritize FileDescription.
|
||||
nameFields = []string{"FileDescription", "InternalName", "OriginalFilename", "ProductName"}
|
||||
}
|
||||
|
||||
var name string
|
||||
for _, field := range nameFields {
|
||||
value := spaceNormalize(versionResources[field])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
name = value
|
||||
break
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
func isMicrosoftVR(versionResources map[string]string) bool {
|
||||
return strings.Contains(strings.ToLower(versionResources["CompanyName"]), "microsoft") ||
|
||||
strings.Contains(strings.ToLower(versionResources["ProductName"]), "microsoft")
|
||||
}
|
||||
|
||||
// spaceNormalize trims and normalizes whitespace in a string.
|
||||
func spaceNormalize(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
// ensure valid UTF-8.
|
||||
value = strings.ToValidUTF8(value, "")
|
||||
// consolidate all whitespace.
|
||||
value = spaceRegex.ReplaceAllString(value, " ")
|
||||
// remove non-printable characters.
|
||||
value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "")
|
||||
// consolidate again and trim.
|
||||
value = spaceRegex.ReplaceAllString(value, " ")
|
||||
value = strings.TrimSpace(value)
|
||||
return value
|
||||
}
|
||||
|
||||
func findVersionFromVR(versionResources map[string]string) string {
|
||||
productVersion := extractVersionFromResourcesValue(versionResources["ProductVersion"])
|
||||
fileVersion := extractVersionFromResourcesValue(versionResources["FileVersion"])
|
||||
|
||||
if productVersion != "" {
|
||||
return productVersion
|
||||
}
|
||||
|
||||
return fileVersion
|
||||
}
|
||||
|
||||
func extractVersionFromResourcesValue(version string) string {
|
||||
version = strings.TrimSpace(version)
|
||||
out := ""
|
||||
for i, f := range strings.Fields(version) {
|
||||
if containsNumber(out) && !containsNumber(f) {
|
||||
return out
|
||||
}
|
||||
if i == 0 {
|
||||
out = f
|
||||
} else {
|
||||
out += " " + f
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func containsNumber(s string) bool {
|
||||
return numberRegex.MatchString(s)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pe"
|
||||
)
|
||||
|
||||
// NewPEPackageCataloger returns a cataloger that interprets packages from DLL and EXE files.
|
||||
func NewPEPackageCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("pe-binary-package-cataloger").
|
||||
WithParserByGlobs(parsePE, "**/*.dll", "**/*.exe")
|
||||
}
|
||||
|
||||
func parsePE(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
f, err := pe.Read(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse PE file %q: %w", reader.RealPath, err)
|
||||
}
|
||||
|
||||
if f == nil {
|
||||
return nil, nil, unknown.Newf(reader, "unable to determine packages")
|
||||
}
|
||||
|
||||
if f.CLR.HasEvidenceOfCLR() {
|
||||
// this is for a .NET application, which is covered by other catalogers already
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
p := newPEPackage(f.VersionResources, f.Location)
|
||||
|
||||
return []pkg.Package{p}, nil, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_PEPackageCataloger(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
fixture string
|
||||
expected []pkg.Package
|
||||
wantErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "non-.NET package",
|
||||
fixture: "image-jruby",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "JRuby",
|
||||
Version: "9.3.15.0",
|
||||
Type: pkg.BinaryPkg,
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/jruby_windows_9_3_15_0.exe"),
|
||||
),
|
||||
FoundBy: "pe-binary-package-cataloger",
|
||||
Metadata: pkg.PEBinary{
|
||||
VersionResources: pkg.KeyValues{
|
||||
{Key: "CompanyName", Value: "JRuby Dev Team"},
|
||||
{Key: "FileDescription", Value: "JRuby"},
|
||||
{Key: "FileVersion", Value: "9.3.15.0"},
|
||||
{Key: "InternalName", Value: "jruby"},
|
||||
{Key: "LegalCopyright", Value: "JRuby Dev Team"},
|
||||
{Key: "OriginalFilename", Value: "jruby_windows-x32_9_3_15_0.exe"},
|
||||
{Key: "ProductName", Value: "JRuby"},
|
||||
{Key: "ProductVersion", Value: "9.3.15.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ignore .NET packages",
|
||||
fixture: "image-dotnet-app",
|
||||
expected: nil, // expect nothing!
|
||||
},
|
||||
}
|
||||
|
||||
for _, v := range cases {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
WithImageResolver(t, v.fixture).
|
||||
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
|
||||
Expects(v.expected, nil).
|
||||
TestCataloger(t, NewPEPackageCataloger())
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM alpine:latest AS builder
|
||||
|
||||
RUN apk add --no-cache wget unzip
|
||||
RUN mkdir -p /app
|
||||
RUN wget -O /app/minver.nupkg https://www.nuget.org/api/v2/package/minver-cli/4.3.0
|
||||
RUN unzip /app/minver.nupkg -d /app/minver
|
||||
|
||||
from busybox:latest
|
||||
|
||||
COPY --from=builder /app/minver /minver
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM --platform=linux/amd64 alpine:latest AS builder
|
||||
|
||||
RUN wget -O jruby_windows_9_3_15_0.exe https://s3.amazonaws.com/jruby.org/downloads/9.3.15.0/jruby_windows_9_3_15_0.exe
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /jruby_windows_9_3_15_0.exe /jruby_windows_9_3_15_0.exe
|
||||
Reference in New Issue
Block a user