From 5056c7f8611d53afaeee18203266830cf7a4eadb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:47:50 -0400 Subject: [PATCH 01/38] chore(deps): bump github/codeql-action from 4.30.7 to 4.30.8 (#4277) --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index c8af41495..d28a63841 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 #v3.29.5 + uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,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@e296a935590eb16afc0c0108289f68c87e2a89a5 #v3.29.5 + uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 #v3.29.5 + uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5 From 450cd72da5518a3454558285e084382f1d0f1ebd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:50:25 -0400 Subject: [PATCH 02/38] chore(deps): bump modernc.org/sqlite from 1.39.0 to 1.39.1 (#4276) Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.39.0 to 1.39.1. - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.39.0...v1.39.1) --- updated-dependencies: - dependency-name: modernc.org/sqlite dependency-version: 1.39.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 1c8c6594c..dec8d83fa 100644 --- a/go.mod +++ b/go.mod @@ -92,7 +92,7 @@ require ( golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/mod v0.28.0 golang.org/x/net v0.46.0 - modernc.org/sqlite v1.39.0 + modernc.org/sqlite v1.39.1 ) require ( @@ -281,7 +281,7 @@ require ( google.golang.org/protobuf v1.36.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 - modernc.org/libc v1.66.3 // indirect + modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index d5c5b668b..e02786685 100644 --- a/go.sum +++ b/go.sum @@ -1489,18 +1489,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= -modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= -modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= -modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= -modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -1509,8 +1509,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY= -modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= +modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 1a58f27f876ee479e90adde525704dbd1d8b58f3 Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:50:41 -0400 Subject: [PATCH 03/38] chore(deps): update tools to latest versions (#4274) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com> --- .binny.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.binny.yaml b/.binny.yaml index 806e4f8fc..1edb0fdc8 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -42,7 +42,7 @@ tools: # used for signing the checksums file at release - name: cosign version: - want: v3.0.1 + want: v3.0.2 method: github-release with: repo: sigstore/cosign @@ -114,7 +114,7 @@ tools: # used to upload test fixture cache - name: yq version: - want: v4.47.2 + want: v4.48.1 method: github-release with: repo: mikefarah/yq From 89948dfa51cfa597d95d8209e76a0e9e34644095 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:50:49 -0400 Subject: [PATCH 04/38] chore(deps): bump golang.org/x/mod from 0.28.0 to 0.29.0 (#4266) Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.28.0 to 0.29.0. - [Commits](https://github.com/golang/mod/compare/v0.28.0...v0.29.0) --- updated-dependencies: - dependency-name: golang.org/x/mod dependency-version: 0.29.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index dec8d83fa..890a92ae3 100644 --- a/go.mod +++ b/go.mod @@ -90,7 +90,7 @@ require ( go.uber.org/goleak v1.3.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b - golang.org/x/mod v0.28.0 + golang.org/x/mod v0.29.0 golang.org/x/net v0.46.0 modernc.org/sqlite v1.39.1 ) diff --git a/go.sum b/go.sum index e02786685..e99144b44 100644 --- a/go.sum +++ b/go.sum @@ -1061,8 +1061,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= From 8ffe15c710ac330ecbdb3f4eec0dd997345e7606 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:50:59 -0400 Subject: [PATCH 05/38] chore(deps): bump golang.org/x/tools from 0.37.0 to 0.38.0 (#4265) Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.37.0 to 0.38.0. - [Release notes](https://github.com/golang/tools/releases) - [Commits](https://github.com/golang/tools/compare/v0.37.0...v0.38.0) --- updated-dependencies: - dependency-name: golang.org/x/tools dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 890a92ae3..8bfb81046 100644 --- a/go.mod +++ b/go.mod @@ -271,7 +271,7 @@ require ( golang.org/x/term v0.36.0 // indirect golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.37.0 + golang.org/x/tools v0.38.0 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/api v0.203.0 // indirect google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect diff --git a/go.sum b/go.sum index e99144b44..ae83871e3 100644 --- a/go.sum +++ b/go.sum @@ -1296,8 +1296,8 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 2d1ada1d0085ec60ac15f6cdb023b2a4014547ca Mon Sep 17 00:00:00 2001 From: Hala Ali <129986297+HalaAli198@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:10:42 -0400 Subject: [PATCH 06/38] fix: enhance setup.py parser to handle unquoted dependencies (#4255) * fix: add support for unquoted Python dependencies in setup.py - Add regex pattern to match unquoted package==version format - Handles common .split() pattern used in projects like mayan-edms - Maintains backward compatibility with quoted dependencies - Prevents duplicate package detection Signed-off-by: Hala Ali alih16@vcu.edu Signed-off-by: HalaAli198 * fix: apply gofmt formatting Signed-off-by: HalaAli198 * lint: incorporate new changes and refactor complexity Signed-off-by: Christopher Phillips --------- Signed-off-by: HalaAli198 Signed-off-by: Christopher Phillips Co-authored-by: Christopher Phillips --- syft/pkg/cataloger/python/parse_setup.go | 110 +++++++++++++----- syft/pkg/cataloger/python/parse_setup_test.go | 88 ++++++++++++++ .../test-fixtures/setup/mixed-format-setup.py | 17 +++ .../setup/multiline-split-setup.py | 23 ++++ 4 files changed, 207 insertions(+), 31 deletions(-) create mode 100644 syft/pkg/cataloger/python/test-fixtures/setup/mixed-format-setup.py create mode 100644 syft/pkg/cataloger/python/test-fixtures/setup/multiline-split-setup.py diff --git a/syft/pkg/cataloger/python/parse_setup.go b/syft/pkg/cataloger/python/parse_setup.go index 3332508ac..6a9148763 100644 --- a/syft/pkg/cataloger/python/parse_setup.go +++ b/syft/pkg/cataloger/python/parse_setup.go @@ -22,6 +22,7 @@ var _ generic.Parser = parseSetup // "mypy==v0.770", --> match(name=mypy version=v0.770) // " mypy2 == v0.770", ' mypy3== v0.770', --> match(name=mypy2 version=v0.770), match(name=mypy3, version=v0.770) var pinnedDependency = regexp.MustCompile(`['"]\W?(\w+\W?==\W?[\w.]*)`) +var unquotedPinnedDependency = regexp.MustCompile(`^\s*(\w+)\s*==\s*([\w\.\-]+)`) func parseSetup(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var packages []pkg.Package @@ -32,42 +33,89 @@ func parseSetup(_ context.Context, _ file.Resolver, _ *generic.Environment, read line := scanner.Text() line = strings.TrimRight(line, "\n") - for _, match := range pinnedDependency.FindAllString(line, -1) { - parts := strings.Split(match, "==") - if len(parts) != 2 { - continue - } - name := strings.Trim(parts[0], "'\"") - name = strings.TrimSpace(name) - name = strings.Trim(name, "'\"") - - version := strings.TrimSpace(parts[len(parts)-1]) - version = strings.Trim(version, "'\"") - - if hasTemplateDirective(name) || hasTemplateDirective(version) { - // this can happen in more dynamic setup.py where there is templating - continue - } - - if name == "" || version == "" { - log.WithFields("path", reader.RealPath).Debugf("unable to parse package in setup.py line: %q", line) - continue - } - - packages = append( - packages, - newPackageForIndex( - name, - version, - reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), - ), - ) - } + packages = processQuotedDependencies(line, reader, packages) + packages = processUnquotedDependency(line, reader, packages) } return packages, nil, nil } +func processQuotedDependencies(line string, reader file.LocationReadCloser, packages []pkg.Package) []pkg.Package { + for _, match := range pinnedDependency.FindAllString(line, -1) { + if p, ok := parseQuotedDependency(match, line, reader); ok { + packages = append(packages, p) + } + } + return packages +} + +func parseQuotedDependency(match, line string, reader file.LocationReadCloser) (pkg.Package, bool) { + parts := strings.Split(match, "==") + if len(parts) != 2 { + return pkg.Package{}, false + } + + name := cleanDependencyString(parts[0]) + version := cleanDependencyString(parts[len(parts)-1]) + + return validateAndCreatePackage(name, version, line, reader) +} + +// processUnquotedDependency extracts and processes an unquoted dependency from a line +func processUnquotedDependency(line string, reader file.LocationReadCloser, packages []pkg.Package) []pkg.Package { + matches := unquotedPinnedDependency.FindStringSubmatch(line) + if len(matches) != 3 { + return packages + } + + name := strings.TrimSpace(matches[1]) + version := strings.TrimSpace(matches[2]) + + if p, ok := validateAndCreatePackage(name, version, line, reader); ok { + if !isDuplicatePackage(p, packages) { + packages = append(packages, p) + } + } + + return packages +} + +func cleanDependencyString(s string) string { + s = strings.Trim(s, "'\"") + s = strings.TrimSpace(s) + s = strings.Trim(s, "'\"") + return s +} + +func validateAndCreatePackage(name, version, line string, reader file.LocationReadCloser) (pkg.Package, bool) { + if hasTemplateDirective(name) || hasTemplateDirective(version) { + // this can happen in more dynamic setup.py where there is templating + return pkg.Package{}, false + } + + if name == "" || version == "" { + log.WithFields("path", reader.RealPath).Debugf("unable to parse package in setup.py line: %q", line) + return pkg.Package{}, false + } + + p := newPackageForIndex( + name, + version, + reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + ) + + return p, true +} + +func isDuplicatePackage(p pkg.Package, packages []pkg.Package) bool { + for _, existing := range packages { + if existing.Name == p.Name && existing.Version == p.Version { + return true + } + } + return false +} + func hasTemplateDirective(s string) bool { return strings.Contains(s, `%s`) || strings.Contains(s, `{`) || strings.Contains(s, `}`) } diff --git a/syft/pkg/cataloger/python/parse_setup_test.go b/syft/pkg/cataloger/python/parse_setup_test.go index 665007296..74cb604a5 100644 --- a/syft/pkg/cataloger/python/parse_setup_test.go +++ b/syft/pkg/cataloger/python/parse_setup_test.go @@ -61,6 +61,94 @@ func TestParseSetup(t *testing.T) { fixture: "test-fixtures/setup/dynamic-setup.py", expected: nil, }, + { + fixture: "test-fixtures/setup/multiline-split-setup.py", + expected: []pkg.Package{ + { + Name: "black", + Version: "23.12.1", + PURL: "pkg:pypi/black@23.12.1", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "cairosvg", + Version: "2.7.1", + PURL: "pkg:pypi/cairosvg@2.7.1", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "celery", + Version: "5.3.4", + PURL: "pkg:pypi/celery@5.3.4", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "django", + Version: "4.2.23", + PURL: "pkg:pypi/django@4.2.23", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "mypy", + Version: "1.7.1", + PURL: "pkg:pypi/mypy@1.7.1", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "pillow", + Version: "11.0.0", + PURL: "pkg:pypi/pillow@11.0.0", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "pytest", + Version: "7.4.3", + PURL: "pkg:pypi/pytest@7.4.3", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "requests", + Version: "2.31.0", + PURL: "pkg:pypi/requests@2.31.0", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + }, + }, + { + // Test mixed quoted and unquoted dependencies - ensure no duplicates + fixture: "test-fixtures/setup/mixed-format-setup.py", + expected: []pkg.Package{ + { + Name: "requests", + Version: "2.31.0", + PURL: "pkg:pypi/requests@2.31.0", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "django", + Version: "4.2.23", + PURL: "pkg:pypi/django@4.2.23", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + { + Name: "flask", + Version: "3.0.0", + PURL: "pkg:pypi/flask@3.0.0", + Language: pkg.Python, + Type: pkg.PythonPkg, + }, + }, + }, } for _, tt := range tests { diff --git a/syft/pkg/cataloger/python/test-fixtures/setup/mixed-format-setup.py b/syft/pkg/cataloger/python/test-fixtures/setup/mixed-format-setup.py new file mode 100644 index 000000000..eec597bd0 --- /dev/null +++ b/syft/pkg/cataloger/python/test-fixtures/setup/mixed-format-setup.py @@ -0,0 +1,17 @@ +from setuptools import setup + +# Test case to ensure duplicate detection works correctly +# when same dependencies appear in both quoted and unquoted forms + +setup( + name='mixed-format-project', + version='1.0.0', + install_requires=[ + # Quoted dependencies (should be caught by pinnedDependency regex) + "requests==2.31.0", + "django==4.2.23", + ] + """ +requests==2.31.0 +flask==3.0.0 +""".split(), +) diff --git a/syft/pkg/cataloger/python/test-fixtures/setup/multiline-split-setup.py b/syft/pkg/cataloger/python/test-fixtures/setup/multiline-split-setup.py new file mode 100644 index 000000000..a14b27ae7 --- /dev/null +++ b/syft/pkg/cataloger/python/test-fixtures/setup/multiline-split-setup.py @@ -0,0 +1,23 @@ +from setuptools import setup + +# Example setup.py using multiline string with .split() pattern +# This pattern is commonly seen in projects like mayan-edms + +setup( + name='example-project', + version='1.0.0', + install_requires=""" +django==4.2.23 +CairoSVG==2.7.1 +Pillow==11.0.0 +requests==2.31.0 +celery==5.3.4 +""".split(), + extras_require={ + 'dev': """ +pytest==7.4.3 +black==23.12.1 +mypy==1.7.1 +""".split(), + }, +) From 760bd9a50a7eda127a6f3adfc45edbcd8a6c6bb9 Mon Sep 17 00:00:00 2001 From: Doug Clarke Date: Mon, 13 Oct 2025 15:59:08 -0400 Subject: [PATCH 07/38] feat: Pom xml only archive parser (#4272) fix: identifying jar files with a single pom.xml and no pom.properties file fix: test works with pom.xml being found, used and reported in metadata Signed-off-by: Doug Clarke test: check for current project path and use Signed-off-by: Christopher Phillips --------- Signed-off-by: Doug Clarke Signed-off-by: Christopher Phillips Co-authored-by: Christopher Phillips --- syft/pkg/cataloger/java/archive_parser.go | 113 +++++++++++++----- .../pkg/cataloger/java/archive_parser_test.go | 105 ++++++++++++++++ .../java/test-fixtures/jar-metadata/Makefile | 7 +- .../micronaut-aop-4.9.11/META-INF/MANIFEST.MF | 5 + .../maven/io.micronaut/micronaut-aop/pom.xml | 15 +++ 5 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/MANIFEST.MF create mode 100644 syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/maven/io.micronaut/micronaut-aop/pom.xml diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 382414d83..5c2a22087 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -257,10 +257,14 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, return nil, err } - name, version, lics, err := j.discoverNameVersionLicense(ctx, manifest) + name, version, lics, parsedPom, err := j.discoverNameVersionLicense(ctx, manifest) if err != nil { return nil, err } + var pkgPomProject *pkg.JavaPomProject + if parsedPom != nil { + pkgPomProject = newPomProject(ctx, nil, parsedPom.path, parsedPom.project) + } return &pkg.Package{ // TODO: maybe select name should just have a pom properties in it? @@ -275,12 +279,13 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, Metadata: pkg.JavaArchive{ VirtualPath: j.location.Path(), Manifest: manifest, + PomProject: pkgPomProject, ArchiveDigests: digests, }, }, nil } -func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest *pkg.JavaManifest) (string, string, []pkg.License, error) { +func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest *pkg.JavaManifest) (string, string, []pkg.License, *parsedPomProject, error) { // we use j.location because we want to associate the license declaration with where we discovered the contents in the manifest // TODO: when we support locations of paths within archives we should start passing the specific manifest location object instead of the top jar lics := pkg.NewLicensesFromLocationWithContext(ctx, j.location, selectLicenses(manifest)...) @@ -302,7 +307,7 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest if len(lics) == 0 { fileLicenses, err := j.getLicenseFromFileInArchive(ctx) if err != nil { - return "", "", nil, err + return "", "", nil, parsedPom, err } if fileLicenses != nil { lics = append(lics, fileLicenses...) @@ -317,7 +322,7 @@ func (j *archiveParser) discoverNameVersionLicense(ctx context.Context, manifest lics = j.findLicenseFromJavaMetadata(ctx, groupID, artifactID, version, parsedPom, manifest) } - return artifactID, version, lics, nil + return artifactID, version, lics, parsedPom, nil } // findLicenseFromJavaMetadata attempts to find license information from all available maven metadata properties and pom info @@ -387,43 +392,93 @@ type parsedPomProject struct { // discoverMainPackageFromPomInfo attempts to resolve maven groupId, artifactId, version and other info from found pom information func (j *archiveParser) discoverMainPackageFromPomInfo(ctx context.Context) (group, name, version string, parsedPom *parsedPomProject) { - var pomProperties pkg.JavaPomProperties - - // Find the pom.properties/pom.xml if the names seem like a plausible match properties, _ := pomPropertiesByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomPropertiesGlob)) projects, _ := pomProjectByParentPath(j.archivePath, j.location, j.fileManifest.GlobMatch(false, pomXMLGlob)) - // map of all the artifacts in the pom properties, in order to chek exact match with the filename + artifactsMap := j.buildArtifactsMap(properties) + pomProperties, parsedPom := j.findBestPomMatch(properties, projects, artifactsMap) + + parsedPom = j.handleSinglePomXML(properties, projects, parsedPom) + + return j.resolveIdentity(ctx, pomProperties, parsedPom) +} + +func (j *archiveParser) buildArtifactsMap(properties map[string]pkg.JavaPomProperties) *strset.Set { artifactsMap := strset.New() for _, propertiesObj := range properties { artifactsMap.Add(propertiesObj.ArtifactID) } + return artifactsMap +} + +func (j *archiveParser) findBestPomMatch(properties map[string]pkg.JavaPomProperties, + projects map[string]*parsedPomProject, artifactsMap *strset.Set) (pkg.JavaPomProperties, *parsedPomProject) { + var pomProperties pkg.JavaPomProperties + var parsedPom *parsedPomProject for parentPath, propertiesObj := range sortedIter(properties) { - // the logic for selecting the best name is as follows: - // if we find an artifact id AND group id which are both contained in the filename - // OR if we have an artifact id that exactly matches the filename, prefer this - // OTHERWISE track the first matching pom properties with a pom.xml - // FINALLY return the first matching pom properties - if artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) { - if pomProperties.ArtifactID == "" { // keep the first match, or overwrite if we find more specific entries - pomProperties = propertiesObj - } - if proj, exists := projects[parentPath]; exists { - if parsedPom == nil { // keep the first matching artifact if we don't find an exact match or groupid + artfiact id match - pomProperties = propertiesObj // set this, as it may not be the first entry found - parsedPom = proj - } - // if artifact ID is the entire filename or BOTH artifactID and groupID are contained in the artifact, prefer this match - if strings.Contains(j.fileInfo.name, propertiesObj.GroupID) || j.fileInfo.name == propertiesObj.ArtifactID { - pomProperties = propertiesObj // this is an exact match, use it - parsedPom = proj - break - } - } + if !artifactIDMatchesFilename(propertiesObj.ArtifactID, j.fileInfo.name, artifactsMap) { + continue + } + + pomProperties, parsedPom = j.updateMatchIfBetter(pomProperties, parsedPom, propertiesObj, parentPath, projects) + + if j.isExactMatch(propertiesObj, parsedPom) { + break } } + return pomProperties, parsedPom +} + +func (j *archiveParser) updateMatchIfBetter(currentProps pkg.JavaPomProperties, currentPom *parsedPomProject, + newProps pkg.JavaPomProperties, parentPath string, projects map[string]*parsedPomProject) (pkg.JavaPomProperties, *parsedPomProject) { + // Keep the first match + if currentProps.ArtifactID == "" { + proj, hasProject := projects[parentPath] + if hasProject { + return newProps, proj + } + return newProps, currentPom + } + + proj, hasProject := projects[parentPath] + if !hasProject { + return currentProps, currentPom + } + + // Keep the first matching artifact with a pom.xml + if currentPom == nil { + return newProps, proj + } + + // Prefer exact matches + if j.isExactMatch(newProps, proj) { + return newProps, proj + } + + return currentProps, currentPom +} + +func (j *archiveParser) isExactMatch(props pkg.JavaPomProperties, pom *parsedPomProject) bool { + if pom == nil { + return false + } + return strings.Contains(j.fileInfo.name, props.GroupID) || j.fileInfo.name == props.ArtifactID +} + +func (j *archiveParser) handleSinglePomXML(properties map[string]pkg.JavaPomProperties, + projects map[string]*parsedPomProject, currentPom *parsedPomProject) *parsedPomProject { + if len(properties) == 0 && len(projects) == 1 { + for _, projectsObj := range projects { + return projectsObj + } + } + return currentPom +} + +func (j *archiveParser) resolveIdentity(ctx context.Context, pomProperties pkg.JavaPomProperties, + parsedPom *parsedPomProject) (group, name, version string, pom *parsedPomProject) { group = pomProperties.GroupID name = pomProperties.ArtifactID version = pomProperties.Version diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index b3d656ecf..9bbc4c39c 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -79,9 +79,12 @@ func TestSearchMavenForLicenses(t *testing.T) { ReadCloser: fixture, }, tc.detectNested, tc.config) defer cleanupFn() + require.NoError(t, err) // assert licenses are discovered from upstream _, _, _, parsedPom := ap.discoverMainPackageFromPomInfo(context.Background()) + require.NotNil(t, parsedPom, "expected to find pom information in the fixture") + require.NotNil(t, parsedPom.project, "expected parsedPom to have a project") resolvedLicenses, _ := ap.maven.ResolveLicenses(context.Background(), parsedPom.project) assert.Equal(t, tc.expectedLicenses, toPkgLicenses(ctx, nil, resolvedLicenses)) }) @@ -148,10 +151,23 @@ func TestParseJar(t *testing.T) { }, PomProperties: &pkg.JavaPomProperties{ Path: "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.properties", + Name: "", GroupID: "io.jenkins.plugins", ArtifactID: "example-jenkins-plugin", Version: "1.0-SNAPSHOT", }, + PomProject: &pkg.JavaPomProject{ + Path: "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.xml", + Name: "Example Jenkins Plugin", + GroupID: "io.jenkins.plugins", + ArtifactID: "example-jenkins-plugin", + Version: "1.0-SNAPSHOT", + Parent: &pkg.JavaPomParent{ + GroupID: "org.jenkins-ci.plugins", + ArtifactID: "plugin", + Version: "4.46", + }, + }, }, }, }, @@ -189,6 +205,14 @@ func TestParseJar(t *testing.T) { }, }, }, + // PomProject: &pkg.JavaPomProject{ + // Path: "META-INF/maven/io.jenkins.plugins/example-jenkins-plugin/pom.xml", + // Parent: &pkg.JavaPomParent{GroupID: "org.jenkins-ci.plugins", ArtifactID: "plugin", Version: "4.46"}, + // GroupID: "io.jenkins.plugins", + // ArtifactID: "example-jenkins-plugin", + // Version: "1.0-SNAPSHOT", + // Name: "Example Jenkins Plugin", + // }, }, }, "joda-time": { @@ -286,6 +310,12 @@ func TestParseJar(t *testing.T) { ArtifactID: "example-java-app-maven", Version: "0.1.0", }, + PomProject: &pkg.JavaPomProject{ + Path: "META-INF/maven/org.anchore/example-java-app-maven/pom.xml", + GroupID: "org.anchore", + ArtifactID: "example-java-app-maven", + Version: "0.1.0", + }, }, }, "joda-time": { @@ -1127,6 +1157,13 @@ func Test_parseJavaArchive_regressions(t *testing.T) { GroupID: "org.apache.directory.api", ArtifactID: "api-all", Version: "2.0.0", + }, PomProject: &pkg.JavaPomProject{ + Path: "META-INF/maven/org.apache.directory.api/api-all/pom.xml", + ArtifactID: "api-all", + GroupID: "org.apache.directory.api", + Version: "2.0.0", + Name: "Apache Directory API All", + Parent: &pkg.JavaPomParent{GroupID: "org.apache.directory.api", ArtifactID: "api-parent", Version: "2.0.0"}, }, }, } @@ -1163,6 +1200,46 @@ func Test_parseJavaArchive_regressions(t *testing.T) { }, } + micronautAop := pkg.Package{ + Name: "micronaut-aop", + Version: "4.9.11", + PURL: "pkg:maven/io.micronaut/micronaut-aop@4.9.11", + Locations: file.NewLocationSet(file.NewLocation("test-fixtures/jar-metadata/cache/micronaut-aop-4.9.11.jar")), + Type: pkg.JavaPkg, + Language: pkg.Java, + Metadata: pkg.JavaArchive{ + VirtualPath: "test-fixtures/jar-metadata/cache/micronaut-aop-4.9.11.jar", + Manifest: &pkg.JavaManifest{ + Main: []pkg.KeyValue{ + { + Key: "Manifest-Version", + Value: "1.0", + }, + { + Key: "Automatic-Module-Name", + Value: "io.micronaut.micronaut_aop", + }, + { + Key: "Implementation-Version", + Value: "4.9.11", + }, + { + Key: "Implementation-Title", + Value: "Micronaut Core", + }, + }, + }, PomProject: &pkg.JavaPomProject{ + Path: "META-INF/maven/io.micronaut/micronaut-aop/pom.xml", + ArtifactID: "micronaut-aop", + GroupID: "io.micronaut", + Version: "4.9.11", + Name: "Micronaut Core", + Description: "Core components supporting the Micronaut Framework", + URL: "https://micronaut.io", + }, + }, + } + tests := []struct { name string fixtureName string @@ -1220,6 +1297,16 @@ func Test_parseJavaArchive_regressions(t *testing.T) { {Key: "Specification-Version", Value: "2.15.2"}, }, }, + PomProject: &pkg.JavaPomProject{ + Path: "META-INF/maven/com.fasterxml.jackson.core/jackson-core/pom.xml", + ArtifactID: "jackson-core", + GroupID: "com.fasterxml.jackson.core", + Version: "2.15.2", + Name: "Jackson-core", + Description: "Core Jackson processing abstractions (aka Streaming API), implementation for JSON", + URL: "https://github.com/FasterXML/jackson-core", + Parent: &pkg.JavaPomParent{GroupID: "com.fasterxml.jackson", ArtifactID: "jackson-base", Version: "2.15.2"}, + }, // not under test //ArchiveDigests: []file.Digest{{Algorithm: "sha1", Value: "d8bc1d9c428c96fe447e2c429fc4304d141024df"}}, }, @@ -1275,6 +1362,16 @@ func Test_parseJavaArchive_regressions(t *testing.T) { {Key: "Specification-Version", Value: "2.15.2"}, }, }, + PomProject: &pkg.JavaPomProject{ + Path: "META-INF/maven/com.fasterxml.jackson.core/jackson-core/pom.xml", + ArtifactID: "jackson-core", + GroupID: "com.fasterxml.jackson.core", + Version: "2.15.2", + Name: "Jackson-core", + Description: "Core Jackson processing abstractions (aka Streaming API), implementation for JSON", + URL: "https://github.com/FasterXML/jackson-core", + Parent: &pkg.JavaPomParent{GroupID: "com.fasterxml.jackson", ArtifactID: "jackson-base", Version: "2.15.2"}, + }, // not under test //ArchiveDigests: []file.Digest{{Algorithm: "sha1", Value: "abd3e329270fc54a2acaceb45420fd5710ecefd5"}}, }, @@ -1341,6 +1438,14 @@ func Test_parseJavaArchive_regressions(t *testing.T) { }, }, }, + { + name: "micronaut-aop", + fixtureName: "micronaut-aop-4.9.11", + fileExtension: "jar", + expectedPkgs: []pkg.Package{ + micronautAop, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile index c8904e725..a7781c168 100644 --- a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile +++ b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile @@ -13,6 +13,7 @@ API_ALL_SOURCES = api-all-2.0.0-sources SPRING_INSTRUMENTATION = spring-instrumentation-4.3.0-1.0 MULTIPLE_MATCHING = multiple-matching-2.11.5 ORG_MULTIPLE_THENAME = org.multiple-thename +MICRONAUT_AOP = micronaut-aop-4.9.11 .DEFAULT_GOAL := fixtures @@ -23,7 +24,7 @@ fixtures: $(CACHE_DIR) # requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted fingerprint: $(FINGERPRINT_FILE) -$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar +$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar $(CACHE_DIR)/$(JACKSON_CORE).jar: mkdir -p $(CACHE_DIR) @@ -53,6 +54,10 @@ $(CACHE_DIR)/$(ORG_MULTIPLE_THENAME).jar: mkdir -p $(CACHE_DIR) cd $(ORG_MULTIPLE_THENAME) && zip -r $(CACHE_PATH)/$(ORG_MULTIPLE_THENAME).jar . +$(CACHE_DIR)/$(MICRONAUT_AOP).jar: + mkdir -p $(CACHE_DIR) + cd $(MICRONAUT_AOP) && zip -r $(CACHE_PATH)/$(MICRONAUT_AOP).jar . + # Jenkins plugins typically do not have the version included in the archive name, # so it is important to not include it in the generated test fixture $(CACHE_DIR)/gradle.hpi: diff --git a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/MANIFEST.MF b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/MANIFEST.MF new file mode 100644 index 000000000..745833dbb --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/MANIFEST.MF @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +Automatic-Module-Name: io.micronaut.micronaut_aop +Implementation-Version: 4.9.11 +Implementation-Title: Micronaut Core + diff --git a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/maven/io.micronaut/micronaut-aop/pom.xml b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/maven/io.micronaut/micronaut-aop/pom.xml new file mode 100644 index 000000000..310d05a48 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/micronaut-aop-4.9.11/META-INF/maven/io.micronaut/micronaut-aop/pom.xml @@ -0,0 +1,15 @@ + + + + + + + + 4.0.0 + io.micronaut + micronaut-aop + 4.9.11 + Micronaut Core + Core components supporting the Micronaut Framework + https://micronaut.io + From d22914baf51d9da7e33364b9c94bf190106011fa Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 14 Oct 2025 13:58:31 -0400 Subject: [PATCH 08/38] add docs to configs (#4281) Signed-off-by: Alex Goodman --- .gitignore | 2 + internal/packagemetadata/generate/main.go | 3 +- internal/sourcemetadata/generate/main.go | 3 +- .../spdxhelpers/to_format_model_test.go | 2 +- .../github/internal/model/model_test.go | 2 +- .../cyclonedxutil/helpers/component.go | 2 +- .../spdxutil/helpers/document_name_test.go | 2 +- .../helpers/document_namespace_test.go | 2 +- .../helpers/originator_supplier_test.go | 2 +- syft/format/syftjson/model/source_test.go | 2 +- syft/format/syftjson/schema_test.go | 3 +- syft/format/syftjson/to_format_model_test.go | 2 +- syft/format/syftjson/to_syft_model_test.go | 2 +- syft/get_source_test.go | 2 +- syft/pkg/cataloger/bitnami/package.go | 2 +- syft/pkg/cataloger/dotnet/config.go | 4 ++ syft/pkg/cataloger/golang/config.go | 49 +++++++++++++++---- .../internal/pkgtest/test_generic_parser.go | 3 ++ syft/pkg/cataloger/java/cataloger.go | 7 ++- syft/pkg/cataloger/java/config.go | 30 +++++++++--- .../cataloger/java/parse_gradle_lockfile.go | 2 - syft/pkg/cataloger/java/parse_jvm_release.go | 4 -- syft/pkg/cataloger/javascript/config.go | 12 +++-- syft/pkg/cataloger/kernel/cataloger.go | 2 + syft/pkg/cataloger/nix/cataloger.go | 2 + syft/pkg/cataloger/python/cataloger.go | 2 + syft/pkg/cataloger/rust/cataloger.go | 4 +- syft/pkg/cataloger/rust/package.go | 1 - 28 files changed, 110 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 3ba60dc50..9c0909fcf 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ bin/ /snapshot /.tool /.task +/generate +/specs # changelog generation CHANGELOG.md diff --git a/internal/packagemetadata/generate/main.go b/internal/packagemetadata/generate/main.go index 88339abff..f452a6934 100644 --- a/internal/packagemetadata/generate/main.go +++ b/internal/packagemetadata/generate/main.go @@ -5,8 +5,9 @@ import ( "os" "strings" - "github.com/anchore/syft/internal/packagemetadata" "github.com/dave/jennifer/jen" + + "github.com/anchore/syft/internal/packagemetadata" ) // This program is invoked from syft/internal and generates packagemetadata/generated.go diff --git a/internal/sourcemetadata/generate/main.go b/internal/sourcemetadata/generate/main.go index a56bfc832..c7f73b841 100644 --- a/internal/sourcemetadata/generate/main.go +++ b/internal/sourcemetadata/generate/main.go @@ -4,8 +4,9 @@ import ( "fmt" "os" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/dave/jennifer/jen" + + "github.com/anchore/syft/internal/sourcemetadata" ) // This program is invoked from syft/internal and generates sourcemetadata/generated.go diff --git a/syft/format/common/spdxhelpers/to_format_model_test.go b/syft/format/common/spdxhelpers/to_format_model_test.go index ae4bc9e7b..fdc0f096f 100644 --- a/syft/format/common/spdxhelpers/to_format_model_test.go +++ b/syft/format/common/spdxhelpers/to_format_model_test.go @@ -7,7 +7,6 @@ import ( "strings" "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/spdx/tools-golang/spdx" @@ -16,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/anchore/syft/internal/relationship" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/format/internal/spdxutil/helpers" diff --git a/syft/format/github/internal/model/model_test.go b/syft/format/github/internal/model/model_test.go index de6f30a6d..308086436 100644 --- a/syft/format/github/internal/model/model_test.go +++ b/syft/format/github/internal/model/model_test.go @@ -3,12 +3,12 @@ package model import ( "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/pkg" diff --git a/syft/format/internal/cyclonedxutil/helpers/component.go b/syft/format/internal/cyclonedxutil/helpers/component.go index e0e9ab774..a8ce96686 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component.go +++ b/syft/format/internal/cyclonedxutil/helpers/component.go @@ -6,9 +6,9 @@ import ( "strings" "github.com/CycloneDX/cyclonedx-go" - "github.com/anchore/syft/internal/packagemetadata" "github.com/anchore/packageurl-go" + "github.com/anchore/syft/internal/packagemetadata" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/format/internal" "github.com/anchore/syft/syft/pkg" diff --git a/syft/format/internal/spdxutil/helpers/document_name_test.go b/syft/format/internal/spdxutil/helpers/document_name_test.go index cd6f5b4f7..d4007fc9d 100644 --- a/syft/format/internal/spdxutil/helpers/document_name_test.go +++ b/syft/format/internal/spdxutil/helpers/document_name_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/stretchr/testify/assert" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/source" ) diff --git a/syft/format/internal/spdxutil/helpers/document_namespace_test.go b/syft/format/internal/spdxutil/helpers/document_namespace_test.go index c39723ae3..2db754663 100644 --- a/syft/format/internal/spdxutil/helpers/document_namespace_test.go +++ b/syft/format/internal/spdxutil/helpers/document_namespace_test.go @@ -5,9 +5,9 @@ import ( "strings" "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/stretchr/testify/assert" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index b7ee3d6f7..a720a814d 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -3,9 +3,9 @@ package helpers import ( "testing" - "github.com/anchore/syft/internal/packagemetadata" "github.com/stretchr/testify/assert" + "github.com/anchore/syft/internal/packagemetadata" "github.com/anchore/syft/syft/pkg" ) diff --git a/syft/format/syftjson/model/source_test.go b/syft/format/syftjson/model/source_test.go index ec45166a7..ad8538ffc 100644 --- a/syft/format/syftjson/model/source_test.go +++ b/syft/format/syftjson/model/source_test.go @@ -4,11 +4,11 @@ import ( "encoding/json" "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/source" ) diff --git a/syft/format/syftjson/schema_test.go b/syft/format/syftjson/schema_test.go index fafbc9c20..2e0a28e72 100644 --- a/syft/format/syftjson/schema_test.go +++ b/syft/format/syftjson/schema_test.go @@ -6,9 +6,10 @@ import ( "path/filepath" "testing" - "github.com/anchore/syft/internal/packagemetadata" "github.com/iancoleman/strcase" "github.com/stretchr/testify/require" + + "github.com/anchore/syft/internal/packagemetadata" ) type schema struct { diff --git a/syft/format/syftjson/to_format_model_test.go b/syft/format/syftjson/to_format_model_test.go index 83f8f526b..bb493176a 100644 --- a/syft/format/syftjson/to_format_model_test.go +++ b/syft/format/syftjson/to_format_model_test.go @@ -4,13 +4,13 @@ import ( "encoding/json" "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" stereoscopeFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/format/syftjson/model" "github.com/anchore/syft/syft/pkg" diff --git a/syft/format/syftjson/to_syft_model_test.go b/syft/format/syftjson/to_syft_model_test.go index dfba66d5d..e0e6d7246 100644 --- a/syft/format/syftjson/to_syft_model_test.go +++ b/syft/format/syftjson/to_syft_model_test.go @@ -7,11 +7,11 @@ import ( "os" "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" stereoFile "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/format/syftjson/model" diff --git a/syft/get_source_test.go b/syft/get_source_test.go index a5b17e899..241854c83 100644 --- a/syft/get_source_test.go +++ b/syft/get_source_test.go @@ -3,10 +3,10 @@ package syft import ( "testing" - "github.com/anchore/syft/internal/sourcemetadata" "github.com/stretchr/testify/require" "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/internal/sourcemetadata" "github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source/sourceproviders" ) diff --git a/syft/pkg/cataloger/bitnami/package.go b/syft/pkg/cataloger/bitnami/package.go index 354dbe320..db29aec48 100644 --- a/syft/pkg/cataloger/bitnami/package.go +++ b/syft/pkg/cataloger/bitnami/package.go @@ -7,7 +7,7 @@ import ( "slices" "strings" - version "github.com/bitnami/go-version/pkg/version" + "github.com/bitnami/go-version/pkg/version" "github.com/anchore/packageurl-go" "github.com/anchore/syft/syft/artifact" diff --git a/syft/pkg/cataloger/dotnet/config.go b/syft/pkg/cataloger/dotnet/config.go index 3c6c1d4c1..b959747d9 100644 --- a/syft/pkg/cataloger/dotnet/config.go +++ b/syft/pkg/cataloger/dotnet/config.go @@ -2,17 +2,21 @@ package dotnet type CatalogerConfig struct { // DepPackagesMustHaveDLL allows for deps.json packages to be included only if there is a DLL on disk for that package. + // app-config: dotnet.dep-packages-must-have-dll DepPackagesMustHaveDLL bool `mapstructure:"dep-packages-must-have-dll" json:"dep-packages-must-have-dll" yaml:"dep-packages-must-have-dll"` // DepPackagesMustClaimDLL allows for deps.json packages to be included only if there is a runtime/resource DLL claimed in the deps.json targets section. // This does not require such claimed DLLs to exist on disk. The behavior of this + // app-config: dotnet.dep-packages-must-claim-dll DepPackagesMustClaimDLL bool `mapstructure:"dep-packages-must-claim-dll" json:"dep-packages-must-claim-dll" yaml:"dep-packages-must-claim-dll"` // PropagateDLLClaimsToParents allows for deps.json packages to be included if any child (transitive) package claims a DLL. This applies to both the claims configuration and evidence-on-disk configurations. + // app-config: dotnet.propagate-dll-claims-to-parents PropagateDLLClaimsToParents bool `mapstructure:"propagate-dll-claims-to-parents" json:"propagate-dll-claims-to-parents" yaml:"propagate-dll-claims-to-parents"` // RelaxDLLClaimsWhenBundlingDetected will look for indications of IL bundle tooling via deps.json package names // and, if found (and this config option is enabled), will relax the DepPackagesMustClaimDLL value to `false` only in those cases. + // app-config: dotnet.relax-dll-claims-when-bundling-detected RelaxDLLClaimsWhenBundlingDetected bool `mapstructure:"relax-dll-claims-when-bundling-detected" json:"relax-dll-claims-when-bundling-detected" yaml:"relax-dll-claims-when-bundling-detected"` } diff --git a/syft/pkg/cataloger/golang/config.go b/syft/pkg/cataloger/golang/config.go index 36dca0fbc..d173f4a7c 100644 --- a/syft/pkg/cataloger/golang/config.go +++ b/syft/pkg/cataloger/golang/config.go @@ -19,19 +19,48 @@ var ( ) type CatalogerConfig struct { - SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"` - LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"` - SearchLocalVendorLicenses bool `yaml:"search-local-vendor-licenses" json:"search-local-vendor-licenses" mapstructure:"search-local-vendor-licenses"` - LocalVendorDir string `yaml:"local-vendor-dir" json:"local-vendor-dir" mapstructure:"local-vendor-dir"` - SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"` - Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"` - NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"` - MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"` + // SearchLocalModCacheLicenses enables searching for go package licenses in the local GOPATH mod cache. + // app-config: golang.search-local-mod-cache-licenses + SearchLocalModCacheLicenses bool `yaml:"search-local-mod-cache-licenses" json:"search-local-mod-cache-licenses" mapstructure:"search-local-mod-cache-licenses"` + + // LocalModCacheDir specifies the location of the local go module cache directory. When not set, syft will attempt to discover the GOPATH env or default to $HOME/go. + // app-config: golang.local-mod-cache-dir + LocalModCacheDir string `yaml:"local-mod-cache-dir" json:"local-mod-cache-dir" mapstructure:"local-mod-cache-dir"` + + // SearchLocalVendorLicenses enables searching for go package licenses in the local vendor directory relative to the go.mod file. + // app-config: golang.search-local-vendor-licenses + SearchLocalVendorLicenses bool `yaml:"search-local-vendor-licenses" json:"search-local-vendor-licenses" mapstructure:"search-local-vendor-licenses"` + + // LocalVendorDir specifies the location of the local vendor directory. When not set, syft will search for a vendor directory relative to the go.mod file. + // app-config: golang.local-vendor-dir + LocalVendorDir string `yaml:"local-vendor-dir" json:"local-vendor-dir" mapstructure:"local-vendor-dir"` + + // SearchRemoteLicenses enables downloading go package licenses from the upstream go proxy (typically proxy.golang.org). + // app-config: golang.search-remote-licenses + SearchRemoteLicenses bool `yaml:"search-remote-licenses" json:"search-remote-licenses" mapstructure:"search-remote-licenses"` + + // Proxies is a list of go module proxies to use when fetching go module metadata and licenses. When not set, syft will use the GOPROXY env or default to https://proxy.golang.org,direct. + // app-config: golang.proxy + Proxies []string `yaml:"proxies,omitempty" json:"proxies,omitempty" mapstructure:"proxies"` + + // NoProxy is a list of glob patterns that match go module names that should not be fetched from the go proxy. When not set, syft will use the GOPRIVATE and GONOPROXY env vars. + // app-config: golang.no-proxy + NoProxy []string `yaml:"no-proxy,omitempty" json:"no-proxy,omitempty" mapstructure:"no-proxy"` + + MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"` } type MainModuleVersionConfig struct { - FromLDFlags bool `yaml:"from-ld-flags" json:"from-ld-flags" mapstructure:"from-ld-flags"` - FromContents bool `yaml:"from-contents" json:"from-contents" mapstructure:"from-contents"` + // FromLDFlags enables parsing the main module version from the -ldflags build settings. + // app-config: golang.main-module-version.from-ld-flags + FromLDFlags bool `yaml:"from-ld-flags" json:"from-ld-flags" mapstructure:"from-ld-flags"` + + // FromContents enables parsing the main module version from the binary contents. This is useful when the version is embedded in the binary but not in the build settings. + // app-config: golang.main-module-version.from-contents + FromContents bool `yaml:"from-contents" json:"from-contents" mapstructure:"from-contents"` + + // FromBuildSettings enables parsing the main module version from the go build settings. + // app-config: golang.main-module-version.from-build-settings FromBuildSettings bool `yaml:"from-build-settings" json:"from-build-settings" mapstructure:"from-build-settings"` } diff --git a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go index 28cffb02f..493d44355 100644 --- a/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go +++ b/syft/pkg/cataloger/internal/pkgtest/test_generic_parser.go @@ -336,6 +336,9 @@ func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationshi opts = append(opts, p.compareOptions...) opts = append(opts, cmp.Reporter(&r)) + // ignore the "FoundBy" field on relationships as it is set in the generic cataloger before it's presence on the relationship + opts = append(opts, cmpopts.IgnoreFields(pkg.Package{}, "FoundBy")) + // order should not matter relationship.Sort(p.expectedRelationships) relationship.Sort(relationships) diff --git a/syft/pkg/cataloger/java/cataloger.go b/syft/pkg/cataloger/java/cataloger.go index 2affb83fd..1e48b05e1 100644 --- a/syft/pkg/cataloger/java/cataloger.go +++ b/syft/pkg/cataloger/java/cataloger.go @@ -41,11 +41,14 @@ func NewPomCataloger(cfg ArchiveCatalogerConfig) pkg.Cataloger { // Note: Older versions of lockfiles aren't supported yet func NewGradleLockfileCataloger() pkg.Cataloger { return generic.NewCataloger("java-gradle-lockfile-cataloger"). - WithParserByGlobs(parseGradleLockfile, gradleLockfileGlob) + WithParserByGlobs(parseGradleLockfile, "**/gradle.lockfile*") } // NewJvmDistributionCataloger returns packages representing JDK/JRE installations (of multiple distribution types). func NewJvmDistributionCataloger() pkg.Cataloger { return generic.NewCataloger("java-jvm-cataloger"). - WithParserByGlobs(parseJVMRelease, jvmReleaseGlob) + // this is a very permissive glob that will match more than just the JVM release file. + // we started with "**/{java,jvm}/*/release", but this prevents scanning JVM archive contents (e.g. jdk8u402.zip). + // this approach lets us check more files for JVM release info, but be rather silent about errors. + WithParserByGlobs(parseJVMRelease, "**/release") } diff --git a/syft/pkg/cataloger/java/config.go b/syft/pkg/cataloger/java/config.go index ed85a308b..cea7a009d 100644 --- a/syft/pkg/cataloger/java/config.go +++ b/syft/pkg/cataloger/java/config.go @@ -9,12 +9,30 @@ import ( type ArchiveCatalogerConfig struct { cataloging.ArchiveSearchConfig `yaml:",inline" json:"" mapstructure:",squash"` - UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"` - UseMavenLocalRepository bool `yaml:"use-maven-localrepository" json:"use-maven-localrepository" mapstructure:"use-maven-localrepository"` - MavenLocalRepositoryDir string `yaml:"maven-localrepository-dir" json:"maven-localrepository-dir" mapstructure:"maven-localrepository-dir"` - MavenBaseURL string `yaml:"maven-base-url" json:"maven-base-url" mapstructure:"maven-base-url"` - MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"` - ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"` + + // UseNetwork enables network operations for java package metadata enrichment, such as fetching parent POMs and license information. + // app-config: java.use-network + UseNetwork bool `yaml:"use-network" json:"use-network" mapstructure:"use-network"` + + // UseMavenLocalRepository enables searching the local maven repository (~/.m2/repository by default) for parent POMs and other metadata. + // app-config: java.use-maven-local-repository + UseMavenLocalRepository bool `yaml:"use-maven-localrepository" json:"use-maven-localrepository" mapstructure:"use-maven-localrepository"` + + // MavenLocalRepositoryDir specifies the location of the local maven repository. When not set, defaults to ~/.m2/repository. + // app-config: java.maven-local-repository-dir + MavenLocalRepositoryDir string `yaml:"maven-localrepository-dir" json:"maven-localrepository-dir" mapstructure:"maven-localrepository-dir"` + + // MavenBaseURL specifies the base URL(s) to use for fetching POMs and metadata from maven central or other repositories. When not set, defaults to https://repo1.maven.org/maven2. + // app-config: java.maven-url + MavenBaseURL string `yaml:"maven-base-url" json:"maven-base-url" mapstructure:"maven-base-url"` + + // MaxParentRecursiveDepth limits how many parent POMs will be fetched recursively before stopping. This prevents infinite loops or excessively deep parent chains. + // app-config: java.max-parent-recursive-depth + MaxParentRecursiveDepth int `yaml:"max-parent-recursive-depth" json:"max-parent-recursive-depth" mapstructure:"max-parent-recursive-depth"` + + // ResolveTransitiveDependencies enables resolving transitive dependencies for java packages found within archives. + // app-config: java.resolve-transitive-dependencies + ResolveTransitiveDependencies bool `yaml:"resolve-transitive-dependencies" json:"resolve-transitive-dependencies" mapstructure:"resolve-transitive-dependencies"` } func DefaultArchiveCatalogerConfig() ArchiveCatalogerConfig { diff --git a/syft/pkg/cataloger/java/parse_gradle_lockfile.go b/syft/pkg/cataloger/java/parse_gradle_lockfile.go index 939a90463..4ba86afb3 100644 --- a/syft/pkg/cataloger/java/parse_gradle_lockfile.go +++ b/syft/pkg/cataloger/java/parse_gradle_lockfile.go @@ -11,8 +11,6 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/generic" ) -const gradleLockfileGlob = "**/gradle.lockfile*" - // lockfileDependency represents a single dependency in the gradle.lockfile file type lockfileDependency struct { Group string diff --git a/syft/pkg/cataloger/java/parse_jvm_release.go b/syft/pkg/cataloger/java/parse_jvm_release.go index 3ef5233ee..c5460ed3b 100644 --- a/syft/pkg/cataloger/java/parse_jvm_release.go +++ b/syft/pkg/cataloger/java/parse_jvm_release.go @@ -22,10 +22,6 @@ import ( ) const ( - // this is a very permissive glob that will match more than just the JVM release file. - // we started with "**/{java,jvm}/*/release", but this prevents scanning JVM archive contents (e.g. jdk8u402.zip). - // this approach lets us check more files for JVM release info, but be rather silent about errors. - jvmReleaseGlob = "**/release" oracleVendor = "oracle" openJdkProduct = "openjdk" jre = "jre" diff --git a/syft/pkg/cataloger/javascript/config.go b/syft/pkg/cataloger/javascript/config.go index e3b837e26..e034f6b12 100644 --- a/syft/pkg/cataloger/javascript/config.go +++ b/syft/pkg/cataloger/javascript/config.go @@ -3,9 +3,15 @@ package javascript const npmBaseURL = "https://registry.npmjs.org" type CatalogerConfig struct { - SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"` - NPMBaseURL string `json:"npm-base-url" yaml:"npm-base-url" mapstructure:"npm-base-url"` - IncludeDevDependencies bool `json:"include-dev-dependencies" yaml:"include-dev-dependencies" mapstructure:"include-dev-dependencies"` + // SearchRemoteLicenses enables querying the NPM registry API to retrieve license information for packages that are missing license data in their local metadata. + // app-config: javascript.search-remote-licenses + SearchRemoteLicenses bool `json:"search-remote-licenses" yaml:"search-remote-licenses" mapstructure:"search-remote-licenses"` + // NPMBaseURL specifies the base URL for the NPM registry API used when searching for remote license information. + // app-config: javascript.npm-base-url + NPMBaseURL string `json:"npm-base-url" yaml:"npm-base-url" mapstructure:"npm-base-url"` + // IncludeDevDependencies controls whether development dependencies should be included in the catalog results, in addition to production dependencies. + // app-config: javascript.include-dev-dependencies + IncludeDevDependencies bool `json:"include-dev-dependencies" yaml:"include-dev-dependencies" mapstructure:"include-dev-dependencies"` } func DefaultCatalogerConfig() CatalogerConfig { diff --git a/syft/pkg/cataloger/kernel/cataloger.go b/syft/pkg/cataloger/kernel/cataloger.go index 2be082ab0..389ccf3d7 100644 --- a/syft/pkg/cataloger/kernel/cataloger.go +++ b/syft/pkg/cataloger/kernel/cataloger.go @@ -17,6 +17,8 @@ import ( var _ pkg.Cataloger = (*linuxKernelCataloger)(nil) type LinuxKernelCatalogerConfig struct { + // CatalogModules enables cataloging linux kernel modules (*.ko files) in addition to the kernel itself. + // app-config: linux-kernel.catalog-modules CatalogModules bool `yaml:"catalog-modules" json:"catalog-modules" mapstructure:"catalog-modules"` } diff --git a/syft/pkg/cataloger/nix/cataloger.go b/syft/pkg/cataloger/nix/cataloger.go index a9309b31d..b4566787b 100644 --- a/syft/pkg/cataloger/nix/cataloger.go +++ b/syft/pkg/cataloger/nix/cataloger.go @@ -10,6 +10,8 @@ import ( ) type Config struct { + // CaptureOwnedFiles determines whether to record the list of files owned by each Nix package discovered in the store. Recording owned files provides more detailed information but increases processing time and memory usage. + // app-config: nix.capture-owned-files CaptureOwnedFiles bool `json:"capture-owned-files" yaml:"capture-owned-files" mapstructure:"capture-owned-files"` } diff --git a/syft/pkg/cataloger/python/cataloger.go b/syft/pkg/cataloger/python/cataloger.go index 18c343585..40fbfff7d 100644 --- a/syft/pkg/cataloger/python/cataloger.go +++ b/syft/pkg/cataloger/python/cataloger.go @@ -11,6 +11,8 @@ import ( const eggInfoGlob = "**/*.egg-info" type CatalogerConfig struct { + // GuessUnpinnedRequirements attempts to infer package versions from version constraints when no explicit version is specified in requirements files. + // app-config: python.guess-unpinned-requirements GuessUnpinnedRequirements bool `yaml:"guess-unpinned-requirements" json:"guess-unpinned-requirements" mapstructure:"guess-unpinned-requirements"` } diff --git a/syft/pkg/cataloger/rust/cataloger.go b/syft/pkg/cataloger/rust/cataloger.go index 3f83f5223..8951f1b28 100644 --- a/syft/pkg/cataloger/rust/cataloger.go +++ b/syft/pkg/cataloger/rust/cataloger.go @@ -9,8 +9,6 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/generic" ) -const cargoAuditBinaryCatalogerName = "cargo-auditable-binary-cataloger" - // NewCargoLockCataloger returns a new Rust Cargo lock file cataloger object. func NewCargoLockCataloger() pkg.Cataloger { return generic.NewCataloger("rust-cargo-lock-cataloger"). @@ -20,6 +18,6 @@ func NewCargoLockCataloger() pkg.Cataloger { // NewAuditBinaryCataloger returns a new Rust auditable binary cataloger object that can detect dependencies // in binaries produced with https://github.com/Shnatsel/rust-audit func NewAuditBinaryCataloger() pkg.Cataloger { - return generic.NewCataloger(cargoAuditBinaryCatalogerName). + return generic.NewCataloger("cargo-auditable-binary-cataloger"). WithParserByMimeTypes(parseAuditBinary, mimetype.ExecutableMIMETypeSet.List()...) } diff --git a/syft/pkg/cataloger/rust/package.go b/syft/pkg/cataloger/rust/package.go index b530647d3..9ed04696d 100644 --- a/syft/pkg/cataloger/rust/package.go +++ b/syft/pkg/cataloger/rust/package.go @@ -33,7 +33,6 @@ func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg Language: pkg.Rust, Type: pkg.RustPkg, Locations: file.NewLocationSet(locations...), - FoundBy: cargoAuditBinaryCatalogerName, Metadata: pkg.RustBinaryAuditEntry{ Name: dep.Name, Version: dep.Version, From 1d5bcc553aab101957e97249bb0a44cf70ff0d51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:22:00 -0400 Subject: [PATCH 09/38] chore(deps): bump github.com/mholt/archives from 0.1.3 to 0.1.5 (#4280) * chore(deps): bump github.com/mholt/archives from 0.1.3 to 0.1.5 Bumps [github.com/mholt/archives](https://github.com/mholt/archives) from 0.1.3 to 0.1.5. - [Release notes](https://github.com/mholt/archives/releases) - [Commits](https://github.com/mholt/archives/compare/v0.1.3...v0.1.5) --- updated-dependencies: - dependency-name: github.com/mholt/archives dependency-version: 0.1.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * chore: allow lzip-go in bouncer yaml Signed-off-by: Will Murphy --------- Signed-off-by: dependabot[bot] Signed-off-by: Will Murphy Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Will Murphy --- .bouncer.yaml | 3 +++ go.mod | 14 +++++++------- go.sum | 28 ++++++++++++++-------------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/.bouncer.yaml b/.bouncer.yaml index 5360418ef..99a74d605 100644 --- a/.bouncer.yaml +++ b/.bouncer.yaml @@ -9,6 +9,9 @@ permit: - Unlicense ignore-packages: + # https://github.com/sorairolake/lzip-go/blob/34a2615d2abf740175c6b0a835baa08364e09430/go.sum.license#L3 + # has `SPDX-License-Identifier: Apache-2.0 OR MIT`, both of which are acceptable + - github.com/sorairolake/lzip-go # packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE - github.com/anchore/packageurl-go diff --git a/go.mod b/go.mod index 8bfb81046..ca6c3330f 100644 --- a/go.mod +++ b/go.mod @@ -62,7 +62,7 @@ require ( github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 github.com/magiconair/properties v1.8.10 - github.com/mholt/archives v0.1.3 + github.com/mholt/archives v0.1.5 github.com/moby/sys/mountinfo v0.7.2 github.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 github.com/olekukonko/tablewriter v1.0.9 @@ -110,11 +110,11 @@ require ( github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/hcsshim v0.11.7 // indirect github.com/ProtonMail/go-crypto v1.3.0 // indirect - github.com/STARRY-S/zip v0.2.1 // indirect + github.com/STARRY-S/zip v0.2.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect; indirectt github.com/anchore/go-lzo v0.1.0 // indirect github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect - github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/aquasecurity/go-version v0.0.1 // indirect github.com/atotto/clipboard v0.1.4 // indirect @@ -122,7 +122,7 @@ require ( github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bodgit/plumbing v1.3.0 // indirect - github.com/bodgit/sevenzip v1.6.0 // indirect + github.com/bodgit/sevenzip v1.6.1 // indirect github.com/bodgit/windows v1.0.1 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect @@ -194,7 +194,7 @@ require ( github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mikelolasagasti/xz v1.0.1 // indirect - github.com/minio/minlz v1.0.0 // indirect + github.com/minio/minlz v1.0.1 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -210,7 +210,7 @@ require ( github.com/muesli/termenv v0.16.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nwaples/rardecode v1.1.3 // indirect - github.com/nwaples/rardecode/v2 v2.1.0 // indirect + github.com/nwaples/rardecode/v2 v2.2.0 // indirect github.com/olekukonko/errors v1.1.0 // indirect github.com/olekukonko/ll v0.0.9 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect @@ -232,7 +232,7 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect github.com/skeema/knownhosts v1.3.1 // indirect - github.com/sorairolake/lzip-go v0.3.5 // indirect + github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.9 // indirect diff --git a/go.sum b/go.sum index ae83871e3..269d7c3d3 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,8 @@ 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.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= github.com/ProtonMail/go-crypto v1.3.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/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= +github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= @@ -141,8 +141,8 @@ github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1: github.com/anchore/stereoscope v0.1.10 h1:BogafIMaW/L1lOUoVS96Hu1jTSP2JktxIayVqcxvcBI= github.com/anchore/stereoscope v0.1.10/go.mod h1:RWFAkQE8tp8yyaf4V83Kq1bO6hX3bzi8gpLCcKgZLIk= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -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/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= 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= @@ -217,8 +217,8 @@ github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/ github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= -github.com/bodgit/sevenzip v1.6.0 h1:a4R0Wu6/P1o1pP/3VV++aEOcyeBxeO/xE2Y9NSTrr6A= -github.com/bodgit/sevenzip v1.6.0/go.mod h1:zOBh9nJUof7tcrlqJFv1koWRrhz3LbDbUNngkuZxLMc= +github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= +github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= @@ -681,15 +681,15 @@ 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.3 h1:aEAaOtNra78G+TvV5ohmXrJOAzf++dIlYeDW3N9q458= -github.com/mholt/archives v0.1.3/go.mod h1:LUCGp++/IbV/I0Xq4SzcIR6uwgeh2yjnQWamjRQfLTU= +github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= +github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= 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= github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= -github.com/minio/minlz v1.0.0 h1:Kj7aJZ1//LlTP1DM8Jm7lNKvvJS2m74gyyXXn3+uJWQ= -github.com/minio/minlz v1.0.0/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= +github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= @@ -743,8 +743,8 @@ github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1a github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= -github.com/nwaples/rardecode/v2 v2.1.0 h1:JQl9ZoBPDy+nIZGb1mx8+anfHp/LV3NE2MjMiv0ct/U= -github.com/nwaples/rardecode/v2 v2.1.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= +github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A= +github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= @@ -851,8 +851,8 @@ github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yf github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= -github.com/sorairolake/lzip-go v0.3.5 h1:ms5Xri9o1JBIWvOFAorYtUNik6HI3HgBTkISiqu0Cwg= -github.com/sorairolake/lzip-go v0.3.5/go.mod h1:N0KYq5iWrMXI0ZEXKXaS9hCyOjZUQdBDEIbXfoUwbdk= +github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= +github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= From 6d790ec6ec98b263dae0c54259859b35c3527e1b Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:09:17 +0000 Subject: [PATCH 10/38] chore(deps): update anchore dependencies (#4282) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com> --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index ca6c3330f..cd7f8feee 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 - github.com/anchore/stereoscope v0.1.10 + github.com/anchore/stereoscope v0.1.11 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/aquasecurity/go-pep440-version v0.0.1 github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef @@ -144,9 +144,9 @@ require ( github.com/containerd/typeurl/v2 v2.2.0 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v28.4.0+incompatible // indirect + github.com/docker/cli v28.5.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker v28.4.0+incompatible // indirect + github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect diff --git a/go.sum b/go.sum index 269d7c3d3..82fb633fe 100644 --- a/go.sum +++ b/go.sum @@ -138,8 +138,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= -github.com/anchore/stereoscope v0.1.10 h1:BogafIMaW/L1lOUoVS96Hu1jTSP2JktxIayVqcxvcBI= -github.com/anchore/stereoscope v0.1.10/go.mod h1:RWFAkQE8tp8yyaf4V83Kq1bO6hX3bzi8gpLCcKgZLIk= +github.com/anchore/stereoscope v0.1.11 h1:YP/XUNcJyMbOOPAWPkeZNCVlKKTRO2cnBTEeUW6I40Y= +github.com/anchore/stereoscope v0.1.11/go.mod h1:G3PZlzPbxFhylj9pQwtqfVPaahuWmy/UCtv5FTIIMvg= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= @@ -322,12 +322,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY= -github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v28.5.1+incompatible h1:ESutzBALAD6qyCLqbQSEf1a/U8Ybms5agw59yGVc+yY= +github.com/docker/cli v28.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk= -github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= From e9a8bc5ab97f94b7f9210d7a0f4a981cdbb22232 Mon Sep 17 00:00:00 2001 From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:12:20 -0400 Subject: [PATCH 11/38] chore: update to use old configuration on new cosign (#4287) Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com> --- .goreleaser.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index bb278f1a9..eaf89dfc3 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -337,6 +337,7 @@ signs: certificate: "${artifact}.pem" args: - "sign-blob" + - "--use-signing-config=false" - "--oidc-issuer=https://token.actions.githubusercontent.com" - "--output-certificate=${certificate}" - "--output-signature=${signature}" From 065ac13ab726f00e2f32a243c23a598d820ae25b Mon Sep 17 00:00:00 2001 From: Kudryavcev Nikolay <35200428+Rupikz@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:05:05 +0300 Subject: [PATCH 12/38] Extract zip archive with multiple entries (#4283) * extract zip archive with multiple entries Signed-off-by: Kudryavcev Nikolay * set OverwriteExisting by type assertion switch case Signed-off-by: Kudryavcev Nikolay --------- Signed-off-by: Kudryavcev Nikolay --- syft/source/filesource/file_source.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/syft/source/filesource/file_source.go b/syft/source/filesource/file_source.go index aa4b4df4b..f74b0c089 100644 --- a/syft/source/filesource/file_source.go +++ b/syft/source/filesource/file_source.go @@ -231,11 +231,14 @@ func fileAnalysisPath(path string, skipExtractArchive bool) (string, func() erro // unarchived. envelopedUnarchiver, err := archiver.ByExtension(path) if unarchiver, ok := envelopedUnarchiver.(archiver.Unarchiver); err == nil && ok { - if tar, ok := unarchiver.(*archiver.Tar); ok { - // when tar files are extracted, if there are multiple entries at the same - // location, the last entry wins - // NOTE: this currently does not display any messages if an overwrite happens - tar.OverwriteExisting = true + // when tar/zip files are extracted, if there are multiple entries at the same + // location, the last entry wins + // NOTE: this currently does not display any messages if an overwrite happens + switch v := unarchiver.(type) { + case *archiver.Tar: + v.OverwriteExisting = true + case *archiver.Zip: + v.OverwriteExisting = true } analysisPath, cleanupFn, err = unarchiveToTmp(path, unarchiver) From 4343d04652a38de75caee86e7628244912f87ff9 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 16 Oct 2025 07:00:31 -0400 Subject: [PATCH 13/38] fix: panic during java archive maven resolution (#4290) Signed-off-by: Keith Zantow --- syft/pkg/cataloger/java/archive_parser.go | 2 +- .../pkg/cataloger/java/archive_parser_test.go | 23 +++++++++++++ .../java/test-fixtures/jar-metadata/Makefile | 8 +++-- .../commons-lang3-3.12.0/META-INF/MANIFEST.MF | 1 + .../maven/org.apache.commons-lang3/pom.xml | 33 +++++++++++++++++++ 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/MANIFEST.MF create mode 100644 syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/maven/org.apache.commons-lang3/pom.xml diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 5c2a22087..477168afc 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -263,7 +263,7 @@ func (j *archiveParser) discoverMainPackage(ctx context.Context) (*pkg.Package, } var pkgPomProject *pkg.JavaPomProject if parsedPom != nil { - pkgPomProject = newPomProject(ctx, nil, parsedPom.path, parsedPom.project) + pkgPomProject = newPomProject(ctx, j.maven, parsedPom.path, parsedPom.project) } return &pkg.Package{ diff --git a/syft/pkg/cataloger/java/archive_parser_test.go b/syft/pkg/cataloger/java/archive_parser_test.go index 9bbc4c39c..71568c18c 100644 --- a/syft/pkg/cataloger/java/archive_parser_test.go +++ b/syft/pkg/cataloger/java/archive_parser_test.go @@ -1632,3 +1632,26 @@ func Test_corruptJarArchive(t *testing.T) { WithError(). TestParser(t, ap.parseJavaArchive) } + +func Test_jarPomPropertyResolutionDoesNotPanic(t *testing.T) { + jarName := generateJavaMetadataJarFixture(t, "commons-lang3-3.12.0", "jar") + fixture, err := os.Open(jarName) + require.NoError(t, err) + + ctx := context.TODO() + // setup parser + ap, cleanupFn, err := newJavaArchiveParser( + ctx, + file.LocationReadCloser{ + Location: file.NewLocation(fixture.Name()), + ReadCloser: fixture, + }, false, ArchiveCatalogerConfig{ + UseMavenLocalRepository: true, + MavenLocalRepositoryDir: "internal/maven/test-fixtures/maven-repo", + }) + defer cleanupFn() + require.NoError(t, err) + + _, _, err = ap.parse(ctx, nil) + require.NoError(t, err) +} diff --git a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile index a7781c168..5cdb05b1e 100644 --- a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile +++ b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/Makefile @@ -14,7 +14,7 @@ SPRING_INSTRUMENTATION = spring-instrumentation-4.3.0-1.0 MULTIPLE_MATCHING = multiple-matching-2.11.5 ORG_MULTIPLE_THENAME = org.multiple-thename MICRONAUT_AOP = micronaut-aop-4.9.11 - +COMMONS_LANG3 = commons-lang3-3.12.0 .DEFAULT_GOAL := fixtures @@ -24,7 +24,7 @@ fixtures: $(CACHE_DIR) # requirement 2: 'fingerprint' goal to determine if the fixture input that indicates any existing cache should be busted fingerprint: $(FINGERPRINT_FILE) -$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar +$(CACHE_DIR): $(CACHE_DIR)/$(JACKSON_CORE).jar $(CACHE_DIR)/$(SBT_JACKSON_CORE).jar $(CACHE_DIR)/$(OPENSAML_CORE).jar $(CACHE_DIR)/$(API_ALL_SOURCES).jar $(CACHE_DIR)/$(SPRING_INSTRUMENTATION).jar $(CACHE_DIR)/$(MULTIPLE_MATCHING).jar $(CACHE_DIR)/$(MICRONAUT_AOP).jar $(CACHE_DIR)/$(COMMONS_LANG3).jar $(CACHE_DIR)/$(JACKSON_CORE).jar: mkdir -p $(CACHE_DIR) @@ -58,6 +58,10 @@ $(CACHE_DIR)/$(MICRONAUT_AOP).jar: mkdir -p $(CACHE_DIR) cd $(MICRONAUT_AOP) && zip -r $(CACHE_PATH)/$(MICRONAUT_AOP).jar . +$(CACHE_DIR)/$(COMMONS_LANG3).jar: + mkdir -p $(CACHE_DIR) + cd $(COMMONS_LANG3) && zip -r $(CACHE_PATH)/$(COMMONS_LANG3).jar . + # Jenkins plugins typically do not have the version included in the archive name, # so it is important to not include it in the generated test fixture $(CACHE_DIR)/gradle.hpi: diff --git a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/MANIFEST.MF b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/MANIFEST.MF new file mode 100644 index 000000000..9d885be53 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/MANIFEST.MF @@ -0,0 +1 @@ +Manifest-Version: 1.0 diff --git a/syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/maven/org.apache.commons-lang3/pom.xml b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/maven/org.apache.commons-lang3/pom.xml new file mode 100644 index 000000000..e7c18c753 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/jar-metadata/commons-lang3-3.12.0/META-INF/maven/org.apache.commons-lang3/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + org.apache.commons + commons-parent + 54 + + org.apache.commons + commons-lang3 + ${commons.release.version} + pom + JUnit 5 (Bill of Materials) + + + Eclipse Public License v2.0 + https://www.eclipse.org/legal/epl-v20.html + + + + scm:git:git://github.com/junit-team/junit5.git + scm:git:git://github.com/junit-team/junit5.git + https://github.com/junit-team/junit5 + + + + org.junit.vintage + junit-vintage-engine + ${commons.release.version} + + + From 0c98a364d503789ff8d7f122763bb8748de9772f Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 07:02:32 -0400 Subject: [PATCH 14/38] chore(deps): update tools to latest versions (#4291) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com> --- .binny.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.binny.yaml b/.binny.yaml index 1edb0fdc8..f59bd7aca 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -98,7 +98,7 @@ tools: # used for triggering a release - name: gh version: - want: v2.81.0 + want: v2.82.0 method: github-release with: repo: cli/cli From e923db2a94343ff8ed86b296dbd1e98bea7a955b Mon Sep 17 00:00:00 2001 From: Pavel Buchart Date: Thu, 16 Oct 2025 14:50:44 +0200 Subject: [PATCH 15/38] Add PDM parser (#4234) Signed-off-by: Pavel Buchart Signed-off-by: Keith Zantow Co-authored-by: Keith Zantow --- .gitignore | 1 + internal/constants.go | 2 +- internal/packagemetadata/generated.go | 1 + internal/packagemetadata/names.go | 1 + schema/json/schema-16.0.41.json | 4011 +++++++++++++++++ schema/json/schema-latest.json | 34 +- .../helpers/originator_supplier_test.go | 20 + syft/pkg/cataloger/python/cataloger.go | 3 +- syft/pkg/cataloger/python/cataloger_test.go | 1 + syft/pkg/cataloger/python/parse_pdm_lock.go | 140 + .../cataloger/python/parse_pdm_lock_test.go | 363 ++ .../test-fixtures/glob-paths/src/pdm.lock | 1 + .../python/test-fixtures/pdm-lock/pdm.lock | 137 + syft/pkg/python.go | 10 + 14 files changed, 4722 insertions(+), 3 deletions(-) create mode 100644 schema/json/schema-16.0.41.json create mode 100644 syft/pkg/cataloger/python/parse_pdm_lock.go create mode 100644 syft/pkg/cataloger/python/parse_pdm_lock_test.go create mode 100644 syft/pkg/cataloger/python/test-fixtures/glob-paths/src/pdm.lock create mode 100644 syft/pkg/cataloger/python/test-fixtures/pdm-lock/pdm.lock diff --git a/.gitignore b/.gitignore index 9c0909fcf..e4a1f4af8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ go.work go.work.sum .tool-versions +.python-version # app configuration /.syft.yaml diff --git a/internal/constants.go b/internal/constants.go index d41a7bf59..452625453 100644 --- a/internal/constants.go +++ b/internal/constants.go @@ -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.40" + JSONSchemaVersion = "16.0.41" ) diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go index 2315352dc..9d746f54d 100644 --- a/internal/packagemetadata/generated.go +++ b/internal/packagemetadata/generated.go @@ -51,6 +51,7 @@ func AllTypes() []any { pkg.PhpPeclEntry{}, pkg.PortageEntry{}, pkg.PythonPackage{}, + pkg.PythonPdmLockEntry{}, pkg.PythonPipfileLockEntry{}, pkg.PythonPoetryLockEntry{}, pkg.PythonRequirementsEntry{}, diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go index 79eed0d4f..e075bc794 100644 --- a/internal/packagemetadata/names.go +++ b/internal/packagemetadata/names.go @@ -102,6 +102,7 @@ var jsonTypes = makeJSONTypes( jsonNames(pkg.PhpPearEntry{}, "php-pear-entry"), jsonNames(pkg.PortageEntry{}, "portage-db-entry", "PortageMetadata"), jsonNames(pkg.PythonPackage{}, "python-package", "PythonPackageMetadata"), + jsonNames(pkg.PythonPdmLockEntry{}, "python-pdm-lock-entry"), jsonNames(pkg.PythonPipfileLockEntry{}, "python-pipfile-lock-entry", "PythonPipfileLockMetadata"), jsonNames(pkg.PythonPoetryLockEntry{}, "python-poetry-lock-entry", "PythonPoetryLockMetadata"), jsonNames(pkg.PythonRequirementsEntry{}, "python-pip-requirements-entry", "PythonRequirementsMetadata"), diff --git a/schema/json/schema-16.0.41.json b/schema/json/schema-16.0.41.json new file mode 100644 index 000000000..20e8b3a9f --- /dev/null +++ b/schema/json/schema-16.0.41.json @@ -0,0 +1,4011 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "anchore.io/schema/syft/json/16.0.41/document", + "$ref": "#/$defs/Document", + "$defs": { + "AlpmDbEntry": { + "properties": { + "basepackage": { + "type": "string", + "description": "BasePackage is the base package name this package was built from (source package in Arch build system)" + }, + "package": { + "type": "string", + "description": "Package is the package name as found in the desc file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the desc file" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture as defined in Arch architecture spec (e.g. x86_64, aarch64, or \"any\" for arch-independent packages)" + }, + "size": { + "type": "integer", + "description": "Size is the installed size in bytes" + }, + "packager": { + "type": "string", + "description": "Packager is the name and email of the person who packaged this (RFC822 format)" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "validation": { + "type": "string", + "description": "Validation is the validation method used for package integrity (e.g. pgp signature, sha256 checksum)" + }, + "reason": { + "type": "integer", + "description": "Reason is the installation reason tracked by pacman (0=explicitly installed by user, 1=installed as dependency)" + }, + "files": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + }, + "backup": { + "items": { + "$ref": "#/$defs/AlpmFileRecord" + }, + "type": "array", + "description": "Backup is the list of configuration files that pacman backs up before upgrades" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (allows other packages to depend on capabilities rather than specific packages)" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the runtime dependencies required by this package" + } + }, + "type": "object", + "required": [ + "basepackage", + "package", + "version", + "description", + "architecture", + "size", + "packager", + "url", + "validation", + "reason", + "files", + "backup" + ], + "description": "AlpmDBEntry is a struct that represents the package data stored in the pacman flat-file stores for arch linux." + }, + "AlpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "type": { + "type": "string", + "description": "Type is the file type (e.g. regular file, directory, symlink)" + }, + "uid": { + "type": "string", + "description": "UID is the file owner user ID as recorded by pacman" + }, + "gid": { + "type": "string", + "description": "GID is the file owner group ID as recorded by pacman" + }, + "time": { + "type": "string", + "format": "date-time", + "description": "Time is the file modification timestamp" + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes" + }, + "link": { + "type": "string", + "description": "Link is the symlink target path if this is a symlink" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "Digests contains file content hashes for integrity verification" + } + }, + "type": "object" + }, + "ApkDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the installed file" + }, + "originPackage": { + "type": "string", + "description": "OriginPackage is the original source package name this binary was built from (used to track which aport/source built this)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer name and email" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the installed file" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "url": { + "type": "string", + "description": "URL is the upstream project URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes (.apk file size)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in bytes" + }, + "pullDependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the runtime dependencies required by this package" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are virtual packages provided by this package (for capability-based dependencies)" + }, + "pullChecksum": { + "type": "string", + "description": "Checksum is the package content checksum for integrity verification" + }, + "gitCommitOfApkPort": { + "type": "string", + "description": "GitCommit is the git commit hash of the APK port definition in Alpine's aports repository" + }, + "files": { + "items": { + "$ref": "#/$defs/ApkFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "originPackage", + "maintainer", + "version", + "architecture", + "url", + "description", + "size", + "installedSize", + "pullDependencies", + "provides", + "pullChecksum", + "gitCommitOfApkPort", + "files" + ], + "description": "ApkDBEntry represents all captured data for the alpine linux package manager flat-file store." + }, + "ApkFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "ownerUid": { + "type": "string", + "description": "OwnerUID is the file owner user ID" + }, + "ownerGid": { + "type": "string", + "description": "OwnerGID is the file owner group ID" + }, + "permissions": { + "type": "string", + "description": "Permissions is the file permission mode string (e.g. \"0755\", \"0644\")" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash for integrity verification" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)." + }, + "BinarySignature": { + "properties": { + "matches": { + "items": { + "$ref": "#/$defs/ClassifierMatch" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "matches" + ], + "description": "BinarySignature represents a set of matched values within a binary file." + }, + "BitnamiSbomEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the Bitnami SPDX file" + }, + "arch": { + "type": "string", + "description": "Architecture is the target CPU architecture (amd64 or arm64 in Bitnami images)" + }, + "distro": { + "type": "string", + "description": "Distro is the distribution name this package is for (base OS like debian, ubuntu, etc.)" + }, + "revision": { + "type": "string", + "description": "Revision is the Bitnami-specific package revision number (incremented for Bitnami rebuilds of same upstream version)" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the Bitnami SPDX file" + }, + "path": { + "type": "string", + "description": "Path is the installation path in the filesystem where the package is located" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the file paths owned by this package (tracked via SPDX relationships)" + } + }, + "type": "object", + "required": [ + "name", + "arch", + "distro", + "revision", + "version", + "path", + "files" + ], + "description": "BitnamiSBOMEntry represents all captured data from Bitnami packages described in Bitnami' SPDX files." + }, + "CConanFileEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanfileEntry represents a single \"Requires\" entry from a conanfile.txt." + }, + "CConanInfoEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConaninfoEntry represents a single \"full_requires\" entry from a conaninfo.txt." + }, + "CConanLockEntry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "package_id": { + "type": "string", + "description": "PackageID is a unique package variant identifier computed from settings/options (static hash in Conan 1.x, can have collisions with complex dependency graphs)" + }, + "prev": { + "type": "string", + "description": "Prev is the previous lock entry reference for versioning" + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires are the runtime package dependencies" + }, + "build_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "BuildRequires are the build-time dependencies (e.g. cmake, compilers)" + }, + "py_requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PythonRequires are the Python dependencies needed for Conan recipes" + }, + "options": { + "$ref": "#/$defs/KeyValues", + "description": "Options are package configuration options as key-value pairs (e.g. shared=True, fPIC=True)" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the package in Conan cache" + }, + "context": { + "type": "string", + "description": "Context is the build context information" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV1LockEntry represents a single \"node\" entry from a conan.lock V1 file." + }, + "CConanLockV2Entry": { + "properties": { + "ref": { + "type": "string", + "description": "Ref is the package reference string in format name/version@user/channel" + }, + "packageID": { + "type": "string", + "description": "PackageID is a unique package variant identifier (dynamic in Conan 2.0, more accurate than V1)" + }, + "username": { + "type": "string", + "description": "Username is the Conan user/organization name" + }, + "channel": { + "type": "string", + "description": "Channel is the Conan channel name indicating stability/purpose (e.g. stable, testing, experimental)" + }, + "recipeRevision": { + "type": "string", + "description": "RecipeRevision is a git-like revision hash (RREV) of the recipe" + }, + "packageRevision": { + "type": "string", + "description": "PackageRevision is a git-like revision hash of the built binary package" + }, + "timestamp": { + "type": "string", + "description": "TimeStamp is when this package was built/locked" + } + }, + "type": "object", + "required": [ + "ref" + ], + "description": "ConanV2LockEntry represents a single \"node\" entry from a conan.lock V2 file." + }, + "CPE": { + "properties": { + "cpe": { + "type": "string" + }, + "source": { + "type": "string" + } + }, + "type": "object", + "required": [ + "cpe" + ] + }, + "ClassifierMatch": { + "properties": { + "classifier": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Location" + } + }, + "type": "object", + "required": [ + "classifier", + "location" + ], + "description": "ClassifierMatch represents a single matched value within a binary file and the \"class\" name the search pattern represents." + }, + "CocoaPodfileLockEntry": { + "properties": { + "checksum": { + "type": "string", + "description": "Checksum is the SHA-1 hash of the podspec file for integrity verification (generated via `pod ipc spec ... | openssl sha1`), ensuring all team members use the same pod specification version" + } + }, + "type": "object", + "required": [ + "checksum" + ], + "description": "CocoaPodfileLockEntry represents a single entry from the \"Pods\" section of a Podfile.lock file." + }, + "CondaLink": { + "properties": { + "source": { + "type": "string", + "description": "Source is the original path where the package was extracted from cache." + }, + "type": { + "type": "integer", + "description": "Type indicates the link type (1 for hard link, 2 for soft link, 3 for copy)." + } + }, + "type": "object", + "required": [ + "source", + "type" + ], + "description": "CondaLink represents link metadata from a Conda package's link.json file describing package installation source." + }, + "CondaMetadataEntry": { + "properties": { + "arch": { + "type": "string", + "description": "Arch is the target CPU architecture for the package (e.g., \"arm64\", \"x86_64\")." + }, + "name": { + "type": "string", + "description": "Name is the package name as found in the conda-meta JSON file." + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the conda-meta JSON file." + }, + "build": { + "type": "string", + "description": "Build is the build string identifier (e.g., \"h90dfc92_1014\")." + }, + "build_number": { + "type": "integer", + "description": "BuildNumber is the sequential build number for this version." + }, + "channel": { + "type": "string", + "description": "Channel is the Conda channel URL where the package was retrieved from." + }, + "subdir": { + "type": "string", + "description": "Subdir is the subdirectory within the channel (e.g., \"osx-arm64\", \"linux-64\")." + }, + "noarch": { + "type": "string", + "description": "Noarch indicates if the package is platform-independent (e.g., \"python\", \"generic\")." + }, + "license": { + "type": "string", + "description": "License is the package license identifier." + }, + "license_family": { + "type": "string", + "description": "LicenseFamily is the general license category (e.g., \"MIT\", \"Apache\", \"GPL\")." + }, + "md5": { + "type": "string", + "description": "MD5 is the MD5 hash of the package archive." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the package archive." + }, + "size": { + "type": "integer", + "description": "Size is the package archive size in bytes." + }, + "timestamp": { + "type": "integer", + "description": "Timestamp is the Unix timestamp when the package was built." + }, + "fn": { + "type": "string", + "description": "Filename is the original package archive filename (e.g., \"zlib-1.2.11-h90dfc92_1014.tar.bz2\")." + }, + "url": { + "type": "string", + "description": "URL is the full download URL for the package archive." + }, + "extracted_package_dir": { + "type": "string", + "description": "ExtractedPackageDir is the local cache directory where the package was extracted." + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends is the list of runtime dependencies with version constraints." + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is the list of files installed by this package." + }, + "paths_data": { + "$ref": "#/$defs/CondaPathsData", + "description": "PathsData contains detailed file metadata from the paths.json file." + }, + "link": { + "$ref": "#/$defs/CondaLink", + "description": "Link contains installation source metadata from the link.json file." + } + }, + "type": "object", + "required": [ + "name", + "version", + "build", + "build_number" + ], + "description": "CondaMetaPackage represents metadata for a Conda package extracted from the conda-meta/*.json files." + }, + "CondaPathData": { + "properties": { + "_path": { + "type": "string", + "description": "Path is the file path relative to the Conda environment root." + }, + "path_type": { + "type": "string", + "description": "PathType indicates the link type for the file (e.g., \"hardlink\", \"softlink\", \"directory\")." + }, + "sha256": { + "type": "string", + "description": "SHA256 is the SHA-256 hash of the file contents." + }, + "sha256_in_prefix": { + "type": "string", + "description": "SHA256InPrefix is the SHA-256 hash of the file after prefix replacement during installation." + }, + "size_in_bytes": { + "type": "integer", + "description": "SizeInBytes is the file size in bytes." + } + }, + "type": "object", + "required": [ + "_path", + "path_type", + "sha256", + "sha256_in_prefix", + "size_in_bytes" + ], + "description": "CondaPathData represents metadata for a single file within a Conda package from the paths.json file." + }, + "CondaPathsData": { + "properties": { + "paths_version": { + "type": "integer", + "description": "PathsVersion is the schema version of the paths data format." + }, + "paths": { + "items": { + "$ref": "#/$defs/CondaPathData" + }, + "type": "array", + "description": "Paths is the list of file metadata entries for all files in the package." + } + }, + "type": "object", + "required": [ + "paths_version", + "paths" + ], + "description": "CondaPathsData represents the paths.json file structure from a Conda package containing file metadata." + }, + "Coordinates": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "Coordinates contains the minimal information needed to describe how to find a file within any possible source object (e.g." + }, + "DartPubspec": { + "properties": { + "homepage": { + "type": "string", + "description": "Homepage is the package homepage URL" + }, + "repository": { + "type": "string", + "description": "Repository is the source code repository URL" + }, + "documentation": { + "type": "string", + "description": "Documentation is the documentation site URL" + }, + "publish_to": { + "type": "string", + "description": "PublishTo is the package repository to publish to, or \"none\" to prevent accidental publishing" + }, + "environment": { + "$ref": "#/$defs/DartPubspecEnvironment", + "description": "Environment is SDK version constraints for Dart and Flutter" + }, + "platforms": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Platforms are the supported platforms (Android, iOS, web, etc.)" + }, + "ignored_advisories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "IgnoredAdvisories are the security advisories to explicitly ignore for this package" + } + }, + "type": "object", + "description": "DartPubspec is a struct that represents a package described in a pubspec.yaml file" + }, + "DartPubspecEnvironment": { + "properties": { + "sdk": { + "type": "string", + "description": "SDK is the Dart SDK version constraint (e.g. \"\u003e=2.12.0 \u003c3.0.0\")" + }, + "flutter": { + "type": "string", + "description": "Flutter is the Flutter SDK version constraint if this is a Flutter package" + } + }, + "type": "object", + "description": "DartPubspecEnvironment represents SDK version constraints from the environment section of pubspec.yaml." + }, + "DartPubspecLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the pubspec.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the pubspec.lock file" + }, + "hosted_url": { + "type": "string", + "description": "HostedURL is the URL of the package repository for hosted packages (typically pub.dev, but can be custom repository identified by hosted-url). When PUB_HOSTED_URL environment variable changes, lockfile tracks the source." + }, + "vcs_url": { + "type": "string", + "description": "VcsURL is the URL of the VCS repository for git/path dependencies (for packages fetched from version control systems like Git)" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "DartPubspecLockEntry is a struct that represents a single entry found in the \"packages\" section in a Dart pubspec.lock file." + }, + "Descriptor": { + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "configuration": true + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "Descriptor describes what created the document as well as surrounding metadata" + }, + "Digest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm specifies the hash algorithm used (e.g., \"sha256\", \"md5\")." + }, + "value": { + "type": "string", + "description": "Value is the hexadecimal string representation of the hash." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "Digest represents a cryptographic hash of file contents." + }, + "Document": { + "properties": { + "artifacts": { + "items": { + "$ref": "#/$defs/Package" + }, + "type": "array" + }, + "artifactRelationships": { + "items": { + "$ref": "#/$defs/Relationship" + }, + "type": "array" + }, + "files": { + "items": { + "$ref": "#/$defs/File" + }, + "type": "array" + }, + "source": { + "$ref": "#/$defs/Source" + }, + "distro": { + "$ref": "#/$defs/LinuxRelease" + }, + "descriptor": { + "$ref": "#/$defs/Descriptor" + }, + "schema": { + "$ref": "#/$defs/Schema" + } + }, + "type": "object", + "required": [ + "artifacts", + "artifactRelationships", + "source", + "distro", + "descriptor", + "schema" + ], + "description": "Document represents the syft cataloging findings as a JSON document" + }, + "DotnetDepsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the deps.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the deps.json file" + }, + "path": { + "type": "string", + "description": "Path is the relative path to the package within the deps structure (e.g. \"app.metrics/3.0.0\")" + }, + "sha512": { + "type": "string", + "description": "Sha512 is the SHA-512 hash of the NuGet package content WITHOUT the signed content for verification (won't match hash from NuGet API or manual calculation of .nupkg file)" + }, + "hashPath": { + "type": "string", + "description": "HashPath is the relative path to the .nupkg.sha512 hash file (e.g. \"app.metrics.3.0.0.nupkg.sha512\")" + }, + "executables": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + } + }, + "type": "object", + "description": "Executables are the map of .NET Portable Executable files within this package with their version resources" + } + }, + "type": "object", + "required": [ + "name", + "version", + "path", + "sha512", + "hashPath" + ], + "description": "DotnetDepsEntry is a struct that represents a single entry found in the \"libraries\" section in a .NET [*.]deps.json file." + }, + "DotnetPackagesLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the packages.lock.json file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the packages.lock.json file" + }, + "contentHash": { + "type": "string", + "description": "ContentHash is the hash of the package content for verification" + }, + "type": { + "type": "string", + "description": "Type is the dependency type indicating how this dependency was added (Direct=explicit in project file, Transitive=pulled in by another package, Project=project reference)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "contentHash", + "type" + ], + "description": "DotnetPackagesLockEntry is a struct that represents a single entry found in the \"dependencies\" section in a .NET packages.lock.json file." + }, + "DotnetPortableExecutableEntry": { + "properties": { + "assemblyVersion": { + "type": "string", + "description": "AssemblyVersion is the .NET assembly version number (strong-named version)" + }, + "legalCopyright": { + "type": "string", + "description": "LegalCopyright is the copyright notice string" + }, + "comments": { + "type": "string", + "description": "Comments are additional comments or description embedded in PE resources" + }, + "internalName": { + "type": "string", + "description": "InternalName is the internal name of the file" + }, + "companyName": { + "type": "string", + "description": "CompanyName is the company that produced the file" + }, + "productName": { + "type": "string", + "description": "ProductName is the name of the product this file is part of" + }, + "productVersion": { + "type": "string", + "description": "ProductVersion is the version of the product (may differ from AssemblyVersion)" + } + }, + "type": "object", + "required": [ + "assemblyVersion", + "legalCopyright", + "companyName", + "productName", + "productVersion" + ], + "description": "DotnetPortableExecutableEntry is a struct that represents a single entry found within \"VersionResources\" section of a .NET Portable Executable binary file." + }, + "DpkgArchiveEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgArchiveEntry represents package metadata extracted from a .deb archive file." + }, + "DpkgDbEntry": { + "properties": { + "package": { + "type": "string", + "description": "Package is the package name as found in the status file" + }, + "source": { + "type": "string", + "description": "Source is the source package name this binary was built from (one source can produce multiple binary packages)" + }, + "version": { + "type": "string", + "description": "Version is the binary package version as found in the status file" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source package version (may differ from binary version when binNMU rebuilds occur)" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target architecture per Debian spec (specific arch like amd64/arm64, wildcard like any, architecture-independent \"all\", or \"source\" for source packages)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is the package maintainer's name and email in RFC822 format (name must come first, then email in angle brackets)" + }, + "installedSize": { + "type": "integer", + "description": "InstalledSize is the total size of installed files in kilobytes" + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides are the virtual packages provided by this package (allows other packages to depend on capabilities. Can include versioned provides like \"libdigest-md5-perl (= 2.55.01)\")" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages required for this package to function (will not be installed unless these requirements are met, creates strict ordering constraint)" + }, + "preDepends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "PreDepends are the packages that must be installed and configured BEFORE even starting installation of this package (stronger than Depends, discouraged unless absolutely necessary as it adds strict constraints for apt)" + }, + "files": { + "items": { + "$ref": "#/$defs/DpkgFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package" + } + }, + "type": "object", + "required": [ + "package", + "source", + "version", + "sourceVersion", + "architecture", + "maintainer", + "installedSize", + "files" + ], + "description": "DpkgDBEntry represents all captured data for a Debian package DB entry; available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section." + }, + "DpkgFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is the file content hash (typically MD5 for dpkg compatibility with legacy systems)" + }, + "isConfigFile": { + "type": "boolean", + "description": "IsConfigFile is whether this file is marked as a configuration file (dpkg will preserve user modifications during upgrades)" + } + }, + "type": "object", + "required": [ + "path", + "isConfigFile" + ], + "description": "DpkgFileRecord represents a single file attributed to a debian package." + }, + "ELFSecurityFeatures": { + "properties": { + "symbolTableStripped": { + "type": "boolean", + "description": "SymbolTableStripped indicates whether debugging symbols have been removed." + }, + "stackCanary": { + "type": "boolean", + "description": "StackCanary indicates whether stack smashing protection is enabled." + }, + "nx": { + "type": "boolean", + "description": "NoExecutable indicates whether NX (no-execute) protection is enabled for the stack." + }, + "relRO": { + "type": "string", + "description": "RelocationReadOnly indicates the RELRO protection level." + }, + "pie": { + "type": "boolean", + "description": "PositionIndependentExecutable indicates whether the binary is compiled as PIE." + }, + "dso": { + "type": "boolean", + "description": "DynamicSharedObject indicates whether the binary is a shared library." + }, + "safeStack": { + "type": "boolean", + "description": "LlvmSafeStack represents a compiler-based security mechanism that separates the stack into a safe stack for storing return addresses and other critical data, and an unsafe stack for everything else, to mitigate stack-based memory corruption errors\nsee https://clang.llvm.org/docs/SafeStack.html" + }, + "cfi": { + "type": "boolean", + "description": "ControlFlowIntegrity represents runtime checks to ensure a program's control flow adheres to the legal paths determined at compile time, thus protecting against various types of control-flow hijacking attacks\nsee https://clang.llvm.org/docs/ControlFlowIntegrity.html" + }, + "fortify": { + "type": "boolean", + "description": "ClangFortifySource is a broad suite of extensions to libc aimed at catching misuses of common library functions\nsee https://android.googlesource.com/platform//bionic/+/d192dbecf0b2a371eb127c0871f77a9caf81c4d2/docs/clang_fortify_anatomy.md" + } + }, + "type": "object", + "required": [ + "symbolTableStripped", + "nx", + "relRO", + "pie", + "dso" + ], + "description": "ELFSecurityFeatures captures security hardening and protection mechanisms in ELF binaries." + }, + "ElfBinaryPackageNoteJsonPayload": { + "properties": { + "type": { + "type": "string", + "description": "Type is the type of the package (e.g. \"rpm\", \"deb\", \"apk\", etc.)" + }, + "architecture": { + "type": "string", + "description": "Architecture of the binary package (e.g. \"amd64\", \"arm\", etc.)" + }, + "osCPE": { + "type": "string", + "description": "OSCPE is a CPE name for the OS, typically corresponding to CPE_NAME in os-release (e.g. cpe:/o:fedoraproject:fedora:33)" + }, + "os": { + "type": "string", + "description": "OS is the OS name, typically corresponding to ID in os-release (e.g. \"fedora\")" + }, + "osVersion": { + "type": "string", + "description": "osVersion is the version of the OS, typically corresponding to VERSION_ID in os-release (e.g. \"33\")" + }, + "system": { + "type": "string", + "description": "System is a context-specific name for the system that the binary package is intended to run on or a part of" + }, + "vendor": { + "type": "string", + "description": "Vendor is the individual or organization that produced the source code for the binary" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo is the URL to the source repository for which the binary was built from" + }, + "commit": { + "type": "string", + "description": "Commit is the commit hash of the source repository for which the binary was built from" + } + }, + "type": "object", + "description": "ELFBinaryPackageNoteJSONPayload Represents metadata captured from the .note.package section of an ELF-formatted binary" + }, + "ElixirMixLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the mix.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the mix.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method, replaces deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum is deprecated - SHA-256 of concatenated file contents excluding CHECKSUM file, now replaced by outer checksum)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ElixirMixLockEntry is a struct that represents a single entry in a mix.lock file" + }, + "ErlangRebarLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the rebar.lock file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the rebar.lock file" + }, + "pkgHash": { + "type": "string", + "description": "PkgHash is the outer checksum (SHA-256) of the entire Hex package tarball for integrity verification (preferred method over deprecated inner checksum)" + }, + "pkgHashExt": { + "type": "string", + "description": "PkgHashExt is the extended package hash format (inner checksum deprecated - was SHA-256 of concatenated file contents)" + } + }, + "type": "object", + "required": [ + "name", + "version", + "pkgHash", + "pkgHashExt" + ], + "description": "ErlangRebarLockEntry represents a single package entry from the \"deps\" section within an Erlang rebar.lock file." + }, + "Executable": { + "properties": { + "format": { + "type": "string", + "description": "Format denotes either ELF, Mach-O, or PE" + }, + "hasExports": { + "type": "boolean", + "description": "HasExports indicates whether the binary exports symbols." + }, + "hasEntrypoint": { + "type": "boolean", + "description": "HasEntrypoint indicates whether the binary has an entry point function." + }, + "importedLibraries": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ImportedLibraries lists the shared libraries required by this executable." + }, + "elfSecurityFeatures": { + "$ref": "#/$defs/ELFSecurityFeatures", + "description": "ELFSecurityFeatures contains ELF-specific security hardening information when Format is ELF." + } + }, + "type": "object", + "required": [ + "format", + "hasExports", + "hasEntrypoint", + "importedLibraries" + ], + "description": "Executable contains metadata about binary files and their security features." + }, + "File": { + "properties": { + "id": { + "type": "string" + }, + "location": { + "$ref": "#/$defs/Coordinates" + }, + "metadata": { + "$ref": "#/$defs/FileMetadataEntry" + }, + "contents": { + "type": "string" + }, + "digests": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/FileLicense" + }, + "type": "array" + }, + "executable": { + "$ref": "#/$defs/Executable" + }, + "unknowns": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "type": "object", + "required": [ + "id", + "location" + ] + }, + "FileLicense": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "evidence": { + "$ref": "#/$defs/FileLicenseEvidence" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type" + ] + }, + "FileLicenseEvidence": { + "properties": { + "confidence": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "extent": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "confidence", + "offset", + "extent" + ] + }, + "FileMetadataEntry": { + "properties": { + "mode": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "linkDestination": { + "type": "string" + }, + "userID": { + "type": "integer" + }, + "groupID": { + "type": "integer" + }, + "mimeType": { + "type": "string" + }, + "size": { + "type": "integer" + } + }, + "type": "object", + "required": [ + "mode", + "type", + "userID", + "groupID", + "mimeType", + "size" + ] + }, + "GithubActionsUseStatement": { + "properties": { + "value": { + "type": "string", + "description": "Value is the action reference (e.g. \"actions/checkout@v3\")" + }, + "comment": { + "type": "string", + "description": "Comment is the inline comment associated with this uses statement" + } + }, + "type": "object", + "required": [ + "value" + ], + "description": "GitHubActionsUseStatement represents a single 'uses' statement in a GitHub Actions workflow file referencing an action or reusable workflow." + }, + "GoModuleBuildinfoEntry": { + "properties": { + "goBuildSettings": { + "$ref": "#/$defs/KeyValues", + "description": "BuildSettings contains the Go build settings and flags used to compile the binary (e.g., GOARCH, GOOS, CGO_ENABLED)." + }, + "goCompiledVersion": { + "type": "string", + "description": "GoCompiledVersion is the version of Go used to compile the binary." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for the binary (extracted from GOARCH build setting)." + }, + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format for the main module from go.sum." + }, + "mainModule": { + "type": "string", + "description": "MainModule is the main module path for the binary (e.g., \"github.com/anchore/syft\")." + }, + "goCryptoSettings": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoCryptoSettings contains FIPS and cryptographic configuration settings if present." + }, + "goExperiments": { + "items": { + "type": "string" + }, + "type": "array", + "description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")." + } + }, + "type": "object", + "required": [ + "goCompiledVersion", + "architecture" + ], + "description": "GolangBinaryBuildinfoEntry represents all captured data for a Golang binary" + }, + "GoModuleEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + } + }, + "type": "object", + "description": "GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum" + }, + "GoSourceEntry": { + "properties": { + "h1Digest": { + "type": "string", + "description": "H1Digest is the Go module hash in h1: format from go.sum for verifying module contents." + }, + "os": { + "type": "string", + "description": "OperatingSystem is the target OS for build constraints (e.g., \"linux\", \"darwin\", \"windows\")." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture for build constraints (e.g., \"amd64\", \"arm64\")." + }, + "buildTags": { + "type": "string", + "description": "BuildTags are the build tags used to conditionally compile code (e.g., \"integration,debug\")." + }, + "cgoEnabled": { + "type": "boolean", + "description": "CgoEnabled indicates whether CGO was enabled for this package." + } + }, + "type": "object", + "required": [ + "cgoEnabled" + ], + "description": "GolangSourceEntry represents all captured data for a Golang package found through source analysis" + }, + "HaskellHackageStackEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + } + }, + "type": "object", + "description": "HackageStackYamlEntry represents a single entry from the \"extra-deps\" section of a stack.yaml file." + }, + "HaskellHackageStackLockEntry": { + "properties": { + "pkgHash": { + "type": "string", + "description": "PkgHash is the package content hash for verification" + }, + "snapshotURL": { + "type": "string", + "description": "SnapshotURL is the URL to the Stack snapshot this package came from" + } + }, + "type": "object", + "description": "HackageStackYamlLockEntry represents a single entry from the \"packages\" section of a stack.yaml.lock file." + }, + "HomebrewFormula": { + "properties": { + "tap": { + "type": "string", + "description": "Tap is Homebrew tap this formula belongs to (e.g. \"homebrew/core\")" + }, + "homepage": { + "type": "string", + "description": "Homepage is the upstream project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable formula description" + } + }, + "type": "object", + "description": "HomebrewFormula represents metadata about a Homebrew formula package extracted from formula JSON files." + }, + "IDLikes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "JavaArchive": { + "properties": { + "virtualPath": { + "type": "string", + "description": "VirtualPath is path within the archive hierarchy, where nested entries are delimited with ':' (for nested JARs)" + }, + "manifest": { + "$ref": "#/$defs/JavaManifest", + "description": "Manifest is parsed META-INF/MANIFEST.MF contents" + }, + "pomProperties": { + "$ref": "#/$defs/JavaPomProperties", + "description": "PomProperties is parsed pom.properties file contents" + }, + "pomProject": { + "$ref": "#/$defs/JavaPomProject", + "description": "PomProject is parsed pom.xml file contents" + }, + "digest": { + "items": { + "$ref": "#/$defs/Digest" + }, + "type": "array", + "description": "ArchiveDigests is cryptographic hashes of the archive file" + } + }, + "type": "object", + "required": [ + "virtualPath" + ], + "description": "JavaArchive encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship." + }, + "JavaJvmInstallation": { + "properties": { + "release": { + "$ref": "#/$defs/JavaVMRelease", + "description": "Release is JVM release information and version details" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files that are part of this JVM installation" + } + }, + "type": "object", + "required": [ + "release", + "files" + ], + "description": "JavaVMInstallation represents a Java Virtual Machine installation discovered on the system with its release information and file list." + }, + "JavaManifest": { + "properties": { + "main": { + "$ref": "#/$defs/KeyValues", + "description": "Main is main manifest attributes as key-value pairs" + }, + "sections": { + "items": { + "$ref": "#/$defs/KeyValues" + }, + "type": "array", + "description": "Sections are the named sections from the manifest (e.g. per-entry attributes)" + } + }, + "type": "object", + "description": "JavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file." + }, + "JavaPomParent": { + "properties": { + "groupId": { + "type": "string", + "description": "GroupID is the parent Maven group identifier" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is the parent Maven artifact identifier" + }, + "version": { + "type": "string", + "description": "Version is the parent version (child inherits configuration from this specific version of parent POM)" + } + }, + "type": "object", + "required": [ + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomParent contains the fields within the \u003cparent\u003e tag in a pom.xml file" + }, + "JavaPomProject": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.xml file within the archive" + }, + "parent": { + "$ref": "#/$defs/JavaPomParent", + "description": "Parent is the parent POM reference for inheritance (child POMs inherit configuration from parent)" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier (reversed domain name like org.apache.maven)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier (project name)" + }, + "version": { + "type": "string", + "description": "Version is project version (together with groupId and artifactId forms Maven coordinates groupId:artifactId:version)" + }, + "name": { + "type": "string", + "description": "Name is a human-readable project name (displayed in Maven-generated documentation)" + }, + "description": { + "type": "string", + "description": "Description is detailed project description" + }, + "url": { + "type": "string", + "description": "URL is the project URL (typically project website or repository)" + } + }, + "type": "object", + "required": [ + "path", + "groupId", + "artifactId", + "version", + "name" + ], + "description": "JavaPomProject represents fields of interest extracted from a Java archive's pom.xml file." + }, + "JavaPomProperties": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the pom.properties file within the archive" + }, + "name": { + "type": "string", + "description": "Name is the project name" + }, + "groupId": { + "type": "string", + "description": "GroupID is Maven group identifier uniquely identifying the project across all projects (follows reversed domain name convention like com.company.project)" + }, + "artifactId": { + "type": "string", + "description": "ArtifactID is Maven artifact identifier, the name of the jar/artifact (unique within the groupId scope)" + }, + "version": { + "type": "string", + "description": "Version is artifact version" + }, + "scope": { + "type": "string", + "description": "Scope is dependency scope determining when dependency is available (compile=default all phases, test=test compilation/execution only, runtime=runtime and test not compile, provided=expected from JDK or container)" + }, + "extraFields": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Extra is additional custom properties not in standard Maven coordinates" + } + }, + "type": "object", + "required": [ + "path", + "name", + "groupId", + "artifactId", + "version" + ], + "description": "JavaPomProperties represents the fields of interest extracted from a Java archive's pom.properties file." + }, + "JavaVMRelease": { + "properties": { + "implementor": { + "type": "string", + "description": "Implementor is extracted with the `java.vendor` JVM property" + }, + "implementorVersion": { + "type": "string", + "description": "ImplementorVersion is extracted with the `java.vendor.version` JVM property" + }, + "javaRuntimeVersion": { + "type": "string", + "description": "JavaRuntimeVersion is extracted from the 'java.runtime.version' JVM property" + }, + "javaVersion": { + "type": "string", + "description": "JavaVersion matches that from `java -version` command output" + }, + "javaVersionDate": { + "type": "string", + "description": "JavaVersionDate is extracted from the 'java.version.date' JVM property" + }, + "libc": { + "type": "string", + "description": "Libc can either be 'glibc' or 'musl'" + }, + "modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Modules is a list of JVM modules that are packaged" + }, + "osArch": { + "type": "string", + "description": "OsArch is the target CPU architecture" + }, + "osName": { + "type": "string", + "description": "OsName is the name of the target runtime operating system environment" + }, + "osVersion": { + "type": "string", + "description": "OsVersion is the version of the target runtime operating system environment" + }, + "source": { + "type": "string", + "description": "Source refers to the origin repository of OpenJDK source" + }, + "buildSource": { + "type": "string", + "description": "BuildSource Git SHA of the build repository" + }, + "buildSourceRepo": { + "type": "string", + "description": "BuildSourceRepo refers to rhe repository URL for the build source" + }, + "sourceRepo": { + "type": "string", + "description": "SourceRepo refers to the OpenJDK repository URL" + }, + "fullVersion": { + "type": "string", + "description": "FullVersion is extracted from the 'java.runtime.version' JVM property" + }, + "semanticVersion": { + "type": "string", + "description": "SemanticVersion is derived from the OpenJDK version" + }, + "buildInfo": { + "type": "string", + "description": "BuildInfo contains additional build information" + }, + "jvmVariant": { + "type": "string", + "description": "JvmVariant specifies the JVM variant (e.g., Hotspot or OpenJ9)" + }, + "jvmVersion": { + "type": "string", + "description": "JvmVersion is extracted from the 'java.vm.version' JVM property" + }, + "imageType": { + "type": "string", + "description": "ImageType can be 'JDK' or 'JRE'" + }, + "buildType": { + "type": "string", + "description": "BuildType can be 'commercial' (used in some older oracle JDK distributions)" + } + }, + "type": "object", + "description": "JavaVMRelease represents JVM version and build information extracted from the release file in a Java installation." + }, + "JavascriptNpmPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in package.json" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in package.json" + }, + "author": { + "type": "string", + "description": "Author is package author name" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is repository or project URL" + }, + "private": { + "type": "boolean", + "description": "Private is whether this is a private package" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "homepage", + "description", + "url", + "private" + ], + "description": "NpmPackage represents the contents of a javascript package.json file." + }, + "JavascriptNpmPackageLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from (registry source)" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification using standard SRI format (sha512-... or sha1-...). npm changed from SHA-1 to SHA-512 in newer versions. For registry sources this is the integrity from registry, for remote tarballs it's SHA-512 of the file. npm verifies tarball matches this hash before unpacking, throwing EINTEGRITY error if mismatch detected." + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ], + "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file." + }, + "JavascriptYarnLockEntry": { + "properties": { + "resolved": { + "type": "string", + "description": "Resolved is URL where this package was downloaded from" + }, + "integrity": { + "type": "string", + "description": "Integrity is Subresource Integrity hash for verification (SRI format)" + } + }, + "type": "object", + "required": [ + "resolved", + "integrity" + ], + "description": "YarnLockEntry represents a single entry section of a yarn.lock file." + }, + "KeyValue": { + "properties": { + "key": { + "type": "string", + "description": "Key is the key name" + }, + "value": { + "type": "string", + "description": "Value is the value associated with the key" + } + }, + "type": "object", + "required": [ + "key", + "value" + ], + "description": "KeyValue represents a single key-value pair." + }, + "KeyValues": { + "items": { + "$ref": "#/$defs/KeyValue" + }, + "type": "array", + "description": "KeyValues represents an ordered collection of key-value pairs that preserves insertion order." + }, + "License": { + "properties": { + "value": { + "type": "string" + }, + "spdxExpression": { + "type": "string" + }, + "type": { + "type": "string" + }, + "urls": { + "items": { + "type": "string" + }, + "type": "array" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "contents": { + "type": "string" + } + }, + "type": "object", + "required": [ + "value", + "spdxExpression", + "type", + "urls", + "locations" + ] + }, + "LinuxKernelArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is kernel name (typically \"Linux\")" + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture" + }, + "version": { + "type": "string", + "description": "Version is kernel version string" + }, + "extendedVersion": { + "type": "string", + "description": "ExtendedVersion is additional version information" + }, + "buildTime": { + "type": "string", + "description": "BuildTime is when the kernel was built" + }, + "author": { + "type": "string", + "description": "Author is who built the kernel" + }, + "format": { + "type": "string", + "description": "Format is kernel image format (e.g. bzImage, zImage)" + }, + "rwRootFS": { + "type": "boolean", + "description": "RWRootFS is whether root filesystem is mounted read-write" + }, + "swapDevice": { + "type": "integer", + "description": "SwapDevice is swap device number" + }, + "rootDevice": { + "type": "integer", + "description": "RootDevice is root device number" + }, + "videoMode": { + "type": "string", + "description": "VideoMode is default video mode setting" + } + }, + "type": "object", + "required": [ + "name", + "architecture", + "version" + ], + "description": "LinuxKernel represents all captured data for a Linux kernel" + }, + "LinuxKernelModule": { + "properties": { + "name": { + "type": "string", + "description": "Name is module name" + }, + "version": { + "type": "string", + "description": "Version is module version string" + }, + "sourceVersion": { + "type": "string", + "description": "SourceVersion is the source code version identifier" + }, + "path": { + "type": "string", + "description": "Path is the filesystem path to the .ko kernel object file (absolute path)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable module description" + }, + "author": { + "type": "string", + "description": "Author is module author name and email" + }, + "license": { + "type": "string", + "description": "License is module license (e.g. GPL, BSD) which must be compatible with kernel" + }, + "kernelVersion": { + "type": "string", + "description": "KernelVersion is kernel version this module was built for" + }, + "versionMagic": { + "type": "string", + "description": "VersionMagic is version magic string for compatibility checking (includes kernel version, SMP status, module loading capabilities like \"3.17.4-302.fc21.x86_64 SMP mod_unload modversions\"). Module will NOT load if vermagic doesn't match running kernel." + }, + "parameters": { + "patternProperties": { + ".*": { + "$ref": "#/$defs/LinuxKernelModuleParameter" + } + }, + "type": "object", + "description": "Parameters are the module parameters that can be configured at load time (user-settable values like module options)" + } + }, + "type": "object", + "description": "LinuxKernelModule represents a loadable kernel module (.ko file) with its metadata, parameters, and dependencies." + }, + "LinuxKernelModuleParameter": { + "properties": { + "type": { + "type": "string", + "description": "Type is parameter data type (e.g. int, string, bool, array types)" + }, + "description": { + "type": "string", + "description": "Description is a human-readable parameter description explaining what the parameter controls" + } + }, + "type": "object", + "description": "LinuxKernelModuleParameter represents a configurable parameter for a kernel module with its type and description." + }, + "LinuxRelease": { + "properties": { + "prettyName": { + "type": "string" + }, + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "idLike": { + "$ref": "#/$defs/IDLikes" + }, + "version": { + "type": "string" + }, + "versionID": { + "type": "string" + }, + "versionCodename": { + "type": "string" + }, + "buildID": { + "type": "string" + }, + "imageID": { + "type": "string" + }, + "imageVersion": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "variantID": { + "type": "string" + }, + "homeURL": { + "type": "string" + }, + "supportURL": { + "type": "string" + }, + "bugReportURL": { + "type": "string" + }, + "privacyPolicyURL": { + "type": "string" + }, + "cpeName": { + "type": "string" + }, + "supportEnd": { + "type": "string" + }, + "extendedSupport": { + "type": "boolean" + } + }, + "type": "object" + }, + "Location": { + "properties": { + "path": { + "type": "string", + "description": "RealPath is the canonical absolute form of the path accessed (all symbolic links have been followed and relative path components like '.' and '..' have been removed)." + }, + "layerID": { + "type": "string", + "description": "FileSystemID is an ID representing and entire filesystem. For container images, this is a layer digest. For directories or a root filesystem, this is blank." + }, + "accessPath": { + "type": "string", + "description": "AccessPath is the path used to retrieve file contents (which may or may not have hardlinks / symlinks in the path)" + }, + "annotations": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object", + "required": [ + "path", + "accessPath" + ], + "description": "Location represents a path relative to a particular filesystem resolved to a specific file.Reference." + }, + "LuarocksPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .rockspec file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .rockspec file" + }, + "license": { + "type": "string", + "description": "License is license identifier" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "url": { + "type": "string", + "description": "URL is the source download URL" + }, + "dependencies": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Dependencies are the map of dependency names to version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "license", + "homepage", + "description", + "url", + "dependencies" + ], + "description": "LuaRocksPackage represents a Lua package managed by the LuaRocks package manager with metadata from .rockspec files." + }, + "MicrosoftKbPatch": { + "properties": { + "product_id": { + "type": "string", + "description": "ProductID is MSRC Product ID (e.g. \"Windows 10 Version 1703 for 32-bit Systems\")" + }, + "kb": { + "type": "string", + "description": "Kb is Knowledge Base article number (e.g. \"5001028\")" + } + }, + "type": "object", + "required": [ + "product_id", + "kb" + ], + "description": "MicrosoftKbPatch is slightly odd in how it is expected to map onto data." + }, + "NixDerivation": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the .drv file in Nix store" + }, + "system": { + "type": "string", + "description": "System is target system string indicating where derivation can be built (e.g. \"x86_64-linux\", \"aarch64-darwin\"). Must match current system for local builds." + }, + "inputDerivations": { + "items": { + "$ref": "#/$defs/NixDerivationReference" + }, + "type": "array", + "description": "InputDerivations are the list of other derivations that were inputs to this build (dependencies)" + }, + "inputSources": { + "items": { + "type": "string" + }, + "type": "array", + "description": "InputSources are the list of source file paths that were inputs to this build" + } + }, + "type": "object", + "description": "NixDerivation represents a Nix .drv file that describes how to build a package including inputs, outputs, and build instructions." + }, + "NixDerivationReference": { + "properties": { + "path": { + "type": "string", + "description": "Path is path to the referenced .drv file" + }, + "outputs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Outputs are which outputs of the referenced derivation were used (e.g. [\"out\"], [\"bin\", \"dev\"])" + } + }, + "type": "object", + "description": "NixDerivationReference represents a reference to another derivation used as a build input or runtime dependency." + }, + "NixStoreEntry": { + "properties": { + "path": { + "type": "string", + "description": "Path is full store path for this output (e.g. /nix/store/abc123...-package-1.0)" + }, + "output": { + "type": "string", + "description": "Output is the specific output name for multi-output packages (empty string for default \"out\" output, can be \"bin\", \"dev\", \"doc\", etc.)" + }, + "outputHash": { + "type": "string", + "description": "OutputHash is hash prefix of the store path basename (first part before the dash)" + }, + "derivation": { + "$ref": "#/$defs/NixDerivation", + "description": "Derivation is information about the .drv file that describes how this package was built" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files are the list of files under the nix/store path for this package" + } + }, + "type": "object", + "required": [ + "outputHash" + ], + "description": "NixStoreEntry represents a package in the Nix store (/nix/store) with its derivation information and metadata." + }, + "OpamPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .opam file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .opam file" + }, + "licenses": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Licenses are the list of applicable licenses" + }, + "url": { + "type": "string", + "description": "URL is download URL for the package source" + }, + "checksum": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Checksums are the list of checksums for verification" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "licenses", + "url", + "checksum", + "homepage", + "dependencies" + ], + "description": "OpamPackage represents an OCaml package managed by the OPAM package manager with metadata from .opam files." + }, + "Package": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "type": { + "type": "string" + }, + "foundBy": { + "type": "string" + }, + "locations": { + "items": { + "$ref": "#/$defs/Location" + }, + "type": "array" + }, + "licenses": { + "$ref": "#/$defs/licenses" + }, + "language": { + "type": "string" + }, + "cpes": { + "$ref": "#/$defs/cpes" + }, + "purl": { + "type": "string" + }, + "metadataType": { + "type": "string" + }, + "metadata": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/AlpmDbEntry" + }, + { + "$ref": "#/$defs/ApkDbEntry" + }, + { + "$ref": "#/$defs/BinarySignature" + }, + { + "$ref": "#/$defs/BitnamiSbomEntry" + }, + { + "$ref": "#/$defs/CConanFileEntry" + }, + { + "$ref": "#/$defs/CConanInfoEntry" + }, + { + "$ref": "#/$defs/CConanLockEntry" + }, + { + "$ref": "#/$defs/CConanLockV2Entry" + }, + { + "$ref": "#/$defs/CocoaPodfileLockEntry" + }, + { + "$ref": "#/$defs/CondaMetadataEntry" + }, + { + "$ref": "#/$defs/DartPubspec" + }, + { + "$ref": "#/$defs/DartPubspecLockEntry" + }, + { + "$ref": "#/$defs/DotnetDepsEntry" + }, + { + "$ref": "#/$defs/DotnetPackagesLockEntry" + }, + { + "$ref": "#/$defs/DotnetPortableExecutableEntry" + }, + { + "$ref": "#/$defs/DpkgArchiveEntry" + }, + { + "$ref": "#/$defs/DpkgDbEntry" + }, + { + "$ref": "#/$defs/ElfBinaryPackageNoteJsonPayload" + }, + { + "$ref": "#/$defs/ElixirMixLockEntry" + }, + { + "$ref": "#/$defs/ErlangRebarLockEntry" + }, + { + "$ref": "#/$defs/GithubActionsUseStatement" + }, + { + "$ref": "#/$defs/GoModuleBuildinfoEntry" + }, + { + "$ref": "#/$defs/GoModuleEntry" + }, + { + "$ref": "#/$defs/GoSourceEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackEntry" + }, + { + "$ref": "#/$defs/HaskellHackageStackLockEntry" + }, + { + "$ref": "#/$defs/HomebrewFormula" + }, + { + "$ref": "#/$defs/JavaArchive" + }, + { + "$ref": "#/$defs/JavaJvmInstallation" + }, + { + "$ref": "#/$defs/JavascriptNpmPackage" + }, + { + "$ref": "#/$defs/JavascriptNpmPackageLockEntry" + }, + { + "$ref": "#/$defs/JavascriptYarnLockEntry" + }, + { + "$ref": "#/$defs/LinuxKernelArchive" + }, + { + "$ref": "#/$defs/LinuxKernelModule" + }, + { + "$ref": "#/$defs/LuarocksPackage" + }, + { + "$ref": "#/$defs/MicrosoftKbPatch" + }, + { + "$ref": "#/$defs/NixStoreEntry" + }, + { + "$ref": "#/$defs/OpamPackage" + }, + { + "$ref": "#/$defs/PeBinary" + }, + { + "$ref": "#/$defs/PhpComposerInstalledEntry" + }, + { + "$ref": "#/$defs/PhpComposerLockEntry" + }, + { + "$ref": "#/$defs/PhpPearEntry" + }, + { + "$ref": "#/$defs/PhpPeclEntry" + }, + { + "$ref": "#/$defs/PortageDbEntry" + }, + { + "$ref": "#/$defs/PythonPackage" + }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, + { + "$ref": "#/$defs/PythonPipRequirementsEntry" + }, + { + "$ref": "#/$defs/PythonPipfileLockEntry" + }, + { + "$ref": "#/$defs/PythonPoetryLockEntry" + }, + { + "$ref": "#/$defs/PythonUvLockEntry" + }, + { + "$ref": "#/$defs/RDescription" + }, + { + "$ref": "#/$defs/RpmArchive" + }, + { + "$ref": "#/$defs/RpmDbEntry" + }, + { + "$ref": "#/$defs/RubyGemspec" + }, + { + "$ref": "#/$defs/RustCargoAuditEntry" + }, + { + "$ref": "#/$defs/RustCargoLockEntry" + }, + { + "$ref": "#/$defs/SnapEntry" + }, + { + "$ref": "#/$defs/SwiftPackageManagerLockEntry" + }, + { + "$ref": "#/$defs/SwiplpackPackage" + }, + { + "$ref": "#/$defs/TerraformLockProviderEntry" + }, + { + "$ref": "#/$defs/WordpressPluginEntry" + } + ] + } + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "foundBy", + "locations", + "licenses", + "language", + "cpes", + "purl" + ], + "description": "Package represents a pkg.Package object specialized for JSON marshaling and unmarshalling." + }, + "PeBinary": { + "properties": { + "VersionResources": { + "$ref": "#/$defs/KeyValues", + "description": "VersionResources contains key-value pairs extracted from the PE file's version resource section (e.g., FileVersion, ProductName, CompanyName)." + } + }, + "type": "object", + "required": [ + "VersionResources" + ], + "description": "PEBinary represents metadata captured from a Portable Executable formatted binary (dll, exe, etc.)" + }, + "PhpComposerAuthors": { + "properties": { + "name": { + "type": "string", + "description": "Name is author's full name" + }, + "email": { + "type": "string", + "description": "Email is author's email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is author's personal or company website" + } + }, + "type": "object", + "required": [ + "name" + ], + "description": "PhpComposerAuthors represents author information for a PHP Composer package from the authors field in composer.json." + }, + "PhpComposerExternalReference": { + "properties": { + "type": { + "type": "string", + "description": "Type is reference type (git for source VCS, zip/tar for dist archives)" + }, + "url": { + "type": "string", + "description": "URL is the URL to the resource (git repository URL or archive download URL)" + }, + "reference": { + "type": "string", + "description": "Reference is git commit hash or version tag for source, or archive version for dist" + }, + "shasum": { + "type": "string", + "description": "Shasum is SHA hash of the archive file for integrity verification (dist only)" + } + }, + "type": "object", + "required": [ + "type", + "url", + "reference" + ], + "description": "PhpComposerExternalReference represents source or distribution information for a PHP package, indicating where the package code is retrieved from." + }, + "PhpComposerInstalledEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerInstalledEntry represents a single package entry from a composer v1/v2 \"installed.json\" files (very similar to composer.lock files)." + }, + "PhpComposerLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is package name in vendor/package format (e.g. symfony/console)" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "source": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Source is the source repository information for development (typically git repo, used when passing --prefer-source). Originates from source code repository." + }, + "dist": { + "$ref": "#/$defs/PhpComposerExternalReference", + "description": "Dist is distribution archive information for production (typically zip/tar, default install method). Packaged version of released code." + }, + "require": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)" + }, + "provide": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)" + }, + "require-dev": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)" + }, + "suggest": { + "patternProperties": { + ".*": { + "type": "string" + } + }, + "type": "object", + "description": "Suggest is optional but recommended dependencies (suggestions for packages that would extend functionality)" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of license identifiers (SPDX format)" + }, + "type": { + "type": "string", + "description": "Type is package type indicating purpose (library=reusable code, project=application, metapackage=aggregates dependencies, etc.)" + }, + "notification-url": { + "type": "string", + "description": "NotificationURL is the URL to notify when package is installed (for tracking/statistics)" + }, + "bin": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Bin is the list of binary/executable files that should be added to PATH" + }, + "authors": { + "items": { + "$ref": "#/$defs/PhpComposerAuthors" + }, + "type": "array", + "description": "Authors are the list of package authors with name/email/homepage" + }, + "description": { + "type": "string", + "description": "Description is a human-readable package description" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "keywords": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Keywords are the list of keywords for package discovery/search" + }, + "time": { + "type": "string", + "description": "Time is timestamp when this package version was released" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "dist" + ], + "description": "PhpComposerLockEntry represents a single package entry found from a composer.lock file." + }, + "PhpPearEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPearEntry represents a single package entry found within php pear metadata files." + }, + "PhpPeclEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name" + }, + "channel": { + "type": "string", + "description": "Channel is PEAR channel this package is from" + }, + "version": { + "type": "string", + "description": "Version is the package version" + }, + "license": { + "items": { + "type": "string" + }, + "type": "array", + "description": "License is the list of applicable licenses" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "PhpPeclEntry represents a single package entry found within php pecl metadata files." + }, + "PortageDbEntry": { + "properties": { + "installedSize": { + "type": "integer", + "description": "InstalledSize is total size of installed files in bytes" + }, + "licenses": { + "type": "string", + "description": "Licenses is license string which may be an expression (e.g. \"GPL-2 OR Apache-2.0\")" + }, + "files": { + "items": { + "$ref": "#/$defs/PortageFileRecord" + }, + "type": "array", + "description": "Files are the files installed by this package (tracked in CONTENTS file)" + } + }, + "type": "object", + "required": [ + "installedSize", + "files" + ], + "description": "PortageEntry represents a single package entry in the portage DB flat-file store." + }, + "PortageFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the file path relative to the filesystem root" + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest is file content hash (MD5 for regular files in CONTENTS format: \"obj filename md5hash mtime\")" + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PortageFileRecord represents a single file attributed to a portage package." + }, + "PythonDirectURLOriginInfo": { + "properties": { + "url": { + "type": "string", + "description": "URL is the source URL from which the package was installed." + }, + "commitId": { + "type": "string", + "description": "CommitID is the VCS commit hash if installed from version control." + }, + "vcs": { + "type": "string", + "description": "VCS is the version control system type (e.g., \"git\", \"hg\")." + } + }, + "type": "object", + "required": [ + "url" + ], + "description": "PythonDirectURLOriginInfo represents installation source metadata from direct_url.json for packages installed from VCS or direct URLs." + }, + "PythonFileDigest": { + "properties": { + "algorithm": { + "type": "string", + "description": "Algorithm is the hash algorithm used (e.g., \"sha256\")." + }, + "value": { + "type": "string", + "description": "Value is the hex-encoded hash digest value." + } + }, + "type": "object", + "required": [ + "algorithm", + "value" + ], + "description": "PythonFileDigest represents the file metadata for a single file attributed to a python package." + }, + "PythonFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the installed file path from the RECORD file." + }, + "digest": { + "$ref": "#/$defs/PythonFileDigest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "size": { + "type": "string", + "description": "Size is the file size in bytes as a string." + } + }, + "type": "object", + "required": [ + "path" + ], + "description": "PythonFileRecord represents a single entry within a RECORD file for a python wheel or egg package" + }, + "PythonPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the Name field in PKG-INFO or METADATA." + }, + "version": { + "type": "string", + "description": "Version is the package version from the Version field in PKG-INFO or METADATA." + }, + "author": { + "type": "string", + "description": "Author is the package author name from the Author field." + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is the package author's email address from the Author-Email field." + }, + "platform": { + "type": "string", + "description": "Platform indicates the target platform for the package (e.g., \"any\", \"linux\", \"win32\")." + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the installed files listed in the RECORD file for wheels or installed-files.txt for eggs." + }, + "sitePackagesRootPath": { + "type": "string", + "description": "SitePackagesRootPath is the root directory path containing the package (e.g., \"/usr/lib/python3.9/site-packages\")." + }, + "topLevelPackages": { + "items": { + "type": "string" + }, + "type": "array", + "description": "TopLevelPackages are the top-level Python module names from top_level.txt file." + }, + "directUrlOrigin": { + "$ref": "#/$defs/PythonDirectURLOriginInfo", + "description": "DirectURLOrigin contains VCS or direct URL installation information from direct_url.json." + }, + "requiresPython": { + "type": "string", + "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")." + }, + "requiresDist": { + "items": { + "type": "string" + }, + "type": "array", + "description": "RequiresDist lists the package dependencies with version specifiers from Requires-Dist fields." + }, + "providesExtra": { + "items": { + "type": "string" + }, + "type": "array", + "description": "ProvidesExtra lists optional feature names that can be installed via extras (e.g., \"dev\", \"test\")." + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "platform", + "sitePackagesRootPath" + ], + "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." + }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications, without environment qualifiers" + } + }, + "type": "object", + "required": [ + "summary", + "files", + "dependencies" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, + "PythonPipRequirementsEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name from the requirements file." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional features to install from the package (e.g., package[dev,test])." + }, + "versionConstraint": { + "type": "string", + "description": "VersionConstraint specifies version requirements (e.g., \"\u003e=1.0,\u003c2.0\")." + }, + "url": { + "type": "string", + "description": "URL is the direct download URL or VCS URL if specified instead of a PyPI package." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions for conditional installation (e.g., \"python_version \u003e= '3.8'\")." + } + }, + "type": "object", + "required": [ + "name", + "versionConstraint" + ], + "description": "PythonRequirementsEntry represents a single entry within a [*-]requirements.txt file." + }, + "PythonPipfileLockEntry": { + "properties": { + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are the package file hash values in the format \"algorithm:digest\" for integrity verification." + }, + "index": { + "type": "string", + "description": "Index is the PyPI index name where the package should be fetched from." + } + }, + "type": "object", + "required": [ + "hashes", + "index" + ], + "description": "PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "version": { + "type": "string", + "description": "Version is the locked version or version constraint for the dependency." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "version", + "optional" + ], + "description": "PythonPoetryLockDependencyEntry represents a single dependency entry within a Poetry lock file." + }, + "PythonPoetryLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonPoetryLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonPoetryLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonPoetryLockEntry represents a single package entry within a Pipfile.lock file." + }, + "PythonPoetryLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonPoetryLockExtraEntry represents an optional feature group in a Poetry lock file." + }, + "PythonUvLockDependencyEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the dependency package name." + }, + "optional": { + "type": "boolean", + "description": "Optional indicates whether this dependency is optional (only needed for certain extras)." + }, + "markers": { + "type": "string", + "description": "Markers are environment marker expressions that conditionally enable the dependency (e.g., \"python_version \u003e= '3.8'\")." + }, + "extras": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Extras are the optional feature names from the dependency that should be installed." + } + }, + "type": "object", + "required": [ + "name", + "optional" + ], + "description": "PythonUvLockDependencyEntry represents a single dependency entry within a uv lock file." + }, + "PythonUvLockEntry": { + "properties": { + "index": { + "type": "string", + "description": "Index is the package repository name where the package should be fetched from." + }, + "dependencies": { + "items": { + "$ref": "#/$defs/PythonUvLockDependencyEntry" + }, + "type": "array", + "description": "Dependencies are the package's runtime dependencies with version constraints." + }, + "extras": { + "items": { + "$ref": "#/$defs/PythonUvLockExtraEntry" + }, + "type": "array", + "description": "Extras are optional feature groups that include additional dependencies." + } + }, + "type": "object", + "required": [ + "index", + "dependencies" + ], + "description": "PythonUvLockEntry represents a single package entry within a uv.lock file." + }, + "PythonUvLockExtraEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the optional feature name (e.g., \"dev\", \"test\")." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the package names required when this extra is installed." + } + }, + "type": "object", + "required": [ + "name", + "dependencies" + ], + "description": "PythonUvLockExtraEntry represents an optional feature group in a uv lock file." + }, + "RDescription": { + "properties": { + "title": { + "type": "string", + "description": "Title is short one-line package title" + }, + "description": { + "type": "string", + "description": "Description is detailed package description" + }, + "author": { + "type": "string", + "description": "Author is package author(s)" + }, + "maintainer": { + "type": "string", + "description": "Maintainer is current package maintainer" + }, + "url": { + "items": { + "type": "string" + }, + "type": "array", + "description": "URL is the list of related URLs" + }, + "repository": { + "type": "string", + "description": "Repository is CRAN or other repository name" + }, + "built": { + "type": "string", + "description": "Built is R version and platform this was built with" + }, + "needsCompilation": { + "type": "boolean", + "description": "NeedsCompilation is whether this package requires compilation" + }, + "imports": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Imports are the packages imported in the NAMESPACE" + }, + "depends": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Depends are the packages this package depends on" + }, + "suggests": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Suggests are the optional packages that extend functionality" + } + }, + "type": "object", + "description": "RDescription represents metadata from an R package DESCRIPTION file containing package information, dependencies, and author details." + }, + "Relationship": { + "properties": { + "parent": { + "type": "string" + }, + "child": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "parent", + "child", + "type" + ] + }, + "RpmArchive": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmArchive represents package metadata extracted directly from a .rpm archive file, containing the same information as an RPM database entry." + }, + "RpmDbEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is the RPM package name as found in the RPM database." + }, + "version": { + "type": "string", + "description": "Version is the upstream version of the package." + }, + "epoch": { + "oneOf": [ + { + "type": "integer", + "description": "Epoch is the version epoch used to force upgrade ordering (null if not set)." + }, + { + "type": "null" + } + ] + }, + "architecture": { + "type": "string", + "description": "Arch is the target CPU architecture (e.g., \"x86_64\", \"aarch64\", \"noarch\")." + }, + "release": { + "type": "string", + "description": "Release is the package release number or distribution-specific version suffix." + }, + "sourceRpm": { + "type": "string", + "description": "SourceRpm is the source RPM filename that was used to build this package." + }, + "signatures": { + "items": { + "$ref": "#/$defs/RpmSignature" + }, + "type": "array", + "description": "Signatures contains GPG signature metadata for package verification." + }, + "size": { + "type": "integer", + "description": "Size is the total installed size of the package in bytes." + }, + "vendor": { + "type": "string", + "description": "Vendor is the organization that packaged the software." + }, + "modularityLabel": { + "type": "string", + "description": "ModularityLabel identifies the module stream for modular RPM packages (e.g., \"nodejs:12:20200101\")." + }, + "provides": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Provides lists the virtual packages and capabilities this package provides." + }, + "requires": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Requires lists the dependencies required by this package." + }, + "files": { + "items": { + "$ref": "#/$defs/RpmFileRecord" + }, + "type": "array", + "description": "Files are the file records for all files owned by this package." + } + }, + "type": "object", + "required": [ + "name", + "version", + "epoch", + "architecture", + "release", + "sourceRpm", + "size", + "vendor", + "files" + ], + "description": "RpmDBEntry represents all captured data from a RPM DB package entry." + }, + "RpmFileRecord": { + "properties": { + "path": { + "type": "string", + "description": "Path is the absolute file path where the file is installed." + }, + "mode": { + "type": "integer", + "description": "Mode is the file permission mode bits following Unix stat.h conventions." + }, + "size": { + "type": "integer", + "description": "Size is the file size in bytes." + }, + "digest": { + "$ref": "#/$defs/Digest", + "description": "Digest contains the hash algorithm and value for file integrity verification." + }, + "userName": { + "type": "string", + "description": "UserName is the owner username for the file." + }, + "groupName": { + "type": "string", + "description": "GroupName is the group name for the file." + }, + "flags": { + "type": "string", + "description": "Flags indicates the file type (e.g., \"%config\", \"%doc\", \"%ghost\")." + } + }, + "type": "object", + "required": [ + "path", + "mode", + "size", + "digest", + "userName", + "groupName", + "flags" + ], + "description": "RpmFileRecord represents the file metadata for a single file attributed to a RPM package." + }, + "RpmSignature": { + "properties": { + "algo": { + "type": "string", + "description": "PublicKeyAlgorithm is the public key algorithm used for signing (e.g., \"RSA\")." + }, + "hash": { + "type": "string", + "description": "HashAlgorithm is the hash algorithm used for the signature (e.g., \"SHA256\")." + }, + "created": { + "type": "string", + "description": "Created is the timestamp when the signature was created." + }, + "issuer": { + "type": "string", + "description": "IssuerKeyID is the GPG key ID that created the signature." + } + }, + "type": "object", + "required": [ + "algo", + "hash", + "created", + "issuer" + ], + "description": "RpmSignature represents a GPG signature for an RPM package used for authenticity verification." + }, + "RubyGemspec": { + "properties": { + "name": { + "type": "string", + "description": "Name is gem name as specified in the gemspec" + }, + "version": { + "type": "string", + "description": "Version is gem version as specified in the gemspec" + }, + "files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Files is logical list of files in the gem (NOT directly usable as filesystem paths. Example: bundler gem lists \"lib/bundler/vendor/uri/lib/uri/ldap.rb\" but actual path is \"/usr/local/lib/ruby/3.2.0/bundler/vendor/uri/lib/uri/ldap.rb\". Would need gem installation path, ruby version, and env vars like GEM_HOME to resolve actual paths.)" + }, + "authors": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Authors are the list of gem authors (stored as array regardless of using `author` or `authors` method in gemspec)" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + } + }, + "type": "object", + "required": [ + "name", + "version" + ], + "description": "RubyGemspec represents all metadata parsed from the *.gemspec file" + }, + "RustCargoAuditEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in audit section of the build binary" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in audit section of the build binary" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository where this crate came from" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source" + ], + "description": "RustBinaryAuditEntry represents Rust crate metadata extracted from a compiled binary using cargo-auditable format." + }, + "RustCargoLockEntry": { + "properties": { + "name": { + "type": "string", + "description": "Name is crate name as specified in Cargo.toml" + }, + "version": { + "type": "string", + "description": "Version is crate version as specified in Cargo.toml" + }, + "source": { + "type": "string", + "description": "Source is the source registry or repository URL in format \"registry+https://github.com/rust-lang/crates.io-index\" for registry packages" + }, + "checksum": { + "type": "string", + "description": "Checksum is content checksum for registry packages only (hexadecimal string). Cargo doesn't require or include checksums for git dependencies. Used to detect MITM attacks by verifying downloaded crate matches lockfile checksum." + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of dependencies with version constraints" + } + }, + "type": "object", + "required": [ + "name", + "version", + "source", + "checksum", + "dependencies" + ], + "description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information." + }, + "Schema": { + "properties": { + "version": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object", + "required": [ + "version", + "url" + ] + }, + "SnapEntry": { + "properties": { + "snapType": { + "type": "string", + "description": "SnapType indicates the snap type (base, kernel, app, gadget, or snapd)." + }, + "base": { + "type": "string", + "description": "Base is the base snap name that this snap depends on (e.g., \"core20\", \"core22\")." + }, + "snapName": { + "type": "string", + "description": "SnapName is the snap package name." + }, + "snapVersion": { + "type": "string", + "description": "SnapVersion is the snap package version." + }, + "architecture": { + "type": "string", + "description": "Architecture is the target CPU architecture (e.g., \"amd64\", \"arm64\")." + } + }, + "type": "object", + "required": [ + "snapType", + "base", + "snapName", + "snapVersion", + "architecture" + ], + "description": "SnapEntry represents metadata for a Snap package extracted from snap.yaml or snapcraft.yaml files." + }, + "Source": { + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "supplier": { + "type": "string" + }, + "type": { + "type": "string" + }, + "metadata": true + }, + "type": "object", + "required": [ + "id", + "name", + "version", + "type", + "metadata" + ], + "description": "Instead, the Supplier can be determined by the user of syft and passed as a config or flag to help fulfill the NTIA minimum elements." + }, + "SwiftPackageManagerLockEntry": { + "properties": { + "revision": { + "type": "string", + "description": "Revision is git commit hash of the resolved package" + } + }, + "type": "object", + "required": [ + "revision" + ], + "description": "SwiftPackageManagerResolvedEntry represents a resolved dependency from a Package.resolved file with its locked version and source location." + }, + "SwiplpackPackage": { + "properties": { + "name": { + "type": "string", + "description": "Name is the package name as found in the .toml file" + }, + "version": { + "type": "string", + "description": "Version is the package version as found in the .toml file" + }, + "author": { + "type": "string", + "description": "Author is author name" + }, + "authorEmail": { + "type": "string", + "description": "AuthorEmail is author email address" + }, + "packager": { + "type": "string", + "description": "Packager is packager name (if different from author)" + }, + "packagerEmail": { + "type": "string", + "description": "PackagerEmail is packager email address" + }, + "homepage": { + "type": "string", + "description": "Homepage is project homepage URL" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the list of required dependencies" + } + }, + "type": "object", + "required": [ + "name", + "version", + "author", + "authorEmail", + "packager", + "packagerEmail", + "homepage", + "dependencies" + ], + "description": "SwiplPackEntry represents a SWI-Prolog package from the pack system with metadata about the package and its dependencies." + }, + "TerraformLockProviderEntry": { + "properties": { + "url": { + "type": "string", + "description": "URL is the provider source address (e.g., \"registry.terraform.io/hashicorp/aws\")." + }, + "constraints": { + "type": "string", + "description": "Constraints specifies the version constraints for the provider (e.g., \"~\u003e 4.0\")." + }, + "version": { + "type": "string", + "description": "Version is the locked provider version selected during terraform init." + }, + "hashes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Hashes are cryptographic checksums for the provider plugin archives across different platforms." + } + }, + "type": "object", + "required": [ + "url", + "constraints", + "version", + "hashes" + ], + "description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)." + }, + "WordpressPluginEntry": { + "properties": { + "pluginInstallDirectory": { + "type": "string", + "description": "PluginInstallDirectory is directory name where the plugin is installed" + }, + "author": { + "type": "string", + "description": "Author is plugin author name" + }, + "authorUri": { + "type": "string", + "description": "AuthorURI is author's website URL" + } + }, + "type": "object", + "required": [ + "pluginInstallDirectory" + ], + "description": "WordpressPluginEntry represents all metadata parsed from the wordpress plugin file" + }, + "cpes": { + "items": { + "$ref": "#/$defs/CPE" + }, + "type": "array" + }, + "licenses": { + "items": { + "$ref": "#/$defs/License" + }, + "type": "array" + } + } +} diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json index cd8198acb..20e8b3a9f 100644 --- a/schema/json/schema-latest.json +++ b/schema/json/schema-latest.json @@ -1,6 +1,6 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "anchore.io/schema/syft/json/16.0.40/document", + "$id": "anchore.io/schema/syft/json/16.0.41/document", "$ref": "#/$defs/Document", "$defs": { "AlpmDbEntry": { @@ -2549,6 +2549,9 @@ { "$ref": "#/$defs/PythonPackage" }, + { + "$ref": "#/$defs/PythonPdmLockEntry" + }, { "$ref": "#/$defs/PythonPipRequirementsEntry" }, @@ -3131,6 +3134,35 @@ ], "description": "PythonPackage represents all captured data for a python egg or wheel package (specifically as outlined in the PyPA core metadata specification https://packaging.python.org/en/latest/specifications/core-metadata/)." }, + "PythonPdmLockEntry": { + "properties": { + "summary": { + "type": "string", + "description": "Summary provides a description of the package" + }, + "files": { + "items": { + "$ref": "#/$defs/PythonFileRecord" + }, + "type": "array", + "description": "Files are the package files with their paths and hash digests" + }, + "dependencies": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Dependencies are the dependency specifications, without environment qualifiers" + } + }, + "type": "object", + "required": [ + "summary", + "files", + "dependencies" + ], + "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file." + }, "PythonPipRequirementsEntry": { "properties": { "name": { diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go index a720a814d..5f30dacf0 100644 --- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go +++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go @@ -42,6 +42,7 @@ func Test_OriginatorSupplier(t *testing.T) { pkg.PhpPeclEntry{}, pkg.PortageEntry{}, pkg.PythonPipfileLockEntry{}, + pkg.PythonPdmLockEntry{}, pkg.PythonRequirementsEntry{}, pkg.PythonPoetryLockEntry{}, pkg.PythonUvLockEntry{}, @@ -342,6 +343,25 @@ func Test_OriginatorSupplier(t *testing.T) { originator: "Person: auth (auth@auth.gov)", supplier: "Person: auth (auth@auth.gov)", }, + { + name: "from python PDM lock", + input: pkg.Package{ + Metadata: pkg.PythonPdmLockEntry{ + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + }, + }, + }, + Summary: "A test package", + }, + }, + originator: "", + supplier: "", + }, { name: "from r -- maintainer > author", input: pkg.Package{ diff --git a/syft/pkg/cataloger/python/cataloger.go b/syft/pkg/cataloger/python/cataloger.go index 40fbfff7d..69284dcb6 100644 --- a/syft/pkg/cataloger/python/cataloger.go +++ b/syft/pkg/cataloger/python/cataloger.go @@ -30,7 +30,8 @@ func NewPackageCataloger(cfg CatalogerConfig) pkg.Cataloger { WithParserByGlobs(parsePoetryLock, "**/poetry.lock"). WithParserByGlobs(parsePipfileLock, "**/Pipfile.lock"). WithParserByGlobs(parseSetup, "**/setup.py"). - WithParserByGlobs(parseUvLock, "**/uv.lock") + WithParserByGlobs(parseUvLock, "**/uv.lock"). + WithParserByGlobs(parsePdmLock, "**/pdm.lock") } // NewInstalledPackageCataloger returns a new cataloger for python packages within egg or wheel installation directories. diff --git a/syft/pkg/cataloger/python/cataloger_test.go b/syft/pkg/cataloger/python/cataloger_test.go index b3b1d4247..25fa4cc7e 100644 --- a/syft/pkg/cataloger/python/cataloger_test.go +++ b/syft/pkg/cataloger/python/cataloger_test.go @@ -454,6 +454,7 @@ func Test_IndexCataloger_Globs(t *testing.T) { "src/poetry.lock", "src/Pipfile.lock", "src/uv.lock", + "src/pdm.lock", }, }, } diff --git a/syft/pkg/cataloger/python/parse_pdm_lock.go b/syft/pkg/cataloger/python/parse_pdm_lock.go new file mode 100644 index 000000000..66993d515 --- /dev/null +++ b/syft/pkg/cataloger/python/parse_pdm_lock.go @@ -0,0 +1,140 @@ +package python + +import ( + "context" + "fmt" + "strings" + + "github.com/BurntSushi/toml" + "github.com/scylladb/go-set/strset" + + "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" +) + +type pdmLock struct { + Metadata struct { + Groups []string `toml:"groups"` + Strategy []string `toml:"strategy"` + LockVersion string `toml:"lock_version"` + ContentHash string `toml:"content_hash"` + } `toml:"metadata"` + Package []pdmLockPackage `toml:"package"` +} + +type pdmLockPackage struct { + Name string `toml:"name"` + Version string `toml:"version"` + RequiresPython string `toml:"requires_python"` + Summary string `toml:"summary"` + Dependencies []string `toml:"dependencies"` + Files []pdmLockPackageFile `toml:"files"` +} + +type pdmLockPackageFile struct { + File string `toml:"file"` + Hash string `toml:"hash"` +} + +var _ generic.Parser = parsePdmLock + +// parsePdmLock is a parser function for pdm.lock contents, returning python packages discovered. +func parsePdmLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { + var lock pdmLock + _, err := toml.NewDecoder(reader).Decode(&lock) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse pdm.lock file: %w", err) + } + + var pkgs []pkg.Package + for _, p := range lock.Package { + var files []pkg.PythonFileRecord + for _, f := range p.Files { + if colonIndex := strings.Index(f.Hash, ":"); colonIndex != -1 { + algorithm := f.Hash[:colonIndex] + value := f.Hash[colonIndex+1:] + + files = append(files, pkg.PythonFileRecord{ + Path: f.File, + Digest: &pkg.PythonFileDigest{ + Algorithm: algorithm, + Value: value, + }, + }) + } + } + + // only store used part of the dependency information + var deps []string + for _, dep := range p.Dependencies { + // remove environment markers (after semicolon) + dep = strings.Split(dep, ";")[0] + dep = strings.TrimSpace(dep) + if dep != "" { + deps = append(deps, dep) + } + } + + pythonPkgMetadata := pkg.PythonPdmLockEntry{ + Files: files, + Summary: p.Summary, + Dependencies: deps, + } + + pkgs = append(pkgs, newPackageForIndexWithMetadata( + p.Name, + p.Version, + pythonPkgMetadata, + reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation), + )) + } + + relationships := buildPdmRelationships(pkgs) + + return pkgs, relationships, unknown.IfEmptyf(pkgs, "unable to determine packages") +} + +func buildPdmRelationships(pkgs []pkg.Package) []artifact.Relationship { + pkgMap := make(map[string]pkg.Package, len(pkgs)) + for _, p := range pkgs { + pkgMap[p.Name] = p + } + + var relationships []artifact.Relationship + for _, p := range pkgs { + meta, ok := p.Metadata.(pkg.PythonPdmLockEntry) + if !ok { + continue + } + + // collect unique dependencies + added := strset.New() + + for _, depName := range meta.Dependencies { + // Handle version specifiers + depName = strings.Split(depName, "<")[0] + depName = strings.Split(depName, ">")[0] + depName = strings.Split(depName, "=")[0] + depName = strings.Split(depName, "~")[0] + depName = strings.TrimSpace(depName) + + if depName == "" || added.Has(depName) { + continue + } + added.Add(depName) + + if dep, exists := pkgMap[depName]; exists { + relationships = append(relationships, artifact.Relationship{ + From: dep, + To: p, + Type: artifact.DependencyOfRelationship, + }) + } + } + } + + return relationships +} diff --git a/syft/pkg/cataloger/python/parse_pdm_lock_test.go b/syft/pkg/cataloger/python/parse_pdm_lock_test.go new file mode 100644 index 000000000..ba1d6c0dc --- /dev/null +++ b/syft/pkg/cataloger/python/parse_pdm_lock_test.go @@ -0,0 +1,363 @@ +package python + +import ( + "testing" + + "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/pkgtest" +) + +func TestParsePdmLock(t *testing.T) { + + fixture := "test-fixtures/pdm-lock/pdm.lock" + locations := file.NewLocationSet(file.NewLocation(fixture)) + expectedPkgs := []pkg.Package{ + { + Name: "certifi", + Version: "2025.1.31", + PURL: "pkg:pypi/certifi@2025.1.31", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "Python package for providing Mozilla's CA Bundle.", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", + }, + }, + }, + }, + }, + { + Name: "chardet", + Version: "3.0.4", + PURL: "pkg:pypi/chardet@3.0.4", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "Universal encoding detector for Python 2 and 3", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + }, + }, + }, + }, + }, + { + Name: "charset-normalizer", + Version: "2.0.12", + PURL: "pkg:pypi/charset-normalizer@2.0.12", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet.", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", + }, + }, + }, + }, + }, + { + Name: "colorama", + Version: "0.3.9", + PURL: "pkg:pypi/colorama@0.3.9", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "Cross-platform colored terminal text.", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1", + }, + }, + }, + }, + }, + { + Name: "idna", + Version: "2.7", + PURL: "pkg:pypi/idna@2.7", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "Internationalized Domain Names in Applications (IDNA)", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16", + }, + }, + }, + }, + }, + { + Name: "py", + Version: "1.4.34", + PURL: "pkg:pypi/py@1.4.34", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "library with cross-python path, ini-parsing, io, code, log facilities", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3", + }, + }, + }, + }, + }, + { + Name: "pytest", + Version: "3.2.5", + PURL: "pkg:pypi/pytest@3.2.5", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "pytest: simple powerful testing with Python", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "6d5bd4f7113b444c55a3bbb5c738a3dd80d43563d063fc42dcb0aaefbdd78b81", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "241d7e7798d79192a123ceaf64c602b4d233eacf6d6e42ae27caa97f498b7dc6", + }, + }, + }, + Dependencies: []string{ + "argparse", + "colorama", + "ordereddict", + "py>=1.4.33", + "setuptools", + }, + }, + }, + { + Name: "requests", + Version: "2.27.1", + PURL: "pkg:pypi/requests@2.27.1", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "Python HTTP for Humans.", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61", + }, + }, + }, + Dependencies: []string{ + "certifi>=2017.4.17", + "chardet<5,>=3.0.2", + "charset-normalizer~=2.0.0", + "idna<3,>=2.5", + "idna<4,>=2.5", + "urllib3<1.27,>=1.21.1", + }, + }, + }, + { + Name: "setuptools", + Version: "39.2.0", + PURL: "pkg:pypi/setuptools@39.2.0", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "Easily download, build, install, upgrade, and uninstall Python packages", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926", + }, + }, + }, + }, + }, + { + Name: "urllib3", + Version: "1.26.20", + PURL: "pkg:pypi/urllib3@1.26.20", + Locations: locations, + Language: pkg.Python, + Type: pkg.PythonPkg, + Metadata: pkg.PythonPdmLockEntry{ + Summary: "HTTP library with thread-safe connection pooling, file post, and more.", + Files: []pkg.PythonFileRecord{ + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", + }, + }, + { + Path: "", + Digest: &pkg.PythonFileDigest{ + Algorithm: "sha256", + Value: "40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", + }, + }, + }, + }, + }, + } + + // Create a map for easy lookup of packages by name + pkgMap := make(map[string]pkg.Package) + for _, p := range expectedPkgs { + pkgMap[p.Name] = p + } + + expectedRelationships := []artifact.Relationship{ + // pytest dependencies + { + From: pkgMap["colorama"], + To: pkgMap["pytest"], + Type: artifact.DependencyOfRelationship, + }, + { + From: pkgMap["py"], + To: pkgMap["pytest"], + Type: artifact.DependencyOfRelationship, + }, + { + From: pkgMap["setuptools"], + To: pkgMap["pytest"], + Type: artifact.DependencyOfRelationship, + }, + // requests dependencies + { + From: pkgMap["certifi"], + To: pkgMap["requests"], + Type: artifact.DependencyOfRelationship, + }, + { + From: pkgMap["chardet"], + To: pkgMap["requests"], + Type: artifact.DependencyOfRelationship, + }, + { + From: pkgMap["charset-normalizer"], + To: pkgMap["requests"], + Type: artifact.DependencyOfRelationship, + }, + { + From: pkgMap["urllib3"], + To: pkgMap["requests"], + Type: artifact.DependencyOfRelationship, + }, + { + From: pkgMap["idna"], + To: pkgMap["requests"], + Type: artifact.DependencyOfRelationship, + }, + } + + pkgtest.TestFileParser(t, fixture, parsePdmLock, expectedPkgs, expectedRelationships) +} + +func Test_corruptPdmLock(t *testing.T) { + pkgtest.NewCatalogTester(). + FromFile(t, "test-fixtures/glob-paths/src/pdm.lock"). + WithError(). + TestParser(t, parsePdmLock) +} diff --git a/syft/pkg/cataloger/python/test-fixtures/glob-paths/src/pdm.lock b/syft/pkg/cataloger/python/test-fixtures/glob-paths/src/pdm.lock new file mode 100644 index 000000000..5ffba7b57 --- /dev/null +++ b/syft/pkg/cataloger/python/test-fixtures/glob-paths/src/pdm.lock @@ -0,0 +1 @@ +bogus diff --git a/syft/pkg/cataloger/python/test-fixtures/pdm-lock/pdm.lock b/syft/pkg/cataloger/python/test-fixtures/pdm-lock/pdm.lock new file mode 100644 index 000000000..4af7b944d --- /dev/null +++ b/syft/pkg/cataloger/python/test-fixtures/pdm-lock/pdm.lock @@ -0,0 +1,137 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default", "security", "tests"] +strategy = ["inherit_metadata", "static_urls"] +lock_version = "4.5.0" +content_hash = "sha256:2584886ac58a0ae70aa36bc0318b62c3e2c89acc9c21ebb9aee74147c0a9dc06" + +[[metadata.targets]] +requires_python = ">=3.3" + +[[package]] +name = "certifi" +version = "2025.1.31" +requires_python = ">=3.6" +summary = "Python package for providing Mozilla's CA Bundle." +groups = ["security"] +marker = "python_version >= \"3.6\"" +files = [ + {url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, +] + +[[package]] +name = "chardet" +version = "3.0.4" +summary = "Universal encoding detector for Python 2 and 3" +groups = ["default"] +marker = "os_name == \"nt\"" +files = [ + {url = "https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {url = "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +requires_python = ">=3.5.0" +summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +groups = ["security"] +marker = "python_version >= \"3.6\"" +files = [ + {url = "https://files.pythonhosted.org/packages/06/b3/24afc8868eba069a7f03650ac750a778862dc34941a4bebeb58706715726/charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, + {url = "https://files.pythonhosted.org/packages/56/31/7bcaf657fafb3c6db8c787a865434290b726653c912085fbd371e9b92e1c/charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, +] + +[[package]] +name = "colorama" +version = "0.3.9" +summary = "Cross-platform colored terminal text." +groups = ["tests"] +marker = "sys_platform == \"win32\"" +files = [ + {url = "https://files.pythonhosted.org/packages/db/c8/7dcf9dbcb22429512708fe3a547f8b6101c0d02137acbd892505aee57adf/colorama-0.3.9-py2.py3-none-any.whl", hash = "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda"}, + {url = "https://files.pythonhosted.org/packages/e6/76/257b53926889e2835355d74fec73d82662100135293e17d382e2b74d1669/colorama-0.3.9.tar.gz", hash = "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"}, +] + +[[package]] +name = "idna" +version = "2.7" +summary = "Internationalized Domain Names in Applications (IDNA)" +groups = ["default", "security"] +files = [ + {url = "https://files.pythonhosted.org/packages/4b/2a/0276479a4b3caeb8a8c1af2f8e4355746a97fab05a372e4a2c6a6b876165/idna-2.7-py2.py3-none-any.whl", hash = "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e"}, + {url = "https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz", hash = "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"}, +] + +[[package]] +name = "py" +version = "1.4.34" +summary = "library with cross-python path, ini-parsing, io, code, log facilities" +groups = ["tests"] +files = [ + {url = "https://files.pythonhosted.org/packages/53/67/9620edf7803ab867b175e4fd23c7b8bd8eba11cb761514dcd2e726ef07da/py-1.4.34-py2.py3-none-any.whl", hash = "sha256:2ccb79b01769d99115aa600d7eed99f524bf752bba8f041dc1c184853514655a"}, + {url = "https://files.pythonhosted.org/packages/68/35/58572278f1c097b403879c1e9369069633d1cbad5239b9057944bb764782/py-1.4.34.tar.gz", hash = "sha256:0f2d585d22050e90c7d293b6451c83db097df77871974d90efd5a30dc12fcde3"}, +] + +[[package]] +name = "pytest" +version = "3.2.5" +summary = "pytest: simple powerful testing with Python" +groups = ["tests"] +dependencies = [ + "argparse; python_version == \"2.6\"", + "colorama; sys_platform == \"win32\"", + "ordereddict; python_version == \"2.6\"", + "py>=1.4.33", + "setuptools", +] +files = [ + {url = "https://files.pythonhosted.org/packages/1f/f8/8cd74c16952163ce0db0bd95fdd8810cbf093c08be00e6e665ebf0dc3138/pytest-3.2.5.tar.gz", hash = "sha256:6d5bd4f7113b444c55a3bbb5c738a3dd80d43563d063fc42dcb0aaefbdd78b81"}, + {url = "https://files.pythonhosted.org/packages/ef/41/d8a61f1b2ba308e96b36106e95024977e30129355fd12087f23e4b9852a1/pytest-3.2.5-py2.py3-none-any.whl", hash = "sha256:241d7e7798d79192a123ceaf64c602b4d233eacf6d6e42ae27caa97f498b7dc6"}, +] + +[[package]] +name = "requests" +version = "2.27.1" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +summary = "Python HTTP for Humans." +groups = ["security"] +marker = "python_version >= \"3.6\"" +dependencies = [ + "certifi>=2017.4.17", + "chardet<5,>=3.0.2; python_version < \"3\"", + "charset-normalizer~=2.0.0; python_version >= \"3\"", + "idna<3,>=2.5; python_version < \"3\"", + "idna<4,>=2.5; python_version >= \"3\"", + "urllib3<1.27,>=1.21.1", +] +files = [ + {url = "https://files.pythonhosted.org/packages/2d/61/08076519c80041bc0ffa1a8af0cbd3bf3e2b62af10435d269a9d0f40564d/requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {url = "https://files.pythonhosted.org/packages/60/f3/26ff3767f099b73e0efa138a9998da67890793bfa475d8278f84a30fec77/requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, +] + +[[package]] +name = "setuptools" +version = "39.2.0" +requires_python = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*" +summary = "Easily download, build, install, upgrade, and uninstall Python packages" +groups = ["tests"] +files = [ + {url = "https://files.pythonhosted.org/packages/1a/04/d6f1159feaccdfc508517dba1929eb93a2854de729fa68da9d5c6b48fa00/setuptools-39.2.0.zip", hash = "sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2"}, + {url = "https://files.pythonhosted.org/packages/7f/e1/820d941153923aac1d49d7fc37e17b6e73bfbd2904959fffbad77900cf92/setuptools-39.2.0-py2.py3-none-any.whl", hash = "sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926"}, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "HTTP library with thread-safe connection pooling, file post, and more." +groups = ["security"] +marker = "python_version >= \"3.6\"" +files = [ + {url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, +] diff --git a/syft/pkg/python.go b/syft/pkg/python.go index 8b1fdbd6a..1d8ac756e 100644 --- a/syft/pkg/python.go +++ b/syft/pkg/python.go @@ -79,6 +79,16 @@ func (m PythonPackage) OwnedFiles() (result []string) { return result } +// PythonPdmLockEntry represents a single package entry within a pdm.lock file. +type PythonPdmLockEntry struct { + // Summary provides a description of the package + Summary string `mapstructure:"summary" json:"summary" toml:"summary"` + // Files are the package files with their paths and hash digests + Files []PythonFileRecord `mapstructure:"files" json:"files" toml:"files"` + // Dependencies are the dependency specifications, without environment qualifiers + Dependencies []string `mapstructure:"dependencies" json:"dependencies" toml:"dependencies"` +} + // PythonPipfileLockEntry represents a single package entry within a Pipfile.lock file. type PythonPipfileLockEntry struct { // Hashes are the package file hash values in the format "algorithm:digest" for integrity verification. From c0f32e1dbad3760f29aefab026513917833bbcc9 Mon Sep 17 00:00:00 2001 From: Tim Olshansky <456103+timols@users.noreply.github.com> Date: Thu, 16 Oct 2025 09:23:06 -0700 Subject: [PATCH 16/38] feat: add option to fetch remote licenses for pnpm-lock.yaml files (#4286) Signed-off-by: Tim Olshansky <456103+timols@users.noreply.github.com> --- syft/pkg/cataloger/javascript/cataloger.go | 3 +- syft/pkg/cataloger/javascript/package.go | 19 +++- .../cataloger/javascript/parse_pnpm_lock.go | 17 ++- .../javascript/parse_pnpm_lock_test.go | 104 ++++++++++++++++- .../javascript/parse_yarn_lock_test.go | 10 +- .../test-fixtures/pnpm-remote/pnpm-lock.yaml | 11 ++ .../pnpm-remote/registry_response.json | 106 ++++++++++++++++++ 7 files changed, 251 insertions(+), 19 deletions(-) create mode 100644 syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/pnpm-lock.yaml create mode 100644 syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/registry_response.json diff --git a/syft/pkg/cataloger/javascript/cataloger.go b/syft/pkg/cataloger/javascript/cataloger.go index 649800c6c..7806829ec 100644 --- a/syft/pkg/cataloger/javascript/cataloger.go +++ b/syft/pkg/cataloger/javascript/cataloger.go @@ -18,8 +18,9 @@ func NewPackageCataloger() pkg.Cataloger { func NewLockCataloger(cfg CatalogerConfig) pkg.Cataloger { yarnLockAdapter := newGenericYarnLockAdapter(cfg) packageLockAdapter := newGenericPackageLockAdapter(cfg) + pnpmLockAdapter := newGenericPnpmLockAdapter(cfg) return generic.NewCataloger("javascript-lock-cataloger"). WithParserByGlobs(packageLockAdapter.parsePackageLock, "**/package-lock.json"). WithParserByGlobs(yarnLockAdapter.parseYarnLock, "**/yarn.lock"). - WithParserByGlobs(parsePnpmLock, "**/pnpm-lock.yaml") + WithParserByGlobs(pnpmLockAdapter.parsePnpmLock, "**/pnpm-lock.yaml") } diff --git a/syft/pkg/cataloger/javascript/package.go b/syft/pkg/cataloger/javascript/package.go index f74a5da9a..ca0063b65 100644 --- a/syft/pkg/cataloger/javascript/package.go +++ b/syft/pkg/cataloger/javascript/package.go @@ -107,7 +107,7 @@ func newPackageLockV1Package(ctx context.Context, cfg CatalogerConfig, resolver licenseSet = pkg.NewLicenseSet(licenses...) } if err != nil { - log.Debugf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, version, err) + log.Debugf("unable to extract licenses from javascript package-lock.json for package %s:%s: %+v", name, version, err) } } @@ -140,7 +140,7 @@ func newPackageLockV2Package(ctx context.Context, cfg CatalogerConfig, resolver licenseSet = pkg.NewLicenseSet(licenses...) } if err != nil { - log.Debugf("unable to extract licenses from javascript yarn.lock for package %s:%s: %+v", name, u.Version, err) + log.Debugf("unable to extract licenses from javascript package-lock.json for package %s:%s: %+v", name, u.Version, err) } } @@ -161,7 +161,19 @@ func newPackageLockV2Package(ctx context.Context, cfg CatalogerConfig, resolver ) } -func newPnpmPackage(ctx context.Context, resolver file.Resolver, location file.Location, name, version string) pkg.Package { +func newPnpmPackage(ctx context.Context, cfg CatalogerConfig, resolver file.Resolver, location file.Location, name, version string) pkg.Package { + var licenseSet pkg.LicenseSet + + if cfg.SearchRemoteLicenses { + license, err := getLicenseFromNpmRegistry(cfg.NPMBaseURL, name, version) + if err == nil && license != "" { + licenses := pkg.NewLicensesFromValuesWithContext(ctx, license) + licenseSet = pkg.NewLicenseSet(licenses...) + } + if err != nil { + log.Debugf("unable to extract licenses from javascript pnpm-lock.yaml for package %s:%s: %+v", name, version, err) + } + } return finalizeLockPkg( ctx, resolver, @@ -169,6 +181,7 @@ func newPnpmPackage(ctx context.Context, resolver file.Resolver, location file.L pkg.Package{ Name: name, Version: version, + Licenses: licenseSet, Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)), PURL: packageURL(name, version), Language: pkg.JavaScript, diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go index 706bcdfe9..536c8e3f2 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock.go @@ -18,9 +18,6 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/generic" ) -// integrity check -var _ generic.Parser = parsePnpmLock - // pnpmPackage holds the raw name and version extracted from the lockfile. type pnpmPackage struct { Name string @@ -45,6 +42,16 @@ type pnpmV9LockYaml struct { Packages map[string]interface{} `yaml:"packages"` } +type genericPnpmLockAdapter struct { + cfg CatalogerConfig +} + +func newGenericPnpmLockAdapter(cfg CatalogerConfig) genericPnpmLockAdapter { + return genericPnpmLockAdapter{ + cfg: cfg, + } +} + // Parse implements the pnpmLockfileParser interface for v6-v8 lockfiles. func (p *pnpmV6LockYaml) Parse(version float64, data []byte) ([]pnpmPackage, error) { if err := yaml.Unmarshal(data, p); err != nil { @@ -116,7 +123,7 @@ func newPnpmLockfileParser(version float64) pnpmLockfileParser { } // parsePnpmLock is the main parser function for pnpm-lock.yaml files. -func parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +func (a genericPnpmLockAdapter) parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { data, err := io.ReadAll(reader) if err != nil { return nil, nil, fmt.Errorf("failed to load pnpm-lock.yaml file: %w", err) @@ -142,7 +149,7 @@ func parsePnpmLock(ctx context.Context, resolver file.Resolver, _ *generic.Envir packages := make([]pkg.Package, len(pnpmPkgs)) for i, p := range pnpmPkgs { - packages[i] = newPnpmPackage(ctx, resolver, reader.Location, p.Name, p.Version) + packages[i] = newPnpmPackage(ctx, a.cfg, resolver, reader.Location, p.Name, p.Version) } return packages, nil, unknown.IfEmptyf(packages, "unable to determine packages") diff --git a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go index 7ba17a546..d62b95eb8 100644 --- a/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_pnpm_lock_test.go @@ -1,6 +1,11 @@ package javascript import ( + "context" + "io" + "net/http" + "net/http/httptest" + "os" "testing" "github.com/anchore/syft/syft/artifact" @@ -50,7 +55,8 @@ func TestParsePnpmLock(t *testing.T) { }, } - pkgtest.TestFileParser(t, fixture, parsePnpmLock, expectedPkgs, expectedRelationships) + adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) + pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) } func TestParsePnpmV6Lock(t *testing.T) { @@ -142,7 +148,8 @@ func TestParsePnpmV6Lock(t *testing.T) { }, } - pkgtest.TestFileParser(t, fixture, parsePnpmLock, expectedPkgs, expectedRelationships) + adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) + pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expectedPkgs, expectedRelationships) } func TestParsePnpmLockV9(t *testing.T) { @@ -184,14 +191,101 @@ func TestParsePnpmLockV9(t *testing.T) { Type: pkg.NpmPkg, }, } - + adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) // TODO: no relationships are under test - pkgtest.TestFileParser(t, fixture, parsePnpmLock, expected, expectedRelationships) + pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, expected, expectedRelationships) } +func TestSearchPnpmForLicenses(t *testing.T) { + ctx := context.TODO() + fixture := "test-fixtures/pnpm-remote/pnpm-lock.yaml" + locations := file.NewLocationSet(file.NewLocation(fixture)) + mux, url, teardown := setupNpmRegistry() + defer teardown() + tests := []struct { + name string + fixture string + config CatalogerConfig + requestHandlers []handlerPath + expectedPackages []pkg.Package + }{ + { + name: "search remote licenses returns the expected licenses when search is set to true", + config: CatalogerConfig{SearchRemoteLicenses: true}, + requestHandlers: []handlerPath{ + { + // https://registry.npmjs.org/nanoid/3.3.4 + path: "/nanoid/3.3.4", + handler: generateMockNpmRegistryHandler("test-fixtures/pnpm-remote/registry_response.json"), + }, + }, + expectedPackages: []pkg.Package{ + { + Name: "nanoid", + Version: "3.3.4", + Locations: locations, + PURL: "pkg:npm/nanoid@3.3.4", + Licenses: pkg.NewLicenseSet(pkg.NewLicenseWithContext(ctx, "MIT")), + Language: pkg.JavaScript, + Type: pkg.NpmPkg, + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // set up the mock server + for _, handler := range tc.requestHandlers { + mux.HandleFunc(handler.path, handler.handler) + } + tc.config.NPMBaseURL = url + adapter := newGenericPnpmLockAdapter(tc.config) + pkgtest.TestFileParser(t, fixture, adapter.parsePnpmLock, tc.expectedPackages, nil) + }) + } +} func Test_corruptPnpmLock(t *testing.T) { + adapter := newGenericPnpmLockAdapter(CatalogerConfig{}) pkgtest.NewCatalogTester(). FromFile(t, "test-fixtures/corrupt/pnpm-lock.yaml"). WithError(). - TestParser(t, parsePnpmLock) + TestParser(t, adapter.parsePnpmLock) +} + +func generateMockNpmRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + // Copy the file's content to the response writer + file, err := os.Open(responseFixture) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer file.Close() + + _, err = io.Copy(w, file) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } +} + +// setup sets up a test HTTP server for mocking requests to a particular registry. +// The returned url is injected into the Config so the client uses the test server. +// Tests should register handlers on mux to simulate the expected request/response structure +func setupNpmRegistry() (mux *http.ServeMux, serverURL string, teardown func()) { + // mux is the HTTP request multiplexer used with the test server. + mux = http.NewServeMux() + + // We want to ensure that tests catch mistakes where the endpoint URL is + // specified as absolute rather than relative. It only makes a difference + // when there's a non-empty base URL path. So, use that. See issue #752. + apiHandler := http.NewServeMux() + apiHandler.Handle("/", mux) + // server is a test HTTP server used to provide mock API responses. + server := httptest.NewServer(apiHandler) + + return mux, server.URL, server.Close } diff --git a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go index f85a2cf3a..f6719db4a 100644 --- a/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go +++ b/syft/pkg/cataloger/javascript/parse_yarn_lock_test.go @@ -239,7 +239,7 @@ func TestSearchYarnForLicenses(t *testing.T) { ctx := context.TODO() fixture := "test-fixtures/yarn-remote/yarn.lock" locations := file.NewLocationSet(file.NewLocation(fixture)) - mux, url, teardown := setup() + mux, url, teardown := setupYarnRegistry() defer teardown() tests := []struct { name string @@ -255,7 +255,7 @@ func TestSearchYarnForLicenses(t *testing.T) { { // https://registry.yarnpkg.com/@babel/code-frame/7.10.4 path: "/@babel/code-frame/7.10.4", - handler: generateMockNPMHandler("test-fixtures/yarn-remote/registry_response.json"), + handler: generateMockYarnRegistryHandler("test-fixtures/yarn-remote/registry_response.json"), }, }, expectedPackages: []pkg.Package{ @@ -445,7 +445,7 @@ func TestParseYarnFindPackageVersions(t *testing.T) { } } -func generateMockNPMHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) { +func generateMockYarnRegistryHandler(responseFixture string) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) // Copy the file's content to the response writer @@ -464,10 +464,10 @@ func generateMockNPMHandler(responseFixture string) func(w http.ResponseWriter, } } -// setup sets up a test HTTP server for mocking requests to maven central. +// setup sets up a test HTTP server for mocking requests to a particular registry. // The returned url is injected into the Config so the client uses the test server. // Tests should register handlers on mux to simulate the expected request/response structure -func setup() (mux *http.ServeMux, serverURL string, teardown func()) { +func setupYarnRegistry() (mux *http.ServeMux, serverURL string, teardown func()) { // mux is the HTTP request multiplexer used with the test server. mux = http.NewServeMux() diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/pnpm-lock.yaml b/syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/pnpm-lock.yaml new file mode 100644 index 000000000..27cc4dcf9 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/pnpm-lock.yaml @@ -0,0 +1,11 @@ +lockfileVersion: 5.4 + +specifiers: + nanoid: ^3.3.4 + +dependencies: + nanoid: 3.3.4 + +packages: + /nanoid/3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} diff --git a/syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/registry_response.json b/syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/registry_response.json new file mode 100644 index 000000000..d67a6bb76 --- /dev/null +++ b/syft/pkg/cataloger/javascript/test-fixtures/pnpm-remote/registry_response.json @@ -0,0 +1,106 @@ +{ + "name": "nanoid", + "version": "3.3.4", + "keywords": [ + "uuid", + "random", + "id", + "url" + ], + "author": { + "name": "Andrey Sitnik", + "email": "andrey@sitnik.ru" + }, + "license": "MIT", + "_id": "nanoid@3.3.4", + "maintainers": [ + { + "name": "ai", + "email": "andrey@sitnik.ru" + } + ], + "homepage": "https://github.com/ai/nanoid#readme", + "bugs": { + "url": "https://github.com/ai/nanoid/issues" + }, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "dist": { + "shasum": "730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab", + "tarball": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "fileCount": 24, + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "signatures": [ + { + "sig": "MEQCIEXG2ta5bIaT6snvQFKV+m1KjuF4DaCpp186tcPo8vsRAiB2Eg9/6nKRi4lZOfwQC1fgq4EzrFjU8T+uqwGxWEQE8A==", + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA" + } + ], + "unpackedSize": 21583, + "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v4.10.10\r\nComment: https://openpgpjs.org\r\n\r\nwsFzBAEBCAAGBQJicQqNACEJED1NWxICdlZqFiEECWMYAoorWMhJKdjhPU1b\r\nEgJ2Vmp6rw/+IRvv2zOtwi8goF3h1VctIQVWtTtYrobDIVC2W++jyxdbgZoP\r\n2CDj1YWjrr+eM6O6sI1Bj+bF+yoqQ+z8ojtfW3vtRPpjzUf/7Sgs4F2ANshp\r\ne3rqdaQLjpHPriHf6HmPJy3YNJ+7n5TPPGoTEGXAe4eCZdko3XidCMWZdHlf\r\nYQU9CVYiG6mjjORkWw1sYctt8exdcGFMh0QoQq7BEp04QWm04JwvHjUiAgvf\r\nmEQLrNrf9nwzjpnubAJD+1z6fKOc9vUE44MOj2PkPoOr6a+iBBBgwBf45cnj\r\ng8R2G5xzxsRRB0a8XZdp67y3WA8rIaYaUuBFtEWYp7QFoA/tp6AGmHEAhjLa\r\nQKTquG7ejBu21ZsQaxpGc/3WWLEm+7F78GF8CXpQdtg0Kg1eugRotSNnU0SO\r\nPLiyYV4Mw6kXnbVchS5Y+HmcDVEcSBMTve/f1KpmIhJueJ20RCg4MGYZWgI9\r\nNJ1KgH2h4djX4XuoXpcsKnX3oVfinHEMke8sLWXHsMAtOxDipEWgW9cE9hk0\r\n71Y6LAAPBu34pmaj73B0qZiIY7wXxoGWQOCl2STS/VyDG/K9w1T+WiYROu+8\r\nE9Gd+f4qXmdi7Jw6May86DDfauCwBP3gnrB5aeOktCjWsgrrdClN3Hv2pIAN\r\noJcjS3IURf6oeV4+Yw1B5GoJu1Y/6U75fOU=\r\n=IMnM\r\n-----END PGP SIGNATURE-----\r\n" + }, + "main": "index.cjs", + "type": "module", + "types": "./index.d.ts", + "module": "index.js", + "browser": { + "./index.js": "./index.browser.js", + "./index.cjs": "./index.browser.cjs", + "./async/index.js": "./async/index.browser.js", + "./async/index.cjs": "./async/index.browser.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js", + "browser": "./index.browser.js", + "default": "./index.js", + "require": "./index.cjs" + }, + "./async": { + "import": "./async/index.js", + "browser": "./async/index.browser.js", + "default": "./async/index.js", + "require": "./async/index.cjs" + }, + "./index.d.ts": "./index.d.ts", + "./non-secure": { + "import": "./non-secure/index.js", + "default": "./non-secure/index.js", + "require": "./non-secure/index.cjs" + }, + "./package.json": "./package.json", + "./url-alphabet": { + "import": "./url-alphabet/index.js", + "default": "./url-alphabet/index.js", + "require": "./url-alphabet/index.cjs" + }, + "./async/package.json": "./async/package.json", + "./non-secure/package.json": "./non-secure/package.json", + "./url-alphabet/package.json": "./url-alphabet/package.json" + }, + "gitHead": "fc5bd0dbba830b1e6f3e572da8e2bc9ddc1b4b44", + "_npmUser": { + "name": "ai", + "email": "andrey@sitnik.ru" + }, + "repository": { + "url": "git+https://github.com/ai/nanoid.git", + "type": "git" + }, + "_npmVersion": "8.6.0", + "description": "A tiny (116 bytes), secure URL-friendly unique string ID generator", + "directories": {}, + "sideEffects": false, + "_nodeVersion": "18.0.0", + "react-native": "index.js", + "_hasShrinkwrap": false, + "_npmOperationalInternal": { + "tmp": "tmp/nanoid_3.3.4_1651575437375_0.2288595018362154", + "host": "s3://npm-registry-packages" + } +} \ No newline at end of file From 6627c5214c1824d37a5df28b8c0bbcd2bcfd644b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:57:17 -0400 Subject: [PATCH 17/38] chore(deps): bump anchore/sbom-action from 0.20.6 to 0.20.7 (#4293) Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.20.6 to 0.20.7. - [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/f8bdd1d8ac5e901a77a92f111440fdb1b593736b...d8a2c0130026bf585de5c176ab8f7ce62d75bf04) --- updated-dependencies: - dependency-name: anchore/sbom-action dependency-version: 0.20.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b6f42fbd5..8fe8876f3 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -161,7 +161,7 @@ jobs: # for updating brew formula in anchore/homebrew-syft GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }} - - uses: anchore/sbom-action@f8bdd1d8ac5e901a77a92f111440fdb1b593736b #v0.20.6 + - uses: anchore/sbom-action@d8a2c0130026bf585de5c176ab8f7ce62d75bf04 #v0.20.7 continue-on-error: true with: file: go.mod From fc74b073699e3c1d3fc441cba346bc152b5c317a Mon Sep 17 00:00:00 2001 From: Kudryavcev Nikolay <35200428+Rupikz@users.noreply.github.com> Date: Thu, 16 Oct 2025 23:19:11 +0300 Subject: [PATCH 18/38] Remove duplicate image source providers (#4289) Signed-off-by: Kudryavcev Nikolay --- go.mod | 2 +- go.sum | 4 ++-- syft/get_source_config_test.go | 37 ++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 syft/get_source_config_test.go diff --git a/go.mod b/go.mod index cd7f8feee..b4dda9cc4 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9 github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716 github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2 - github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 + github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c 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 diff --git a/go.sum b/go.sum index 82fb633fe..0b9418596 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716 h1:2sIdYJlQESEnyk3Y0W github.com/anchore/clio v0.0.0-20250319180342-2cfe4b0cb716/go.mod h1:Utb9i4kwiCWvqAIxZaJeMIXFO9uOgQXlvH2BfbfO/zI= github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2 h1:GC2QaO0YsmjpsZ4rtVKv9DnproIxqqn+qkskpc+i8MA= github.com/anchore/fangs v0.0.0-20250319222917-446a1e748ec2/go.mod h1:XUbUECwVKuD3qYRUj+QZIOHjyyXua2gFmVjKA40iHXA= -github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 h1:GjNGuwK5jWjJMyVppBjYS54eOiiSNv4Ba869k4wh72Q= -github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= +github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c h1:eoJXyC0n7DZ4YvySG/ETdYkTar2Due7eH+UmLK6FbrA= +github.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= diff --git a/syft/get_source_config_test.go b/syft/get_source_config_test.go new file mode 100644 index 000000000..9d0ad8774 --- /dev/null +++ b/syft/get_source_config_test.go @@ -0,0 +1,37 @@ +package syft + +import ( + "testing" + + "github.com/anchore/stereoscope" + "github.com/anchore/syft/syft/source/sourceproviders" +) + +func TestGetProviders_DefaultImagePullSource(t *testing.T) { + userInput := "" + cfg := &GetSourceConfig{DefaultImagePullSource: stereoscope.RegistryTag} + allSourceProviders := sourceproviders.All(userInput, cfg.SourceProviderConfig) + + providers, err := cfg.getProviders(userInput) + if err != nil { + t.Errorf("Expected no error for DefaultImagePullSource parameter, got: %v", err) + } + + if len(providers) != len(allSourceProviders) { + t.Errorf("Expected %d providers, got %d", len(allSourceProviders), len(providers)) + } +} + +func TestGetProviders_Sources(t *testing.T) { + userInput := "" + cfg := &GetSourceConfig{Sources: []string{stereoscope.RegistryTag}} + + providers, err := cfg.getProviders(userInput) + if err != nil { + t.Errorf("Expected no error for Sources parameter, got: %v", err) + } + + if len(providers) != 1 { + t.Errorf("Expected 1 providers, got %d", len(providers)) + } +} From 538b4a21946ba685a051aed51f942c1769e3b960 Mon Sep 17 00:00:00 2001 From: JoeyShapiro <57827758+JoeyShapiro@users.noreply.github.com> Date: Fri, 17 Oct 2025 08:29:06 -0500 Subject: [PATCH 19/38] convert posix path back to windows (#4285) Signed-off-by: Joseph Shapiro --- .../fileresolver/directory_indexer.go | 6 +-- syft/internal/fileresolver/file_indexer.go | 4 +- syft/internal/fileresolver/get_xid.go | 20 ++++++++ syft/internal/fileresolver/get_xid_win.go | 12 +++++ syft/internal/fileresolver/metadata.go | 44 ++++++++++++++++ syft/internal/fileresolver/metadata_test.go | 50 +++++++++++++++++++ 6 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 syft/internal/fileresolver/get_xid.go create mode 100644 syft/internal/fileresolver/get_xid_win.go create mode 100644 syft/internal/fileresolver/metadata.go create mode 100644 syft/internal/fileresolver/metadata_test.go diff --git a/syft/internal/fileresolver/directory_indexer.go b/syft/internal/fileresolver/directory_indexer.go index 1facdc44b..bc3d8a5f2 100644 --- a/syft/internal/fileresolver/directory_indexer.go +++ b/syft/internal/fileresolver/directory_indexer.go @@ -322,7 +322,7 @@ func (r directoryIndexer) addDirectoryToIndex(p string, info os.FileInfo) error return err } - metadata := file.NewMetadataFromPath(p, info) + metadata := NewMetadataFromPath(p, info) r.index.Add(*ref, metadata) return nil @@ -334,7 +334,7 @@ func (r directoryIndexer) addFileToIndex(p string, info os.FileInfo) error { return err } - metadata := file.NewMetadataFromPath(p, info) + metadata := NewMetadataFromPath(p, info) r.index.Add(*ref, metadata) return nil @@ -416,7 +416,7 @@ func (r directoryIndexer) addSymlinkToIndex(p string, info os.FileInfo) (string, targetAbsPath = filepath.Clean(filepath.Join(path.Dir(p), linkTarget)) } - metadata := file.NewMetadataFromPath(p, info) + metadata := NewMetadataFromPath(p, info) metadata.LinkDestination = linkTarget r.index.Add(*ref, metadata) diff --git a/syft/internal/fileresolver/file_indexer.go b/syft/internal/fileresolver/file_indexer.go index cf257dc95..086dc75a9 100644 --- a/syft/internal/fileresolver/file_indexer.go +++ b/syft/internal/fileresolver/file_indexer.go @@ -173,7 +173,7 @@ func (r *fileIndexer) addDirectoryToIndex(path string, info os.FileInfo) error { return err } - metadata := file.NewMetadataFromPath(path, info) + metadata := NewMetadataFromPath(path, info) r.index.Add(*ref, metadata) return nil @@ -185,7 +185,7 @@ func (r *fileIndexer) addFileToIndex(path string, info os.FileInfo) error { return err } - metadata := file.NewMetadataFromPath(path, info) + metadata := NewMetadataFromPath(path, info) r.index.Add(*ref, metadata) return nil diff --git a/syft/internal/fileresolver/get_xid.go b/syft/internal/fileresolver/get_xid.go new file mode 100644 index 000000000..8e00bc7a0 --- /dev/null +++ b/syft/internal/fileresolver/get_xid.go @@ -0,0 +1,20 @@ +//go:build !windows + +package fileresolver + +import ( + "os" + "syscall" +) + +// getXid is the UID GID system info for unix +func getXid(info os.FileInfo) (uid, gid int) { + uid = -1 + gid = -1 + if stat, ok := info.Sys().(*syscall.Stat_t); ok { + uid = int(stat.Uid) + gid = int(stat.Gid) + } + + return uid, gid +} diff --git a/syft/internal/fileresolver/get_xid_win.go b/syft/internal/fileresolver/get_xid_win.go new file mode 100644 index 000000000..c5202e92b --- /dev/null +++ b/syft/internal/fileresolver/get_xid_win.go @@ -0,0 +1,12 @@ +//go:build windows + +package fileresolver + +import ( + "os" +) + +// getXid is a placeholder for windows file information +func getXid(info os.FileInfo) (uid, gid int) { + return -1, -1 +} diff --git a/syft/internal/fileresolver/metadata.go b/syft/internal/fileresolver/metadata.go new file mode 100644 index 000000000..af15c8733 --- /dev/null +++ b/syft/internal/fileresolver/metadata.go @@ -0,0 +1,44 @@ +package fileresolver + +import ( + "os" + + "github.com/anchore/stereoscope/pkg/file" + "github.com/anchore/syft/internal" + "github.com/anchore/syft/syft/internal/windows" +) + +func NewMetadataFromPath(path string, info os.FileInfo) file.Metadata { + var mimeType string + uid, gid := getXid(info) + + ty := file.TypeFromMode(info.Mode()) + + if ty == file.TypeRegular { + usablePath := path + // denormalize the path back to windows so we can open the file + if windows.HostRunningOnWindows() { + usablePath = windows.FromPosix(usablePath) + } + + f, err := os.Open(usablePath) + if err != nil { + // TODO: it may be that the file is inaccessible, however, this is not an error or a warning. In the future we need to track these as known-unknowns + f = nil + } else { + defer internal.CloseAndLogError(f, usablePath) + } + + mimeType = file.MIMEType(f) + } + + return file.Metadata{ + FileInfo: info, + Path: path, + Type: ty, + // unsupported across platforms + UserID: uid, + GroupID: gid, + MIMEType: mimeType, + } +} diff --git a/syft/internal/fileresolver/metadata_test.go b/syft/internal/fileresolver/metadata_test.go new file mode 100644 index 000000000..936cd227c --- /dev/null +++ b/syft/internal/fileresolver/metadata_test.go @@ -0,0 +1,50 @@ +package fileresolver + +import ( + "os" + "testing" + + "github.com/anchore/stereoscope/pkg/file" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFileMetadataFromPath(t *testing.T) { + + tests := []struct { + path string + expectedType file.Type + expectedMIMEType string + }{ + { + path: "test-fixtures/symlinks-simple/readme", + expectedType: file.TypeRegular, + expectedMIMEType: "text/plain", + }, + { + path: "test-fixtures/symlinks-simple/link_to_new_readme", + expectedType: file.TypeSymLink, + expectedMIMEType: "", + }, + { + path: "test-fixtures/symlinks-simple/link_to_link_to_new_readme", + expectedType: file.TypeSymLink, + expectedMIMEType: "", + }, + { + path: "test-fixtures/symlinks-simple", + expectedType: file.TypeDirectory, + expectedMIMEType: "", + }, + } + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + info, err := os.Lstat(test.path) + require.NoError(t, err) + + actual := NewMetadataFromPath(test.path, info) + assert.Equal(t, test.expectedMIMEType, actual.MIMEType, "unexpected MIME type for %s", test.path) + assert.Equal(t, test.expectedType, actual.Type, "unexpected type for %s", test.path) + }) + } +} From f4de1e863cc975d6e5d7338bd6c7e6332176dce9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:22:10 -0400 Subject: [PATCH 20/38] chore(deps): bump anchore/sbom-action from 0.20.7 to 0.20.8 (#4297) Bumps [anchore/sbom-action](https://github.com/anchore/sbom-action) from 0.20.7 to 0.20.8. - [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/d8a2c0130026bf585de5c176ab8f7ce62d75bf04...aa0e114b2e19480f157109b9922bda359bd98b90) --- updated-dependencies: - dependency-name: anchore/sbom-action dependency-version: 0.20.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 8fe8876f3..d0451d090 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -161,7 +161,7 @@ jobs: # for updating brew formula in anchore/homebrew-syft GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }} - - uses: anchore/sbom-action@d8a2c0130026bf585de5c176ab8f7ce62d75bf04 #v0.20.7 + - uses: anchore/sbom-action@aa0e114b2e19480f157109b9922bda359bd98b90 #v0.20.8 continue-on-error: true with: file: go.mod From 07029ead8a2c6ba727757e494b516ea740b210e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 10:22:20 -0400 Subject: [PATCH 21/38] chore(deps): bump sigstore/cosign-installer from 3.10.0 to 4.0.0 (#4296) Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.10.0 to 4.0.0. - [Release notes](https://github.com/sigstore/cosign-installer/releases) - [Commits](https://github.com/sigstore/cosign-installer/compare/d7543c93d881b35a8faa02e8e3605f69b7a1ce62...faadad0cce49287aee09b3a48701e75088a2c6ad) --- updated-dependencies: - dependency-name: sigstore/cosign-installer dependency-version: 4.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/validations.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index 296c7e6bd..d070dc6a2 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -210,7 +210,7 @@ jobs: runs-on: macos-latest steps: - name: Install Cosign - uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 #v5.0.0 with: From 31b2c4c0907f281b51148d7359dd48796af5120a Mon Sep 17 00:00:00 2001 From: JoeyShapiro <57827758+JoeyShapiro@users.noreply.github.com> Date: Fri, 17 Oct 2025 12:41:59 -0500 Subject: [PATCH 22/38] support universal (fat) mach-o binary files (#4278) Signed-off-by: Joseph Shapiro --- syft/file/cataloger/executable/macho.go | 35 ++++++++++++++---- syft/file/cataloger/executable/macho_test.go | 37 +++++++++++++++++++ .../shared-info/project/hello/Makefile | 9 +++-- .../shared-info/project/libhello/Makefile | 9 +++-- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/syft/file/cataloger/executable/macho.go b/syft/file/cataloger/executable/macho.go index 8493c04a0..7cd80f7e5 100644 --- a/syft/file/cataloger/executable/macho.go +++ b/syft/file/cataloger/executable/macho.go @@ -3,6 +3,7 @@ package executable import ( "debug/macho" + "github.com/anchore/syft/internal" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/unionreader" ) @@ -19,20 +20,38 @@ const ( func findMachoFeatures(data *file.Executable, reader unionreader.UnionReader) error { // TODO: support security features - // TODO: support multi-architecture binaries - f, err := macho.NewFile(reader) + // a universal binary may have multiple architectures, so we need to check each one + readers, err := unionreader.GetReaders(reader) if err != nil { return err } - libs, err := f.ImportedLibraries() - if err != nil { - return err + var libs []string + for _, r := range readers { + f, err := macho.NewFile(r) + if err != nil { + return err + } + + rLibs, err := f.ImportedLibraries() + if err != nil { + return err + } + libs = append(libs, rLibs...) + + // TODO handle only some having entrypoints/exports? If that is even practical + // only check for entrypoint if we don't already have one + if !data.HasEntrypoint { + data.HasEntrypoint = machoHasEntrypoint(f) + } + // only check for exports if we don't already have them + if !data.HasExports { + data.HasExports = machoHasExports(f) + } } - data.ImportedLibraries = libs - data.HasEntrypoint = machoHasEntrypoint(f) - data.HasExports = machoHasExports(f) + // de-duplicate libraries + data.ImportedLibraries = internal.NewSet(libs...).ToSlice() return nil } diff --git a/syft/file/cataloger/executable/macho_test.go b/syft/file/cataloger/executable/macho_test.go index 1d4e7ab7a..ed8816671 100644 --- a/syft/file/cataloger/executable/macho_test.go +++ b/syft/file/cataloger/executable/macho_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/internal/unionreader" ) @@ -83,3 +84,39 @@ func Test_machoHasExports(t *testing.T) { }) } } + +func Test_machoUniversal(t *testing.T) { + readerForFixture := func(t *testing.T, fixture string) unionreader.UnionReader { + t.Helper() + f, err := os.Open(filepath.Join("test-fixtures/shared-info", fixture)) + require.NoError(t, err) + return f + } + + tests := []struct { + name string + fixture string + want file.Executable + }{ + { + name: "universal lib", + fixture: "bin/libhello_universal.dylib", + want: file.Executable{HasExports: true, HasEntrypoint: false}, + }, + { + name: "universal application", + fixture: "bin/hello_mac_universal", + want: file.Executable{HasExports: false, HasEntrypoint: true}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var data file.Executable + err := findMachoFeatures(&data, readerForFixture(t, tt.fixture)) + require.NoError(t, err) + + assert.Equal(t, tt.want.HasEntrypoint, data.HasEntrypoint) + assert.Equal(t, tt.want.HasExports, data.HasExports) + }) + } +} diff --git a/syft/file/cataloger/executable/test-fixtures/shared-info/project/hello/Makefile b/syft/file/cataloger/executable/test-fixtures/shared-info/project/hello/Makefile index 6a0bbbf4e..578d7a95f 100644 --- a/syft/file/cataloger/executable/test-fixtures/shared-info/project/hello/Makefile +++ b/syft/file/cataloger/executable/test-fixtures/shared-info/project/hello/Makefile @@ -2,13 +2,13 @@ BIN=../../bin -all: $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac +all: $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac $(BIN)/hello_mac_universal linux: $(BIN)/libhello.so windows: $(BIN)/libhello.dll -mac: $(BIN)/libhello.dylib +mac: $(BIN)/libhello.dylib $(BIN)/hello_mac_universal $(BIN)/hello_linux: gcc hello.c -o $(BIN)/hello_linux @@ -19,5 +19,8 @@ $(BIN)/hello.exe: $(BIN)/hello_mac: o64-clang hello.c -o $(BIN)/hello_mac +$(BIN)/hello_mac_universal: + o64-clang -arch arm64 -arch x86_64 hello.c -o $(BIN)/hello_mac_universal + clean: - rm -f $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac + rm -f $(BIN)/hello_linux $(BIN)/hello.exe $(BIN)/hello_mac $(BIN)/hello_mac_universal diff --git a/syft/file/cataloger/executable/test-fixtures/shared-info/project/libhello/Makefile b/syft/file/cataloger/executable/test-fixtures/shared-info/project/libhello/Makefile index e9b3860bb..af1fb12d9 100644 --- a/syft/file/cataloger/executable/test-fixtures/shared-info/project/libhello/Makefile +++ b/syft/file/cataloger/executable/test-fixtures/shared-info/project/libhello/Makefile @@ -2,13 +2,13 @@ BIN=../../bin -all: $(BIN)/libhello.so $(BIN)/libhello.dll $(BIN)/libhello.dylib +all: $(BIN)/libhello.so $(BIN)/libhello.dll $(BIN)/libhello.dylib $(BIN)/libhello_universal.dylib linux: $(BIN)/libhello.so windows: $(BIN)/libhello.dll -mac: $(BIN)/libhello.dylib +mac: $(BIN)/libhello.dylib $(BIN)/libhello_universal.dylib $(BIN)/libhello.so: gcc -shared -fPIC -o $(BIN)/libhello.so hello.c @@ -19,5 +19,8 @@ $(BIN)/libhello.dll: $(BIN)/libhello.dylib: o64-clang -dynamiclib -o $(BIN)/libhello.dylib hello.c +$(BIN)/libhello_universal.dylib: + o64-clang -dynamiclib -arch arm64 -arch x86_64 hello.c -o $(BIN)/libhello_universal.dylib + clean: - rm -f $(BIN)/libhello.so $(BIN)/hello.dll $(BIN)/libhello.dylib $(BIN)/libhello.a + rm -f $(BIN)/libhello.so $(BIN)/hello.dll $(BIN)/libhello.dylib $(BIN)/libhello.a $(BIN)/libhello_universal.dylib From 675075e8828d04090ec6bf4a566542bf1dec019f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 10:08:39 -0400 Subject: [PATCH 23/38] chore(deps): bump github/codeql-action from 4.30.8 to 4.30.9 (#4299) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.8 to 4.30.9. - [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/f443b600d91635bebf5b0d9ebc620189c0d6fba5...16140ae1a102900babc80a33c44059580f687047) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.30.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d28a63841..a370ff8f3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5 + uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,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@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5 + uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 #v3.29.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 #v3.29.5 + uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v3.29.5 From 44b7b0947c88244b8ce1eb349ab9fc858bb58eaf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 09:34:26 -0400 Subject: [PATCH 24/38] chore(deps): bump github.com/github/go-spdx/v2 from 2.3.3 to 2.3.4 (#4301) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b4dda9cc4..38037f371 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/elliotchance/phpserialize v1.4.0 github.com/facebookincubator/nvdtools v0.1.5 - github.com/github/go-spdx/v2 v2.3.3 + github.com/github/go-spdx/v2 v2.3.4 github.com/gkampitakis/go-snaps v0.5.15 github.com/go-git/go-billy/v5 v5.6.2 github.com/go-git/go-git/v5 v5.16.3 diff --git a/go.sum b/go.sum index 0b9418596..1be9a16bf 100644 --- a/go.sum +++ b/go.sum @@ -385,8 +385,8 @@ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8 github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/github/go-spdx/v2 v2.3.3 h1:QI7evnHWEfWkT54eJwkoV/f3a0xD3gLlnVmT5wQG6LE= -github.com/github/go-spdx/v2 v2.3.3/go.mod h1:2ZxKsOhvBp+OYBDlsGnUMcchLeo2mrpEBn2L1C+U3IQ= +github.com/github/go-spdx/v2 v2.3.4 h1:6VNAsYWvQge+SOeoubTlH81MY21d5uekXNIRGfXMNXo= +github.com/github/go-spdx/v2 v2.3.4/go.mod h1:7LYNCshU2Gj17qZ0heJ5CQUKWWmpd98K7o93K8fJSMk= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= From 8be463911ce718ff70179ded9a2a4dd37549d374 Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Wed, 22 Oct 2025 09:38:18 -0400 Subject: [PATCH 25/38] chore(deps): update tools to latest versions (#4302) Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: spiffcs <32073428+spiffcs@users.noreply.github.com> --- .binny.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.binny.yaml b/.binny.yaml index f59bd7aca..b2ef8a8c0 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -58,7 +58,7 @@ tools: # used to release all artifacts - name: goreleaser version: - want: v2.12.5 + want: v2.12.6 method: github-release with: repo: goreleaser/goreleaser @@ -98,7 +98,7 @@ tools: # used for triggering a release - name: gh version: - want: v2.82.0 + want: v2.82.1 method: github-release with: repo: cli/cli From d5ca1ad543a929e9046e183192ab91c2e843d281 Mon Sep 17 00:00:00 2001 From: Ross Kirk Date: Thu, 23 Oct 2025 21:23:58 +0100 Subject: [PATCH 26/38] fix: ignore dpkg entries with "deinstall" status (#4231) Signed-off-by: Ross Kirk --- syft/pkg/cataloger/debian/parse_dpkg_db.go | 10 +++++ .../cataloger/debian/parse_dpkg_db_test.go | 31 +++++++++++++++ .../var/lib/dpkg/status.d/deinstall | 38 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 syft/pkg/cataloger/debian/test-fixtures/var/lib/dpkg/status.d/deinstall diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db.go b/syft/pkg/cataloger/debian/parse_dpkg_db.go index 2f020d3f8..27dc0978d 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db.go @@ -24,6 +24,10 @@ import ( "github.com/anchore/syft/syft/pkg/cataloger/generic" ) +const ( + deinstallStatus string = "deinstall" +) + var ( errEndOfPackages = fmt.Errorf("no more packages to read") sourceRegexp = regexp.MustCompile(`(?P\S+)( \((?P.*)\))?`) @@ -112,6 +116,7 @@ type dpkgExtractedMetadata struct { Provides string `mapstructure:"Provides"` Depends string `mapstructure:"Depends"` PreDepends string `mapstructure:"PreDepends"` // note: original doc is Pre-Depends + Status string `mapstructure:"Status"` } // parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader. @@ -134,6 +139,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgDBEntry, error) { return nil, err } + // Skip entries which have been removed but not purged, e.g. "rc" status in dpkg -l + if strings.Contains(raw.Status, deinstallStatus) { + return nil, nil + } + sourceName, sourceVersion := extractSourceVersion(raw.Source) if sourceVersion != "" { raw.SourceVersion = sourceVersion diff --git a/syft/pkg/cataloger/debian/parse_dpkg_db_test.go b/syft/pkg/cataloger/debian/parse_dpkg_db_test.go index 50ab3485b..c0659a0e0 100644 --- a/syft/pkg/cataloger/debian/parse_dpkg_db_test.go +++ b/syft/pkg/cataloger/debian/parse_dpkg_db_test.go @@ -237,6 +237,37 @@ func Test_parseDpkgStatus(t *testing.T) { }, }, }, + { + name: "deinstall status packages are ignored", + fixturePath: "test-fixtures/var/lib/dpkg/status.d/deinstall", + expected: []pkg.DpkgDBEntry{ + { + Package: "linux-image-6.14.0-1012-aws", + Source: "linux-signed-aws-6.14", + Version: "6.14.0-1012.12~24.04.1", + Architecture: "amd64", + InstalledSize: 15221, + Maintainer: "Canonical Kernel Team ", + Description: `Signed kernel image aws + A kernel image for aws. This version of it is signed with + Canonical's signing key.`, + Provides: []string{"fuse-module", + "linux-image", + "spl-dkms", + "spl-modules", + "v4l2loopback-dkms", + "v4l2loopback-modules", + "zfs-dkms", + "zfs-modules"}, + Depends: []string{ + "kmod", + "linux-base (>= 4.5ubuntu1~16.04.1)", + "linux-modules-6.14.0-1012-aws", + }, + Files: []pkg.DpkgFileRecord{}, + }, + }, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/debian/test-fixtures/var/lib/dpkg/status.d/deinstall b/syft/pkg/cataloger/debian/test-fixtures/var/lib/dpkg/status.d/deinstall new file mode 100644 index 000000000..f899e53d4 --- /dev/null +++ b/syft/pkg/cataloger/debian/test-fixtures/var/lib/dpkg/status.d/deinstall @@ -0,0 +1,38 @@ +Package: linux-image-6.14.0-1012-aws +Status: install ok installed +Priority: optional +Section: kernel +Installed-Size: 15221 +Maintainer: Canonical Kernel Team +Architecture: amd64 +Source: linux-signed-aws-6.14 +Version: 6.14.0-1012.12~24.04.1 +Provides: fuse-module, linux-image, spl-dkms, spl-modules, v4l2loopback-dkms, v4l2loopback-modules, zfs-dkms, zfs-modules +Depends: kmod, linux-base (>= 4.5ubuntu1~16.04.1), linux-modules-6.14.0-1012-aws +Recommends: grub-pc | grub-efi-amd64 | grub-efi-ia32 | grub | lilo, initramfs-tools | linux-initramfs-tool +Suggests: bpftool, linux-perf, linux-aws-6.14-doc-6.14.0 | linux-aws-6.14-source-6.14.0, linux-aws-6.14-tools, linux-headers-6.14.0-1012-aws +Conflicts: linux-image-unsigned-6.14.0-1012-aws +Description: Signed kernel image aws + A kernel image for aws. This version of it is signed with + Canonical's signing key. +Built-Using: linux-aws-6.14 (= 6.14.0-1012.12~24.04.1) + +Package: linux-image-6.8.0-1029-aws +Status: deinstall ok config-files +Priority: optional +Section: kernel +Installed-Size: 14591 +Maintainer: Canonical Kernel Team +Architecture: amd64 +Source: linux-signed-aws +Version: 6.8.0-1029.31 +Config-Version: 6.8.0-1029.31 +Provides: fuse-module, linux-image, spl-dkms, spl-modules, v4l2loopback-dkms, v4l2loopback-modules, zfs-dkms, zfs-modules +Depends: kmod, linux-base (>= 4.5ubuntu1~16.04.1), linux-modules-6.8.0-1029-aws +Recommends: grub-pc | grub-efi-amd64 | grub-efi-ia32 | grub | lilo, initramfs-tools | linux-initramfs-tool +Suggests: fdutils, linux-aws-doc-6.8.0 | linux-aws-source-6.8.0, linux-aws-tools, linux-headers-6.8.0-1029-aws +Conflicts: linux-image-unsigned-6.8.0-1029-aws +Description: Signed kernel image aws + A kernel image for aws. This version of it is signed with + Canonical's signing key. +Built-Using: linux-aws (= 6.8.0-1029.31) From 16f851c5d936b40c840542124662b8cd929fe286 Mon Sep 17 00:00:00 2001 From: Marc <152644146+thomassui@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:55:02 +0200 Subject: [PATCH 27/38] feat: include .rar files as Java archives for Java resource adapters (#4137) Signed-off-by: Marc Thomas --- README.md | 2 +- syft/pkg/cataloger/java/archive_filename.go | 2 +- syft/pkg/cataloger/java/archive_filename_test.go | 7 +++++++ syft/pkg/cataloger/java/archive_parser.go | 1 + syft/pkg/cataloger/java/cataloger_test.go | 1 + .../test-fixtures/glob-paths/java-archives/example.rar | 1 + 6 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.rar diff --git a/README.md b/README.md index 12577f10f..26d55bc1f 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Note that flags using the @ can be used for earlier versions of each sp - Go (go.mod, Go binaries) - GitHub (workflows, actions) - Haskell (cabal, stack) -- Java (jar, ear, war, par, sar, nar, native-image) +- Java (jar, ear, war, par, sar, nar, rar, native-image) - JavaScript (npm, yarn) - Jenkins Plugins (jpi, hpi) - Linux kernel archives (vmlinz) diff --git a/syft/pkg/cataloger/java/archive_filename.go b/syft/pkg/cataloger/java/archive_filename.go index b753def1d..1e3efb50d 100644 --- a/syft/pkg/cataloger/java/archive_filename.go +++ b/syft/pkg/cataloger/java/archive_filename.go @@ -108,7 +108,7 @@ func (a archiveFilename) extension() string { func (a archiveFilename) pkgType() pkg.Type { switch strings.ToLower(a.extension()) { - case "jar", "war", "ear", "lpkg", "par", "sar", "nar", "kar": + case "jar", "war", "ear", "lpkg", "par", "sar", "nar", "kar", "rar": return pkg.JavaPkg case "jpi", "hpi": return pkg.JenkinsPluginPkg diff --git a/syft/pkg/cataloger/java/archive_filename_test.go b/syft/pkg/cataloger/java/archive_filename_test.go index 985604912..6c73426eb 100644 --- a/syft/pkg/cataloger/java/archive_filename_test.go +++ b/syft/pkg/cataloger/java/archive_filename_test.go @@ -187,6 +187,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) { name: "gradle-build-cache", ty: pkg.JavaPkg, }, + { + filename: "pkg-extra-field-maven-4.3.2-rc1.rar", + version: "4.3.2-rc1", + extension: "rar", + name: "pkg-extra-field-maven", + ty: pkg.JavaPkg, + }, } for _, test := range tests { diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 477168afc..5e3d188a1 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -45,6 +45,7 @@ var archiveFormatGlobs = []string{ // LifeRay makes it pretty cumbersome to make a such plugins; their docs are // out of date, and they charge for their IDE. If you find an example // project that we can build in CI feel free to include it + "**/*.rar", // Java Resource Adapter Archive } // javaArchiveHashes are all the current hash algorithms used to calculate archive digests diff --git a/syft/pkg/cataloger/java/cataloger_test.go b/syft/pkg/cataloger/java/cataloger_test.go index 7bed8cfcb..b5acbe4a9 100644 --- a/syft/pkg/cataloger/java/cataloger_test.go +++ b/syft/pkg/cataloger/java/cataloger_test.go @@ -30,6 +30,7 @@ func Test_ArchiveCataloger_Globs(t *testing.T) { "java-archives/example.jpi", "java-archives/example.hpi", "java-archives/example.lpkg", + "java-archives/example.rar", "archives/example.zip", "archives/example.tar", "archives/example.tar.gz", diff --git a/syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.rar b/syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.rar new file mode 100644 index 000000000..8944cbcc0 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.rar @@ -0,0 +1 @@ +example archive From e0680eb704aab6c861e2ecf43db262811226be75 Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 02:02:47 -0400 Subject: [PATCH 28/38] chore(deps): update tools to latest versions (#4307) --- .binny.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.binny.yaml b/.binny.yaml index b2ef8a8c0..8f6c6afa5 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -58,7 +58,7 @@ tools: # used to release all artifacts - name: goreleaser version: - want: v2.12.6 + want: v2.12.7 method: github-release with: repo: goreleaser/goreleaser From 88bbcbe9c66d0e1f7e6696dc46a128c92f216872 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 02:03:09 -0400 Subject: [PATCH 29/38] chore(deps): bump anchore/sbom-action from 0.20.8 to 0.20.9 (#4305) --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d0451d090..38c5a4395 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -161,7 +161,7 @@ jobs: # for updating brew formula in anchore/homebrew-syft GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }} - - uses: anchore/sbom-action@aa0e114b2e19480f157109b9922bda359bd98b90 #v0.20.8 + - uses: anchore/sbom-action@8e94d75ddd33f69f691467e42275782e4bfefe84 #v0.20.9 continue-on-error: true with: file: go.mod From bee78c0b16cc5bfbfee194994e045084360dedff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Oct 2025 10:43:04 -0400 Subject: [PATCH 30/38] chore(deps): bump github/codeql-action from 4.30.9 to 4.31.0 (#4310) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.30.9 to 4.31.0. - [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/16140ae1a102900babc80a33c44059580f687047...4e94bd11f71e507f7f87df81788dff88d1dacbfb) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.31.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a370ff8f3..f4fa817a4 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v3.29.5 + uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb #v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -58,7 +58,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@16140ae1a102900babc80a33c44059580f687047 #v3.29.5 + uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb #v3.29.5 # â„šī¸ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -72,4 +72,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v3.29.5 + uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb #v3.29.5 From 0d9ea69a668d69e80ceaf37753dafe899eb05a06 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Tue, 28 Oct 2025 09:35:11 -0400 Subject: [PATCH 31/38] Respect "rpmmod" PURL qualifier (#4314) Red Hat purls the RPM modularity info in a query param in the PURLs in their vulnerability data. It would be nice if Syft respected this qualifier so that Grype can use it when a Red Hat purl is passed. Signed-off-by: Will Murphy --- syft/format/internal/backfill.go | 36 +++++++++++++++++++++ syft/format/internal/backfill_test.go | 19 +++++++++++ syft/internal/fileresolver/metadata_test.go | 3 +- syft/pkg/url.go | 3 ++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/syft/format/internal/backfill.go b/syft/format/internal/backfill.go index f40aa9215..6e5544b95 100644 --- a/syft/format/internal/backfill.go +++ b/syft/format/internal/backfill.go @@ -29,6 +29,7 @@ func Backfill(p *pkg.Package) { var cpes []cpe.CPE epoch := "" + rpmmod := "" for _, qualifier := range purl.Qualifiers { switch qualifier.Key { @@ -44,6 +45,8 @@ func Backfill(p *pkg.Package) { } case pkg.PURLQualifierEpoch: epoch = qualifier.Value + case pkg.PURLQualifierRpmModularity: + rpmmod = qualifier.Value } } @@ -63,6 +66,10 @@ func Backfill(p *pkg.Package) { setJavaMetadataFromPurl(p, purl) } + if p.Type == pkg.RpmPkg { + setRpmMetadataFromPurl(p, rpmmod) + } + for _, c := range cpes { if slices.Contains(p.CPEs, c) { continue @@ -82,6 +89,35 @@ func setJavaMetadataFromPurl(p *pkg.Package, _ packageurl.PackageURL) { } } +func setRpmMetadataFromPurl(p *pkg.Package, rpmmod string) { + if p.Type != pkg.RpmPkg { + return + } + if rpmmod == "" { + return + } + + if p.Metadata == nil { + p.Metadata = pkg.RpmDBEntry{ + ModularityLabel: &rpmmod, + } + return + } + + switch m := p.Metadata.(type) { + case pkg.RpmDBEntry: + if m.ModularityLabel == nil { + m.ModularityLabel = &rpmmod + p.Metadata = m + } + case pkg.RpmArchive: + if m.ModularityLabel == nil { + m.ModularityLabel = &rpmmod + p.Metadata = m + } + } +} + func setVersionFromPurl(p *pkg.Package, purl packageurl.PackageURL, epoch string) { if p.Version == "" { p.Version = purl.Version diff --git a/syft/format/internal/backfill_test.go b/syft/format/internal/backfill_test.go index 0f88fa604..7e396e2b5 100644 --- a/syft/format/internal/backfill_test.go +++ b/syft/format/internal/backfill_test.go @@ -53,6 +53,21 @@ func Test_Backfill(t *testing.T) { Version: "1:1.12.8-26.el8", }, }, + { + name: "rpm with rpmmod", + in: pkg.Package{ + PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4", + }, + expected: pkg.Package{ + PURL: "pkg:rpm/redhat/httpd@2.4.37-51?arch=x86_64&distro=rhel-8.7&rpmmod=httpd:2.4", + Type: pkg.RpmPkg, + Name: "httpd", + Version: "2.4.37-51", + Metadata: pkg.RpmDBEntry{ + ModularityLabel: strRef("httpd:2.4"), + }, + }, + }, { name: "bad cpe", in: pkg.Package{ @@ -171,3 +186,7 @@ func Test_nameFromPurl(t *testing.T) { }) } } + +func strRef(s string) *string { + return &s +} diff --git a/syft/internal/fileresolver/metadata_test.go b/syft/internal/fileresolver/metadata_test.go index 936cd227c..ccb68d65f 100644 --- a/syft/internal/fileresolver/metadata_test.go +++ b/syft/internal/fileresolver/metadata_test.go @@ -4,9 +4,10 @@ import ( "os" "testing" - "github.com/anchore/stereoscope/pkg/file" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/anchore/stereoscope/pkg/file" ) func TestFileMetadataFromPath(t *testing.T) { diff --git a/syft/pkg/url.go b/syft/pkg/url.go index ef995f918..0c9335523 100644 --- a/syft/pkg/url.go +++ b/syft/pkg/url.go @@ -18,6 +18,9 @@ const ( // PURLQualifierUpstream this qualifier is not in the pURL spec, but is used by grype to perform indirect matching based on source information PURLQualifierUpstream = "upstream" + // PURLQualifierRpmModularity this qualifier is not in the pURL spec, but is used to specify RPM modularity information + PURLQualifierRpmModularity = "rpmmod" + purlCargoPkgType = "cargo" purlGradlePkgType = "gradle" ) From 9478cd974bb329c7fb5b562acc333a1f747af69f Mon Sep 17 00:00:00 2001 From: Brian Muenzenmeyer Date: Tue, 28 Oct 2025 10:29:07 -0500 Subject: [PATCH 32/38] docs: update template link in README.md (#4306) Signed-off-by: Brian Muenzenmeyer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26d55bc1f..52b210cb5 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Where the `formats` available are: - `spdx-json@2.2`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json). - `github-json`: A JSON report conforming to GitHub's dependency snapshot format. - `syft-table`: A columnar summary (default). -- `template`: Lets the user specify the output format. See ["Using templates"](#using-templates) below. +- `template`: Lets the user specify the output format. See ["Using templates"](https://github.com/anchore/syft/wiki/using-templates) below. Note that flags using the @ can be used for earlier versions of each specification as well. From 45bf8b14ab1b74ca7db2444bf6633269bfce6d4e Mon Sep 17 00:00:00 2001 From: Rez Moss Date: Tue, 28 Oct 2025 18:34:10 -0400 Subject: [PATCH 33/38] fix: omit records with empty PURL in GitHub format (#4312) Signed-off-by: Rez Moss --- syft/format/github/internal/model/model.go | 3 +++ .../snapshot/TestGithubDirectoryEncoder.golden | 5 ----- .../test-fixtures/snapshot/TestGithubImageEncoder.golden | 7 ------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/syft/format/github/internal/model/model.go b/syft/format/github/internal/model/model.go index 7d67e8763..69d2b9876 100644 --- a/syft/format/github/internal/model/model.go +++ b/syft/format/github/internal/model/model.go @@ -87,6 +87,9 @@ func toGithubManifests(s *sbom.SBOM) Manifests { } name := dependencyName(p) + if name == "" || p.PURL == "" { + continue + } manifest.Resolved[name] = DependencyNode{ PackageURL: p.PURL, Metadata: toDependencyMetadata(p), diff --git a/syft/format/github/test-fixtures/snapshot/TestGithubDirectoryEncoder.golden b/syft/format/github/test-fixtures/snapshot/TestGithubDirectoryEncoder.golden index 10cde9982..c428bd1e2 100644 --- a/syft/format/github/test-fixtures/snapshot/TestGithubDirectoryEncoder.golden +++ b/syft/format/github/test-fixtures/snapshot/TestGithubDirectoryEncoder.golden @@ -16,11 +16,6 @@ "source_location": "redacted/some/path/some/path/pkg1" }, "resolved": { - "": { - "package_url": "a-purl-2", - "relationship": "direct", - "scope": "runtime" - }, "pkg:deb/debian/package-2@2.0.1": { "package_url": "pkg:deb/debian/package-2@2.0.1", "relationship": "direct", diff --git a/syft/format/github/test-fixtures/snapshot/TestGithubImageEncoder.golden b/syft/format/github/test-fixtures/snapshot/TestGithubImageEncoder.golden index 45adf2b07..933cde693 100644 --- a/syft/format/github/test-fixtures/snapshot/TestGithubImageEncoder.golden +++ b/syft/format/github/test-fixtures/snapshot/TestGithubImageEncoder.golden @@ -17,13 +17,6 @@ }, "metadata": { "syft:filesystem":"redacted" - }, - "resolved": { - "": { - "package_url": "a-purl-1", - "relationship": "direct", - "scope": "runtime" - } } }, "user-image-input:/somefile-2.txt": { From 45fb52dca1f987602c7bfc2a4df60e5a55acb154 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:06:37 -0400 Subject: [PATCH 34/38] chore(deps): bump github.com/jedib0t/go-pretty/v6 from 6.6.8 to 6.6.9 (#4315) Bumps [github.com/jedib0t/go-pretty/v6](https://github.com/jedib0t/go-pretty) from 6.6.8 to 6.6.9. - [Release notes](https://github.com/jedib0t/go-pretty/releases) - [Commits](https://github.com/jedib0t/go-pretty/compare/v6.6.8...v6.6.9) --- updated-dependencies: - dependency-name: github.com/jedib0t/go-pretty/v6 dependency-version: 6.6.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 38037f371..6aac39732 100644 --- a/go.mod +++ b/go.mod @@ -58,7 +58,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/iancoleman/strcase v0.3.0 github.com/invopop/jsonschema v0.7.0 - github.com/jedib0t/go-pretty/v6 v6.6.8 + github.com/jedib0t/go-pretty/v6 v6.6.9 github.com/jinzhu/copier v0.4.0 github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 github.com/magiconair/properties v1.8.10 diff --git a/go.sum b/go.sum index 1be9a16bf..c28ddaa7e 100644 --- a/go.sum +++ b/go.sum @@ -609,8 +609,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77 github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= -github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jedib0t/go-pretty/v6 v6.6.9 h1:PQecJLK3L8ODuVyMe2223b61oRJjrKnmXAncbWTv9MY= +github.com/jedib0t/go-pretty/v6 v6.6.9/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= From 728feea620ced6473a7db763d902262844611d81 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Wed, 29 Oct 2025 10:07:47 -0400 Subject: [PATCH 35/38] ci: use apple creds before pushing tags (#4313) We have had a few releases fail because the Apple credentials needed some sort of fix. These release were operationally more interesting because they failed after pushing a git tag (which effectively releases the golagn package). Therefore, try to use these creds early, before there's a tag pushed. Signed-off-by: Will Murphy --- .github/workflows/release.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 38c5a4395..a74f10376 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -19,6 +19,16 @@ jobs: with: persist-credentials: false + - name: Bootstrap environment + uses: ./.github/actions/bootstrap + + - name: Validate Apple notarization credentials + run: .tool/quill submission list + env: + QUILL_NOTARY_ISSUER: ${{ secrets.APPLE_NOTARY_ISSUER }} + QUILL_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }} + QUILL_NOTARY_KEY: ${{ secrets.APPLE_NOTARY_KEY }} + - name: Check if running on main if: github.ref != 'refs/heads/main' # we are using the following flag when running `cosign blob-verify` for checksum signature verification: From f5c765192c1378ab517f37030a1779c0955cb1df Mon Sep 17 00:00:00 2001 From: Kudryavcev Nikolay <35200428+Rupikz@users.noreply.github.com> Date: Wed, 29 Oct 2025 17:41:18 +0300 Subject: [PATCH 36/38] Refactor fileresolver to not require base path (#4298) * ref: close source in test and examples Signed-off-by: Kudryavcev Nikolay * ref: pretty file/directory source resolver (make them more similar) Signed-off-by: Kudryavcev Nikolay * ref: move absoluteSymlinkFreePathToParent to file resolver Signed-off-by: Kudryavcev Nikolay * revert breaking change Signed-off-by: Kudryavcev Nikolay --------- Signed-off-by: Kudryavcev Nikolay --- cmd/syft/internal/commands/attest.go | 1 - cmd/syft/internal/commands/scan.go | 1 - .../test/integration/catalog_packages_test.go | 1 - .../internal/test/integration/utils_test.go | 4 +- examples/create_custom_sbom/main.go | 2 +- examples/create_simple_sbom/main.go | 2 +- examples/select_catalogers/main.go | 2 +- examples/source_from_registry/main.go | 1 + internal/os/feature_detection_test.go | 4 + syft/internal/fileresolver/directory.go | 10 +- syft/internal/fileresolver/file.go | 28 +++- .../fileresolver/filetree_resolver_test.go | 41 ++++-- .../sqlitetest/no_sqlite_driver_test.go | 2 +- .../directorysource/directory_source.go | 139 +++++++++--------- syft/source/filesource/file_source.go | 99 +++++-------- 15 files changed, 177 insertions(+), 160 deletions(-) diff --git a/cmd/syft/internal/commands/attest.go b/cmd/syft/internal/commands/attest.go index 417aa44d2..526303edb 100644 --- a/cmd/syft/internal/commands/attest.go +++ b/cmd/syft/internal/commands/attest.go @@ -253,7 +253,6 @@ func generateSBOMForAttestation(ctx context.Context, id clio.Identification, opt } src, err := getSource(ctx, opts, userInput, stereoscope.RegistryTag) - if err != nil { return nil, err } diff --git a/cmd/syft/internal/commands/scan.go b/cmd/syft/internal/commands/scan.go index c44b66d19..bc510948a 100644 --- a/cmd/syft/internal/commands/scan.go +++ b/cmd/syft/internal/commands/scan.go @@ -185,7 +185,6 @@ func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, use } src, err := getSource(ctx, &opts.Catalog, userInput, sources...) - if err != nil { return err } diff --git a/cmd/syft/internal/test/integration/catalog_packages_test.go b/cmd/syft/internal/test/integration/catalog_packages_test.go index 9fc65fdb0..c11be21c5 100644 --- a/cmd/syft/internal/test/integration/catalog_packages_test.go +++ b/cmd/syft/internal/test/integration/catalog_packages_test.go @@ -25,7 +25,6 @@ func BenchmarkImagePackageCatalogers(b *testing.B) { // get the source object for the image theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) require.NoError(b, err) - b.Cleanup(func() { require.NoError(b, theSource.Close()) }) diff --git a/cmd/syft/internal/test/integration/utils_test.go b/cmd/syft/internal/test/integration/utils_test.go index 978b8a7cc..7781fd89f 100644 --- a/cmd/syft/internal/test/integration/utils_test.go +++ b/cmd/syft/internal/test/integration/utils_test.go @@ -38,11 +38,11 @@ func catalogFixtureImageWithConfig(t *testing.T, fixtureImageName string, cfg *s // get the source to build an SBOM against theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) require.NoError(t, err) - t.Cleanup(func() { require.NoError(t, theSource.Close()) }) + // build the SBOM s, err := syft.CreateSBOM(context.Background(), theSource, cfg) require.NoError(t, err) @@ -66,7 +66,7 @@ func catalogDirectory(t *testing.T, dir string, catalogerSelection ...string) (s func catalogDirectoryWithConfig(t *testing.T, dir string, cfg *syft.CreateSBOMConfig) (sbom.SBOM, source.Source) { cfg.CatalogerSelection = cfg.CatalogerSelection.WithDefaults(pkgcataloging.DirectoryTag) - // get the source to build an sbom against + // get the source to build an SBOM against theSource, err := syft.GetSource(context.Background(), dir, syft.DefaultGetSourceConfig().WithSources("dir")) require.NoError(t, err) t.Cleanup(func() { diff --git a/examples/create_custom_sbom/main.go b/examples/create_custom_sbom/main.go index 459bb632c..5de359d89 100644 --- a/examples/create_custom_sbom/main.go +++ b/examples/create_custom_sbom/main.go @@ -23,6 +23,7 @@ const defaultImage = "alpine:3.19" func main() { // automagically get a source.Source for arbitrary string input src := getSource(imageReference()) + defer src.Close() // will catalog the given source and return a SBOM keeping in mind several configurable options sbom := getSBOM(src) @@ -46,7 +47,6 @@ func getSource(input string) source.Source { fmt.Println("detecting source type for input:", input, "...") src, err := syft.GetSource(context.Background(), input, nil) - if err != nil { panic(err) } diff --git a/examples/create_simple_sbom/main.go b/examples/create_simple_sbom/main.go index 788e8e530..59f365bfd 100644 --- a/examples/create_simple_sbom/main.go +++ b/examples/create_simple_sbom/main.go @@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19" func main() { // automagically get a source.Source for arbitrary string input src := getSource(imageReference()) + defer src.Close() // catalog the given source and return a SBOM sbom := getSBOM(src) @@ -40,7 +41,6 @@ func imageReference() string { func getSource(input string) source.Source { src, err := syft.GetSource(context.Background(), input, nil) - if err != nil { panic(err) } diff --git a/examples/select_catalogers/main.go b/examples/select_catalogers/main.go index bc8c6c813..fed786fb1 100644 --- a/examples/select_catalogers/main.go +++ b/examples/select_catalogers/main.go @@ -19,6 +19,7 @@ const defaultImage = "alpine:3.19" func main() { // automagically get a source.Source for arbitrary string input src := getSource(imageReference()) + defer src.Close() // catalog the given source and return a SBOM // let's explicitly use catalogers that are: @@ -44,7 +45,6 @@ func imageReference() string { func getSource(input string) source.Source { src, err := syft.GetSource(context.Background(), input, nil) - if err != nil { panic(err) } diff --git a/examples/source_from_registry/main.go b/examples/source_from_registry/main.go index c43ad8afd..894d7cfae 100644 --- a/examples/source_from_registry/main.go +++ b/examples/source_from_registry/main.go @@ -15,6 +15,7 @@ func main() { image := "alpine:3.19" src, _ := syft.GetSource(context.Background(), image, syft.DefaultGetSourceConfig().WithSources("registry")) + defer src.Close() sbom, _ := syft.CreateSBOM(context.Background(), src, syft.DefaultCreateSBOMConfig()) diff --git a/internal/os/feature_detection_test.go b/internal/os/feature_detection_test.go index f7622e7ff..9a1fe32f9 100644 --- a/internal/os/feature_detection_test.go +++ b/internal/os/feature_detection_test.go @@ -81,6 +81,10 @@ func Test_EnvironmentTask(t *testing.T) { // get the source theSource, err := syft.GetSource(context.Background(), tarPath, syft.DefaultGetSourceConfig().WithSources("docker-archive")) require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, theSource.Close()) + }) + resolver, err := theSource.FileResolver(source.SquashedScope) require.NoError(t, err) diff --git a/syft/internal/fileresolver/directory.go b/syft/internal/fileresolver/directory.go index 55882b802..750cfffd9 100644 --- a/syft/internal/fileresolver/directory.go +++ b/syft/internal/fileresolver/directory.go @@ -19,16 +19,16 @@ type Directory struct { indexer *directoryIndexer } -func NewFromDirectory(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { - r, err := newFromDirectoryWithoutIndex(root, base, pathFilters...) +func NewFromDirectory(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { + resolver, err := newFromDirectoryWithoutIndex(root, base, pathFilters...) if err != nil { return nil, err } - return r, r.buildIndex() + return resolver, resolver.buildIndex() } -func newFromDirectoryWithoutIndex(root string, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { +func newFromDirectoryWithoutIndex(root, base string, pathFilters ...PathIndexVisitor) (*Directory, error) { chroot, err := NewChrootContextFromCWD(root, base) if err != nil { return nil, fmt.Errorf("unable to interpret chroot context: %w", err) @@ -66,6 +66,6 @@ func (r *Directory) buildIndex() error { } // Stringer to represent a directory path data source -func (r Directory) String() string { +func (r *Directory) String() string { return fmt.Sprintf("dir:%s", r.path) } diff --git a/syft/internal/fileresolver/file.go b/syft/internal/fileresolver/file.go index 114612f82..16ffadffe 100644 --- a/syft/internal/fileresolver/file.go +++ b/syft/internal/fileresolver/file.go @@ -17,17 +17,31 @@ type File struct { indexer *fileIndexer } -// parent should be the symlink free absolute path to the parent directory +// NewFromFile single file analyser // path is the filepath of the file we're creating content access for -func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, error) { - chroot, err := NewChrootContextFromCWD(parent, parent) +func NewFromFile(path string, pathFilters ...PathIndexVisitor) (*File, error) { + resolver, err := newFromFileWithoutIndex(path, pathFilters...) + if err != nil { + return nil, err + } + + return resolver, resolver.buildIndex() +} + +func newFromFileWithoutIndex(path string, pathFilters ...PathIndexVisitor) (*File, error) { + absParentDir, err := absoluteSymlinkFreePathToParent(path) + if err != nil { + return nil, err + } + + chroot, err := NewChrootContextFromCWD(absParentDir, absParentDir) if err != nil { return nil, fmt.Errorf("unable to interpret chroot context: %w", err) } cleanBase := chroot.Base() - file := &File{ + return &File{ path: path, FiletreeResolver: FiletreeResolver{ Chroot: *chroot, @@ -36,9 +50,7 @@ func NewFromFile(parent, path string, pathFilters ...PathIndexVisitor) (*File, e Opener: nativeOSFileOpener, }, indexer: newFileIndexer(path, cleanBase, pathFilters...), - } - - return file, file.buildIndex() + }, nil } func (r *File) buildIndex() error { @@ -58,6 +70,6 @@ func (r *File) buildIndex() error { } // Stringer to represent a file path data source -func (r File) String() string { +func (r *File) String() string { return fmt.Sprintf("file:%s", r.path) } diff --git a/syft/internal/fileresolver/filetree_resolver_test.go b/syft/internal/fileresolver/filetree_resolver_test.go index a3c5209e1..d2ad67990 100644 --- a/syft/internal/fileresolver/filetree_resolver_test.go +++ b/syft/internal/fileresolver/filetree_resolver_test.go @@ -1384,9 +1384,10 @@ func TestFileResolver_FilesByPath(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, tt.filePath) + resolver, err := NewFromFile(tt.filePath) require.NoError(t, err) require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) refs, err := resolver.FilesByPath(tt.fileByPathInput) require.NoError(t, err) @@ -1431,8 +1432,11 @@ func TestFileResolver_MultipleFilesByPath(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) assert.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) + refs, err := resolver.FilesByPath(tt.input...) assert.NoError(t, err) @@ -1449,8 +1453,11 @@ func TestFileResolver_FilesByGlob(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) assert.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) + refs, err := resolver.FilesByGlob("**/*.txt") assert.NoError(t, err) @@ -1476,8 +1483,11 @@ func Test_fileResolver_FilesByMIMEType(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) assert.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) + locations, err := resolver.FilesByMIMEType(test.mimeType) assert.NoError(t, err) assert.Equal(t, test.expectedPaths.Size(), len(locations)) @@ -1497,10 +1507,12 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) { require.NoError(t, err) require.NotNil(t, parentPath) - r, err := NewFromFile(parentPath, filePath) + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) - exists, existingPath, err := r.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt"))) + exists, existingPath, err := resolver.Tree.File(stereoscopeFile.Path(filepath.Join(cwd, "test-fixtures/image-simple/file-1.txt"))) require.True(t, exists) require.NoError(t, err) require.True(t, existingPath.HasReference()) @@ -1525,7 +1537,7 @@ func Test_fileResolver_FileContentsByLocation(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - actual, err := r.FileContentsByLocation(test.location) + actual, err := resolver.FileContentsByLocation(test.location) if test.err { require.Error(t, err) return @@ -1546,8 +1558,11 @@ func TestFileResolver_AllLocations_errorOnDirRequest(t *testing.T) { parentPath, err := absoluteSymlinkFreePathToParent(filePath) require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) var dirLoc *file.Location ctx, cancel := context.WithCancel(context.Background()) @@ -1575,8 +1590,11 @@ func TestFileResolver_AllLocations(t *testing.T) { parentPath, err := absoluteSymlinkFreePathToParent(filePath) require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) paths := strset.New() for loc := range resolver.AllLocations(context.Background()) { @@ -1600,8 +1618,11 @@ func Test_FileResolver_AllLocationsDoesNotLeakGoRoutine(t *testing.T) { parentPath, err := absoluteSymlinkFreePathToParent(filePath) require.NoError(t, err) require.NotNil(t, parentPath) - resolver, err := NewFromFile(parentPath, filePath) + + resolver, err := NewFromFile(filePath) require.NoError(t, err) + require.NotNil(t, resolver) + assert.Equal(t, resolver.Chroot.Base(), parentPath) require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) diff --git a/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go b/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go index f1a3f17e4..894569200 100644 --- a/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go +++ b/syft/pkg/cataloger/redhat/sqlitetest/no_sqlite_driver_test.go @@ -13,7 +13,7 @@ import ( func Test_noSQLiteDriverError(t *testing.T) { // this test package does must not import the sqlite library file := "../test-fixtures/Packages" - resolver, err := fileresolver.NewFromFile(file, file) + resolver, err := fileresolver.NewFromFile(file) require.NoError(t, err) cataloger := redhat.NewDBCataloger() diff --git a/syft/source/directorysource/directory_source.go b/syft/source/directorysource/directory_source.go index d45084a4e..e4f79eee4 100644 --- a/syft/source/directorysource/directory_source.go +++ b/syft/source/directorysource/directory_source.go @@ -30,24 +30,21 @@ type Config struct { type directorySource struct { id artifact.ID config Config - resolver *fileresolver.Directory + resolver file.Resolver mutex *sync.Mutex } func NewFromPath(path string) (source.Source, error) { - cfg := Config{ - Path: path, - } - return New(cfg) + return New(Config{Path: path}) } func New(cfg Config) (source.Source, error) { - fi, err := os.Stat(cfg.Path) + fileMeta, err := os.Stat(cfg.Path) if err != nil { return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err) } - if !fi.IsDir() { + if !fileMeta.IsDir() { return nil, fmt.Errorf("given path is not a directory: %q", cfg.Path) } @@ -58,53 +55,6 @@ func New(cfg Config) (source.Source, error) { }, nil } -// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then -// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived -// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory -// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without -// consideration for the path. -func deriveIDFromDirectory(cfg Config) artifact.ID { - var info string - if !cfg.Alias.IsEmpty() { - // don't use any of the path information -- instead use the alias name and version as the artifact ID. - // why? this allows the user to set a dependable stable value for the artifact ID in case the - // scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again). - info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version) - } else { - log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)") - info = cleanDirPath(cfg.Path, cfg.Base) - } - - return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String()) -} - -func cleanDirPath(path, base string) string { - if path == base { - return path - } - - if base != "" { - cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path) - cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base) - - if rootErr == nil && baseErr == nil { - // allows for normalizing inputs: - // cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path - // cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001 - // normalized: some/path - - relPath, err := filepath.Rel(cleanBase, cleanRoot) - if err == nil { - path = relPath - } - // this is odd, but this means we can't use base - } - // if the base is not a valid chroot, then just use the path as-is - } - - return path -} - func (s directorySource) ID() artifact.ID { return s.id } @@ -118,9 +68,11 @@ func (s directorySource) Describe() source.Description { if a.Name != "" { name = a.Name } + if a.Version != "" { version = a.Version } + if a.Supplier != "" { supplier = a.Supplier } @@ -141,29 +93,31 @@ func (s *directorySource) FileResolver(_ source.Scope) (file.Resolver, error) { s.mutex.Lock() defer s.mutex.Unlock() - if s.resolver == nil { - exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths) - if err != nil { - return nil, err - } - - // this should be the only file resolver that might have overlap with where files are cached - exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...) - - res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...) - if err != nil { - return nil, fmt.Errorf("unable to create directory resolver: %w", err) - } - - s.resolver = res + if s.resolver != nil { + return s.resolver, nil } + exclusionFunctions, err := GetDirectoryExclusionFunctions(s.config.Path, s.config.Exclude.Paths) + if err != nil { + return nil, err + } + + // this should be the only file resolver that might have overlap with where files are cached + exclusionFunctions = append(exclusionFunctions, excludeCachePathVisitors()...) + + res, err := fileresolver.NewFromDirectory(s.config.Path, s.config.Base, exclusionFunctions...) + if err != nil { + return nil, fmt.Errorf("unable to create directory resolver: %w", err) + } + + s.resolver = res return s.resolver, nil } func (s *directorySource) Close() error { s.mutex.Lock() defer s.mutex.Unlock() + s.resolver = nil return nil } @@ -221,3 +175,50 @@ func GetDirectoryExclusionFunctions(root string, exclusions []string) ([]fileres }, }, nil } + +// deriveIDFromDirectory generates an artifact ID from the given directory config. If an alias is provided, then +// the artifact ID is derived exclusively from the alias name and version. Otherwise, the artifact ID is derived +// from the path provided with an attempt to prune a prefix if a base is given. Since the contents of the directory +// are not considered, there is no semantic meaning to the artifact ID -- this is why the alias is preferred without +// consideration for the path. +func deriveIDFromDirectory(cfg Config) artifact.ID { + var info string + if !cfg.Alias.IsEmpty() { + // don't use any of the path information -- instead use the alias name and version as the artifact ID. + // why? this allows the user to set a dependable stable value for the artifact ID in case the + // scanning root changes (e.g. a user scans a directory, then moves it to a new location and scans again). + info = fmt.Sprintf("%s@%s", cfg.Alias.Name, cfg.Alias.Version) + } else { + log.Warn("no explicit name and version provided for directory source, deriving artifact ID from the given path (which is not ideal)") + info = cleanDirPath(cfg.Path, cfg.Base) + } + + return internal.ArtifactIDFromDigest(digest.SHA256.FromString(filepath.Clean(info)).String()) +} + +func cleanDirPath(path, base string) string { + if path == base { + return path + } + + if base != "" { + cleanRoot, rootErr := fileresolver.NormalizeRootDirectory(path) + cleanBase, baseErr := fileresolver.NormalizeBaseDirectory(base) + + if rootErr == nil && baseErr == nil { + // allows for normalizing inputs: + // cleanRoot: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001/some/path + // cleanBase: /var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/TestDirectoryEncoder1121632790/001 + // normalized: some/path + + relPath, err := filepath.Rel(cleanBase, cleanRoot) + if err == nil { + path = relPath + } + // this is odd, but this means we can't use base + } + // if the base is not a valid chroot, then just use the path as-is + } + + return path +} diff --git a/syft/source/filesource/file_source.go b/syft/source/filesource/file_source.go index f74b0c089..da2be0e19 100644 --- a/syft/source/filesource/file_source.go +++ b/syft/source/filesource/file_source.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path" - "path/filepath" "sync" "github.com/opencontainers/go-digest" @@ -50,7 +49,13 @@ func NewFromPath(path string) (source.Source, error) { } func New(cfg Config) (source.Source, error) { - fileMeta, err := os.Stat(cfg.Path) + f, err := os.Open(cfg.Path) + if err != nil { + return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err) + } + defer f.Close() + + fileMeta, err := f.Stat() if err != nil { return nil, fmt.Errorf("unable to stat path=%q: %w", cfg.Path, err) } @@ -59,33 +64,19 @@ func New(cfg Config) (source.Source, error) { return nil, fmt.Errorf("given path is a directory: %q", cfg.Path) } - analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive) - if err != nil { - return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err) - } - var digests []file.Digest if len(cfg.DigestAlgorithms) > 0 { - fh, err := os.Open(cfg.Path) - if err != nil { - return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err) - } - - defer fh.Close() - - digests, err = intFile.NewDigestsFromFile(context.TODO(), fh, cfg.DigestAlgorithms) + digests, err = intFile.NewDigestsFromFile(context.TODO(), f, cfg.DigestAlgorithms) if err != nil { return nil, fmt.Errorf("unable to calculate digests for file=%q: %w", cfg.Path, err) } } - fh, err := os.Open(cfg.Path) + analysisPath, cleanupFn, err := fileAnalysisPath(cfg.Path, cfg.SkipExtractArchive) if err != nil { - return nil, fmt.Errorf("unable to open file=%q: %w", cfg.Path, err) + return nil, fmt.Errorf("unable to extract file analysis path=%q: %w", cfg.Path, err) } - defer fh.Close() - id, versionDigest := deriveIDFromFile(cfg) return &fileSource{ @@ -96,26 +87,10 @@ func New(cfg Config) (source.Source, error) { analysisPath: analysisPath, digestForVersion: versionDigest, digests: digests, - mimeType: stereoFile.MIMEType(fh), + mimeType: stereoFile.MIMEType(f), }, nil } -// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included -// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be -// logically different, then ID will express that. -func deriveIDFromFile(cfg Config) (artifact.ID, string) { - d := digestOfFileContents(cfg.Path) - info := d - - if !cfg.Alias.IsEmpty() { - // if the user provided an alias, we want to consider that in the artifact ID. This way if the user - // scans the same item but is considered to be logically different, then ID will express that. - info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version) - } - - return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d -} - func (s fileSource) ID() artifact.ID { return s.id } @@ -168,52 +143,56 @@ func (s fileSource) FileResolver(_ source.Scope) (file.Resolver, error) { if err != nil { return nil, fmt.Errorf("unable to stat path=%q: %w", s.analysisPath, err) } - isArchiveAnalysis := fi.IsDir() - absParentDir, err := absoluteSymlinkFreePathToParent(s.analysisPath) - if err != nil { - return nil, err - } - - if isArchiveAnalysis { + if isArchiveAnalysis := fi.IsDir(); isArchiveAnalysis { // this is an analysis of an archive file... we should scan the directory where the archive contents res, err := fileresolver.NewFromDirectory(s.analysisPath, "", exclusionFunctions...) if err != nil { return nil, fmt.Errorf("unable to create directory resolver: %w", err) } + s.resolver = res return s.resolver, nil } // This is analysis of a single file. Use file indexer. - res, err := fileresolver.NewFromFile(absParentDir, s.analysisPath, exclusionFunctions...) + res, err := fileresolver.NewFromFile(s.analysisPath, exclusionFunctions...) if err != nil { return nil, fmt.Errorf("unable to create file resolver: %w", err) } + s.resolver = res return s.resolver, nil } -func absoluteSymlinkFreePathToParent(path string) (string, error) { - absAnalysisPath, err := filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err) - } - dereferencedAbsAnalysisPath, err := filepath.EvalSymlinks(absAnalysisPath) - if err != nil { - return "", fmt.Errorf("unable to get absolute path for analysis path=%q: %w", path, err) - } - return filepath.Dir(dereferencedAbsAnalysisPath), nil -} - func (s *fileSource) Close() error { + s.mutex.Lock() + defer s.mutex.Unlock() + if s.closer == nil { return nil } + s.resolver = nil return s.closer() } +// deriveIDFromFile derives an artifact ID from the contents of a file. If an alias is provided, it will be included +// in the ID derivation (along with contents). This way if the user scans the same item but is considered to be +// logically different, then ID will express that. +func deriveIDFromFile(cfg Config) (artifact.ID, string) { + d := digestOfFileContents(cfg.Path) + info := d + + if !cfg.Alias.IsEmpty() { + // if the user provided an alias, we want to consider that in the artifact ID. This way if the user + // scans the same item but is considered to be logically different, then ID will express that. + info += fmt.Sprintf(":%s@%s", cfg.Alias.Name, cfg.Alias.Version) + } + + return internal.ArtifactIDFromDigest(digest.SHA256.FromString(info).String()), d +} + // fileAnalysisPath returns the path given, or in the case the path is an archive, the location where the archive // contents have been made available. A cleanup function is provided for any temp files created (if any). // Users can disable unpacking archives, allowing individual cataloguers to extract them instead (where @@ -253,15 +232,17 @@ func fileAnalysisPath(path string, skipExtractArchive bool) (string, func() erro } func digestOfFileContents(path string) string { - file, err := os.Open(path) + f, err := os.Open(path) if err != nil { return digest.SHA256.FromString(path).String() } - defer file.Close() - di, err := digest.SHA256.FromReader(file) + defer f.Close() + + di, err := digest.SHA256.FromReader(f) if err != nil { return digest.SHA256.FromString(path).String() } + return di.String() } From c5c14548485dc2a656c60a4805433f3345621772 Mon Sep 17 00:00:00 2001 From: kyounghoonJang Date: Thu, 30 Oct 2025 00:41:27 +0900 Subject: [PATCH 37/38] feat(java): Add support for .far (Feature Archive) files (#4193) * feat(java): add support for .far archivesEnables the Java cataloger to recognize and catalog dependencies within .far files, which are used in Apache Sling applications. Signed-off-by: Kyounghoon Jang * feat(java): Add tests for .far (Feature Archive) file support Signed-off-by: Kyounghoon Jang --------- Signed-off-by: Kyounghoon Jang Signed-off-by: Alex Goodman Co-authored-by: Alex Goodman --- syft/pkg/cataloger/java/archive_parser.go | 1 + syft/pkg/cataloger/java/cataloger_test.go | 1 + .../java/test-fixtures/glob-paths/java-archives/example.far | 1 + 3 files changed, 3 insertions(+) create mode 100644 syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.far diff --git a/syft/pkg/cataloger/java/archive_parser.go b/syft/pkg/cataloger/java/archive_parser.go index 5e3d188a1..af09bb616 100644 --- a/syft/pkg/cataloger/java/archive_parser.go +++ b/syft/pkg/cataloger/java/archive_parser.go @@ -37,6 +37,7 @@ var archiveFormatGlobs = []string{ "**/*.jpi", "**/*.hpi", "**/*.kar", + "**/*.far", "**/*.lpkg", // Zip-compressed package used to deploy applications // (aka plugins) to Liferay Portal server. Those files contains .JAR(s) and a .PROPERTIES file, the latter // has information about the application and installation requirements. diff --git a/syft/pkg/cataloger/java/cataloger_test.go b/syft/pkg/cataloger/java/cataloger_test.go index b5acbe4a9..f24a2744a 100644 --- a/syft/pkg/cataloger/java/cataloger_test.go +++ b/syft/pkg/cataloger/java/cataloger_test.go @@ -29,6 +29,7 @@ func Test_ArchiveCataloger_Globs(t *testing.T) { "java-archives/example.kar", "java-archives/example.jpi", "java-archives/example.hpi", + "java-archives/example.far", "java-archives/example.lpkg", "java-archives/example.rar", "archives/example.zip", diff --git a/syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.far b/syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.far new file mode 100644 index 000000000..8944cbcc0 --- /dev/null +++ b/syft/pkg/cataloger/java/test-fixtures/glob-paths/java-archives/example.far @@ -0,0 +1 @@ +example archive From efc2f0012c0cf97fb26c2d2164feb0f9ceb9b364 Mon Sep 17 00:00:00 2001 From: Stepan Date: Wed, 29 Oct 2025 18:59:47 +0300 Subject: [PATCH 38/38] fix: go binary replace handling in path (#4156) * Fix issue with relative paths on go binary Signed-off-by: Stepan * Linting Signed-off-by: Stepan --------- Signed-off-by: Stepan Co-authored-by: Alex Goodman --- syft/pkg/cataloger/golang/package.go | 11 +++- syft/pkg/cataloger/golang/package_test.go | 65 +++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/golang/package.go b/syft/pkg/cataloger/golang/package.go index bc3ac8e27..f07867e4c 100644 --- a/syft/pkg/cataloger/golang/package.go +++ b/syft/pkg/cataloger/golang/package.go @@ -10,7 +10,14 @@ import ( ) func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBinaryBuildinfoEntry, licenses []pkg.License, locations ...file.Location) pkg.Package { + // Similar to syft/pkg/cataloger/golang/parse_go_mod.go logic - use original path for relative replacements + finalPath := dep.Path if dep.Replace != nil { + if strings.HasPrefix(dep.Replace.Path, ".") || strings.HasPrefix(dep.Replace.Path, "/") { + finalPath = dep.Path + } else { + finalPath = dep.Replace.Path + } dep = dep.Replace } @@ -23,10 +30,10 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi } p := pkg.Package{ - Name: dep.Path, + Name: finalPath, Version: version, Licenses: pkg.NewLicenseSet(licenses...), - PURL: packageURL(dep.Path, version), + PURL: packageURL(finalPath, version), Language: pkg.Go, Type: pkg.GoModulePkg, Locations: file.NewLocationSet(locations...), diff --git a/syft/pkg/cataloger/golang/package_test.go b/syft/pkg/cataloger/golang/package_test.go index 548e6eeeb..199a5d07e 100644 --- a/syft/pkg/cataloger/golang/package_test.go +++ b/syft/pkg/cataloger/golang/package_test.go @@ -1,6 +1,7 @@ package golang import ( + "runtime/debug" "testing" "github.com/stretchr/testify/assert" @@ -54,3 +55,67 @@ func Test_packageURL(t *testing.T) { }) } } + +func Test_newGoBinaryPackage_relativeReplace(t *testing.T) { + tests := []struct { + name string + dep *debug.Module + expectedName string + }{ + { + name: "relative replace with ../", + dep: &debug.Module{ + Path: "github.com/aws/aws-sdk-go-v2", + Version: "(devel)", + Replace: &debug.Module{ + Path: "../../", + Version: "(devel)", + }, + }, + expectedName: "github.com/aws/aws-sdk-go-v2", // should use original path, not relative + }, + { + name: "relative replace with ./", + dep: &debug.Module{ + Path: "github.com/example/module", + Version: "v1.0.0", + Replace: &debug.Module{ + Path: "./local", + Version: "v0.0.0", + }, + }, + expectedName: "github.com/example/module", // should use original path + }, + { + name: "absolute replace", + dep: &debug.Module{ + Path: "github.com/old/module", + Version: "v1.0.0", + Replace: &debug.Module{ + Path: "github.com/new/module", + Version: "v2.0.0", + }, + }, + expectedName: "github.com/new/module", // should use replacement path + }, + { + name: "no replace", + dep: &debug.Module{ + Path: "github.com/normal/module", + Version: "v1.0.0", + }, + expectedName: "github.com/normal/module", // should use original path + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cataloger := &goBinaryCataloger{} + result := cataloger.newGoBinaryPackage(test.dep, pkg.GolangBinaryBuildinfoEntry{}, nil) + + assert.Equal(t, test.expectedName, result.Name) + assert.Equal(t, pkg.Go, result.Language) + assert.Equal(t, pkg.GoModulePkg, result.Type) + }) + } +}