Compare commits

...
18 Commits
Author SHA1 Message Date
Alex Goodman ac883f52ed add cdx group as purl namespace (#3922)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-20 15:56:08 -04:00
Alex Goodman e23ca43a83 add PE binary cataloger (#3911)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-19 14:17:09 -04:00
Christopher Angelo Phillips b4ca04001c chore: update dockerfile base images to latest rolling tags (#3915)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-19 13:43:14 +00:00
anchore-actions-token-generator[bot]andwagoodman 828645ec27 chore(deps): update CPE dictionary index (#3913)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: wagoodman <590471+wagoodman@users.noreply.github.com>
2025-05-19 09:16:25 -04:00
Alex Goodman db77b54c01 finalize go mod ref (#3908)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 17:36:26 +00:00
Alex Goodman 2d4fe513ec remove benchmark workflow (#3906)
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 11:08:43 -04:00
Christopher Angelo Phillips e1374f758e fix: update license content filtering default case to be 'none' for no content returned
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-16 14:25:15 +00:00
dependabot[bot] 945893847f chore(deps): bump github/codeql-action from 3.28.17 to 3.28.18 (#3905)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.17 to 3.28.18.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/60168efe1c415ce0f5521ea06d5c2062adbeed1b...ff0a06e83cb2de871e5a09832bc6a81e7276941f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 14:16:11 +00:00
sathiya06andAlex Goodman 8cbdd38a63 fix: Make Native Image contains no embedded SBOM Error Discoverable (#3805)
* fix: Make Native Image contains no embedded SBOM Error Discoverable

Signed-off-by: Sathiya Narayanan Venkatesan <sathiyavenkat06@gmail.com>

* adjust error phrasing + wrap error

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Sathiya Narayanan Venkatesan <sathiyavenkat06@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 13:54:40 +00:00
Adam McClenaghanandAlex Goodman 8f02bd85f6 fix: Distinguish openjdk vs jdk when using file source (#3895)
* fix: Distinguish openjdk vs jdk when using file source

Signed-off-by: adammcclenaghan <adam@mcclenaghan.co.uk>

* fix: Fix goimport order

Signed-off-by: adammcclenaghan <adam@mcclenaghan.co.uk>

* add comment

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: adammcclenaghan <adam@mcclenaghan.co.uk>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-16 13:29:53 +00:00
Alex Goodman 0480b516f6 chore: fix publishing test fixture images (#3896)
* update pkg counts

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump max cache size

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-15 14:35:11 -04:00
Christopher Angelo Phillips 2a055690e6 chore: delete unused fixture (#3901)
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-15 17:30:36 +00:00
Christopher Angelo Phillips 4f73d35051 Include default config licenses (#3900)
* fix: the licenses config was not being carried through causing content to show by default
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
2025-05-15 16:48:18 +00:00
Ralph BeanandAlex Goodman b369b02f4f Expose RPM signature information (for RPM DB and RPM archives) (#3179)
* feat: expose rpm signature information

This helps with more confident identification of an rpm.

In theory, two rpms can be built that have the same purl string, and
otherwise look identical in syft's output, but the PGP information
would distinguish them as signed either by different keys, or signed at
different times.

In practice, this usually makes no difference since rpms tend to have
unique name/version/release strings. This just gives increased
confidence about the identity of the rpm found in the db.

Signed-off-by: Ralph Bean <rbean@redhat.com>

* chore: generate json schema

Signed-off-by: Ralph Bean <rbean@redhat.com>

* re-generate json schema

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename to a more generic signature field

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* rename rpm.pgp to rpm.signatures

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* split out signature fields

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* bump json schema

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* include RPM archives

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* update json schema

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* dont fail on unknown signature type

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Ralph Bean <rbean@redhat.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-15 16:01:00 +00:00
dependabot[bot] 5effed06a8 chore(deps): bump github.com/mholt/archives from 0.1.1 to 0.1.2 (#3898)
Bumps [github.com/mholt/archives](https://github.com/mholt/archives) from 0.1.1 to 0.1.2.
- [Release notes](https://github.com/mholt/archives/releases)
- [Commits](https://github.com/mholt/archives/compare/v0.1.1...v0.1.2)

---
updated-dependencies:
- dependency-name: github.com/mholt/archives
  dependency-version: 0.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-15 10:23:30 -04:00
dependabot[bot] 5e25d52845 chore(deps): bump anchore/sbom-action from 0.19.0 to 0.20.0 (#3899)
Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/anchore/sbom-action/releases)
- [Changelog](https://github.com/anchore/sbom-action/blob/main/RELEASE.md)
- [Commits](https://github.com/anchore/sbom-action/compare/9f7302141466aa6482940f15371237e9d9f4c34a...e11c554f704a0b820cbf8c51673f6945e0731532)

---
updated-dependencies:
- dependency-name: anchore/sbom-action
  dependency-version: 0.20.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-05-15 10:23:20 -04:00
Laurent GoderreandAlex Goodman a8e5b25632 Add PHP interpreter + extensions cataloger (#2585)
* Add PHP extensions binary classifiers

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>

* [wip] add php extensions cataloger

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* find interpreters + extension

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* internalize binary cataloger utilities

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* default to linux/amd64 for test fixtures

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-15 08:22:50 -04:00
Alex Goodman 0521ccaf5e chore: update fixtures based on CI builds (#3894)
* update fixtures based on ci builds

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix typo

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
2025-05-14 21:30:20 +00:00
84 changed files with 9184 additions and 1248 deletions
-62
View File
@@ -1,62 +0,0 @@
name: "Benchmark testing"
on:
workflow_dispatch:
pull_request:
permissions:
contents: read
jobs:
Benchmark-Test:
name: "Benchmark tests"
runs-on: ubuntu-24.04
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
# the job by event.
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
- name: Restore base benchmark result
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 #v4.2.3
with:
path: test/results/benchmark-main.txt
# use base sha for PR or new commit hash for main push in benchmark result key
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
- name: Run benchmark tests
id: benchmark
run: |
REF_NAME=${GITHUB_REF##*/} make benchmark
OUTPUT=$(make show-benchstat)
OUTPUT="${OUTPUT//'%'/'%25'}" # URL encode all '%' characters
OUTPUT="${OUTPUT//$'\n'/'%0A'}" # URL encode all '\n' characters
OUTPUT="${OUTPUT//$'\r'/'%0D'}" # URL encode all '\r' characters
echo "result=$OUTPUT" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: benchmark-test-results
path: test/results/**/*
- name: Update PR benchmark results comment
uses: marocchino/sticky-pull-request-comment@67d0dec7b07ed060a405f9b2a64b8ab319fdd7db #v2.9.2
continue-on-error: true
with:
header: benchmark
message: |
### Benchmark Test Results
<details>
<summary>Benchmark results from the latest changes vs base branch</summary>
```
${{ steps.benchmark.outputs.result }}
```
</details>
+3 -3
View File
@@ -45,7 +45,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -56,7 +56,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -70,4 +70,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@60168efe1c415ce0f5521ea06d5c2062adbeed1b #v3.28.17
uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18
+1 -1
View File
@@ -148,7 +148,7 @@ jobs:
# for updating brew formula in anchore/homebrew-syft
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
- uses: anchore/sbom-action@9f7302141466aa6482940f15371237e9d9f4c34a #v0.19.0
- uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 #v0.20.0
continue-on-error: true
with:
file: go.mod
+1 -1
View File
@@ -1,4 +1,4 @@
FROM gcr.io/distroless/static-debian12:debug@sha256:5c474275684bbf271ec40502ab50158b2f9826de5877d8feec27e22e8d6ee3d2 AS build
FROM gcr.io/distroless/static-debian12:latest AS build
FROM scratch
# needed for version check HTTPS request
+1 -1
View File
@@ -1,4 +1,4 @@
FROM gcr.io/distroless/static-debian12:debug@sha256:5c474275684bbf271ec40502ab50158b2f9826de5877d8feec27e22e8d6ee3d2
FROM gcr.io/distroless/static-debian12:debug
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
+2 -3
View File
@@ -75,7 +75,6 @@ tasks:
- task: unit
- task: integration
- task: validate-cyclonedx-schema
- task: benchmark
- task: test-utils
- task: cli
- task: check-docker-cache
@@ -453,8 +452,8 @@ tasks:
find . | grep cache | grep tar | xargs du
echo "total $total_size KB"
if [ "$total_size" -gt 1048576 ]; then
echo 'docker cache is larger than 1GB'
if [ "$total_size" -gt 5242880 ]; then
echo 'docker cache is larger than 5GB'
exit 1
fi
+6 -38
View File
@@ -4,18 +4,13 @@ import (
"fmt"
"github.com/anchore/clio"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/cataloging"
)
type licenseConfig struct {
Content cataloging.LicenseContent `yaml:"content" json:"content" mapstructure:"content"`
// Deprecated: please use include-license-content instead
IncludeUnknownLicenseContent *bool `yaml:"-" json:"-" mapstructure:"include-unknown-license-content"`
Coverage float64 `yaml:"coverage" json:"coverage" mapstructure:"coverage"`
// Deprecated: please use coverage instead
LicenseCoverage *float64 `yaml:"license-coverage" json:"license-coverage" mapstructure:"license-coverage"`
Content cataloging.LicenseContent `yaml:"content" json:"content" mapstructure:"content"`
Coverage float64 `yaml:"coverage" json:"coverage" mapstructure:"coverage"`
AvailableLicenseContent []cataloging.LicenseContent `yaml:"-" json:"-" mapstructure:"-"`
}
@@ -25,43 +20,16 @@ var _ interface {
func (o *licenseConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
descriptions.Add(&o.Content, fmt.Sprintf("include the content of licenses in the SBOM for a given syft scan; valid values are: %s", o.AvailableLicenseContent))
descriptions.Add(&o.IncludeUnknownLicenseContent, `deprecated: please use 'license-content' instead`)
descriptions.Add(&o.Coverage, `adjust the percent as a fraction of the total text, in normalized words, that
matches any valid license for the given inputs, expressed as a percentage across all of the licenses matched.`)
descriptions.Add(&o.LicenseCoverage, `deprecated: please use 'coverage' instead`)
}
func (o *licenseConfig) PostLoad() error {
cfg := cataloging.DefaultLicenseConfig()
defaultContent := cfg.IncludeContent
defaultCoverage := cfg.Coverage
// if both legacy and new fields are specified, error out
if o.IncludeUnknownLicenseContent != nil && o.Content != defaultContent {
return fmt.Errorf("both 'include-unknown-license-content' and 'content' are set, please use only 'content'")
validContent := internal.NewSet(o.AvailableLicenseContent...)
if !validContent.Contains(o.Content) {
return fmt.Errorf("could not use %q as license content option; valid values are: %v", o.Content, validContent.ToSlice())
}
if o.LicenseCoverage != nil && o.Coverage != defaultCoverage {
return fmt.Errorf("both 'license-coverage' and 'coverage' are set, please use only 'coverage'")
}
// finalize the license content value
if o.IncludeUnknownLicenseContent != nil {
// convert 'include-unknown-license-content' -> 'license-content'
v := cataloging.LicenseContentExcludeAll
if *o.IncludeUnknownLicenseContent {
v = cataloging.LicenseContentIncludeUnknown
}
o.Content = v
}
// finalize the coverage value
if o.LicenseCoverage != nil {
// convert 'license-coverage' -> 'coverage'
o.Coverage = *o.LicenseCoverage
}
return nil
}
@@ -19,7 +19,7 @@ func TestPackageDeduplication(t *testing.T) {
}{
{
scope: source.AllLayersScope,
packageCount: 178, // without deduplication this would be ~600
packageCount: 175, // without deduplication this would be ~600
instanceCount: map[string]int{
"basesystem": 1,
"wget": 1,
@@ -30,15 +30,15 @@ func TestPackageDeduplication(t *testing.T) {
locationCount: map[string]int{
"basesystem-11-13.el9": 5, // in all layers
"curl-minimal-7.76.1-26.el9_3.2.0.1": 2, // base + wget layer
"curl-minimal-7.76.1-29.el9_4.1": 3, // curl upgrade layer + all above layers
"curl-minimal-7.76.1-31.el9": 3, // curl upgrade layer + all above layers
"wget-1.21.1-8.el9_4": 4, // wget + all above layers
"vsftpd-3.0.5-5.el9": 2, // vsftpd + all above layers
"httpd-2.4.57-11.el9_4.1": 1, // last layer
"vsftpd-3.0.5-6.el9": 2, // vsftpd + all above layers
"httpd-2.4.62-1.el9_5.2": 1, // last layer
},
},
{
scope: source.SquashedScope,
packageCount: 172,
packageCount: 169,
instanceCount: map[string]int{
"basesystem": 1,
"wget": 1,
@@ -47,11 +47,11 @@ func TestPackageDeduplication(t *testing.T) {
"httpd": 1, // rpm, binary is now excluded by overlap
},
locationCount: map[string]int{
"basesystem-11-13.el9": 1,
"curl-minimal-7.76.1-29.el9_4.1": 1, // upgrade
"wget-1.21.1-8.el9_4": 1,
"vsftpd-3.0.5-5.el9": 1,
"httpd-2.4.57-11.el9_4.1": 1,
"basesystem-11-13.el9": 1,
"curl-minimal-7.76.1-31.el9": 1, // upgrade
"wget-1.21.1-8.el9_4": 1,
"vsftpd-3.0.5-6.el9": 1,
"httpd-2.4.62-1.el9_5.2": 1,
},
},
}
@@ -3,18 +3,18 @@ package integration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func TestPhotonPackageRegression(t *testing.T) { // Regression: https://github.com/anchore/syft/pull/1997
sbom, _ := catalogFixtureImage(t, "image-photon-all-layers", source.AllLayersScope)
var packages []pkg.Package
for p := range sbom.Artifacts.Packages.Enumerate() {
packages = append(packages, p)
var count int
for range sbom.Artifacts.Packages.Enumerate(pkg.RpmPkg) {
count++
}
if len(packages) < 1 {
t.Errorf("failed to find packages for photon distro; wanted > 0 got 0")
}
assert.Greater(t, count, 0, "expected to find RPM packages in the SBOM (but did not)")
}
+6 -5
View File
@@ -19,6 +19,7 @@ require (
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
@@ -57,9 +58,8 @@ require (
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
github.com/knqyf263/go-rpmdb v0.1.1
github.com/magiconair/properties v1.8.10
github.com/mholt/archives v0.1.1
github.com/mholt/archives v0.1.2
github.com/moby/sys/mountinfo v0.7.2
github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1
github.com/olekukonko/tablewriter v0.0.5
@@ -100,11 +100,11 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Microsoft/hcsshim v0.11.7 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/ProtonMail/go-crypto v1.2.0 // indirect
github.com/STARRY-S/zip v0.2.1 // indirect
github.com/agext/levenshtein v1.2.1 // indirect; indirectt
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/aquasecurity/go-version v0.0.1 // indirect
@@ -260,6 +260,7 @@ require (
)
retract (
v1.25.0 // published with a replace directive (confusing for API users)
v0.53.2
v0.53.1 // Published accidentally with incorrect license in depdencies
v0.53.1 // published accidentally with incorrect license in depdencies
)
+8 -8
View File
@@ -78,8 +78,8 @@ github.com/Microsoft/hcsshim v0.11.7/go.mod h1:MV8xMfmECjl5HdO7U/3/hFVnkmSBjAjmA
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.2.0 h1:+PhXXn4SPGd+qk76TlEePBfOfivE0zkWFenhGhFLzWs=
github.com/ProtonMail/go-crypto v1.2.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg=
github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
@@ -110,6 +110,8 @@ github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4
github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU=
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk=
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec h1:SjjPMOXTzpuU1ZME4XeoHyek+dry3/C7I8gzaCo02eg=
github.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc=
github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=
github.com/anchore/go-sync v0.0.0-20250326131806-4eda43a485b6 h1:Ha+LSCVuXYSYGi7wIkJK6G8g6jI3LH7y6LbyEVyp4Io=
@@ -123,8 +125,8 @@ github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:
github.com/anchore/stereoscope v0.1.4 h1:e+iT9UdUzLBabWGe84hn5sTHDRioY+4IHsVzJXuJlek=
github.com/anchore/stereoscope v0.1.4/go.mod h1:omWgXDEp/XfqCJlZXIByEo1c3ArZg/qTJ5LBKVLAIdw=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@@ -548,8 +550,6 @@ github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYW
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584=
github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
@@ -600,8 +600,8 @@ github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archives v0.1.1 h1:c7J3qXN1FB54y0qiUXiq9Bxk4eCUc8pdXWwOhZdRzeY=
github.com/mholt/archives v0.1.1/go.mod h1:FQVz01Q2uXKB/35CXeW/QFO23xT+hSCGZHVtha78U4I=
github.com/mholt/archives v0.1.2 h1:UBSe5NfYKHI1sy+S5dJsEsG9jsKKk8NJA4HCC+xTI4A=
github.com/mholt/archives v0.1.2/go.mod h1:D7QzTHgw3ctfS6wgOO9dN+MFgdZpbksGCxprUOwZWDs=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
+1 -1
View File
@@ -3,5 +3,5 @@ package internal
const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "16.0.32"
JSONSchemaVersion = "16.0.34"
)
+5 -5
View File
@@ -278,16 +278,16 @@ func applyLicenseContentRules(p *pkg.Package, cfg cataloging.LicenseConfig) {
l := &licenses[i]
switch cfg.IncludeContent {
case cataloging.LicenseContentIncludeUnknown:
// we don't have an SPDX expression, which means we didn't find an SPDX license
// include the unknown licenses content in the final SBOM
// we have an SPDX expression, which means this is NOT an unknown license
// remove the content, we are only including content for unknown licenses by default
if l.SPDXExpression != "" {
licenses[i].Contents = ""
}
case cataloging.LicenseContentExcludeAll:
// clear it all out
licenses[i].Contents = ""
case cataloging.LicenseContentIncludeAll:
// always include the content
default:
// clear it all out
licenses[i].Contents = ""
}
}
+36 -1
View File
@@ -174,7 +174,7 @@ func TestApplyLicenseContentRules(t *testing.T) {
},
},
{
name: "IncludeLicenseContentDefault",
name: "LicenseContentIncludeAll",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
@@ -193,6 +193,41 @@ func TestApplyLicenseContentRules(t *testing.T) {
},
},
},
{
name: "default license config should be LicenseContentExcludeAll",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.DefaultLicenseConfig(),
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
},
{
Value: "License-Not-A-SPDX-Expression",
},
},
},
{
name: "invalid license content cataloging config results in the default case",
inputLicenses: []pkg.License{
licenseWithSPDX,
licenseWithoutSPDX,
},
cfg: cataloging.LicenseConfig{
IncludeContent: cataloging.LicenseContent("invalid"),
},
expectedLicenses: []pkg.License{
{
SPDXExpression: "MIT",
},
{
Value: "License-Not-A-SPDX-Expression",
Contents: "", // content all removed
},
},
},
{
name: "Empty licenses",
inputLicenses: []pkg.License{},
+3 -1
View File
@@ -154,7 +154,8 @@ func DefaultPackageTaskFactories() Factories {
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary",
),
newSimplePackageTaskFactory(binary.NewELFPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "elf-package"),
newSimplePackageTaskFactory(binary.NewELFPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "elf-package", "elf"),
newSimplePackageTaskFactory(binary.NewPEPackageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "pe-package", "pe", "dll", "exe"),
newSimplePackageTaskFactory(githubactions.NewActionUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
newSimplePackageTaskFactory(githubactions.NewWorkflowUsageCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "github", "github-actions"),
newSimplePackageTaskFactory(java.NewJvmDistributionCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "java", "jvm", "jdk", "jre"),
@@ -164,6 +165,7 @@ func DefaultPackageTaskFactories() Factories {
},
pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "linux", "kernel",
),
newSimplePackageTaskFactory(php.NewInterpreterCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "binary", "php"),
newSimplePackageTaskFactory(sbomCataloger.NewCataloger, "sbom"), // note: not evidence of installed packages
newSimplePackageTaskFactory(bitnamiSbomCataloger.NewCataloger, "bitnami", pkgcataloging.InstalledTag, pkgcataloging.ImageTag),
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+50 -1
View File
@@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.32/document",
"$id": "anchore.io/schema/syft/json/16.0.34/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -1971,6 +1971,9 @@
{
"$ref": "#/$defs/OpamPackage"
},
{
"$ref": "#/$defs/PeBinary"
},
{
"$ref": "#/$defs/PhpComposerInstalledEntry"
},
@@ -2045,6 +2048,17 @@
"purl"
]
},
"PeBinary": {
"properties": {
"VersionResources": {
"$ref": "#/$defs/KeyValues"
}
},
"type": "object",
"required": [
"VersionResources"
]
},
"PhpComposerAuthors": {
"properties": {
"name": {
@@ -2677,6 +2691,12 @@
"sourceRpm": {
"type": "string"
},
"signatures": {
"items": {
"$ref": "#/$defs/RpmSignature"
},
"type": "array"
},
"size": {
"type": "integer"
},
@@ -2745,6 +2765,12 @@
"sourceRpm": {
"type": "string"
},
"signatures": {
"items": {
"$ref": "#/$defs/RpmSignature"
},
"type": "array"
},
"size": {
"type": "integer"
},
@@ -2821,6 +2847,29 @@
"flags"
]
},
"RpmSignature": {
"properties": {
"algo": {
"type": "string"
},
"hash": {
"type": "string"
},
"created": {
"type": "string"
},
"issuer": {
"type": "string"
}
},
"type": "object",
"required": [
"algo",
"hash",
"created",
"issuer"
]
},
"RubyGemspec": {
"properties": {
"name": {
-4
View File
@@ -14,10 +14,6 @@ const (
)
type LicenseConfig struct {
// IncludeUnknownLicenseContent controls whether the content of a license should be included in the SBOM when the license ID cannot be determined.
// Deprecated: use IncludeContent instead
IncludeUnknownLicenseContent bool `json:"-" yaml:"-" mapstructure:"-"`
// IncludeContent controls whether license copy discovered should be included in the SBOM.
IncludeContent LicenseContent `json:"include-content" yaml:"include-content" mapstructure:"include-content"`
+1
View File
@@ -255,6 +255,7 @@ func (c *CreateSBOMConfig) selectTasks(src source.Description) ([]task.Task, []t
RelationshipsConfig: c.Relationships,
DataGenerationConfig: c.DataGeneration,
PackagesConfig: c.Packages,
LicenseConfig: c.Licenses,
ComplianceConfig: c.Compliance,
FilesConfig: c.Files,
}
@@ -958,7 +958,7 @@ func Test_otherLicenses(t *testing.T) {
{
LicenseIdentifier: "LicenseRef-3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
// Carries through the syft-json license value when we shasum large texts
LicenseName: "LicenseRef-sha256:3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
LicenseName: "sha256:3f17782eef51ae86f18fdd6832f5918e2b40f688b52c9adc07ba6ec1024ef408",
ExtractedText: strings.TrimSpace(bigText),
},
},
+9 -1
View File
@@ -94,7 +94,15 @@ func redactor(values ...string) testutil.Redactor {
testutil.PatternReplacement{
// only the source component bom-ref (not package or other component bom-refs)
Search: regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="file">`),
Groups: []string{"redact"}, // use the regex to anchore the search, but only replace bytes within the capture group
Groups: []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
Replace: "redacted",
},
).
WithPatternRedactorSpec(
testutil.PatternReplacement{
// only the source component bom-ref (not package or other component bom-refs)
Search: regexp.MustCompile(`<component bom-ref="(?P<redact>[^"]*)" type="container">`),
Groups: []string{"redact"}, // use the regex to anchor the search, but only replace bytes within the capture group
Replace: "redacted",
},
).
+5 -22
View File
@@ -71,31 +71,14 @@ func Backfill(p *pkg.Package) {
}
}
func setJavaMetadataFromPurl(p *pkg.Package, purl packageurl.PackageURL) {
func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) {
if p.Type != pkg.JavaPkg {
return
}
if purl.Namespace != "" {
if p.Metadata == nil {
p.Metadata = pkg.JavaArchive{}
}
meta, got := p.Metadata.(pkg.JavaArchive)
if got && meta.PomProperties == nil {
meta.PomProperties = &pkg.JavaPomProperties{}
p.Metadata = meta
}
if meta.PomProperties != nil {
// capture the group id from the purl if it is not already set
if meta.PomProperties.ArtifactID == "" {
meta.PomProperties.ArtifactID = purl.Name
}
if meta.PomProperties.GroupID == "" {
meta.PomProperties.GroupID = purl.Namespace
}
if meta.PomProperties.Version == "" {
meta.PomProperties.Version = purl.Version
}
}
if p.Metadata == nil {
// since we don't know if the purl elements directly came from pom properties or the manifest,
// we can only go as far as to set the type to JavaArchive, but not fill in the group id and artifact id
p.Metadata = pkg.JavaArchive{}
}
}
+3 -7
View File
@@ -101,13 +101,9 @@ func Test_Backfill(t *testing.T) {
Language: pkg.Java,
Name: "some-thing",
Version: "1.2.3",
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache",
ArtifactID: "some-thing",
Version: "1.2.3",
},
},
// we intentionally don't claim we found a pom properties file with a groupID from the purl.
// but we do claim that we found java data with an empty type.
Metadata: pkg.JavaArchive{},
},
},
}
@@ -3,6 +3,7 @@ package helpers
import (
"fmt"
"reflect"
"strings"
"github.com/CycloneDX/cyclonedx-go"
@@ -90,15 +91,19 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
Locations: decodeLocations(values),
Licenses: pkg.NewLicenseSet(decodeLicenses(c)...),
CPEs: decodeCPEs(c),
PURL: c.PackageURL,
}
// note: this may write in syft package type information
DecodeInto(p, values, "syft:package", CycloneDXFields)
metadataType := values["syft:package:metadataType"]
p.Metadata = decodePackageMetadata(values, c, metadataType)
// this will either use the purl from the component or generate a new one based off of any type information
// that was decoded above.
p.PURL = getPURL(c, p.Type)
if p.Type == "" {
p.Type = pkg.TypeFromPURL(p.PURL)
}
@@ -111,6 +116,41 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package {
return p
}
func getPURL(c *cyclonedx.Component, ty pkg.Type) string {
if c.PackageURL != "" {
// if there is a purl that where the namespace does not match the group information, we may
// accidentally drop group. We should consider adding group as a top-level syft package field.
return c.PackageURL
}
if strings.HasPrefix(c.BOMRef, "pkg:") {
// the bomref is a purl, so try to use that as the purl
_, err := packageurl.FromString(c.BOMRef)
if err == nil {
return c.BOMRef
}
}
if ty == "" {
return ""
}
tyStr := ty.PackageURLType()
switch tyStr {
case "", packageurl.TypeGeneric:
return ""
}
purl := packageurl.PackageURL{
Type: tyStr,
Namespace: c.Group,
Name: c.Name,
Version: c.Version,
}
return purl.ToString()
}
func setPackageName(p *pkg.Package, c *cyclonedx.Component) {
name := c.Name
if c.Group != "" {
@@ -275,6 +275,7 @@ func Test_decodeComponent(t *testing.T) {
component cyclonedx.Component
wantLanguage pkg.Language
wantMetadata any
wantPURL string
}{
{
name: "derive language from pURL if missing",
@@ -286,6 +287,18 @@ func Test_decodeComponent(t *testing.T) {
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
wantLanguage: pkg.Java,
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
{
name: "derive language from bomref if missing",
component: cyclonedx.Component{
Name: "ch.qos.logback/logback-classic",
Version: "1.2.3",
Type: "library",
BOMRef: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
wantLanguage: pkg.Java,
wantPURL: "pkg:maven/ch.qos.logback/logback-classic@1.2.3",
},
{
name: "handle RpmdbMetadata type without properties",
@@ -303,6 +316,7 @@ func Test_decodeComponent(t *testing.T) {
},
},
wantMetadata: pkg.RpmDBEntry{},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},
{
name: "handle RpmdbMetadata type with properties",
@@ -326,6 +340,24 @@ func Test_decodeComponent(t *testing.T) {
wantMetadata: pkg.RpmDBEntry{
Release: "some-release",
},
wantPURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=x86_64&upstream=acl-2.2.53-1.el8.src.rpm&distro=centos-8",
},
{
name: "generate a purl from package type",
component: cyclonedx.Component{
Name: "log4j",
Group: "org.apache.logging.log4j",
Version: "2.0.4",
Type: "library",
BOMRef: "log4j",
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:type",
Value: "java-archive",
},
},
},
wantPURL: "pkg:maven/org.apache.logging.log4j/log4j@2.0.4",
},
}
@@ -338,9 +370,122 @@ func Test_decodeComponent(t *testing.T) {
if tt.wantMetadata != nil {
assert.Truef(t, reflect.DeepEqual(tt.wantMetadata, p.Metadata), "metadata should match: %+v != %+v", tt.wantMetadata, p.Metadata)
}
if tt.wantMetadata == nil && tt.wantLanguage == "" {
if tt.wantPURL != "" {
assert.Equal(t, tt.wantPURL, p.PURL, "purl should match")
}
if tt.wantMetadata == nil && tt.wantLanguage == "" && tt.wantPURL == "" {
t.Fatal("this is a useless test, please remove it")
}
})
}
}
func TestGetPURL(t *testing.T) {
tests := []struct {
name string
component *cyclonedx.Component
pkgType pkg.Type
expected string
}{
{
name: "component with PackageURL",
component: &cyclonedx.Component{
PackageURL: "pkg:npm/lodash@4.17.21",
Name: "lodash",
Version: "4.17.20", // different version to verify PackageURL is used
Group: "npm",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/lodash@4.17.21",
},
{
name: "component with BOMRef as valid PURL",
component: &cyclonedx.Component{
BOMRef: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
Name: "commons-lang3",
Version: "3.11.0", // different version to verify BOMRef is used
Group: "org.apache.commons",
},
pkgType: pkg.JavaPkg,
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
},
{
name: "component with BOMRef not a valid PURL",
component: &cyclonedx.Component{
BOMRef: "not-a-purl-ref",
Name: "commons-lang3",
Version: "3.12.0",
Group: "org.apache.commons",
},
pkgType: pkg.JavaPkg,
expected: "pkg:maven/org.apache.commons/commons-lang3@3.12.0",
},
{
name: "component with unknown package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: pkg.UnknownPkg,
expected: "",
},
{
name: "component with empty package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: "",
expected: "",
},
{
name: "component with generic package type",
component: &cyclonedx.Component{
Name: "some-component",
Version: "1.0.0",
Group: "org.example",
},
pkgType: pkg.LinuxKernelModulePkg,
expected: "",
},
{
name: "component with valid package type",
component: &cyclonedx.Component{
Name: "react",
Version: "18.2.0",
Group: "facebook",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/facebook/react@18.2.0",
},
{
name: "component with no group",
component: &cyclonedx.Component{
Name: "express",
Version: "4.18.2",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/express@4.18.2",
},
{
name: "component with no version",
component: &cyclonedx.Component{
Name: "express",
Group: "npm",
},
pkgType: pkg.NpmPkg,
expected: "pkg:npm/npm/express",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getPURL(tt.component, tt.pkgType)
assert.Equal(t, tt.expected, result)
})
}
}
@@ -73,7 +73,7 @@ func ParseLicenses(raw []pkg.License) (concluded, declared []SPDXLicense, otherL
for _, l := range raw {
candidate := createSPDXLicense(l)
// isCustomLicense determines if the candidate falls under https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#
// this determines if the candidate falls under https://spdx.github.io/spdx-spec/v2.3/other-licensing-information-detected/#
// of the SPDX spec, where:
// - we should not have a complex SPDX expression
// - if a single license, it should not be a known license (on the SPDX license list)
@@ -50,6 +50,14 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
case pkg.DotnetPortableExecutableEntry:
typ = orgType
author = metadata.CompanyName
case pkg.PEBinary:
// this is a known common keyword used in version resources
// for more info see: https://learn.microsoft.com/en-us/windows/win32/menurc/versioninfo-resource
val, ok := metadata.VersionResources.Get("CompanyName")
if ok {
typ = orgType
author = val
}
case pkg.DpkgDBEntry:
author = metadata.Maintainer
@@ -112,6 +112,21 @@ func Test_OriginatorSupplier(t *testing.T) {
originator: "Organization: Microsoft Corporation",
supplier: "Organization: Microsoft Corporation",
},
{
name: "from PE binary",
input: pkg.Package{
Metadata: pkg.PEBinary{
VersionResources: pkg.KeyValues{
{
Key: "CompanyName",
Value: "Microsoft Corporation",
},
},
},
},
originator: "Organization: Microsoft Corporation",
supplier: "Organization: Microsoft Corporation",
},
{
name: "from dpkg DB",
input: pkg.Package{
+3 -7
View File
@@ -165,13 +165,9 @@ func TestDecoder_Decode(t *testing.T) {
Type: pkg.JavaPkg,
PURL: "pkg:maven/org.apache/some-pkg@4.11.3",
Language: pkg.Java,
Metadata: pkg.JavaArchive{
PomProperties: &pkg.JavaPomProperties{
GroupID: "org.apache",
ArtifactID: "some-pkg",
Version: "4.11.3",
},
},
// we intentionally do not claim we found a pom properties file (don't derive this from the purl).
// but we need a metadata allocated since all Java packages have a this metadata type (a consistency point)
Metadata: pkg.JavaArchive{},
},
},
},
@@ -42,6 +42,7 @@ func AllTypes() []any {
pkg.NpmPackage{},
pkg.NpmPackageLockEntry{},
pkg.OpamPackage{},
pkg.PEBinary{},
pkg.PhpComposerInstalledEntry{},
pkg.PhpComposerLockEntry{},
pkg.PhpPearEntry{},
+1
View File
@@ -94,6 +94,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.NpmPackage{}, "javascript-npm-package", "NpmPackageJsonMetadata"),
jsonNames(pkg.NpmPackageLockEntry{}, "javascript-npm-package-lock-entry", "NpmPackageLockJsonMetadata"),
jsonNames(pkg.YarnLockEntry{}, "javascript-yarn-lock-entry", "YarnLockJsonMetadata"),
jsonNames(pkg.PEBinary{}, "pe-binary"),
jsonNames(pkg.PhpComposerLockEntry{}, "php-composer-lock-entry", "PhpComposerJsonMetadata"),
jsonNamesWithoutLookup(pkg.PhpComposerInstalledEntry{}, "php-composer-installed-entry", "PhpComposerJsonMetadata"), // the legacy value is split into two types, where the other is preferred
jsonNames(pkg.PhpPeclEntry{}, "php-pecl-entry", "PhpPeclMetadata"),
+5
View File
@@ -46,3 +46,8 @@ type ELFBinaryPackageNoteJSONPayload struct {
// Commit is the commit hash of the source repository for which the binary was built from
Commit string `json:"commit,omitempty"`
}
// PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)
type PEBinary struct {
VersionResources KeyValues
}
@@ -13,12 +13,13 @@ import (
"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/internal/binutils"
)
const catalogerName = "binary-classifier-cataloger"
type ClassifierCatalogerConfig struct {
Classifiers []Classifier `yaml:"classifiers" json:"classifiers" mapstructure:"classifiers"`
Classifiers []binutils.Classifier `yaml:"classifiers" json:"classifiers" mapstructure:"classifiers"`
}
func DefaultClassifierCatalogerConfig() ClassifierCatalogerConfig {
@@ -48,7 +49,7 @@ func (cfg ClassifierCatalogerConfig) MarshalJSON() ([]byte, error) {
// related runtimes like Python, Go, Java, or Node. Some exceptions can be made for widely-used binaries such
// as busybox.
type cataloger struct {
classifiers []Classifier
classifiers []binutils.Classifier
}
// Name returns a string that uniquely describes the cataloger
@@ -101,7 +102,7 @@ func mergePackages(target *pkg.Package, extra *pkg.Package) {
target.Metadata = meta
}
func catalog(resolver file.Resolver, cls Classifier) (packages []pkg.Package, err error) {
func catalog(resolver file.Resolver, cls binutils.Classifier) (packages []pkg.Package, err error) {
var errs error
locations, err := resolver.FilesByGlob(cls.FileGlob)
if err != nil {
@@ -109,7 +110,7 @@ func catalog(resolver file.Resolver, cls Classifier) (packages []pkg.Package, er
return nil, err
}
for _, location := range locations {
pkgs, err := cls.EvidenceMatcher(cls, matcherContext{resolver: resolver, location: location})
pkgs, err := cls.EvidenceMatcher(cls, binutils.MatcherContext{Resolver: resolver, Location: location})
if err != nil {
errs = unknown.Append(errs, location, err)
continue
@@ -20,6 +20,7 @@ import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil"
"github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/stereoscopesource"
@@ -248,45 +249,6 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("httpd-binary"),
},
},
{
// TODO: find original binary...
// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
logicalFixture: "php-cli/8.2.1/linux-amd64",
expected: pkg.Package{
Name: "php-cli",
Version: "8.2.1",
Type: "binary",
PURL: "pkg:generic/php-cli@8.2.1",
Locations: locations("php"),
Metadata: metadata("php-cli-binary"),
},
},
{
// TODO: find original binary...
// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
logicalFixture: "php-fpm/8.2.1/linux-amd64",
expected: pkg.Package{
Name: "php-fpm",
Version: "8.2.1",
Type: "binary",
PURL: "pkg:generic/php-fpm@8.2.1",
Locations: locations("php-fpm"),
Metadata: metadata("php-fpm-binary"),
},
},
{
// TODO: find original binary...
// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
logicalFixture: "php-apache/8.2.1/linux-amd64",
expected: pkg.Package{
Name: "libphp",
Version: "8.2.1",
Type: "binary",
PURL: "pkg:generic/php@8.2.1",
Locations: locations("libphp.so"),
Metadata: metadata("php-apache-binary"),
},
},
{
// TODO: original binary is different than whats in config.yaml
// note: cannot find the original binary, using a custom snippet based on the original snippet in the repo
@@ -1497,11 +1459,13 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
Locations: locations("foo"),
Metadata: metadata("foo-binary"),
}
fooClassifier := Classifier{
fooClassifier := binutils.Classifier{
Class: "foo-binary",
FileGlob: "**/foo",
EvidenceMatcher: FileContentsVersionMatcher(
`(?m)foobar\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
EvidenceMatcher: binutils.FileContentsVersionMatcher(
`(?m)foobar\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
catalogerName,
),
Package: "foo",
PURL: mustPURL("pkg:generic/foo@version"),
CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
@@ -1516,7 +1480,7 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
{
name: "empty-negative",
config: ClassifierCatalogerConfig{
Classifiers: []Classifier{},
Classifiers: []binutils.Classifier{},
},
fixtureDir: "test-fixtures/custom/go-1.14",
expected: nil,
@@ -1532,7 +1496,7 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
{
name: "nodefault-negative",
config: ClassifierCatalogerConfig{
Classifiers: []Classifier{fooClassifier},
Classifiers: []binutils.Classifier{fooClassifier},
},
fixtureDir: "test-fixtures/custom/go-1.14",
expected: nil,
@@ -1541,7 +1505,7 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
name: "default-extended-positive",
config: ClassifierCatalogerConfig{
Classifiers: append(
append([]Classifier{}, defaultClassifers...),
append([]binutils.Classifier{}, defaultClassifers...),
fooClassifier,
),
},
@@ -1553,11 +1517,11 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
config: ClassifierCatalogerConfig{
Classifiers: append(
append([]Classifier{}, defaultClassifers...),
Classifier{
append([]binutils.Classifier{}, defaultClassifers...),
binutils.Classifier{
Class: "foo-binary",
FileGlob: "**/foo",
EvidenceMatcher: FileContentsVersionMatcher(`(?m)not there`),
EvidenceMatcher: binutils.FileContentsVersionMatcher(`(?m)not there`, catalogerName),
Package: "foo",
PURL: mustPURL("pkg:generic/foo@version"),
CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
@@ -1571,7 +1535,7 @@ func Test_Cataloger_CustomClassifiers(t *testing.T) {
name: "default-cutsom-positive",
config: ClassifierCatalogerConfig{
Classifiers: append(
append([]Classifier{}, defaultClassifers...),
append([]binutils.Classifier{}, defaultClassifers...),
fooClassifier,
),
},
@@ -1771,11 +1735,11 @@ func TestCatalogerConfig_MarshalJSON(t *testing.T) {
{
name: "only show names of classes",
cfg: ClassifierCatalogerConfig{
Classifiers: []Classifier{
Classifiers: []binutils.Classifier{
{
Class: "class",
FileGlob: "glob",
EvidenceMatcher: FileContentsVersionMatcher(".thing"),
EvidenceMatcher: binutils.FileContentsVersionMatcher(".thing", catalogerName),
Package: "pkg",
PURL: packageurl.PackageURL{
Type: "type",
+121 -123
View File
@@ -1,22 +1,44 @@
package binary
import (
"fmt"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
)
// in both binaries and shared libraries, the version pattern is [NUL]3.11.2[NUL]
var pythonVersionTemplate = `(?m)\x00(?P<version>{{ .version }}[-._a-zA-Z0-9]*)\x00`
//nolint:funlen
func DefaultClassifiers() []Classifier {
return []Classifier{
func DefaultClassifiers() []binutils.Classifier {
m := binutils.ContextualEvidenceMatchers{CatalogerName: catalogerName}
var libpythonMatcher = m.FileNameTemplateVersionMatcher(
`(?:.*/|^)libpython(?P<version>[0-9]+(?:\.[0-9]+)+)[a-z]?\.so.*$`,
pythonVersionTemplate,
)
var rubyMatcher = m.FileContentsVersionMatcher(
// ruby 3.4.0dev (2024-09-15T01:06:11Z master 532af89e3b) [x86_64-linux]
// ruby 3.4.0preview1 (2024-05-16 master 9d69619623) [x86_64-linux]
// ruby 3.3.0rc1 (2023-12-11 master a49643340e) [x86_64-linux]
// ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux]
// ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [x86_64-linux]
`(?m)ruby (?P<version>[0-9]+\.[0-9]+\.[0-9]+((p|preview|rc|dev)[0-9]*)?) `)
return []binutils.Classifier{
{
Class: "python-binary",
FileGlob: "**/python*",
EvidenceMatcher: evidenceMatchers(
EvidenceMatcher: binutils.EvidenceMatchers(
// try to find version information from libpython shared libraries
sharedLibraryLookup(
binutils.SharedLibraryLookup(
`^libpython[0-9]+(?:\.[0-9]+)+[a-z]?\.so.*$`,
libpythonMatcher),
// check for version information in the binary
fileNameTemplateVersionMatcher(
m.FileNameTemplateVersionMatcher(
`(?:.*/|^)python(?P<version>[0-9]+(?:\.[0-9]+)+)$`,
pythonVersionTemplate),
),
@@ -41,7 +63,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "pypy-binary-lib",
FileGlob: "**/libpypy*.so*",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\[PyPy (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "pypy",
PURL: mustPURL("pkg:generic/pypy@version"),
@@ -49,7 +71,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "go-binary",
FileGlob: "**/go",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)\x00`),
Package: "go",
PURL: mustPURL("pkg:generic/go@version"),
@@ -58,7 +80,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "julia-binary",
FileGlob: "**/libjulia-internal.so",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)__init__\x00(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00verify`),
Package: "julia",
PURL: mustPURL("pkg:generic/julia@version"),
@@ -67,7 +89,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "helm",
FileGlob: "**/helm",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\x00v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`),
Package: "helm",
PURL: mustPURL("pkg:golang/helm.sh/helm@version"),
@@ -76,13 +98,13 @@ func DefaultClassifiers() []Classifier {
{
Class: "redis-binary",
FileGlob: "**/redis-server",
EvidenceMatcher: evidenceMatchers(
EvidenceMatcher: binutils.EvidenceMatchers(
// matches most recent versions of redis (~v7), e.g. "7.0.14buildkitsandbox-1702957741000000000"
FileContentsVersionMatcher(`[^\d](?P<version>\d+.\d+\.\d+)buildkitsandbox-\d+`),
m.FileContentsVersionMatcher(`[^\d](?P<version>\d+.\d+\.\d+)buildkitsandbox-\d+`),
// matches against older versions of redis (~v3 - v6), e.g. "4.0.11841ce7054bd9-1542359302000000000"
FileContentsVersionMatcher(`[^\d](?P<version>[0-9]+\.[0-9]+\.[0-9]+)\w{12}-\d+`),
m.FileContentsVersionMatcher(`[^\d](?P<version>[0-9]+\.[0-9]+\.[0-9]+)\w{12}-\d+`),
// matches against older versions of redis (~v2), e.g. "Server started, Redis version 2.8.23"
FileContentsVersionMatcher(`Redis version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
m.FileContentsVersionMatcher(`Redis version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
),
Package: "redis",
PURL: mustPURL("pkg:generic/redis@version"),
@@ -94,13 +116,13 @@ func DefaultClassifiers() []Classifier {
{
Class: "java-binary-openjdk",
FileGlob: "**/java",
EvidenceMatcher: matchExcluding(
evidenceMatchers(
FileContentsVersionMatcher(
EvidenceMatcher: binutils.MatchExcluding(
binutils.EvidenceMatchers(
m.FileContentsVersionMatcher(
// [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL]
// [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL]
`(?m)\x00openjdk\x00java\x00(?P<release>[0-9]+[.0-9]*)\x00(?P<version>[0-9]+[^\x00]+)\x00`),
FileContentsVersionMatcher(
m.FileContentsVersionMatcher(
// arm64 versions: [NUL]0.0[NUL][NUL][NUL][NUL][NUL]11.0.22+7[NUL][NUL][NUL][NUL][NUL][NUL][NUL]openjdk[NUL]java[NUL]
`(?m)\x00(?P<release>[0-9]+[.0-9]*)\x00+(?P<version>[0-9]+[^\x00]+)\x00+openjdk\x00java`),
),
@@ -115,7 +137,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "java-binary-ibm",
FileGlob: "**/java",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// [NUL]java[NUL]1.8[NUL][NUL][NUL][NUL]1.8.0-foreman_2022_09_22_15_30-b00[NUL]
`(?m)\x00java\x00(?P<release>[0-9]+[.0-9]+)\x00{4}(?P<version>[0-9]+[-._a-zA-Z0-9]+)\x00`),
Package: "java/jre",
@@ -125,8 +147,8 @@ func DefaultClassifiers() []Classifier {
{
Class: "java-binary-oracle",
FileGlob: "**/java",
EvidenceMatcher: matchExcluding(
FileContentsVersionMatcher(
EvidenceMatcher: binutils.MatchExcluding(
m.FileContentsVersionMatcher(
// [NUL]19.0.1+10-21[NUL]
`(?m)\x00(?P<version>[0-9]+[.0-9]+[+][-0-9]+)\x00`),
// don't match openjdk
@@ -139,7 +161,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "java-binary-graalvm",
FileGlob: "**/java",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\x00(?P<version>[0-9]+[.0-9]+[.0-9]+\+[0-9]+-jvmci-[0-9]+[.0-9]+-b[0-9]+)\x00`),
Package: "java/graalvm",
PURL: mustPURL("pkg:generic/java/graalvm@version"),
@@ -148,7 +170,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "java-binary-jdk",
FileGlob: "**/jdb",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\x00(?P<version>[0-9]+\.[0-9]+\.[0-9]+(\+[0-9]+)?([-._a-zA-Z0-9]+)?)\x00`),
Package: "java/jdk",
PURL: mustPURL("pkg:generic/java/jdk@version"),
@@ -157,13 +179,13 @@ func DefaultClassifiers() []Classifier {
{
Class: "nodejs-binary",
FileGlob: "**/node",
EvidenceMatcher: evidenceMatchers(
EvidenceMatcher: binutils.EvidenceMatchers(
// [NUL]node v0.10.48[NUL]
// [NUL]v0.12.18[NUL]
// [NUL]v4.9.1[NUL]
// node.js/v22.9.0
FileContentsVersionMatcher(`(?m)\x00(node )?v(?P<version>(0|4|5|6)\.[0-9]+\.[0-9]+)\x00`),
FileContentsVersionMatcher(`(?m)node\.js\/v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
m.FileContentsVersionMatcher(`(?m)\x00(node )?v(?P<version>(0|4|5|6)\.[0-9]+\.[0-9]+)\x00`),
m.FileContentsVersionMatcher(`(?m)node\.js\/v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
),
Package: "node",
PURL: mustPURL("pkg:generic/node@version"),
@@ -172,7 +194,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "go-binary-hint",
FileGlob: "**/VERSION*",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?(-[0-9a-f]{7})?)`),
Package: "go",
PURL: mustPURL("pkg:generic/go@version"),
@@ -181,7 +203,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "busybox-binary",
FileGlob: "**/busybox",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)BusyBox\s+v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "busybox",
PURL: mustPURL("pkg:generic/busybox@version"),
@@ -190,7 +212,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "util-linux-binary",
FileGlob: "**/getopt",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`\x00util-linux\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`),
Package: "util-linux",
PURL: mustPURL("pkg:generic/util-linux@version"),
@@ -199,10 +221,10 @@ func DefaultClassifiers() []Classifier {
{
Class: "haproxy-binary",
FileGlob: "**/haproxy",
EvidenceMatcher: evidenceMatchers(
FileContentsVersionMatcher(`(?m)version (?P<version>[0-9]+\.[0-9]+(\.|-dev|-rc)[0-9]+)(-[a-z0-9]{7})?, released 20`),
FileContentsVersionMatcher(`(?m)HA-Proxy version (?P<version>[0-9]+\.[0-9]+(\.|-dev)[0-9]+)`),
FileContentsVersionMatcher(`(?m)(?P<version>[0-9]+\.[0-9]+(\.|-dev)[0-9]+)-[0-9a-zA-Z]{7}.+HAProxy version`),
EvidenceMatcher: binutils.EvidenceMatchers(
m.FileContentsVersionMatcher(`(?m)version (?P<version>[0-9]+\.[0-9]+(\.|-dev|-rc)[0-9]+)(-[a-z0-9]{7})?, released 20`),
m.FileContentsVersionMatcher(`(?m)HA-Proxy version (?P<version>[0-9]+\.[0-9]+(\.|-dev)[0-9]+)`),
m.FileContentsVersionMatcher(`(?m)(?P<version>[0-9]+\.[0-9]+(\.|-dev)[0-9]+)-[0-9a-zA-Z]{7}.+HAProxy version`),
),
Package: "haproxy",
PURL: mustPURL("pkg:generic/haproxy@version"),
@@ -211,44 +233,16 @@ func DefaultClassifiers() []Classifier {
{
Class: "perl-binary",
FileGlob: "**/perl",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "perl",
PURL: mustPURL("pkg:generic/perl@version"),
CPEs: singleCPE("cpe:2.3:a:perl:perl:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
{
Class: "php-cli-binary",
FileGlob: "**/php*",
EvidenceMatcher: fileNameTemplateVersionMatcher(
`(.*/|^)php[0-9]*$`,
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "php-cli",
PURL: mustPURL("pkg:generic/php-cli@version"),
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
{
Class: "php-fpm-binary",
FileGlob: "**/php-fpm*",
EvidenceMatcher: FileContentsVersionMatcher(
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "php-fpm",
PURL: mustPURL("pkg:generic/php-fpm@version"),
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
{
Class: "php-apache-binary",
FileGlob: "**/libphp*.so",
EvidenceMatcher: FileContentsVersionMatcher(
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "libphp",
PURL: mustPURL("pkg:generic/php@version"),
CPEs: singleCPE("cpe:2.3:a:php:php:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
{
Class: "php-composer-binary",
FileGlob: "**/composer*",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)'pretty_version'\s*=>\s*'(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)'`),
Package: "composer",
PURL: mustPURL("pkg:generic/composer@version"),
@@ -257,7 +251,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "httpd-binary",
FileGlob: "**/httpd",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)Apache\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "httpd",
PURL: mustPURL("pkg:generic/httpd@version"),
@@ -266,7 +260,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "memcached-binary",
FileGlob: "**/memcached",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)memcached\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "memcached",
PURL: mustPURL("pkg:generic/memcached@version"),
@@ -275,7 +269,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "traefik-binary",
FileGlob: "**/traefik",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// [NUL]v1.7.34[NUL]
// [NUL]2.9.6[NUL]
// 3.0.4[NUL]
@@ -287,7 +281,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "arangodb-binary",
FileGlob: "**/arangosh",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\x00*(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?)\s\[linux\]`),
Package: "arangodb",
PURL: mustPURL("pkg:generic/arangodb@version"),
@@ -296,7 +290,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "postgresql-binary",
FileGlob: "**/postgres",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// [NUL]PostgreSQL 15beta4
// [NUL]PostgreSQL 15.1
// [NUL]PostgreSQL 9.6.24
@@ -309,11 +303,11 @@ func DefaultClassifiers() []Classifier {
{
Class: "mysql-binary",
FileGlob: "**/mysql",
EvidenceMatcher: evidenceMatchers(
EvidenceMatcher: binutils.EvidenceMatchers(
// shutdown[NUL]8.0.37[NUL][NUL][NUL][NUL][NUL]mysql_real_esc
FileContentsVersionMatcher(`\x00(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)\x00+mysql`),
m.FileContentsVersionMatcher(`\x00(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)\x00+mysql`),
// /export/home/pb2/build/sb_0-26781090-1516292385.58/release/mysql-8.0.4-rc/mysys_ssl/my_default.cc
FileContentsVersionMatcher(`(?m).*/mysql-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
m.FileContentsVersionMatcher(`(?m).*/mysql-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
),
Package: "mysql",
PURL: mustPURL("pkg:generic/mysql@version"),
@@ -322,7 +316,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "mysql-binary",
FileGlob: "**/mysql",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m).*/percona-server-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
Package: "percona-server",
PURL: mustPURL("pkg:generic/percona-server@version"),
@@ -334,7 +328,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "mysql-binary",
FileGlob: "**/mysql",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m).*/Percona-XtraDB-Cluster-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
Package: "percona-xtradb-cluster",
PURL: mustPURL("pkg:generic/percona-xtradb-cluster@version"),
@@ -347,7 +341,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "xtrabackup-binary",
FileGlob: "**/xtrabackup",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m).*/percona-xtrabackup-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
Package: "percona-xtrabackup",
PURL: mustPURL("pkg:generic/percona-xtrabackup@version"),
@@ -356,7 +350,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "mariadb-binary",
FileGlob: "**/{mariadb,mysql}",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// 10.6.15-MariaDB
`(?m)(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)-MariaDB`),
Package: "mariadb",
@@ -366,7 +360,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "rust-standard-library-linux",
FileGlob: "**/libstd-????????????????.so",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// clang LLVM (rustc version 1.48.0 (7eac88abb 2020-11-16))
`(?m)(\x00)clang LLVM \(rustc version (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
Package: "rust",
@@ -376,7 +370,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "rust-standard-library-macos",
FileGlob: "**/libstd-????????????????.dylib",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// c 1.48.0 (7eac88abb 2020-11-16)
`(?m)c (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
Package: "rust",
@@ -386,9 +380,9 @@ func DefaultClassifiers() []Classifier {
{
Class: "ruby-binary",
FileGlob: "**/ruby",
EvidenceMatcher: evidenceMatchers(
EvidenceMatcher: binutils.EvidenceMatchers(
rubyMatcher,
sharedLibraryLookup(
binutils.SharedLibraryLookup(
// try to find version information from libruby shared libraries
`^libruby\.so.*$`,
rubyMatcher),
@@ -400,12 +394,12 @@ func DefaultClassifiers() []Classifier {
{
Class: "erlang-binary",
FileGlob: "**/erlexec",
EvidenceMatcher: evidenceMatchers(
FileContentsVersionMatcher(
EvidenceMatcher: binutils.EvidenceMatchers(
m.FileContentsVersionMatcher(
// <artificial>[NUL]/usr/src/otp_src_25.3.2.6/erts/
`(?m)/src/otp_src_(?P<version>[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`,
),
FileContentsVersionMatcher(
m.FileContentsVersionMatcher(
// <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/
`(?m)/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`,
),
@@ -417,16 +411,16 @@ func DefaultClassifiers() []Classifier {
{
Class: "erlang-alpine-binary",
FileGlob: "**/beam.smp",
EvidenceMatcher: evidenceMatchers(
FileContentsVersionMatcher(
EvidenceMatcher: binutils.EvidenceMatchers(
m.FileContentsVersionMatcher(
// <artificial>[NUL]/usr/src/otp_src_25.3.2.6/erts/
`(?m)/src/otp_src_(?P<version>[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`,
),
FileContentsVersionMatcher(
m.FileContentsVersionMatcher(
// <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/
`(?m)/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`,
),
FileContentsVersionMatcher(
m.FileContentsVersionMatcher(
// [NUL][NUL]26.1.2[NUL][NUL][NUL][NUL][NUL][NUL][NUL]NUL[NUL][NUL]Erlang/OTP
`\x00+(?P<version>[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)\x00+Erlang/OTP`,
),
@@ -438,12 +432,12 @@ func DefaultClassifiers() []Classifier {
{
Class: "erlang-library",
FileGlob: "**/liberts_internal.a",
EvidenceMatcher: evidenceMatchers(
FileContentsVersionMatcher(
EvidenceMatcher: binutils.EvidenceMatchers(
m.FileContentsVersionMatcher(
// <artificial>[NUL]/usr/src/otp_src_25.3.2.6/erts/
`(?m)/src/otp_src_(?P<version>[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`,
),
FileContentsVersionMatcher(
m.FileContentsVersionMatcher(
// <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/
`(?m)/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+(\.[0-9]+){0,2}(-rc[0-9])?)/erts/`,
),
@@ -455,7 +449,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "swipl-binary",
FileGlob: "**/swipl",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)swipl-(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\/`,
),
Package: "swipl",
@@ -465,7 +459,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "dart-binary",
FileGlob: "**/dart",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// MathAtan[NUL]2.12.4 (stable)
// "%s"[NUL]3.0.0 (stable)
// Dart,GC"[NUL]3.5.2 (stable)
@@ -479,7 +473,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "haskell-ghc-binary",
FileGlob: "**/ghc*",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\x00GHC (?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`,
),
Package: "haskell/ghc",
@@ -489,7 +483,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "haskell-cabal-binary",
FileGlob: "**/cabal",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)\x00Cabal-(?P<version>[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?)-`,
),
Package: "haskell/cabal",
@@ -499,7 +493,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "haskell-stack-binary",
FileGlob: "**/stack",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)Version\s*(?P<version>[0-9]+\.[0-9]+\.[0-9]+),\s*Git`,
),
Package: "haskell/stack",
@@ -509,7 +503,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "consul-binary",
FileGlob: "**/consul",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// NOTE: This is brittle and may not work for past or future versions
`CONSUL_VERSION: (?P<version>\d+\.\d+\.\d+)`,
),
@@ -520,7 +514,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "nginx-binary",
FileGlob: "**/nginx",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// [NUL]nginx version: nginx/1.25.1 - fetches '1.25.1'
// [NUL]nginx version: openresty/1.21.4.1 - fetches '1.21.4' as this is the nginx version part
`(?m)(\x00|\?)nginx version: [^\/]+\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(?:\+\d+)?(?:-\d+)?)`,
@@ -535,7 +529,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "bash-binary",
FileGlob: "**/bash",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// @(#)Bash version 5.2.15(1) release GNU
// @(#)Bash version 5.2.0(1) alpha GNU
// @(#)Bash version 5.2.0(1) beta GNU
@@ -549,7 +543,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "openssl-binary",
FileGlob: "**/openssl",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// [NUL]OpenSSL 3.1.4'
// [NUL]OpenSSL 1.1.1w'
`\x00OpenSSL (?P<version>[0-9]+\.[0-9]+\.[0-9]+([a-z]|-alpha[0-9]|-beta[0-9]|-rc[0-9])?)`,
@@ -561,7 +555,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "gcc-binary",
FileGlob: "**/gcc",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// GCC: \(GNU\) 12.3.0'
`GCC: \(GNU\) (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
),
@@ -572,7 +566,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "fluent-bit-binary",
FileGlob: "**/fluent-bit",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// [NUL]3.0.2[NUL]%sFluent Bit
// [NUL]2.2.3[NUL]Fluent Bit
// [NUL]2.2.1[NUL][NUL][NUL]Fluent Bit
@@ -587,7 +581,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "wordpress-cli-binary",
FileGlob: "**/wp",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// wp-cli/wp-cli 2.9.0'
`(?m)wp-cli/wp-cli (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
),
@@ -598,7 +592,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "curl-binary",
FileGlob: "**/curl",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`curl/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
),
Package: "curl",
@@ -608,7 +602,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "lighttpd-binary",
FileGlob: "**/lighttpd",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`\x00lighttpd/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`,
),
Package: "lighttpd",
@@ -618,7 +612,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "proftpd-binary",
FileGlob: "**/proftpd",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`\x00ProFTPD Version (?P<version>[0-9]+\.[0-9]+\.[0-9]+[a-z]?)\x00`,
),
Package: "proftpd",
@@ -628,7 +622,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "zstd-binary",
FileGlob: "**/zstd",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`\x00v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`,
),
Package: "zstd",
@@ -638,7 +632,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "xz-binary",
FileGlob: "**/xz",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`\x00xz \(XZ Utils\) (?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`,
),
Package: "xz",
@@ -648,7 +642,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "gzip-binary",
FileGlob: "**/gzip",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`\x00(?P<version>[0-9]+\.[0-9]+)\x00`,
),
Package: "gzip",
@@ -658,7 +652,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "sqlcipher-binary",
FileGlob: "**/sqlcipher",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`[^0-9]\x00(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`,
),
Package: "sqlcipher",
@@ -668,7 +662,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "jq-binary",
FileGlob: "**/jq",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
`\x00(?P<version>[0-9]{1,3}\.[0-9]{1,3}(\.[0-9]+)?)\x00`,
),
Package: "jq",
@@ -678,7 +672,7 @@ func DefaultClassifiers() []Classifier {
{
Class: "chrome-binary",
FileGlob: "**/chrome",
EvidenceMatcher: FileContentsVersionMatcher(
EvidenceMatcher: m.FileContentsVersionMatcher(
// [NUL]127.0.6533.119[NUL]Default
`\x00(?P<version>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)\x00Default`,
),
@@ -689,18 +683,22 @@ func DefaultClassifiers() []Classifier {
}
}
// in both binaries and shared libraries, the version pattern is [NUL]3.11.2[NUL]
var pythonVersionTemplate = `(?m)\x00(?P<version>{{ .version }}[-._a-zA-Z0-9]*)\x00`
// singleCPE returns a []cpe.CPE with Source: Generated based on the cpe string or panics if the
// cpe string cannot be parsed into valid CPE Attributes
func singleCPE(cpeString string, source ...cpe.Source) []cpe.CPE {
src := cpe.GeneratedSource
if len(source) > 0 {
src = source[0]
}
return []cpe.CPE{
cpe.Must(cpeString, src),
}
}
var libpythonMatcher = fileNameTemplateVersionMatcher(
`(?:.*/|^)libpython(?P<version>[0-9]+(?:\.[0-9]+)+)[a-z]?\.so.*$`,
pythonVersionTemplate,
)
var rubyMatcher = FileContentsVersionMatcher(
// ruby 3.4.0dev (2024-09-15T01:06:11Z master 532af89e3b) [x86_64-linux]
// ruby 3.4.0preview1 (2024-05-16 master 9d69619623) [x86_64-linux]
// ruby 3.3.0rc1 (2023-12-11 master a49643340e) [x86_64-linux]
// ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux]
// ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [x86_64-linux]
`(?m)ruby (?P<version>[0-9]+\.[0-9]+\.[0-9]+((p|preview|rc|dev)[0-9]*)?) `)
func mustPURL(purl string) packageurl.PackageURL {
p, err := packageurl.FromString(purl)
if err != nil {
panic(fmt.Sprintf("invalid PURL: %s", p))
}
return p
}
+18
View File
@@ -0,0 +1,18 @@
package binary
import "github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
// Note: all generic utilities for catalogers have been moved to the internal/binutils package.
// Deprecated: This package is deprecated and will be removed in syft v2
type Classifier = binutils.Classifier
// Deprecated: This package is deprecated and will be removed in syft v2
type EvidenceMatcher = binutils.EvidenceMatcher
// Deprecated: This package is deprecated and will be removed in syft v2
func FileContentsVersionMatcher(
pattern string,
) EvidenceMatcher {
return binutils.FileContentsVersionMatcher(pattern, catalogerName)
}
+2 -2
View File
@@ -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))
})
}
}
+134
View File
@@ -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())
})
}
}
@@ -1,12 +0,0 @@
name: php
offset: unknown
length: unknown
snippetSha256: d39ac8dadf5ba868455c487f1d0bb4c8bec64006fd7e5d76e3e27a26e47e637f
fileSha256: unknown
### byte snippet to follow ###
%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -1,12 +0,0 @@
name: php-fpm
offset: unknown
length: unknown
snippetSha256: d39ac8dadf5ba868455c487f1d0bb4c8bec64006fd7e5d76e3e27a26e47e637f
fileSha256: unknown
### byte snippet to follow ###
%s'
%s,%s
X-Powered-By: PHP/8.2.1
index pointer
PHP_VERSION
@@ -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
@@ -473,7 +473,7 @@ func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) {
}
defer internal.CloseAndLogError(reader, loc.RealPath)
ldpe, err := getLogicalDotnetPE(file.NewLocationReadCloser(loc, reader))
ldpe, err := readLogicalPE(file.NewLocationReadCloser(loc, reader))
if err != nil {
return nil, unknown.New(loc, fmt.Errorf("unable to parse PE file: %w", err))
}
@@ -482,7 +482,7 @@ func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) {
return nil, nil
}
if !ldpe.CLR.hasEvidenceOfCLR() {
if !ldpe.CLR.HasEvidenceOfCLR() {
// this is not a .NET binary
return nil, nil
}
+1 -1
View File
@@ -150,7 +150,7 @@ func newDotnetDepsEntry(lp logicalDepsJSONPackage) pkg.DotnetDepsEntry {
}
}
// newDotnetPortableExecutableEntry creates a portable executable entry from a logicalPE.
// newDotnetPortableExecutableEntry creates a portable executable entry from a File.
func newDotnetPortableExecutableEntry(pe logicalPE) pkg.DotnetPortableExecutableEntry {
return newDotnetPortableExecutableEntryFromMap(pe.VersionResources)
}
+8 -718
View File
@@ -1,740 +1,30 @@
package dotnet
import (
"bytes"
"debug/pe"
"encoding/binary"
"errors"
"fmt"
"io"
"unicode/utf16"
"github.com/scylladb/go-set/strset"
"github.com/scylladb/go-set/u32set"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pe"
)
const peMaxAllowedDirectoryEntries = 0x1000
var imageDirectoryEntryIndexes = []int{
pe.IMAGE_DIRECTORY_ENTRY_RESOURCE, // where version resources are stored
pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR, // where info about the CLR is stored
}
// logicalPE does not directly represent a binary shape to be parsed, instead it represents the
// information of interest extracted from a PE file.
// logicalPE represents a PE file within the context of a .NET project (considering the deps.json file).
type logicalPE struct {
// Location is where the PE file was found
Location file.Location
pe.File
// TargetPath is the path is the deps.json target entry. This is not present in the PE file
// but instead is used in downstream processing to track associations between the PE file and the deps.json file.
TargetPath string
// CLR is the information about the CLR (common language runtime) version found in the PE file which helps
// understand if this executable is even a .NET application.
CLR *clrEvidence
// VersionResources is a map of version resource keys to their values found in the VERSIONINFO resource directory.
VersionResources map[string]string
}
// clrEvidence is basic info about the CLR (common language runtime) version from the COM descriptor.
// This is not a complete representation of the CLR version, but rather a subset of the information that is
// useful to us.
type clrEvidence struct {
// HasClrResourceNames is true if there are CLR resource names found in the PE file (e.g. "CLRDEBUGINFO").
HasClrResourceNames bool
// MajorVersion is the minimum supported major version of the CLR.
MajorVersion uint16
// MinorVersion is the minimum supported minor version of the CLR.
MinorVersion uint16
}
// hasEvidenceOfCLR returns true if the PE file has evidence of a CLR (common language runtime) version.
func (c *clrEvidence) hasEvidenceOfCLR() bool {
return c != nil && (c.MajorVersion != 0 && c.MinorVersion != 0 || c.HasClrResourceNames)
}
type peDosHeader struct {
Magic [2]byte // "MZ"
Unused [58]byte
AddressOfNewEXEHeader uint32 // offset to PE header
}
// peImageCore20 represents the .NET Core 2.0 header structure.
// Source: https://github.com/dotnet/msbuild/blob/9fa9d800dabce3bfcf8365f651f3a713e01f8a85/src/Tasks/NativeMethods.cs#L761-L775
type peImageCore20 struct {
Cb uint32
MajorRuntimeVersion uint16
MinorRuntimeVersion uint16
}
// peImageResourceDirectory represents the resource directory structure.
type peImageResourceDirectory struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
NumberOfNamedEntries uint16
NumberOfIDEntries uint16
}
// peImageResourceDirectoryEntry represents an entry in the resource directory entries.
type peImageResourceDirectoryEntry struct {
Name uint32
OffsetToData uint32
}
// peImageResourceDataEntry is the unit of raw data in the Resource Data area.
type peImageResourceDataEntry struct {
OffsetToData uint32
Size uint32
CodePage uint32
Reserved uint32
}
// peVsFixedFileInfo represents the fixed file information structure.
type peVsFixedFileInfo struct {
Signature uint32
StructVersion uint32
FileVersionMS uint32
FileVersionLS uint32
ProductVersionMS uint32
ProductVersionLS uint32
FileFlagsMask uint32
FileFlags uint32
FileOS uint32
FileType uint32
FileSubtype uint32
FileDateMS uint32
FileDateLS uint32
}
type peVsVersionInfo peLenValLenType
type peStringFileInfo peLenValLenType
type peStringTable peLenValLenType
type peString peLenValLenType
type peLenValLenType struct {
Length uint16
ValueLength uint16
Type uint16
}
type extractedSection struct {
RVA uint32
BaseRVA uint32
Size uint32
Reader *bytes.Reader
}
func (s extractedSection) exists() bool {
return s.RVA != 0 && s.Size != 0
}
func directoryName(i int) string {
switch i {
case pe.IMAGE_DIRECTORY_ENTRY_RESOURCE:
return "Resource"
case pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
return "COM Descriptor"
}
return fmt.Sprintf("Unknown (%d)", i)
}
func getLogicalDotnetPE(f file.LocationReadCloser) (*logicalPE, error) {
r, err := unionreader.GetUnionReader(f)
func readLogicalPE(reader file.LocationReadCloser) (*logicalPE, error) {
peFile, err := pe.Read(reader)
if err != nil {
return nil, err
}
sections, _, err := parsePEFile(r)
if err != nil {
return nil, fmt.Errorf("unable to parse PE sections: %w", err)
}
dirs := u32set.New() // keep track of the RVAs we have already parsed (prevent infinite recursion edge cases)
versionResources := make(map[string]string) // map of version resource keys to their values
resourceNames := strset.New() // set of resource names found in the PE file
err = parseResourceDirectory(sections[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE], dirs, versionResources, resourceNames)
if err != nil {
return nil, err
}
c, err := parseCLR(sections[pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR], resourceNames)
if err != nil {
return nil, fmt.Errorf("unable to parse PE CLR directory: %w", err)
if peFile == nil {
return nil, nil
}
return &logicalPE{
Location: f.Location,
CLR: c,
VersionResources: versionResources,
File: *peFile,
}, nil
}
// parsePEFile creates readers for targeted sections of the binary used by downstream processing.
func parsePEFile(file unionreader.UnionReader) (map[int]*extractedSection, []pe.SectionHeader32, error) {
fileHeader, magic, err := parsePEHeader(file)
if err != nil {
return nil, nil, fmt.Errorf("error parsing PE header: %w", err)
}
soi, headers, err := parseSectionHeaders(file, magic, fileHeader.NumberOfSections)
if err != nil {
return nil, nil, fmt.Errorf("error parsing section headers: %w", err)
}
for i, sec := range soi {
if !sec.exists() {
continue
}
data, err := readDataFromRVA(file, sec.RVA, sec.Size, headers)
if err != nil {
return nil, nil, fmt.Errorf("error reading %q section data: %w", directoryName(i), err)
}
sec.Reader = data
}
return soi, headers, nil
}
// parsePEHeader reads the beginning of a PE formatted file, returning the file header and "magic" indicator
// for downstream logic to determine 32/64 bit parsing.
func parsePEHeader(file unionreader.UnionReader) (*pe.FileHeader, uint16, error) {
var dosHeader peDosHeader
if err := binary.Read(file, binary.LittleEndian, &dosHeader); err != nil {
return nil, 0, fmt.Errorf("error reading DOS header: %w", err)
}
if string(dosHeader.Magic[:]) != "MZ" {
return nil, 0, fmt.Errorf("invalid DOS header magic")
}
peOffset := int64(dosHeader.AddressOfNewEXEHeader)
if _, err := file.Seek(peOffset, io.SeekStart); err != nil {
return nil, 0, fmt.Errorf("error seeking to PE header: %w", err)
}
var signature [4]byte
if err := binary.Read(file, binary.LittleEndian, &signature); err != nil {
return nil, 0, fmt.Errorf("error reading PE signature: %w", err)
}
if !bytes.Equal(signature[:], []byte("PE\x00\x00")) {
return nil, 0, fmt.Errorf("invalid PE signature")
}
var fileHeader pe.FileHeader
if err := binary.Read(file, binary.LittleEndian, &fileHeader); err != nil {
return nil, 0, fmt.Errorf("error reading file header: %w", err)
}
var magic uint16
if err := binary.Read(file, binary.LittleEndian, &magic); err != nil {
return nil, 0, fmt.Errorf("error reading optional header magic: %w", err)
}
// seek back to before reading magic (since that value is in the header)
if _, err := file.Seek(-2, io.SeekCurrent); err != nil {
return nil, 0, fmt.Errorf("error seeking back to before reading magic: %w", err)
}
return &fileHeader, magic, nil
}
// parseSectionHeaders reads the section headers from the PE file and extracts the virtual addresses + section size
// information for the sections of interest. Additionally, all section headers are returned to aid in downstream processing.
func parseSectionHeaders(file unionreader.UnionReader, magic uint16, numberOfSections uint16) (map[int]*extractedSection, []pe.SectionHeader32, error) {
soi := make(map[int]*extractedSection)
switch magic {
case 0x10B: // PE32
var optHeader pe.OptionalHeader32
if err := binary.Read(file, binary.LittleEndian, &optHeader); err != nil {
return nil, nil, fmt.Errorf("error reading optional header (PE32): %w", err)
}
for _, i := range imageDirectoryEntryIndexes {
sectionHeader := optHeader.DataDirectory[i]
if sectionHeader.Size == 0 {
continue
}
soi[i] = &extractedSection{
RVA: sectionHeader.VirtualAddress,
Size: sectionHeader.Size,
}
}
case 0x20B: // PE32+ (64 bit)
var optHeader pe.OptionalHeader64
if err := binary.Read(file, binary.LittleEndian, &optHeader); err != nil {
return nil, nil, fmt.Errorf("error reading optional header (PE32+): %w", err)
}
for _, i := range imageDirectoryEntryIndexes {
sectionHeader := optHeader.DataDirectory[i]
if sectionHeader.Size == 0 {
continue
}
soi[i] = &extractedSection{
RVA: sectionHeader.VirtualAddress,
Size: sectionHeader.Size,
}
}
default:
return nil, nil, fmt.Errorf("unknown optional header magic: 0x%x", magic)
}
// read section headers
headers := make([]pe.SectionHeader32, numberOfSections)
for i := 0; i < int(numberOfSections); i++ {
if err := binary.Read(file, binary.LittleEndian, &headers[i]); err != nil {
return nil, nil, fmt.Errorf("error reading section header: %w", err)
}
}
return soi, headers, nil
}
// parseCLR extracts the CLR (common language runtime) version information from the COM descriptor and makes
// present/not-present determination based on the presence of CLR resource names.
func parseCLR(sec *extractedSection, resourceNames *strset.Set) (*clrEvidence, error) {
hasCLRDebugResourceNames := resourceNames.HasAny("CLRDEBUGINFO")
if sec == nil || sec.Reader == nil {
return &clrEvidence{
HasClrResourceNames: hasCLRDebugResourceNames,
}, nil
}
reader := sec.Reader
var c peImageCore20
if err := binary.Read(reader, binary.LittleEndian, &c); err != nil {
return nil, fmt.Errorf("error reading CLR header: %w", err)
}
return &clrEvidence{
HasClrResourceNames: hasCLRDebugResourceNames,
MajorVersion: c.MajorRuntimeVersion,
MinorVersion: c.MinorRuntimeVersion,
}, nil
}
// rvaToFileOffset is a helper function to convert RVA to file offset using section headers
func rvaToFileOffset(rva uint32, sections []pe.SectionHeader32) (uint32, error) {
for _, section := range sections {
if rva >= section.VirtualAddress && rva < section.VirtualAddress+section.VirtualSize {
return section.PointerToRawData + (rva - section.VirtualAddress), nil
}
}
return 0, fmt.Errorf("RVA 0x%x not found in any section", rva)
}
// readDataFromRVA will read data from a specific RVA in the PE file
func readDataFromRVA(file io.ReadSeeker, rva, size uint32, sections []pe.SectionHeader32) (*bytes.Reader, error) {
if size == 0 {
return nil, fmt.Errorf("zero size specified")
}
offset, err := rvaToFileOffset(rva, sections)
if err != nil {
return nil, err
}
if _, err := file.Seek(int64(offset), io.SeekStart); err != nil {
return nil, fmt.Errorf("error seeking to data: %w", err)
}
data := make([]byte, size)
if _, err := io.ReadFull(file, data); err != nil {
return nil, fmt.Errorf("error reading data: %w", err)
}
return bytes.NewReader(data), nil
}
// parseResourceDirectory recursively parses a PE resource directory. This takes a relative virtual address (offset of
// a piece of data or code relative to the base address), the size of the resource directory, the set of RVAs already
// parsed, and the map to populate discovered version resource values.
//
// .rsrc Section
// +------------------------------+
// | Resource Directory Table |
// +------------------------------+
// | Resource Directory Entries |
// | +------------------------+ |
// | | Subdirectory or Data | |
// | +------------------------+ |
// +------------------------------+
// | Resource Data Entries |
// | +------------------------+ |
// | | Resource Data | |
// | +------------------------+ |
// +------------------------------+
// | Actual Resource Data |
// +------------------------------+
//
// sources:
// - https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-rsrc-section
// - https://learn.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)#pe-file-resources
func parseResourceDirectory(sec *extractedSection, dirs *u32set.Set, fields map[string]string, names *strset.Set) error {
if sec == nil || sec.Size <= 0 {
return nil
}
if sec.Reader == nil {
return errors.New("resource section not found")
}
baseRVA := sec.BaseRVA
if baseRVA == 0 {
baseRVA = sec.RVA
}
offset := int64(sec.RVA - baseRVA)
if _, err := sec.Reader.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("error seeking to directory offset: %w", err)
}
var directoryHeader peImageResourceDirectory
if err := readIntoStruct(sec.Reader, &directoryHeader); err != nil {
return fmt.Errorf("error reading directory header: %w", err)
}
numEntries := int(directoryHeader.NumberOfNamedEntries + directoryHeader.NumberOfIDEntries)
switch {
case numEntries > peMaxAllowedDirectoryEntries:
return fmt.Errorf("too many entries in resource directory: %d", numEntries)
case numEntries == 0:
return fmt.Errorf("no entries in resource directory")
case numEntries < 0:
return fmt.Errorf("invalid number of entries in resource directory: %d", numEntries)
}
for i := 0; i < numEntries; i++ {
var entry peImageResourceDirectoryEntry
entryOffset := offset + int64(binary.Size(directoryHeader)) + int64(i*binary.Size(entry))
if _, err := sec.Reader.Seek(entryOffset, io.SeekStart); err != nil {
log.Tracef("error seeking to PE entry offset: %v", err)
continue
}
if err := readIntoStruct(sec.Reader, &entry); err != nil {
continue
}
if err := processResourceEntry(entry, baseRVA, sec, dirs, fields, names); err != nil {
log.Tracef("error processing resource entry: %v", err)
continue
}
}
return nil
}
func processResourceEntry(entry peImageResourceDirectoryEntry, baseRVA uint32, sec *extractedSection, dirs *u32set.Set, fields map[string]string, names *strset.Set) error {
// if the high bit is set, this is a directory entry, otherwise it is a data entry
isDirectory := entry.OffsetToData&0x80000000 != 0
// note: the offset is relative to the beginning of the resource section, not an RVA
entryOffsetToData := entry.OffsetToData & 0x7FFFFFFF
nameIsString := entry.Name&0x80000000 != 0
nameOffset := entry.Name & 0x7FFFFFFF
// read the string name of the resource directory
if nameIsString {
currentPos, err := sec.Reader.Seek(0, io.SeekCurrent)
if err != nil {
return fmt.Errorf("error getting current reader position: %w", err)
}
if _, err := sec.Reader.Seek(int64(nameOffset), io.SeekStart); err != nil {
return fmt.Errorf("error restoring reader position: %w", err)
}
name, err := readUTF16WithLength(sec.Reader)
if err == nil {
names.Add(name)
}
if _, err := sec.Reader.Seek(currentPos, io.SeekStart); err != nil {
return fmt.Errorf("error restoring reader position: %w", err)
}
}
if isDirectory {
subRVA := baseRVA + entryOffsetToData
if dirs.Has(subRVA) {
// some malware uses recursive PE references to evade analysis
return fmt.Errorf("recursive PE reference detected; skipping directory at baseRVA=0x%x subRVA=0x%x", baseRVA, subRVA)
}
dirs.Add(subRVA)
err := parseResourceDirectory(
&extractedSection{
RVA: subRVA,
BaseRVA: baseRVA,
Size: sec.Size - (sec.RVA - baseRVA),
Reader: sec.Reader,
},
dirs, fields, names)
if err != nil {
return err
}
return nil
}
return parseResourceDataEntry(sec.Reader, baseRVA, baseRVA+entryOffsetToData, sec.Size, fields)
}
func parseResourceDataEntry(reader *bytes.Reader, baseRVA, rva, remainingSize uint32, fields map[string]string) error {
var dataEntry peImageResourceDataEntry
offset := int64(rva - baseRVA)
if _, err := reader.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("error seeking to data entry offset: %w", err)
}
if err := readIntoStruct(reader, &dataEntry); err != nil {
return fmt.Errorf("error reading resource data entry: %w", err)
}
if remainingSize < dataEntry.Size {
return fmt.Errorf("resource data entry size exceeds remaining size")
}
data := make([]byte, dataEntry.Size)
if _, err := reader.Seek(int64(dataEntry.OffsetToData-baseRVA), io.SeekStart); err != nil {
return fmt.Errorf("error seeking to resource data: %w", err)
}
if _, err := reader.Read(data); err != nil {
return fmt.Errorf("error reading resource data: %w", err)
}
return parseVersionResourceSection(bytes.NewReader(data), fields)
}
// parseVersionResourceSection parses a PE version resource section from within a resource directory.
//
// "The main structure in a version resource is the VS_FIXEDFILEINFO structure. Additional structures include the
// VarFileInfo structure to store language information data, and StringFileInfo for user-defined string information.
// All strings in a version resource are in Unicode format. Each block of information is aligned on a DWORD boundary."
//
// "VS_VERSIONINFO" (utf16)
// +---------------------------------------------------+
// | wLength (2 bytes) |
// | wValueLength (2 bytes) |
// | wType (2 bytes) |
// | szKey ("VS_VERSION_INFO") (utf16) |
// | Padding (to DWORD) |
// +---------------------------------------------------+
// | VS_FIXEDFILEINFO (52 bytes) |
// +---------------------------------------------------+
// | "StringFileInfo" (utf16) |
// +---------------------------------------------------+
// | wLength (2 bytes) |
// | wValueLength (2 bytes) |
// | wType (2 bytes) |
// | szKey ("StringFileInfo") (utf16) |
// | Padding (to DWORD) |
// | StringTable |
// | +--------------------------------------------+ |
// | | wLength (2 bytes) | |
// | | wValueLength (2 bytes) | |
// | | wType (2 bytes) | |
// | | szKey ("040904b0") | |
// | | Padding (to DWORD) | |
// | | String | |
// | | +--------------------------------------+ | |
// | | | wLength (2 bytes) | | |
// | | | wValueLength (2 bytes) | | |
// | | | wType (2 bytes) | | |
// | | | szKey ("FileVersion") | | |
// | | | Padding (to DWORD) | | |
// | | | szValue ("15.00.0913.015") | | |
// | | | Padding (to DWORD) | | |
// | +--------------------------------------------+ |
// +---------------------------------------------------+
// | VarFileInfo (utf16) |
// +---------------------------------------------------+
// | (skip!) |
// +---------------------------------------------------+
//
// sources:
// - https://learn.microsoft.com/en-us/windows/win32/menurc/resource-file-formats
// - https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
// - https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/varfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable
func parseVersionResourceSection(reader *bytes.Reader, fields map[string]string) error {
offset := 0
var info peVsVersionInfo
if szKey, err := readIntoStructAndSzKey(reader, &info, &offset); err != nil {
return fmt.Errorf("error reading PE version info: %v", err)
} else if szKey != "VS_VERSION_INFO" {
// this is a resource section, but not the version resources
return nil
}
if err := alignAndSeek(reader, &offset); err != nil {
return fmt.Errorf("error aligning past PE version info: %w", err)
}
var fixedFileInfo peVsFixedFileInfo
if err := readIntoStruct(reader, &fixedFileInfo, &offset); err != nil {
return fmt.Errorf("error reading PE FixedFileInfo: %v", err)
}
for reader.Len() > 0 {
if err := alignAndSeek(reader, &offset); err != nil {
return fmt.Errorf("error seeking to PE StringFileInfo: %w", err)
}
var sfiHeader peStringFileInfo
if szKey, err := readIntoStructAndSzKey(reader, &sfiHeader, &offset); err != nil {
return fmt.Errorf("error reading PE string file info header: %v", err)
} else if szKey != "StringFileInfo" {
// we only care about extracting strings from any string tables, skip this
offset += int(sfiHeader.ValueLength)
continue
}
var stOffset int
// note: the szKey for the prStringTable is the language
var stHeader peStringTable
if _, err := readIntoStructAndSzKey(reader, &stHeader, &offset, &stOffset); err != nil {
return fmt.Errorf("error reading PE string table header: %v", err)
}
for stOffset < int(stHeader.Length) {
var stringHeader peString
if err := readIntoStruct(reader, &stringHeader, &offset, &stOffset); err != nil {
break
}
key := readUTF16(reader, &offset, &stOffset)
if err := alignAndSeek(reader, &offset, &stOffset); err != nil {
return fmt.Errorf("error aligning to next PE string table value: %w", err)
}
var value string
if stringHeader.ValueLength > 0 {
value = readUTF16(reader, &offset, &stOffset)
}
fields[key] = value
if err := alignAndSeek(reader, &offset, &stOffset); err != nil {
return fmt.Errorf("error aligning to next PE string table key: %w", err)
}
}
}
if fields["FileVersion"] == "" {
// we can derive the file version from the fixed file info if it is not already specified as a string entry... neat!
fields["FileVersion"] = fmt.Sprintf("%d.%d.%d.%d",
fixedFileInfo.FileVersionMS>>16, fixedFileInfo.FileVersionMS&0xFFFF,
fixedFileInfo.FileVersionLS>>16, fixedFileInfo.FileVersionLS&0xFFFF)
}
return nil
}
// readIntoStructAndSzKey reads a struct from the reader and updates the offsets if provided, returning the szKey value.
// This is only useful in the context of the resource directory parsing in narrow cases (this is invalid to use outside of that context).
func readIntoStructAndSzKey[T any](reader *bytes.Reader, data *T, offsets ...*int) (string, error) {
if err := readIntoStruct(reader, data, offsets...); err != nil {
return "", err
}
return readUTF16(reader, offsets...), nil
}
// readIntoStruct reads a struct from the reader and updates the offsets if provided.
func readIntoStruct[T any](reader io.Reader, data *T, offsets ...*int) error {
if err := binary.Read(reader, binary.LittleEndian, data); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
for i := range offsets {
*offsets[i] += binary.Size(*data)
}
return nil
}
// alignAndSeek aligns the reader to the next DWORD boundary and seeks to the new offset (updating any provided trackOffsets).
func alignAndSeek(reader io.Seeker, offset *int, trackOffsets ...*int) error {
ogOffset := *offset
*offset = alignToDWORD(*offset)
diff := *offset - ogOffset
for i := range trackOffsets {
*trackOffsets[i] += diff
}
_, err := reader.Seek(int64(*offset), io.SeekStart)
return err
}
// alignToDWORD aligns the offset to the next DWORD boundary (4 byte boundary)
func alignToDWORD(offset int) int {
return (offset + 3) & ^3
}
// readUTF16 is a helper function to read a null-terminated UTF16 string
func readUTF16(reader *bytes.Reader, offsets ...*int) string {
startPos, err := reader.Seek(0, io.SeekCurrent)
if err != nil {
return ""
}
var result []rune
for {
var char uint16
err := binary.Read(reader, binary.LittleEndian, &char)
if err != nil || char == 0 {
break
}
result = append(result, rune(char))
}
// calculate how many bytes we've actually read (including null terminator)
endPos, _ := reader.Seek(0, io.SeekCurrent)
bytesRead := int(endPos - startPos)
for i := range offsets {
*offsets[i] += bytesRead
}
return string(result)
}
// readUTF16WithLength reads a length-prefixed UTF-16 string from reader.
// The first 2 bytes represent the number of UTF-16 code units.
func readUTF16WithLength(reader *bytes.Reader) (string, error) {
var length uint16
if err := binary.Read(reader, binary.LittleEndian, &length); err != nil {
return "", err
}
if length == 0 {
return "", nil
}
// read length UTF-16 code units.
codes := make([]uint16, length)
if err := binary.Read(reader, binary.LittleEndian, &codes); err != nil {
return "", err
}
return string(utf16.Decode(codes)), nil
}
@@ -1,4 +1,4 @@
package binary
package binutils
import (
"bytes"
@@ -34,7 +34,7 @@ type Classifier struct {
// location. If the matcher returns a package, the file will be considered a candidate.
EvidenceMatcher EvidenceMatcher `json:"-"`
// Information below is used to specify the Package information when returned
// The information below is used to specify the Package information when returned
// Package is the name to use for the package
Package string `json:"package"`
@@ -72,16 +72,16 @@ func (cfg Classifier) MarshalJSON() ([]byte, error) {
}
// EvidenceMatcher is a function called to catalog Packages that match some sort of evidence
type EvidenceMatcher func(classifier Classifier, context matcherContext) ([]pkg.Package, error)
type EvidenceMatcher func(classifier Classifier, context MatcherContext) ([]pkg.Package, error)
type matcherContext struct {
resolver file.Resolver
location file.Location
getReader func(resolver matcherContext) (unionreader.UnionReader, error)
type MatcherContext struct {
Resolver file.Resolver
Location file.Location
GetReader func(resolver MatcherContext) (unionreader.UnionReader, error)
}
func evidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher {
return func(classifier Classifier, context matcherContext) ([]pkg.Package, error) {
func EvidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher {
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
for _, matcher := range matchers {
match, err := matcher(classifier, context)
if err != nil {
@@ -95,14 +95,26 @@ func evidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher {
}
}
func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate string) EvidenceMatcher {
type ContextualEvidenceMatchers struct {
CatalogerName string
}
func (c ContextualEvidenceMatchers) FileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate string) EvidenceMatcher {
return FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, c.CatalogerName)
}
func (c ContextualEvidenceMatchers) FileContentsVersionMatcher(pattern string) EvidenceMatcher {
return FileContentsVersionMatcher(pattern, c.CatalogerName)
}
func FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, catalogerName string) EvidenceMatcher {
pat := regexp.MustCompile(fileNamePattern)
return func(classifier Classifier, context matcherContext) ([]pkg.Package, error) {
if !pat.MatchString(context.location.RealPath) {
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
if !pat.MatchString(context.Location.RealPath) {
return nil, nil
}
filepathNamedGroupValues := internal.MatchNamedCaptureGroups(pat, context.location.RealPath)
filepathNamedGroupValues := internal.MatchNamedCaptureGroups(pat, context.Location.RealPath)
// versions like 3.5 should not match any character, but explicit dot
for k, v := range filepathNamedGroupValues {
@@ -135,7 +147,7 @@ func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate stri
return nil, fmt.Errorf("unable to match version: %w", err)
}
p := newClassifierPackage(classifier, context.location, matchMetadata)
p := NewClassifierPackage(classifier, context.Location, matchMetadata, catalogerName)
if p == nil {
return nil, nil
}
@@ -144,9 +156,9 @@ func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate stri
}
}
func FileContentsVersionMatcher(pattern string) EvidenceMatcher {
func FileContentsVersionMatcher(pattern, catalogerName string) EvidenceMatcher {
pat := regexp.MustCompile(pattern)
return func(classifier Classifier, context matcherContext) ([]pkg.Package, error) {
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
contents, err := getReader(context)
if err != nil {
return nil, fmt.Errorf("unable to get read contents for file: %w", err)
@@ -173,7 +185,7 @@ func FileContentsVersionMatcher(pattern string) EvidenceMatcher {
}
}
p := newClassifierPackage(classifier, context.location, matchMetadata)
p := NewClassifierPackage(classifier, context.Location, matchMetadata, catalogerName)
if p == nil {
return nil, nil
}
@@ -182,14 +194,14 @@ func FileContentsVersionMatcher(pattern string) EvidenceMatcher {
}
}
// matchExcluding tests the provided regular expressions against the file, and if matched, DOES NOT return
// MatchExcluding tests the provided regular expressions against the file, and if matched, DOES NOT return
// anything that the matcher would otherwise return
func matchExcluding(matcher EvidenceMatcher, contentPatternsToExclude ...string) EvidenceMatcher {
func MatchExcluding(matcher EvidenceMatcher, contentPatternsToExclude ...string) EvidenceMatcher {
var nonMatchPatterns []*regexp.Regexp
for _, p := range contentPatternsToExclude {
nonMatchPatterns = append(nonMatchPatterns, regexp.MustCompile(p))
}
return func(classifier Classifier, context matcherContext) ([]pkg.Package, error) {
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
contents, err := getReader(context)
if err != nil {
return nil, fmt.Errorf("unable to get read contents for file: %w", err)
@@ -205,9 +217,9 @@ func matchExcluding(matcher EvidenceMatcher, contentPatternsToExclude ...string)
}
}
func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher EvidenceMatcher) EvidenceMatcher {
func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher EvidenceMatcher) EvidenceMatcher {
pat := regexp.MustCompile(sharedLibraryPattern)
return func(classifier Classifier, context matcherContext) (packages []pkg.Package, _ error) {
return func(classifier Classifier, context MatcherContext) (packages []pkg.Package, _ error) {
libs, err := sharedLibraries(context)
if err != nil {
return nil, err
@@ -217,24 +229,24 @@ func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide
continue
}
locations, err := context.resolver.FilesByGlob("**/" + lib)
locations, err := context.Resolver.FilesByGlob("**/" + lib)
if err != nil {
return nil, err
}
for _, libraryLocation := range locations {
newResolver := matcherContext{
resolver: context.resolver,
location: libraryLocation,
getReader: context.getReader,
newResolver := MatcherContext{
Resolver: context.Resolver,
Location: libraryLocation,
GetReader: context.GetReader,
}
newResolver.location = libraryLocation
newResolver.Location = libraryLocation
pkgs, err := sharedLibraryMatcher(classifier, newResolver)
if err != nil {
return nil, err
}
for _, p := range pkgs {
// set the source binary as the first location
locationSet := file.NewLocationSet(context.location)
locationSet := file.NewLocationSet(context.Location)
locationSet.Add(p.Locations.ToSlice()...)
p.Locations = locationSet
meta, _ := p.Metadata.(pkg.BinarySignature)
@@ -242,7 +254,7 @@ func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide
Matches: append([]pkg.ClassifierMatch{
{
Classifier: classifier.Class,
Location: context.location,
Location: context.Location,
},
}, meta.Matches...),
}
@@ -254,19 +266,11 @@ func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide
}
}
func mustPURL(purl string) packageurl.PackageURL {
p, err := packageurl.FromString(purl)
if err != nil {
panic(fmt.Sprintf("invalid PURL: %s", p))
func getReader(context MatcherContext) (unionreader.UnionReader, error) {
if context.GetReader != nil {
return context.GetReader(context)
}
return p
}
func getReader(context matcherContext) (unionreader.UnionReader, error) {
if context.getReader != nil {
return context.getReader(context)
}
reader, err := context.resolver.FileContentsByLocation(context.location) //nolint:gocritic
reader, err := context.Resolver.FileContentsByLocation(context.Location) //nolint:gocritic
if err != nil {
return nil, err
}
@@ -274,32 +278,20 @@ func getReader(context matcherContext) (unionreader.UnionReader, error) {
return unionreader.GetUnionReader(reader)
}
// singleCPE returns a []cpe.CPE with Source: Generated based on the cpe string or panics if the
// cpe string cannot be parsed into valid CPE Attributes
func singleCPE(cpeString string, source ...cpe.Source) []cpe.CPE {
src := cpe.GeneratedSource
if len(source) > 0 {
src = source[0]
}
return []cpe.CPE{
cpe.Must(cpeString, src),
}
}
// sharedLibraries returns a list of all shared libraries found within a binary, currently
// supporting: elf, macho, and windows pe
func sharedLibraries(context matcherContext) ([]string, error) {
func sharedLibraries(context MatcherContext) ([]string, error) {
contents, err := getReader(context)
if err != nil {
return nil, err
}
defer internal.CloseAndLogError(contents, context.location.RealPath)
defer internal.CloseAndLogError(contents, context.Location.RealPath)
e, _ := elf.NewFile(contents)
if e != nil {
symbols, err := e.ImportedLibraries()
if err != nil {
log.Debugf("unable to read elf binary at: %s -- %s", context.location.RealPath, err)
log.Debugf("unable to read elf binary at: %s -- %s", context.Location.RealPath, err)
}
return symbols, nil
}
@@ -311,7 +303,7 @@ func sharedLibraries(context matcherContext) ([]string, error) {
if m != nil {
symbols, err := m.ImportedLibraries()
if err != nil {
log.Debugf("unable to read macho binary at: %s -- %s", context.location.RealPath, err)
log.Debugf("unable to read macho binary at: %s -- %s", context.Location.RealPath, err)
}
return symbols, nil
}
@@ -323,7 +315,7 @@ func sharedLibraries(context matcherContext) ([]string, error) {
if p != nil {
symbols, err := p.ImportedLibraries()
if err != nil {
log.Debugf("unable to read pe binary at: %s -- %s", context.location.RealPath, err)
log.Debugf("unable to read pe binary at: %s -- %s", context.Location.RealPath, err)
}
return symbols, nil
}
@@ -1,4 +1,4 @@
package binary
package binutils
import (
"reflect"
@@ -11,7 +11,7 @@ import (
var emptyPURL = packageurl.PackageURL{}
func newClassifierPackage(classifier Classifier, location file.Location, matchMetadata map[string]string) *pkg.Package {
func NewClassifierPackage(classifier Classifier, location file.Location, matchMetadata map[string]string, catalogerName string) *pkg.Package {
version, ok := matchMetadata["version"]
if !ok {
return nil
@@ -1,4 +1,4 @@
package binary
package binutils
import (
"bytes"
@@ -27,7 +27,7 @@ func Test_ClassifierCPEs(t *testing.T) {
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`, "cataloger-name"),
CPEs: []cpe.CPE{},
},
cpes: nil,
@@ -38,7 +38,7 @@ func Test_ClassifierCPEs(t *testing.T) {
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`, "cataloger-name"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
},
@@ -53,7 +53,7 @@ func Test_ClassifierCPEs(t *testing.T) {
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`, "cataloger-name"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
@@ -70,7 +70,7 @@ func Test_ClassifierCPEs(t *testing.T) {
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version-parts.txt",
EvidenceMatcher: FileContentsVersionMatcher(`(?m)\x00(?P<major>[0-9.]+)\x00(?P<minor>[0-9.]+)\x00(?P<patch>[0-9.]+)\x00`),
EvidenceMatcher: FileContentsVersionMatcher(`(?m)\x00(?P<major>[0-9.]+)\x00(?P<minor>[0-9.]+)\x00(?P<patch>[0-9.]+)\x00`, "cataloger-name"),
CPEs: []cpe.CPE{},
},
cpes: nil,
@@ -84,7 +84,7 @@ func Test_ClassifierCPEs(t *testing.T) {
require.NoError(t, err)
require.Len(t, ls, 1)
pkgs, err := test.classifier.EvidenceMatcher(test.classifier, matcherContext{resolver: resolver, location: ls[0]})
pkgs, err := test.classifier.EvidenceMatcher(test.classifier, MatcherContext{Resolver: resolver, Location: ls[0]})
require.NoError(t, err)
require.Len(t, pkgs, 1)
@@ -113,7 +113,7 @@ func TestClassifier_MarshalJSON(t *testing.T) {
classifier: Classifier{
Class: "class",
FileGlob: "glob",
EvidenceMatcher: FileContentsVersionMatcher(".thing"),
EvidenceMatcher: FileContentsVersionMatcher(".thing", "cataloger-name"),
Package: "pkg",
PURL: packageurl.PackageURL{
Type: "type",
@@ -165,12 +165,12 @@ func TestFileContentsVersionMatcher(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockGetContent := func(context matcherContext) (unionreader.UnionReader, error) {
mockGetContent := func(context MatcherContext) (unionreader.UnionReader, error) {
return unionreader.GetUnionReader(io.NopCloser(bytes.NewBufferString(tt.data)))
}
fn := FileContentsVersionMatcher(tt.pattern)
p, err := fn(Classifier{}, matcherContext{
getReader: mockGetContent,
fn := FileContentsVersionMatcher(tt.pattern, "cataloger-name")
p, err := fn(Classifier{}, MatcherContext{
GetReader: mockGetContent,
})
if err != nil {
@@ -7319,6 +7319,9 @@
"aco-product-labels-for-woocommerce": [
"cpe:2.3:a:acowebs:product_labels_for_woocommerce_\\(sale_badges\\):*:*:*:*:*:wordpress:*:*"
],
"aco-woo-dynamic-pricing": [
"cpe:2.3:a:acowebs:dynamic_pricing_with_discount_rules_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"actionwear-products-sync": [
"cpe:2.3:a:marcoingraiti:actionwear_products_sync:*:*:*:*:*:wordpress:*:*"
],
@@ -7473,6 +7476,10 @@
"admin-side-data-storage-for-contact-form-7": [
"cpe:2.3:a:zestard:admin_side_data_storage_for_contact_form_7:*:*:*:*:*:wordpress:*:*"
],
"admin-site-enhancements": [
"cpe:2.3:a:wpase:admin_and_site_enhancements:*:*:*:*:free:wordpress:*:*",
"cpe:2.3:a:wpase:admin_and_site_enhancements:*:*:*:*:pro:wordpress:*:*"
],
"adminer": [
"cpe:2.3:a:adminer_login_project:adminer_login:*:*:*:*:*:wordpress:*:*"
],
@@ -8109,6 +8116,9 @@
"auto-hyperlink-urls": [
"cpe:2.3:a:auto-hyperlink_urls_project:auto-hyperlink_urls:*:*:*:*:*:wordpress:*:*"
],
"auto-iframe": [
"cpe:2.3:a:toolstack:auto_iframe:*:*:*:*:*:wordpress:*:*"
],
"auto-limit-posts-reloaded": [
"cpe:2.3:a:thefreewindows:auto_limit_posts_reloaded:*:*:*:*:*:wordpress:*:*"
],
@@ -9080,6 +9090,9 @@
"cf7-store-to-db-lite": [
"cpe:2.3:a:accesspressthemes:form_store_to_db:*:*:*:*:*:wordpress:*:*"
],
"cf7-submission-dom-tracking": [
"cpe:2.3:a:apasionados:submission_dom_tracking_for_contact_form_7:*:*:*:*:*:wordpress:*:*"
],
"cf7-widget-elementor": [
"cpe:2.3:a:voidcoders:void_contact_form_7_widget_for_elementor_page_builder:*:*:*:*:*:wordpress:*:*"
],
@@ -9126,7 +9139,7 @@
"cpe:2.3:a:bluecoral:chat_bubble:*:*:*:*:*:wordpress:*:*"
],
"chatbot": [
"cpe:2.3:a:quantumcloud:ai_chatbot:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:quantumcloud:wpbot:*:*:*:*:free:wordpress:*:*"
],
"chatbot-chatgpt": [
"cpe:2.3:a:kognetics:kognetiks_chatbot:*:*:*:*:*:wordpress:*:*",
@@ -9266,12 +9279,19 @@
"cm-download-manager": [
"cpe:2.3:a:cminds:cm_download_manager:*:*:*:*:*:wordpress:*:*"
],
"cm-email-blacklist": [
"cpe:2.3:a:cminds:cm_e-mail_blacklist:*:*:*:*:*:wordpress:*:*"
],
"cm-on-demand-search-and-replace": [
"cpe:2.3:a:cminds:cm_on_demand_search_and_replace:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:cminds:cm_on_demand_search_and_replace:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:cminds:cm_search_and_replace:*:*:*:*:*:wordpress:*:*"
],
"cm-pop-up-banners": [
"cpe:2.3:a:cminds:cm_popup:*:*:*:*:*:wordpress:*:*"
],
"cm-table-of-content": [
"cpe:2.3:a:cminds:cm_table_of_contents:*:*:*:*:*:wordpress:*:*"
],
"cm-video-lesson-manager": [
"cpe:2.3:a:creativemindssolutions:video_lessons_manager:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:creativemindssolutions:video_lessons_manager_pro:*:*:*:*:*:wordpress:*:*"
@@ -9711,7 +9731,7 @@
"cpe:2.3:a:marcorulicke:coru_lfmember:*:*:*:*:*:wordpress:*:*"
],
"cost-calculator-builder": [
"cpe:2.3:a:stylemixthemes:cost_calculator_builder:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:stylemixthemes:cost_calculator_builder:*:*:*:*:free:wordpress:*:*"
],
"countdown-block": [
"cpe:2.3:a:wpdeveloper:countdown_block:*:*:*:*:*:wordpress:*:*"
@@ -10568,6 +10588,9 @@
"easy-newsletter-signups": [
"cpe:2.3:a:alphabpo:easy_newsletter_signups:*:*:*:*:*:wordpress:*:*"
],
"easy-notify-lite": [
"cpe:2.3:a:ghozylab:popup_builder:*:*:*:*:*:wordpress:*:*"
],
"easy-paypal-donation": [
"cpe:2.3:a:wpplugin:accept_donations_with_paypal:*:*:*:*:*:wordpress:*:*"
],
@@ -10610,6 +10633,9 @@
"easy-table": [
"cpe:2.3:a:easy_table_project:easy_table:*:*:*:*:*:wordpress:*:*"
],
"easy-table-of-contents": [
"cpe:2.3:a:magazine3:easy_table_of_contents:*:*:*:*:*:wordpress:*:*"
],
"easy-testimonial-manager": [
"cpe:2.3:a:easy_testimonial_manager_project:easy_testimonial_manager:*:*:*:*:*:wordpress:*:*"
],
@@ -12404,7 +12430,8 @@
"cpe:2.3:a:idxbroker:impress_for_idx_broker:*:*:*:*:platinum:wordpress:*:*"
],
"if-so": [
"cpe:2.3:a:if-so:dynamic_content_personalization:*:*:*:*:*:wordpress:*:*"
"cpe:2.3:a:if-so:dynamic_content_personalization:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:if-so:if-so:*:*:*:*:*:wordpress:*:*"
],
"ifeature-slider": [
"cpe:2.3:a:cyberchimps:ifeature_slider:*:*:*:*:*:wordpress:*:*"
@@ -12494,6 +12521,9 @@
"image-vertical-reel-scroll-slideshow": [
"cpe:2.3:a:gopiplus:image_vertical_reel_scroll_slideshow:*:*:*:*:*:wordpress:*:*"
],
"image-widget": [
"cpe:2.3:a:stellarwp:image_widget:*:*:*:*:*:wordpress:*:*"
],
"imageboss": [
"cpe:2.3:a:imageboss:imageboss:*:*:*:*:*:wordpress:*:*"
],
@@ -12782,6 +12812,9 @@
"jetformbuilder": [
"cpe:2.3:a:crocoblock:jetformbuilder:*:*:*:*:*:wordpress:*:*"
],
"jetpack": [
"cpe:2.3:a:automattic:jetpack:*:*:*:*:*:wordpress:*:*"
],
"jetwidgets-for-elementor": [
"cpe:2.3:a:crocoblock:jetwidgets_for_elementor:*:*:*:*:*:wordpress:*:*"
],
@@ -13723,6 +13756,9 @@
"message-ticker": [
"cpe:2.3:a:gopiplus:message_ticker:*:*:*:*:*:wordpress:*:*"
],
"meta-box": [
"cpe:2.3:a:metabox:meta_box:*:*:*:*:*:wordpress:*:*"
],
"meta-store-elements": [
"cpe:2.3:a:mysticalthemes:meta_store_elements:*:*:*:*:*:wordpress:*:*"
],
@@ -13984,6 +14020,9 @@
"cpe:2.3:a:codepeople:music_store:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:musicstore:music_store:*:*:*:*:*:wordpress:*:*"
],
"muslim-prayer-time-bd": [
"cpe:2.3:a:realwebcare:muslim_prayer_time_bd:*:*:*:*:*:wordpress:*:*"
],
"mw-wp-form": [
"cpe:2.3:a:web-soudan:mw_wp_form:*:*:*:*:*:wordpress:*:*"
],
@@ -14426,6 +14465,9 @@
"opening-hours": [
"cpe:2.3:a:designextreme:we\\'re_open\\!:*:*:*:*:*:wordpress:*:*"
],
"openpgp-form-encryption": [
"cpe:2.3:a:arnesonium:openpgp_form_encryption:*:*:*:*:*:wordpress:*:*"
],
"ops-robots-txt": [
"cpe:2.3:a:ops-robots-txt_project:ops-robots-txt:*:*:*:*:*:wordpress:*:*"
],
@@ -14769,6 +14811,9 @@
"cpe:2.3:a:greentreelabs:gallery_photoblocks:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wpchill:gallery_photoblocks:*:*:*:*:*:wordpress:*:*"
],
"photoshow": [
"cpe:2.3:a:codepeople:smart_image_gallery:*:*:*:*:*:wordpress:*:*"
],
"photospace": [
"cpe:2.3:a:photospace_gallery_project:photospace_gallery:*:*:*:*:*:wordpress:*:*"
],
@@ -15236,6 +15281,9 @@
"product-designer": [
"cpe:2.3:a:pickplugins:product_designer:*:*:*:*:*:wordpress:*:*"
],
"product-enquiry-for-woocommerce": [
"cpe:2.3:a:wisdmlabs:product_enquiry_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
"product-expiry-for-woocommerce": [
"cpe:2.3:a:webcodingplace:product_expiry_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
@@ -15460,6 +15508,7 @@
"cpe:2.3:a:softlabbd:radio_player:*:*:*:*:*:wordpress:*:*"
],
"rafflepress": [
"cpe:2.3:a:rafflepress:rafflepress:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:seedprod:rafflepress:*:*:*:*:*:wordpress:*:*"
],
"random-banner": [
@@ -16306,6 +16355,9 @@
"simple-ajax-chat": [
"cpe:2.3:a:plugin-planet:simple_ajax_chat:*:*:*:*:*:wordpress:*:*"
],
"simple-al-slider": [
"cpe:2.3:a:alexdtn:simple_al_slider:*:*:*:*:*:wordpress:*:*"
],
"simple-author-box": [
"cpe:2.3:a:webfactoryltd:simple_author_box:*:*:*:*:*:wordpress:*:*"
],
@@ -16322,6 +16374,9 @@
"simple-bitcoin-faucets": [
"cpe:2.3:a:simple_bitcoin_faucets_project:simple_bitcoin_faucets:*:*:*:*:*:wordpress:*:*"
],
"simple-buttons-creator": [
"cpe:2.3:a:robbychen:simple_buttons_creator:*:*:*:*:*:wordpress:*:*"
],
"simple-calendar-for-elementor": [
"cpe:2.3:a:migaweb:simple_calendar_for_elementor:*:*:*:*:*:wordpress:*:*"
],
@@ -16494,6 +16549,9 @@
"simple-site-verify": [
"cpe:2.3:a:idoweb:simple_site_verify:*:*:*:*:*:wordpress:*:*"
],
"simple-sitemap": [
"cpe:2.3:a:wpgoplugins:simple_sitemap:*:*:*:*:free:wordpress:*:*"
],
"simple-slug-translate": [
"cpe:2.3:a:simple_slug_translate_project:simple_slug_translate:*:*:*:*:*:wordpress:*:*"
],
@@ -17233,12 +17291,18 @@
"surveys": [
"cpe:2.3:a:surveys_project:surveys:*:*:*:*:*:wordpress:*:*"
],
"svg-block": [
"cpe:2.3:a:boldblocks:svg_block:*:*:*:*:*:wordpress:*:*"
],
"svg-support": [
"cpe:2.3:a:benbodhi:svg_support:*:*:*:*:*:wordpress:*:*"
],
"svg-vector-icon-plugin": [
"cpe:2.3:a:wp_svg_icons_project:wp_svg_icons:*:*:*:*:*:wordpress:*:*"
],
"svgator": [
"cpe:2.3:a:svgator:svgator:*:*:*:*:*:wordpress:*:*"
],
"svgmagic": [
"cpe:2.3:a:andibauer:svgmagic:*:*:*:*:*:wordpress:*:*"
],
@@ -17684,6 +17748,9 @@
"tripetto": [
"cpe:2.3:a:tripetto:tripetto:*:*:*:*:*:wordpress:*:*"
],
"truebooker-appointment-booking": [
"cpe:2.3:a:themetechmount:truebooker:*:*:*:*:*:wordpress:*:*"
],
"ts-webfonts-for-conoha": [
"cpe:2.3:a:gmo:typesquare_webfonts_for_conoha:*:*:*:*:*:wordpress:*:*"
],
@@ -17914,6 +17981,9 @@
"underconstruction": [
"cpe:2.3:a:underconstruction_project:underconstruction:*:*:*:*:*:wordpress:*:*"
],
"ungallery": [
"cpe:2.3:a:mmond:ungallery:*:*:*:*:*:wordpress:*:*"
],
"uninstall": [
"cpe:2.3:a:wordpress_uninstall_project:wordpress_uninstall:*:*:*:*:*:wordpress:*:*"
],
@@ -18697,6 +18767,9 @@
"woo-tranzila-gateway": [
"cpe:2.3:a:antonbond:woocommerce_tranzila_payment_gateway:*:*:*:*:*:wordpress:*:*"
],
"woo-ups-pickup": [
"cpe:2.3:a:ashamil:opsi_israel_domestic_shipments:*:*:*:*:*:wordpress:*:*"
],
"woo-variation-gallery": [
"cpe:2.3:a:getwooplugins:additional_variation_images_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
@@ -19971,6 +20044,9 @@
"wp-seo-redirect-301": [
"cpe:2.3:a:wp_seo_redirect_301_project:wp_seo_redirect_301:*:*:*:*:*:wordpress:*:*"
],
"wp-seo-structured-data-schema": [
"cpe:2.3:a:wpsemplugins:wp_seo_structured_data_schema:*:*:*:*:free:wordpress:*:*"
],
"wp-seo-tags": [
"cpe:2.3:a:wp_seo_tags_project:wp_seo_tags:*:*:*:*:*:wordpress:*:*"
],
@@ -20284,6 +20360,9 @@
"wp-webauthn": [
"cpe:2.3:a:axton:wp-webauthn:*:*:*:*:*:wordpress:*:*"
],
"wp-webinarsystem": [
"cpe:2.3:a:webinarpress:webinarpress:*:*:*:*:*:wordpress:*:*"
],
"wp-whatsapp": [
"cpe:2.3:a:ninjateam:wp_chat_app:*:*:*:*:*:wordpress:*:*"
],
@@ -20305,6 +20384,9 @@
"wp-worthy": [
"cpe:2.3:a:tiggerswelt:worthy:*:*:*:*:*:wordpress:*:*"
],
"wp-yadisk-files": [
"cpe:2.3:a:antongorodezkiy:yadisk_files:*:*:*:*:*:wordpress:*:*"
],
"wp-yelp-review-slider": [
"cpe:2.3:a:ljapps:wp_yelp_review_slider:*:*:*:*:*:wordpress:*:*"
],
@@ -20669,6 +20751,10 @@
"yith-maintenance-mode": [
"cpe:2.3:a:yithemes:yith_maintenance_mode:*:*:*:*:*:wordpress:*:*"
],
"yith-woocommerce-ajax-search": [
"cpe:2.3:a:yithemes:yith_woocommerce_ajax_search:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:yithemes:yith_woocommerce_ajax_search:*:*:*:*:free:wordpress:*:*"
],
"yith-woocommerce-gift-cards": [
"cpe:2.3:a:yithemes:yith_woocommerce_gift_cards:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:yithemes:yith_woocommerce_gift_cards:*:*:*:*:free:wordpress:*:*"
+736
View File
@@ -0,0 +1,736 @@
package pe
import (
"bytes"
"debug/pe"
"encoding/binary"
"errors"
"fmt"
"io"
"unicode/utf16"
"github.com/scylladb/go-set/strset"
"github.com/scylladb/go-set/u32set"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
)
const peMaxAllowedDirectoryEntries = 0x1000
var imageDirectoryEntryIndexes = []int{
pe.IMAGE_DIRECTORY_ENTRY_RESOURCE, // where version resources are stored
pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR, // where info about the CLR is stored
}
// File does not directly represent a binary shape to be parsed, instead it represents the
// information of interest extracted from a PE file.
type File struct {
// Location is where the PE file was found
Location file.Location
// CLR is the information about the CLR (common language runtime) version found in the PE file which helps
// understand if this executable is even a .NET application.
CLR *CLREvidence
// VersionResources is a map of version resource keys to their values found in the VERSIONINFO resource directory.
VersionResources map[string]string
}
// CLREvidence is basic info about the CLR (common language runtime) version from the COM descriptor.
// This is not a complete representation of the CLR version, but rather a subset of the information that is
// useful to us.
type CLREvidence struct {
// HasClrResourceNames is true if there are CLR resource names found in the PE file (e.g. "CLRDEBUGINFO").
HasClrResourceNames bool
// MajorVersion is the minimum supported major version of the CLR.
MajorVersion uint16
// MinorVersion is the minimum supported minor version of the CLR.
MinorVersion uint16
}
// HasEvidenceOfCLR returns true if the PE file has evidence of a CLR (common language runtime) version.
func (c *CLREvidence) HasEvidenceOfCLR() bool {
return c != nil && (c.MajorVersion != 0 && c.MinorVersion != 0 || c.HasClrResourceNames)
}
type peDosHeader struct {
Magic [2]byte // "MZ"
Unused [58]byte
AddressOfNewEXEHeader uint32 // offset to PE header
}
// peImageCore20 represents the .NET Core 2.0 header structure.
// Source: https://github.com/dotnet/msbuild/blob/9fa9d800dabce3bfcf8365f651f3a713e01f8a85/src/Tasks/NativeMethods.cs#L761-L775
type peImageCore20 struct {
Cb uint32
MajorRuntimeVersion uint16
MinorRuntimeVersion uint16
}
// peImageResourceDirectory represents the resource directory structure.
type peImageResourceDirectory struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
NumberOfNamedEntries uint16
NumberOfIDEntries uint16
}
// peImageResourceDirectoryEntry represents an entry in the resource directory entries.
type peImageResourceDirectoryEntry struct {
Name uint32
OffsetToData uint32
}
// peImageResourceDataEntry is the unit of raw data in the Resource Data area.
type peImageResourceDataEntry struct {
OffsetToData uint32
Size uint32
CodePage uint32
Reserved uint32
}
// peVsFixedFileInfo represents the fixed file information structure.
type peVsFixedFileInfo struct {
Signature uint32
StructVersion uint32
FileVersionMS uint32
FileVersionLS uint32
ProductVersionMS uint32
ProductVersionLS uint32
FileFlagsMask uint32
FileFlags uint32
FileOS uint32
FileType uint32
FileSubtype uint32
FileDateMS uint32
FileDateLS uint32
}
type peVsVersionInfo peLenValLenType
type peStringFileInfo peLenValLenType
type peStringTable peLenValLenType
type peString peLenValLenType
type peLenValLenType struct {
Length uint16
ValueLength uint16
Type uint16
}
type extractedSection struct {
RVA uint32
BaseRVA uint32
Size uint32
Reader *bytes.Reader
}
func (s extractedSection) exists() bool {
return s.RVA != 0 && s.Size != 0
}
func directoryName(i int) string {
switch i {
case pe.IMAGE_DIRECTORY_ENTRY_RESOURCE:
return "Resource"
case pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR:
return "COM Descriptor"
}
return fmt.Sprintf("Unknown (%d)", i)
}
func Read(f file.LocationReadCloser) (*File, error) {
r, err := unionreader.GetUnionReader(f)
if err != nil {
return nil, err
}
sections, _, err := parsePEFile(r)
if err != nil {
return nil, fmt.Errorf("unable to parse PE sections: %w", err)
}
dirs := u32set.New() // keep track of the RVAs we have already parsed (prevent infinite recursion edge cases)
versionResources := make(map[string]string) // map of version resource keys to their values
resourceNames := strset.New() // set of resource names found in the PE file
err = parseResourceDirectory(sections[pe.IMAGE_DIRECTORY_ENTRY_RESOURCE], dirs, versionResources, resourceNames)
if err != nil {
return nil, err
}
c, err := parseCLR(sections[pe.IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR], resourceNames)
if err != nil {
return nil, fmt.Errorf("unable to parse PE CLR directory: %w", err)
}
return &File{
Location: f.Location,
CLR: c,
VersionResources: versionResources,
}, nil
}
// parsePEFile creates readers for targeted sections of the binary used by downstream processing.
func parsePEFile(file unionreader.UnionReader) (map[int]*extractedSection, []pe.SectionHeader32, error) {
fileHeader, magic, err := parsePEHeader(file)
if err != nil {
return nil, nil, fmt.Errorf("error parsing PE header: %w", err)
}
soi, headers, err := parseSectionHeaders(file, magic, fileHeader.NumberOfSections)
if err != nil {
return nil, nil, fmt.Errorf("error parsing section headers: %w", err)
}
for i, sec := range soi {
if !sec.exists() {
continue
}
data, err := readDataFromRVA(file, sec.RVA, sec.Size, headers)
if err != nil {
return nil, nil, fmt.Errorf("error reading %q section data: %w", directoryName(i), err)
}
sec.Reader = data
}
return soi, headers, nil
}
// parsePEHeader reads the beginning of a PE formatted file, returning the file header and "magic" indicator
// for downstream logic to determine 32/64 bit parsing.
func parsePEHeader(file unionreader.UnionReader) (*pe.FileHeader, uint16, error) {
var dosHeader peDosHeader
if err := binary.Read(file, binary.LittleEndian, &dosHeader); err != nil {
return nil, 0, fmt.Errorf("error reading DOS header: %w", err)
}
if string(dosHeader.Magic[:]) != "MZ" {
return nil, 0, fmt.Errorf("invalid DOS header magic")
}
peOffset := int64(dosHeader.AddressOfNewEXEHeader)
if _, err := file.Seek(peOffset, io.SeekStart); err != nil {
return nil, 0, fmt.Errorf("error seeking to PE header: %w", err)
}
var signature [4]byte
if err := binary.Read(file, binary.LittleEndian, &signature); err != nil {
return nil, 0, fmt.Errorf("error reading PE signature: %w", err)
}
if !bytes.Equal(signature[:], []byte("PE\x00\x00")) {
return nil, 0, fmt.Errorf("invalid PE signature")
}
var fileHeader pe.FileHeader
if err := binary.Read(file, binary.LittleEndian, &fileHeader); err != nil {
return nil, 0, fmt.Errorf("error reading file header: %w", err)
}
var magic uint16
if err := binary.Read(file, binary.LittleEndian, &magic); err != nil {
return nil, 0, fmt.Errorf("error reading optional header magic: %w", err)
}
// seek back to before reading magic (since that value is in the header)
if _, err := file.Seek(-2, io.SeekCurrent); err != nil {
return nil, 0, fmt.Errorf("error seeking back to before reading magic: %w", err)
}
return &fileHeader, magic, nil
}
// parseSectionHeaders reads the section headers from the PE file and extracts the virtual addresses + section size
// information for the sections of interest. Additionally, all section headers are returned to aid in downstream processing.
func parseSectionHeaders(file unionreader.UnionReader, magic uint16, numberOfSections uint16) (map[int]*extractedSection, []pe.SectionHeader32, error) {
soi := make(map[int]*extractedSection)
switch magic {
case 0x10B: // PE32
var optHeader pe.OptionalHeader32
if err := binary.Read(file, binary.LittleEndian, &optHeader); err != nil {
return nil, nil, fmt.Errorf("error reading optional header (PE32): %w", err)
}
for _, i := range imageDirectoryEntryIndexes {
sectionHeader := optHeader.DataDirectory[i]
if sectionHeader.Size == 0 {
continue
}
soi[i] = &extractedSection{
RVA: sectionHeader.VirtualAddress,
Size: sectionHeader.Size,
}
}
case 0x20B: // PE32+ (64 bit)
var optHeader pe.OptionalHeader64
if err := binary.Read(file, binary.LittleEndian, &optHeader); err != nil {
return nil, nil, fmt.Errorf("error reading optional header (PE32+): %w", err)
}
for _, i := range imageDirectoryEntryIndexes {
sectionHeader := optHeader.DataDirectory[i]
if sectionHeader.Size == 0 {
continue
}
soi[i] = &extractedSection{
RVA: sectionHeader.VirtualAddress,
Size: sectionHeader.Size,
}
}
default:
return nil, nil, fmt.Errorf("unknown optional header magic: 0x%x", magic)
}
// read section headers
headers := make([]pe.SectionHeader32, numberOfSections)
for i := 0; i < int(numberOfSections); i++ {
if err := binary.Read(file, binary.LittleEndian, &headers[i]); err != nil {
return nil, nil, fmt.Errorf("error reading section header: %w", err)
}
}
return soi, headers, nil
}
// parseCLR extracts the CLR (common language runtime) version information from the COM descriptor and makes
// present/not-present determination based on the presence of CLR resource names.
func parseCLR(sec *extractedSection, resourceNames *strset.Set) (*CLREvidence, error) {
hasCLRDebugResourceNames := resourceNames.HasAny("CLRDEBUGINFO")
if sec == nil || sec.Reader == nil {
return &CLREvidence{
HasClrResourceNames: hasCLRDebugResourceNames,
}, nil
}
reader := sec.Reader
var c peImageCore20
if err := binary.Read(reader, binary.LittleEndian, &c); err != nil {
return nil, fmt.Errorf("error reading CLR header: %w", err)
}
return &CLREvidence{
HasClrResourceNames: hasCLRDebugResourceNames,
MajorVersion: c.MajorRuntimeVersion,
MinorVersion: c.MinorRuntimeVersion,
}, nil
}
// rvaToFileOffset is a helper function to convert RVA to file offset using section headers
func rvaToFileOffset(rva uint32, sections []pe.SectionHeader32) (uint32, error) {
for _, section := range sections {
if rva >= section.VirtualAddress && rva < section.VirtualAddress+section.VirtualSize {
return section.PointerToRawData + (rva - section.VirtualAddress), nil
}
}
return 0, fmt.Errorf("RVA 0x%x not found in any section", rva)
}
// readDataFromRVA will read data from a specific RVA in the PE file
func readDataFromRVA(file io.ReadSeeker, rva, size uint32, sections []pe.SectionHeader32) (*bytes.Reader, error) {
if size == 0 {
return nil, fmt.Errorf("zero size specified")
}
offset, err := rvaToFileOffset(rva, sections)
if err != nil {
return nil, err
}
if _, err := file.Seek(int64(offset), io.SeekStart); err != nil {
return nil, fmt.Errorf("error seeking to data: %w", err)
}
data := make([]byte, size)
if _, err := io.ReadFull(file, data); err != nil {
return nil, fmt.Errorf("error reading data: %w", err)
}
return bytes.NewReader(data), nil
}
// parseResourceDirectory recursively parses a PE resource directory. This takes a relative virtual address (offset of
// a piece of data or code relative to the base address), the size of the resource directory, the set of RVAs already
// parsed, and the map to populate discovered version resource values.
//
// .rsrc Section
// +------------------------------+
// | Resource Directory Table |
// +------------------------------+
// | Resource Directory Entries |
// | +------------------------+ |
// | | Subdirectory or Data | |
// | +------------------------+ |
// +------------------------------+
// | Resource Data Entries |
// | +------------------------+ |
// | | Resource Data | |
// | +------------------------+ |
// +------------------------------+
// | Actual Resource Data |
// +------------------------------+
//
// sources:
// - https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-rsrc-section
// - https://learn.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10)#pe-file-resources
func parseResourceDirectory(sec *extractedSection, dirs *u32set.Set, fields map[string]string, names *strset.Set) error {
if sec == nil || sec.Size <= 0 {
return nil
}
if sec.Reader == nil {
return errors.New("resource section not found")
}
baseRVA := sec.BaseRVA
if baseRVA == 0 {
baseRVA = sec.RVA
}
offset := int64(sec.RVA - baseRVA)
if _, err := sec.Reader.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("error seeking to directory offset: %w", err)
}
var directoryHeader peImageResourceDirectory
if err := readIntoStruct(sec.Reader, &directoryHeader); err != nil {
return fmt.Errorf("error reading directory header: %w", err)
}
numEntries := int(directoryHeader.NumberOfNamedEntries + directoryHeader.NumberOfIDEntries)
switch {
case numEntries > peMaxAllowedDirectoryEntries:
return fmt.Errorf("too many entries in resource directory: %d", numEntries)
case numEntries == 0:
return fmt.Errorf("no entries in resource directory")
case numEntries < 0:
return fmt.Errorf("invalid number of entries in resource directory: %d", numEntries)
}
for i := 0; i < numEntries; i++ {
var entry peImageResourceDirectoryEntry
entryOffset := offset + int64(binary.Size(directoryHeader)) + int64(i*binary.Size(entry))
if _, err := sec.Reader.Seek(entryOffset, io.SeekStart); err != nil {
log.Tracef("error seeking to PE entry offset: %v", err)
continue
}
if err := readIntoStruct(sec.Reader, &entry); err != nil {
continue
}
if err := processResourceEntry(entry, baseRVA, sec, dirs, fields, names); err != nil {
log.Tracef("error processing resource entry: %v", err)
continue
}
}
return nil
}
func processResourceEntry(entry peImageResourceDirectoryEntry, baseRVA uint32, sec *extractedSection, dirs *u32set.Set, fields map[string]string, names *strset.Set) error {
// if the high bit is set, this is a directory entry, otherwise it is a data entry
isDirectory := entry.OffsetToData&0x80000000 != 0
// note: the offset is relative to the beginning of the resource section, not an RVA
entryOffsetToData := entry.OffsetToData & 0x7FFFFFFF
nameIsString := entry.Name&0x80000000 != 0
nameOffset := entry.Name & 0x7FFFFFFF
// read the string name of the resource directory
if nameIsString {
currentPos, err := sec.Reader.Seek(0, io.SeekCurrent)
if err != nil {
return fmt.Errorf("error getting current reader position: %w", err)
}
if _, err := sec.Reader.Seek(int64(nameOffset), io.SeekStart); err != nil {
return fmt.Errorf("error restoring reader position: %w", err)
}
name, err := readUTF16WithLength(sec.Reader)
if err == nil {
names.Add(name)
}
if _, err := sec.Reader.Seek(currentPos, io.SeekStart); err != nil {
return fmt.Errorf("error restoring reader position: %w", err)
}
}
if isDirectory {
subRVA := baseRVA + entryOffsetToData
if dirs.Has(subRVA) {
// some malware uses recursive PE references to evade analysis
return fmt.Errorf("recursive PE reference detected; skipping directory at baseRVA=0x%x subRVA=0x%x", baseRVA, subRVA)
}
dirs.Add(subRVA)
err := parseResourceDirectory(
&extractedSection{
RVA: subRVA,
BaseRVA: baseRVA,
Size: sec.Size - (sec.RVA - baseRVA),
Reader: sec.Reader,
},
dirs, fields, names)
if err != nil {
return err
}
return nil
}
return parseResourceDataEntry(sec.Reader, baseRVA, baseRVA+entryOffsetToData, sec.Size, fields)
}
func parseResourceDataEntry(reader *bytes.Reader, baseRVA, rva, remainingSize uint32, fields map[string]string) error {
var dataEntry peImageResourceDataEntry
offset := int64(rva - baseRVA)
if _, err := reader.Seek(offset, io.SeekStart); err != nil {
return fmt.Errorf("error seeking to data entry offset: %w", err)
}
if err := readIntoStruct(reader, &dataEntry); err != nil {
return fmt.Errorf("error reading resource data entry: %w", err)
}
if remainingSize < dataEntry.Size {
return fmt.Errorf("resource data entry size exceeds remaining size")
}
data := make([]byte, dataEntry.Size)
if _, err := reader.Seek(int64(dataEntry.OffsetToData-baseRVA), io.SeekStart); err != nil {
return fmt.Errorf("error seeking to resource data: %w", err)
}
if _, err := reader.Read(data); err != nil {
return fmt.Errorf("error reading resource data: %w", err)
}
return parseVersionResourceSection(bytes.NewReader(data), fields)
}
// parseVersionResourceSection parses a PE version resource section from within a resource directory.
//
// "The main structure in a version resource is the VS_FIXEDFILEINFO structure. Additional structures include the
// VarFileInfo structure to store language information data, and StringFileInfo for user-defined string information.
// All strings in a version resource are in Unicode format. Each block of information is aligned on a DWORD boundary."
//
// "VS_VERSIONINFO" (utf16)
// +---------------------------------------------------+
// | wLength (2 bytes) |
// | wValueLength (2 bytes) |
// | wType (2 bytes) |
// | szKey ("VS_VERSION_INFO") (utf16) |
// | Padding (to DWORD) |
// +---------------------------------------------------+
// | VS_FIXEDFILEINFO (52 bytes) |
// +---------------------------------------------------+
// | "StringFileInfo" (utf16) |
// +---------------------------------------------------+
// | wLength (2 bytes) |
// | wValueLength (2 bytes) |
// | wType (2 bytes) |
// | szKey ("StringFileInfo") (utf16) |
// | Padding (to DWORD) |
// | StringTable |
// | +--------------------------------------------+ |
// | | wLength (2 bytes) | |
// | | wValueLength (2 bytes) | |
// | | wType (2 bytes) | |
// | | szKey ("040904b0") | |
// | | Padding (to DWORD) | |
// | | String | |
// | | +--------------------------------------+ | |
// | | | wLength (2 bytes) | | |
// | | | wValueLength (2 bytes) | | |
// | | | wType (2 bytes) | | |
// | | | szKey ("FileVersion") | | |
// | | | Padding (to DWORD) | | |
// | | | szValue ("15.00.0913.015") | | |
// | | | Padding (to DWORD) | | |
// | +--------------------------------------------+ |
// +---------------------------------------------------+
// | VarFileInfo (utf16) |
// +---------------------------------------------------+
// | (skip!) |
// +---------------------------------------------------+
//
// sources:
// - https://learn.microsoft.com/en-us/windows/win32/menurc/resource-file-formats
// - https://learn.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
// - https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/varfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo
// - https://learn.microsoft.com/en-us/windows/win32/menurc/stringtable
func parseVersionResourceSection(reader *bytes.Reader, fields map[string]string) error {
offset := 0
var info peVsVersionInfo
if szKey, err := readIntoStructAndSzKey(reader, &info, &offset); err != nil {
return fmt.Errorf("error reading PE version info: %v", err)
} else if szKey != "VS_VERSION_INFO" {
// this is a resource section, but not the version resources
return nil
}
if err := alignAndSeek(reader, &offset); err != nil {
return fmt.Errorf("error aligning past PE version info: %w", err)
}
var fixedFileInfo peVsFixedFileInfo
if err := readIntoStruct(reader, &fixedFileInfo, &offset); err != nil {
return fmt.Errorf("error reading PE FixedFileInfo: %v", err)
}
for reader.Len() > 0 {
if err := alignAndSeek(reader, &offset); err != nil {
return fmt.Errorf("error seeking to PE StringFileInfo: %w", err)
}
var sfiHeader peStringFileInfo
if szKey, err := readIntoStructAndSzKey(reader, &sfiHeader, &offset); err != nil {
return fmt.Errorf("error reading PE string file info header: %v", err)
} else if szKey != "StringFileInfo" {
// we only care about extracting strings from any string tables, skip this
offset += int(sfiHeader.ValueLength)
continue
}
var stOffset int
// note: the szKey for the prStringTable is the language
var stHeader peStringTable
if _, err := readIntoStructAndSzKey(reader, &stHeader, &offset, &stOffset); err != nil {
return fmt.Errorf("error reading PE string table header: %v", err)
}
for stOffset < int(stHeader.Length) {
var stringHeader peString
if err := readIntoStruct(reader, &stringHeader, &offset, &stOffset); err != nil {
break
}
key := readUTF16(reader, &offset, &stOffset)
if err := alignAndSeek(reader, &offset, &stOffset); err != nil {
return fmt.Errorf("error aligning to next PE string table value: %w", err)
}
var value string
if stringHeader.ValueLength > 0 {
value = readUTF16(reader, &offset, &stOffset)
}
fields[key] = value
if err := alignAndSeek(reader, &offset, &stOffset); err != nil {
return fmt.Errorf("error aligning to next PE string table key: %w", err)
}
}
}
if fields["FileVersion"] == "" {
// we can derive the file version from the fixed file info if it is not already specified as a string entry... neat!
fields["FileVersion"] = fmt.Sprintf("%d.%d.%d.%d",
fixedFileInfo.FileVersionMS>>16, fixedFileInfo.FileVersionMS&0xFFFF,
fixedFileInfo.FileVersionLS>>16, fixedFileInfo.FileVersionLS&0xFFFF)
}
return nil
}
// readIntoStructAndSzKey reads a struct from the reader and updates the offsets if provided, returning the szKey value.
// This is only useful in the context of the resource directory parsing in narrow cases (this is invalid to use outside of that context).
func readIntoStructAndSzKey[T any](reader *bytes.Reader, data *T, offsets ...*int) (string, error) {
if err := readIntoStruct(reader, data, offsets...); err != nil {
return "", err
}
return readUTF16(reader, offsets...), nil
}
// readIntoStruct reads a struct from the reader and updates the offsets if provided.
func readIntoStruct[T any](reader io.Reader, data *T, offsets ...*int) error {
if err := binary.Read(reader, binary.LittleEndian, data); err != nil {
if errors.Is(err, io.EOF) {
return nil
}
return err
}
for i := range offsets {
*offsets[i] += binary.Size(*data)
}
return nil
}
// alignAndSeek aligns the reader to the next DWORD boundary and seeks to the new offset (updating any provided trackOffsets).
func alignAndSeek(reader io.Seeker, offset *int, trackOffsets ...*int) error {
ogOffset := *offset
*offset = alignToDWORD(*offset)
diff := *offset - ogOffset
for i := range trackOffsets {
*trackOffsets[i] += diff
}
_, err := reader.Seek(int64(*offset), io.SeekStart)
return err
}
// alignToDWORD aligns the offset to the next DWORD boundary (4 byte boundary)
func alignToDWORD(offset int) int {
return (offset + 3) & ^3
}
// readUTF16 is a helper function to read a null-terminated UTF16 string
func readUTF16(reader *bytes.Reader, offsets ...*int) string {
startPos, err := reader.Seek(0, io.SeekCurrent)
if err != nil {
return ""
}
var result []rune
for {
var char uint16
err := binary.Read(reader, binary.LittleEndian, &char)
if err != nil || char == 0 {
break
}
result = append(result, rune(char))
}
// calculate how many bytes we've actually read (including null terminator)
endPos, _ := reader.Seek(0, io.SeekCurrent)
bytesRead := int(endPos - startPos)
for i := range offsets {
*offsets[i] += bytesRead
}
return string(result)
}
// readUTF16WithLength reads a length-prefixed UTF-16 string from reader.
// The first 2 bytes represent the number of UTF-16 code units.
func readUTF16WithLength(reader *bytes.Reader) (string, error) {
var length uint16
if err := binary.Read(reader, binary.LittleEndian, &length); err != nil {
return "", err
}
if length == 0 {
return "", nil
}
// read length UTF-16 code units.
codes := make([]uint16, length)
if err := binary.Read(reader, binary.LittleEndian, &codes); err != nil {
return "", err
}
return string(utf16.Decode(codes)), nil
}
@@ -1,4 +1,4 @@
package dotnet
package pe
import (
"testing"
@@ -13,7 +13,7 @@ import (
"github.com/anchore/syft/syft/source/stereoscopesource"
)
func Test_getLogicalDotnetPE(t *testing.T) {
func Test_Read_DotNetDetection(t *testing.T) {
tests := []struct {
name string
fixture string
@@ -126,7 +126,7 @@ func Test_getLogicalDotnetPE(t *testing.T) {
reader := fixtureFile(t, tt.fixture, tt.path)
got, err := getLogicalDotnetPE(reader)
got, err := Read(reader)
tt.wantErr(t, err)
if err != nil {
return
@@ -136,7 +136,7 @@ func Test_getLogicalDotnetPE(t *testing.T) {
t.Errorf("unexpected version resources (-want +got): %s", d)
}
assert.Equal(t, tt.wantCLR, got.CLR.hasEvidenceOfCLR())
assert.Equal(t, tt.wantCLR, got.CLR.HasEvidenceOfCLR())
})
}
}
@@ -0,0 +1 @@
../../../dotnet/test-fixtures/image-net8-app
@@ -0,0 +1 @@
../../../dotnet/test-fixtures/image-net8-app-single-file
@@ -27,6 +27,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
"github.com/anchore/syft/syft/source/filesource"
"github.com/anchore/syft/syft/source/stereoscopesource"
)
@@ -96,6 +97,10 @@ func (p *CatalogTester) WithContext(ctx context.Context) *CatalogTester {
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
t.Helper()
if path == "" {
return p
}
s, err := directorysource.NewFromPath(path)
require.NoError(t, err)
@@ -106,9 +111,25 @@ func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester
return p
}
func (p *CatalogTester) FromFileSource(t *testing.T, path string) *CatalogTester {
t.Helper()
s, err := filesource.NewFromPath(path)
require.NoError(t, err)
resolver, err := s.FileResolver(source.AllLayersScope)
require.NoError(t, err)
p.resolver = resolver
return p
}
func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester {
t.Helper()
if path == "" {
return p
}
fixture, err := os.Open(path)
require.NoError(t, err)
@@ -157,6 +178,11 @@ func (p *CatalogTester) WithResolver(r file.Resolver) *CatalogTester {
func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *CatalogTester {
t.Helper()
if fixtureName == "" {
return p
}
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
s := stereoscopesource.New(img, stereoscopesource.ImageConfig{
@@ -327,6 +353,11 @@ func TestCataloger(t *testing.T, fixtureDir string, cataloger pkg.Cataloger, exp
NewCatalogTester().FromDirectory(t, fixtureDir).Expects(expectedPkgs, expectedRelationships).TestCataloger(t, cataloger)
}
func TestCatalogerFromFileSource(t *testing.T, fixturePath string, cataloger pkg.Cataloger, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
t.Helper()
NewCatalogTester().FromFileSource(t, fixturePath).Expects(expectedPkgs, expectedRelationships).TestCataloger(t, cataloger)
}
func TestFileParserWithEnv(t *testing.T, fixturePath string, parser generic.Parser, env *generic.Environment, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
t.Helper()
+108
View File
@@ -215,3 +215,111 @@ func TestJvmDistributionCataloger(t *testing.T) {
}
}
func TestJvmDistributionCatalogerFromFile(t *testing.T) {
cases := []struct {
name string
fixture string
expected pkg.Package
}{
{
name: "valid 1.8.0",
fixture: "test-fixtures/jvm-installs/oracle-jdk-se-8/usr/lib/jvm/jdk-1.8-oracle-x64/release",
expected: pkg.Package{
Name: "jdk",
Version: "1.8.0_411-b25",
FoundBy: "java-jvm-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/release")),
Licenses: pkg.NewLicenseSet(),
Type: pkg.BinaryPkg,
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:oracle:java_se:1.8.0:update411:*:*:*:*:*:*", cpe.DeclaredSource),
cpe.Must("cpe:2.3:a:oracle:jre:1.8.0:update411:*:*:*:*:*:*", cpe.DeclaredSource),
cpe.Must("cpe:2.3:a:oracle:jdk:1.8.0:update411:*:*:*:*:*:*", cpe.DeclaredSource),
},
PURL: "pkg:generic/oracle/jdk@1.8.0_411-b25",
Metadata: pkg.JavaVMInstallation{
Release: pkg.JavaVMRelease{
JavaRuntimeVersion: "1.8.0_411-b25",
JavaVersion: "1.8.0_411",
OsArch: "amd64",
OsName: "Linux",
OsVersion: "2.6",
Source: ".:git:71ec2089cf8c+",
BuildType: "commercial",
},
Files: []string{
"/release",
},
},
},
},
{
name: "valid post-jep223",
fixture: "test-fixtures/jvm-installs/valid-post-jep223/jvm/openjdk/release",
expected: pkg.Package{
Name: "openjdk",
Version: "21.0.4+7-LTS",
FoundBy: "java-jvm-cataloger",
Locations: file.NewLocationSet(file.NewLocation("/release")),
Licenses: pkg.NewLicenseSet(),
Type: pkg.BinaryPkg,
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:oracle:openjdk:21.0.4:*:*:*:*:*:*:*", cpe.DeclaredSource)},
PURL: "pkg:generic/oracle/openjdk@21.0.4%2B7-LTS?repository_url=https%3A%2F%2Fgithub.com%2Fadoptium%2Fjdk21u.git",
Metadata: pkg.JavaVMInstallation{
Release: pkg.JavaVMRelease{
Implementor: "Eclipse Adoptium",
ImplementorVersion: "Temurin-21.0.4+7",
JavaRuntimeVersion: "21.0.4+7-LTS",
JavaVersion: "21.0.4",
JavaVersionDate: "2024-07-16",
Libc: "gnu",
Modules: []string{
"java.base", "java.compiler", "java.datatransfer", "java.xml", "java.prefs",
"java.desktop", "java.instrument", "java.logging", "java.management",
"java.security.sasl", "java.naming", "java.rmi", "java.management.rmi",
"java.net.http", "java.scripting", "java.security.jgss",
"java.transaction.xa", "java.sql", "java.sql.rowset", "java.xml.crypto", "java.se",
"java.smartcardio", "jdk.accessibility", "jdk.internal.jvmstat", "jdk.attach",
"jdk.charsets", "jdk.internal.opt", "jdk.zipfs", "jdk.compiler", "jdk.crypto.ec",
"jdk.crypto.cryptoki", "jdk.dynalink", "jdk.internal.ed", "jdk.editpad", "jdk.hotspot.agent",
"jdk.httpserver", "jdk.incubator.vector", "jdk.internal.le", "jdk.internal.vm.ci",
"jdk.internal.vm.compiler", "jdk.internal.vm.compiler.management", "jdk.jartool",
"jdk.javadoc", "jdk.jcmd", "jdk.management", "jdk.management.agent", "jdk.jconsole",
"jdk.jdeps", "jdk.jdwp.agent", "jdk.jdi", "jdk.jfr", "jdk.jlink", "jdk.jpackage", "jdk.jshell",
"jdk.jsobject", "jdk.jstatd", "jdk.localedata", "jdk.management.jfr", "jdk.naming.dns",
"jdk.naming.rmi", "jdk.net", "jdk.nio.mapmode", "jdk.random", "jdk.sctp", "jdk.security.auth",
"jdk.security.jgss", "jdk.unsupported", "jdk.unsupported.desktop", "jdk.xml.dom",
},
OsArch: "aarch64",
OsName: "Linux",
Source: ".:git:13710926b798",
BuildSource: "git:1271f10a26c47e1489a814dd2731f936a588d621",
BuildSourceRepo: "https://github.com/adoptium/temurin-build.git",
SourceRepo: "https://github.com/adoptium/jdk21u.git",
FullVersion: "21.0.4+7-LTS",
SemanticVersion: "21.0.4+7",
BuildInfo: "OS: Linux Version: 5.4.0-150-generic",
JvmVariant: "Hotspot",
JvmVersion: "21.0.4+7-LTS",
ImageType: "JDK",
},
Files: []string{
"/release",
},
},
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
p := tt.expected
p.SetID()
pkgtest.TestCatalogerFromFileSource(t, tt.fixture, NewJvmDistributionCataloger(), []pkg.Package{p}, nil)
})
}
}
@@ -16,6 +16,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/mimetype"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/format/cyclonedxjson"
@@ -508,7 +509,7 @@ func (ni nativeImagePE) fetchPkgs() (pkgs []pkg.Package, relationships []artifac
}
// fetchPkgs provides the packages available in a UnionReader.
func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package, []artifact.Relationship) {
func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
var relationships []artifact.Relationship
imageFormats := []func(string, io.ReaderAt) (nativeImage, error){newElf, newMachO, newPE}
@@ -518,8 +519,9 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package,
readers, err := unionreader.GetReaders(reader)
if err != nil {
log.Debugf("failed to open the java native-image binary: %v", err)
return nil, nil
return nil, nil, nil
}
var unknowns error
for _, r := range readers {
for _, makeNativeImage := range imageFormats {
ni, err := makeNativeImage(filename, r)
@@ -532,13 +534,14 @@ func fetchPkgs(reader unionreader.UnionReader, filename string) ([]pkg.Package,
newPkgs, newRelationships, err := ni.fetchPkgs()
if err != nil {
log.Tracef("unable to extract SBOM from possible java native-image %s: %v", filename, err)
unknowns = unknown.Join(unknowns, fmt.Errorf("unable to extract SBOM from possible java native-image %s: %w", filename, err))
continue
}
pkgs = append(pkgs, newPkgs...)
relationships = append(relationships, newRelationships...)
}
}
return pkgs, relationships
return pkgs, relationships, unknowns
}
// Catalog attempts to find any native image executables reachable from a resolver.
@@ -549,17 +552,18 @@ func (c *nativeImageCataloger) Catalog(_ context.Context, resolver file.Resolver
if err != nil {
return pkgs, nil, fmt.Errorf("failed to find binaries by mime types: %w", err)
}
var errs error
for _, location := range fileMatches {
newPkgs, newRelationships, err := processLocation(location, resolver)
if err != nil {
return nil, nil, err
errs = unknown.Append(errs, location, err)
continue
}
pkgs = append(pkgs, newPkgs...)
relationships = append(relationships, newRelationships...)
}
return pkgs, relationships, nil
return pkgs, relationships, errs
}
func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
@@ -574,6 +578,6 @@ func processLocation(location file.Location, resolver file.Resolver) ([]pkg.Pack
if err != nil {
return nil, nil, err
}
pkgs, relationships := fetchPkgs(reader, location.RealPath)
return pkgs, relationships, nil
pkgs, relationships, err := fetchPkgs(reader, location.RealPath)
return pkgs, relationships, err
}
+4 -1
View File
@@ -88,7 +88,10 @@ func parseJVMRelease(_ context.Context, resolver file.Resolver, _ *generic.Envir
installDir := path.Dir(reader.Path())
files, hasJdk := findJvmFiles(resolver, installDir)
vendor, product := jvmPrimaryVendorProduct(ri.Implementor, reader.Path(), ri.ImageType, hasJdk)
// the reason we use the reference to get the real path is in cases where the file cataloger is involved
// (thus the real path is not available except for in the original file reference). This is important
// since the path is critical for distinguishing between different JVM vendors.
vendor, product := jvmPrimaryVendorProduct(ri.Implementor, string(reader.Reference().RealPath), ri.ImageType, hasJdk)
p := pkg.Package{
Name: product,
@@ -0,0 +1,240 @@
package php
import (
"context"
"fmt"
"path"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/binutils"
)
type interpreterCataloger struct {
name string
extensionsGlob string
interpreterClassifiers []binutils.Classifier
}
// NewInterpreterCataloger returns a new cataloger for PHP interpreters (php and php-fpm) as well as any installed C extensions.
func NewInterpreterCataloger() pkg.Cataloger { //nolint:funlen
name := "php-interpreter-cataloger"
m := binutils.ContextualEvidenceMatchers{CatalogerName: name}
return interpreterCataloger{
name: name,
// example matches:
// - as found in php-fpm docker library images: /usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so
// - as found in alpine images: /usr/lib/php83/modules/bcmath.so
extensionsGlob: "**/php*/**/*.so",
interpreterClassifiers: []binutils.Classifier{
{
Class: "php-cli-binary",
FileGlob: "**/php*",
EvidenceMatcher: m.FileNameTemplateVersionMatcher(
`(.*/|^)php[0-9]*$`,
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "php-cli",
PURL: packageurl.PackageURL{
Type: packageurl.TypeGeneric,
Name: "php-cli",
// the version will be filled in dynamically
},
CPEs: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "php",
Product: "php",
},
Source: cpe.NVDDictionaryLookupSource,
},
},
},
{
Class: "php-fpm-binary",
FileGlob: "**/php-fpm*",
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "php-fpm",
PURL: packageurl.PackageURL{
Type: packageurl.TypeGeneric,
Name: "php-fpm",
// the version will be filled in dynamically
},
CPEs: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "php",
Product: "php",
},
Source: cpe.NVDDictionaryLookupSource,
},
},
},
{
Class: "php-apache-binary",
FileGlob: "**/apache*/**/libphp*.so",
EvidenceMatcher: m.FileContentsVersionMatcher(
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "libphp",
PURL: packageurl.PackageURL{
Type: packageurl.TypeGeneric,
Name: "php",
// the version will be filled in dynamically
},
CPEs: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: "php",
Product: "php",
},
Source: cpe.NVDDictionaryLookupSource,
},
},
},
},
}
}
func (p interpreterCataloger) Name() string {
return p.name
}
func (p interpreterCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
interpreterPkgs, intErrs := p.catalogInterpreters(resolver)
extensionPkgs, extErrs := p.catalogExtensions(resolver)
// TODO: a future iteration of this cataloger could be to read all php.ini / php/conf.d/*.ini files and indicate which extensions are enabled
// and attempt to resolve the extension_dir. This can be tricky as it is a #define in the php source code and not always available
// in configuration. For the meantime we report all extensions present
// create a relationship for each interpreter package to the extensions
var relationships []artifact.Relationship
for _, interpreter := range interpreterPkgs {
for _, extension := range extensionPkgs {
relationships = append(relationships, artifact.Relationship{
From: extension,
To: interpreter,
Type: artifact.DependencyOfRelationship,
})
}
}
var allPkgs []pkg.Package
allPkgs = append(allPkgs, interpreterPkgs...)
allPkgs = append(allPkgs, extensionPkgs...)
return allPkgs, relationships, unknown.Join(intErrs, extErrs)
}
func (p interpreterCataloger) catalogInterpreters(resolver file.Resolver) ([]pkg.Package, error) {
var errs error
var packages []pkg.Package
for _, cls := range p.interpreterClassifiers {
locations, err := resolver.FilesByGlob(cls.FileGlob)
if err != nil {
// convert any file.Resolver path errors to unknowns with locations
errs = unknown.Join(errs, unknown.ProcessPathErrors(err))
continue
}
for _, location := range locations {
pkgs, err := cls.EvidenceMatcher(cls, binutils.MatcherContext{Resolver: resolver, Location: location})
if err != nil {
errs = unknown.Append(errs, location, err)
continue
}
packages = append(packages, pkgs...)
}
}
return packages, errs
}
func (p interpreterCataloger) catalogExtensions(resolver file.Resolver) ([]pkg.Package, error) {
locations, err := resolver.FilesByGlob(p.extensionsGlob)
if err != nil {
// convert any file.Resolver path errors to unknowns with locations
return nil, unknown.ProcessPathErrors(err)
}
var packages []pkg.Package
var errs error
for _, location := range locations {
pkgs, err := p.catalogExtension(resolver, location)
if err != nil {
errs = unknown.Append(errs, location, err)
continue
}
packages = append(packages, pkgs...)
}
return packages, errs
}
func (p interpreterCataloger) catalogExtension(resolver file.Resolver, location file.Location) ([]pkg.Package, error) {
reader, err := resolver.FileContentsByLocation(location)
defer internal.CloseAndLogError(reader, location.RealPath)
if err != nil {
return nil, unknown.ProcessPathErrors(err)
}
name, cls := p.getClassifier(location.RealPath)
if name == "" || cls == nil {
return nil, nil
}
pkgs, err := cls.EvidenceMatcher(*cls, binutils.MatcherContext{Resolver: resolver, Location: location})
if err != nil {
return nil, unknown.New(location, err)
}
return pkgs, err
}
func (p interpreterCataloger) getClassifier(realPath string) (string, *binutils.Classifier) {
if !strings.HasSuffix(realPath, ".so") {
return "", nil
}
base := path.Base(realPath)
name := strings.TrimSuffix(base, ".so")
var match string
switch name {
case "mysqli":
match = `(mysqlnd|mysqli)?\s*\x00*(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+API`
case "opcache":
match = `(?m)\x00+(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+`
case "zip":
match = `\x00+(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+Zip`
default:
match = fmt.Sprintf(`(?m)(\x00+%s)?\x00+(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00+API`, name)
}
return name, &binutils.Classifier{
Class: fmt.Sprintf("php-ext-%s-binary", name),
EvidenceMatcher: binutils.FileContentsVersionMatcher(match, p.name),
Package: name,
PURL: packageurl.PackageURL{
Type: packageurl.TypeGeneric,
Name: name,
// the version will be filled in dynamically
},
CPEs: []cpe.CPE{
{
Attributes: cpe.Attributes{
Part: "a",
Vendor: fmt.Sprintf("php-%s", name),
Product: fmt.Sprintf("php-%s", name),
},
Source: cpe.GeneratedSource,
},
},
}
}
@@ -0,0 +1,149 @@
package php
import (
"testing"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func Test_InterpreterCataloger(t *testing.T) {
tests := []struct {
name string
fixture string
expectedPkgs []string
expectedRels []string
}{
{
name: "native installation with extensions",
fixture: "image-extensions",
expectedPkgs: []string{
// interpreters
"php-cli @ 8.3.21 (/usr/local/bin/php)",
"php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
// extensions
"bcmath @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so)",
"exif @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so)",
"ftp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so)",
"gd @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so)",
"gmp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so)",
"intl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so)",
"ldap @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so)",
"opcache @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so)",
"pcntl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so)",
"pdo_mysql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so)",
"pdo_pgsql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so)",
"sodium @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so)",
"sysvsem @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so)",
},
expectedRels: []string{
"bcmath @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"bcmath @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/bcmath.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"exif @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"exif @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/exif.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"ftp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"ftp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ftp.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"gd @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"gd @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gd.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"gmp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"gmp @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/gmp.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"intl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"intl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/intl.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"ldap @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"ldap @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/ldap.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"opcache @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"opcache @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/opcache.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"pcntl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"pcntl @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pcntl.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"pdo_mysql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"pdo_mysql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_mysql.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"pdo_pgsql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"pdo_pgsql @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/pdo_pgsql.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"sodium @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"sodium @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sodium.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"sysvsem @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"sysvsem @ 8.3.21 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/sysvsem.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so) [dependency-of] php-cli @ 8.3.21 (/usr/local/bin/php)",
"zip @ 1.22.3 (/usr/local/lib/php/extensions/no-debug-non-zts-20230831/zip.so) [dependency-of] php-fpm @ 8.3.21 (/usr/local/sbin/php-fpm)",
},
},
{
name: "apache installation with libphp and extensions",
fixture: "image-apache",
expectedPkgs: []string{
// interpreters
"libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
// extensions
"calendar @ 8.2.28 (/usr/lib/php/20220829/calendar.so)",
"ctype @ 8.2.28 (/usr/lib/php/20220829/ctype.so)",
"exif @ 8.2.28 (/usr/lib/php/20220829/exif.so)",
"ffi @ 8.2.28 (/usr/lib/php/20220829/ffi.so)",
"fileinfo @ 8.2.28 (/usr/lib/php/20220829/fileinfo.so)",
"ftp @ 8.2.28 (/usr/lib/php/20220829/ftp.so)",
"gettext @ 8.2.28 (/usr/lib/php/20220829/gettext.so)",
"iconv @ 8.2.28 (/usr/lib/php/20220829/iconv.so)",
"mysqli @ 8.2.28 (/usr/lib/php/20220829/mysqli.so)",
"opcache @ 8.2.28 (/usr/lib/php/20220829/opcache.so)",
"pdo @ 8.2.28 (/usr/lib/php/20220829/pdo.so)",
"pdo_mysql @ 8.2.28 (/usr/lib/php/20220829/pdo_mysql.so)",
"phar @ 8.2.28 (/usr/lib/php/20220829/phar.so)",
"posix @ 8.2.28 (/usr/lib/php/20220829/posix.so)",
"readline @ 8.2.28 (/usr/lib/php/20220829/readline.so)",
"shmop @ 8.2.28 (/usr/lib/php/20220829/shmop.so)",
"simplexml @ 8.2.28 (/usr/lib/php/20220829/simplexml.so)",
"sockets @ 8.2.28 (/usr/lib/php/20220829/sockets.so)",
"sysvmsg @ 8.2.28 (/usr/lib/php/20220829/sysvmsg.so)",
"sysvsem @ 8.2.28 (/usr/lib/php/20220829/sysvsem.so)",
"sysvshm @ 8.2.28 (/usr/lib/php/20220829/sysvshm.so)",
"tokenizer @ 8.2.28 (/usr/lib/php/20220829/tokenizer.so)",
"xml @ 8.2.28 (/usr/lib/php/20220829/xml.so)",
"xmlreader @ 8.2.28 (/usr/lib/php/20220829/xmlreader.so)",
"xmlwriter @ 8.2.28 (/usr/lib/php/20220829/xmlwriter.so)",
"xsl @ 8.2.28 (/usr/lib/php/20220829/xsl.so)",
},
expectedRels: []string{
"calendar @ 8.2.28 (/usr/lib/php/20220829/calendar.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"ctype @ 8.2.28 (/usr/lib/php/20220829/ctype.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"exif @ 8.2.28 (/usr/lib/php/20220829/exif.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"ffi @ 8.2.28 (/usr/lib/php/20220829/ffi.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"fileinfo @ 8.2.28 (/usr/lib/php/20220829/fileinfo.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"ftp @ 8.2.28 (/usr/lib/php/20220829/ftp.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"gettext @ 8.2.28 (/usr/lib/php/20220829/gettext.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"iconv @ 8.2.28 (/usr/lib/php/20220829/iconv.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"mysqli @ 8.2.28 (/usr/lib/php/20220829/mysqli.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"opcache @ 8.2.28 (/usr/lib/php/20220829/opcache.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"pdo @ 8.2.28 (/usr/lib/php/20220829/pdo.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"pdo_mysql @ 8.2.28 (/usr/lib/php/20220829/pdo_mysql.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"phar @ 8.2.28 (/usr/lib/php/20220829/phar.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"posix @ 8.2.28 (/usr/lib/php/20220829/posix.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"readline @ 8.2.28 (/usr/lib/php/20220829/readline.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"shmop @ 8.2.28 (/usr/lib/php/20220829/shmop.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"simplexml @ 8.2.28 (/usr/lib/php/20220829/simplexml.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"sockets @ 8.2.28 (/usr/lib/php/20220829/sockets.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"sysvmsg @ 8.2.28 (/usr/lib/php/20220829/sysvmsg.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"sysvsem @ 8.2.28 (/usr/lib/php/20220829/sysvsem.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"sysvshm @ 8.2.28 (/usr/lib/php/20220829/sysvshm.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"tokenizer @ 8.2.28 (/usr/lib/php/20220829/tokenizer.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"xml @ 8.2.28 (/usr/lib/php/20220829/xml.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"xmlreader @ 8.2.28 (/usr/lib/php/20220829/xmlreader.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"xmlwriter @ 8.2.28 (/usr/lib/php/20220829/xmlwriter.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
"xsl @ 8.2.28 (/usr/lib/php/20220829/xsl.so) [dependency-of] libphp @ 8.2.28 (/usr/lib/apache2/modules/libphp8.2.so)",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewInterpreterCataloger()
pkgtest.NewCatalogTester().
WithImageResolver(t, tt.fixture).
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
//Expects(tt.expected, nil).
ExpectsPackageStrings(tt.expectedPkgs).
ExpectsRelationshipStrings(tt.expectedRels).
TestCataloger(t, c)
})
}
}
@@ -0,0 +1,11 @@
FROM --platform=linux/amd64 httpd:2.4.63-bookworm AS builder
RUN apt update -y && apt install -y libapache2-mod-php php8.2-memcache php8.2-memcache php8.2-xml php8.2-mysqli php8.2-opcache
FROM busybox:latest
# phplib.so
COPY --from=builder /usr/lib/apache2/ /usr/lib/apache2/
# php extensions
COPY --from=builder /usr/lib/php/ /usr/lib/php/
@@ -0,0 +1,105 @@
# source https://github.com/nextcloud/docker/blob/master/30/fpm-alpine/Dockerfile#L1
FROM --platform=linux/amd64 php:8.3-fpm-alpine3.21 AS builder
# entrypoint.sh and cron.sh dependencies
RUN set -ex; \
\
apk add --no-cache \
imagemagick \
imagemagick-pdf \
imagemagick-jpeg \
imagemagick-raw \
imagemagick-tiff \
imagemagick-heic \
imagemagick-webp \
imagemagick-svg \
rsync \
; \
\
rm /var/spool/cron/crontabs/root; \
echo '*/5 * * * * php -f /var/www/html/cron.php' > /var/spool/cron/crontabs/www-data
# install the PHP extensions we need
# see https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html
RUN set -ex; \
\
apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
autoconf \
freetype-dev \
gmp-dev \
icu-dev \
imagemagick-dev \
libevent-dev \
libjpeg-turbo-dev \
libmcrypt-dev \
libmemcached-dev \
libpng-dev \
libwebp-dev \
libxml2-dev \
libzip-dev \
openldap-dev \
pcre-dev \
postgresql-dev \
; \
\
docker-php-ext-configure ftp --with-openssl-dir=/usr; \
docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \
docker-php-ext-configure ldap; \
docker-php-ext-install -j "$(nproc)" \
bcmath \
exif \
ftp \
gd \
gmp \
intl \
ldap \
opcache \
pcntl \
pdo_mysql \
pdo_pgsql \
sysvsem \
zip \
; \
\
# pecl will claim success even if one install fails, so we need to perform each install separately
pecl install APCu-5.1.24; \
pecl install igbinary-3.2.16; \
pecl install imagick-3.8.0; \
pecl install memcached-3.3.0 \
--configureoptions 'enable-memcached-igbinary="yes"'; \
pecl install redis-6.2.0 \
--configureoptions 'enable-redis-igbinary="yes" enable-redis-zstd="yes" enable-redis-lz4="yes"'; \
\
docker-php-ext-enable \
apcu \
igbinary \
imagick \
memcached \
redis \
; \
rm -r /tmp/pear; \
\
runDeps="$( \
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \
| tr ',' '\n' \
| sort -u \
| awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
)"; \
apk add --no-network --virtual .nextcloud-phpext-rundeps $runDeps; \
apk del --no-network .build-deps
FROM busybox:latest
# interpreters + process manager
COPY --from=builder /usr/local/sbin/php-fpm /usr/local/sbin/php-fpm
COPY --from=builder /usr/local/bin/php /usr/local/bin/php
# extensions
COPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions
# configs
COPY --from=builder /usr/local/etc/php-fpm.conf /usr/local/etc/php-fpm.conf
COPY --from=builder /usr/local/etc/php/conf.d /usr/local/etc/php/conf.d
COPY --from=builder /usr/local/etc/php-fpm.d /usr/local/etc/php-fpm.d
COPY --from=builder /usr/local/etc/php-fpm.conf /usr/local/etc/php-fpm.conf
+1 -1
View File
@@ -758,7 +758,7 @@ func Test_PackageCataloger_SitePackageRelationships(t *testing.T) {
// Note: we'll only see new relationships, so any relationship where there is at least one new player (in FROM or TO)
"blessed @ 1.20.0 (/usr/local/lib/python3.9/dist-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)", // note: depends on global site package!
"python-editor @ 1.0.4 (/usr/local/lib/python3.9/dist-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)", // note: depends on global site package!
"readchar @ 4.2.0 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)",
"readchar @ 4.2.1 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)",
"soupsieve @ 2.3 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] beautifulsoup4 @ 4.10.0 (/app/project1/venv/lib/python3.9/site-packages)",
// project 2 virtual env
@@ -13,10 +13,10 @@ RUN python3.9 -m pip install click==8.0.3 beautifulsoup4==4.9.3 soupsieve==2.2.1
RUN python3.9 -m pip install six==1.16.0 wcwidth==0.2.13 blessed==1.20.0 python-editor==1.0.4 # total dependencies for inquirer in project1 (which is linked)
RUN python3.9 -m pip install requests==2.25.0 certifi==2020.12.5 chardet==3.0.4 idna==2.10 urllib3==1.26.18 # total dependencies for requests
RUN python3.8 -m pip install readchar==4.2.1
RUN python3.8 -m pip install click==8.0.2 beautifulsoup4==4.9.2 soupsieve==2.2.0 requests==2.25.0
RUN python3.8 -m pip install runs==1.2.2 xmod==1.8.1 # partial dependencies for inquirer in project2 (which is a red herring)
RUN python3.8 -m pip install requests==2.25.0 certifi==2020.12.5 chardet==3.0.4 idna==2.10 urllib3==1.26.18 # total dependencies for requests
RUN python3.8 -m pip install readchar==4.2.0
# create directories for the two projects
RUN mkdir -p /app/project1 /app/project2
+18 -8
View File
@@ -7,6 +7,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
_ "modernc.org/sqlite"
"github.com/anchore/syft/syft/artifact"
@@ -29,14 +30,15 @@ func Test_DBCataloger(t *testing.T) {
FoundBy: "rpm-db-cataloger",
PURL: "pkg:rpm/basesystem@11-13.el9?arch=noarch&upstream=basesystem-11-13.el9.src.rpm",
Metadata: pkg.RpmDBEntry{
Name: "basesystem",
Version: "11",
Arch: "noarch",
Release: "13.el9",
SourceRpm: "basesystem-11-13.el9.src.rpm",
Size: 0,
Vendor: "Rocky Enterprise Software Foundation",
Provides: []string{"basesystem"},
Name: "basesystem",
Version: "11",
Arch: "noarch",
Release: "13.el9",
SourceRpm: "basesystem-11-13.el9.src.rpm",
Size: 0,
Vendor: "Rocky Enterprise Software Foundation",
Signatures: mustParseSignatures(t, "RSA/SHA256, Wed May 11 11:12:32 2022, Key ID 702d426d350d275d"),
Provides: []string{"basesystem"},
Requires: []string{
"filesystem",
"rpmlib(CompressedFileNames)",
@@ -65,6 +67,7 @@ func Test_DBCataloger(t *testing.T) {
Release: "6.el9_1",
SourceRpm: "bash-5.1.8-6.el9_1.src.rpm",
Size: 7738634,
Signatures: mustParseSignatures(t, "RSA/SHA256, Mon Jan 23 22:49:22 2023, Key ID 702d426d350d275d"),
ModularityLabel: strRef(""),
Vendor: "Rocky Enterprise Software Foundation",
Provides: []string{
@@ -117,6 +120,7 @@ func Test_DBCataloger(t *testing.T) {
Release: "2.el9",
SourceRpm: "filesystem-3.16-2.el9.src.rpm",
Size: 106,
Signatures: mustParseSignatures(t, "RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"),
ModularityLabel: strRef(""),
Vendor: "Rocky Enterprise Software Foundation",
Provides: []string{
@@ -334,3 +338,9 @@ func Test_denySelfReferences(t *testing.T) {
})
}
}
func mustParseSignatures(t testing.TB, sigs ...string) []pkg.RpmSignature {
signatures, err := parseSignatures(sigs...)
require.NoError(t, err)
return signatures
}
+171 -11
View File
@@ -1,13 +1,18 @@
package redhat
import (
"bytes"
"context"
"encoding/binary"
"fmt"
"io"
"sort"
"strconv"
"time"
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
"github.com/sassoftware/go-rpmutils"
rpmdb "github.com/anchore/go-rpmdb/pkg"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -15,6 +20,42 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
type pgpSig struct {
_ [3]byte
Date int32
KeyID [8]byte
PubKeyAlgo uint8
HashAlgo uint8
}
type textSig struct {
_ [2]byte
PubKeyAlgo uint8
HashAlgo uint8
_ [4]byte
Date int32
_ [4]byte
KeyID [8]byte
}
type pgp4Sig struct {
_ [2]byte
PubKeyAlgo uint8
HashAlgo uint8
_ [17]byte
KeyID [8]byte
_ [2]byte
Date int32
}
var pubKeyLookup = map[uint8]string{
0x01: "RSA",
}
var hashLookup = map[uint8]string{
0x02: "SHA1",
0x08: "SHA256",
}
// parseRpmArchive parses a single RPM
func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
rpm, err := rpmutils.ReadRpm(reader)
@@ -44,21 +85,140 @@ func parseRpmArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
files, err := rpm.Header.GetFiles()
logRpmArchiveErr(reader.Location, "files", err)
rsa, err := rpm.Header.GetBytes(rpmutils.SIG_RSA)
logRpmArchiveErr(reader.Location, "rsa signature", err)
pgp, err := rpm.Header.GetBytes(rpmutils.SIG_PGP)
logRpmArchiveErr(reader.Location, "pgp signature", err)
var allSigs [][]byte
allSigs = append(allSigs, rsa)
allSigs = append(allSigs, pgp)
sigs, err := parseSignatureHeaders(allSigs)
logRpmArchiveErr(reader.Location, "signature", err)
metadata := pkg.RpmArchive{
Name: nevra.Name,
Version: nevra.Version,
Epoch: parseEpoch(nevra.Epoch),
Arch: nevra.Arch,
Release: nevra.Release,
SourceRpm: sourceRpm,
Vendor: vendor,
Size: int(size),
Files: mapFiles(files, digestAlgorithm),
Name: nevra.Name,
Version: nevra.Version,
Epoch: parseEpoch(nevra.Epoch),
Arch: nevra.Arch,
Release: nevra.Release,
SourceRpm: sourceRpm,
Signatures: sigs,
Vendor: vendor,
Size: int(size),
Files: mapFiles(files, digestAlgorithm),
}
return []pkg.Package{newArchivePackage(ctx, reader.Location, metadata, licenses)}, nil, nil
}
func parseSignatureHeaders(data [][]byte) ([]pkg.RpmSignature, error) {
sigMap := make(map[string]pkg.RpmSignature)
var keys []string
for _, sig := range data {
if len(sig) == 0 {
continue
}
s, err := parsePGP(sig)
if err != nil {
log.WithFields("error", err).Trace("unable to parse RPM archive signature")
return nil, err
}
k := s.String()
if _, ok := sigMap[k]; ok {
// if we have a duplicate signature, just skip it
continue
}
sigMap[k] = *s
keys = append(keys, k)
}
var signatures []pkg.RpmSignature
sort.Strings(keys)
for _, k := range keys {
signatures = append(signatures, sigMap[k])
}
return signatures, nil
}
func parsePGP(data []byte) (*pkg.RpmSignature, error) {
var tag, signatureType, version uint8
r := bytes.NewReader(data)
err := binary.Read(r, binary.BigEndian, &tag)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.BigEndian, &signatureType)
if err != nil {
return nil, err
}
err = binary.Read(r, binary.BigEndian, &version)
if err != nil {
return nil, err
}
switch signatureType {
case 0x01:
switch version {
case 0x1c:
sig := textSig{}
err = binary.Read(r, binary.BigEndian, &sig)
if err != nil {
return nil, fmt.Errorf("invalid PGP signature on decode: %w", err)
}
return &pkg.RpmSignature{
PublicKeyAlgorithm: pubKeyLookup[sig.PubKeyAlgo],
HashAlgorithm: hashLookup[sig.HashAlgo],
Created: time.Unix(int64(sig.Date), 0).UTC().Format("Mon Jan _2 15:04:05 2006"),
IssuerKeyID: fmt.Sprintf("%x", sig.KeyID),
}, nil
default:
return decodePGPSig(version, r)
}
case 0x02:
return decodePGPSig(version, r)
}
return nil, fmt.Errorf("unknown signature type: %d", signatureType)
}
func decodePGPSig(version uint8, r io.Reader) (*pkg.RpmSignature, error) {
var pubKeyAlgo, hashAlgo, pkgDate string
var keyID [8]byte
switch {
case version > 0x15:
sig := pgp4Sig{}
err := binary.Read(r, binary.BigEndian, &sig)
if err != nil {
return nil, fmt.Errorf("invalid PGP v4 signature on decode: %w", err)
}
pubKeyAlgo = pubKeyLookup[sig.PubKeyAlgo]
hashAlgo = hashLookup[sig.HashAlgo]
pkgDate = time.Unix(int64(sig.Date), 0).UTC().Format("Mon Jan _2 15:04:05 2006")
keyID = sig.KeyID
default:
sig := pgpSig{}
err := binary.Read(r, binary.BigEndian, &sig)
if err != nil {
return nil, fmt.Errorf("invalid PGP signature on decode: %w", err)
}
pubKeyAlgo = pubKeyLookup[sig.PubKeyAlgo]
hashAlgo = hashLookup[sig.HashAlgo]
pkgDate = time.Unix(int64(sig.Date), 0).UTC().Format("Mon Jan _2 15:04:05 2006")
keyID = sig.KeyID
}
return &pkg.RpmSignature{
PublicKeyAlgorithm: pubKeyAlgo,
HashAlgorithm: hashAlgo,
Created: pkgDate,
IssuerKeyID: fmt.Sprintf("%x", keyID),
}, nil
}
func getDigestAlgorithm(location file.Location, header *rpmutils.RpmHeader) string {
digestAlgorithm, err := header.GetString(rpmutils.FILEDIGESTALGO)
logRpmArchiveErr(location, "file digest algo", err)
@@ -109,6 +269,6 @@ func parseEpoch(epoch string) *int {
func logRpmArchiveErr(location file.Location, operation string, err error) {
if err != nil {
log.Debugf("ERROR in parse_rpm_archive %s file: %s: %v", operation, location.RealPath, err)
log.WithFields("error", err, "operation", operation, "path", location.RealPath).Trace("unable to parse RPM archive")
}
}
@@ -2,8 +2,14 @@ package redhat
import (
"context"
"encoding/hex"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"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"
@@ -14,11 +20,15 @@ func TestParseRpmFiles(t *testing.T) {
abcRpmLocation := file.NewLocation("abc-1.01-9.hg20160905.el7.x86_64.rpm")
zorkRpmLocation := file.NewLocation("zork-1.0.3-1.el7.x86_64.rpm")
tests := []struct {
fixture string
expected []pkg.Package
name string
fixtureDir string
fixtureImage string
skipFiles bool
expected []pkg.Package
}{
{
fixture: "test-fixtures/rpms",
name: "go case",
fixtureDir: "test-fixtures/rpms",
expected: []pkg.Package{
{
Name: "abc",
@@ -37,8 +47,16 @@ func TestParseRpmFiles(t *testing.T) {
Release: "9.hg20160905.el7",
Version: "1.01",
SourceRpm: "abc-1.01-9.hg20160905.el7.src.rpm",
Size: 17396,
Vendor: "Fedora Project",
Signatures: []pkg.RpmSignature{
{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Wed Sep 21 07:09:44 2016",
IssuerKeyID: "6a2faea2352c64e5",
},
},
Size: 17396,
Vendor: "Fedora Project",
Files: []pkg.RpmFileRecord{
{"/usr/bin/abc", 33261, 7120, file.Digest{"sha256", "8f8495a65c66762b60afa0c3949d81b275ca6fa0601696caba5af762f455d0b9"}, "root", "root", ""},
{"/usr/share/doc/abc-1.01", 16877, 4096, file.Digest{}, "root", "root", ""},
@@ -66,7 +84,15 @@ func TestParseRpmFiles(t *testing.T) {
Version: "1.0.3",
SourceRpm: "zork-1.0.3-1.el7.src.rpm",
Size: 262367,
Vendor: "Fedora Project",
Signatures: []pkg.RpmSignature{
{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Tue Mar 2 17:32:21 2021",
IssuerKeyID: "6a2faea2352c64e5",
},
},
Vendor: "Fedora Project",
Files: []pkg.RpmFileRecord{
{"/usr/bin/zork", 33261, 115440, file.Digest{"sha256", "31b2ffc20b676a8fff795a45308f584273b9c47e8f7e196b4f36220b2734b472"}, "root", "root", ""},
{"/usr/share/doc/zork-1.0.3", 16877, 38, file.Digest{}, "root", "root", ""},
@@ -83,20 +109,128 @@ func TestParseRpmFiles(t *testing.T) {
},
},
{
fixture: "test-fixtures/bad",
name: "bad rpms",
fixtureDir: "test-fixtures/bad",
},
{
name: "rpms with signatures from RSA header",
fixtureImage: "image-rpm-archive",
skipFiles: true,
expected: []pkg.Package{
{
Name: "postgresql14-server",
Version: "0:14.10-1PGDG.rhel9",
PURL: "pkg:rpm/postgresql14-server@14.10-1PGDG.rhel9?arch=x86_64&epoch=0&upstream=postgresql14-14.10-1PGDG.rhel9.src.rpm",
Locations: file.NewLocationSet(file.NewLocation("/postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm")),
FoundBy: "rpm-archive-cataloger",
Type: pkg.RpmPkg,
Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocations("PostgreSQL", file.NewLocation("/postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm"))),
Language: "",
CPEs: nil,
Metadata: pkg.RpmArchive{
Name: "postgresql14-server",
Version: "14.10",
Epoch: ref(0),
Arch: "x86_64",
Release: "1PGDG.rhel9",
SourceRpm: "postgresql14-14.10-1PGDG.rhel9.src.rpm",
Size: 24521699,
Signatures: []pkg.RpmSignature{
{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Tue Jan 2 16:45:56 2024",
IssuerKeyID: "40bca2b408b40d20",
},
},
Vendor: "PostgreSQL Global Development Group",
// note: files are not asserted in this test
},
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
var opts []cmp.Option
if test.skipFiles {
opts = append(opts, cmpopts.IgnoreFields(pkg.RpmArchive{}, "Files"))
}
pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
WithCompareOptions(opts...).
FromDirectory(t, test.fixtureDir).
WithImageResolver(t, test.fixtureImage).
IgnoreLocationLayer().
Expects(test.expected, nil).
TestCataloger(t, NewArchiveCataloger())
})
}
}
func Test_parseRSA(t *testing.T) {
tests := []struct {
name string
data string
want *pkg.RpmSignature
wantErr assert.ErrorAssertionFunc
}{
{
name: "older RSA header",
data: "89021503050058d3e39b0946fca2c105b9de0102b12a1000a2b3d347b51142e83b2de5e03ba9096f6330b72c140e46200d662b01c78534d14fab2ad4f07325119386830dd590219f27a22e420680283c500c40e6fba95404884b0a0abca8f198030ddc03653b7db2883b8230687e9e73d43eb5a24dbabfa48bbb3d1151ed264744e5e8ca169b0c4673a1440a9b99e53e693c9722f6423833cd7795e3044227fb922e21b7c007f03e923fae3f04d1ac2e8581e68c6790115b6dccfc02c8cb41681ed84785df086d6e26008c257d088a524ba2e7a7a5f41ad26b106c67b87fe48118b69662db612c23d2140059286f1ba7764627def6867ad0e11fe3a01fb1422dabe6f5cdf4cd876dc4fadfd2364bc3ba3758db94aaf3b82368cba65cf762287f713eb7ddc773acf93b083c739577a7eaf1f99e7dcbb8db1da050490e9fb67c838448db060a9e619d318c96f03e4363808d84ce29e8c102c290cc2bfab5746f3d9ddc9eb8b428f3ad2678abb2d46e846ddca7fc41322d76a97be6d416b4750f23320ec725e082be4496483b4cd3a3d2c515b3c8a6e27541139d809245140303877b84842ed2dd0454a78b2dfb7d6213784697077a8167942ebda5995a28d8256957e33e301706c35944ae05c7a54a4dd89be654d26cefa5cf0f616bbeaf317138371b09c5bbd5531f716020e553354ce5dbce3d9bb72f21e1857408dfd5a35250ff40f61ae1e25409ae9d21db76b8878341f4762a22be2189",
// RSA/SHA1, Thu Mar 23 15:02:51 2017, Key ID 0946fca2c105b9de
want: &pkg.RpmSignature{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA1",
Created: "Thu Mar 23 15:02:51 2017",
IssuerKeyID: "0946fca2c105b9de",
},
},
{
name: "newer RSA header",
data: "89024a04000108003416210421cb256ae16fc54c6e652949702d426d350d275d050262804369161c72656c656e6740726f636b796c696e75782e6f7267000a0910702d426d350d275dc8910ffd14f0f80297481fea648e7ba5a74bce10c5faccc2bbe588caece04be34d304a6a445538afc97a7033d43c983d27cc8f5ee515b2dd92f3e03354c413e55372a4d19386eb0f2354f9a26ee5fc2e56dfda49555e4a58b49279b70cd2036b04f28125f85942f640f2984e29e079f26bf6f76831d83d95983aa084a3e7b6327be2e23d0d799c4b4d1cfb36147ddfb782bf9df7b331d97f4f46b38f968b6130d87b0ef6bb0d424390fe34e38092babed37440569a93f55f50a2bdb58be0259f35badf7e728bd49824ed47f69fa53b6e26736bde4d8358d959b090e88054c3e179745dc7377e41b54b4e10223f4859e88162c7c5ec64b78d36cf8a914c1c2deb8c4f19a70d406e70756a89195d6aee488a9b40b9dbb76b2c38e528eb88d08ec35774a48ed9ce4e0dfac45cb7613ad5921f54c61d3aae5d7b3ab0e2e6ff867ac8f395b37af78b5c01022a4a4e62f7a99425fccb7439880cd6b393a3050b2e9512693bc36f6fe9de2921dda59710a1508965065244cf9f0f8cfc5bd554777f1a84d2249339234d62f2441249f617ad7df4fb01367a91d3a880e86fdb84bc6d03a127b44a28c6ceadef89e438db9640aa59b8a3f460b07272511f8187a5f3b163c8fd1caa61667401bce2ccdb1c176c46be10ef8033903132cca5889fa3661b2fba590c41fa1c104c08426677bdbf745a52ccd28f581960cf9d7e4ede3b9584aacb2f20ef93",
// RSA/SHA256, Sun May 15 00:03:53 2022, Key ID 702d426d350d275d
want: &pkg.RpmSignature{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Sun May 15 00:03:53 2022",
IssuerKeyID: "702d426d350d275d",
},
},
{
name: "example from rocky9",
data: "8901b304000108001d162104d4bf08ae67a0b4c7a1dbccd240bca2b408b40d20050265943dc4000a091040bca2b408b40d203b270bff71678ffeb190833a19a82112f59eee64cba186ab454d4526e0b3c8797e723f6916daff1b1f18cbf53c0da5d398a3a42065e79e5ca939f721652f38400dd4cac1107a902b1dae880649437ad0242444f3f07115172cae0a207b7cf8340af2f4a94976325f1dc165d5c2a564be322c4e130adb6217e7138b689f08898c407b223aa1ff8f8d592f31eba2256c02fae70ce4022d688a487972646b8bf1b518b5d6549c1e60fd812134422d9fdb41cf799f5eab80e48b4ab7cff84362dc867ed1af1416dd78e92bcc59217de7064b9a015d94a5097788689b9b6fbdeea679cfe4a6947f73dc3a6c810f2cb999d279b01564422d1500fc1bd8bd1eefa2d60660127ffef24067354660f93c0faf81f4edd599dd7e4b77fe4bff6c7a0ea83530c817c38d1f2364175883c6ef7b6dec86ad282bdd5138b8597567db96810c4ed6454a4ab1d98f0425dcd8892a5d46ed9289cb3ae3e1f1e2663d3e8188e873428f6cf7163563ed3860edc4fee81522389508847e692e2d13310eb4b40f7fdd7eb364a0b2dc",
// RSA/SHA256, Tue Jan 2 16:45:56 2024, Key ID 40bca2b408b40d20
want: &pkg.RpmSignature{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Tue Jan 2 16:45:56 2024",
IssuerKeyID: "40bca2b408b40d20",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantErr == nil {
tt.wantErr = assert.NoError
}
data, err := hex.DecodeString(tt.data)
require.NoError(t, err)
got, err := parsePGP(data)
if !tt.wantErr(t, err) {
return
}
assert.Equal(t, tt.want, got)
})
}
}
func ref[T any](v T) *T {
return &v
}
func Test_corruptRpmArchive(t *testing.T) {
pkgtest.NewCatalogTester().
FromFile(t, "test-fixtures/bad/bad.rpm").
+76 -2
View File
@@ -2,12 +2,13 @@ package redhat
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
rpmdb "github.com/knqyf263/go-rpmdb/pkg"
rpmdb "github.com/anchore/go-rpmdb/pkg"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
@@ -69,6 +70,14 @@ func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Enviro
files, err := extractRpmFileRecords(resolver, *entry)
errs = unknown.Join(errs, err)
// there is a period of time when RPM DB entries contain both PGP and RSA signatures that are the same.
// This appears to be a holdover, where nowadays only the RSA Header is used.
sigs, err := parseSignatures(strings.TrimSpace(entry.PGP), strings.TrimSpace(entry.RSAHeader))
if err != nil {
log.WithFields("error", err, "location", reader.RealPath, "pkg", fmt.Sprintf("%s@%s", entry.Name, entry.Version)).Trace("unable to parse signatures for package %s", entry.Name)
sigs = nil
}
metadata := pkg.RpmDBEntry{
Name: entry.Name,
Version: entry.Version,
@@ -76,6 +85,7 @@ func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Enviro
Arch: entry.Arch,
Release: entry.Release,
SourceRpm: entry.SourceRpm,
Signatures: sigs,
Vendor: entry.Vendor,
Size: entry.Size,
ModularityLabel: &entry.Modularitylabel,
@@ -110,6 +120,70 @@ func parseRpmDB(ctx context.Context, resolver file.Resolver, env *generic.Enviro
return allPkgs, nil, errs
}
func parseSignatures(sigs ...string) ([]pkg.RpmSignature, error) {
var parsedSigs []pkg.RpmSignature
var errs error
for _, sig := range sigs {
if sig == "" {
continue
}
parts := strings.Split(sig, ",")
if len(parts) != 3 {
errs = errors.Join(fmt.Errorf("invalid signature format: %s", sig))
continue
}
methodParts := strings.SplitN(strings.TrimSpace(parts[0]), "/", 2)
if len(methodParts) != 2 {
errs = errors.Join(fmt.Errorf("invalid signature method format: %s", parts[0]))
continue
}
pka := strings.TrimSpace(methodParts[0])
hash := strings.TrimSpace(methodParts[1])
if pka == "" || hash == "" {
errs = errors.Join(fmt.Errorf("invalid signature method values: public-key=%q hash=%q", pka, hash))
continue
}
created := strings.TrimSpace(parts[1])
if created == "" {
errs = errors.Join(fmt.Errorf("invalid signature created value: %q", parts[1]))
continue
}
issuerFields := strings.Split(strings.TrimSpace(parts[2]), " ")
var issuer string
switch len(issuerFields) {
case 0:
errs = errors.Join(fmt.Errorf("no signature issuer value: %q", parts[2]))
case 1:
issuer = issuerFields[0]
default:
issuer = issuerFields[len(issuerFields)-1]
if issuer == "" {
errs = errors.Join(fmt.Errorf("invalid signature issuer value: %q", parts[2]))
continue
}
}
if len(issuer) < 5 {
errs = errors.Join(fmt.Errorf("invalid signature issuer length: %q", parts[2]))
continue
}
parsedSig := pkg.RpmSignature{
PublicKeyAlgorithm: pka,
HashAlgorithm: hash,
Created: created,
IssuerKeyID: issuer,
}
parsedSigs = append(parsedSigs, parsedSig)
}
return parsedSigs, errs
}
// The RPM naming scheme is [name]-[version]-[release]-[arch], where version is implicitly expands to [epoch]:[version].
// RPM version comparison depends on comparing at least the version and release fields together as a subset of the
// naming scheme. This toELVersion function takes a RPM DB package information and converts it into a minimally comparable
@@ -7,6 +7,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
@@ -222,6 +223,132 @@ func Test_corruptRpmDbEntry(t *testing.T) {
TestParser(t, parseRpmDB)
}
func TestParseSignatures(t *testing.T) {
tests := []struct {
name string
sigs []string
expected []pkg.RpmSignature
expectedError require.ErrorAssertionFunc
}{
{
name: "valid signature",
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"},
expected: []pkg.RpmSignature{
{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Mon May 16 12:32:55 2022",
IssuerKeyID: "702d426d350d275d",
},
},
},
{
name: "multiple valid signatures",
sigs: []string{
"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d",
"DSA/SHA1, Tue Jun 14 09:45:12 2023, Key ID 123abc456def789",
},
expected: []pkg.RpmSignature{
{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Mon May 16 12:32:55 2022",
IssuerKeyID: "702d426d350d275d",
},
{
PublicKeyAlgorithm: "DSA",
HashAlgorithm: "SHA1",
Created: "Tue Jun 14 09:45:12 2023",
IssuerKeyID: "123abc456def789",
},
},
},
{
name: "no signatures",
sigs: []string{},
expected: nil,
},
{
name: "empty signatures",
sigs: []string{"", "", ""},
expected: nil,
},
{
name: "invalid parts count",
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022"},
expected: nil,
expectedError: require.Error,
},
{
name: "invalid method format",
sigs: []string{"RSASHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"},
expected: nil,
expectedError: require.Error,
},
{
name: "empty method values",
sigs: []string{"/, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"},
expected: nil,
expectedError: require.Error,
},
{
name: "empty created value",
sigs: []string{"RSA/SHA256, , Key ID 702d426d350d275d"},
expected: nil,
expectedError: require.Error,
},
{
name: "empty issuer value",
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID "},
expected: nil,
expectedError: require.Error,
},
{
name: "issuer without prefix",
sigs: []string{"RSA/SHA256, Mon May 16 12:32:55 2022, 702d426d350d275d"},
expected: []pkg.RpmSignature{
{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Mon May 16 12:32:55 2022",
IssuerKeyID: "702d426d350d275d",
},
},
},
{
name: "mixed valid and invalid signatures",
sigs: []string{
"RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d",
"DSASHA1, Tue Jun 14 09:45:12 2023, Key ID 123abc456def789",
},
expected: []pkg.RpmSignature{
{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Mon May 16 12:32:55 2022",
IssuerKeyID: "702d426d350d275d",
},
},
expectedError: require.Error,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.expectedError == nil {
tt.expectedError = require.NoError
}
got, err := parseSignatures(tt.sigs...)
tt.expectedError(t, err)
if err != nil {
return
}
require.Equal(t, tt.expected, got)
})
}
}
func intRef(i int) *int {
return &i
}
@@ -0,0 +1,26 @@
FROM --platform=linux/amd64 rockylinux:9 AS rpm-downloader
# download a signed RPM (PostgreSQL in this example, known to be signed)
# using PostgreSQL official RPM which is signed with their GPG key
# $ rpm -Kv postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
# postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm:
# Header V4 RSA/SHA256 Signature, key ID 08b40d20: NOKEY
# Header SHA256 digest: OK
# Header SHA1 digest: OK
# Payload SHA256 digest: OK
# MD5 digest: OK
#
# $ rpm -ivh --nodeps --force postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
# warning: postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm: Header V4 RSA/SHA256 Signature, key ID 08b40d20: NOKEY
# Verifying... ################################# [100%]
# Preparing... ################################# [100%]
#
# $ rpm -q --qf '%{NAME}-%{VERSION}-%{RELEASE} %{RSAHEADER:pgpsig}\n' postgresql14-server-14.10-1PGDG.rhel9.x86_64
# postgresql14-server-14.10-1PGDG.rhel9 RSA/SHA256, Tue Jan 2 16:45:56 2024, Key ID 40bca2b408b40d20
RUN curl -O https://download.postgresql.org/pub/repos/yum/14/redhat/rhel-9-x86_64/postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
FROM scratch
COPY --from=rpm-downloader /postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm /postgresql14-server-14.10-1PGDG.rhel9.x86_64.rpm
+1 -1
View File
@@ -365,7 +365,7 @@ func (b *licenseBuilder) licensesFromEvidenceAndContent(evidence []licenses.Evid
func (b *licenseBuilder) licenseFromContentHash(content string) License {
hash := sha256HexFromString(content)
value := "LicenseRef-sha256:" + hash
value := "sha256:" + hash
return License{
Value: value,
+1 -1
View File
@@ -149,7 +149,7 @@ func TestLicenseSet_Add(t *testing.T) {
Locations: file.NewLocationSet(),
},
{
Value: "LicenseRef-sha256:eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
Value: "sha256:eebcea3ab1d1a28e671de90119ffcfb35fe86951e4af1b17af52b7a82fcf7d0a",
Contents: readFileAsString("../../internal/licenses/test-fixtures/nvidia-software-and-cuda-supplement"),
Type: license.Declared,
},
+1 -1
View File
@@ -255,7 +255,7 @@ func TestFullText(t *testing.T) {
name: "Full Text field is populated with the correct full text and contents are given a sha256 as value",
value: fullText,
want: License{
Value: "LicenseRef-sha256:108067fa71229a2b98b9696af0ce21cd11d9639634c8bc94bda70ebedf291e5a",
Value: "sha256:108067fa71229a2b98b9696af0ce21cd11d9639634c8bc94bda70ebedf291e5a",
Type: license.Declared,
Contents: fullText,
},
+18
View File
@@ -2,6 +2,7 @@ package pkg
import (
"sort"
"strings"
"github.com/scylladb/go-set/strset"
@@ -32,6 +33,7 @@ type RpmDBEntry struct {
Arch string `json:"architecture"`
Release string `json:"release" cyclonedx:"release"`
SourceRpm string `json:"sourceRpm" cyclonedx:"sourceRpm"`
Signatures []RpmSignature `json:"signatures,omitempty" cyclonedx:"signatures"`
Size int `json:"size" cyclonedx:"size"`
Vendor string `json:"vendor"`
ModularityLabel *string `json:"modularityLabel,omitempty"`
@@ -40,6 +42,22 @@ type RpmDBEntry struct {
Files []RpmFileRecord `json:"files"`
}
type RpmSignature struct {
PublicKeyAlgorithm string `json:"algo"`
HashAlgorithm string `json:"hash"`
Created string `json:"created"`
IssuerKeyID string `json:"issuer"`
}
func (s RpmSignature) String() string {
if s.PublicKeyAlgorithm == "" && s.HashAlgorithm == "" && s.Created == "" && s.IssuerKeyID == "" {
return ""
}
// mimics the output you would see from rpm -q --qf "%{RSAHEADER}"
// e.g."RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"
return strings.Join([]string{s.PublicKeyAlgorithm + "/" + s.HashAlgorithm, s.Created, "Key ID " + s.IssuerKeyID}, ", ")
}
// RpmFileRecord represents the file metadata for a single file attributed to a RPM package.
type RpmFileRecord struct {
Path string `json:"path"`
+47
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func TestRpmMetadata_FileOwner(t *testing.T) {
@@ -46,3 +47,49 @@ func TestRpmMetadata_FileOwner(t *testing.T) {
})
}
}
func TestRpmSignature_String(t *testing.T) {
tests := []struct {
name string
signature RpmSignature
expected string
}{
{
name: "standard signature",
signature: RpmSignature{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "SHA256",
Created: "Mon May 16 12:32:55 2022",
IssuerKeyID: "702d426d350d275d",
},
expected: "RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d",
},
{
name: "empty fields",
signature: RpmSignature{
PublicKeyAlgorithm: "",
HashAlgorithm: "",
Created: "",
IssuerKeyID: "",
},
expected: "",
},
{
name: "partial empty fields",
signature: RpmSignature{
PublicKeyAlgorithm: "RSA",
HashAlgorithm: "",
Created: "Mon May 16 12:32:55 2022",
IssuerKeyID: "",
},
expected: "RSA/, Mon May 16 12:32:55 2022, Key ID ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tt.signature.String()
assert.Equal(t, tt.expected, result)
})
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ package cli
import "testing"
func Test_Licenses(t *testing.T) {
testImage := getFixtureImage(t, "image-unknown-licenses")
testImage := getFixtureImage(t, "image-pkg-coverage")
tests := []struct {
name string
args []string
@@ -1,3 +0,0 @@
FROM alpine@sha256:c5c5fda71656f28e49ac9c5416b3643eaa6a108a8093151d6d1afc9463be8e33
RUN rm -rf /lib/apk/db/installed
COPY . /home/files
+1 -1
View File
@@ -169,7 +169,7 @@ func assertUnknownLicenseContent(required bool) traitAssertion {
for _, pkg := range data.Artifacts {
for _, lic := range pkg.Licenses {
if strings.Contains(lic.SPDXExpression, "UNKNOWN") && required {
if strings.Contains(lic.Value, "sha256") && required {
assert.NotZero(tb, len(lic.Contents))
} else {
assert.Empty(tb, lic.Contents)