gXjlJh$%o)#-BROI0e?r!>T|;l>(NlI7Svjr)d!6m00)!y%6Mdy=}I
zrRbBL;`Dtf?&1u$zuoslwzzX3hv&=$k&>D?0M>Q?%()yVkbxD
zX1CQ3+uhaeb;IW$Y@h_(q_(5X=aowVR)8?8_v2s2=hxYy{CuA;yu3JF__*?J;xC+G
BQ*!_S
literal 0
HcmV?d00001
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.18.4/linux-amd64/libkrb5.so.3.3 b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/krb5/1.18.4/linux-amd64/libkrb5.so.3.3
new file mode 100644
index 0000000000000000000000000000000000000000..71d35dd487b04a6e39fcc6e73164b067beb343d1
GIT binary patch
literal 357
zcmZvYze)r_48|=4VelaauD)fG`M;f~op8nZ05Wrv@fOG3!(9+NAHmM|@)fM@d;rJw
zusBSSUy_e}g!JKoT3)(%{}^>M4vlOi8*(0LYPl0AonVFfyJ^>QK*7eo+wW<**#*?5
zuTV~fRt$LERo;Y>aaJP)M|PrOR(M^W*Jw=GTFrmEY++
BRmuPW
literal 0
HcmV?d00001
diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml
index 41d402bc9..5f8fa88aa 100644
--- a/syft/pkg/cataloger/binary/testdata/config.yaml
+++ b/syft/pkg/cataloger/binary/testdata/config.yaml
@@ -1433,6 +1433,31 @@ from-images:
platform: linux/amd64
paths:
- /usr/lib/x86_64-linux-gnu/libQtCore.so.4.8.6
+
+ - name: krb5
+ version: 1.18.4
+ images:
+ - ref: apache/ozone-testkrb5:20230318-1@sha256:6a7eeac1ebd12e8968e34ab93fb8d21f2b92ae52bc0a85b662a2d41065f05d3a
+ platform: linux/amd64
+ paths:
+ - /usr/lib/libkrb5.so.3.3
+
+ - name: krb5
+ version: 1.17
+ images:
+ - ref: mongo:4.4@sha256:4be76f674fc4b27859816811b8baa3c51830eb1dbf4ca81a51e26b79edd662ef
+ platform: linux/amd64
+ paths:
+ - /usr/lib/x86_64-linux-gnu/libkrb5.so.3.3
+
+ - name: heimdal-krb5
+ version: 7.8.0
+ images:
+ - ref: nadimz/heimdal-krb5:7.8.0@sha256:23a5046493f3e97669353c18abcba6bd57fc9ddf53faa63b91b1a94f52f590e5
+ platform: linux/amd64
+ paths:
+ - /usr/lib/x86_64-linux-gnu/libkrb5.so.26.0.0
+
- version: 1.36.4
images:
- ref: envoyproxy/envoy:v1.36.4@sha256:ae31562b8cede20913a2d3d6a4f44c8479a50551e033cb8ef7bb8e38cec4b573
From a34f86fba1c226dc0f9fadc2aeb4571b234efeb2 Mon Sep 17 00:00:00 2001
From: sputnik-mac
Date: Mon, 29 Jun 2026 22:55:51 +0700
Subject: [PATCH 033/168] fix(template): expose sprig date/time functions in Go
templates (#4644)
* fix(template): expose sprig date functions in Go templates
Replace HermeticTxtFuncMap with TxtFuncMap to expose date/time
functions (now, date, dateInZone, etc.) while still excluding
security-sensitive env/expandenv functions.
Users can now use date functions in templates, e.g.:
{{ now | unixEpoch }}
{{ now | date "2006-01-02" }}
Fixes #2372
Signed-off-by: Sputnik-MAC
Signed-off-by: Alex Goodman
* invert to add functions to the hermetic set, not the other way around
Signed-off-by: Alex Goodman
---------
Signed-off-by: Sputnik-MAC
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
syft/format/template/encoder.go | 9 +++++++++
syft/format/template/encoder_test.go | 21 +++++++++++++++++++++
2 files changed, 30 insertions(+)
diff --git a/syft/format/template/encoder.go b/syft/format/template/encoder.go
index e387601c3..08ed33a65 100644
--- a/syft/format/template/encoder.go
+++ b/syft/format/template/encoder.go
@@ -33,7 +33,16 @@ type encoder struct {
func NewFormatEncoder(cfg EncoderConfig) (sbom.FormatEncoder, error) {
// TODO: revisit this... should no template file be an error or simply render an empty result? or render the json output?
// Note: do not check for the existence of the template file here, as the default encoder cannot provide one.
+ // start from the hermetic (repeatable) sprig map, which omits functions that reach into the
+ // environment or network (env, expandenv, getHostByName), then re-expose just the date/time
+ // helpers requested in issue #2372. Allowlisting here keeps the user-template trust boundary
+ // safe even if sprig adds new non-hermetic functions in the future.
f := sprig.HermeticTxtFuncMap()
+ full := sprig.TxtFuncMap()
+ for _, name := range []string{"now", "date", "dateInZone", "dateModify", "date_in_zone", "date_modify", "htmlDate", "htmlDateInZone"} {
+ f[name] = full[name]
+ }
+
f["getLastIndex"] = func(collection any) int {
if v := reflect.ValueOf(collection); v.Kind() == reflect.Slice {
return v.Len() - 1
diff --git a/syft/format/template/encoder_test.go b/syft/format/template/encoder_test.go
index 4418b20af..dc487730d 100644
--- a/syft/format/template/encoder_test.go
+++ b/syft/format/template/encoder_test.go
@@ -82,6 +82,27 @@ func TestFormatWithOptionAndHasField(t *testing.T) {
)
}
+func TestFuncMap_ExposesDateFunctions_ExcludesEnvAndNetwork(t *testing.T) {
+ enc, err := NewFormatEncoder(DefaultEncoderConfig())
+ require.NoError(t, err)
+
+ e, ok := enc.(encoder)
+ require.True(t, ok)
+
+ // date/time functions (the reason for issue #2372) should be available
+ for _, name := range []string{"now", "date", "dateInZone", "dateModify", "unixEpoch"} {
+ _, exists := e.funcMap[name]
+ assert.Truef(t, exists, "expected date function %q to be available", name)
+ }
+
+ // functions that reach into the environment or network must remain excluded.
+ // rand*/uuidv4 are also kept out to preserve hermetic (repeatable) output.
+ for _, name := range []string{"env", "expandenv", "getHostByName", "randAlphaNum", "uuidv4"} {
+ _, exists := e.funcMap[name]
+ assert.Falsef(t, exists, "expected non-hermetic function %q to be excluded", name)
+ }
+}
+
func TestFormatWithoutOptions(t *testing.T) {
f, err := NewFormatEncoder(DefaultEncoderConfig())
require.NoError(t, err)
From e7f1a803e7587741a6133800329927280ee5098e Mon Sep 17 00:00:00 2001
From: Rez Moss
Date: Mon, 29 Jun 2026 13:52:55 -0400
Subject: [PATCH 034/168] =?UTF-8?q?fixed=20dotnet=20cataloger=20can't=20fi?=
=?UTF-8?q?nd=20packages=20from=20deps.json=20in=20linux=20el=E2=80=A6=20(?=
=?UTF-8?q?#4517)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fixed dotnet cataloger can't find packages from deps.json in linux elf, fixed #4514
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* split bundle and PE concerns
Signed-off-by: Alex Goodman
* limit resource usage of readall call
Signed-off-by: Alex Goodman
* removed duplicat
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* make sure the first 4 bytes in elf arent lostt
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* revert readelfbundle func, check size of readdeps json
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* revert readelfbundle func, check size of readdeps json, fixed #4514
Co-authored-by: Alex Goodman
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* move dotnet net8 linux fixture to testdata convention
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman
* address malformed elf size claims + add tests
Signed-off-by: Alex Goodman
* dont key off of cataloger name in testing
Signed-off-by: Alex Goodman
---------
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Co-authored-by: Alex Goodman
Co-authored-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
---
cmd/syft/internal/options/catalog.go | 4 +-
.../cataloger/binary/pe_package_cataloger.go | 2 +-
syft/pkg/cataloger/dotnet/cataloger_test.go | 182 ++++++-
.../cataloger/dotnet/deps_binary_cataloger.go | 72 +++
syft/pkg/cataloger/dotnet/package.go | 22 +-
syft/pkg/cataloger/dotnet/pe.go | 2 +-
.../.gitignore | 2 +
.../Dockerfile | 16 +
.../src/Program.cs | 48 ++
.../src/dotnetapp.csproj | 15 +
.../src/packages.lock.json | 459 ++++++++++++++++++
.../internal/{pe => dotnet/bundle}/bundle.go | 97 +---
.../internal/dotnet/bundle/bundle_elf.go | 83 ++++
.../internal/dotnet/bundle/bundle_elf_test.go | 63 +++
.../cataloger/internal/dotnet/pe/bundle.go | 103 ++++
.../internal/{ => dotnet}/pe/bundle_test.go | 0
.../cataloger/internal/{ => dotnet}/pe/pe.go | 0
.../internal/{ => dotnet}/pe/pe_test.go | 0
.../{ => dotnet}/pe/testdata/Makefile | 0
.../image-dotnet31-single-file/Dockerfile | 2 +-
.../image-dotnet31-single-file/src/Program.cs | 0
.../src/hello.csproj | 0
.../image-dotnet5-single-file/Dockerfile | 2 +-
.../image-dotnet5-single-file/src/Program.cs | 0
.../src/hello.csproj | 0
.../image-dotnet6-single-file/Dockerfile | 2 +-
.../image-dotnet6-single-file/src/Program.cs | 0
.../src/hello.csproj | 0
.../dotnet/pe/testdata/image-net8-app | 1 +
.../pe/testdata/image-net8-app-single-file | 1 +
.../testdata/net8-app-single-file.deps.json | 0
.../internal/pe/testdata/image-net8-app | 1 -
.../pe/testdata/image-net8-app-single-file | 1 -
33 files changed, 1070 insertions(+), 110 deletions(-)
create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore
create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile
create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs
create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj
create mode 100644 syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json
rename syft/pkg/cataloger/internal/{pe => dotnet/bundle}/bundle.go (52%)
create mode 100644 syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go
create mode 100644 syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go
create mode 100644 syft/pkg/cataloger/internal/dotnet/pe/bundle.go
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/bundle_test.go (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/pe.go (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/pe_test.go (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/Makefile (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/Dockerfile (95%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/src/Program.cs (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet31-single-file/src/hello.csproj (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/Dockerfile (95%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/src/Program.cs (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet5-single-file/src/hello.csproj (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/Dockerfile (95%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/src/Program.cs (100%)
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/image-dotnet6-single-file/src/hello.csproj (100%)
create mode 120000 syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app
create mode 120000 syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file
rename syft/pkg/cataloger/internal/{ => dotnet}/pe/testdata/net8-app-single-file.deps.json (100%)
delete mode 120000 syft/pkg/cataloger/internal/pe/testdata/image-net8-app
delete mode 120000 syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file
diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go
index bcb8ab66f..1b74df358 100644
--- a/cmd/syft/internal/options/catalog.go
+++ b/cmd/syft/internal/options/catalog.go
@@ -176,7 +176,9 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
WithPropagateDLLClaimsToParents(cfg.Dotnet.PropagateDLLClaimsToParents).
- WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected),
+ WithRelaxDLLClaimsWhenBundlingDetected(cfg.Dotnet.RelaxDLLClaimsWhenBundlingDetected).
+ WithExcludeProjectReferences(cfg.Dotnet.ExcludeProjectReferences),
+
Golang: golang.DefaultCatalogerConfig().
WithSearchLocalModCacheLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.SearchLocalModCacheLicenses)).
WithLocalModCacheDir(cfg.Golang.LocalModCacheDir).
diff --git a/syft/pkg/cataloger/binary/pe_package_cataloger.go b/syft/pkg/cataloger/binary/pe_package_cataloger.go
index ba90d8835..16292be76 100644
--- a/syft/pkg/cataloger/binary/pe_package_cataloger.go
+++ b/syft/pkg/cataloger/binary/pe_package_cataloger.go
@@ -9,7 +9,7 @@ import (
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
- "github.com/anchore/syft/syft/pkg/cataloger/internal/pe"
+ "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/pe"
)
// NewPEPackageCataloger returns a cataloger that interprets packages from DLL, EXE, and BPL files.
diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go
index 65076f9f1..04b2caf20 100644
--- a/syft/pkg/cataloger/dotnet/cataloger_test.go
+++ b/syft/pkg/cataloger/dotnet/cataloger_test.go
@@ -14,10 +14,11 @@ import (
func TestCataloger_Globs(t *testing.T) {
tests := []struct {
- name string
- fixture string
- cataloger pkg.Cataloger
- expected []string
+ name string
+ fixture string
+ cataloger pkg.Cataloger
+ expected []string
+ ignoreUnfulfilled []string
}{
{
name: "obtain deps.json files",
@@ -47,15 +48,21 @@ func TestCataloger_Globs(t *testing.T) {
"src/something.dll",
"src/something.exe",
},
+ // the binary cataloger probes executables by MIME type to find embedded bundles,
+ // but the glob fixtures aren't real binaries so those queries go unfulfilled
+ ignoreUnfulfilled: []string{"application/x-executable", "application/x-sharedlib"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- pkgtest.NewCatalogTester().
+ tester := pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
- ExpectsResolverContentQueries(test.expected).
- TestCataloger(t, test.cataloger)
+ ExpectsResolverContentQueries(test.expected)
+ if len(test.ignoreUnfulfilled) > 0 {
+ tester = tester.IgnoreUnfulfilledPathResponses(test.ignoreUnfulfilled...)
+ }
+ tester.TestCataloger(t, test.cataloger)
})
}
}
@@ -1105,6 +1112,167 @@ func TestCataloger(t *testing.T) {
},
assertion: assertSingleFileDeployment,
},
+ {
+ name: "combined cataloger (single file linux)",
+ fixture: "image-net8-app-single-file-linux",
+ cataloger: NewDotnetDepsBinaryCataloger(DefaultCatalogerConfig()),
+ expectedPkgs: []string{
+ "Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.af @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.az @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.da @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.de @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.el @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.es @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.he @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.id @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.is @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.it @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp)",
+ "Newtonsoft.Json @ 13.0.3 (/app/dotnetapp)",
+ "dotnetapp @ 1.0.0 (/app/dotnetapp)",
+ "runtimepack.Microsoft.NETCore.App.Runtime.linux-musl-x64 @ 8.0.14 (/app/dotnetapp)",
+ },
+ expectedRels: []string{
+ "Humanizer @ 2.14.1 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.af @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.az @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.da @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.de @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.el @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.es @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.he @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.id @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.is @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.it @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.af @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ar @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.az @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.bg @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.bn-BD @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.cs @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.da @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.de @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.el @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.es @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fa @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fi-FI @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.fr-BE @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.he @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.hr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.hu @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.hy @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.id @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.is @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.it @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ja @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ko-KR @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ku @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.lv @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ms-MY @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.mt @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.nb @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.nb-NO @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.nl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.pl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.pt @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ro @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.ru @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sk @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sl @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sr-Latn @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.sv @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.th-TH @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.tr @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.uk @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.uz-Cyrl-UZ @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.uz-Latn-UZ @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.vi @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.zh-CN @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.zh-Hans @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Humanizer.Core.zh-Hant @ 2.14.1 (/app/dotnetapp) [dependency-of] Humanizer @ 2.14.1 (/app/dotnetapp)",
+ "Newtonsoft.Json @ 13.0.3 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)",
+ "runtimepack.Microsoft.NETCore.App.Runtime.linux-musl-x64 @ 8.0.14 (/app/dotnetapp) [dependency-of] dotnetapp @ 1.0.0 (/app/dotnetapp)",
+ },
+ },
{
name: "deps cataloger (self-contained)",
fixture: "image-net8-app-self-contained",
diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go
index fbf53e648..ea96b1710 100644
--- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go
+++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go
@@ -1,6 +1,7 @@
package dotnet
import (
+ "bytes"
"context"
"fmt"
"io"
@@ -16,7 +17,9 @@ import (
"github.com/anchore/syft/internal/unknown"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle"
)
const (
@@ -26,6 +29,8 @@ const (
bplGlob = "**/*.bpl"
)
+var elfMagic = []byte{0x7f, 'E', 'L', 'F'}
+
// depsBinaryCataloger will search for both deps.json evidence and PE file evidence to create packages. All packages
// from both sources are raised up, but with one merge operation applied; If a deps.json package reference can be
// correlated with a PE file, the PE file is attached to the package as supporting evidence.
@@ -38,11 +43,16 @@ func (c depsBinaryCataloger) Name() string {
}
func (c depsBinaryCataloger) Catalog(_ context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) { //nolint:funlen
+ elfDepsJSONs, elfUnknowns := findELFBundledDepsJSON(resolver)
+
depJSONDocs, unknowns, err := findDepsJSON(resolver)
if err != nil {
return nil, nil, err
}
+ depJSONDocs = append(depJSONDocs, elfDepsJSONs...)
+ unknowns = unknown.Join(unknowns, elfUnknowns)
+
peFiles, ldpeUnknownErr, err := findPEFiles(resolver)
if err != nil {
return nil, nil, err
@@ -520,6 +530,68 @@ func readPEFile(resolver file.Resolver, loc file.Location) (*logicalPE, error) {
return ldpe, nil
}
+func findELFBundledDepsJSON(resolver file.Resolver) ([]logicalDepsJSON, error) {
+ locs, err := resolver.FilesByMIMEType("application/x-executable", "application/x-sharedlib")
+ if err != nil {
+ return nil, nil
+ }
+
+ var depsJSONs []logicalDepsJSON
+ var unknownErr error
+ for _, loc := range locs {
+ doc, err := readELFBundledDepsJSON(resolver, loc)
+ if err != nil {
+ unknownErr = unknown.Append(unknownErr, loc, err)
+ continue
+ }
+ if doc != nil {
+ depsJSONs = append(depsJSONs, *doc)
+ }
+ }
+
+ return depsJSONs, unknownErr
+}
+
+func readELFBundledDepsJSON(resolver file.Resolver, loc file.Location) (*logicalDepsJSON, error) {
+ reader, err := resolver.FileContentsByLocation(loc)
+ if err != nil {
+ return nil, err
+ }
+ defer internal.CloseAndLogError(reader, loc.RealPath)
+
+ header := make([]byte, 4)
+ if _, err := io.ReadFull(reader, header); err != nil {
+ return nil, nil
+ }
+ if !bytes.Equal(header, elfMagic) {
+ return nil, nil
+ }
+
+ uReader, err := unionreader.GetUnionReader(reader)
+ if err != nil {
+ return nil, err
+ }
+
+ depsJSON, err := bundle.ExtractDepsJSONFromELFBundle(uReader)
+ if err != nil {
+ return nil, err
+ }
+
+ if depsJSON == "" {
+ return nil, nil
+ }
+
+ doc, err := newDepsJSON(file.NewLocationReadCloser(loc, io.NopCloser(strings.NewReader(depsJSON))))
+ if err != nil || doc == nil {
+ return nil, nil
+ }
+
+ doc.Location = loc
+ lDoc := getLogicalDepsJSON(*doc, nil)
+
+ return &lDoc, nil
+}
+
func extractEmbeddedDeps(pe logicalPE) *logicalDepsJSON {
doc, err := newDepsJSON(file.NewLocationReadCloser(pe.Location, io.NopCloser(strings.NewReader(pe.EmbeddedDepsJSON))))
if err != nil || doc == nil {
diff --git a/syft/pkg/cataloger/dotnet/package.go b/syft/pkg/cataloger/dotnet/package.go
index 9a11745fe..ac2361d60 100644
--- a/syft/pkg/cataloger/dotnet/package.go
+++ b/syft/pkg/cataloger/dotnet/package.go
@@ -20,6 +20,7 @@ var (
spaceRegex = regexp.MustCompile(`[\s\xa0]+`)
numberRegex = regexp.MustCompile(`\d`)
versionPunctuationRegex = regexp.MustCompile(`[.,]+`)
+ nonPrintableRegex = regexp.MustCompile(`[\x00-\x1f]`)
)
type runtimeFamily string
@@ -204,18 +205,29 @@ func cleanVersionResourceField(values ...string) string {
return ""
}
+var (
+ depsJSONPathRegex = regexp.MustCompile(`([^\\\/]+)\.deps\.json$`)
+ exePathRegex = regexp.MustCompile(`([^\\\/]+)\.exe$`)
+ singleFileRegex = regexp.MustCompile(`([^\\\/]+)$`)
+)
+
func getDepsJSONFilePrefix(p string) string {
- r := regexp.MustCompile(`([^\\\/]+)\.deps\.json$`)
- match := r.FindStringSubmatch(p)
+ match := depsJSONPathRegex.FindStringSubmatch(p)
if len(match) > 1 {
return match[1]
}
- r = regexp.MustCompile(`([^\\\/]+)\.exe$`)
- match = r.FindStringSubmatch(p)
+ match = exePathRegex.FindStringSubmatch(p)
if len(match) > 1 {
return match[1]
}
+
+ // Handle ELF
+ match = singleFileRegex.FindStringSubmatch(p)
+ if len(match) > 1 && !strings.Contains(match[1], ".") {
+ return match[1]
+ }
+
return ""
}
@@ -336,7 +348,7 @@ func spaceNormalize(value string) string {
// Consolidate all whitespace.
value = spaceRegex.ReplaceAllString(value, " ")
// Remove non-printable characters.
- value = regexp.MustCompile(`[\x00-\x1f]`).ReplaceAllString(value, "")
+ value = nonPrintableRegex.ReplaceAllString(value, "")
// Consolidate again and trim.
value = spaceRegex.ReplaceAllString(value, " ")
value = strings.TrimSpace(value)
diff --git a/syft/pkg/cataloger/dotnet/pe.go b/syft/pkg/cataloger/dotnet/pe.go
index 77e433c06..a1257c56c 100644
--- a/syft/pkg/cataloger/dotnet/pe.go
+++ b/syft/pkg/cataloger/dotnet/pe.go
@@ -2,7 +2,7 @@ package dotnet
import (
"github.com/anchore/syft/syft/file"
- "github.com/anchore/syft/syft/pkg/cataloger/internal/pe"
+ "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/pe"
)
// logicalPE represents a PE file within the context of a .NET project (considering the deps.json file).
diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore
new file mode 100644
index 000000000..b0b8376dc
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/.gitignore
@@ -0,0 +1,2 @@
+/app
+/extract.sh
\ No newline at end of file
diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile
new file mode 100644
index 000000000..88095de5d
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/Dockerfile
@@ -0,0 +1,16 @@
+# This is the same as the net8-app image, however, the entire .NET runtime is compiled into a single binary, residing with the application.
+FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:8.0-alpine@sha256:7d3a75ca5c8ac4679908ef7a2591b9bc257c62bd530167de32bba105148bb7be AS build
+ARG RUNTIME=linux-musl-x64
+WORKDIR /src
+
+COPY src/*.csproj .
+COPY src/packages.lock.json .
+RUN dotnet restore -r $RUNTIME --verbosity normal --locked-mode --force-evaluate
+
+COPY src/ .
+RUN dotnet publish -r $RUNTIME -p:PublishSingleFile=true -p:RestorePackagesWithLockFile=true -p:ErrorOnDuplicatePublishOutputFiles=false --self-contained true -p:EnableCompressionInSingleFile=true -p:DebugSymbols=false -p:DebugType=None -o /app
+
+
+FROM busybox
+WORKDIR /app
+COPY --from=build /app .
\ No newline at end of file
diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs
new file mode 100644
index 000000000..d1856dbc2
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/Program.cs
@@ -0,0 +1,48 @@
+using System;
+using Humanizer;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+
+namespace IndirectDependencyExample
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ string runtimeInfo = "hello world!\n";
+ Console.WriteLine(runtimeInfo);
+
+
+ Console.WriteLine($"\"this_is_a_test\" to title case: {"this_is_a_test".Humanize(LetterCasing.Title)}");
+
+ const string jsonString = @"
+ {
+ ""message"": ""Hello from JSON!"",
+ ""details"": {
+ ""timestamp"": ""2025-03-26T12:00:00Z"",
+ ""version"": ""1.0.0"",
+ ""metadata"": {
+ ""author"": ""Claude"",
+ ""environment"": ""Development""
+ }
+ },
+ ""items"": [
+ {
+ ""id"": 1,
+ ""name"": ""Item One""
+ },
+ {
+ ""id"": 2,
+ ""name"": ""Item Two""
+ }
+ ]
+ }";
+
+ JObject jsonObject = JObject.Parse(jsonString);
+
+ string message = (string)jsonObject["message"];
+ Console.WriteLine($"Message: {message}");
+ }
+ }
+}
\ No newline at end of file
diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj
new file mode 100644
index 000000000..de8b12f87
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/dotnetapp.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ true
+
+
+
+
+
+
+
+
diff --git a/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json
new file mode 100644
index 000000000..a0f55dc2c
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/image-net8-app-single-file-linux/src/packages.lock.json
@@ -0,0 +1,459 @@
+{
+ "version": 1,
+ "dependencies": {
+ "net8.0": {
+ "Humanizer": {
+ "type": "Direct",
+ "requested": "[2.14.1, )",
+ "resolved": "2.14.1",
+ "contentHash": "/FUTD3cEceAAmJSCPN9+J+VhGwmL/C12jvwlyM1DFXShEMsBzvLzLqSrJ2rb+k/W2znKw7JyflZgZpyE+tI7lA==",
+ "dependencies": {
+ "Humanizer.Core.af": "2.14.1",
+ "Humanizer.Core.ar": "2.14.1",
+ "Humanizer.Core.az": "2.14.1",
+ "Humanizer.Core.bg": "2.14.1",
+ "Humanizer.Core.bn-BD": "2.14.1",
+ "Humanizer.Core.cs": "2.14.1",
+ "Humanizer.Core.da": "2.14.1",
+ "Humanizer.Core.de": "2.14.1",
+ "Humanizer.Core.el": "2.14.1",
+ "Humanizer.Core.es": "2.14.1",
+ "Humanizer.Core.fa": "2.14.1",
+ "Humanizer.Core.fi-FI": "2.14.1",
+ "Humanizer.Core.fr": "2.14.1",
+ "Humanizer.Core.fr-BE": "2.14.1",
+ "Humanizer.Core.he": "2.14.1",
+ "Humanizer.Core.hr": "2.14.1",
+ "Humanizer.Core.hu": "2.14.1",
+ "Humanizer.Core.hy": "2.14.1",
+ "Humanizer.Core.id": "2.14.1",
+ "Humanizer.Core.is": "2.14.1",
+ "Humanizer.Core.it": "2.14.1",
+ "Humanizer.Core.ja": "2.14.1",
+ "Humanizer.Core.ko-KR": "2.14.1",
+ "Humanizer.Core.ku": "2.14.1",
+ "Humanizer.Core.lv": "2.14.1",
+ "Humanizer.Core.ms-MY": "2.14.1",
+ "Humanizer.Core.mt": "2.14.1",
+ "Humanizer.Core.nb": "2.14.1",
+ "Humanizer.Core.nb-NO": "2.14.1",
+ "Humanizer.Core.nl": "2.14.1",
+ "Humanizer.Core.pl": "2.14.1",
+ "Humanizer.Core.pt": "2.14.1",
+ "Humanizer.Core.ro": "2.14.1",
+ "Humanizer.Core.ru": "2.14.1",
+ "Humanizer.Core.sk": "2.14.1",
+ "Humanizer.Core.sl": "2.14.1",
+ "Humanizer.Core.sr": "2.14.1",
+ "Humanizer.Core.sr-Latn": "2.14.1",
+ "Humanizer.Core.sv": "2.14.1",
+ "Humanizer.Core.th-TH": "2.14.1",
+ "Humanizer.Core.tr": "2.14.1",
+ "Humanizer.Core.uk": "2.14.1",
+ "Humanizer.Core.uz-Cyrl-UZ": "2.14.1",
+ "Humanizer.Core.uz-Latn-UZ": "2.14.1",
+ "Humanizer.Core.vi": "2.14.1",
+ "Humanizer.Core.zh-CN": "2.14.1",
+ "Humanizer.Core.zh-Hans": "2.14.1",
+ "Humanizer.Core.zh-Hant": "2.14.1"
+ }
+ },
+ "Newtonsoft.Json": {
+ "type": "Direct",
+ "requested": "[13.0.3, )",
+ "resolved": "13.0.3",
+ "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ=="
+ },
+ "Humanizer.Core": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw=="
+ },
+ "Humanizer.Core.af": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "BoQHyu5le+xxKOw+/AUM7CLXneM/Bh3++0qh1u0+D95n6f9eGt9kNc8LcAHLIOwId7Sd5hiAaaav0Nimj3peNw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ar": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "3d1V10LDtmqg5bZjWkA/EkmGFeSfNBcyCH+TiHcHP+HGQQmRq3eBaLcLnOJbVQVn3Z6Ak8GOte4RX4kVCxQlFA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.az": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "8Z/tp9PdHr/K2Stve2Qs/7uqWPWLUK9D8sOZDNzyv42e20bSoJkHFn7SFoxhmaoVLJwku2jp6P7HuwrfkrP18Q==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.bg": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "S+hIEHicrOcbV2TBtyoPp1AVIGsBzlarOGThhQYCnP6QzEYo/5imtok6LMmhZeTnBFoKhM8yJqRfvJ5yqVQKSQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.bn-BD": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "U3bfj90tnUDRKlL1ZFlzhCHoVgpTcqUlTQxjvGCaFKb+734TTu3nkHUWVZltA1E/swTvimo/aXLtkxnLFrc0EQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.cs": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "jWrQkiCTy3L2u1T86cFkgijX6k7hoB0pdcFMWYaSZnm6rvG/XJE40tfhYyKhYYgIc1x9P2GO5AC7xXvFnFdqMQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.da": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "5o0rJyE/2wWUUphC79rgYDnif/21MKTTx9LIzRVz9cjCIVFrJ2bDyR2gapvI9D6fjoyvD1NAfkN18SHBsO8S9g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.de": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "9JD/p+rqjb8f5RdZ3aEJqbjMYkbk4VFii2QDnnOdNo6ywEfg/A5YeOQ55CaBJmy7KvV4tOK4+qHJnX/tg3Z54A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.el": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "Xmv6sTL5mqjOWGGpqY7bvbfK5RngaUHSa8fYDGSLyxY9mGdNbDcasnRnMOvi0SxJS9gAqBCn21Xi90n2SHZbFA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.es": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "e//OIAeMB7pjBV1HqqI4pM2Bcw3Jwgpyz9G5Fi4c+RJvhqFwztoWxW57PzTnNJE2lbhGGLQZihFZjsbTUsbczA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fa": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "nzDOj1x0NgjXMjsQxrET21t1FbdoRYujzbmZoR8u8ou5CBWY1UNca0j6n/PEJR/iUbt4IxstpszRy41wL/BrpA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fi-FI": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "Vnxxx4LUhp3AzowYi6lZLAA9Lh8UqkdwRh4IE2qDXiVpbo08rSbokATaEzFS+o+/jCNZBmoyyyph3vgmcSzhhQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "2p4g0BYNzFS3u9SOIDByp2VClYKO0K1ecDV4BkB9EYdEPWfFODYnF+8CH8LpUrpxL2TuWo2fiFx/4Jcmrnkbpg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.fr-BE": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "o6R3SerxCRn5Ij8nCihDNMGXlaJ/1AqefteAssgmU2qXYlSAGdhxmnrQAXZUDlE4YWt/XQ6VkNLtH7oMqsSPFQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.he": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "FPsAhy7Iw6hb+ZitLgYC26xNcgGAHXb0V823yFAzcyoL5ozM+DCJtYfDPYiOpsJhEZmKFTM9No0jUn1M89WGvg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.hr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "chnaD89yOlST142AMkAKLuzRcV5df3yyhDyRU5rypDiqrq2HN8y1UR3h1IicEAEtXLoOEQyjSAkAQ6QuXkn7aw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.hu": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "hAfnaoF9LTGU/CmFdbnvugN4tIs8ppevVMe3e5bD24+tuKsggMc5hYta9aiydI8JH9JnuVmxvNI4DJee1tK05A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.hy": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "sVIKxOiSBUb4gStRHo9XwwAg9w7TNvAXbjy176gyTtaTiZkcjr9aCPziUlYAF07oNz6SdwdC2mwJBGgvZ0Sl2g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.id": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "4Zl3GTvk3a49Ia/WDNQ97eCupjjQRs2iCIZEQdmkiqyaLWttfb+cYXDMGthP42nufUL0SRsvBctN67oSpnXtsg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.is": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "R67A9j/nNgcWzU7gZy1AJ07ABSLvogRbqOWvfRDn4q6hNdbg/mjGjZBp4qCTPnB2mHQQTCKo3oeCUayBCNIBCw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.it": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "jYxGeN4XIKHVND02FZ+Woir3CUTyBhLsqxu9iqR/9BISArkMf1Px6i5pRZnvq4fc5Zn1qw71GKKoCaHDJBsLFw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ja": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "TM3ablFNoYx4cYJybmRgpDioHpiKSD7q0QtMrmpsqwtiiEsdW5zz/q4PolwAczFnvrKpN6nBXdjnPPKVet93ng==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ko-KR": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "CtvwvK941k/U0r8PGdEuBEMdW6jv/rBiA9tUhakC7Zd2rA/HCnDcbr1DiNZ+/tRshnhzxy/qwmpY8h4qcAYCtQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ku": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "vHmzXcVMe+LNrF9txpdHzpG7XJX65SiN9GQd/Zkt6gsGIIEeECHrkwCN5Jnlkddw2M/b0HS4SNxdR1GrSn7uCA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.lv": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "E1/KUVnYBS1bdOTMNDD7LV/jdoZv/fbWTLPtvwdMtSdqLyRTllv6PGM9xVQoFDYlpvVGtEl/09glCojPHw8ffA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ms-MY": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "vX8oq9HnYmAF7bek4aGgGFJficHDRTLgp/EOiPv9mBZq0i4SA96qVMYSjJ2YTaxs7Eljqit7pfpE2nmBhY5Fnw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.mt": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "pEgTBzUI9hzemF7xrIZigl44LidTUhNu4x/P6M9sAwZjkUF0mMkbpxKkaasOql7lLafKrnszs0xFfaxQyzeuZQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.nb": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "mbs3m6JJq53ssLqVPxNfqSdTxAcZN3njlG8yhJVx83XVedpTe1ECK9aCa8FKVOXv93Gl+yRHF82Hw9T9LWv2hw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.nb-NO": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "AsJxrrVYmIMbKDGe8W6Z6//wKv9dhWH7RsTcEHSr4tQt/80pcNvLi0hgD3fqfTtg0tWKtgch2cLf4prorEV+5A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.nl": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "24b0OUdzJxfoqiHPCtYnR5Y4l/s4Oh7KW7uDp+qX25NMAHLCGog2eRfA7p2kRJp8LvnynwwQxm2p534V9m55wQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.pl": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "17mJNYaBssENVZyQHduiq+bvdXS0nhZJGEXtPKoMhKv3GD//WO0mEfd9wjEBsWCSmWI7bjRqhCidxzN+YtJmsg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.pt": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "8HB8qavcVp2la1GJX6t+G9nDYtylPKzyhxr9LAooIei9MnQvNsjEiIE4QvHoeDZ4weuQ9CsPg1c211XUMVEZ4A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ro": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "psXNOcA6R8fSHoQYhpBTtTTYiOk8OBoN3PKCEDgsJKIyeY5xuK81IBdGi77qGZMu/OwBRQjQCBMtPJb0f4O1+A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.ru": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "zm245xUWrajSN2t9H7BTf84/2APbUkKlUJpcdgsvTdAysr1ag9fi1APu6JEok39RRBXDfNRVZHawQ/U8X0pSvQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sk": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "Ncw24Vf3ioRnbU4MsMFHafkyYi8JOnTqvK741GftlQvAbULBoTz2+e7JByOaasqeSi0KfTXeegJO+5Wk1c0Mbw==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sl": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "l8sUy4ciAIbVThWNL0atzTS2HWtv8qJrsGWNlqrEKmPwA4SdKolSqnTes9V89fyZTc2Q43jK8fgzVE2C7t009A==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "rnNvhpkOrWEymy7R/MiFv7uef8YO5HuXDyvojZ7JpijHWA5dXuVXooCOiA/3E93fYa3pxDuG2OQe4M/olXbQ7w==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sr-Latn": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "nuy/ykpk974F8ItoQMS00kJPr2dFNjOSjgzCwfysbu7+gjqHmbLcYs7G4kshLwdA4AsVncxp99LYeJgoh1JF5g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.sv": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "E53+tpAG0RCp+cSSI7TfBPC+NnsEqUuoSV0sU+rWRXWr9MbRWx1+Zj02XMojqjGzHjjOrBFBBio6m74seFl0AA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.th-TH": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "eSevlJtvs1r4vQarNPfZ2kKDp/xMhuD00tVVzRXkSh1IAZbBJI/x2ydxUOwfK9bEwEp+YjvL1Djx2+kw7ziu7g==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.tr": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "rQ8N+o7yFcFqdbtu1mmbrXFi8TQ+uy+fVH9OPI0CI3Cu1om5hUU/GOMC3hXsTCI6d79y4XX+0HbnD7FT5khegA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.uk": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "2uEfujwXKNm6bdpukaLtEJD+04uUtQD65nSGCetA1fYNizItEaIBUboNfr3GzJxSMQotNwGVM3+nSn8jTd0VSg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.uz-Cyrl-UZ": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "TD3ME2sprAvFqk9tkWrvSKx5XxEMlAn1sjk+cYClSWZlIMhQQ2Bp/w0VjX1Kc5oeKjxRAnR7vFcLUFLiZIDk9Q==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.uz-Latn-UZ": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "/kHAoF4g0GahnugZiEMpaHlxb+W6jCEbWIdsq9/I1k48ULOsl/J0pxZj93lXC3omGzVF1BTVIeAtv5fW06Phsg==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.vi": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "rsQNh9rmHMBtnsUUlJbShMsIMGflZtPmrMM6JNDw20nhsvqfrdcoDD8cMnLAbuSovtc3dP+swRmLQzKmXDTVPA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.zh-CN": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "uH2dWhrgugkCjDmduLdAFO9w1Mo0q07EuvM0QiIZCVm6FMCu/lGv2fpMu4GX+4HLZ6h5T2Pg9FIdDLCPN2a67w==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.zh-Hans": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "WH6IhJ8V1UBG7rZXQk3dZUoP2gsi8a0WkL8xL0sN6WGiv695s8nVcmab9tWz20ySQbuzp0UkSxUQFi5jJHIpOQ==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ },
+ "Humanizer.Core.zh-Hant": {
+ "type": "Transitive",
+ "resolved": "2.14.1",
+ "contentHash": "VIXB7HCUC34OoaGnO3HJVtSv2/wljPhjV7eKH4+TFPgQdJj2lvHNKY41Dtg0Bphu7X5UaXFR4zrYYyo+GNOjbA==",
+ "dependencies": {
+ "Humanizer.Core": "[2.14.1]"
+ }
+ }
+ },
+ "net8.0/win-x64": {}
+ }
+}
\ No newline at end of file
diff --git a/syft/pkg/cataloger/internal/pe/bundle.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go
similarity index 52%
rename from syft/pkg/cataloger/internal/pe/bundle.go
rename to syft/pkg/cataloger/internal/dotnet/bundle/bundle.go
index a360441d5..facb70e73 100644
--- a/syft/pkg/cataloger/internal/pe/bundle.go
+++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle.go
@@ -1,8 +1,6 @@
-package pe
+package bundle
import (
- "bytes"
- "debug/pe"
"encoding/binary"
"errors"
"fmt"
@@ -45,92 +43,8 @@ const (
dotNetFileTypeSymbols
)
-// extractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle.
-// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into
-// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed
-// in a placeholder location within the PE structure, pointing to the bundle header which contains
-// file entry metadata. For V2+ bundles (.NET 5+), the header includes direct offsets to deps.json;
-// for V1 bundles (.NET Core 3.x), we parse the manifest to locate it.
-//
-// ┌──────────────────────────────────┐
-// │ PE AppHost Binary │ Standard PE structure
-// │ ... │
-// │ [8B offset][32B signature] │ Bundle marker (in placeholder within PE)
-// │ ... │
-// ├──────────────────────────────────┤
-// │ Bundled Files │ Raw file contents (assemblies, deps.json, etc.)
-// ├──────────────────────────────────┤
-// │ Bundle Header │ Version info, file count, deps.json offset (V2+)
-// │ File Manifest │ Per-file: offset, size, type, path
-// └──────────────────────────────────┘
-//
-// Parsing strategy:
-// 1. Search only the PE portion (using section headers) for the bundle signature
-// 2. Read 8 bytes before signature to get header offset
-// 3. Parse header to get deps.json location (V2+) or scan manifest entries (V1)
-//
-// See related documentation for more information:
-// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md
-// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/bundler.md
-// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
-// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
-// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/header.h
-// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h
-// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h
-func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) {
- headerOffset, err := findBundleHeaderOffset(r, sections)
- if err != nil {
- return "", err
- }
- if headerOffset == 0 {
- return "", nil // not a .NET single-file bundle
- }
-
- return readDepsJSONFromBundleHeader(r, headerOffset)
-}
-
-// findBundleHeaderOffset locates the bundle marker within the PE structure and returns the header offset.
-// Returns 0 if no bundle marker is found (not a single-file bundle).
-func findBundleHeaderOffset(r io.ReadSeeker, sections []pe.SectionHeader32) (int64, error) {
- peEndOffset := calculatePEEndOffset(sections)
-
- if _, err := r.Seek(0, io.SeekStart); err != nil {
- return 0, err
- }
-
- peData := make([]byte, peEndOffset)
- n, err := io.ReadFull(r, peData)
- if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
- return 0, err
- }
- peData = peData[:n]
-
- idx := bytes.Index(peData, dotNetBundleSignature)
- if idx == -1 || idx < 8 {
- return 0, nil
- }
-
- // the header offset is stored in the 8 bytes immediately before the signature
- headerOffset := int64(binary.LittleEndian.Uint64(peData[idx-8 : idx]))
- return headerOffset, nil
-}
-
-// calculatePEEndOffset determines where the PE structure ends based on section headers,
-// adding padding for alignment. This bounds our search for the bundle marker.
-func calculatePEEndOffset(sections []pe.SectionHeader32) int64 {
- var peEndOffset int64
- for _, sec := range sections {
- endOfSection := int64(sec.PointerToRawData) + int64(sec.SizeOfRawData)
- if endOfSection > peEndOffset {
- peEndOffset = endOfSection
- }
- }
- // add buffer for alignment padding after sections
- return peEndOffset + 4096
-}
-
-// readDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content.
-func readDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) {
+// ReadDepsJSONFromBundleHeader parses the bundle header at the given offset and extracts deps.json content.
+func ReadDepsJSONFromBundleHeader(r io.ReadSeeker, headerOffset int64) (string, error) {
if _, err := r.Seek(headerOffset, io.SeekStart); err != nil {
return "", err
}
@@ -194,6 +108,9 @@ func read7BitEncodedInt(r io.Reader) (int, error) {
// readDepsJSONAtOffset reads deps.json content at a specific offset using seeks (avoiding loading entire file)
func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) {
+ if size <= 0 || size > 3*1024*1024 { // 3MB max deps.json size in dotnet bundle
+ return "", nil
+ }
if _, err := r.Seek(offset, io.SeekStart); err != nil {
return "", fmt.Errorf("failed to seek to deps.json at offset %d: %w", offset, err)
}
@@ -206,7 +123,7 @@ func readDepsJSONAtOffset(r io.ReadSeeker, offset, size int64) (string, error) {
// findDepsJSONInManifest parses manifest entries to find deps.json (for V1 bundles or fallback)
func findDepsJSONInManifest(r io.ReadSeeker, numFiles int32, majorVersion uint32) (string, error) {
- for range numFiles {
+ for i := int32(0); i < numFiles; i++ {
var offset, size int64
if err := binary.Read(r, binary.LittleEndian, &offset); err != nil {
diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go
new file mode 100644
index 000000000..8725a7d01
--- /dev/null
+++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf.go
@@ -0,0 +1,83 @@
+package bundle
+
+import (
+ "bytes"
+ "debug/elf"
+ "encoding/binary"
+ "errors"
+ "io"
+
+ "github.com/anchore/syft/syft/internal/unionreader"
+)
+
+// ExtractDepsJSONFromELFBundle extracts the deps.json content from a .net singlefile
+// bundle contained within an ELF bin
+func ExtractDepsJSONFromELFBundle(r unionreader.UnionReader) (string, error) {
+ headerOffset, err := findBundleHeaderOffsetInELF(r)
+ if err != nil || headerOffset == 0 {
+ return "", err
+ }
+ return ReadDepsJSONFromBundleHeader(r, headerOffset)
+}
+
+func findBundleHeaderOffsetInELF(r unionreader.UnionReader) (int64, error) {
+ elfFile, err := elf.NewFile(r)
+ if err != nil {
+ return 0, nil
+ }
+
+ elfEndOffset := calculateELFEndOffset(elfFile)
+ if elfEndOffset == 0 {
+ return 0, nil
+ }
+
+ // clamp to the actual file size so a malformed ELF (with bogus segment/section
+ // offsets+sizes) can't drive an arbitrarily large allocation below.
+ fileSize, err := r.Seek(0, io.SeekEnd)
+ if err != nil {
+ return 0, err
+ }
+ if elfEndOffset > fileSize {
+ elfEndOffset = fileSize
+ }
+
+ if _, err := r.Seek(0, io.SeekStart); err != nil {
+ return 0, err
+ }
+
+ searchData := make([]byte, elfEndOffset)
+ n, err := io.ReadFull(r, searchData)
+ if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
+ return 0, err
+ }
+ searchData = searchData[:n]
+
+ idx := bytes.Index(searchData, dotNetBundleSignature)
+ if idx == -1 || idx < 8 {
+ return 0, nil
+ }
+
+ return int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx])), nil
+}
+
+func calculateELFEndOffset(f *elf.File) int64 {
+ var endOffset int64
+
+ for _, prog := range f.Progs {
+ end := int64(prog.Off) + int64(prog.Filesz)
+ if end > endOffset {
+ endOffset = end
+ }
+ }
+
+ for _, sec := range f.Sections {
+ if sec.Type == elf.SHT_NOBITS {
+ continue
+ }
+ end := int64(sec.Offset) + int64(sec.Size)
+ if end > endOffset {
+ endOffset = end
+ }
+ }
+ return endOffset + 4096
+}
diff --git a/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go
new file mode 100644
index 000000000..7a49dbea0
--- /dev/null
+++ b/syft/pkg/cataloger/internal/dotnet/bundle/bundle_elf_test.go
@@ -0,0 +1,63 @@
+package bundle
+
+import (
+ "bytes"
+ "encoding/binary"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// readSeekCloser adapts a *bytes.Reader to the unionreader.UnionReader interface (adds Close).
+type readSeekCloser struct {
+ *bytes.Reader
+}
+
+func (readSeekCloser) Close() error { return nil }
+
+// buildELFWithHugeFilesz returns a minimal, parseable ELF64 whose single program header
+// declares an absurd p_filesz. calculateELFEndOffset will compute a huge end offset; the
+// clamp in findBundleHeaderOffsetInELF must keep the allocation bounded to the real file.
+func buildELFWithHugeFilesz() []byte {
+ const (
+ ehSize = 64
+ phSize = 56
+ phOff = ehSize
+ phCount = 1
+ )
+ buf := make([]byte, ehSize+phSize)
+
+ // e_ident
+ copy(buf[0:4], []byte{0x7f, 'E', 'L', 'F'})
+ buf[4] = 2 // ELFCLASS64
+ buf[5] = 1 // ELFDATA2LSB
+ buf[6] = 1 // EV_CURRENT
+
+ le := binary.LittleEndian
+ le.PutUint16(buf[16:], 2) // e_type = ET_EXEC
+ le.PutUint16(buf[18:], 0x3e) // e_machine = x86-64
+ le.PutUint32(buf[20:], 1) // e_version
+ le.PutUint64(buf[32:], phOff) // e_phoff
+ le.PutUint16(buf[52:], ehSize) // e_ehsize
+ le.PutUint16(buf[54:], phSize) // e_phentsize
+ le.PutUint16(buf[56:], phCount) // e_phnum
+ // e_shoff/e_shnum left zero so no sections are parsed
+
+ ph := buf[phOff:]
+ le.PutUint32(ph[0:], 1) // p_type = PT_LOAD
+ le.PutUint64(ph[16:], 0) // p_offset
+ le.PutUint64(ph[32:], 1<<60) // p_filesz (bogus, attacker-controlled)
+ le.PutUint64(ph[40:], 1<<60) // p_memsz
+ return buf
+}
+
+func TestExtractDepsJSONFromELFBundle_MalformedFileszDoesNotOverAllocate(t *testing.T) {
+ data := buildELFWithHugeFilesz()
+ r := readSeekCloser{bytes.NewReader(data)}
+
+ // must not OOM/panic on the bogus p_filesz, and find no bundle signature
+ content, err := ExtractDepsJSONFromELFBundle(r)
+ require.NoError(t, err)
+ assert.Empty(t, content)
+}
diff --git a/syft/pkg/cataloger/internal/dotnet/pe/bundle.go b/syft/pkg/cataloger/internal/dotnet/pe/bundle.go
new file mode 100644
index 000000000..791b476b2
--- /dev/null
+++ b/syft/pkg/cataloger/internal/dotnet/pe/bundle.go
@@ -0,0 +1,103 @@
+package pe
+
+import (
+ "bytes"
+ "debug/pe"
+ "encoding/binary"
+ "errors"
+ "io"
+
+ "github.com/anchore/syft/syft/pkg/cataloger/internal/dotnet/bundle"
+)
+
+// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles.
+var dotNetBundleSignature = []byte{
+ 0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
+ 0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
+ 0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
+ 0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
+}
+
+// ExtractDepsJSONFromBundle searches for an embedded deps.json file in a .NET single-file bundle.
+// When built with PublishSingleFile=true, .NET embeds the application and all dependencies into
+// the AppHost executable. The bundle marker (8-byte header offset + 32-byte signature) is placed
+// in a placeholder location within the PE structure, pointing to the bundle header which contains
+// file entry metadata. For V2+ bundles (.NET 5+), the header includes direct offsets to deps.json;
+// for V1 bundles (.NET Core 3.x), we parse the manifest to locate it.
+//
+// ┌──────────────────────────────────┐
+// │ PE AppHost Binary │ Standard PE structure
+// │ ... │
+// │ [8B offset][32B signature] │ Bundle marker (in placeholder within PE)
+// │ ... │
+// ├──────────────────────────────────┤
+// │ Bundled Files │ Raw file contents (assemblies, deps.json, etc.)
+// ├──────────────────────────────────┤
+// │ Bundle Header │ Version info, file count, deps.json offset (V2+)
+// │ File Manifest │ Per-file: offset, size, type, path
+// └──────────────────────────────────┘
+//
+// Parsing strategy:
+// 1. Search only the PE portion (using section headers) for the bundle signature
+// 2. Read 8 bytes before signature to get header offset
+// 3. Parse header to get deps.json location (V2+) or scan manifest entries (V1)
+//
+// See related documentation for more information:
+// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md
+// - https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/bundler.md
+// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Manifest.cs
+// - https://github.com/dotnet/runtime/blob/main/src/installer/managed/Microsoft.NET.HostModel/Bundle/Bundler.cs
+// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/header.h
+// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_entry.h
+// - https://github.com/dotnet/runtime/blob/main/src/native/corehost/bundle/file_type.h
+func extractDepsJSONFromBundle(r io.ReadSeeker, sections []pe.SectionHeader32) (string, error) {
+ headerOffset, err := findBundleHeaderOffset(r, sections)
+ if err != nil {
+ return "", err
+ }
+ if headerOffset == 0 {
+ return "", nil // not a .NET single-file bundle
+ }
+
+ return bundle.ReadDepsJSONFromBundleHeader(r, headerOffset)
+}
+
+// findBundleHeaderOffset locates the bundle marker within the PE structure and returns the header offset.
+// Returns 0 if no bundle marker is found (not a single-file bundle).
+func findBundleHeaderOffset(r io.ReadSeeker, sections []pe.SectionHeader32) (int64, error) {
+ peEndOffset := calculatePEEndOffset(sections)
+
+ if _, err := r.Seek(0, io.SeekStart); err != nil {
+ return 0, err
+ }
+
+ peData := make([]byte, peEndOffset)
+ n, err := io.ReadFull(r, peData)
+ if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
+ return 0, err
+ }
+ peData = peData[:n]
+
+ idx := bytes.Index(peData, dotNetBundleSignature)
+ if idx == -1 || idx < 8 {
+ return 0, nil
+ }
+
+ // the header offset is stored in the 8 bytes immediately before the signature
+ headerOffset := int64(binary.LittleEndian.Uint64(peData[idx-8 : idx]))
+ return headerOffset, nil
+}
+
+// calculatePEEndOffset determines where the PE structure ends based on section headers,
+// adding padding for alignment. This bounds our search for the bundle marker.
+func calculatePEEndOffset(sections []pe.SectionHeader32) int64 {
+ var peEndOffset int64
+ for _, sec := range sections {
+ endOfSection := int64(sec.PointerToRawData) + int64(sec.SizeOfRawData)
+ if endOfSection > peEndOffset {
+ peEndOffset = endOfSection
+ }
+ }
+ // add buffer for alignment padding after sections
+ return peEndOffset + 4096
+}
diff --git a/syft/pkg/cataloger/internal/pe/bundle_test.go b/syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/bundle_test.go
rename to syft/pkg/cataloger/internal/dotnet/pe/bundle_test.go
diff --git a/syft/pkg/cataloger/internal/pe/pe.go b/syft/pkg/cataloger/internal/dotnet/pe/pe.go
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/pe.go
rename to syft/pkg/cataloger/internal/dotnet/pe/pe.go
diff --git a/syft/pkg/cataloger/internal/pe/pe_test.go b/syft/pkg/cataloger/internal/dotnet/pe/pe_test.go
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/pe_test.go
rename to syft/pkg/cataloger/internal/dotnet/pe/pe_test.go
diff --git a/syft/pkg/cataloger/internal/pe/testdata/Makefile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/Makefile
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/Makefile
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/Makefile
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile
similarity index 95%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile
index dc7f30763..71fdc97f7 100644
--- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/Dockerfile
+++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/Dockerfile
@@ -1,6 +1,6 @@
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/core/sdk:3.1 AS build
WORKDIR /src
-COPY src/ .
+COPY src .
RUN dotnet publish -c Release -r win-x64 \
-p:PublishSingleFile=true \
-p:SelfContained=true \
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/Program.cs
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/Program.cs
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/Program.cs
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/hello.csproj
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet31-single-file/src/hello.csproj
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet31-single-file/src/hello.csproj
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile
similarity index 95%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile
index 71ad584de..d9ab0b574 100644
--- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/Dockerfile
+++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/Dockerfile
@@ -1,6 +1,6 @@
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
-COPY src/ .
+COPY src .
RUN dotnet publish -c Release -r win-x64 \
-p:PublishSingleFile=true \
-p:SelfContained=true \
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/Program.cs
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/Program.cs
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/Program.cs
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/hello.csproj
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet5-single-file/src/hello.csproj
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet5-single-file/src/hello.csproj
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile
similarity index 95%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile
index 2089ef9e6..bf89e026f 100644
--- a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/Dockerfile
+++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/Dockerfile
@@ -1,6 +1,6 @@
FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
-COPY src/ .
+COPY src .
RUN dotnet publish -c Release -r win-x64 \
-p:PublishSingleFile=true \
-p:SelfContained=true \
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/Program.cs b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/Program.cs
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/Program.cs
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/Program.cs
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/hello.csproj b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/hello.csproj
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/image-dotnet6-single-file/src/hello.csproj
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/image-dotnet6-single-file/src/hello.csproj
diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app
new file mode 120000
index 000000000..34cfd36fd
--- /dev/null
+++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app
@@ -0,0 +1 @@
+../../../../dotnet/testdata/image-net8-app
\ No newline at end of file
diff --git a/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file
new file mode 120000
index 000000000..964979916
--- /dev/null
+++ b/syft/pkg/cataloger/internal/dotnet/pe/testdata/image-net8-app-single-file
@@ -0,0 +1 @@
+../../../../dotnet/testdata/image-net8-app-single-file
\ No newline at end of file
diff --git a/syft/pkg/cataloger/internal/pe/testdata/net8-app-single-file.deps.json b/syft/pkg/cataloger/internal/dotnet/pe/testdata/net8-app-single-file.deps.json
similarity index 100%
rename from syft/pkg/cataloger/internal/pe/testdata/net8-app-single-file.deps.json
rename to syft/pkg/cataloger/internal/dotnet/pe/testdata/net8-app-single-file.deps.json
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app b/syft/pkg/cataloger/internal/pe/testdata/image-net8-app
deleted file mode 120000
index 497e7d17c..000000000
--- a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app
+++ /dev/null
@@ -1 +0,0 @@
-../../../dotnet/testdata/image-net8-app
\ No newline at end of file
diff --git a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file b/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file
deleted file mode 120000
index 884c00cff..000000000
--- a/syft/pkg/cataloger/internal/pe/testdata/image-net8-app-single-file
+++ /dev/null
@@ -1 +0,0 @@
-../../../dotnet/testdata/image-net8-app-single-file
\ No newline at end of file
From deee79411a1ee0e3941dc06abcf6354955961128 Mon Sep 17 00:00:00 2001
From: Keith Zantow
Date: Mon, 29 Jun 2026 15:23:47 -0400
Subject: [PATCH 035/168] fix: composite action version parsing (#4616)
Signed-off-by: Keith Zantow
---
syft/pkg/cataloger/githubactions/package.go | 1 -
.../githubactions/parse_composite_action.go | 70 ++++++++++++++++++-
.../parse_composite_action_test.go | 22 ++++++
.../cataloger/githubactions/parse_workflow.go | 3 +-
.../testdata/composite-action.yaml | 7 ++
5 files changed, 99 insertions(+), 4 deletions(-)
diff --git a/syft/pkg/cataloger/githubactions/package.go b/syft/pkg/cataloger/githubactions/package.go
index cba6cea89..179957510 100644
--- a/syft/pkg/cataloger/githubactions/package.go
+++ b/syft/pkg/cataloger/githubactions/package.go
@@ -73,7 +73,6 @@ func parseStepUsageStatement(use, comment string) (string, string) {
// if version looks like a commit hash and we have a comment, try to extract version from comment
if version != "" && regexp.MustCompile(`^[0-9a-f]{7,}$`).MatchString(version) && comment != "" {
- versionRegex := regexp.MustCompile(`v?\d+\.\d+\.\d+`)
matches := versionRegex.FindStringSubmatch(comment)
if len(matches) >= 1 {
diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action.go b/syft/pkg/cataloger/githubactions/parse_composite_action.go
index 96378815e..09949aa03 100644
--- a/syft/pkg/cataloger/githubactions/parse_composite_action.go
+++ b/syft/pkg/cataloger/githubactions/parse_composite_action.go
@@ -24,12 +24,19 @@ type compositeActionRunsDef struct {
}
func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
- var ca compositeActionDef
var errs error
- if errs = yaml.NewDecoder(reader).Decode(&ca); errs != nil {
+ var node yaml.Node
+ if errs = yaml.NewDecoder(reader).Decode(&node); errs != nil {
+ return nil, nil, fmt.Errorf("unable to parse yaml workflow file: %w", errs)
+ }
+
+ var ca compositeActionDef
+ if errs = node.Decode(&ca); errs != nil {
return nil, nil, fmt.Errorf("unable to parse yaml composite action file: %w", errs)
}
+ attachCompositeActionUsageComments(&node, ca.Runs.Steps)
+
// we use a collection to help with deduplication before raising to higher level processing
pkgs := pkg.NewCollection()
@@ -49,3 +56,62 @@ func parseCompositeActionForActionUsage(_ context.Context, _ file.Resolver, _ *g
return pkgs.Sorted(), nil, errs
}
+
+func attachCompositeActionUsageComments(node *yaml.Node, steps []stepDef) {
+ root := node
+ if root.Kind == yaml.DocumentNode && len(root.Content) > 0 {
+ root = root.Content[0]
+ }
+ if root.Kind != yaml.MappingNode {
+ return
+ }
+
+ // find the "runs" key
+ for i := 0; i < len(root.Content); i += 2 {
+ key := root.Content[i]
+ value := root.Content[i+1]
+ if key.Value != "runs" || value.Kind != yaml.MappingNode {
+ continue
+ }
+ // find the "steps" key within runs
+ for j := 0; j < len(value.Content); j += 2 {
+ stepsKey := value.Content[j]
+ stepsValue := value.Content[j+1]
+ if stepsKey.Value != "steps" || stepsValue.Kind != yaml.SequenceNode {
+ continue
+ }
+ readSteps(stepsValue, steps)
+ }
+ }
+}
+
+func readSteps(stepsValue *yaml.Node, steps []stepDef) {
+ // iterate over each step
+ for stepIdx, stepNode := range stepsValue.Content {
+ if stepNode.Kind != yaml.MappingNode {
+ continue
+ }
+ // find the "uses" key within the step
+ for k := 0; k < len(stepNode.Content); k += 2 {
+ usesKey := stepNode.Content[k]
+ usesValue := stepNode.Content[k+1]
+ if usesKey.Value != "uses" || usesValue.Kind != yaml.ScalarNode {
+ continue
+ }
+ comment := usesValue.LineComment
+ if comment == "" {
+ comment = usesValue.HeadComment
+ }
+ if comment == "" {
+ comment = usesValue.FootComment
+ }
+ if comment == "" {
+ continue
+ }
+ versionMatch := versionRegex.FindString(comment)
+ if versionMatch != "" && stepIdx < len(steps) {
+ steps[stepIdx].UsesComment = versionMatch
+ }
+ }
+ }
+}
diff --git a/syft/pkg/cataloger/githubactions/parse_composite_action_test.go b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go
index 2ed607fe3..b089f939f 100644
--- a/syft/pkg/cataloger/githubactions/parse_composite_action_test.go
+++ b/syft/pkg/cataloger/githubactions/parse_composite_action_test.go
@@ -14,6 +14,28 @@ func Test_parseCompositeActionForActionUsage(t *testing.T) {
fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
expected := []pkg.Package{
+ {
+ Name: "actions/checkout",
+ Version: "11",
+ Type: pkg.GithubActionPkg,
+ Locations: fixtureLocationSet,
+ PURL: "pkg:github/actions/checkout@11",
+ Metadata: pkg.GitHubActionsUseStatement{
+ Value: "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683",
+ Comment: "11",
+ },
+ },
+ {
+ Name: "actions/setup-go",
+ Version: "v5.1.0",
+ Type: pkg.GithubActionPkg,
+ Locations: fixtureLocationSet,
+ PURL: "pkg:github/actions/setup-go@v5.1.0",
+ Metadata: pkg.GitHubActionsUseStatement{
+ Value: "actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed",
+ Comment: "v5.1.0",
+ },
+ },
{
Name: "actions/setup-go",
Version: "v4",
diff --git a/syft/pkg/cataloger/githubactions/parse_workflow.go b/syft/pkg/cataloger/githubactions/parse_workflow.go
index f784bce63..1957ac2f5 100644
--- a/syft/pkg/cataloger/githubactions/parse_workflow.go
+++ b/syft/pkg/cataloger/githubactions/parse_workflow.go
@@ -19,6 +19,8 @@ var (
_ generic.Parser = parseWorkflowForWorkflowUsage
)
+var versionRegex = regexp.MustCompile(`v?\d+(\.\d+)*`)
+
type workflowDef struct {
Jobs map[string]workflowJobDef `yaml:"jobs"`
}
@@ -185,7 +187,6 @@ func processUsesNode(node *yaml.Node, wf *workflowDef, currentJob *string, curre
}
if comment != "" {
- versionRegex := regexp.MustCompile(`v?\d+(\.\d+)*`)
versionMatch := versionRegex.FindString(comment)
if versionMatch != "" {
diff --git a/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml b/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml
index b294c90ae..8e8038b12 100644
--- a/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml
+++ b/syft/pkg/cataloger/githubactions/testdata/composite-action.yaml
@@ -35,6 +35,13 @@ runs:
path: ${{ github.workspace }}/.tmp
key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v 11
+
+ - name: Setup Go
+ uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed #v5.1.0
+ with:
+ go-version: ${{ inputs.go-version }}
+
# note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in
# some installations of project tools.
- name: Restore go module cache
From 148fe572bcd63ba1d9feffe4c15e3003e5f2acfd Mon Sep 17 00:00:00 2001
From: Rez Moss
Date: Tue, 30 Jun 2026 10:32:57 -0400
Subject: [PATCH 036/168] added macOS .app cataloger (#4490)
* added macOS .app cataloger, fixed #4010
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* added macOS .app cataloger, fixed #4010
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* added macOS .app cataloger, fixed #4010
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
* address review comments
Signed-off-by: Alex Goodman
* bump schema to 16.1.7
Signed-off-by: Alex Goodman
* address static analysis failures
Signed-off-by: Alex Goodman
* migrate to testdata
Signed-off-by: Alex Goodman
* expand fields and improve test coverage
Signed-off-by: Alex Goodman
---------
Signed-off-by: Rez Moss
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
.../test/integration/catalog_packages_test.go | 2 +
go.mod | 1 +
go.sum | 4 +
internal/constants.go | 3 +-
internal/packagemetadata/generated.go | 1 +
internal/packagemetadata/names.go | 1 +
internal/task/package_tasks.go | 2 +
schema/json/schema-16.1.7.json | 4422 +++++++++++++++++
schema/json/schema-latest.json | 66 +-
.../helpers/originator_supplier_test.go | 1 +
.../internal/spdxutil/helpers/source_info.go | 2 +
.../spdxutil/helpers/source_info_test.go | 8 +
syft/pkg/apple.go | 44 +
syft/pkg/cataloger/apple/capabilities.yaml | 42 +
syft/pkg/cataloger/apple/cataloger.go | 12 +
syft/pkg/cataloger/apple/cataloger_test.go | 180 +
syft/pkg/cataloger/apple/package.go | 20 +
syft/pkg/cataloger/apple/parse_info_plist.go | 91 +
.../Slack.app/Contents/Info.plist | 26 +
.../Ghostty.app/Contents/Info.plist | 30 +
.../testdata/parse-cases/malformed.plist | 1 +
.../parse-cases/name-from-executable.plist | 10 +
.../parse-cases/name-from-identifier.plist | 10 +
.../apple/testdata/parse-cases/no-name.plist | 8 +
.../testdata/parse-cases/no-version.plist | 8 +
.../version-from-bundle-version.plist | 10 +
syft/pkg/type.go | 2 +
syft/pkg/type_test.go | 5 +-
28 files changed, 5008 insertions(+), 4 deletions(-)
create mode 100644 schema/json/schema-16.1.7.json
create mode 100644 syft/pkg/apple.go
create mode 100644 syft/pkg/cataloger/apple/capabilities.yaml
create mode 100644 syft/pkg/cataloger/apple/cataloger.go
create mode 100644 syft/pkg/cataloger/apple/cataloger_test.go
create mode 100644 syft/pkg/cataloger/apple/package.go
create mode 100644 syft/pkg/cataloger/apple/parse_info_plist.go
create mode 100644 syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist
create mode 100644 syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist
create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist
create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist
create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist
create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist
create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist
create mode 100644 syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist
diff --git a/cmd/syft/internal/test/integration/catalog_packages_test.go b/cmd/syft/internal/test/integration/catalog_packages_test.go
index 53b0f4757..9abbb338d 100644
--- a/cmd/syft/internal/test/integration/catalog_packages_test.go
+++ b/cmd/syft/internal/test/integration/catalog_packages_test.go
@@ -89,6 +89,7 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.ModelPkg))
+ definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
var cases []testCase
cases = append(cases, commonTestCases...)
@@ -164,6 +165,7 @@ func TestPkgCoverageDirectory(t *testing.T) {
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
definedPkgs.Remove(string(pkg.ModelPkg))
+ definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
// for directory scans we should not expect to see any of the following package types
definedPkgs.Remove(string(pkg.KbPkg))
diff --git a/go.mod b/go.mod
index c7369689f..74adff89d 100644
--- a/go.mod
+++ b/go.mod
@@ -104,6 +104,7 @@ require (
require (
github.com/pb33f/ordered-map/v2 v2.3.1
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
+ howett.net/plist v1.0.1
)
require (
diff --git a/go.sum b/go.sum
index 8dcd0c532..5b38989f4 100644
--- a/go.sum
+++ b/go.sum
@@ -626,6 +626,7 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.8.1 h1:0fkCNhjrX0zPpwkWaDYU5VMrygg41Tu197mWILIJoqQ=
github.com/jedib0t/go-pretty/v6 v6.8.1/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
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=
@@ -1489,6 +1490,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
+gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1509,6 +1511,8 @@ 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=
+howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
+howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
diff --git a/internal/constants.go b/internal/constants.go
index dccdfdfd7..3f27e106c 100644
--- a/internal/constants.go
+++ b/internal/constants.go
@@ -3,7 +3,7 @@ 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.1.6"
+ JSONSchemaVersion = "16.1.7"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@@ -13,5 +13,6 @@ const (
// 16.1.4 - add BunLockEntry metadata type for bun.lock support
// 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
+ // 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
)
diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go
index eaa9643d3..1d39a8288 100644
--- a/internal/packagemetadata/generated.go
+++ b/internal/packagemetadata/generated.go
@@ -9,6 +9,7 @@ func AllTypes() []any {
return []any{
pkg.AlpmDBEntry{},
pkg.ApkDBEntry{},
+ pkg.AppleAppBundleEntry{},
pkg.BinarySignature{},
pkg.BitnamiSBOMEntry{},
pkg.BunLockEntry{},
diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go
index 3e8d9f174..408667974 100644
--- a/internal/packagemetadata/names.go
+++ b/internal/packagemetadata/names.go
@@ -124,6 +124,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.SnapEntry{}, "snap-entry"),
jsonNames(pkg.WordpressPluginEntry{}, "wordpress-plugin-entry", "WordpressMetadata"),
jsonNames(pkg.HomebrewFormula{}, "homebrew-formula"),
+ jsonNames(pkg.AppleAppBundleEntry{}, "apple-app-bundle-entry"),
jsonNames(pkg.LuaRocksPackage{}, "luarocks-package"),
jsonNames(pkg.TerraformLockProviderEntry{}, "terraform-lock-provider-entry"),
jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"),
diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go
index 016042424..71e9e3af6 100644
--- a/internal/task/package_tasks.go
+++ b/internal/task/package_tasks.go
@@ -5,6 +5,7 @@ import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/ai"
"github.com/anchore/syft/syft/pkg/cataloger/alpine"
+ "github.com/anchore/syft/syft/pkg/cataloger/apple"
"github.com/anchore/syft/syft/pkg/cataloger/arch"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
bitnamiSbomCataloger "github.com/anchore/syft/syft/pkg/cataloger/bitnami"
@@ -177,6 +178,7 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(wordpress.NewWordpressPluginCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "wordpress"),
newSimplePackageTaskFactory(terraform.NewLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, "terraform"),
newSimplePackageTaskFactory(homebrew.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "homebrew"),
+ newSimplePackageTaskFactory(apple.NewAppBundleCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "apple"),
newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"),
newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"),
newSimplePackageTaskFactory(ai.NewGGUFCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "gguf", "ml"),
diff --git a/schema/json/schema-16.1.7.json b/schema/json/schema-16.1.7.json
new file mode 100644
index 000000000..71e1dd2ff
--- /dev/null
+++ b/schema/json/schema-16.1.7.json
@@ -0,0 +1,4422 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/syft/json/16.1.7/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",
+ "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman."
+ },
+ "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)."
+ },
+ "AppleAppBundleEntry": {
+ "properties": {
+ "bundleIdentifier": {
+ "type": "string",
+ "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the short name of the bundle"
+ },
+ "displayName": {
+ "type": "string",
+ "description": "DisplayName is the user-facing name of the bundle"
+ },
+ "executable": {
+ "type": "string",
+ "description": "Executable is the name of the executable within the bundle"
+ },
+ "shortVersion": {
+ "type": "string",
+ "description": "ShortVersion is the release (marketing) version of the bundle"
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the build version of the bundle, which often differs from the short version"
+ },
+ "packageType": {
+ "type": "string",
+ "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)"
+ },
+ "supportedPlatforms": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")"
+ },
+ "minimumSystemVersion": {
+ "type": "string",
+ "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle"
+ },
+ "minimumOSVersion": {
+ "type": "string",
+ "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)"
+ },
+ "copyright": {
+ "type": "string",
+ "description": "Copyright is the human-readable copyright notice for the bundle"
+ },
+ "platformName": {
+ "type": "string",
+ "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")"
+ },
+ "sdkName": {
+ "type": "string",
+ "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")"
+ }
+ },
+ "type": "object",
+ "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files."
+ },
+ "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",
+ "description": "Value is the CPE string identifier."
+ },
+ "source": {
+ "type": "string",
+ "description": "Source is the source where this CPE was obtained or generated from."
+ }
+ },
+ "type": "object",
+ "required": [
+ "cpe"
+ ],
+ "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases."
+ },
+ "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."
+ },
+ "DenoLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies is the list of package specifiers that this package depends on"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies"
+ ],
+ "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file"
+ },
+ "DenoRemoteLockEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the remote URL from which the module fetcef"
+ },
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "integrity"
+ ],
+ "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file"
+ },
+ "Descriptor": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the tool that generated this SBOM."
+ },
+ "configuration": {
+ "description": "Configuration contains the tool configuration used during SBOM generation."
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "version"
+ ],
+ "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation."
+ },
+ "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",
+ "description": "Artifacts is the list of packages discovered and placed into the catalog"
+ },
+ "artifactRelationships": {
+ "items": {
+ "$ref": "#/$defs/Relationship"
+ },
+ "type": "array"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/File"
+ },
+ "type": "array",
+ "description": "note: must have omitempty"
+ },
+ "source": {
+ "$ref": "#/$defs/Source",
+ "description": "Source represents the original object that was cataloged"
+ },
+ "distro": {
+ "$ref": "#/$defs/LinuxRelease",
+ "description": "Distro represents the Linux distribution that was detected from the source"
+ },
+ "descriptor": {
+ "$ref": "#/$defs/Descriptor",
+ "description": "Descriptor is a block containing self-describing information about syft"
+ },
+ "schema": {
+ "$ref": "#/$defs/Schema",
+ "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape"
+ }
+ },
+ "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\")"
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is type of entry could be package or project for internal refs"
+ },
+ "executables": {
+ "additionalProperties": {
+ "$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)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing."
+ },
+ "appCpe": {
+ "type": "string",
+ "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)"
+ },
+ "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)"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages."
+ }
+ },
+ "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",
+ "description": "ID is a unique identifier for this file within the SBOM."
+ },
+ "location": {
+ "$ref": "#/$defs/Coordinates",
+ "description": "Location is the file path and layer information where this file was found."
+ },
+ "metadata": {
+ "$ref": "#/$defs/FileMetadataEntry",
+ "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the file contents for small files."
+ },
+ "digests": {
+ "items": {
+ "$ref": "#/$defs/Digest"
+ },
+ "type": "array",
+ "description": "Digests contains cryptographic hashes of the file contents."
+ },
+ "licenses": {
+ "items": {
+ "$ref": "#/$defs/FileLicense"
+ },
+ "type": "array",
+ "description": "Licenses contains license information discovered within this file."
+ },
+ "executable": {
+ "$ref": "#/$defs/Executable",
+ "description": "Executable contains executable metadata if this file is a binary."
+ },
+ "unknowns": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Unknowns contains unknown fields for forward compatibility."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "location"
+ ],
+ "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages."
+ },
+ "FileLicense": {
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Value is the raw license identifier or text as found in the file."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "evidence": {
+ "$ref": "#/$defs/FileLicenseEvidence",
+ "description": "Evidence contains supporting evidence for this license detection."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type"
+ ],
+ "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression."
+ },
+ "FileLicenseEvidence": {
+ "properties": {
+ "confidence": {
+ "type": "integer",
+ "description": "Confidence is the confidence score for this license detection (0-100)."
+ },
+ "offset": {
+ "type": "integer",
+ "description": "Offset is the byte offset where the license text starts in the file."
+ },
+ "extent": {
+ "type": "integer",
+ "description": "Extent is the length of the license text in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "confidence",
+ "offset",
+ "extent"
+ ],
+ "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level."
+ },
+ "FileMetadataEntry": {
+ "properties": {
+ "mode": {
+ "type": "integer",
+ "description": "Mode is the Unix file permission mode in octal format."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")."
+ },
+ "linkDestination": {
+ "type": "string",
+ "description": "LinkDestination is the target path for symbolic links."
+ },
+ "userID": {
+ "type": "integer",
+ "description": "UserID is the file owner user ID."
+ },
+ "groupID": {
+ "type": "integer",
+ "description": "GroupID is the file owner group ID."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIMEType is the MIME type of the file contents."
+ },
+ "size": {
+ "type": "integer",
+ "description": "Size is the file size in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "mode",
+ "type",
+ "userID",
+ "groupID",
+ "mimeType",
+ "size"
+ ],
+ "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file."
+ },
+ "GgufFileHeader": {
+ "properties": {
+ "ggufVersion": {
+ "type": "integer",
+ "description": "GGUFVersion is the GGUF format version (e.g., 3)"
+ },
+ "fileSize": {
+ "type": "integer",
+ "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)"
+ },
+ "architecture": {
+ "type": "string",
+ "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")"
+ },
+ "quantization": {
+ "type": "string",
+ "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")"
+ },
+ "parameters": {
+ "type": "integer",
+ "description": "Parameters is the number of model parameters (if present in header)"
+ },
+ "tensorCount": {
+ "type": "integer",
+ "description": "TensorCount is the number of tensors in the model"
+ },
+ "header": {
+ "type": "object",
+ "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication."
+ },
+ "metadataHash": {
+ "type": "string",
+ "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames."
+ },
+ "parts": {
+ "items": {
+ "$ref": "#/$defs/GgufFileHeader"
+ },
+ "type": "array",
+ "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)."
+ }
+ },
+ "type": "object",
+ "required": [
+ "ggufVersion",
+ "tensorCount"
+ ],
+ "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file."
+ },
+ "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",
+ "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field."
+ },
+ "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": {
+ "additionalProperties": {
+ "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."
+ },
+ "JavascriptBunLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of runtime dependencies and their version specifiers"
+ },
+ "optionalDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "OptionalDependencies is a map of optional dependencies and their version specifiers"
+ },
+ "peerDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "PeerDependencies is a map of peer dependencies and their version specifiers"
+ },
+ "bin": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Bin is a map of binary names to the paths they are installed to"
+ },
+ "os": {
+ "type": "string",
+ "description": "OS is the operating system constraint for the package (e.g. \"darwin\")"
+ },
+ "cpu": {
+ "type": "string",
+ "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies",
+ "optionalDependencies",
+ "peerDependencies",
+ "bin",
+ "os",
+ "cpu"
+ ],
+ "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file"
+ },
+ "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."
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\""
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file."
+ },
+ "JavascriptPnpmLockEntry": {
+ "properties": {
+ "resolution": {
+ "$ref": "#/$defs/PnpmLockResolution",
+ "description": "Resolution is the resolution information for the package"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolution",
+ "dependencies"
+ ],
+ "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml 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)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "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",
+ "description": "Value is the raw license identifier or expression as found."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "urls": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "URLs are URLs where license text or information can be found."
+ },
+ "locations": {
+ "items": {
+ "$ref": "#/$defs/Location"
+ },
+ "type": "array",
+ "description": "Locations are file locations where this license was discovered."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the full license text content."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type",
+ "urls",
+ "locations"
+ ],
+ "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence 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": {
+ "additionalProperties": {
+ "$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",
+ "description": "PrettyName is a human-readable operating system name with version."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the operating system name without version information."
+ },
+ "id": {
+ "type": "string",
+ "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")."
+ },
+ "idLike": {
+ "$ref": "#/$defs/IDLikes",
+ "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the operating system version including codename if available."
+ },
+ "versionID": {
+ "type": "string",
+ "description": "VersionID is the operating system version number or identifier."
+ },
+ "versionCodename": {
+ "type": "string",
+ "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")."
+ },
+ "buildID": {
+ "type": "string",
+ "description": "BuildID is a build identifier for the operating system."
+ },
+ "imageID": {
+ "type": "string",
+ "description": "ImageID is an identifier for container or cloud images."
+ },
+ "imageVersion": {
+ "type": "string",
+ "description": "ImageVersion is the version for container or cloud images."
+ },
+ "variant": {
+ "type": "string",
+ "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")."
+ },
+ "variantID": {
+ "type": "string",
+ "description": "VariantID is the lower-case operating system variant identifier."
+ },
+ "homeURL": {
+ "type": "string",
+ "description": "HomeURL is the homepage URL for the operating system."
+ },
+ "supportURL": {
+ "type": "string",
+ "description": "SupportURL is the support or help URL for the operating system."
+ },
+ "bugReportURL": {
+ "type": "string",
+ "description": "BugReportURL is the bug reporting URL for the operating system."
+ },
+ "privacyPolicyURL": {
+ "type": "string",
+ "description": "PrivacyPolicyURL is the privacy policy URL for the operating system."
+ },
+ "cpeName": {
+ "type": "string",
+ "description": "CPEName is the Common Platform Enumeration name for the operating system."
+ },
+ "supportEnd": {
+ "type": "string",
+ "description": "SupportEnd is the end of support date or version identifier."
+ },
+ "extendedSupport": {
+ "type": "boolean",
+ "description": "ExtendedSupport indicates whether extended security or support is available."
+ }
+ },
+ "type": "object",
+ "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files."
+ },
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Arbitrary key-value pairs that can be used to annotate a location"
+ }
+ },
+ "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": {
+ "additionalProperties": {
+ "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 represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)."
+ },
+ "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/AppleAppBundleEntry"
+ },
+ {
+ "$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/DenoLockEntry"
+ },
+ {
+ "$ref": "#/$defs/DenoRemoteLockEntry"
+ },
+ {
+ "$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/GgufFileHeader"
+ },
+ {
+ "$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/JavascriptBunLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackage"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackageLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptPnpmLockEntry"
+ },
+ {
+ "$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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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."
+ },
+ "PnpmLockResolution": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity"
+ ],
+ "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification."
+ },
+ "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/)."
+ },
+ "PythonPdmFileEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the file download URL"
+ },
+ "digest": {
+ "$ref": "#/$defs/PythonFileDigest",
+ "description": "Digest is the hash digest of the file hosted at the URL"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "digest"
+ ]
+ },
+ "PythonPdmLockEntry": {
+ "properties": {
+ "summary": {
+ "type": "string",
+ "description": "Summary provides a description of the package"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files with their paths and hash digests (for the base package without extras)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment"
+ },
+ "requiresPython": {
+ "type": "string",
+ "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")."
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependency specifications for the base package (without extras)"
+ },
+ "extras": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmLockExtraVariant"
+ },
+ "type": "array",
+ "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "summary",
+ "files"
+ ],
+ "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file."
+ },
+ "PythonPdmLockExtraVariant": {
+ "properties": {
+ "extras": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependencies specific to this extras variant"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files specific to this variant (only populated if different from base)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "extras"
+ ],
+ "description": "PythonPdmLockExtraVariant represents a specific extras combination variant 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",
+ "description": "Parent is the ID of the parent artifact in this relationship."
+ },
+ "child": {
+ "type": "string",
+ "description": "Child is the ID of the child artifact in this relationship."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional relationship-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "parent",
+ "child",
+ "type"
+ ],
+ "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package."
+ },
+ "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",
+ "description": "Version is the JSON schema version for this document format."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL is the URL to the JSON schema definition document."
+ }
+ },
+ "type": "object",
+ "required": [
+ "version",
+ "url"
+ ],
+ "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format."
+ },
+ "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",
+ "description": "ID is a unique identifier for the analyzed source artifact."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the analyzed artifact (e.g., image tag)."
+ },
+ "supplier": {
+ "type": "string",
+ "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional source-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "type",
+ "metadata"
+ ],
+ "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive."
+ },
+ "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 98796e99c..71e1dd2ff 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.1.6/document",
+ "$id": "anchore.io/schema/syft/json/16.1.7/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -249,6 +249,67 @@
],
"description": "ApkFileRecord represents a single file listing and metadata from a APK DB entry (which may have many of these file records)."
},
+ "AppleAppBundleEntry": {
+ "properties": {
+ "bundleIdentifier": {
+ "type": "string",
+ "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the short name of the bundle"
+ },
+ "displayName": {
+ "type": "string",
+ "description": "DisplayName is the user-facing name of the bundle"
+ },
+ "executable": {
+ "type": "string",
+ "description": "Executable is the name of the executable within the bundle"
+ },
+ "shortVersion": {
+ "type": "string",
+ "description": "ShortVersion is the release (marketing) version of the bundle"
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the build version of the bundle, which often differs from the short version"
+ },
+ "packageType": {
+ "type": "string",
+ "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)"
+ },
+ "supportedPlatforms": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")"
+ },
+ "minimumSystemVersion": {
+ "type": "string",
+ "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle"
+ },
+ "minimumOSVersion": {
+ "type": "string",
+ "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)"
+ },
+ "copyright": {
+ "type": "string",
+ "description": "Copyright is the human-readable copyright notice for the bundle"
+ },
+ "platformName": {
+ "type": "string",
+ "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")"
+ },
+ "sdkName": {
+ "type": "string",
+ "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")"
+ }
+ },
+ "type": "object",
+ "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files."
+ },
"BinarySignature": {
"properties": {
"matches": {
@@ -2674,6 +2735,9 @@
{
"$ref": "#/$defs/ApkDbEntry"
},
+ {
+ "$ref": "#/$defs/AppleAppBundleEntry"
+ },
{
"$ref": "#/$defs/BinarySignature"
},
diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
index f42654945..d18075450 100644
--- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
+++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
@@ -31,6 +31,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.GolangModuleEntry{},
pkg.GolangSourceEntry{},
pkg.HomebrewFormula{},
+ pkg.AppleAppBundleEntry{},
pkg.HackageStackYamlLockEntry{},
pkg.HackageStackYamlEntry{},
pkg.LinuxKernel{},
diff --git a/syft/format/internal/spdxutil/helpers/source_info.go b/syft/format/internal/spdxutil/helpers/source_info.go
index a26c1d74b..267703ab0 100644
--- a/syft/format/internal/spdxutil/helpers/source_info.go
+++ b/syft/format/internal/spdxutil/helpers/source_info.go
@@ -80,6 +80,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from found wordpress plugin PHP source files"
case pkg.HomebrewPkg:
answer = "acquired package info from Homebrew formula"
+ case pkg.AppleAppBundlePkg:
+ answer = "acquired package info from Apple application bundle Info.plist"
case pkg.TerraformPkg:
answer = "acquired package info from Terraform dependency lock file"
case pkg.ModelPkg:
diff --git a/syft/format/internal/spdxutil/helpers/source_info_test.go b/syft/format/internal/spdxutil/helpers/source_info_test.go
index 13338f8c4..e16b6f974 100644
--- a/syft/format/internal/spdxutil/helpers/source_info_test.go
+++ b/syft/format/internal/spdxutil/helpers/source_info_test.go
@@ -343,6 +343,14 @@ func Test_SourceInfo(t *testing.T) {
"acquired package info from Homebrew formula",
},
},
+ {
+ input: pkg.Package{
+ Type: pkg.AppleAppBundlePkg,
+ },
+ expected: []string{
+ "acquired package info from Apple application bundle Info.plist",
+ },
+ },
{
input: pkg.Package{
Type: pkg.TerraformPkg,
diff --git a/syft/pkg/apple.go b/syft/pkg/apple.go
new file mode 100644
index 000000000..63fd01c48
--- /dev/null
+++ b/syft/pkg/apple.go
@@ -0,0 +1,44 @@
+package pkg
+
+// AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files.
+// These bundles share the same plist structure across macOS, iOS, watchOS, visionOS, and others.
+type AppleAppBundleEntry struct {
+ // BundleIdentifier is the unique identifier for the bundle (e.g. "com.apple.Safari")
+ BundleIdentifier string `json:"bundleIdentifier,omitempty"`
+
+ // Name is the short name of the bundle
+ Name string `json:"name,omitempty"`
+
+ // DisplayName is the user-facing name of the bundle
+ DisplayName string `json:"displayName,omitempty"`
+
+ // Executable is the name of the executable within the bundle
+ Executable string `json:"executable,omitempty"`
+
+ // ShortVersion is the release (marketing) version of the bundle
+ ShortVersion string `json:"shortVersion,omitempty"`
+
+ // Version is the build version of the bundle, which often differs from the short version
+ Version string `json:"version,omitempty"`
+
+ // PackageType is the four-letter type code (e.g. "APPL" for apps, "FMWK" for frameworks, "BNDL" for bundles)
+ PackageType string `json:"packageType,omitempty"`
+
+ // SupportedPlatforms lists the platforms the bundle targets (e.g. "MacOSX", "iPhoneOS")
+ SupportedPlatforms []string `json:"supportedPlatforms,omitempty"`
+
+ // MinimumSystemVersion is the minimum macOS version required to run the bundle
+ MinimumSystemVersion string `json:"minimumSystemVersion,omitempty"`
+
+ // MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)
+ MinimumOSVersion string `json:"minimumOSVersion,omitempty"`
+
+ // Copyright is the human-readable copyright notice for the bundle
+ Copyright string `json:"copyright,omitempty"`
+
+ // PlatformName is the platform name of the SDK used to build the bundle (e.g. "macosx")
+ PlatformName string `json:"platformName,omitempty"`
+
+ // SDKName is the name of the SDK used to build the bundle (e.g. "macosx14.0")
+ SDKName string `json:"sdkName,omitempty"`
+}
diff --git a/syft/pkg/cataloger/apple/capabilities.yaml b/syft/pkg/cataloger/apple/capabilities.yaml
new file mode 100644
index 000000000..178a17283
--- /dev/null
+++ b/syft/pkg/cataloger/apple/capabilities.yaml
@@ -0,0 +1,42 @@
+# Cataloger capabilities. See ../README.md for documentation.
+
+catalogers:
+ - ecosystem: apple # MANUAL
+ name: apple-app-bundle-cataloger # AUTO-GENERATED
+ type: generic # AUTO-GENERATED
+ source: # AUTO-GENERATED
+ file: syft/pkg/cataloger/apple/cataloger.go
+ function: NewAppBundleCataloger
+ selectors: # AUTO-GENERATED
+ - apple
+ - directory
+ - image
+ - installed
+ - package
+ parsers: # AUTO-GENERATED structure
+ - function: parseInfoPlist
+ detector: # AUTO-GENERATED
+ method: glob # AUTO-GENERATED
+ criteria: # AUTO-GENERATED
+ - '**/*.app/Contents/Info.plist'
+ metadata_types: # AUTO-GENERATED
+ - pkg.AppleAppBundleEntry
+ package_types: # AUTO-GENERATED
+ - apple-app-bundle
+ json_schema_types: # AUTO-GENERATED
+ - AppleAppBundleEntry
+ capabilities: # MANUAL - preserved across regeneration
+ - name: license
+ default: false
+ - name: dependency.depth
+ default: []
+ - name: dependency.edges
+ default: ""
+ - name: dependency.kinds
+ default: []
+ - name: package_manager.files.listing
+ default: false
+ - name: package_manager.files.digests
+ default: false
+ - name: package_manager.package_integrity_hash
+ default: false
diff --git a/syft/pkg/cataloger/apple/cataloger.go b/syft/pkg/cataloger/apple/cataloger.go
new file mode 100644
index 000000000..c5ca61984
--- /dev/null
+++ b/syft/pkg/cataloger/apple/cataloger.go
@@ -0,0 +1,12 @@
+package apple
+
+import (
+ "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/pkg/cataloger/generic"
+)
+
+// NewAppBundleCataloger returns a cataloger for Apple application bundles (.app), reading their Info.plist (CFBundle).
+func NewAppBundleCataloger() pkg.Cataloger {
+ return generic.NewCataloger("apple-app-bundle-cataloger").
+ WithParserByGlobs(parseInfoPlist, "**/*.app/Contents/Info.plist")
+}
diff --git a/syft/pkg/cataloger/apple/cataloger_test.go b/syft/pkg/cataloger/apple/cataloger_test.go
new file mode 100644
index 000000000..6264e88f9
--- /dev/null
+++ b/syft/pkg/cataloger/apple/cataloger_test.go
@@ -0,0 +1,180 @@
+package apple
+
+import (
+ "context"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "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 Test_AppleAppBundleCataloger_Globs(t *testing.T) {
+ pkgtest.NewCatalogTester().
+ FromDirectory(t, "testdata/install-example").
+ ExpectsResolverContentQueries([]string{
+ "Applications/Slack.app/Contents/Info.plist",
+ }).
+ TestCataloger(t, NewAppBundleCataloger())
+}
+
+func Test_AppleAppBundleCataloger(t *testing.T) {
+ tests := []struct {
+ name string
+ fixture string
+ expected []pkg.Package
+ }{
+ {
+ name: "xml plist (Slack)",
+ fixture: "testdata/install-example",
+ expected: []pkg.Package{
+ {
+ Name: "Slack",
+ Version: "4.50.128",
+ Type: pkg.AppleAppBundlePkg,
+ FoundBy: "apple-app-bundle-cataloger",
+ Locations: file.NewLocationSet(
+ file.NewLocation("Applications/Slack.app/Contents/Info.plist").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ ),
+ PURL: "", // no standard purl type for apple app bundles
+ Metadata: pkg.AppleAppBundleEntry{
+ BundleIdentifier: "com.tinyspeck.slackmacgap",
+ Name: "Slack",
+ DisplayName: "Slack",
+ Executable: "Slack",
+ ShortVersion: "4.50.128",
+ Version: "450000128",
+ PackageType: "APPL",
+ MinimumSystemVersion: "12.0",
+ Copyright: "©2026 Slack Technologies LLC, a Salesforce company. All rights reserved.",
+ SDKName: "macosx15.5",
+ },
+ },
+ },
+ },
+ {
+ // platform/array fields; values are from a real Ghostty.app
+ name: "platform fields (Ghostty)",
+ fixture: "testdata/native-app-example",
+ expected: []pkg.Package{
+ {
+ Name: "Ghostty",
+ Version: "1.3.1",
+ Type: pkg.AppleAppBundlePkg,
+ FoundBy: "apple-app-bundle-cataloger",
+ Locations: file.NewLocationSet(
+ file.NewLocation("Applications/Ghostty.app/Contents/Info.plist").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ ),
+ PURL: "",
+ Metadata: pkg.AppleAppBundleEntry{
+ BundleIdentifier: "com.mitchellh.ghostty",
+ Name: "Ghostty",
+ DisplayName: "Ghostty",
+ Executable: "ghostty",
+ ShortVersion: "1.3.1",
+ Version: "15212",
+ PackageType: "APPL",
+ SupportedPlatforms: []string{"MacOSX"},
+ MinimumSystemVersion: "13.0",
+ PlatformName: "macosx",
+ SDKName: "macosx26.2",
+ },
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ pkgtest.NewCatalogTester().
+ FromDirectory(t, tt.fixture).
+ Expects(tt.expected, nil).
+ TestCataloger(t, NewAppBundleCataloger())
+ })
+ }
+}
+
+func Test_parseInfoPlist(t *testing.T) {
+ tests := []struct {
+ name string
+ fixture string
+ wantName string
+ wantVersion string
+ wantBundleID string
+ wantNoPackage bool
+ wantParseErr bool
+ }{
+ {
+ name: "name falls back to executable",
+ fixture: "testdata/parse-cases/name-from-executable.plist",
+ wantName: "Exec",
+ wantVersion: "1.0",
+ },
+ {
+ name: "name falls back to bundle identifier",
+ fixture: "testdata/parse-cases/name-from-identifier.plist",
+ wantName: "com.example.app",
+ wantVersion: "1.0",
+ wantBundleID: "com.example.app",
+ },
+ {
+ name: "version falls back to bundle version",
+ fixture: "testdata/parse-cases/version-from-bundle-version.plist",
+ wantName: "Name",
+ wantVersion: "42",
+ },
+ {
+ name: "no package without a name",
+ fixture: "testdata/parse-cases/no-name.plist",
+ wantNoPackage: true,
+ },
+ {
+ name: "no package without a version",
+ fixture: "testdata/parse-cases/no-version.plist",
+ wantNoPackage: true,
+ },
+ {
+ name: "malformed plist returns an error",
+ fixture: "testdata/parse-cases/malformed.plist",
+ wantParseErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ pkgs, _, err := parseFixture(t, tt.fixture)
+ if tt.wantParseErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+
+ if tt.wantNoPackage {
+ assert.Empty(t, pkgs)
+ return
+ }
+
+ require.Len(t, pkgs, 1)
+ assert.Equal(t, tt.wantName, pkgs[0].Name)
+ assert.Equal(t, tt.wantVersion, pkgs[0].Version)
+ assert.Equal(t, pkg.AppleAppBundlePkg, pkgs[0].Type)
+ assert.Empty(t, pkgs[0].PURL)
+ meta, ok := pkgs[0].Metadata.(pkg.AppleAppBundleEntry)
+ require.True(t, ok)
+ assert.Equal(t, tt.wantBundleID, meta.BundleIdentifier)
+ })
+ }
+}
+
+func parseFixture(t *testing.T, path string) ([]pkg.Package, []artifact.Relationship, error) {
+ t.Helper()
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = f.Close() })
+ return parseInfoPlist(context.Background(), nil, nil, file.NewLocationReadCloser(file.NewLocation(path), f))
+}
diff --git a/syft/pkg/cataloger/apple/package.go b/syft/pkg/cataloger/apple/package.go
new file mode 100644
index 000000000..57f7a9798
--- /dev/null
+++ b/syft/pkg/cataloger/apple/package.go
@@ -0,0 +1,20 @@
+package apple
+
+import (
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+func newAppBundlePackage(name, version string, metadata pkg.AppleAppBundleEntry, location file.Location) pkg.Package {
+ p := pkg.Package{
+ Name: name,
+ Version: version,
+ Type: pkg.AppleAppBundlePkg,
+ // note: there is no standard purl type for Apple app bundles, so we intentionally leave the PURL empty.
+ Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
+ Metadata: metadata,
+ }
+
+ p.SetID()
+ return p
+}
diff --git a/syft/pkg/cataloger/apple/parse_info_plist.go b/syft/pkg/cataloger/apple/parse_info_plist.go
new file mode 100644
index 000000000..a0e2d7ddf
--- /dev/null
+++ b/syft/pkg/cataloger/apple/parse_info_plist.go
@@ -0,0 +1,91 @@
+package apple
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "howett.net/plist"
+
+ "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"
+)
+
+// infoPlist captures the raw CFBundle keys read from an Info.plist. It is kept separate from the public
+// pkg.AppleAppBundleEntry so the parser can read fields without committing to expressing all of them.
+type infoPlist struct {
+ CFBundleIdentifier string `plist:"CFBundleIdentifier"`
+ CFBundleName string `plist:"CFBundleName"`
+ CFBundleDisplayName string `plist:"CFBundleDisplayName"`
+ CFBundleExecutable string `plist:"CFBundleExecutable"`
+ CFBundleShortVersionString string `plist:"CFBundleShortVersionString"`
+ CFBundleVersion string `plist:"CFBundleVersion"`
+ CFBundlePackageType string `plist:"CFBundlePackageType"`
+ CFBundleSupportedPlatforms []string `plist:"CFBundleSupportedPlatforms"`
+ LSMinimumSystemVersion string `plist:"LSMinimumSystemVersion"`
+ MinimumOSVersion string `plist:"MinimumOSVersion"`
+ NSHumanReadableCopyright string `plist:"NSHumanReadableCopyright"`
+ DTPlatformName string `plist:"DTPlatformName"`
+ DTSDKName string `plist:"DTSDKName"`
+}
+
+func parseInfoPlist(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
+ // 5MB cap matches the convention in syft/linux; Info.plist files are far smaller
+ data, err := io.ReadAll(io.LimitReader(reader, 5*1024*1024))
+ if err != nil {
+ return nil, nil, fmt.Errorf("unable to read file: %w", err)
+ }
+
+ var info infoPlist
+ if _, err := plist.Unmarshal(data, &info); err != nil {
+ return nil, nil, fmt.Errorf("unable to parse plist: %w", err)
+ }
+
+ name := info.CFBundleDisplayName
+ if name == "" {
+ name = info.CFBundleName
+ }
+ if name == "" {
+ name = info.CFBundleExecutable
+ }
+ if name == "" {
+ // last-resort name so identifiable bundles aren't dropped (e.g. com.apple.Safari)
+ name = info.CFBundleIdentifier
+ }
+
+ version := info.CFBundleShortVersionString
+ if version == "" {
+ version = info.CFBundleVersion
+ }
+
+ if name == "" || version == "" {
+ return nil, nil, nil
+ }
+
+ pkgs := []pkg.Package{
+ newAppBundlePackage(name, version, info.toEntry(), reader.Location),
+ }
+
+ return pkgs, nil, nil
+}
+
+// toEntry maps the parsed plist into the public metadata type.
+func (i infoPlist) toEntry() pkg.AppleAppBundleEntry {
+ return pkg.AppleAppBundleEntry{
+ BundleIdentifier: i.CFBundleIdentifier,
+ Name: i.CFBundleName,
+ DisplayName: i.CFBundleDisplayName,
+ Executable: i.CFBundleExecutable,
+ ShortVersion: i.CFBundleShortVersionString,
+ Version: i.CFBundleVersion,
+ PackageType: i.CFBundlePackageType,
+ SupportedPlatforms: i.CFBundleSupportedPlatforms,
+ MinimumSystemVersion: i.LSMinimumSystemVersion,
+ MinimumOSVersion: i.MinimumOSVersion,
+ Copyright: i.NSHumanReadableCopyright,
+ PlatformName: i.DTPlatformName,
+ SDKName: i.DTSDKName,
+ }
+}
diff --git a/syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist b/syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist
new file mode 100644
index 000000000..2e5c78a6b
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/install-example/Applications/Slack.app/Contents/Info.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDisplayName
+ Slack
+ CFBundleName
+ Slack
+ CFBundleExecutable
+ Slack
+ CFBundleIdentifier
+ com.tinyspeck.slackmacgap
+ CFBundleShortVersionString
+ 4.50.128
+ CFBundleVersion
+ 450000128
+ CFBundlePackageType
+ APPL
+ LSMinimumSystemVersion
+ 12.0
+ NSHumanReadableCopyright
+ ©2026 Slack Technologies LLC, a Salesforce company. All rights reserved.
+ DTSDKName
+ macosx15.5
+
+
diff --git a/syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist b/syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist
new file mode 100644
index 000000000..70ad52af5
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/native-app-example/Applications/Ghostty.app/Contents/Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ CFBundleName
+ Ghostty
+ CFBundleDisplayName
+ Ghostty
+ CFBundleExecutable
+ ghostty
+ CFBundleIdentifier
+ com.mitchellh.ghostty
+ CFBundleShortVersionString
+ 1.3.1
+ CFBundleVersion
+ 15212
+ CFBundlePackageType
+ APPL
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ LSMinimumSystemVersion
+ 13.0
+ DTPlatformName
+ macosx
+ DTSDKName
+ macosx26.2
+
+
diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist
new file mode 100644
index 000000000..06dd84d16
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/parse-cases/malformed.plist
@@ -0,0 +1 @@
+this is not a valid plist
diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist
new file mode 100644
index 000000000..eda8f5d08
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-executable.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ CFBundleExecutable
+ Exec
+ CFBundleShortVersionString
+ 1.0
+
+
diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist
new file mode 100644
index 000000000..d68bc8a46
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/parse-cases/name-from-identifier.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ CFBundleIdentifier
+ com.example.app
+ CFBundleShortVersionString
+ 1.0
+
+
diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist
new file mode 100644
index 000000000..a300be381
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/parse-cases/no-name.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ CFBundleShortVersionString
+ 1.0
+
+
diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist
new file mode 100644
index 000000000..e4228731d
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/parse-cases/no-version.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ CFBundleName
+ Name
+
+
diff --git a/syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist b/syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist
new file mode 100644
index 000000000..67076c0e4
--- /dev/null
+++ b/syft/pkg/cataloger/apple/testdata/parse-cases/version-from-bundle-version.plist
@@ -0,0 +1,10 @@
+
+
+
+
+ CFBundleName
+ Name
+ CFBundleVersion
+ 42
+
+
diff --git a/syft/pkg/type.go b/syft/pkg/type.go
index d4abe75a6..b7155bdb6 100644
--- a/syft/pkg/type.go
+++ b/syft/pkg/type.go
@@ -54,6 +54,7 @@ const (
TerraformPkg Type = "terraform"
WordpressPluginPkg Type = "wordpress-plugin"
HomebrewPkg Type = "homebrew"
+ AppleAppBundlePkg Type = "apple-app-bundle"
ModelPkg Type = "model"
)
@@ -99,6 +100,7 @@ var AllPkgs = []Type{
TerraformPkg,
WordpressPluginPkg,
HomebrewPkg,
+ AppleAppBundlePkg,
ModelPkg,
}
diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go
index 580ea5c79..0e4201706 100644
--- a/syft/pkg/type_test.go
+++ b/syft/pkg/type_test.go
@@ -160,8 +160,9 @@ func TestTypeFromPURL(t *testing.T) {
expectedTypes.Remove(string(HomebrewPkg))
expectedTypes.Remove(string(TerraformPkg))
expectedTypes.Remove(string(GraalVMNativeImagePkg))
- expectedTypes.Remove(string(ModelPkg)) // no valid purl for ai artifacts currently
- expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package
+ expectedTypes.Remove(string(ModelPkg)) // no valid purl for ai artifacts currently
+ expectedTypes.Remove(string(AppleAppBundlePkg)) // no standard purl type for apple app bundles
+ expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package
for _, test := range tests {
t.Run(string(test.expected), func(t *testing.T) {
From 656a4d46d7418c1326c4d3648392eb024bb7b461 Mon Sep 17 00:00:00 2001
From: gab <32674884+gabetrau@users.noreply.github.com>
Date: Wed, 1 Jul 2026 08:11:33 -0500
Subject: [PATCH 037/168] Vcpkg Cataloger (#4081)
* Vcpkg cataloger for vcpkg "Manifest Mode"
Find and parse vcpkg-lock.json to get HEAD commit hash
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* just use local vcpkg git repo if it exists, clone it if it doesn't
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* Config opt for git remote clones for vcpkg and README update
Signed-off-by: Gabriel Rau
* Look in vcpkg cache git repo for custom git repos
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* add triplet to metadata and support overlay-ports from config file
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* Add PURL to packages (not sure if this is correct)
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* flatten structs in pkg module and move vcpkg structs to resolver
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* account for overriden versions in toplevel manifest
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* generate json schema for vcpkg metadata
Signed-off-by: Gabriel Rau
* test for basic vcpkg project
dependencies for vcpkg registry to be pulled in
add tree hashes and use correct git hash in builtin-baseline for helloworld test
vcpkg-registry for testing that uses object hashes from syft repo
fix broken tests
Signed-off-by: Gabriel Rau
* formatting
Signed-off-by: Gabriel Rau
* fix static-analysis violations
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* fix integration test failure
Signed-off-by: Gabriel Rau
* remove uneeded files from vcpkg test fixture and use custom registry
Signed-off-by: Gabriel Rau
* change vcpkg registry to anchore one
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* purl spec based on open PR
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* generate-json-schema
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
* rebased and generate json schema 16.0.40
Signed-off-by: Gabriel Rau
* address low hanging fruit
Signed-off-by: Alex Goodman
* handle additional comments
Signed-off-by: Alex Goodman
* migrate to testdata
Signed-off-by: Alex Goodman
* improve docs and testing
Signed-off-by: Alex Goodman
* fix static analysis
Signed-off-by: Alex Goodman
* remove license from pkg metadata
Signed-off-by: Alex Goodman
* fix capabilities claim
Signed-off-by: Alex Goodman
---------
Signed-off-by: Gabriel Rau
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
cmd/syft/internal/options/catalog.go | 6 +
cmd/syft/internal/options/cpp.go | 30 +
.../test/integration/catalog_packages_test.go | 2 +
internal/capabilities/appconfig.yaml | 2 +
internal/constants.go | 4 +-
internal/packagemetadata/generated.go | 1 +
internal/packagemetadata/names.go | 1 +
internal/task/package_tasks.go | 9 +
schema/json/schema-16.1.8.json | 4505 +++++++++++++++++
schema/json/schema-latest.json | 85 +-
syft/cataloging/pkgcataloging/config.go | 8 +
.../spdxutil/helpers/originator_supplier.go | 5 +
.../helpers/originator_supplier_test.go | 1 +
.../internal/spdxutil/helpers/source_info.go | 2 +
.../spdxutil/helpers/source_info_test.go | 8 +
syft/pkg/cataloger/cpp/capabilities.yaml | 52 +
syft/pkg/cataloger/cpp/cataloger.go | 32 +
syft/pkg/cataloger/cpp/cataloger_test.go | 4 +-
syft/pkg/cataloger/cpp/config.go | 19 +
.../cataloger/cpp/internal/vcpkg/resolver.go | 697 +++
syft/pkg/cataloger/cpp/package.go | 68 +-
syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go | 182 +
.../cataloger/cpp/parse_vcpkgmanifest_test.go | 244 +
syft/pkg/cataloger/cpp/testdata/.gitignore | 2 +
syft/pkg/cataloger/cpp/testdata/README.md | 15 +-
.../vcpkg_installed/vcpkg/vcpkg-lock.json | 1 +
.../somewhere/src/vcpkg-configuration.json | 1 +
.../glob-paths/somewhere/src/vcpkg.json | 1 +
.../testdata/vcpkg/helloworld/helloworld.cpp | 7 +
.../vcpkg/helloworld/vcpkg-configuration.json | 7 +
.../cpp/testdata/vcpkg/helloworld/vcpkg.json | 10 +
syft/pkg/type.go | 6 +
syft/pkg/type_test.go | 1 +
syft/pkg/vcpkg.go | 35 +
34 files changed, 6042 insertions(+), 11 deletions(-)
create mode 100644 cmd/syft/internal/options/cpp.go
create mode 100644 schema/json/schema-16.1.8.json
create mode 100644 syft/pkg/cataloger/cpp/config.go
create mode 100644 syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go
create mode 100644 syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go
create mode 100644 syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go
create mode 100644 syft/pkg/cataloger/cpp/testdata/.gitignore
create mode 100644 syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json
create mode 100644 syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json
create mode 100644 syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json
create mode 100644 syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp
create mode 100644 syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json
create mode 100644 syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json
create mode 100644 syft/pkg/vcpkg.go
diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go
index 1b74df358..4ffbfad56 100644
--- a/cmd/syft/internal/options/catalog.go
+++ b/cmd/syft/internal/options/catalog.go
@@ -20,6 +20,7 @@ import (
"github.com/anchore/syft/syft/file/cataloger/executable"
"github.com/anchore/syft/syft/file/cataloger/filecontent"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
+ "github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
@@ -44,6 +45,7 @@ type Catalog struct {
Enrich []string `yaml:"enrich" json:"enrich" mapstructure:"enrich"`
// ecosystem-specific cataloger configuration
+ Cpp cppConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnetConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golangConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
Java javaConfig `yaml:"java" json:"java" mapstructure:"java"`
@@ -80,6 +82,7 @@ func DefaultCatalog() Catalog {
JavaScript: defaultJavaScriptConfig(),
Python: defaultPythonConfig(),
Nix: defaultNixConfig(),
+ Cpp: defaultCppConfig(),
Dotnet: defaultDotnetConfig(),
Golang: defaultGolangConfig(),
Java: defaultJavaConfig(),
@@ -172,6 +175,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
}
return pkgcataloging.Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
+ Cpp: cpp.DefaultCatalogerConfig().
+ WithVcpkgAllowGitClone(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.Cpp, task.Vcpkg), cfg.Cpp.VcpkgAllowGitClone)),
Dotnet: dotnet.DefaultCatalogerConfig().
WithDepPackagesMustHaveDLL(cfg.Dotnet.DepPackagesMustHaveDLL).
WithDepPackagesMustClaimDLL(cfg.Dotnet.DepPackagesMustClaimDLL).
@@ -301,6 +306,7 @@ var publicisedEnrichmentOptions = []string{
task.Java,
task.JavaScript,
task.Python,
+ task.Vcpkg,
}
func enrichmentEnabled(enrichDirectives []string, features ...string) *bool {
diff --git a/cmd/syft/internal/options/cpp.go b/cmd/syft/internal/options/cpp.go
new file mode 100644
index 000000000..84a45047e
--- /dev/null
+++ b/cmd/syft/internal/options/cpp.go
@@ -0,0 +1,30 @@
+package options
+
+import (
+ "github.com/anchore/clio"
+ "github.com/anchore/syft/syft/pkg/cataloger/cpp"
+)
+
+type cppConfig struct {
+ VcpkgAllowGitClone *bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"`
+}
+
+func defaultCppConfig() cppConfig {
+ // reference the cataloger default so capability generation can associate this config with the cpp
+ // ecosystem (it discovers ecosystem configs by their cataloger import). the value itself stays nil:
+ // nil defaults to false (no network), and leaving it unset lets --enrich opt in. cloning requires a
+ // network connection, which must be opt-in.
+ _ = cpp.DefaultCatalogerConfig()
+ return cppConfig{
+ VcpkgAllowGitClone: nil,
+ }
+}
+
+var _ interface {
+ clio.FieldDescriber
+} = (*cppConfig)(nil)
+
+func (o *cppConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
+ descriptions.Add(&o.VcpkgAllowGitClone, `enables Syft to use clone remote repositories for vcpkg custom git registries.
+ (also useful if the builtin vcpkg registry is not cloned locally)`)
+}
diff --git a/cmd/syft/internal/test/integration/catalog_packages_test.go b/cmd/syft/internal/test/integration/catalog_packages_test.go
index 9abbb338d..5816c1999 100644
--- a/cmd/syft/internal/test/integration/catalog_packages_test.go
+++ b/cmd/syft/internal/test/integration/catalog_packages_test.go
@@ -89,6 +89,7 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // we have coverage for pear instead
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.ModelPkg))
+ definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
var cases []testCase
@@ -165,6 +166,7 @@ func TestPkgCoverageDirectory(t *testing.T) {
definedPkgs.Remove(string(pkg.CondaPkg))
definedPkgs.Remove(string(pkg.PhpPeclPkg)) // this is covered as pear packages
definedPkgs.Remove(string(pkg.ModelPkg))
+ definedPkgs.Remove(string(pkg.VcpkgPkg))
definedPkgs.Remove(string(pkg.AppleAppBundlePkg))
// for directory scans we should not expect to see any of the following package types
diff --git a/internal/capabilities/appconfig.yaml b/internal/capabilities/appconfig.yaml
index b42a8ee29..e073dd32c 100644
--- a/internal/capabilities/appconfig.yaml
+++ b/internal/capabilities/appconfig.yaml
@@ -2,6 +2,8 @@
# This file is partially auto-generated. Run 'go generate ./internal/capabilities' to regenerate.
application: # AUTO-GENERATED - application-level config keys
+ - key: cpp.vcpkg-allow-git-clone
+ description: enables Syft to use clone remote repositories for vcpkg custom git registries. (also useful if the builtin vcpkg registry is not cloned locally)
- key: dotnet.dep-packages-must-claim-dll
description: only keep dep.json packages which have a runtime/resource DLL claimed in the deps.json targets section (but not necessarily found on disk). The package is also included if any child package claims a DLL, even if the package itself does not claim a DLL.
- key: dotnet.dep-packages-must-have-dll
diff --git a/internal/constants.go b/internal/constants.go
index 3f27e106c..0efb094e2 100644
--- a/internal/constants.go
+++ b/internal/constants.go
@@ -3,7 +3,7 @@ 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.1.7"
+ JSONSchemaVersion = "16.1.8"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@@ -14,5 +14,5 @@ const (
// 16.1.5 - add DenoLockEntry and DenoRemoteLockEntry metadata types for deno.lock support
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
-
+ // 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
)
diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go
index 1d39a8288..78fe51347 100644
--- a/internal/packagemetadata/generated.go
+++ b/internal/packagemetadata/generated.go
@@ -72,6 +72,7 @@ func AllTypes() []any {
pkg.SwiftPackageManagerResolvedEntry{},
pkg.SwiplPackEntry{},
pkg.TerraformLockProviderEntry{},
+ pkg.VcpkgManifest{},
pkg.WordpressPluginEntry{},
pkg.YarnLockEntry{},
}
diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go
index 408667974..e4370a897 100644
--- a/internal/packagemetadata/names.go
+++ b/internal/packagemetadata/names.go
@@ -130,6 +130,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"),
jsonNames(pkg.CondaMetaPackage{}, "conda-metadata-entry", "CondaPackageMetadata"),
jsonNames(pkg.GGUFFileHeader{}, "gguf-file-header"),
+ jsonNames(pkg.VcpkgManifest{}, "vcpkg-manifest"),
)
func expandLegacyNameVariants(names ...string) []string {
diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go
index 71e9e3af6..fa6e3a950 100644
--- a/internal/task/package_tasks.go
+++ b/internal/task/package_tasks.go
@@ -57,6 +57,10 @@ const (
// Python ecosystem labels
Python = "python"
+
+ // C/C++ ecosystem labels
+ Cpp = "cpp"
+ Vcpkg = "vcpkg"
)
//nolint:funlen
@@ -83,6 +87,11 @@ func DefaultPackageTaskFactories() Factories {
// language-specific package declared catalogers ///////////////////////////////////////////////////////////////////////////
newSimplePackageTaskFactory(cpp.NewConanCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "cpp", "conan"),
+ newPackageTaskFactory(
+ func(cfg CatalogingFactoryConfig) pkg.Cataloger {
+ return cpp.NewVcpkgManifestCataloger(cfg.PackagesConfig.Cpp)
+ },
+ pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, Cpp, Vcpkg),
newSimplePackageTaskFactory(dart.NewPubspecLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(dart.NewPubspecCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dart"),
newSimplePackageTaskFactory(elixir.NewMixLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "elixir"),
diff --git a/schema/json/schema-16.1.8.json b/schema/json/schema-16.1.8.json
new file mode 100644
index 000000000..1fc811850
--- /dev/null
+++ b/schema/json/schema-16.1.8.json
@@ -0,0 +1,4505 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/syft/json/16.1.8/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",
+ "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman."
+ },
+ "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)."
+ },
+ "AppleAppBundleEntry": {
+ "properties": {
+ "bundleIdentifier": {
+ "type": "string",
+ "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the short name of the bundle"
+ },
+ "displayName": {
+ "type": "string",
+ "description": "DisplayName is the user-facing name of the bundle"
+ },
+ "executable": {
+ "type": "string",
+ "description": "Executable is the name of the executable within the bundle"
+ },
+ "shortVersion": {
+ "type": "string",
+ "description": "ShortVersion is the release (marketing) version of the bundle"
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the build version of the bundle, which often differs from the short version"
+ },
+ "packageType": {
+ "type": "string",
+ "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)"
+ },
+ "supportedPlatforms": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")"
+ },
+ "minimumSystemVersion": {
+ "type": "string",
+ "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle"
+ },
+ "minimumOSVersion": {
+ "type": "string",
+ "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)"
+ },
+ "copyright": {
+ "type": "string",
+ "description": "Copyright is the human-readable copyright notice for the bundle"
+ },
+ "platformName": {
+ "type": "string",
+ "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")"
+ },
+ "sdkName": {
+ "type": "string",
+ "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")"
+ }
+ },
+ "type": "object",
+ "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files."
+ },
+ "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",
+ "description": "Value is the CPE string identifier."
+ },
+ "source": {
+ "type": "string",
+ "description": "Source is the source where this CPE was obtained or generated from."
+ }
+ },
+ "type": "object",
+ "required": [
+ "cpe"
+ ],
+ "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases."
+ },
+ "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."
+ },
+ "DenoLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies is the list of package specifiers that this package depends on"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies"
+ ],
+ "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file"
+ },
+ "DenoRemoteLockEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the remote URL from which the module fetcef"
+ },
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "integrity"
+ ],
+ "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file"
+ },
+ "Descriptor": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the tool that generated this SBOM."
+ },
+ "configuration": {
+ "description": "Configuration contains the tool configuration used during SBOM generation."
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "version"
+ ],
+ "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation."
+ },
+ "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",
+ "description": "Artifacts is the list of packages discovered and placed into the catalog"
+ },
+ "artifactRelationships": {
+ "items": {
+ "$ref": "#/$defs/Relationship"
+ },
+ "type": "array"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/File"
+ },
+ "type": "array",
+ "description": "note: must have omitempty"
+ },
+ "source": {
+ "$ref": "#/$defs/Source",
+ "description": "Source represents the original object that was cataloged"
+ },
+ "distro": {
+ "$ref": "#/$defs/LinuxRelease",
+ "description": "Distro represents the Linux distribution that was detected from the source"
+ },
+ "descriptor": {
+ "$ref": "#/$defs/Descriptor",
+ "description": "Descriptor is a block containing self-describing information about syft"
+ },
+ "schema": {
+ "$ref": "#/$defs/Schema",
+ "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape"
+ }
+ },
+ "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\")"
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is type of entry could be package or project for internal refs"
+ },
+ "executables": {
+ "additionalProperties": {
+ "$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)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing."
+ },
+ "appCpe": {
+ "type": "string",
+ "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)"
+ },
+ "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)"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages."
+ }
+ },
+ "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",
+ "description": "ID is a unique identifier for this file within the SBOM."
+ },
+ "location": {
+ "$ref": "#/$defs/Coordinates",
+ "description": "Location is the file path and layer information where this file was found."
+ },
+ "metadata": {
+ "$ref": "#/$defs/FileMetadataEntry",
+ "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the file contents for small files."
+ },
+ "digests": {
+ "items": {
+ "$ref": "#/$defs/Digest"
+ },
+ "type": "array",
+ "description": "Digests contains cryptographic hashes of the file contents."
+ },
+ "licenses": {
+ "items": {
+ "$ref": "#/$defs/FileLicense"
+ },
+ "type": "array",
+ "description": "Licenses contains license information discovered within this file."
+ },
+ "executable": {
+ "$ref": "#/$defs/Executable",
+ "description": "Executable contains executable metadata if this file is a binary."
+ },
+ "unknowns": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Unknowns contains unknown fields for forward compatibility."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "location"
+ ],
+ "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages."
+ },
+ "FileLicense": {
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Value is the raw license identifier or text as found in the file."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "evidence": {
+ "$ref": "#/$defs/FileLicenseEvidence",
+ "description": "Evidence contains supporting evidence for this license detection."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type"
+ ],
+ "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression."
+ },
+ "FileLicenseEvidence": {
+ "properties": {
+ "confidence": {
+ "type": "integer",
+ "description": "Confidence is the confidence score for this license detection (0-100)."
+ },
+ "offset": {
+ "type": "integer",
+ "description": "Offset is the byte offset where the license text starts in the file."
+ },
+ "extent": {
+ "type": "integer",
+ "description": "Extent is the length of the license text in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "confidence",
+ "offset",
+ "extent"
+ ],
+ "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level."
+ },
+ "FileMetadataEntry": {
+ "properties": {
+ "mode": {
+ "type": "integer",
+ "description": "Mode is the Unix file permission mode in octal format."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")."
+ },
+ "linkDestination": {
+ "type": "string",
+ "description": "LinkDestination is the target path for symbolic links."
+ },
+ "userID": {
+ "type": "integer",
+ "description": "UserID is the file owner user ID."
+ },
+ "groupID": {
+ "type": "integer",
+ "description": "GroupID is the file owner group ID."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIMEType is the MIME type of the file contents."
+ },
+ "size": {
+ "type": "integer",
+ "description": "Size is the file size in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "mode",
+ "type",
+ "userID",
+ "groupID",
+ "mimeType",
+ "size"
+ ],
+ "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file."
+ },
+ "GgufFileHeader": {
+ "properties": {
+ "ggufVersion": {
+ "type": "integer",
+ "description": "GGUFVersion is the GGUF format version (e.g., 3)"
+ },
+ "fileSize": {
+ "type": "integer",
+ "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)"
+ },
+ "architecture": {
+ "type": "string",
+ "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")"
+ },
+ "quantization": {
+ "type": "string",
+ "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")"
+ },
+ "parameters": {
+ "type": "integer",
+ "description": "Parameters is the number of model parameters (if present in header)"
+ },
+ "tensorCount": {
+ "type": "integer",
+ "description": "TensorCount is the number of tensors in the model"
+ },
+ "header": {
+ "type": "object",
+ "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication."
+ },
+ "metadataHash": {
+ "type": "string",
+ "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames."
+ },
+ "parts": {
+ "items": {
+ "$ref": "#/$defs/GgufFileHeader"
+ },
+ "type": "array",
+ "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)."
+ }
+ },
+ "type": "object",
+ "required": [
+ "ggufVersion",
+ "tensorCount"
+ ],
+ "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file."
+ },
+ "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",
+ "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field."
+ },
+ "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": {
+ "additionalProperties": {
+ "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."
+ },
+ "JavascriptBunLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of runtime dependencies and their version specifiers"
+ },
+ "optionalDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "OptionalDependencies is a map of optional dependencies and their version specifiers"
+ },
+ "peerDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "PeerDependencies is a map of peer dependencies and their version specifiers"
+ },
+ "bin": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Bin is a map of binary names to the paths they are installed to"
+ },
+ "os": {
+ "type": "string",
+ "description": "OS is the operating system constraint for the package (e.g. \"darwin\")"
+ },
+ "cpu": {
+ "type": "string",
+ "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies",
+ "optionalDependencies",
+ "peerDependencies",
+ "bin",
+ "os",
+ "cpu"
+ ],
+ "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file"
+ },
+ "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."
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\""
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file."
+ },
+ "JavascriptPnpmLockEntry": {
+ "properties": {
+ "resolution": {
+ "$ref": "#/$defs/PnpmLockResolution",
+ "description": "Resolution is the resolution information for the package"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolution",
+ "dependencies"
+ ],
+ "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml 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)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "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",
+ "description": "Value is the raw license identifier or expression as found."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "urls": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "URLs are URLs where license text or information can be found."
+ },
+ "locations": {
+ "items": {
+ "$ref": "#/$defs/Location"
+ },
+ "type": "array",
+ "description": "Locations are file locations where this license was discovered."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the full license text content."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type",
+ "urls",
+ "locations"
+ ],
+ "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence 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": {
+ "additionalProperties": {
+ "$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",
+ "description": "PrettyName is a human-readable operating system name with version."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the operating system name without version information."
+ },
+ "id": {
+ "type": "string",
+ "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")."
+ },
+ "idLike": {
+ "$ref": "#/$defs/IDLikes",
+ "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the operating system version including codename if available."
+ },
+ "versionID": {
+ "type": "string",
+ "description": "VersionID is the operating system version number or identifier."
+ },
+ "versionCodename": {
+ "type": "string",
+ "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")."
+ },
+ "buildID": {
+ "type": "string",
+ "description": "BuildID is a build identifier for the operating system."
+ },
+ "imageID": {
+ "type": "string",
+ "description": "ImageID is an identifier for container or cloud images."
+ },
+ "imageVersion": {
+ "type": "string",
+ "description": "ImageVersion is the version for container or cloud images."
+ },
+ "variant": {
+ "type": "string",
+ "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")."
+ },
+ "variantID": {
+ "type": "string",
+ "description": "VariantID is the lower-case operating system variant identifier."
+ },
+ "homeURL": {
+ "type": "string",
+ "description": "HomeURL is the homepage URL for the operating system."
+ },
+ "supportURL": {
+ "type": "string",
+ "description": "SupportURL is the support or help URL for the operating system."
+ },
+ "bugReportURL": {
+ "type": "string",
+ "description": "BugReportURL is the bug reporting URL for the operating system."
+ },
+ "privacyPolicyURL": {
+ "type": "string",
+ "description": "PrivacyPolicyURL is the privacy policy URL for the operating system."
+ },
+ "cpeName": {
+ "type": "string",
+ "description": "CPEName is the Common Platform Enumeration name for the operating system."
+ },
+ "supportEnd": {
+ "type": "string",
+ "description": "SupportEnd is the end of support date or version identifier."
+ },
+ "extendedSupport": {
+ "type": "boolean",
+ "description": "ExtendedSupport indicates whether extended security or support is available."
+ }
+ },
+ "type": "object",
+ "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files."
+ },
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Arbitrary key-value pairs that can be used to annotate a location"
+ }
+ },
+ "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": {
+ "additionalProperties": {
+ "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 represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)."
+ },
+ "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/AppleAppBundleEntry"
+ },
+ {
+ "$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/DenoLockEntry"
+ },
+ {
+ "$ref": "#/$defs/DenoRemoteLockEntry"
+ },
+ {
+ "$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/GgufFileHeader"
+ },
+ {
+ "$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/JavascriptBunLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackage"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackageLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptPnpmLockEntry"
+ },
+ {
+ "$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/VcpkgManifest"
+ },
+ {
+ "$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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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."
+ },
+ "PnpmLockResolution": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity"
+ ],
+ "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification."
+ },
+ "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/)."
+ },
+ "PythonPdmFileEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the file download URL"
+ },
+ "digest": {
+ "$ref": "#/$defs/PythonFileDigest",
+ "description": "Digest is the hash digest of the file hosted at the URL"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "digest"
+ ]
+ },
+ "PythonPdmLockEntry": {
+ "properties": {
+ "summary": {
+ "type": "string",
+ "description": "Summary provides a description of the package"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files with their paths and hash digests (for the base package without extras)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment"
+ },
+ "requiresPython": {
+ "type": "string",
+ "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")."
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependency specifications for the base package (without extras)"
+ },
+ "extras": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmLockExtraVariant"
+ },
+ "type": "array",
+ "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "summary",
+ "files"
+ ],
+ "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file."
+ },
+ "PythonPdmLockExtraVariant": {
+ "properties": {
+ "extras": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependencies specific to this extras variant"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files specific to this variant (only populated if different from base)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "extras"
+ ],
+ "description": "PythonPdmLockExtraVariant represents a specific extras combination variant 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",
+ "description": "Parent is the ID of the parent artifact in this relationship."
+ },
+ "child": {
+ "type": "string",
+ "description": "Child is the ID of the child artifact in this relationship."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional relationship-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "parent",
+ "child",
+ "type"
+ ],
+ "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package."
+ },
+ "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",
+ "description": "Version is the JSON schema version for this document format."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL is the URL to the JSON schema definition document."
+ }
+ },
+ "type": "object",
+ "required": [
+ "version",
+ "url"
+ ],
+ "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format."
+ },
+ "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",
+ "description": "ID is a unique identifier for the analyzed source artifact."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the analyzed artifact (e.g., image tag)."
+ },
+ "supplier": {
+ "type": "string",
+ "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional source-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "type",
+ "metadata"
+ ],
+ "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive."
+ },
+ "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)."
+ },
+ "VcpkgManifest": {
+ "properties": {
+ "description": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "documentation": {
+ "type": "string"
+ },
+ "full-version": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "port-version": {
+ "type": "integer"
+ },
+ "maintainers": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "name": {
+ "type": "string"
+ },
+ "supports": {
+ "type": "string"
+ },
+ "registry": {
+ "$ref": "#/$defs/VcpkgRegistryEntry",
+ "description": "to show where it came from"
+ },
+ "triplet": {
+ "type": "string",
+ "description": "found by looking at build folder to find target. ex. \"x64-linux\""
+ }
+ },
+ "type": "object",
+ "required": [
+ "full-version",
+ "version",
+ "port-version",
+ "name"
+ ],
+ "description": "used for metadata."
+ },
+ "VcpkgRegistryEntry": {
+ "properties": {
+ "baseline": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "packages": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "path": {
+ "type": "string"
+ },
+ "reference": {
+ "type": "string"
+ },
+ "repository": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "description": "Matches definition of Vcpkg \"Registry\"."
+ },
+ "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 71e1dd2ff..1fc811850 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.1.7/document",
+ "$id": "anchore.io/schema/syft/json/16.1.8/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -2927,6 +2927,9 @@
{
"$ref": "#/$defs/TerraformLockProviderEntry"
},
+ {
+ "$ref": "#/$defs/VcpkgManifest"
+ },
{
"$ref": "#/$defs/WordpressPluginEntry"
}
@@ -4385,6 +4388,86 @@
],
"description": "TerraformLockProviderEntry represents a single provider entry in a Terraform dependency lock file (.terraform.lock.hcl)."
},
+ "VcpkgManifest": {
+ "properties": {
+ "description": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "documentation": {
+ "type": "string"
+ },
+ "full-version": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "port-version": {
+ "type": "integer"
+ },
+ "maintainers": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "name": {
+ "type": "string"
+ },
+ "supports": {
+ "type": "string"
+ },
+ "registry": {
+ "$ref": "#/$defs/VcpkgRegistryEntry",
+ "description": "to show where it came from"
+ },
+ "triplet": {
+ "type": "string",
+ "description": "found by looking at build folder to find target. ex. \"x64-linux\""
+ }
+ },
+ "type": "object",
+ "required": [
+ "full-version",
+ "version",
+ "port-version",
+ "name"
+ ],
+ "description": "used for metadata."
+ },
+ "VcpkgRegistryEntry": {
+ "properties": {
+ "baseline": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "packages": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "path": {
+ "type": "string"
+ },
+ "reference": {
+ "type": "string"
+ },
+ "repository": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "description": "Matches definition of Vcpkg \"Registry\"."
+ },
"WordpressPluginEntry": {
"properties": {
"pluginInstallDirectory": {
diff --git a/syft/cataloging/pkgcataloging/config.go b/syft/cataloging/pkgcataloging/config.go
index e5bac0cb8..4a1e2cff6 100644
--- a/syft/cataloging/pkgcataloging/config.go
+++ b/syft/cataloging/pkgcataloging/config.go
@@ -2,6 +2,7 @@ package pkgcataloging
import (
"github.com/anchore/syft/syft/pkg/cataloger/binary"
+ "github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java"
@@ -13,6 +14,7 @@ import (
type Config struct {
Binary binary.ClassifierCatalogerConfig `yaml:"binary" json:"binary" mapstructure:"binary"`
+ Cpp cpp.CatalogerConfig `yaml:"cpp" json:"cpp" mapstructure:"cpp"`
Dotnet dotnet.CatalogerConfig `yaml:"dotnet" json:"dotnet" mapstructure:"dotnet"`
Golang golang.CatalogerConfig `yaml:"golang" json:"golang" mapstructure:"golang"`
JavaArchive java.ArchiveCatalogerConfig `yaml:"java-archive" json:"java-archive" mapstructure:"java-archive"`
@@ -25,6 +27,7 @@ type Config struct {
func DefaultConfig() Config {
return Config{
Binary: binary.DefaultClassifierCatalogerConfig(),
+ Cpp: cpp.DefaultCatalogerConfig(),
Dotnet: dotnet.DefaultCatalogerConfig(),
Golang: golang.DefaultCatalogerConfig(),
JavaArchive: java.DefaultArchiveCatalogerConfig(),
@@ -44,6 +47,11 @@ func (c Config) WithBinaryConfig(cfg binary.ClassifierCatalogerConfig) Config {
return c
}
+func (c Config) WithCppConfig(cfg cpp.CatalogerConfig) Config {
+ c.Cpp = cfg
+ return c
+}
+
func (c Config) WithDotnetConfig(cfg dotnet.CatalogerConfig) Config {
c.Dotnet = cfg
return c
diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier.go b/syft/format/internal/spdxutil/helpers/originator_supplier.go
index e5d4c183c..8452d951a 100644
--- a/syft/format/internal/spdxutil/helpers/originator_supplier.go
+++ b/syft/format/internal/spdxutil/helpers/originator_supplier.go
@@ -138,6 +138,11 @@ func Originator(p pkg.Package) (typ string, author string) { //nolint: gocyclo,f
case pkg.SwiplPackEntry:
author = formatPersonOrOrg(metadata.Author, metadata.AuthorEmail)
+
+ case pkg.VcpkgManifest:
+ if len(metadata.Maintainers) > 0 {
+ author = metadata.Maintainers[0]
+ }
}
if typ == "" && author != "" {
diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
index d18075450..dec9d06f0 100644
--- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
+++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
@@ -60,6 +60,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.GGUFFileHeader{},
pkg.DenoLockEntry{},
pkg.DenoRemoteLockEntry{},
+ pkg.VcpkgManifest{},
)
tests := []struct {
name string
diff --git a/syft/format/internal/spdxutil/helpers/source_info.go b/syft/format/internal/spdxutil/helpers/source_info.go
index 267703ab0..5682e22da 100644
--- a/syft/format/internal/spdxutil/helpers/source_info.go
+++ b/syft/format/internal/spdxutil/helpers/source_info.go
@@ -86,6 +86,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from Terraform dependency lock file"
case pkg.ModelPkg:
answer = "acquired package info from AI artifact (e.g. GGUF File)"
+ case pkg.VcpkgPkg:
+ answer = "acquired package info from vcpkg manifest file"
default:
answer = "acquired package info from the following paths"
}
diff --git a/syft/format/internal/spdxutil/helpers/source_info_test.go b/syft/format/internal/spdxutil/helpers/source_info_test.go
index e16b6f974..8757258ff 100644
--- a/syft/format/internal/spdxutil/helpers/source_info_test.go
+++ b/syft/format/internal/spdxutil/helpers/source_info_test.go
@@ -199,6 +199,14 @@ func Test_SourceInfo(t *testing.T) {
"from conda metadata",
},
},
+ {
+ input: pkg.Package{
+ Type: pkg.VcpkgPkg,
+ },
+ expected: []string{
+ "from vcpkg manifest",
+ },
+ },
{
input: pkg.Package{
Type: pkg.PortagePkg,
diff --git a/syft/pkg/cataloger/cpp/capabilities.yaml b/syft/pkg/cataloger/cpp/capabilities.yaml
index 2f27f4d7f..802646b75 100644
--- a/syft/pkg/cataloger/cpp/capabilities.yaml
+++ b/syft/pkg/cataloger/cpp/capabilities.yaml
@@ -1,5 +1,11 @@
# Cataloger capabilities. See ../README.md for documentation.
+configs: # AUTO-GENERATED - config structs and their fields
+ cpp.CatalogerConfig:
+ fields:
+ - key: VcpkgAllowGitClone
+ description: VcpkgAllowGitClone enables cloning remote git registries to resolve vcpkg manifest dependencies (requires network access).
+ app_key: cpp.vcpkg-allow-git-clone
catalogers:
- ecosystem: c++ # MANUAL
name: conan-cataloger # AUTO-GENERATED
@@ -126,3 +132,49 @@ catalogers:
default: false
- name: package_manager.package_integrity_hash
default: false
+ - ecosystem: c++ # MANUAL
+ name: vcpkg-manifest-cataloger # AUTO-GENERATED
+ type: generic # AUTO-GENERATED
+ source: # AUTO-GENERATED
+ file: syft/pkg/cataloger/cpp/cataloger.go
+ function: NewVcpkgManifestCataloger
+ config: cpp.CatalogerConfig # AUTO-GENERATED
+ selectors: # AUTO-GENERATED
+ - cpp
+ - declared
+ - directory
+ - language
+ - package
+ - vcpkg
+ parsers: # AUTO-GENERATED structure
+ - function: parseVcpkgManifest
+ detector: # AUTO-GENERATED
+ method: glob # AUTO-GENERATED
+ criteria: # AUTO-GENERATED
+ - '**/vcpkg.json'
+ metadata_types: # AUTO-GENERATED
+ - pkg.VcpkgManifest
+ package_types: # AUTO-GENERATED
+ - vcpkg
+ purl_types: # AUTO-GENERATED
+ - vcpkg
+ json_schema_types: # AUTO-GENERATED
+ - VcpkgManifest
+ capabilities: # MANUAL - preserved across regeneration
+ - name: license
+ default: true
+ - name: dependency.depth
+ default:
+ - direct
+ - indirect
+ - name: dependency.edges
+ default: complete
+ - name: dependency.kinds
+ default:
+ - runtime
+ - name: package_manager.files.listing
+ default: false
+ - name: package_manager.files.digests
+ default: false
+ - name: package_manager.package_integrity_hash
+ default: false
diff --git a/syft/pkg/cataloger/cpp/cataloger.go b/syft/pkg/cataloger/cpp/cataloger.go
index aa031204e..c4d2565d0 100644
--- a/syft/pkg/cataloger/cpp/cataloger.go
+++ b/syft/pkg/cataloger/cpp/cataloger.go
@@ -20,3 +20,35 @@ func NewConanInfoCataloger() pkg.Cataloger {
return generic.NewCataloger("conan-info-cataloger").
WithParserByGlobs(parseConaninfo, "**/conaninfo.txt")
}
+
+// vcpkg (the Microsoft C/C++ package manager) has two modes
+// (https://learn.microsoft.com/en-us/vcpkg/concepts/classic-mode):
+// - classic mode: `vcpkg install ` populates a central tree at $VCPKG_ROOT/installed/.
+// - manifest mode: a vcpkg.json declares dependencies; after a build the resolved tree appears
+// under vcpkg_installed/ (a vcpkg/status DB + per-triplet dirs).
+//
+// what is on disk depends on where in the lifecycle the scan happens:
+// - source checkout (the dir-scan case): vcpkg.json (+ vcpkg-configuration.json, overlay ports)
+// are present. vcpkg_installed/ is a build artifact and is gitignored, so it is NOT present;
+// exact transitive versions live in the registry, not the manifest.
+// - built artifact: vcpkg_installed/ holds the actually-installed truth.
+//
+// NewVcpkgManifestCataloger covers ONLY the manifest (dir/source) case: it reads vcpkg.json and its
+// declared dependencies and resolves each dependency's manifest from the vcpkg registry. resolving the
+// registry needs a local registry clone or a network clone, which is opt-in via
+// CatalogerConfig.VcpkgAllowGitClone (wired to --enrich).
+//
+// it deliberately does NOT cover installed state: vcpkg_installed/ (vcpkg/status, per-package
+// vcpkg.spdx.json, copyright, ABI info), the build triplet (a build-time choice recorded only under
+// vcpkg_installed/), and classic-mode central installs.
+//
+// why there is no installed-state (vcpkg/status) cataloger yet: vcpkg_installed/ tends to be a gitignored
+// build artifact, so whether it survives into a scannable target is pattern-dependent. it is dropped by slim
+// multi-stage runtime images (the documented best practice copies only the app binary / binary cache
+// between stages) and by dev/base images (which bootstrap the tool, not packages); no canonical public
+// image ships a populated vcpkg/status by default. it is retained mainly in single-stage builder / CI /
+// "fat" app images. so a status-based cataloger has real but not universal payoff and is deferred until
+// it is worth the maintenance.
+func NewVcpkgManifestCataloger(opts CatalogerConfig) pkg.Cataloger {
+ return generic.NewCataloger("vcpkg-manifest-cataloger").WithParserByGlobs(newVcpkgCataloger(opts.VcpkgAllowGitClone).parseVcpkgManifest, "**/vcpkg.json")
+}
diff --git a/syft/pkg/cataloger/cpp/cataloger_test.go b/syft/pkg/cataloger/cpp/cataloger_test.go
index 99b4543c9..cd34d7c4c 100644
--- a/syft/pkg/cataloger/cpp/cataloger_test.go
+++ b/syft/pkg/cataloger/cpp/cataloger_test.go
@@ -6,7 +6,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
-func TestCataloger_Globs(t *testing.T) {
+func TestCatalogerConan_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
@@ -32,7 +32,7 @@ func TestCataloger_Globs(t *testing.T) {
}
}
-func TestCatalogerInfo_Globs(t *testing.T) {
+func TestCatalogerConanInfo_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
diff --git a/syft/pkg/cataloger/cpp/config.go b/syft/pkg/cataloger/cpp/config.go
new file mode 100644
index 000000000..6568d9a25
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/config.go
@@ -0,0 +1,19 @@
+package cpp
+
+type CatalogerConfig struct {
+ // VcpkgAllowGitClone enables cloning remote git registries to resolve vcpkg manifest dependencies (requires network access).
+ // app-config: cpp.vcpkg-allow-git-clone
+ VcpkgAllowGitClone bool `yaml:"vcpkg-allow-git-clone" json:"vcpkg-allow-git-clone" mapstructure:"vcpkg-allow-git-clone"`
+}
+
+func DefaultCatalogerConfig() CatalogerConfig {
+ return CatalogerConfig{
+ // syft defaults to not sending requests over the network. You must opt in
+ VcpkgAllowGitClone: false,
+ }
+}
+
+func (c CatalogerConfig) WithVcpkgAllowGitClone(input bool) CatalogerConfig {
+ c.VcpkgAllowGitClone = input
+ return c
+}
diff --git a/syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go b/syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go
new file mode 100644
index 000000000..302b49850
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/internal/vcpkg/resolver.go
@@ -0,0 +1,697 @@
+package vcpkg
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "slices"
+ "strconv"
+ "strings"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/go-git/go-git/v5/storage"
+ "github.com/go-git/go-git/v5/storage/memory"
+
+ "github.com/anchore/syft/internal/cache"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// this is the default registry for vcpkg. it is the default "builtin" registry if a builtin one isn't specified
+var defaultRegistry = pkg.VcpkgRegistryEntry{
+ Baseline: "master",
+ Kind: pkg.Git,
+ Repository: "https://github.com/microsoft/vcpkg",
+}
+
+// represents contents of "vcpkg.json" file. (a.k.a the manifest file)
+type Vcpkg struct {
+ BuiltinBaseline string `json:"builtin-baseline,omitempty"`
+ DefaultFeatures []any `json:"default-features,omitempty"`
+ // string or []VcpkgDependency
+ Dependencies []any `json:"dependencies,omitempty"`
+ // string or []string
+ Description any `json:"description,omitempty"`
+ Documentation string `json:"documentation,omitempty"`
+ Features map[string]vcpkgFeatureEntry `json:"features,omitempty"`
+ Homepage string `json:"homepage,omitempty"`
+ // In SPDX license expression format. see https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json
+ License string `json:"license,omitempty"`
+ Maintainers []string `json:"maintainers,omitempty"`
+ Name string `json:"name,omitempty"`
+ // Only overrides defined by the top-level project are used
+ Overrides []vcpkgOverrideEntry `json:"overrides,omitempty"`
+ PortVersion float64 `json:"port-version,omitempty"`
+ Supports string `json:"supports,omitempty"`
+ // at most one of these version fields will be present and represent different versioning strategies
+ // see https://learn.microsoft.com/en-us/vcpkg/users/versioning#version-schemes for more details
+ Version string `json:"version,omitempty"`
+ VersionSemver string `json:"version-semver,omitempty"`
+ VersionDate string `json:"version-date,omitempty"`
+ VersionString string `json:"version-string,omitempty"`
+}
+
+// Confusingly not the same as Feature Object
+// see https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json#feature vs https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-json#feature-object
+type vcpkgFeatureEntry struct {
+ Description string `json:"description"`
+ // string or []VcpkgDependency
+ Dependencies []any `json:"dependencies,omitempty"`
+ // "Platform expression"
+ Supports string `json:"supports,omitempty"`
+ // SPDX license expression
+ License string `json:"license,omitempty"`
+}
+
+type vcpkgOverrideEntry struct {
+ Name string `json:"name"`
+ Version string `json:"version,omitempty"`
+ VersionSemver string `json:"version-semver,omitempty"`
+ VersionDate string `json:"version-date,omitempty"`
+ VersionString string `json:"version-string,omitempty"`
+ PortVersion float64 `json:"port-version,omitempty"`
+}
+
+// represents contents of "vcpkg-configuration.json" file
+type Config struct {
+ DefaultRegistry *pkg.VcpkgRegistryEntry `json:"default-registry"`
+ OverlayPorts []string `json:"overlay-ports,omitempty"`
+ OverlayTriplets []string `json:"overlay-triplets,omitempty"`
+ Registries []pkg.VcpkgRegistryEntry `json:"registries,omitempty"`
+}
+
+// used to get specific dependency from git history
+type vcpkgGitVersionObjectEntry struct {
+ // Sha1 value used to retrieve specific git tree object from Github. https://docs.github.com/en/rest/git/trees?apiVersion=2022-11-28
+ GitTree string `json:"git-tree"`
+ Version string `json:"version,omitempty"`
+ VersionSemver string `json:"version-semver,omitempty"`
+ VersionDate string `json:"version-date,omitempty"`
+ VersionString string `json:"version-string,omitempty"`
+ PortVersion float64 `json:"port-version"`
+}
+
+// represents versions file ".json" found in versions folder
+type vcpkgGitVersions struct {
+ Versions []vcpkgGitVersionObjectEntry `json:"versions"`
+}
+
+// Filesystem VersionObject
+type vcpkgFsVersionObjectEntry struct {
+ Path string `json:"path"`
+ Version string `json:"version,omitempty"`
+ VersionSemver string `json:"version-semver,omitempty"`
+ VersionDate string `json:"version-date,omitempty"`
+ VersionString string `json:"version-string,omitempty"`
+ PortVersion float64 `json:"port-version"`
+}
+
+// represents filesystem versions file ".json" found in versions folder
+type vcpkgFsVersions struct {
+ Versions []vcpkgFsVersionObjectEntry `json:"versions"`
+}
+
+// helpful to define relationships between Vcpkgs
+// ResolvedManifest is a source manifest (vcpkg.json) together with the registry it was resolved from.
+// it carries the raw Vcpkg so the cataloger can build the SBOM metadata and set package-level fields
+// (e.g. license) on pkg.Package rather than on the metadata.
+type ResolvedManifest struct {
+ Vcpkg *Vcpkg
+ Registry *pkg.VcpkgRegistryEntry
+}
+
+type ManifestNode struct {
+ Parent *ResolvedManifest
+ Child *ResolvedManifest
+}
+
+type vcpkgBaselineVersionObjectEntry struct {
+ Baseline string `json:"baseline"`
+ PortVersion float64 `json:"port-version"`
+}
+
+func (v *Vcpkg) GetFullVersion() string {
+ popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
+ if v.PortVersion != 0 {
+ return popVer + "#" + strconv.Itoa(int(v.PortVersion))
+ }
+ return popVer
+}
+
+func (v *vcpkgGitVersionObjectEntry) GetFullVersion() string {
+ popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
+ if v.PortVersion != 0 {
+ return popVer + "#" + strconv.Itoa(int(v.PortVersion))
+ }
+ return popVer
+}
+
+func (v *vcpkgFsVersionObjectEntry) GetFullVersion() string {
+ popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
+ if v.PortVersion != 0 {
+ return popVer + "#" + strconv.Itoa(int(v.PortVersion))
+ }
+ return popVer
+}
+
+func (v *vcpkgOverrideEntry) GetFullVersion() string {
+ popVer := getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
+ if v.PortVersion != 0 {
+ return popVer + "#" + strconv.Itoa(int(v.PortVersion))
+ }
+ return popVer
+}
+
+func (v *vcpkgGitVersionObjectEntry) GetPopulatedVersion() string {
+ return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
+}
+
+func (v *vcpkgFsVersionObjectEntry) GetPopulatedVersion() string {
+ return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
+}
+
+func (v *Vcpkg) GetPopulatedVersion() string {
+ return getPopulatedVersion(v.Version, v.VersionSemver, v.VersionDate, v.VersionString)
+}
+
+func getPopulatedVersion(version, versionSemver, versionDate, versionString string) string {
+ switch {
+ case version != "":
+ return version
+ case versionSemver != "":
+ return versionSemver
+ case versionDate != "":
+ return versionDate
+ case versionString != "":
+ return versionString
+ default:
+ return ""
+ }
+}
+
+const vcpkgRepo string = "https://github.com/microsoft/vcpkg"
+
+type ID struct {
+ location string
+ head string
+ name string
+ version string
+}
+
+// Resolver is a short-lived utility to resolve vcpkg manifests from multiple sources, including:
+// the filesystem, local maven cache directories, remote maven repositories, and the syft cache
+type Resolver struct {
+ ctx context.Context
+ gitRepos map[string]storage.Storer
+ allowGitClone bool
+ cfg *Config
+ resolved map[ID]*ResolvedManifest
+}
+
+// NewResolver constructs a new Resolver with the given vcpkg configuration.
+func NewResolver(ctx context.Context, cfg *Config, allowGitClone bool) *Resolver {
+ return &Resolver{
+ ctx: ctx,
+ gitRepos: map[string]storage.Storer{},
+ allowGitClone: allowGitClone,
+ cfg: cfg,
+ resolved: map[ID]*ResolvedManifest{},
+ }
+}
+
+// Get all of the manifest/vcpkg.json files for a vcpkg dependency
+func (r *Resolver) FindManifests(dependency any, df bool, builtinBaseline string, overrides []vcpkgOverrideEntry, parent *ResolvedManifest) ([]ManifestNode, error) {
+ var name string
+ var fullVersion string
+ defaultFeatures := df
+ var features []any
+ // can be either string or VcpkgDependency
+ switch d := dependency.(type) {
+ case string:
+ name = d
+ case map[string]any:
+ if n, ok := d["name"].(string); ok {
+ name = n
+ }
+ if v, ok := d["version>="].(string); ok {
+ fullVersion = v
+ }
+ if v, ok := d["default-features"].(bool); ok {
+ defaultFeatures = defaultFeatures && v
+ }
+ if v, ok := d["features"].([]any); ok {
+ features = v
+ }
+ }
+ // for when top-level manifest has this dependency version overridden
+ if over, ok := depVerOverriden(name, overrides); ok {
+ fullVersion = over.Version
+ }
+ reg := r.depRegistry(name, builtinBaseline)
+ vcpkg, err := r.findManifestFromReg(reg, name, fullVersion)
+ if err != nil {
+ return nil, err
+ }
+ child := &ResolvedManifest{Vcpkg: vcpkg, Registry: reg}
+ // need to add dependency to map, even if it doesn't find its manifest, to avoid infinite loops caused by circular depenencies
+ switch reg.Kind {
+ case pkg.Git:
+ r.resolved[ID{reg.Repository, reg.Baseline, name, fullVersion}] = child
+ case pkg.Builtin:
+ vcpkgRoot := os.Getenv("VCPKG_ROOT")
+ r.resolved[ID{vcpkgRoot, reg.Baseline, name, fullVersion}] = child
+ case pkg.FileSystem:
+ r.resolved[ID{reg.Path, reg.Baseline, name, fullVersion}] = child
+ }
+ vcpkg.appendFtrDepsToDeps(defaultFeatures, features)
+
+ manNodes := []ManifestNode{}
+ manNodes = append(manNodes, ManifestNode{
+ Parent: parent,
+ Child: child,
+ })
+ if len(vcpkg.Dependencies) != 0 {
+ for _, dep := range vcpkg.Dependencies {
+ resolvedManifest, ok := r.depResolved(dep, builtinBaseline)
+ if ok {
+ // this is to catch duplicates
+ manNodes = append(manNodes, ManifestNode{
+ // child is parent in this case
+ Parent: child,
+ Child: resolvedManifest,
+ })
+ } else {
+ childManNodes, err := r.FindManifests(dep, df, builtinBaseline, overrides, child)
+ if err != nil {
+ return nil, fmt.Errorf("could not find vcpkg.json file for dependency. %w", err)
+ }
+ manNodes = append(manNodes, childManNodes...)
+ }
+ }
+ }
+ return manNodes, nil
+}
+
+func (v *Vcpkg) appendFtrDepsToDeps(df bool, features []any) {
+ // collect explicitly requested feature names (a feature is either a string or a feature object)
+ requested := map[string]bool{}
+ for _, feature := range features {
+ switch fo := feature.(type) {
+ case string:
+ requested[fo] = true
+ case map[string]any:
+ // json decodes a feature object into map[string]any, not a struct
+ if name, ok := fo["name"].(string); ok {
+ requested[name] = true
+ }
+ }
+ }
+ // add each feature's deps at most once: when requested, or when it's a default feature and defaults are enabled.
+ // this must be independent of the requested loop so default features apply even when none are requested.
+ for name, f := range v.Features {
+ if requested[name] || (df && isDefaultFeature(name, v.DefaultFeatures)) {
+ v.Dependencies = append(v.Dependencies, f.Dependencies...)
+ }
+ }
+}
+
+func depVerOverriden(name string, overrides []vcpkgOverrideEntry) (*vcpkgOverrideEntry, bool) {
+ for _, over := range overrides {
+ if over.Name == name {
+ return &over, true
+ }
+ }
+ return nil, false
+}
+
+func (r *Resolver) resolveRepo(repoStr string) (*git.Repository, error) {
+ if r.gitRepos[repoStr] != nil {
+ return git.Open(r.gitRepos[repoStr], nil)
+ } else if r.allowGitClone {
+ repo, err := git.CloneContext(r.ctx, memory.NewStorage(), nil, &git.CloneOptions{
+ URL: repoStr,
+ })
+ if err != nil {
+ return nil, err
+ }
+ r.gitRepos[repoStr] = repo.Storer
+ return repo, nil
+ }
+ return nil, fmt.Errorf("could not resolve %s. enable vcpkg-allow-git-clone flag to allow cloning of remote git repos", repoStr)
+}
+
+func (r *Resolver) getRepo(repoStr string, path *string) (*git.Repository, error) {
+ vcpkgCachePath := getVcpkgGitCachePath()
+ // best to use vcpkg cache. Only able to locate if it's in the same directory as syft cache
+ if r.gitRepos[repoStr] == nil && vcpkgCachePath != "" {
+ repo, err := git.PlainOpen(vcpkgCachePath)
+ if err == nil {
+ r.gitRepos[repoStr] = repo.Storer
+ return repo, err
+ }
+ }
+ if r.gitRepos[repoStr] == nil && path != nil {
+ repo, err := git.PlainOpen(*path)
+ if err == nil {
+ r.gitRepos[repoStr] = repo.Storer
+ return repo, err
+ }
+ }
+ return r.resolveRepo(repoStr)
+}
+
+// get path of vcpkg cache
+func getVcpkgGitCachePath() string {
+ roots := cache.GetManager().RootDirs()
+ if len(roots) == 0 {
+ return ""
+ }
+ // vcpkg keeps its git registry cache as a sibling of the syft cache dir
+ return roots[0] + "/../vcpkg/registries/git"
+}
+
+// determines which registry to use by the name of the dependency
+func (r *Resolver) depRegistry(name string, builtinBaseline string) *pkg.VcpkgRegistryEntry {
+ var reg *pkg.VcpkgRegistryEntry
+ if r.cfg != nil {
+ for _, res := range r.cfg.Registries {
+ if slices.Contains(res.Packages, name) {
+ reg = &res
+ }
+ }
+ if r.cfg.DefaultRegistry != nil && reg == nil {
+ reg = r.cfg.DefaultRegistry
+ }
+ }
+ if reg == nil {
+ // copy the package-global so concurrent catalogers don't clobber its baseline
+ def := defaultRegistry
+ def.Baseline = builtinBaseline
+ reg = &def
+ } else if reg.Kind == pkg.Builtin {
+ reg.Baseline = builtinBaseline
+ }
+ return reg
+}
+
+// checks if dependency has been retrieved already this run. Without this check, there were infinite loops from circular dependencies
+func (r *Resolver) depResolved(dep any, builtinBaseline string) (*ResolvedManifest, bool) {
+ var name string
+ var version string
+ switch d := dep.(type) {
+ case string:
+ name = d
+ case map[string]any:
+ if n, ok := d["name"].(string); ok {
+ name = n
+ }
+ if v, ok := d["version>="].(string); ok {
+ version = v
+ }
+ }
+ reg := r.depRegistry(name, builtinBaseline)
+ var location string
+ switch {
+ case reg.Repository != "":
+ location = reg.Repository
+ case reg.Path != "":
+ location = reg.Path
+ default:
+ location = vcpkgRepo
+ }
+ resolved, ok := r.resolved[ID{location, reg.Baseline, name, version}]
+ return resolved, ok
+}
+
+func isDefaultFeature(name string, defaultFeatures []any) bool {
+ for _, df := range defaultFeatures {
+ switch d := df.(type) {
+ case string:
+ if name == d {
+ return true
+ }
+ case map[string]any:
+ if n, ok := d["name"].(string); ok && name == n {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func (r *Resolver) findManifestFromReg(reg *pkg.VcpkgRegistryEntry, name, fullVersion string) (*Vcpkg, error) {
+ if reg == nil {
+ return nil, fmt.Errorf("no vcpkg registry found which is required")
+ }
+ switch reg.Kind {
+ case pkg.Git:
+ var vcpkg *Vcpkg
+ var err error
+ if reg.Repository == "" {
+ return nil, fmt.Errorf("no repo found for vcpkg git registry")
+ }
+ if strings.TrimSuffix(reg.Repository, ".git") == vcpkgRepo {
+ path := os.Getenv("VCPKG_ROOT")
+ gitRepo, err := r.getRepo(vcpkgRepo, &path)
+ if err != nil {
+ return nil, err
+ }
+ vcpkg, err = r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ gitRepo, err := r.getRepo(reg.Repository, nil)
+ if err != nil {
+ return nil, err
+ }
+ vcpkg, err = r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return vcpkg, err
+ case pkg.Builtin:
+ path := os.Getenv("VCPKG_ROOT")
+ gitRepo, err := r.getRepo(vcpkgRepo, &path)
+ if err != nil {
+ return nil, err
+ }
+ vcpkg, err := r.getManifestFromGitRepo(gitRepo, reg.Baseline, name, fullVersion)
+ if err != nil {
+ return nil, err
+ }
+ return vcpkg, err
+ case pkg.FileSystem:
+ if reg.Path == "" {
+ return nil, fmt.Errorf("no path found for vcpkg filesystem registry")
+ }
+ baseline := reg.Baseline
+ if baseline == "" {
+ baseline = "default"
+ }
+ vcpkg, err := r.getManifestFromFilesystem(reg.Path, baseline, name, fullVersion)
+ if err != nil {
+ return nil, err
+ }
+ return vcpkg, err
+ default:
+ return nil, fmt.Errorf("vcpkg registry has no kind which is required")
+ }
+}
+
+// locates and gets the manifest file via the go-git
+func (r *Resolver) getManifestFromGitRepo(repo *git.Repository, head, name, fullVersion string) (*Vcpkg, error) {
+ headObj, err := repo.CommitObject(plumbing.NewHash(head))
+ if err != nil {
+ return nil, err
+ }
+ tree, err := headObj.Tree()
+ if err != nil {
+ return nil, err
+ }
+ var resultMan Vcpkg
+ if fullVersion != "" {
+ verPath := "versions/" + name[0:1] + "-/" + name + ".json"
+ verFile, err := findFileInTree(verPath, tree)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get versions file from vcpkg git tree. %w", err)
+ }
+ content, err := verFile.Contents()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get contents of versions file from vcpkg git tree. %w", err)
+ }
+ var versions vcpkgGitVersions
+ err = json.Unmarshal([]byte(content), &versions)
+ if err != nil {
+ return nil, fmt.Errorf("failed to unmarshal versions file from vcpkg git tree. %w", err)
+ }
+ // get tree object sha for the port version
+ var gitTreeHash string
+ for _, v := range versions.Versions {
+ if fullVersion == v.GetFullVersion() {
+ gitTreeHash = v.GitTree
+ break
+ }
+ }
+ if gitTreeHash == "" {
+ return nil, fmt.Errorf("version %q not found in versions file for port %q", fullVersion, name)
+ }
+ verTree, err := repo.TreeObject(plumbing.NewHash(gitTreeHash))
+ if err != nil {
+ return nil, fmt.Errorf("failed to get tree from hash %v. %w", gitTreeHash, err)
+ }
+ manFile, err := verTree.File("vcpkg.json")
+ if err != nil {
+ return nil, fmt.Errorf("failed to get vcpkg.json file from git tree. %w", err)
+ }
+ manContents, err := manFile.Contents()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get contents of vcpkg.json file from git tree. %w", err)
+ }
+ err = json.Unmarshal([]byte(manContents), &resultMan)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ portPath := "ports/" + name + "/vcpkg.json"
+ manFile, err := findFileInTree(portPath, tree)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get vcpkg.json file from ports directory in git tree. %w", err)
+ }
+ manContents, err := manFile.Contents()
+ if err != nil {
+ return nil, fmt.Errorf("failed to get contents of vcpkg.json file from ports directory in git tree. %w", err)
+ }
+ err = json.Unmarshal([]byte(manContents), &resultMan)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return &resultMan, err
+}
+
+func findFileInTree(path string, tree *object.Tree) (*object.File, error) {
+ verFile, err := tree.File(path)
+ return verFile, err
+}
+
+// locates and gets the manifest file via the filesystem path
+func (r *Resolver) getManifestFromFilesystem(path, baseline, name, fullVersion string) (*Vcpkg, error) {
+ if path == "" {
+ return &Vcpkg{}, fmt.Errorf("no/empty path specified for vcpkg filesystem registry")
+ }
+ var finalVer string
+ if fullVersion == "" {
+ baselineBytes, err := os.ReadFile(path + "/versions/baseline.json")
+ if err != nil {
+ return &Vcpkg{}, err
+ }
+ var baselineGen map[string]any
+ err = json.Unmarshal(baselineBytes, &baselineGen)
+ if err != nil {
+ return &Vcpkg{}, err
+ }
+ var baselineMatch map[string]any
+ for k, v := range baselineGen {
+ if k == baseline {
+ baselineMatch, _ = v.(map[string]any)
+ break
+ }
+ }
+ var baselineVer vcpkgBaselineVersionObjectEntry
+ for k, v := range baselineMatch {
+ if k == name {
+ foundBaseline, ok := v.(map[string]any)
+ if !ok {
+ continue
+ }
+ bl, _ := foundBaseline["baseline"].(string)
+ // default for go json package when unmarshalling number is float64
+ pv, _ := foundBaseline["port-version"].(float64)
+ baselineVer = vcpkgBaselineVersionObjectEntry{
+ Baseline: bl,
+ PortVersion: pv,
+ }
+ }
+ }
+ if baselineVer.Baseline == "" {
+ return nil, fmt.Errorf("could not find a baseline version for dependency with name %v", name)
+ }
+ if baselineVer.PortVersion > 0 {
+ finalVer = baselineVer.Baseline + "#" + strconv.Itoa(int(baselineVer.PortVersion))
+ } else {
+ finalVer = baselineVer.Baseline
+ }
+ } else {
+ finalVer = fullVersion
+ }
+ return getFsManifest(path, name, finalVer)
+}
+
+// retrieves manifest file from the filesystem
+func getFsManifest(path, name, ver string) (*Vcpkg, error) {
+ versionBytes, err := os.ReadFile(path + "/versions/" + name[0:1] + "-/" + name + ".json")
+ if err != nil {
+ return nil, err
+ }
+ var verFileCont vcpkgFsVersions
+ err = json.Unmarshal(versionBytes, &verFileCont)
+ if err != nil {
+ return nil, err
+ }
+ for _, v := range verFileCont.Versions {
+ if v.GetFullVersion() == ver {
+ // the $ character can be used to reference the root of the registry
+ manifestPath := strings.ReplaceAll(v.Path, "$", path)
+ manBytes, err := os.ReadFile(manifestPath + "/vcpkg.json")
+ if err != nil {
+ return nil, err
+ }
+ var vcpkgManRes Vcpkg
+ err = json.Unmarshal(manBytes, &vcpkgManRes)
+ if err != nil {
+ return nil, err
+ }
+ return &vcpkgManRes, nil
+ }
+ }
+ return nil, fmt.Errorf("failed to find vcpkg.json file for dependency name: %v", name)
+}
+
+func (v *Vcpkg) BuildManifest(reg *pkg.VcpkgRegistryEntry, triplet string) *pkg.VcpkgManifest {
+ var desc []string
+ switch d := v.Description.(type) {
+ case string:
+ desc = append(desc, d)
+ case []any:
+ // json decodes a description array into []any, not []string
+ for _, item := range d {
+ if s, ok := item.(string); ok {
+ desc = append(desc, s)
+ }
+ }
+ }
+ // give each manifest its own registry copy: the resolver hands out a shared *VcpkgRegistryEntry
+ // (e.g. cfg.DefaultRegistry) to every package, and a shared pointer means one package's metadata
+ // can alias another's
+ var regCopy *pkg.VcpkgRegistryEntry
+ if reg != nil {
+ r := *reg
+ regCopy = &r
+ }
+ return &pkg.VcpkgManifest{
+ Description: desc,
+ Documentation: v.Documentation,
+ FullVersion: v.GetFullVersion(),
+ Version: v.GetPopulatedVersion(),
+ PortVersion: int(v.PortVersion),
+ Maintainers: v.Maintainers,
+ Name: v.Name,
+ Supports: v.Supports,
+ Registry: regCopy,
+ Triplet: triplet,
+ }
+}
diff --git a/syft/pkg/cataloger/cpp/package.go b/syft/pkg/cataloger/cpp/package.go
index bc003ea7f..fbc5c2cad 100644
--- a/syft/pkg/cataloger/cpp/package.go
+++ b/syft/pkg/cataloger/cpp/package.go
@@ -1,11 +1,15 @@
package cpp
import (
+ "context"
+ "net/url"
+ "strconv"
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/pkg/cataloger/cpp/internal/vcpkg"
)
type conanRef struct {
@@ -92,7 +96,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p
Name: ref.Name,
Version: ref.Version,
Locations: file.NewLocationSet(locations...),
- PURL: packageURL(ref),
+ PURL: packageURLFromConanRef(ref),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: metadata,
@@ -103,7 +107,7 @@ func newConanPackage(refStr string, metadata any, locations ...file.Location) *p
return &p
}
-func packageURL(ref *conanRef) string {
+func packageURLFromConanRef(ref *conanRef) string {
qualifiers := packageurl.Qualifiers{}
if ref.Channel != "" {
qualifiers = append(qualifiers, packageurl.Qualifier{
@@ -120,3 +124,63 @@ func packageURL(ref *conanRef) string {
"",
).ToString()
}
+
+func newVcpkgPackage(ctx context.Context, rm *vcpkg.ResolvedManifest, l file.Location) pkg.Package {
+ // build the SBOM metadata from the source manifest; license is a package-level field (pkg.Package),
+ // not metadata, so it is read off the source manifest here rather than carried in pkg.VcpkgManifest
+ man := rm.Vcpkg.BuildManifest(rm.Registry, "")
+ p := pkg.Package{
+ Name: man.Name,
+ Version: man.FullVersion,
+ Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, rm.Vcpkg.License, l)),
+ Locations: file.NewLocationSet(l),
+ PURL: packageURLFromVcpkgManifest(man),
+ Language: pkg.CPP,
+ Type: pkg.VcpkgPkg,
+ Metadata: man,
+ }
+
+ p.SetID()
+ return p
+}
+
+func packageURLFromVcpkgManifest(v *pkg.VcpkgManifest) string {
+ qualifiers := packageurl.Qualifiers{}
+ //
+ if v.Triplet != "" {
+ qualifiers = append(qualifiers, packageurl.Qualifier{
+ Key: "triplet",
+ Value: v.Triplet,
+ })
+ }
+ if v.PortVersion != 0 {
+ qualifiers = append(qualifiers, packageurl.Qualifier{
+ Key: "port_revision",
+ Value: strconv.Itoa(v.PortVersion),
+ })
+ }
+ if v.Registry != nil && v.Registry.Repository != "" {
+ unescRepoURL, err := url.PathUnescape(v.Registry.Repository)
+ if err == nil {
+ qualifiers = append(qualifiers, packageurl.Qualifier{
+ Key: "repository_url",
+ Value: unescRepoURL,
+ })
+ }
+ }
+ if v.Registry != nil && v.Registry.Baseline != "" {
+ qualifiers = append(qualifiers, packageurl.Qualifier{
+ Key: "repository_revision",
+ Value: v.Registry.Baseline,
+ })
+ }
+ return packageurl.NewPackageURL(
+ // https://github.com/package-url/purl-spec/pull/245
+ "vcpkg",
+ "",
+ v.Name,
+ v.Version,
+ qualifiers,
+ "",
+ ).ToString()
+}
diff --git a/syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go
new file mode 100644
index 000000000..cf11d15bb
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest.go
@@ -0,0 +1,182 @@
+package cpp
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+
+ "github.com/anchore/syft/internal"
+ "github.com/anchore/syft/internal/log"
+ "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/cpp/internal/vcpkg"
+ "github.com/anchore/syft/syft/pkg/cataloger/generic"
+)
+
+// vcpkg manifests are small JSON files; cap reads to guard against pathological inputs
+const maxVcpkgManifestSize = 5 * 1024 * 1024
+
+type vcpkgCataloger struct {
+ allowGitClone bool
+}
+
+func newVcpkgCataloger(allowGitClone bool) *vcpkgCataloger {
+ return &vcpkgCataloger{
+ allowGitClone: allowGitClone,
+ }
+}
+
+// parser is for vcpkg in "Manifest" mode. This is opposed to "Classic" mode which or is more akin to a system package manager. (https://learn.microsoft.com/en-us/vcpkg/concepts/classic-mode)
+func (v *vcpkgCataloger) parseVcpkgManifest(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
+ conf, err := findVcpkgConfig(resolver)
+ if err != nil {
+ return nil, nil, fmt.Errorf("something went wrong parsing vcpkg-configuration.json file: %w", err)
+ }
+
+ var toplevelVcpkg vcpkg.Vcpkg
+ dec := json.NewDecoder(reader)
+ err = dec.Decode(&toplevelVcpkg)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to parse vcpkg.json file: %w", err)
+ }
+
+ var vcpkgs []vcpkg.Vcpkg
+ vcpkgs = append(vcpkgs, toplevelVcpkg)
+ overlayVcpkgs, err := findOverlayManifests(resolver, conf.OverlayPorts)
+ if err != nil {
+ return nil, nil, fmt.Errorf("could not get overlay port manifests: %w", err)
+ }
+ vcpkgs = append(vcpkgs, overlayVcpkgs...)
+ var pkgs []pkg.Package
+ var relationships []artifact.Relationship
+ for _, parentVcpkg := range vcpkgs {
+ pv := parentVcpkg
+ // the top-level project manifest has no registry (it is the thing being scanned, not a resolved dep)
+ parentMan := &vcpkg.ResolvedManifest{Vcpkg: &pv, Registry: nil}
+ pPkg := newVcpkgPackage(ctx, parentMan, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
+ pkgs = append(
+ pkgs,
+ pPkg)
+
+ r := vcpkg.NewResolver(
+ ctx,
+ conf,
+ v.allowGitClone,
+ )
+ for _, dep := range parentVcpkg.Dependencies {
+ cMans, fetchErr := r.FindManifests(dep, true, toplevelVcpkg.BuiltinBaseline, toplevelVcpkg.Overrides, parentMan)
+ if fetchErr != nil {
+ // best-effort: a single unresolvable dependency shouldn't discard packages already found
+ log.Debugf("vcpkg: unable to resolve dependency in %q: %v", parentVcpkg.Name, fetchErr)
+ continue
+ }
+ pkgs, relationships = appendPkgsAndRelationships(ctx, cMans, overlayVcpkgs, reader, relationships, pkgs)
+ }
+ }
+ pkg.Sort(pkgs)
+ return pkgs, relationships, nil
+}
+
+func appendPkgsAndRelationships(ctx context.Context, cMans []vcpkg.ManifestNode, overlayVcpkgs []vcpkg.Vcpkg, reader file.LocationReadCloser, relationships []artifact.Relationship, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) {
+ p := pkgs
+ r := relationships
+ for _, c := range cMans {
+ if c.Child != nil && !hasBeenOverlayed(c.Child.Vcpkg.Name, overlayVcpkgs) {
+ cPkg := newVcpkgPackage(ctx, c.Child, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
+ if c.Parent != nil {
+ pPkg := newVcpkgPackage(ctx, c.Parent, reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
+ pPkg.FoundBy = "vcpkg-manifest-cataloger"
+ cPkg.FoundBy = "vcpkg-manifest-cataloger"
+ rship := artifact.Relationship{
+ // From is the dependency, To is the dependent (syft convention)
+ From: cPkg,
+ To: pPkg,
+ Type: artifact.DependencyOfRelationship,
+ }
+ r = append(
+ r,
+ rship)
+ }
+ p = append(
+ p,
+ cPkg)
+ }
+ }
+ return p, r
+}
+
+// These are to be used in place of dependencies with the same name
+func findOverlayManifests(resolver file.Resolver, overlayPorts []string) ([]vcpkg.Vcpkg, error) {
+ var manifests []vcpkg.Vcpkg
+ for _, op := range overlayPorts {
+ // overlay port path is relative to location of vcpkg-configuration.json file
+ locs, err := resolver.FilesByGlob(op + "/**/vcpkg.json")
+ if err != nil {
+ return nil, err
+ }
+ for _, loc := range locs {
+ man, err := findManAtLoc(loc, resolver)
+ if err != nil {
+ return nil, err
+ }
+ manifests = append(manifests, *man)
+ }
+ }
+ return manifests, nil
+}
+
+func findManAtLoc(loc file.Location, resolver file.Resolver) (*vcpkg.Vcpkg, error) {
+ manCont, err := resolver.FileContentsByLocation(loc)
+ if err != nil {
+ return nil, err
+ }
+ defer internal.CloseAndLogError(manCont, loc.RealPath)
+ manBytes, err := io.ReadAll(io.LimitReader(manCont, maxVcpkgManifestSize))
+ if err != nil {
+ return nil, err
+ }
+ var man vcpkg.Vcpkg
+ err = json.Unmarshal(manBytes, &man)
+ if err != nil {
+ return nil, err
+ }
+ return &man, nil
+}
+
+// check to see if a package is not going to get pulled in because it's apart of the overlay-ports in vcpkg-configuration.json file
+func hasBeenOverlayed(pkgName string, overlayMans []vcpkg.Vcpkg) bool {
+ for _, om := range overlayMans {
+ if om.Name == pkgName {
+ return true
+ }
+ }
+ return false
+}
+
+// needed to know what vcpkg registries to use for what packages when looking for manifest files
+func findVcpkgConfig(resolver file.Resolver) (*vcpkg.Config, error) {
+ locs, err := resolver.FilesByGlob("**/vcpkg-configuration.json")
+ if err != nil {
+ return nil, err
+ }
+ if len(locs) != 0 {
+ cfgCont, err := resolver.FileContentsByLocation(locs[0])
+ if err != nil {
+ return nil, err
+ }
+ defer internal.CloseAndLogError(cfgCont, locs[0].RealPath)
+ cfgBytes, err := io.ReadAll(io.LimitReader(cfgCont, maxVcpkgManifestSize))
+ if err != nil {
+ return nil, err
+ }
+ var vcpkgConf vcpkg.Config
+ err = json.Unmarshal(cfgBytes, &vcpkgConf)
+ if err != nil {
+ return nil, err
+ }
+ return &vcpkgConf, err
+ }
+ return &vcpkg.Config{}, nil
+}
diff --git a/syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go
new file mode 100644
index 000000000..cd86a4b45
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/parse_vcpkgmanifest_test.go
@@ -0,0 +1,244 @@
+package cpp
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/go-git/go-git/v5"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/syft/internal/cache"
+ "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 TestParseVcpkgManifest(t *testing.T) {
+
+ fixture := "testdata/vcpkg/helloworld"
+ // the resolver reads the vcpkg registry from a local git clone rather than cloning over the network
+ // at test time. the clone is materialized just-in-time on first run (network required once) into a
+ // gitignored cache dir, then reused offline on later runs.
+ useLocalVcpkgRegistryCache(t)
+ fileLocs := []file.Location{file.NewLocation("vcpkg.json")}
+ fixtureLocationSet := file.NewLocationSet(fileLocs...)
+ ctx := pkgtest.Context(t)
+
+ fmtPkg := pkg.Package{
+ Name: "fmt",
+ Version: "11.0.2#1",
+ PURL: "pkg:vcpkg/fmt@11.0.2?port_revision=1&repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
+ FoundBy: "vcpkg-manifest-cataloger",
+ Locations: fixtureLocationSet,
+ Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
+ Language: pkg.CPP,
+ Type: pkg.VcpkgPkg,
+ Metadata: &pkg.VcpkgManifest{
+ Description: []string{"{fmt} is an open-source formatting library providing a fast and safe alternative to C stdio and C++ iostreams."},
+ FullVersion: "11.0.2#1",
+ Version: "11.0.2",
+ PortVersion: 1,
+ Name: "fmt",
+ Registry: &pkg.VcpkgRegistryEntry{
+ Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
+ Kind: pkg.Git,
+ Repository: "https://github.com/anchore/vcpkg-test-fixture",
+ },
+ },
+ }
+ helloPkg := pkg.Package{
+ Name: "hello",
+ Version: "0.1.0",
+ PURL: "pkg:vcpkg/hello@0.1.0",
+ FoundBy: "vcpkg-manifest-cataloger",
+ Locations: fixtureLocationSet,
+ Language: pkg.CPP,
+ Type: pkg.VcpkgPkg,
+ Metadata: &pkg.VcpkgManifest{
+ FullVersion: "0.1.0",
+ Version: "0.1.0",
+ Name: "hello",
+ Registry: nil,
+ },
+ }
+ vcpkgCmakeConfigPkg := pkg.Package{
+ Name: "vcpkg-cmake-config",
+ Version: "2024-05-23",
+ PURL: "pkg:vcpkg/vcpkg-cmake-config@2024-05-23?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
+ FoundBy: "vcpkg-manifest-cataloger",
+ Locations: fixtureLocationSet,
+ Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
+ Language: pkg.CPP,
+ Type: pkg.VcpkgPkg,
+ Metadata: &pkg.VcpkgManifest{
+ Documentation: "https://learn.microsoft.com/vcpkg/maintainers/functions/vcpkg_cmake_config_fixup",
+ FullVersion: "2024-05-23",
+ Version: "2024-05-23",
+ Name: "vcpkg-cmake-config",
+ Registry: &pkg.VcpkgRegistryEntry{
+ Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
+ Kind: pkg.Git,
+ Repository: "https://github.com/anchore/vcpkg-test-fixture",
+ },
+ },
+ }
+ vcpkgCmakePkg := pkg.Package{
+ Name: "vcpkg-cmake",
+ Version: "2024-04-23",
+ PURL: "pkg:vcpkg/vcpkg-cmake@2024-04-23?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
+ FoundBy: "vcpkg-manifest-cataloger",
+ Locations: fixtureLocationSet,
+ Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
+ Language: pkg.CPP,
+ Type: pkg.VcpkgPkg,
+ Metadata: &pkg.VcpkgManifest{
+ Documentation: "https://learn.microsoft.com/vcpkg/maintainers/functions/vcpkg_cmake_configure",
+ FullVersion: "2024-04-23",
+ Version: "2024-04-23",
+ Name: "vcpkg-cmake",
+ Registry: &pkg.VcpkgRegistryEntry{
+ Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
+ Kind: pkg.Git,
+ Repository: "https://github.com/anchore/vcpkg-test-fixture",
+ },
+ },
+ }
+ sampleLibPkg := pkg.Package{
+ Name: "vcpkg-sample-library",
+ Version: "1.0.2",
+ PURL: "pkg:vcpkg/vcpkg-sample-library@1.0.2?repository_revision=fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3&repository_url=https%3A%2F%2Fgithub.com%2Fanchore%2Fvcpkg-test-fixture",
+ FoundBy: "vcpkg-manifest-cataloger",
+ Locations: fixtureLocationSet,
+ Licenses: pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "MIT", fileLocs...)),
+ Language: pkg.CPP,
+ Type: pkg.VcpkgPkg,
+ Metadata: &pkg.VcpkgManifest{
+ Description: []string{"A sample C++ library designed to serve as a foundational example for a tutorial on packaging libraries with vcpkg."},
+ FullVersion: "1.0.2",
+ Version: "1.0.2",
+ Name: "vcpkg-sample-library",
+ Registry: &pkg.VcpkgRegistryEntry{
+ Baseline: "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
+ Kind: pkg.Git,
+ Repository: "https://github.com/anchore/vcpkg-test-fixture",
+ },
+ },
+ }
+
+ expectedPkgs := []pkg.Package{
+ fmtPkg,
+ fmtPkg,
+ helloPkg,
+ vcpkgCmakeConfigPkg,
+ vcpkgCmakeConfigPkg,
+ vcpkgCmakePkg,
+ vcpkgCmakePkg,
+ sampleLibPkg,
+ }
+
+ // relationships require IDs to be set to be sorted similarly
+ for i := range expectedPkgs {
+ expectedPkgs[i].SetID()
+ }
+ pkg.Sort(expectedPkgs)
+
+ expectedRelationships := []artifact.Relationship{
+ {
+ From: fmtPkg,
+ To: helloPkg,
+ Type: artifact.DependencyOfRelationship,
+ Data: nil,
+ },
+ {
+ From: vcpkgCmakePkg,
+ To: fmtPkg,
+ Type: artifact.DependencyOfRelationship,
+ Data: nil,
+ },
+ {
+ From: vcpkgCmakeConfigPkg,
+ To: fmtPkg,
+ Type: artifact.DependencyOfRelationship,
+ Data: nil,
+ },
+ {
+ From: sampleLibPkg,
+ To: helloPkg,
+ Type: artifact.DependencyOfRelationship,
+ Data: nil,
+ },
+ {
+ From: fmtPkg,
+ To: sampleLibPkg,
+ Type: artifact.DependencyOfRelationship,
+ Data: nil,
+ },
+ {
+ From: vcpkgCmakePkg,
+ To: sampleLibPkg,
+ Type: artifact.DependencyOfRelationship,
+ Data: nil,
+ },
+ {
+ From: vcpkgCmakeConfigPkg,
+ To: sampleLibPkg,
+ Type: artifact.DependencyOfRelationship,
+ Data: nil,
+ },
+ }
+
+ catalogerCfg := CatalogerConfig{
+ VcpkgAllowGitClone: false,
+ }
+ pkgtest.TestCataloger(t, fixture, NewVcpkgManifestCataloger(catalogerCfg), expectedPkgs, expectedRelationships)
+}
+
+// the registry the helloworld fixture pins; see testdata/vcpkg/helloworld/vcpkg-configuration.json
+const vcpkgTestRegistryURL = "https://github.com/anchore/vcpkg-test-fixture"
+
+// useLocalVcpkgRegistryCache points syft's cache manager at a gitignored testdata cache and ensures a local
+// clone of the vcpkg test registry exists there, so the resolver resolves it offline via git.PlainOpen
+// (getVcpkgGitCachePath resolves to "/../vcpkg/registries/git").
+func useLocalVcpkgRegistryCache(t *testing.T) {
+ t.Helper()
+
+ cacheRoot := filepath.Join("testdata", "cache")
+ syftCacheRoot := filepath.Join(cacheRoot, "syft")
+ registryGitDir := filepath.Join(cacheRoot, "vcpkg", "registries", "git")
+
+ prepareVcpkgRegistryCache(t, registryGitDir)
+
+ mgr, err := cache.NewFromDir(syftCacheRoot, 24*time.Hour)
+ require.NoError(t, err)
+ prev := cache.GetManager()
+ cache.SetManager(mgr)
+ t.Cleanup(func() { cache.SetManager(prev) })
+}
+
+// prepareVcpkgRegistryCache clones the vcpkg test registry into gitDir if a valid clone is not already present.
+// the clone happens once (first run, requires network); later runs reuse it offline.
+func prepareVcpkgRegistryCache(t *testing.T, gitDir string) {
+ t.Helper()
+
+ if _, err := git.PlainOpen(gitDir); err == nil {
+ return // valid cache already present
+ }
+ // clear any partial/stale state left by an interrupted run
+ require.NoError(t, os.RemoveAll(gitDir))
+ require.NoError(t, os.MkdirAll(filepath.Dir(gitDir), 0o755))
+
+ // clone into a temp sibling then atomically move into place so an interrupted or concurrent run can't
+ // observe a half-written cache
+ tmp, err := os.MkdirTemp(filepath.Dir(gitDir), "clone-")
+ require.NoError(t, err)
+ defer os.RemoveAll(tmp)
+
+ cloneTarget := filepath.Join(tmp, "git")
+ if _, err := git.PlainClone(cloneTarget, false, &git.CloneOptions{URL: vcpkgTestRegistryURL}); err != nil {
+ t.Skipf("vcpkg registry cache is not present and could not be prepared (first run requires network): %v", err)
+ }
+ require.NoError(t, os.Rename(cloneTarget, gitDir))
+}
diff --git a/syft/pkg/cataloger/cpp/testdata/.gitignore b/syft/pkg/cataloger/cpp/testdata/.gitignore
new file mode 100644
index 000000000..5bc1ec057
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/.gitignore
@@ -0,0 +1,2 @@
+/cache/
+test-observations.json
diff --git a/syft/pkg/cataloger/cpp/testdata/README.md b/syft/pkg/cataloger/cpp/testdata/README.md
index e1311bc2c..35456cf73 100644
--- a/syft/pkg/cataloger/cpp/testdata/README.md
+++ b/syft/pkg/cataloger/cpp/testdata/README.md
@@ -1,8 +1,10 @@
-# Conan test data
+# C++ test data
-This folder contains the test data for the Conan package manager.
+This folder contains the test data for the Conan and Vcpkg package managers.
-## conan.lock
+## Conan test data
+
+### conan.lock
The conan lock file is created in the following way.
@@ -14,4 +16,9 @@ This is necessary to verify that the dependency tree is properly parsed.
`sed -i 's|mfast/1.2.2#c6f6387c9b99780f0ee05e25f99d0f39|mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39|g' conan.lock`
3. Manually delete the package id and prev from tinyxml2 entry to test conan lock parsing if they are missing:
`sed -i 's|\"package_id\": \"6557f18ca99c0b6a233f43db00e30efaa525e27e\",||g' conan.lock`
- `sed -i 's|\"prev\": \"548bb273d2980991baa519453d68e5cd\",||g' conan.lock`
\ No newline at end of file
+ `sed -i 's|\"prev\": \"548bb273d2980991baa519453d68e5cd\",||g' conan.lock`
+
+## Vcpkg test data
+
+vcpkg/helloworld contains a vcpkg project that contains only the files necessary to retrieve all of the manifests for dependencies listed
+in the vcpkg.json file. These manifests exist in a remote vcpkg git registry located at https://github.com/anchore/vcpkg-test-fixture
diff --git a/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/build/vcpkg_installed/vcpkg/vcpkg-lock.json
@@ -0,0 +1 @@
+{}
diff --git a/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg-configuration.json
@@ -0,0 +1 @@
+{}
diff --git a/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json
new file mode 100644
index 000000000..0967ef424
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/glob-paths/somewhere/src/vcpkg.json
@@ -0,0 +1 @@
+{}
diff --git a/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp
new file mode 100644
index 000000000..0fbad3524
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/helloworld.cpp
@@ -0,0 +1,7 @@
+#include
+
+int main()
+{
+ fmt::print("Hello World!\n");
+ return 0;
+}
diff --git a/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json
new file mode 100644
index 000000000..43f7a635d
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg-configuration.json
@@ -0,0 +1,7 @@
+{
+ "default-registry": {
+ "kind": "git",
+ "baseline": "fbfe5a93a4b9562d88dcbc9cefca0016594ba3b3",
+ "repository": "https://github.com/anchore/vcpkg-test-fixture"
+ }
+}
diff --git a/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json
new file mode 100644
index 000000000..5d2f1c9dc
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/vcpkg/helloworld/vcpkg.json
@@ -0,0 +1,10 @@
+{
+ "name": "hello",
+ "version": "0.1.0",
+ "dependencies": [
+ {
+ "name": "fmt"
+ },
+ "vcpkg-sample-library"
+ ]
+}
diff --git a/syft/pkg/type.go b/syft/pkg/type.go
index b7155bdb6..b4d42853f 100644
--- a/syft/pkg/type.go
+++ b/syft/pkg/type.go
@@ -52,6 +52,7 @@ const (
SwiftPkg Type = "swift"
SwiplPackPkg Type = "swiplpack"
TerraformPkg Type = "terraform"
+ VcpkgPkg Type = "vcpkg"
WordpressPluginPkg Type = "wordpress-plugin"
HomebrewPkg Type = "homebrew"
AppleAppBundlePkg Type = "apple-app-bundle"
@@ -98,6 +99,7 @@ var AllPkgs = []Type{
SwiftPkg,
SwiplPackPkg,
TerraformPkg,
+ VcpkgPkg,
WordpressPluginPkg,
HomebrewPkg,
AppleAppBundlePkg,
@@ -174,6 +176,8 @@ func (t Type) PackageURLType() string {
return "swiplpack"
case TerraformPkg:
return "terraform"
+ case VcpkgPkg:
+ return "vcpkg"
case WordpressPluginPkg:
return "wordpress-plugin"
case HomebrewPkg:
@@ -262,6 +266,8 @@ func TypeByName(name string) Type {
return SwiplPackPkg
case "terraform":
return TerraformPkg
+ case "vcpkg":
+ return VcpkgPkg
case "wordpress-plugin":
return WordpressPluginPkg
case "homebrew":
diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go
index 0e4201706..514d06b13 100644
--- a/syft/pkg/type_test.go
+++ b/syft/pkg/type_test.go
@@ -163,6 +163,7 @@ func TestTypeFromPURL(t *testing.T) {
expectedTypes.Remove(string(ModelPkg)) // no valid purl for ai artifacts currently
expectedTypes.Remove(string(AppleAppBundlePkg)) // no standard purl type for apple app bundles
expectedTypes.Remove(string(PhpPeclPkg)) // we should always consider this a pear package
+ expectedTypes.Remove(string(VcpkgPkg))
for _, test := range tests {
t.Run(string(test.expected), func(t *testing.T) {
diff --git a/syft/pkg/vcpkg.go b/syft/pkg/vcpkg.go
new file mode 100644
index 000000000..165f846f1
--- /dev/null
+++ b/syft/pkg/vcpkg.go
@@ -0,0 +1,35 @@
+package pkg
+
+type VcpkgRegistryKind string
+
+const (
+ FileSystem VcpkgRegistryKind = "filesystem"
+ Git VcpkgRegistryKind = "git"
+ Builtin VcpkgRegistryKind = "builtin"
+)
+
+// used for metadata. Includes summary of data found in manifest (vcpkg.json file) relevant to just that vcpkg
+type VcpkgManifest struct {
+ Description []string `json:"description,omitempty"`
+ Documentation string `json:"documentation,omitempty"`
+ FullVersion string `json:"full-version"`
+ Version string `json:"version"`
+ PortVersion int `json:"port-version"`
+ Maintainers []string `json:"maintainers,omitempty"`
+ Name string `json:"name"`
+ Supports string `json:"supports,omitempty"`
+ // to show where it came from
+ Registry *VcpkgRegistryEntry `json:"registry,omitempty"`
+ // found by looking at build folder to find target. ex. "x64-linux"
+ Triplet string `json:"triplet,omitempty"`
+}
+
+// Matches definition of Vcpkg "Registry". https://learn.microsoft.com/en-us/vcpkg/reference/vcpkg-configuration-json#registry
+type VcpkgRegistryEntry struct {
+ Baseline string `json:"baseline,omitempty"`
+ Kind VcpkgRegistryKind `json:"kind"`
+ Packages []string `json:"packages,omitempty"`
+ Path string `json:"path,omitempty"`
+ Reference string `json:"reference,omitempty"`
+ Repository string `json:"repository,omitempty"`
+}
From df1fc98e7aa861e39b34e0798aeaace028e28728 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:26:04 +0000
Subject: [PATCH 038/168] chore(deps): bump
anchore/workflows/.github/workflows/codeql.yaml (#5032)
Bumps [anchore/workflows/.github/workflows/codeql.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0.
- [Release notes](https://github.com/anchore/workflows/releases)
- [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4)
---
updated-dependencies:
- dependency-name: anchore/workflows/.github/workflows/codeql.yaml
dependency-version: 0.8.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.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml
index 6384d5959..022c2b9cb 100644
--- a/.github/workflows/codeql.yaml
+++ b/.github/workflows/codeql.yaml
@@ -13,7 +13,7 @@ permissions: {}
jobs:
analyze:
name: Analyze
- uses: anchore/workflows/.github/workflows/codeql.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2
+ uses: anchore/workflows/.github/workflows/codeql.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
permissions:
security-events: write
packages: read
From 5a7a72b53e10074cb3c210b9d6995203dc5ad013 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:26:27 +0000
Subject: [PATCH 039/168] chore(deps): bump zizmorcore/zizmor-action from 0.5.6
to 0.5.7 (#5030)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.6 to 0.5.7.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/5f14fd08f7cf1cb1609c1e344975f152c7ee938d...192e21d79ab29983730a13d1382995c2307fbcaa)
---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
dependency-version: 0.5.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/validate-github-actions.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml
index d4c38eb98..b6a492f25 100644
--- a/.github/workflows/validate-github-actions.yaml
+++ b/.github/workflows/validate-github-actions.yaml
@@ -25,7 +25,7 @@ jobs:
persist-credentials: false
- name: "Run zizmor"
- uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
+ uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
with:
# there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default)
advanced-security: true
From 832d96c1dd75c083e6256742c128d905ec8b457a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:26:37 +0000
Subject: [PATCH 040/168] chore(deps): bump actions/cache in
/.github/actions/bootstrap (#5035)
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9)
---
updated-dependencies:
- dependency-name: actions/cache
dependency-version: 6.1.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/actions/bootstrap/action.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml
index 26067046f..f32bd0f7d 100644
--- a/.github/actions/bootstrap/action.yaml
+++ b/.github/actions/bootstrap/action.yaml
@@ -60,7 +60,7 @@ runs:
# ORAS cache: restore + save on the default branch of the canonical repo only.
- name: Restore and save ORAS cache from github actions
if: ${{ inputs.download-test-fixture-cache == 'true' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.event.repository.fork == false }}
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ github.workspace }}/.tmp/oras-cache
key: ${{ inputs.cache-key-prefix }}-oras-cache
From 4eb61fc104bc4a09d6b5fb19d4e606d3bd1adf5d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:26:50 +0000
Subject: [PATCH 041/168] chore(deps): bump
anchore/workflows/.github/workflows/check-gate.yaml (#5034)
Bumps [anchore/workflows/.github/workflows/check-gate.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0.
- [Release notes](https://github.com/anchore/workflows/releases)
- [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4)
---
updated-dependencies:
- dependency-name: anchore/workflows/.github/workflows/check-gate.yaml
dependency-version: 0.8.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/release.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index b282397d4..42b9086d1 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -41,7 +41,7 @@ jobs:
permissions:
contents: read
checks: read # required for getting the status of specific check names
- uses: anchore/workflows/.github/workflows/check-gate.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2
+ uses: anchore/workflows/.github/workflows/check-gate.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
with:
# these are checks that should be run on pull-request and merges to main.
# we do NOT want to kick off a release if these have not been verified on main.
From f2c9463e76fc96d6438e6ae7d658dd9266010faf Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:30:15 +0000
Subject: [PATCH 042/168] chore(deps): bump
anchore/go-make/.github/actions/setup (#5036)
Bumps [anchore/go-make/.github/actions/setup](https://github.com/anchore/go-make) from 0.6.0 to 0.8.0.
- [Release notes](https://github.com/anchore/go-make/releases)
- [Commits](https://github.com/anchore/go-make/compare/39fe5f71112d4dceb3ff0a92a40f272f067fc457...430e2175bb166bfd138ef75466adbcc8ddab875f)
---
updated-dependencies:
- dependency-name: anchore/go-make/.github/actions/setup
dependency-version: 0.8.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/actions/bootstrap/action.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml
index f32bd0f7d..d9e3be414 100644
--- a/.github/actions/bootstrap/action.yaml
+++ b/.github/actions/bootstrap/action.yaml
@@ -30,7 +30,7 @@ runs:
using: "composite"
steps:
- name: Setup go + go-make tooling
- uses: anchore/go-make/.github/actions/setup@39fe5f71112d4dceb3ff0a92a40f272f067fc457 # v0.6.0
+ uses: anchore/go-make/.github/actions/setup@430e2175bb166bfd138ef75466adbcc8ddab875f # v0.8.0
with:
go-version: ${{ inputs.go-version }}
cache-key-prefix: ${{ inputs.cache-key-prefix }}
From 312f2532d0b0ca6a731a87f22ca4910aa8d001d1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:30:45 +0000
Subject: [PATCH 043/168] chore(deps): bump modernc.org/sqlite from 1.51.0 to
1.53.0 (#5039)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.51.0 to 1.53.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.51.0...v1.53.0)
---
updated-dependencies:
- dependency-name: modernc.org/sqlite
dependency-version: 1.53.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 | 4 ++--
go.sum | 20 ++++++++++----------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/go.mod b/go.mod
index 74adff89d..f180f961b 100644
--- a/go.mod
+++ b/go.mod
@@ -98,7 +98,7 @@ require (
golang.org/x/time v0.15.0
golang.org/x/tools v0.46.0
gopkg.in/yaml.v3 v3.0.1
- modernc.org/sqlite v1.51.0
+ modernc.org/sqlite v1.53.0
)
require (
@@ -324,7 +324,7 @@ require (
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
- modernc.org/libc v1.72.3 // indirect
+ modernc.org/libc v1.73.4 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
diff --git a/go.sum b/go.sum
index 5b38989f4..c259ce2c5 100644
--- a/go.sum
+++ b/go.sum
@@ -1513,20 +1513,20 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
-modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
-modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
-modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
-modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
+modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
+modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
+modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
+modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
-modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
-modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
+modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
+modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
-modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
-modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
+modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
+modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
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=
@@ -1535,8 +1535,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/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.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
-modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
+modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
+modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
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 f6bf15bb3c1d8799c9fea06c0119f7bf69046847 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:31:03 +0000
Subject: [PATCH 044/168] chore(deps): bump golang.org/x/tools from 0.46.0 to
0.47.0 (#5038)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.46.0 to 0.47.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.46.0...v0.47.0)
---
updated-dependencies:
- dependency-name: golang.org/x/tools
dependency-version: 0.47.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 f180f961b..5fc0388f1 100644
--- a/go.mod
+++ b/go.mod
@@ -96,7 +96,7 @@ require (
golang.org/x/mod v0.37.0
golang.org/x/net v0.56.0
golang.org/x/time v0.15.0
- golang.org/x/tools v0.46.0
+ golang.org/x/tools v0.47.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.53.0
)
diff --git a/go.sum b/go.sum
index c259ce2c5..e902f2d3c 100644
--- a/go.sum
+++ b/go.sum
@@ -1315,8 +1315,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.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
-golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
+golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
+golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
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 ee48c01c06b0c3ce4da53d0eea1983ba1ceec3cc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:31:51 +0000
Subject: [PATCH 045/168] chore(deps): bump actions/cache/restore in
/.github/actions/bootstrap (#5037)
Bumps [actions/cache/restore](https://github.com/actions/cache) from 5.0.5 to 6.1.0.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/27d5ce7f107fe9357f9df03efb73ab90386fccae...55cc8345863c7cc4c66a329aec7e433d2d1c52a9)
---
updated-dependencies:
- dependency-name: actions/cache/restore
dependency-version: 6.1.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/actions/bootstrap/action.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml
index d9e3be414..aad6d6090 100644
--- a/.github/actions/bootstrap/action.yaml
+++ b/.github/actions/bootstrap/action.yaml
@@ -52,7 +52,7 @@ runs:
# ORAS cache: restore-only on non-default branches / forks
- name: Restore ORAS cache from github actions
if: ${{ inputs.download-test-fixture-cache == 'true' && (github.ref != format('refs/heads/{0}', github.event.repository.default_branch) || github.event.repository.fork == true) }}
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ github.workspace }}/.tmp/oras-cache
key: ${{ inputs.cache-key-prefix }}-oras-cache
From 6f5c83a5585326b4a50d9bc0b178bd6991763535 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Jul 2026 13:42:48 +0000
Subject: [PATCH 046/168] chore(deps): bump actions/checkout from 6.0.3 to
7.0.0 (#5024)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6.0.3 to 7.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/df4cb1c069e1874edd31b4311f1884172cec0e10...9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: 7.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/release.yaml | 2 +-
.github/workflows/validate-github-actions.yaml | 2 +-
.github/workflows/validations.yaml | 14 +++++++-------
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 42b9086d1..5d13d2565 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -70,7 +70,7 @@ jobs:
packages: write # required for publishing release artifacts to GitHub packages
id-token: write # required for keyless signing (cosign/sigstore OIDC)
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
fetch-depth: 0
persist-credentials: true
diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml
index b6a492f25..6aaa5b82c 100644
--- a/.github/workflows/validate-github-actions.yaml
+++ b/.github/workflows/validate-github-actions.yaml
@@ -20,7 +20,7 @@ jobs:
contents: read
security-events: write # for uploading SARIF results
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml
index e7eea1288..c520a05b7 100644
--- a/.github/workflows/validations.yaml
+++ b/.github/workflows/validations.yaml
@@ -28,7 +28,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
persist-credentials: false
@@ -50,7 +50,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
persist-credentials: false
@@ -72,7 +72,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
persist-credentials: false
@@ -99,7 +99,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
persist-credentials: false
@@ -129,7 +129,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
persist-credentials: false
@@ -173,7 +173,7 @@ jobs:
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
persist-credentials: false
@@ -207,7 +207,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
with:
persist-credentials: false
From 8c0d21e147b814cbfed3daad0406ef3bfcfb422b Mon Sep 17 00:00:00 2001
From: anchore-oss-update-bot
Date: Mon, 6 Jul 2026 10:56:45 -0400
Subject: [PATCH 047/168] chore(deps): update CPE dictionary index (#5042)
Signed-off-by: anchore-oss-update-bot
Co-authored-by: anchore-oss-update-bot
---
.../dictionary/data/cpe-index.json | 439 +++++++++++++++++-
1 file changed, 432 insertions(+), 7 deletions(-)
diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
index 22f6e3ef1..1d7a1dd69 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
+++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
@@ -37,6 +37,9 @@
"github.com/charmbracelet/soft-serve": [
"cpe:2.3:a:charm:soft_serve:*:*:*:*:*:go:*:*"
],
+ "github.com/cloudflare/circl": [
+ "cpe:2.3:a:cloudflare:circl:*:*:*:*:*:go:*:*"
+ ],
"github.com/containers/podman": [
"cpe:2.3:a:podman_project:podman:*:*:*:*:*:*:*:*"
],
@@ -61,9 +64,18 @@
"github.com/free5gc/amf": [
"cpe:2.3:a:free5gc:amf:*:*:*:*:*:go:*:*"
],
+ "github.com/free5gc/bsf": [
+ "cpe:2.3:a:free5gc:bsf:*:*:*:*:*:go:*:*"
+ ],
"github.com/free5gc/go-upf": [
"cpe:2.3:a:free5gc:go-upf:*:*:*:*:*:go:*:*"
],
+ "github.com/free5gc/nef": [
+ "cpe:2.3:a:free5gc:nef:*:*:*:*:*:go:*:*"
+ ],
+ "github.com/free5gc/nrf": [
+ "cpe:2.3:a:free5gc:nrf:*:*:*:*:*:go:*:*"
+ ],
"github.com/free5gc/smf": [
"cpe:2.3:a:free5gc:smf:*:*:*:*:*:go:*:*"
],
@@ -106,6 +118,12 @@
"github.com/hashicorp/go-slug": [
"cpe:2.3:a:hashicorp:go-slug:*:*:*:*:*:*:*:*"
],
+ "github.com/jackc/pgx": [
+ "cpe:2.3:a:jackc:pgx:*:*:*:*:*:go:*:*"
+ ],
+ "github.com/jackc/pgx/v5": [
+ "cpe:2.3:a:jackc:pgx:*:*:*:*:*:go:*:*"
+ ],
"github.com/jumpserver/koko/pkg/koko": [
"cpe:2.3:a:fit2cloud:koko:*:*:*:*:*:go:*:*"
],
@@ -145,6 +163,12 @@
"github.com/ntbosscher/gobase": [
"cpe:2.3:a:gobase_project:gobase:*:*:*:*:*:go:*:*"
],
+ "github.com/open-telemetry/opentelemetry-collector-contrib": [
+ "cpe:2.3:a:opentelemetry:opentelemetry_collector_contrib:*:*:*:*:*:go:*:*"
+ ],
+ "github.com/pocketbase/pocketbase": [
+ "cpe:2.3:a:pocketbase:pocketbase:*:*:*:*:*:go:*:*"
+ ],
"github.com/proglottis/gpgme": [
"cpe:2.3:a:gpgme_project:gpgme:*:*:*:*:*:go:*:*"
],
@@ -163,6 +187,9 @@
"github.com/satori/go.uuid": [
"cpe:2.3:a:satori:uuid:*:*:*:*:*:go:*:*"
],
+ "github.com/shamaton/msgpack/v3": [
+ "cpe:2.3:a:shamaton:msgpack:*:*:*:*:*:go:*:*"
+ ],
"github.com/sigstore/gitsign": [
"cpe:2.3:a:sigstore:gitsign:*:*:*:*:*:go:*:*"
],
@@ -193,14 +220,21 @@
"github.com/whilp/git-urls": [
"cpe:2.3:a:git-urls_project:git-urls:*:*:*:*:*:go:*:*"
],
+ "github.com/xddxdd/bird-lg-go": [
+ "cpe:2.3:a:xddxdd:bird-lg-go:*:*:*:*:*:go:*:*"
+ ],
"github.com/yuin/goldmark": [
"cpe:2.3:a:yuin:goldmark:*:*:*:*:*:go:*:*"
],
"go.opentelemetry.io/otel": [
"cpe:2.3:a:opentelemetry:opentelemetry:*:*:*:*:*:go:*:*"
],
+ "go.opentelemetry.io/otel/schema": [
+ "cpe:2.3:a:opentelemetry:telemetry_schema_files:*:*:*:*:*:go:*:*"
+ ],
"golang.org/x/crypto": [
- "cpe:2.3:a:go:ssh:*:*:*:*:*:go:*:*"
+ "cpe:2.3:a:go:ssh:*:*:*:*:*:go:*:*",
+ "cpe:2.3:a:golang:crypto:*:*:*:*:*:go:*:*"
],
"golang.org/x/crypto/ssh": [
"cpe:2.3:a:golang:package_ssh:*:*:*:*:*:*:*:*",
@@ -217,6 +251,7 @@
"cpe:2.3:a:golang:tiff:*:*:*:*:*:go:*:*"
],
"golang.org/x/net": [
+ "cpe:2.3:a:golang:net:*:*:*:*:*:go:*:*",
"cpe:2.3:a:golang:networking:*:*:*:*:*:go:*:*"
],
"golang.org/x/net/html": [
@@ -673,6 +708,9 @@
"covcomplplot": [
"cpe:2.3:a:jenkins:coverage\\/complexity_scatter_plot:*:*:*:*:*:jenkins:*:*"
],
+ "coverage": [
+ "cpe:2.3:a:jenkins:coverage:*:*:*:*:*:jenkins:*:*"
+ ],
"coverity": [
"cpe:2.3:a:jenkins:coverity:*:*:*:*:*:jenkins:*:*"
],
@@ -863,6 +901,9 @@
"eggplant": [
"cpe:2.3:a:jenkins:eggplant:*:*:*:*:*:jenkins:*:*"
],
+ "eggplant-runner": [
+ "cpe:2.3:a:jenkins:eggplant_runner:*:*:*:*:*:jenkins:*:*"
+ ],
"elastest": [
"cpe:2.3:a:jenkins:elastest:*:*:*:*:*:jenkins:*:*"
],
@@ -903,6 +944,9 @@
"extended-choice-parameter": [
"cpe:2.3:a:jenkins:extended_choice_parameter:*:*:*:*:*:jenkins:*:*"
],
+ "extensible-choice-parameter": [
+ "cpe:2.3:a:jenkins:extensible_choice_parameter:*:*:*:*:*:jenkins:*:*"
+ ],
"extensivetesting": [
"cpe:2.3:a:jenkins:extensive_testing:*:*:*:*:*:jenkins:*:*"
],
@@ -1032,6 +1076,9 @@
"github-pr-coverage-status": [
"cpe:2.3:a:jenkins:github_pull_request_coverage_status:*:*:*:*:*:jenkins:*:*"
],
+ "github-pullrequest": [
+ "cpe:2.3:a:kostyasha:github_integration:*:*:*:*:*:jenkins:*:*"
+ ],
"gitlab": [
"cpe:2.3:a:jenkins:gitlab:*:*:*:*:*:jenkins:*:*"
],
@@ -1184,6 +1231,9 @@
"jenkins-cloudformation": [
"cpe:2.3:a:jenkins:jenkins-cloudformation-plugin:*:*:*:*:*:jenkins:*:*"
],
+ "jenkins-multijob": [
+ "cpe:2.3:a:jenkins:multijob:*:*:*:*:*:jenkins:*:*"
+ ],
"jenkins-multijob-plugin": [
"cpe:2.3:a:jenkins:multijob:*:*:*:*:*:jenkins:*:*"
],
@@ -1285,6 +1335,9 @@
"lambdatest-automation": [
"cpe:2.3:a:jenkins:lambdatest-automation:*:*:*:*:*:jenkins:*:*"
],
+ "ldap": [
+ "cpe:2.3:a:jenkins:ldap:*:*:*:*:*:jenkins:*:*"
+ ],
"libvirt-slave": [
"cpe:2.3:a:jenkins:libvirt_agents:*:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:jenkins:libvirt_slaves:*:*:*:*:*:jenkins:*:*"
@@ -1385,6 +1438,9 @@
"maven-release-cascade": [
"cpe:2.3:a:barchart:maven_cascade_release:*:*:*:*:*:jenkins:*:*"
],
+ "mcp-server": [
+ "cpe:2.3:a:jenkins:mcp_server:*:*:*:*:*:jenkins:*:*"
+ ],
"meliora-testlab": [
"cpe:2.3:a:jenkins:meliora_testlab:*:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:melioratestlab:melioratestlab:*:*:*:*:*:jenkins:*:*"
@@ -1558,6 +1614,9 @@
"pipeline-githubnotify-step": [
"cpe:2.3:a:jenkins:pipeline_github_notify_step:*:*:*:*:*:jenkins:*:*"
],
+ "pipeline-groovy-lib": [
+ "cpe:2.3:a:jenkins:pipeline\\:_groovy_libraries:*:*:*:*:*:*:*:*"
+ ],
"pipeline-input-step": [
"cpe:2.3:a:jenkins:pipeline\\:_input_step:*:*:*:*:*:jenkins:*:*",
"cpe:2.3:a:jenkins:pipeline\\:input_step:*:*:*:*:*:jenkins:*:*"
@@ -1568,6 +1627,9 @@
"pipeline-model-definition": [
"cpe:2.3:a:jenkins:pipeline\\:_declarative:*:*:*:*:*:jenkins:*:*"
],
+ "pipeline-reporter-by-redpen": [
+ "cpe:2.3:a:jenkins:redpen_-_pipeline_reporter_for_jira:*:*:*:*:*:jenkins:*:*"
+ ],
"pipeline-restful-api": [
"cpe:2.3:a:jenkins:pipeline_restful_api:*:*:*:*:*:jenkins:*:*"
],
@@ -2335,6 +2397,9 @@
"@backstage/techdocs-common": [
"cpe:2.3:a:linuxfoundation:\\@backstage\\/techdocs-common:*:*:*:*:*:node.js:*:*"
],
+ "@beproduct/nestjs-auth": [
+ "cpe:2.3:a:beproduct:beproduct\\/nestjs-auth:*:*:*:*:*:node.js:*:*"
+ ],
"@better-auth/oauth-provider": [
"cpe:2.3:a:better-auth:better-auth\\/oauth-provider:*:*:*:*:*:node.js:*:*"
],
@@ -2350,6 +2415,9 @@
"@chainsafe/libp2p-noise": [
"cpe:2.3:a:chainsafe:js-libp2p-noise:*:*:*:*:*:node.js:*:*"
],
+ "@chargeover/redoc": [
+ "cpe:2.3:a:chargeover:chargeover\\/redoc:*:*:*:*:*:node.js:*:*"
+ ],
"@ckeditor/ckeditor5-engine": [
"cpe:2.3:a:ckeditor:ckeditor5-engine:*:*:*:*:*:node.js:*:*"
],
@@ -2374,12 +2442,18 @@
"@ckeditor/ckeditor5-widget": [
"cpe:2.3:a:ckeditor:ckeditor5-widget:*:*:*:*:*:node.js:*:*"
],
+ "@clerk/backend": [
+ "cpe:2.3:a:clerk:clerk\\/backend:*:*:*:*:*:node.js:*:*"
+ ],
"@coding-solo/godot-mcp": [
"cpe:2.3:a:coding-solo:godot_mcp:*:*:*:*:*:*:*:*"
],
"@contentauth/c2pa-web": [
"cpe:2.3:a:adobe:c2pa-web:*:*:*:*:*:node.js:*:*"
],
+ "@continuedev/cli": [
+ "cpe:2.3:a:continue:continue:*:*:*:*:*:node.js:*:*"
+ ],
"@cookiex/deep": [
"cpe:2.3:a:cookiex-deep_project:cookiex-deep:*:*:*:*:*:node.js:*:*"
],
@@ -2392,15 +2466,36 @@
"@diez/generation": [
"cpe:2.3:a:haikuforteams:diez:*:*:*:*:*:node.js:*:*"
],
+ "@draftlab/auth": [
+ "cpe:2.3:a:matheuspergoli:draftlab\\/auth:*:*:*:*:*:node.js:*:*"
+ ],
+ "@draftlab/db": [
+ "cpe:2.3:a:matheuspergoli:draftlab\\/db:*:*:*:*:*:node.js:*:*"
+ ],
"@electron/packager": [
"cpe:2.3:a:electron:packager:*:*:*:*:*:node.js:*:*"
],
+ "@ensdomains/ens-contracts": [
+ "cpe:2.3:a:ens.domains:ethereum_name_service:*:*:*:*:*:node.js:*:*"
+ ],
"@evershop/evershop": [
"cpe:2.3:a:evershop:evershop:*:*:*:*:*:node.js:*:*"
],
+ "@fastify/accepts-serializer": [
+ "cpe:2.3:a:fastify:fastify\\/accepts-serializer:*:*:*:*:*:node.js:*:*"
+ ],
+ "@fastify/express": [
+ "cpe:2.3:a:fastify:fastify\\/express:*:*:*:*:*:node.js:*:*"
+ ],
+ "@fastify/http-proxy": [
+ "cpe:2.3:a:fastify:fastify\\/http-proxy:*:*:*:*:*:node.js:*:*"
+ ],
"@fastify/middie": [
"cpe:2.3:a:fastify:fastify\\/middie:*:*:*:*:*:node.js:*:*"
],
+ "@fastify/reply-from": [
+ "cpe:2.3:a:fastify:reply-from:*:*:*:*:*:node.js:*:*"
+ ],
"@fastly/js-compute": [
"cpe:2.3:a:fastly:js-compute:*:*:*:*:*:node.js:*:*"
],
@@ -2452,6 +2547,9 @@
"@keystonejs/keystone": [
"cpe:2.3:a:keystonejs:keystone:*:*:*:*:*:node.js:*:*"
],
+ "@kilocode/cli": [
+ "cpe:2.3:a:kilo:kilo_code_cli:*:*:*:*:*:node.js:*:*"
+ ],
"@knight-lab/timelinejs": [
"cpe:2.3:a:northwestern:timelinejs:*:*:*:*:*:*:*:*"
],
@@ -2464,6 +2562,18 @@
"@mattkrick/sanitize-svg": [
"cpe:2.3:a:sanitize-svg_project:sanitize-svg:*:*:*:*:*:node.js:*:*"
],
+ "@mesadev/rest": [
+ "cpe:2.3:a:mesa:mesadev\\/rest:*:*:*:*:*:node.js:*:*"
+ ],
+ "@mesadev/saguaro": [
+ "cpe:2.3:a:mesa:mesadev\\/saguaro:*:*:*:*:*:node.js:*:*"
+ ],
+ "@mesadev/sdk": [
+ "cpe:2.3:a:mesa:mesadev\\/sdk:*:*:*:*:*:node.js:*:*"
+ ],
+ "@microsoft/live-share-canvas": [
+ "cpe:2.3:a:microsoft:live_share_canvas:*:*:*:*:*:node.js:*:*"
+ ],
"@misskey-dev/summaly": [
"cpe:2.3:a:misskey:summaly:*:*:*:*:*:node.js:*:*"
],
@@ -2476,6 +2586,9 @@
"@nextcloud/dialogs": [
"cpe:2.3:a:nextcloud\\/dialogs_project:nextcloud\\/dialogs:*:*:*:*:*:node.js:*:*"
],
+ "@node-oauth/oauth2-server": [
+ "cpe:2.3:a:node-oauth:node-oauth\\/oauth2-server:*:*:*:*:*:node.js:*:*"
+ ],
"@node-saml/node-saml": [
"cpe:2.3:a:node_saml_project:node_saml:*:*:*:*:*:node.js:*:*"
],
@@ -2488,6 +2601,15 @@
"@nuxt/devalue": [
"cpe:2.3:a:nuxtjs:\\@nuxt\\/devalue:*:*:*:*:*:node.js:*:*"
],
+ "@nuxt/nitro-server": [
+ "cpe:2.3:a:nuxt:nuxt\\/nitro-server:*:*:*:*:*:node.js:*:*"
+ ],
+ "@nuxt/rspack-builder": [
+ "cpe:2.3:a:nuxt:nuxt\\/rspack-builder:*:*:*:*:*:node.js:*:*"
+ ],
+ "@nuxt/webpack-builder": [
+ "cpe:2.3:a:nuxt:nuxt\\/webpack-builder:*:*:*:*:*:node.js:*:*"
+ ],
"@nyariv/sandboxjs": [
"cpe:2.3:a:nyariv:sandboxjs:*:*:*:*:*:node.js:*:*"
],
@@ -2500,6 +2622,9 @@
"@opennextjs/cloudflare": [
"cpe:2.3:a:opennextjs:opennext_for_cloudflare:*:*:*:*:*:node.js:*:*"
],
+ "@opensearch-project/opensearch": [
+ "cpe:2.3:a:linuxfoundation:opensearch:*:*:*:*:*:node.js:*:*"
+ ],
"@openzeppelin/contracts": [
"cpe:2.3:a:openzeppelin:contracts:*:*:*:*:*:node.js:*:*"
],
@@ -2522,6 +2647,12 @@
"@progfay/scrapbox-parser": [
"cpe:2.3:a:scrapbox-parser_project:scrapbox-parser:*:*:*:*:*:node.js:*:*"
],
+ "@rage-against-the-pixel/unity-cli": [
+ "cpe:2.3:a:rageagainstthepixel:unity-cli:*:*:*:*:*:node.js:*:*"
+ ],
+ "@remix-run/server-runtime": [
+ "cpe:2.3:a:shopify:remix-run\\/server-runtime:*:*:*:*:*:node.js:*:*"
+ ],
"@rkesters/gnuplot": [
"cpe:2.3:a:gnuplot_project:gnuplot:*:*:*:*:*:node.js:*:*"
],
@@ -2552,6 +2683,12 @@
"@strikeentco/set": [
"cpe:2.3:a:set_project:set:*:*:*:*:*:node.js:*:*"
],
+ "@supersurkhet/cli": [
+ "cpe:2.3:a:abhishake1:supersurkhet\\/cli:*:*:*:*:*:node.js:*:*"
+ ],
+ "@supersurkhet/sdk": [
+ "cpe:2.3:a:abhishake1:supersurkhet\\/sdk:*:*:*:*:*:node.js:*:*"
+ ],
"@sveltejs/adapter-node": [
"cpe:2.3:a:svelte:adapter-node:*:*:*:*:*:node.js:*:*"
],
@@ -2564,6 +2701,9 @@
"@tarojs/taro": [
"cpe:2.3:a:taro:taro:*:*:*:*:*:node.js:*:*"
],
+ "@taskflow-corp/cli": [
+ "cpe:2.3:a:abhishake1:taskflow-corp\\/cli:*:*:*:*:*:node.js:*:*"
+ ],
"@thi.ng/egf": [
"cpe:2.3:a:\\@thi.ng\\/egf_project:\\@thi.ng\\/egf:*:*:*:*:*:node.js:*:*"
],
@@ -2576,6 +2716,9 @@
"@tiptap/extension-link": [
"cpe:2.3:a:tiptap:tiptap\\/extension-link:*:*:*:*:*:node.js:*:*"
],
+ "@tolka/cli": [
+ "cpe:2.3:a:christianalares:tolka\\/cli:*:*:*:*:*:node.js:*:*"
+ ],
"@tryghost/portal": [
"cpe:2.3:a:ghost:portal:*:*:*:*:*:node.js:*:*"
],
@@ -2909,6 +3052,9 @@
"chatbyvista": [
"cpe:2.3:a:chatbyvista_project:chatbyvista:*:*:*:*:*:node.js:*:*"
],
+ "chrome-devtools-mcp": [
+ "cpe:2.3:a:google:chrome-devtools-mcp:*:*:*:*:*:node.js:*:*"
+ ],
"chrome-launcher": [
"cpe:2.3:a:google:chrome-launcher:*:*:*:*:*:node.js:*:*"
],
@@ -2955,6 +3101,9 @@
"cmake": [
"cpe:2.3:a:cmake_project:cmake:*:*:*:*:*:node.js:*:*"
],
+ "cmux-agent-mcp": [
+ "cpe:2.3:a:multiagentcognition:cmux-agent-mcp:*:*:*:*:*:node.js:*:*"
+ ],
"co-cli-installer": [
"cpe:2.3:a:co-cli-installer_project:co-cli-installer:*:*:*:*:*:node.js:*:*"
],
@@ -3161,7 +3310,7 @@
"cpe:2.3:a:deep.assign_project:deep.assign:*:*:*:*:*:node.js:*:*"
],
"deephas": [
- "cpe:2.3:a:deephas_project:deephas:*:*:*:*:*:node.js:*:*"
+ "cpe:2.3:a:sharpred:deephas:*:*:*:*:*:node.js:*:*"
],
"deeply": [
"cpe:2.3:a:deeply_project:deeply:*:*:*:*:*:node.js:*:*"
@@ -3412,6 +3561,9 @@
"external-svg-loader": [
"cpe:2.3:a:shubhamjain:svg_loader:*:*:*:*:*:node.js:*:*"
],
+ "extract-zip": [
+ "cpe:2.3:a:max-mapper:extract-zip:*:*:*:*:*:node.js:*:*"
+ ],
"exxxxxxxxxxx": [
"cpe:2.3:a:exxxxxxxxxxx_project:exxxxxxxxxxx:*:*:*:*:*:node.js:*:*"
],
@@ -3595,9 +3747,15 @@
"git-archive": [
"cpe:2.3:a:git-archive_project:git-archive:*:*:*:*:*:node.js:*:*"
],
+ "git-branch-selector": [
+ "cpe:2.3:a:christianalares:git_branch_selector:*:*:*:*:*:node.js:*:*"
+ ],
"git-dummy-commit": [
"cpe:2.3:a:git-dummy-commit_project:git-dummy-commit:*:*:*:*:*:node.js:*:*"
],
+ "git-git-git": [
+ "cpe:2.3:a:christianalares:git-git-git:*:*:*:*:*:node.js:*:*"
+ ],
"git-parse": [
"cpe:2.3:a:wayfair:git-parse:*:*:*:*:*:node.js:*:*"
],
@@ -3828,6 +3986,9 @@
"http-proxy": [
"cpe:2.3:a:http-proxy_project:http-proxy:*:*:*:*:*:node.js:*:*"
],
+ "http-proxy-middleware": [
+ "cpe:2.3:a:chimurai:http-proxy-middleware:*:*:*:*:*:*:*:*"
+ ],
"http-proxy.js": [
"cpe:2.3:a:http-proxy.js_project:http-proxy.js:*:*:*:*:*:node.js:*:*"
],
@@ -3864,6 +4025,18 @@
"i18n-node-angular": [
"cpe:2.3:a:i18n-node-angular_project:i18n-node-angular:*:*:*:*:*:node.js:*:*"
],
+ "i18next-fs-backend": [
+ "cpe:2.3:a:i18next:i18next-fs-backend:*:*:*:*:*:node.js:*:*"
+ ],
+ "i18next-http-backend": [
+ "cpe:2.3:a:i18next:i18next-http-backend:*:*:*:*:*:node.js:*:*"
+ ],
+ "i18next-http-middleware": [
+ "cpe:2.3:a:i18next:i18next-http-middleware:*:*:*:*:*:node.js:*:*"
+ ],
+ "i18nextify": [
+ "cpe:2.3:a:i18next:i18nextify:*:*:*:*:*:node.js:*:*"
+ ],
"ibapi": [
"cpe:2.3:a:interactivebrokers:ibapi:*:*:*:*:*:node.js:*:*"
],
@@ -3927,6 +4100,9 @@
"ip": [
"cpe:2.3:a:fedorindutny:ip:*:*:*:*:*:node.js:*:*"
],
+ "ip-address": [
+ "cpe:2.3:a:beaugunderson:ip-address:*:*:*:*:*:node.js:*:*"
+ ],
"ipip": [
"cpe:2.3:a:ipip_project:ipip:*:*:*:*:*:node.js:*:*"
],
@@ -3986,7 +4162,7 @@
"cpe:2.3:a:joplin_project:joplin:*:*:*:*:*:node.js:*:*"
],
"jose": [
- "cpe:2.3:a:jose_project:jose:*:*:*:*:*:node.js:*:*"
+ "cpe:2.3:a:panva:jose:*:*:*:*:*:node.js:*:*"
],
"jpeg-js": [
"cpe:2.3:a:jpeg-js_project:jpeg-js:*:*:*:*:*:node.js:*:*"
@@ -4022,6 +4198,9 @@
"js-given": [
"cpe:2.3:a:js-given_project:js-given:*:*:*:*:*:node.js:*:*"
],
+ "js-toml": [
+ "cpe:2.3:a:sunnyadn:js-toml:*:*:*:*:*:node.js:*:*"
+ ],
"js-yaml": [
"cpe:2.3:a:nodeca:js-yaml:*:*:*:*:*:node.js:*:*"
],
@@ -4068,7 +4247,7 @@
"cpe:2.3:a:jsreport:jsreport:*:*:*:*:*:node.js:*:*"
],
"jsrsasign": [
- "cpe:2.3:a:jsrsasign_project:jsrsasign:*:*:*:*:*:node.js:*:*"
+ "cpe:2.3:a:kjur:jsrsasign:*:*:*:*:*:node.js:*:*"
],
"jstestdriver": [
"cpe:2.3:a:jstestdriver_project:jstestdriver:*:*:*:*:*:node.js:*:*"
@@ -4299,6 +4478,9 @@
"madlib-object-utils": [
"cpe:2.3:a:springtree:madlib-object-utils:*:*:*:*:*:node.js:*:*"
],
+ "magicmirror": [
+ "cpe:2.3:a:magicmirror:magicmirror:*:*:*:*:*:node.js:*:*"
+ ],
"mailparser": [
"cpe:2.3:a:nodemailer:mailparser:*:*:*:*:*:node.js:*:*"
],
@@ -4309,6 +4491,7 @@
"cpe:2.3:a:mapbox:mapbox.js:*:*:*:*:*:node.js:*:*"
],
"mariadb": [
+ "cpe:2.3:a:mariadb:mariadb:*:*:*:*:*:*:*:*",
"cpe:2.3:a:mariadb:mariadb:*:*:*:*:*:node.js:*:*"
],
"marionette-socket-host": [
@@ -4356,6 +4539,9 @@
"mc-kill-port": [
"cpe:2.3:a:mc-kill-port_project:mc-kill-port:*:*:*:*:*:node.js:*:*"
],
+ "mcp-from-openapi": [
+ "cpe:2.3:a:frontmcp:mcp-from-openapi:*:*:*:*:*:node.js:*:*"
+ ],
"mcp-server-kubernetes": [
"cpe:2.3:a:suyogs:mcp-server-kubernetes:*:*:*:*:*:node.js:*:*"
],
@@ -4606,6 +4792,9 @@
"next-auth": [
"cpe:2.3:a:nextauth.js:next-auth:*:*:*:*:*:node.js:*:*"
],
+ "nextmove-mcp": [
+ "cpe:2.3:a:christianalares:nextmove-mcp:*:*:*:*:*:node.js:*:*"
+ ],
"nis-utils": [
"cpe:2.3:a:nis-utils_project:nis-utils:*:*:*:*:*:node.js:*:*"
],
@@ -4722,6 +4911,9 @@
"node-stringbuilder": [
"cpe:2.3:a:magiclen:stringbuilder:*:*:*:*:*:node.js:*:*"
],
+ "node-tesseract-ocr": [
+ "cpe:2.3:a:zapolnoch:tesseract_ocr:*:*:*:*:*:node.js:*:*"
+ ],
"node-thulac": [
"cpe:2.3:a:geohey:node-thulac:*:*:*:*:*:node.js:*:*"
],
@@ -5085,6 +5277,9 @@
"protobufjs": [
"cpe:2.3:a:protobufjs_project:protobufjs:*:*:*:*:*:node.js:*:*"
],
+ "protobufjs-cli": [
+ "cpe:2.3:a:protobufjs_project:protobufjs-cli:*:*:*:*:*:node.js:*:*"
+ ],
"proxy": [
"cpe:2.3:a:proxy_project:proxy:*:*:*:*:*:node.js:*:*"
],
@@ -5263,6 +5458,9 @@
"s3-uploader": [
"cpe:2.3:a:s3-uploader_project:s3-uploader:*:*:*:*:*:node.js:*:*"
],
+ "safe-action": [
+ "cpe:2.3:a:matheuspergoli:simple_type-safe_actions:*:*:*:*:*:node.js:*:*"
+ ],
"safe-eval": [
"cpe:2.3:a:safe-eval_project:safe-eval:*:*:*:*:*:node.js:*:*"
],
@@ -5594,7 +5792,6 @@
"cpe:2.3:a:storybook:storybook:*:*:*:*:*:node.js:*:*"
],
"strapi": [
- "cpe:2.3:a:strapi:strapi:*:*:*:*:*:*:*:*",
"cpe:2.3:a:strapi:strapi:*:*:*:*:*:node.js:*:*"
],
"strider-sauce": [
@@ -5651,7 +5848,11 @@
],
"tar": [
"cpe:2.3:a:isaacs:tar:*:*:*:*:*:node.js:*:*",
- "cpe:2.3:a:tar_project:tar:*:*:*:*:*:node.js:*:*"
+ "cpe:2.3:a:tar_project:tar:*:*:*:*:*:node.js:*:*",
+ "cpe:2.3:a:tar_project:tar:*:*:*:*:*:rust:*:*"
+ ],
+ "tarteaucitronjs": [
+ "cpe:2.3:a:amauri:tarteaucitronjs:*:*:*:*:*:node.js:*:*"
],
"teddy": [
"cpe:2.3:a:teddy_project:teddy:*:*:*:*:*:node.js:*:*"
@@ -5698,6 +5899,9 @@
"tmock": [
"cpe:2.3:a:tmock_project:tmock:*:*:*:*:*:node.js:*:*"
],
+ "tmp": [
+ "cpe:2.3:a:raszi:tmp:*:*:*:*:*:node.js:*:*"
+ ],
"tmpl": [
"cpe:2.3:a:tmpl_project:tmpl:*:*:*:*:*:node.js:*:*"
],
@@ -5755,6 +5959,9 @@
"tsup": [
"cpe:2.3:a:egoist:tsup:*:*:*:*:*:node.js:*:*"
],
+ "turbo-stream": [
+ "cpe:2.3:a:turbo-stream:turbo_stream:*:*:*:*:*:node.js:*:*"
+ ],
"ua-parser": [
"cpe:2.3:a:ua-parser_project:ua-parser:*:*:*:*:*:node.js:*:*"
],
@@ -5866,6 +6073,12 @@
"vega-functions": [
"cpe:2.3:a:vega-functions_project:vega-functions:*:*:*:*:*:node.js:*:*"
],
+ "velocityjs": [
+ "cpe:2.3:a:shepherdwind:velocity.js:*:*:*:*:*:node.js:*:*"
+ ],
+ "vercel": [
+ "cpe:2.3:a:vercel:vercel:*:*:*:*:*:node.js:*:*"
+ ],
"vite": [
"cpe:2.3:a:vitejs:vite:*:*:*:*:*:node.js:*:*"
],
@@ -5903,6 +6116,9 @@
"webdriver-launcher": [
"cpe:2.3:a:webdriver-launcher_project:webdriver-launcher:*:*:*:*:*:node.js:*:*"
],
+ "webdriverio": [
+ "cpe:2.3:a:openjsf:webdriverio:*:*:*:*:*:node.js:*:*"
+ ],
"webdrvr": [
"cpe:2.3:a:webdrvr_project:webdrvr:*:*:*:*:*:node.js:*:*"
],
@@ -5942,6 +6158,9 @@
"wifiscanner": [
"cpe:2.3:a:thingssdk:wifiscanner:*:*:*:*:*:node.js:*:*"
],
+ "willitmerge": [
+ "cpe:2.3:a:dontkry:willitmerge:*:*:*:*:*:node.js:*:*"
+ ],
"wincred": [
"cpe:2.3:a:wincred_project:wincred:*:*:*:*:*:*:*:*"
],
@@ -6047,6 +6266,9 @@
"joomla/session": [
"cpe:2.3:a:joomla:session:*:*:*:*:*:*:*:*"
],
+ "mtdowling/jmespath.php": [
+ "cpe:2.3:a:jmespath:jmespath:*:*:*:*:*:php:*:*"
+ ],
"mustache/mustache": [
"cpe:2.3:a:mustache_project:mustache:*:*:*:*:*:php:*:*"
],
@@ -6160,6 +6382,9 @@
"XML2Dict": [
"cpe:2.3:a:xml2dict_project:xml2dict:*:*:*:*:*:python:*:*"
],
+ "adx-mcp-server": [
+ "cpe:2.3:a:pab1it0:azure_data_explorer_mcp_server:*:*:*:*:*:python:*:*"
+ ],
"agixt": [
"cpe:2.3:a:agixt:agixt:*:*:*:*:*:*:*:*"
],
@@ -6172,6 +6397,12 @@
"aniso8601": [
"cpe:2.3:a:aniso8601_project:aniso8601:*:*:*:*:*:*:*:*"
],
+ "apache-airflow-providers-amazon": [
+ "cpe:2.3:a:apache:apache-airflow-providers-amazon:*:*:*:*:*:*:*:*"
+ ],
+ "apache-airflow-providers-cncf-kubernetes": [
+ "cpe:2.3:a:apache:apache-airflow-providers-cncf-kubernetes:*:*:*:*:*:*:*:*"
+ ],
"api-res-py": [
"cpe:2.3:a:api-res-py_project:api-res-py:*:*:*:*:*:python:*:*"
],
@@ -6184,6 +6415,9 @@
"autobahn": [
"cpe:2.3:a:crossbar:autobahn:*:*:*:*:*:*:*:*"
],
+ "awslabs.aws-api-mcp-server": [
+ "cpe:2.3:a:amazon:aws_api_mcp_server:*:*:*:*:*:python:*:*"
+ ],
"azure-core": [
"cpe:2.3:a:microsoft:azure_core_shared_client_library:*:*:*:*:*:python:*:*"
],
@@ -6220,6 +6454,9 @@
"certifi": [
"cpe:2.3:a:certifi:certifi:*:*:*:*:*:python:*:*"
],
+ "chromadb": [
+ "cpe:2.3:a:trychroma:chromadb:*:*:*:*:*:python:*:*"
+ ],
"claude-agent-sdk": [
"cpe:2.3:a:anthropic:claude_agent_sdk:*:*:*:*:*:python:*:*"
],
@@ -6269,6 +6506,9 @@
"d8s-uuids": [
"cpe:2.3:a:democritus_uuids_project:democritus_uuids:*:*:*:*:*:python:*:*"
],
+ "dash-uploader": [
+ "cpe:2.3:a:fohrloop:dash-uploader:*:*:*:*:*:python:*:*"
+ ],
"datapizza-ai": [
"cpe:2.3:a:datapizza:datapizza_ai:*:*:*:*:*:*:*:*"
],
@@ -6284,6 +6524,9 @@
"django-user-sessions": [
"cpe:2.3:a:django-user-sessions_project:django-user-sessions:*:*:*:*:*:*:*:*"
],
+ "docling": [
+ "cpe:2.3:a:docling:docling:*:*:*:*:*:python:*:*"
+ ],
"docling-core": [
"cpe:2.3:a:docling:docling-core:*:*:*:*:*:python:*:*"
],
@@ -6350,6 +6593,9 @@
"guarddog": [
"cpe:2.3:a:datadoghq:guarddog:*:*:*:*:*:python:*:*"
],
+ "guardrails-ai": [
+ "cpe:2.3:a:guardrailsai:guardrails_ai:*:*:*:*:*:python:*:*"
+ ],
"hail": [
"cpe:2.3:a:hail:hail:*:*:*:*:*:python:*:*"
],
@@ -6372,6 +6618,9 @@
"hyper-bump-it": [
"cpe:2.3:a:plannigan:hyper_bump_it:*:*:*:*:*:python:*:*"
],
+ "idna": [
+ "cpe:2.3:a:kjd:internationalized_domain_names_in_applications:*:*:*:*:*:python:*:*"
+ ],
"invenio-records": [
"cpe:2.3:a:inveniosoftware:invenio-records:*:*:*:*:*:*:*:*"
],
@@ -6384,6 +6633,9 @@
"jwcrypto": [
"cpe:2.3:a:latchset:jwcrypto:*:*:*:*:*:*:*:*"
],
+ "kafka-python": [
+ "cpe:2.3:a:dpkp:kafka-python:*:*:*:*:*:python:*:*"
+ ],
"kedro": [
"cpe:2.3:a:linuxfoundation:kedro:*:*:*:*:*:python:*:*"
],
@@ -6402,9 +6654,15 @@
"langchain-experimental": [
"cpe:2.3:a:langchain:langchain_experimental:*:*:*:*:*:python:*:*"
],
+ "langflow": [
+ "cpe:2.3:a:langflow:langflow:*:*:*:*:*:*:*:*"
+ ],
"langflow-base": [
"cpe:2.3:a:langflow:langflow-base:*:*:*:*:*:python:*:*"
],
+ "langgraph-checkpoint": [
+ "cpe:2.3:a:langchain:langgraph-checkpoint:*:*:*:*:*:python:*:*"
+ ],
"lerobot": [
"cpe:2.3:a:huggingface:lerobot:*:*:*:*:*:python:*:*"
],
@@ -6438,12 +6696,21 @@
"mcp-memory-service": [
"cpe:2.3:a:doobidoo:mcp-memory-service:*:*:*:*:*:*:*:*"
],
+ "megatron-bridge": [
+ "cpe:2.3:a:nvidia:nemo_megatron_bridge:*:*:*:*:*:*:*:*"
+ ],
+ "mistralai": [
+ "cpe:2.3:a:mistral:mistralai:*:*:*:*:*:python:*:*"
+ ],
"mltable": [
"cpe:2.3:a:microsoft:azure_machine_learning_software_development_kit:*:*:*:*:*:*:*:*"
],
"mpxj": [
"cpe:2.3:a:mpxj:mpxj:*:*:*:*:*:python:*:*"
],
+ "nanobot-ai": [
+ "cpe:2.3:a:nanobot:nanobot:*:*:*:*:*:python:*:*"
+ ],
"nbconvert": [
"cpe:2.3:a:jupyter:nbconvert:*:*:*:*:*:python:*:*"
],
@@ -6453,6 +6720,9 @@
"networkx": [
"cpe:2.3:a:python:networkx:*:*:*:*:*:*:*:*"
],
+ "nltk": [
+ "cpe:2.3:a:nltk:nltk:*:*:*:*:*:*:*:*"
+ ],
"novajoin": [
"cpe:2.3:a:python:novajoin:*:*:*:*:*:*:*:*"
],
@@ -6579,6 +6849,9 @@
"pymdown-extensions": [
"cpe:2.3:a:facelessuser:pymdown_extensions:*:*:*:*:*:*:*:*"
],
+ "pymetasploit3": [
+ "cpe:2.3:a:danmcinerney:pymetasploit3:*:*:*:*:*:*:*:*"
+ ],
"pyo": [
"cpe:2.3:a:pyo_project:pyo:*:*:*:*:*:*:*:*"
],
@@ -6591,6 +6864,9 @@
"pypolicyd-spf": [
"cpe:2.3:a:pypolicyd-spf_project:pypolicyd-spf:*:*:*:*:*:*:*:*"
],
+ "pyspector": [
+ "cpe:2.3:a:parzivalhack:pyspector:*:*:*:*:*:python:*:*"
+ ],
"python-dotenv": [
"cpe:2.3:a:saurabh-kumar:python-dotenv:*:*:*:*:*:python:*:*"
],
@@ -6606,12 +6882,18 @@
"python-libnmap": [
"cpe:2.3:a:python-libnmap_project:python-libnmap:*:*:*:*:*:python:*:*"
],
+ "python-liquid": [
+ "cpe:2.3:a:jg-rp:python_liquid:*:*:*:*:*:python:*:*"
+ ],
"python-multipart": [
"cpe:2.3:a:fastapiexpert:python-multipart:*:*:*:*:*:python:*:*"
],
"python-scciclient": [
"cpe:2.3:a:python-scciclient_project:python-scciclient:*:*:*:*:*:python:*:*"
],
+ "python-statemachine": [
+ "cpe:2.3:a:fgmacedo:python_statemachine:*:*:*:*:*:python:*:*"
+ ],
"pytorch-lightning": [
"cpe:2.3:a:lightningai:pytorch_lightning:*:*:*:*:*:python:*:*"
],
@@ -6699,6 +6981,9 @@
"strawberry-graphql": [
"cpe:2.3:a:strawberry:strawberry_graphql:*:*:*:*:*:python:*:*"
],
+ "streamlink": [
+ "cpe:2.3:a:streamlink:streamlink:*:*:*:*:*:python:*:*"
+ ],
"tablib": [
"cpe:2.3:a:python:tablib:*:*:*:*:*:*:*:*"
],
@@ -6744,6 +7029,9 @@
"vyper": [
"cpe:2.3:a:vyperlang:vyper:*:*:*:*:*:python:*:*"
],
+ "web3": [
+ "cpe:2.3:a:apeworx:web3.py:*:*:*:*:*:python:*:*"
+ ],
"wikifaces": [
"cpe:2.3:a:wikifaces_project:wikifaces:*:*:*:*:*:pypi:*:*"
],
@@ -6836,6 +7124,9 @@
"commonmarker": [
"cpe:2.3:a:gjtorikian:commonmarker:*:*:*:*:*:ruby:*:*"
],
+ "concurrent-ruby": [
+ "cpe:2.3:a:rubyconcurrency:concurrent_ruby:*:*:*:*:*:ruby:*:*"
+ ],
"consul": [
"cpe:2.3:a:makandra:consul:*:*:*:*:*:ruby:*:*"
],
@@ -6948,6 +7239,9 @@
"iodine": [
"cpe:2.3:a:boazsegev:iodine:*:*:*:*:*:ruby:*:*"
],
+ "jmespath": [
+ "cpe:2.3:a:jmespath:jmespath:*:*:*:*:*:ruby:*:*"
+ ],
"json": [
"cpe:2.3:a:json_project:json:*:*:*:*:*:ruby:*:*",
"cpe:2.3:a:ruby-lang:json:*:*:*:*:*:ruby:*:*"
@@ -7246,6 +7540,9 @@
],
"websocket-extensions": [
"cpe:2.3:a:websocket-extensions_project:websocket-extensions:*:*:*:*:*:ruby:*:*"
+ ],
+ "zlib": [
+ "cpe:2.3:a:ruby-lang:zlib:*:*:*:*:*:ruby:*:*"
]
},
"rust_crates": {
@@ -7312,6 +7609,9 @@
"asn1_der": [
"cpe:2.3:a:asn1_der_project:asn1_der:*:*:*:*:*:*:*:*"
],
+ "astral-tokio-tar": [
+ "cpe:2.3:a:astral:astral-tokio-tar:*:*:*:*:*:rust:*:*"
+ ],
"async-h1": [
"cpe:2.3:a:rust-lang:async-h1:*:*:*:*:*:rust:*:*"
],
@@ -7642,6 +7942,9 @@
"obstack": [
"cpe:2.3:a:obstack_project:obstack:*:*:*:*:*:rust:*:*"
],
+ "one2html": [
+ "cpe:2.3:a:msiemens:one2html:*:*:*:*:*:rust:*:*"
+ ],
"outer_cgi": [
"cpe:2.3:a:outer_cgi_project:outer_cgi:*:*:*:*:*:rust:*:*"
],
@@ -7744,6 +8047,9 @@
"smallvec": [
"cpe:2.3:a:servo:smallvec:*:*:*:*:*:rust:*:*"
],
+ "spiral-rs": [
+ "cpe:2.3:a:blyssprivacy:spiral-rs:*:*:*:*:*:rust:*:*"
+ ],
"stack": [
"cpe:2.3:a:stack_project:stack:*:*:*:*:*:rust:*:*"
],
@@ -7861,6 +8167,9 @@
"zebra-consensus": [
"cpe:2.3:a:zfnd:zebra-consensus:*:*:*:*:*:rust:*:*"
],
+ "zebra-network": [
+ "cpe:2.3:a:zfnd:zebra-network:*:*:*:*:*:rust:*:*"
+ ],
"zebra-rpc": [
"cpe:2.3:a:zfnd:zebra-rpc:*:*:*:*:*:rust:*:*"
],
@@ -8807,6 +9116,9 @@
"aprils-super-functions-pack": [
"cpe:2.3:a:april\\'s_super_functions_pack_project:april\\'s_super_functions_pack:*:*:*:*:*:wordpress:*:*"
],
+ "aptivada-for-wp": [
+ "cpe:2.3:a:aptivada:aptivada_for_wp:*:*:*:*:*:wordpress:*:*"
+ ],
"apus-framework": [
"cpe:2.3:a:apusthemes:superio:*:*:*:*:*:wordpress:*:*"
],
@@ -9408,6 +9720,9 @@
"bluet-keywords-tooltip-generator": [
"cpe:2.3:a:tooltipy:tooltipy:*:*:*:*:*:wordpress:*:*"
],
+ "bluetrait-event-viewer": [
+ "cpe:2.3:a:bluetrait:blue_trait_event_viewer:*:*:*:*:*:wordpress:*:*"
+ ],
"bmi-bmr-calculator": [
"cpe:2.3:a:bmi_bmr_calculator_project:bmi_bmr_calculator:*:*:*:*:*:wordpress:*:*"
],
@@ -10081,6 +10396,9 @@
"christian-science-bible-lesson-subjects": [
"cpe:2.3:a:sharethepractice:christian_science_bible_lesson_subjects:*:*:*:*:*:wordpress:*:*"
],
+ "christmasify": [
+ "cpe:2.3:a:cyberfoxdigital:christmasify\\!:*:*:*:*:*:wordpress:*:*"
+ ],
"chronosly-events-calendar": [
"cpe:2.3:a:chronosly-events-calendar_project:chronosly-events-calendar:*:*:*:*:*:wordpress:*:*"
],
@@ -10120,7 +10438,7 @@
"cpe:2.3:a:cm-wp:clearfy:*:*:*:*:*:wordpress:*:*"
],
"clerkio": [
- "cpe:2.3:a:clerk:clerk.io:*:*:*:*:*:wordpress:*:*"
+ "cpe:2.3:a:clerk.io:clerk.io:*:*:*:*:*:wordpress:*:*"
],
"clever-fox": [
"cpe:2.3:a:nayrathemes:clever_fox:*:*:*:*:*:wordpress:*:*"
@@ -10458,6 +10776,9 @@
"contact-form-manager": [
"cpe:2.3:a:xyzscripts:contact_form_manager:*:*:*:*:*:wordpress:*:*"
],
+ "contact-form-master": [
+ "cpe:2.3:a:edmonparker:contact_form_master:*:*:*:*:*:wordpress:*:*"
+ ],
"contact-form-multi": [
"cpe:2.3:a:bestwebsoft:contact_form_multi:*:*:*:*:*:wordpress:*:*"
],
@@ -10651,6 +10972,9 @@
"corner-ad": [
"cpe:2.3:a:dwbooster:corner_ad:*:*:*:*:*:wordpress:*:*"
],
+ "coronavirus-covid-19-notice-message": [
+ "cpe:2.3:a:gallagherwebsitedesign:coronavirus_\\(covid-19\\)_notice_message:*:*:*:*:*:wordpress:*:*"
+ ],
"correos-express": [
"cpe:2.3:a:correosexpress_project:correosexpress:*:*:*:*:*:wordpress:*:*"
],
@@ -10904,6 +11228,9 @@
"custom-field-for-wp-job-manager": [
"cpe:2.3:a:custom_field_for_wp_job_manager_project:custom_field_for_wp_job_manager:*:*:*:*:*:wordpress:*:*"
],
+ "custom-field-manager": [
+ "cpe:2.3:a:f1logic:custom_field_manager:*:*:*:*:*:wordpress:*:*"
+ ],
"custom-field-suite": [
"cpe:2.3:a:custom_field_suite_project:custom_field_suite:*:*:*:*:*:wordpress:*:*"
],
@@ -11032,6 +11359,9 @@
"cysteme-finder": [
"cpe:2.3:a:cysteme:cysteme-finder:*:*:*:*:*:wordpress:*:*"
],
+ "cz-loan-management": [
+ "cpe:2.3:a:contrive:cz_loan_management:*:*:*:*:*:wordpress:*:*"
+ ],
"daext-autolinks-manager": [
"cpe:2.3:a:daext:autolinks_manager:*:*:*:*:*:wordpress:*:*"
],
@@ -11203,6 +11533,9 @@
"directorypress-frontend": [
"cpe:2.3:a:designinvento:directorypress:*:*:*:*:*:wordpress:*:*"
],
+ "disable-auto-updates": [
+ "cpe:2.3:a:exeebit:disable_auto_updates:*:*:*:*:*:wordpress:*:*"
+ ],
"disable-comments": [
"cpe:2.3:a:disable_comments:disable_comments_project:*:*:*:*:*:wordpress:*:*"
],
@@ -11242,6 +11575,12 @@
"dk-pricr-responsive-pricing-table": [
"cpe:2.3:a:wpdarko:responsive_pricing_table:*:*:*:*:*:wordpress:*:*"
],
+ "dl-robotstxt": [
+ "cpe:2.3:a:dyadyalesha:dl_robots.txt:*:*:*:*:*:wordpress:*:*"
+ ],
+ "dl-verification": [
+ "cpe:2.3:a:dyadyalesha:dl_verification:*:*:*:*:*:wordpress:*:*"
+ ],
"dmsguestbook": [
"cpe:2.3:a:dmsguestbook_project:dmsguestbook:*:*:*:*:*:wordpress:*:*"
],
@@ -11622,6 +11961,9 @@
"cpe:2.3:a:flowdee:easyazon:*:*:*:*:*:*:*:*",
"cpe:2.3:a:getaawp:easyazon:*:*:*:*:*:wordpress:*:*"
],
+ "easyevent": [
+ "cpe:2.3:a:dwalliance:easyevent:*:*:*:*:*:wordpress:*:*"
+ ],
"easyjobs": [
"cpe:2.3:a:easy.jobs:easy.jobs:*:*:*:*:*:wordpress:*:*"
],
@@ -12394,6 +12736,9 @@
"cpe:2.3:a:octolize:woocommerce_ups_shipping:*:*:*:*:free:wordpress:*:*",
"cpe:2.3:a:octolize:woocommerce_ups_shipping:*:*:*:*:pro:wordpress:*:*"
],
+ "flickr-gallery": [
+ "cpe:2.3:a:dancoulter:flickr_gallery:*:*:*:*:*:wordpress:*:*"
+ ],
"flickr-justified-gallery": [
"cpe:2.3:a:flickr_justified_gallery_project:flickr_justified_gallery:*:*:*:*:*:wordpress:*:*"
],
@@ -12540,6 +12885,9 @@
"formality": [
"cpe:2.3:a:giorgi:formality:*:*:*:*:*:wordpress:*:*"
],
+ "formassembly-web-forms": [
+ "cpe:2.3:a:formassembly:wp-formassembly:*:*:*:*:*:wordpress:*:*"
+ ],
"formbuilder": [
"cpe:2.3:a:formbuilder_project:formbuilder:*:*:*:*:*:wordpress:*:*"
],
@@ -12677,6 +13025,9 @@
"furikake": [
"cpe:2.3:a:furikake_project:furikake:*:*:*:*:*:wordpress:*:*"
],
+ "fuse-social-floating-sidebar": [
+ "cpe:2.3:a:daniyalahmedk:fuse_social_floating_sidebar:*:*:*:*:*:wordpress:*:*"
+ ],
"fusedesk": [
"cpe:2.3:a:jeremyshapiro:fusedesk:*:*:*:*:*:wordpress:*:*"
],
@@ -12831,6 +13182,9 @@
"gerryworks-post-by-mail": [
"cpe:2.3:a:gerryntabuhashe:gerryworks_post_by_mail:*:*:*:*:*:wordpress:*:*"
],
+ "gestpay-for-woocommerce": [
+ "cpe:2.3:a:fabrick:gestpay_for_woocommerce:*:*:*:*:*:wordpress:*:*"
+ ],
"get-custom-field-values": [
"cpe:2.3:a:get_custom_field_values_project:get_custom_field_values:*:*:*:*:*:wordpress:*:*"
],
@@ -12861,6 +13215,9 @@
"gf-custom-style": [
"cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*"
],
+ "gf-zoho": [
+ "cpe:2.3:a:crmperks:wp_gravity_forms_zoho_crm_and_bigin:*:*:*:*:*:wordpress:*:*"
+ ],
"gg-woo-feed": [
"cpe:2.3:a:gutengeek:gg_woo_feed:*:*:*:*:*:wordpress:*:*"
],
@@ -13706,6 +14063,9 @@
"inpost-gallery": [
"cpe:2.3:a:pluginus:inpost_gallery:*:*:*:*:*:wordpress:*:*"
],
+ "inprosysmedia-likes-dislikes-post": [
+ "cpe:2.3:a:erumfaham:likes_and_dislikes:*:*:*:*:*:wordpress:*:*"
+ ],
"insert-estimated-reading-time": [
"cpe:2.3:a:nigauri:insert_estimated_reading_time:*:*:*:*:*:wordpress:*:*"
],
@@ -14597,6 +14957,9 @@
"magical-addons-for-elementor": [
"cpe:2.3:a:wpthemespace:magical_addons_for_elementor:*:*:*:*:*:wordpress:*:*"
],
+ "magicform": [
+ "cpe:2.3:a:dcooperman:magicform:*:*:*:*:*:wordpress:*:*"
+ ],
"mail-control": [
"cpe:2.3:a:instareza:mail_control:*:*:*:*:*:wordpress:*:*"
],
@@ -14664,6 +15027,7 @@
"cpe:2.3:a:mainwp:mainwp_child:*:*:*:*:*:wordpress:*:*"
],
"mainwp-child-reports": [
+ "cpe:2.3:a:mainwp:mainwp_child:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:mainwp:mainwp_child_reports:*:*:*:*:*:wordpress:*:*"
],
"majestic-support": [
@@ -14776,6 +15140,9 @@
"masterstudy-lms-learning-management-system": [
"cpe:2.3:a:stylemixthemes:masterstudy_lms:*:*:*:*:*:wordpress:*:*"
],
+ "masy-gallery": [
+ "cpe:2.3:a:dineshrawat:masy_gallery:*:*:*:*:*:wordpress:*:*"
+ ],
"material-design-for-contact-form-7": [
"cpe:2.3:a:material_design_for_contact_form_7_project:material_design_for_contact_form_7:*:*:*:*:*:wordpress:*:*"
],
@@ -16013,6 +16380,9 @@
"personalize-woocommerce-cart-page": [
"cpe:2.3:a:najeebmedia:personalized_woocommerce_cart_page:*:*:*:*:*:wordpress:*:*"
],
+ "pet-manager": [
+ "cpe:2.3:a:dianakcury:pet_manager:*:*:*:*:*:wordpress:*:*"
+ ],
"peters-collaboration-e-mails": [
"cpe:2.3:a:peter\\'s_collaboration_e-mails_project:peter\\'s_collaboration_e-mails:*:*:*:*:*:wordpress:*:*"
],
@@ -16090,6 +16460,9 @@
"phpfreechat": [
"cpe:2.3:a:frumph:phpfreechat:*:*:*:*:*:wordpress:*:*"
],
+ "phpinfo-wp": [
+ "cpe:2.3:a:exeebit:phpinfo-wp:*:*:*:*:free:wordpress:*:*"
+ ],
"pi-woocommerce-order-date-time-and-type": [
"cpe:2.3:a:piwebsolution:pi-woocommerce-order-date-time-and-type:*:*:*:*:*:wordpress:*:*"
],
@@ -16142,6 +16515,9 @@
"plexx-elementor-extension": [
"cpe:2.3:a:themeworm:plexx_elementor_extension:*:*:*:*:*:wordpress:*:*"
],
+ "plugin-groups": [
+ "cpe:2.3:a:davidcramer:plugin_groups:*:*:*:*:*:wordpress:*:*"
+ ],
"plugin-logic": [
"cpe:2.3:a:plugin_logic_project:plugin_logic:*:*:*:*:*:wordpress:*:*"
],
@@ -16943,6 +17319,9 @@
"cpe:2.3:a:redirection:redirection:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:redirection_project:redirection:*:*:*:*:*:wordpress:*:*"
],
+ "redirects": [
+ "cpe:2.3:a:declaire:redirects:*:*:*:*:*:wordpress:*:*"
+ ],
"redux-framework": [
"cpe:2.3:a:redux:gutenberg_template_library_\\\u0026_redux_framework:*:*:*:*:*:wordpress:*:*"
],
@@ -17050,6 +17429,9 @@
"responsive-css-editor": [
"cpe:2.3:a:wpwox:responsive_css_editor:*:*:*:*:*:wordpress:*:*"
],
+ "responsive-food-and-drink-menu": [
+ "cpe:2.3:a:corporatezen:responsive_food_and_drink_menu:*:*:*:*:*:wordpress:*:*"
+ ],
"responsive-gallery-grid": [
"cpe:2.3:a:bdwm:responsive_gallery_grid:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:responsive_gallery_grid_project:responsive_gallery_grid:*:*:*:*:*:wordpress:*:*"
@@ -17531,6 +17913,7 @@
"cpe:2.3:a:fabiantodt:private_post_share:*:*:*:*:*:wordpress:*:*"
],
"share-buttons": [
+ "cpe:2.3:a:artlosk:share_buttons:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:artlosk:social_share_buttons:*:*:*:*:*:wordpress:*:*"
],
"share-on-diaspora": [
@@ -17690,6 +18073,9 @@
"simple-al-slider": [
"cpe:2.3:a:alexdtn:simple_al_slider:*:*:*:*:*:wordpress:*:*"
],
+ "simple-amazon-affiliate": [
+ "cpe:2.3:a:duogeek:simple_amazon_affiliate:*:*:*:*:*:wordpress:*:*"
+ ],
"simple-author-box": [
"cpe:2.3:a:webfactoryltd:simple_author_box:*:*:*:*:*:wordpress:*:*"
],
@@ -18100,6 +18486,9 @@
"slide-anything": [
"cpe:2.3:a:simonpedge:slide_anything:*:*:*:*:*:wordpress:*:*"
],
+ "slidedeck-lite-for-wordpress": [
+ "cpe:2.3:a:dtelepathy:slidedeck_1_lite_content_slider:*:*:*:*:*:wordpress:*:*"
+ ],
"slidedeck2": [
"cpe:2.3:a:hbwsl:slidedeck_2:*:*:*:*:lite:wordpress:*:*"
],
@@ -18169,6 +18558,9 @@
"smart-cookie-kit": [
"cpe:2.3:a:nicolamodugno:smart_cookie_kit:*:*:*:*:*:wordpress:*:*"
],
+ "smart-docs": [
+ "cpe:2.3:a:archalj:smart_docs:*:*:*:*:*:wordpress:*:*"
+ ],
"smart-donations": [
"cpe:2.3:a:rednao:donations_made_easy_-_smart_donations:*:*:*:*:*:wordpress:*:*"
],
@@ -18488,6 +18880,9 @@
"stars-rating": [
"cpe:2.3:a:stars_rating_project:stars_rating:*:*:*:*:*:wordpress:*:*"
],
+ "starter-templates": [
+ "cpe:2.3:a:fancywp:starter_templates:*:*:*:*:*:wordpress:*:*"
+ ],
"startklar-elmentor-forms-extwidgets": [
"cpe:2.3:a:web-shop-host:startklar_elmentor_addons:*:*:*:*:*:wordpress:*:*"
],
@@ -19237,6 +19632,9 @@
"twitter-cards-meta": [
"cpe:2.3:a:wpdeveloper:twitter_cards_meta:*:*:*:*:*:wordpress:*:*"
],
+ "twitter-follow": [
+ "cpe:2.3:a:firecask:twitter_follow_button:*:*:*:*:*:wordpress:*:*"
+ ],
"twitter-friends-widget": [
"cpe:2.3:a:twitter_friends_widget_project:twitter_friends_widget:*:*:*:*:*:wordpress:*:*"
],
@@ -19476,6 +19874,9 @@
"upunzipper": [
"cpe:2.3:a:upunzipper_project:upunzipper:*:*:*:*:*:wordpress:*:*"
],
+ "url-media-uploader": [
+ "cpe:2.3:a:apprhyme:url_media_uploader:*:*:*:*:*:wordpress:*:*"
+ ],
"url-shortify": [
"cpe:2.3:a:kaizencoders:url_shortify:*:*:*:*:*:wordpress:*:*"
],
@@ -19854,6 +20255,9 @@
"wds-multisite-aggregate": [
"cpe:2.3:a:webdevstudios:wds_multisite_aggregate:*:*:*:*:*:wordpress:*:*"
],
+ "we-testimonial-slider": [
+ "cpe:2.3:a:wordpresteem:we_-_testimonial_slide:*:*:*:*:*:wordpress:*:*"
+ ],
"weather-effect": [
"cpe:2.3:a:awplife:weather_effect:*:*:*:*:*:wordpress:*:*"
],
@@ -20507,6 +20911,9 @@
"workreap": [
"cpe:2.3:a:amentotech:workreap:*:*:*:*:*:wordpress:*:*"
],
+ "wow-carousel-for-divi-lite": [
+ "cpe:2.3:a:elegantthemes:divi_carousel:*:*:*:*:free:wordpress:*:*"
+ ],
"wow-moodboard-lite": [
"cpe:2.3:a:wownmedia:wow_moodboard:*:*:*:*:lite:wordpress:*:*"
],
@@ -20764,6 +21171,9 @@
"wp-crm": [
"cpe:2.3:a:usabilitydynamics:wp-crm:*:*:*:*:*:wordpress:*:*"
],
+ "wp-crm-system": [
+ "cpe:2.3:a:wp-crm:wp-crm_system:*:*:*:*:*:wordpress:*:*"
+ ],
"wp-crowdfunding": [
"cpe:2.3:a:themeum:wp_crowdfunding:*:*:*:*:*:wordpress:*:*"
],
@@ -20879,6 +21289,9 @@
"wp-easy-contact": [
"cpe:2.3:a:emarketdesign:wp_easy_contact:*:*:*:*:*:wordpress:*:*"
],
+ "wp-easy-events": [
+ "cpe:2.3:a:emarketdesign:event_rsvp_and_simple_event_management:*:*:*:*:*:wordpress:*:*"
+ ],
"wp-easy-gallery": [
"cpe:2.3:a:plugingarden:wp_easy_gallery:*:*:*:*:*:wordpress:*:*"
],
@@ -21243,6 +21656,9 @@
"cpe:2.3:a:3cx:live_chat:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:3cx:wp-live_chat:*:*:*:*:*:wordpress:*:*"
],
+ "wp-login-control": [
+ "cpe:2.3:a:forsyspress:wp_login_control:*:*:*:*:*:wordpress:*:*"
+ ],
"wp-login-security-and-history": [
"cpe:2.3:a:clogica:wp_login_security_and_history:*:*:*:*:*:wordpress:*:*"
],
@@ -21391,6 +21807,9 @@
"wp-photo-text-slider-50": [
"cpe:2.3:a:gopiplus:wp_photo_text_slider_50:*:*:*:*:*:wordpress:*:*"
],
+ "wp-photonav": [
+ "cpe:2.3:a:fmos:wp-photonav:*:*:*:*:*:wordpress:*:*"
+ ],
"wp-phpmyadmin-extension": [
"cpe:2.3:a:puvox:wp_phpmyadmin:*:*:*:*:*:wordpress:*:*"
],
@@ -22138,6 +22557,9 @@
"wpschoolpress": [
"cpe:2.3:a:igexsolutions:wpschoolpress:*:*:*:*:*:wordpress:*:*"
],
+ "wpshop": [
+ "cpe:2.3:a:eoxia:wpshop_2:*:*:*:*:*:wordpress:*:*"
+ ],
"wpshopgermany-it-recht-kanzlei": [
"cpe:2.3:a:maennchen1:wpshopgermany_it-recht_kanzlei:*:*:*:*:*:wordpress:*:*"
],
@@ -22722,6 +23144,9 @@
"hello-agency": [
"cpe:2.3:a:cozythemes:hello_agency:*:*:*:*:*:wordpress:*:*"
],
+ "homey": [
+ "cpe:2.3:a:favethemes:homey:*:*:*:*:*:wordpress:*:*"
+ ],
"hotel-galaxy": [
"cpe:2.3:a:webdzier:hotel_galaxy:*:*:*:*:*:wordpress:*:*"
],
From e4f5df936ced46274c610bb0e3fae234a0035e6b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 6 Jul 2026 10:59:47 -0400
Subject: [PATCH 048/168] chore(deps): bump
anchore/workflows/.github/workflows/release-install-script.yaml (#5031)
Bumps [anchore/workflows/.github/workflows/release-install-script.yaml](https://github.com/anchore/workflows) from 0.7.2 to 0.8.0.
- [Release notes](https://github.com/anchore/workflows/releases)
- [Commits](https://github.com/anchore/workflows/compare/b0c30a80409130d329aaa356fd64a34d8c0b3375...7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4)
---
updated-dependencies:
- dependency-name: anchore/workflows/.github/workflows/release-install-script.yaml
dependency-version: 0.8.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/release.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 5d13d2565..90776175a 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -119,7 +119,7 @@ jobs:
if: ${{ always() && (needs.release.result == 'success' || github.event.inputs.phase == 'install-script-only') }}
permissions:
contents: read # required for the reusable workflow to check out the repo and publish the install script
- uses: anchore/workflows/.github/workflows/release-install-script.yaml@b0c30a80409130d329aaa356fd64a34d8c0b3375 # v0.7.2
+ uses: anchore/workflows/.github/workflows/release-install-script.yaml@7212994dc8fc3a53fe9c8e766ab5b4ddd16ea3d4 # v0.8.0
with:
tag: ${{ github.event.inputs.version }}
secrets:
From f1457f255424730655c8b024c1febfdd722be49f Mon Sep 17 00:00:00 2001
From: Alex Goodman
Date: Mon, 6 Jul 2026 11:01:44 -0400
Subject: [PATCH 049/168] pin php apache fixture to debian snapshot (#5043)
the plain version pin drifted out from under us: debian mirrors only keep
the latest point release, so 8.2.31 became uninstallable once 8.2.32 shipped
and apt pulled the unpinned transitive php8.2-cli at 8.2.32. pointing apt at
snapshot.debian.org keeps the exact point release installable so the fixture
(and its test expectations) stay reproducible.
Signed-off-by: Alex Goodman
---
.../cataloger/php/testdata/image-apache/Dockerfile | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile b/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile
index 4484d04bc..409677dec 100644
--- a/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile
+++ b/syft/pkg/cataloger/php/testdata/image-apache/Dockerfile
@@ -1,5 +1,19 @@
FROM --platform=linux/amd64 httpd:2.4.63-bookworm AS builder
+# pin apt to a debian snapshot so the exact php8.2 point release stays installable.
+# a plain version pin drifts out from under us: debian's mirrors only keep the latest
+# point release, so 8.2.31 disappears once 8.2.32 ships. snapshot.debian.org archives
+# every version forever, which is what actually keeps the fixture (and its test
+# expectations) reproducible. timestamp is any point where 8.2.31 was current
+# (8.2.31: 2026-05-08, 8.2.32: 2026-07-04).
+ARG SNAPSHOT=20260701T000000Z
+RUN rm -f /etc/apt/sources.list /etc/apt/sources.list.d/debian.sources && \
+ printf '%s\n' \
+ "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/${SNAPSHOT}/ bookworm main" \
+ "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/${SNAPSHOT}/ bookworm-updates main" \
+ "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/${SNAPSHOT}/ bookworm-security main" \
+ > /etc/apt/sources.list
+
# pin php8.2 packages to avoid version drift when debian publishes security updates
ARG PHP_VERSION=8.2.31-1~deb12u1
RUN apt update -y && apt install -y \
From aebdf4bff444e8c0e16938203bd8371cd218b7e0 Mon Sep 17 00:00:00 2001
From: Synvoya
Date: Tue, 7 Jul 2026 01:13:20 +1000
Subject: [PATCH 050/168] Fix mix.lock git/path deps mislabeled as hex.pm with
bogus PURL (#5041)
parseMixLock read the version from a hex-only token position and hardcoded a pkg:hex PURL, so git/path deps got a URL-fragment version and a false hex.pm PURL (false CVE matches). Branch on the source atom: git uses the commit SHA, path an empty version, and pkg:hex PURLs are emitted only for hex deps.
Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
---
syft/pkg/cataloger/elixir/package.go | 18 +++++++++--
syft/pkg/cataloger/elixir/parse_mix_lock.go | 30 ++++++++++++++++++-
.../cataloger/elixir/parse_mix_lock_test.go | 28 +++++++++++++++++
syft/pkg/cataloger/elixir/testdata/mix.lock | 2 ++
4 files changed, 74 insertions(+), 4 deletions(-)
diff --git a/syft/pkg/cataloger/elixir/package.go b/syft/pkg/cataloger/elixir/package.go
index 50cd3f28f..54fb21193 100644
--- a/syft/pkg/cataloger/elixir/package.go
+++ b/syft/pkg/cataloger/elixir/package.go
@@ -6,13 +6,18 @@ import (
"github.com/anchore/syft/syft/pkg"
)
-func newPackage(d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Package {
+// newPackage builds a package from a mix.lock entry. source is the entry's
+// source atom (`hex`, `git`, or `path`). Only hex entries are backed by the
+// hex.pm registry, so only they receive a pkg:hex/ PURL; git/path entries get
+// an empty PURL to avoid being asserted as hex.pm packages (which would produce
+// false hex.pm vulnerability matches).
+func newPackage(source string, d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Package {
p := pkg.Package{
Name: d.Name,
Version: d.Version,
Language: pkg.Elixir,
Locations: file.NewLocationSet(locations...),
- PURL: packageURL(d),
+ PURL: packageURL(source, d),
Type: pkg.HexPkg,
Metadata: d,
}
@@ -22,7 +27,14 @@ func newPackage(d pkg.ElixirMixLockEntry, locations ...file.Location) pkg.Packag
return p
}
-func packageURL(m pkg.ElixirMixLockEntry) string {
+func packageURL(source string, m pkg.ElixirMixLockEntry) string {
+ // Non-hex sources (git, path) are not published to the hex.pm registry, so a
+ // pkg:hex/ PURL would be incorrect and would drive false hex.pm vulnerability
+ // matches. Emit no PURL for them.
+ if source != "hex" {
+ return ""
+ }
+
var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL(
diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock.go b/syft/pkg/cataloger/elixir/parse_mix_lock.go
index b444cde2a..e44aaa8c0 100644
--- a/syft/pkg/cataloger/elixir/parse_mix_lock.go
+++ b/syft/pkg/cataloger/elixir/parse_mix_lock.go
@@ -53,7 +53,34 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re
errs = unknown.Appendf(errs, reader, "unable to read mix lock line %d: %s", lineNum, line)
continue
}
- name, version, hash, hashExt := tokens[1], tokens[4], tokens[5], tokens[len(tokens)-2]
+
+ // tokens[2] is the source atom of the entry's tuple: `hex`, `git`, or
+ // `path`. The layout of the remaining tokens differs per source, so the
+ // version/hash positions must be read accordingly. Only hex entries are
+ // backed by the hex.pm registry; git/path entries must not be emitted as
+ // hex packages (see newPackage), otherwise a bogus pkg:hex/ PURL produces
+ // false hex.pm vulnerability matches.
+ source := tokens[2]
+
+ var name, version, hash, hashExt string
+ switch source {
+ case "git":
+ // e.g. `"dep": {:git, "https://host/dep.git", "", [ref: "..."]}`
+ // tokens: ["", name, "git", "", "//host/dep.git", "", ...]
+ // The version is the commit SHA/ref immediately after the URL; there
+ // is no hex checksum for a git-sourced dependency.
+ name, version = tokens[1], tokens[5]
+ case "path":
+ // e.g. `"dep": {:path, "../local", []}`
+ // tokens: ["", name, "path", "../local", "[]", ""]
+ // A path dependency has no version or checksum; tokens[4] is the empty
+ // dependency list `[]`, not a version.
+ name = tokens[1]
+ default:
+ // hex (and any registry-style tuple): keep the original behavior.
+ // tokens: ["", name, "hex", name, version, hash, ..., hashExt, ""]
+ name, version, hash, hashExt = tokens[1], tokens[4], tokens[5], tokens[len(tokens)-2]
+ }
if name == "" {
log.WithFields("path", reader.RealPath).Debug("skipping empty package name from mix.lock file")
@@ -63,6 +90,7 @@ func parseMixLock(_ context.Context, _ file.Resolver, _ *generic.Environment, re
packages = append(packages,
newPackage(
+ source,
pkg.ElixirMixLockEntry{
Name: name,
Version: version,
diff --git a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go
index 0e1e36e3c..c19e908d0 100644
--- a/syft/pkg/cataloger/elixir/parse_mix_lock_test.go
+++ b/syft/pkg/cataloger/elixir/parse_mix_lock_test.go
@@ -230,6 +230,34 @@ func TestParseMixLock(t *testing.T) {
Dependencies: []string{"decimal"},
},
},
+ {
+ // git-sourced dependency: version is the commit SHA (not a URL
+ // fragment) and no pkg:hex/ PURL is emitted, so it cannot be
+ // matched as a hex.pm package.
+ Name: "enacl",
+ Version: "2f50ba6289f2f2d9fef05d22a396c0bab4f64149",
+ Language: pkg.Elixir,
+ Type: pkg.HexPkg,
+ Locations: locations,
+ PURL: "",
+ Metadata: pkg.ElixirMixLockEntry{
+ Name: "enacl",
+ Version: "2f50ba6289f2f2d9fef05d22a396c0bab4f64149",
+ },
+ },
+ {
+ // path-sourced dependency: no version (tokens[4] is the empty
+ // dependency list `[]`, not a version) and no pkg:hex/ PURL.
+ Name: "local_dep",
+ Version: "",
+ Language: pkg.Elixir,
+ Type: pkg.HexPkg,
+ Locations: locations,
+ PURL: "",
+ Metadata: pkg.ElixirMixLockEntry{
+ Name: "local_dep",
+ },
+ },
}
fixture := "testdata/mix.lock"
diff --git a/syft/pkg/cataloger/elixir/testdata/mix.lock b/syft/pkg/cataloger/elixir/testdata/mix.lock
index 8d9806c1b..8b67d4e02 100644
--- a/syft/pkg/cataloger/elixir/testdata/mix.lock
+++ b/syft/pkg/cataloger/elixir/testdata/mix.lock
@@ -14,4 +14,6 @@
"gettext": {:hex, :gettext, "0.19.1", "564953fd21f29358e68b91634799d9d26989f8d039d7512622efb3c3b1c97892", [:mix], [], "hexpm", "10c656c0912b8299adba9b061c06947511e3f109ab0d18b44a866a4498e77222"},
"hpax": {:hex, :hpax, "0.1.1", "2396c313683ada39e98c20a75a82911592b47e5c24391363343bde74f82396ca", [:mix], [], "hexpm", "0ae7d5a0b04a8a60caf7a39fcf3ec476f35cc2cc16c05abea730d3ce6ac6c826"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
+ "enacl": {:git, "https://github.com/aeternity/enacl.git", "2f50ba6289f2f2d9fef05d22a396c0bab4f64149", [ref: "2f50ba6"]},
+ "local_dep": {:path, "../local_dep", []},
}
From ebac92a86ebae0002ec5bee50c2f5ebf6f32b26b Mon Sep 17 00:00:00 2001
From: Marcus
Date: Mon, 6 Jul 2026 15:17:42 +0000
Subject: [PATCH 051/168] fix: conanfile comments in [requires] no longer stop
dependency detection (#5020)
The [requires] section parser set inRequirements=false when it
encountered a comment line (#) inside the section, because the comment
check was OR'd with the section-change check. Any dependency listed
after a comment was silently skipped.
Skip blank lines and comments before evaluating section state so they
never toggle inRequirements. Add a regression test fixture with a
comment inside [requires].
Fixes #5017
Signed-off-by: jeff
---
syft/pkg/cataloger/cpp/parse_conanfile.go | 17 ++++++----
.../pkg/cataloger/cpp/parse_conanfile_test.go | 33 +++++++++++++++++++
.../conanfile-comment-in-requires.txt | 9 +++++
3 files changed, 53 insertions(+), 6 deletions(-)
create mode 100644 syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt
diff --git a/syft/pkg/cataloger/cpp/parse_conanfile.go b/syft/pkg/cataloger/cpp/parse_conanfile.go
index 5ae124051..677a1b8cb 100644
--- a/syft/pkg/cataloger/cpp/parse_conanfile.go
+++ b/syft/pkg/cataloger/cpp/parse_conanfile.go
@@ -31,15 +31,20 @@ func parseConanfile(_ context.Context, _ file.Resolver, _ *generic.Environment,
return nil, nil, fmt.Errorf("failed to parse conanfile.txt file: %w", err)
}
+ trimmed := strings.TrimSpace(line)
+
+ // skip blank lines and comments without affecting section state
+ if trimmed == "" || strings.HasPrefix(trimmed, "#") {
+ continue
+ }
+
switch {
case strings.Contains(line, "[requires]"):
inRequirements = true
- case strings.ContainsAny(line, "[]") || strings.HasPrefix(strings.TrimSpace(line), "#"):
+ continue
+ case strings.ContainsAny(line, "[]"):
inRequirements = false
- }
-
- m := pkg.ConanfileEntry{
- Ref: strings.TrimSpace(line),
+ continue
}
if !inRequirements {
@@ -47,7 +52,7 @@ func parseConanfile(_ context.Context, _ file.Resolver, _ *generic.Environment,
}
p := newConanfilePackage(
- m,
+ pkg.ConanfileEntry{Ref: trimmed},
reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if p == nil {
diff --git a/syft/pkg/cataloger/cpp/parse_conanfile_test.go b/syft/pkg/cataloger/cpp/parse_conanfile_test.go
index 53995493b..9fc6a9384 100644
--- a/syft/pkg/cataloger/cpp/parse_conanfile_test.go
+++ b/syft/pkg/cataloger/cpp/parse_conanfile_test.go
@@ -86,3 +86,36 @@ func TestParseConanfile(t *testing.T) {
pkgtest.TestFileParser(t, fixture, parseConanfile, expected, expectedRelationships)
}
+
+func TestParseConanfileCommentInRequires(t *testing.T) {
+ fixture := "testdata/conanfile-comment-in-requires.txt"
+ fixtureLocationSet := file.NewLocationSet(file.NewLocation(fixture))
+ expected := []pkg.Package{
+ {
+ Name: "catch2",
+ Version: "2.13.8",
+ PURL: "pkg:conan/catch2@2.13.8",
+ Locations: fixtureLocationSet,
+ Language: pkg.CPP,
+ Type: pkg.ConanPkg,
+ Metadata: pkg.ConanfileEntry{
+ Ref: "catch2/2.13.8",
+ },
+ },
+ {
+ Name: "docopt.cpp",
+ Version: "0.6.3",
+ PURL: "pkg:conan/docopt.cpp@0.6.3",
+ Locations: fixtureLocationSet,
+ Language: pkg.CPP,
+ Type: pkg.ConanPkg,
+ Metadata: pkg.ConanfileEntry{
+ Ref: "docopt.cpp/0.6.3",
+ },
+ },
+ }
+
+ var expectedRelationships []artifact.Relationship
+
+ pkgtest.TestFileParser(t, fixture, parseConanfile, expected, expectedRelationships)
+}
diff --git a/syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt b/syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt
new file mode 100644
index 000000000..207158045
--- /dev/null
+++ b/syft/pkg/cataloger/cpp/testdata/conanfile-comment-in-requires.txt
@@ -0,0 +1,9 @@
+# Docs at https://docs.conan.io/en/latest/reference/conanfile_txt.html
+
+[requires]
+# this is a comment inside requires
+catch2/2.13.8
+docopt.cpp/0.6.3
+
+[generators]
+cmake_find_package_multi
From 3e399256b3dc36b699d5e9cec33928f7e3c12c5e Mon Sep 17 00:00:00 2001
From: OsamaSE <25825931+OsamaSE@users.noreply.github.com>
Date: Mon, 6 Jul 2026 19:18:37 +0400
Subject: [PATCH 052/168] fix(debian): supplement point release from
/etc/debian_version (#4997)
Debian's /etc/os-release declares only the major version in VERSION_ID (e.g. "13"), while /etc/debian_version carries the point release (e.g. "13.5"). The cataloger previously skipped debian_version whenever os-release already had a Version/VersionID, so point releases were lost.
Signed-off-by: Osama Elhindawy <25825931+OsamaSE@users.noreply.github.com>
---
syft/linux/identify_release_test.go | 16 ++++++++++++++++
syft/linux/supplement_release.go | 9 +++++----
.../debian/with-point-release/etc/debian_version | 1 +
.../os/debian/with-point-release/etc/os-release | 9 +++++++++
4 files changed, 31 insertions(+), 4 deletions(-)
create mode 100644 syft/linux/testdata/os/debian/with-point-release/etc/debian_version
create mode 100644 syft/linux/testdata/os/debian/with-point-release/etc/os-release
diff --git a/syft/linux/identify_release_test.go b/syft/linux/identify_release_test.go
index d1f67f957..1c719351d 100644
--- a/syft/linux/identify_release_test.go
+++ b/syft/linux/identify_release_test.go
@@ -101,6 +101,22 @@ func TestIdentifyRelease(t *testing.T) {
BugReportURL: "https://github.com/GoogleContainerTools/distroless/issues/new",
},
},
+ {
+ // standard Debian: VERSION_ID has major only, debian_version has the point release
+ fixture: "testdata/os/debian/with-point-release",
+ release: &Release{
+ PrettyName: "Debian GNU/Linux 13 (trixie)",
+ Name: "Debian GNU/Linux",
+ ID: "debian",
+ IDLike: nil,
+ Version: "13 (trixie)",
+ VersionID: "13.5",
+ VersionCodename: "trixie",
+ HomeURL: "https://www.debian.org/",
+ SupportURL: "https://www.debian.org/support",
+ BugReportURL: "https://bugs.debian.org/",
+ },
+ },
{
fixture: "testdata/os/fedora",
release: &Release{
diff --git a/syft/linux/supplement_release.go b/syft/linux/supplement_release.go
index dfa2c9b28..33a6815f6 100644
--- a/syft/linux/supplement_release.go
+++ b/syft/linux/supplement_release.go
@@ -11,11 +11,10 @@ import (
)
func supplementDebianVersion(resolver file.Resolver, release *Release) {
- // we're only looking for version information for debian when none is present in /etc/os-release
- if release.Version != "" || release.VersionID != "" || !strings.EqualFold(release.ID, "debian") {
+ if !strings.EqualFold(release.ID, "debian") {
return
}
- // if we have a debian release with no version, look for a debian_version
+ // debian_version has a more specific point release than os-release
locations, err := resolver.FilesByGlob("/etc/debian_version")
if err != nil {
log.Debugf("error reading /etc/debian_version: %v", err)
@@ -24,8 +23,10 @@ func supplementDebianVersion(resolver file.Resolver, release *Release) {
for _, location := range locations {
version := readDebianVersionFile(resolver, location)
if version != "" {
- release.Version = version
release.VersionID = version
+ if release.Version == "" {
+ release.Version = version
+ }
return // keep the first result
}
}
diff --git a/syft/linux/testdata/os/debian/with-point-release/etc/debian_version b/syft/linux/testdata/os/debian/with-point-release/etc/debian_version
new file mode 100644
index 000000000..e09420105
--- /dev/null
+++ b/syft/linux/testdata/os/debian/with-point-release/etc/debian_version
@@ -0,0 +1 @@
+13.5
diff --git a/syft/linux/testdata/os/debian/with-point-release/etc/os-release b/syft/linux/testdata/os/debian/with-point-release/etc/os-release
new file mode 100644
index 000000000..e99969d90
--- /dev/null
+++ b/syft/linux/testdata/os/debian/with-point-release/etc/os-release
@@ -0,0 +1,9 @@
+PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
+NAME="Debian GNU/Linux"
+VERSION_ID="13"
+VERSION="13 (trixie)"
+VERSION_CODENAME=trixie
+ID=debian
+HOME_URL="https://www.debian.org/"
+SUPPORT_URL="https://www.debian.org/support"
+BUG_REPORT_URL="https://bugs.debian.org/"
From 002a326c41e9fc14def8869bfafa3614b5016b4b Mon Sep 17 00:00:00 2001
From: Alex Goodman
Date: Tue, 7 Jul 2026 10:11:15 -0400
Subject: [PATCH 053/168] detect Ubuntu Pro/ESM extended support (#5028)
Mirrors the existing RHEL `ExtendedSupport` detector for Ubuntu so grype can
auto-enable its ESM distro channel on Pro/ESM-enabled scans.
`findUbuntuFeatures` sets `linux.Release.ExtendedSupport` when the scanned OS is
Ubuntu and any of these hold:
- an uncommented apt source (classic `.list` or DEB822 `.sources`, or the
ubuntu-advantage auth entry) referencing `esm.ubuntu.com`
- an enabled `esm-infra`/`esm-apps` service in the ubuntu-advantage
`status.json` cache
- an installed dpkg package whose version carries an `[~+]esm` pocket suffix
(the usual in-image signal, since containers are rarely Pro-attached)
The package fallback reads `s.Artifacts.Packages` from the `DetectFeatures`
SBOM callback, which runs after package cataloging, so no new plumbing was
needed. Conservative by design: plain Ubuntu and disabled/commented ESM
evidence stay false.
Signed-off-by: Alex Goodman
---
internal/os/feature_detection.go | 170 +++++++++++++-
internal/os/feature_detection_test.go | 214 ++++++++++++++++++
.../apt/sources.list.d/ubuntu-esm-infra.list | 6 +
.../etc/apt/auth.conf.d/90ubuntu-advantage | 2 +
.../sources.list.d/ubuntu-esm-infra.sources | 5 +
.../apt/sources.list.d/ubuntu-esm-infra.list | 3 +
.../var/lib/ubuntu-advantage/status.json | 18 ++
.../var/lib/ubuntu-advantage/status.json | 21 ++
.../var/lib/ubuntu-advantage/status.json | 1 +
.../var/lib/ubuntu-advantage/status.json | 28 +++
.../etc/apt/sources.list.d/ubuntu-fips.list | 3 +
.../var/lib/ubuntu-advantage/status.json | 28 +++
.../etc/apt/sources.list.d/ubuntu.sources | 5 +
13 files changed, 493 insertions(+), 11 deletions(-)
create mode 100644 internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list
create mode 100644 internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage
create mode 100644 internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources
create mode 100644 internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list
create mode 100644 internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json
create mode 100644 internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json
create mode 100644 internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json
create mode 100644 internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json
create mode 100644 internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list
create mode 100644 internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json
create mode 100644 internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources
diff --git a/internal/os/feature_detection.go b/internal/os/feature_detection.go
index c23b8b67b..1aaf3f23c 100644
--- a/internal/os/feature_detection.go
+++ b/internal/os/feature_detection.go
@@ -2,6 +2,8 @@ package os
import (
"context"
+ "encoding/json"
+ "errors"
"fmt"
"regexp"
@@ -10,6 +12,7 @@ import (
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
+ "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
)
@@ -19,8 +22,11 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync.
return
}
- err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution)
- if err != nil {
+ if err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution); err != nil {
+ log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support")
+ }
+
+ if err := findUbuntuFeatures(resolver, s.Artifacts.LinuxDistribution, s.Artifacts.Packages); err != nil {
log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support")
}
})
@@ -28,6 +34,9 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync.
return nil
}
+// rhelEUSPatterns match a RHEL content manifest referencing an extended-update-support (EUS) repo.
+var rhelEUSPatterns = []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)}
+
func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
if release == nil || release.ID != "rhel" {
return nil
@@ -37,9 +46,9 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
return fmt.Errorf("unable to find content manifests: %w", err)
}
for _, contentManifestFile := range contentManifestFiles {
- found, err := hasRhelExtendedSupportInContentManifest(resolver, contentManifestFile)
+ found, err := fileMatchesAny(resolver, contentManifestFile, rhelEUSPatterns...)
if err != nil {
- return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
+ return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.RealPath, err)
}
if found {
release.ExtendedSupport = true
@@ -49,13 +58,152 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error {
return nil
}
-func hasRhelExtendedSupportInContentManifest(resolver file.Resolver, contentManifestFile file.Location) (bool, error) {
- contents, err := resolver.FileContentsByLocation(contentManifestFile)
- if err != nil {
- return false, fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err)
- }
- defer internal.CloseAndLogError(contents, "content-manifest")
+// Ubuntu Pro (formerly Ubuntu Advantage) is Canonical's subscription that unlocks Expanded Security
+// Maintenance (ESM): security fixes past a package's standard support window. we detect the two base ESM
+// streams, esm-infra (the `main` repo) and esm-apps (`universe`), and collapse them into one ExtendedSupport
+// signal; the infra-vs-apps split is disambiguated by the downstream consumer, not by this boolean.
+//
+// the other Pro streams on the same host (esm.ubuntu.com/fips, /fips-updates, /realtime) are deliberately
+// excluded: they are separate compliance products with their own downstream channels, and a host can run
+// them with base ESM disabled, so folding them in would be unsound.
+//
+// detection is deliberately forgiving: apt/status evidence disappears when ESM is disabled, but an installed
+// +esmN package is durable proof ESM content is on disk. so ExtendedSupport means "ESM was or is in effect
+// for this host or its content", not "currently entitled" (mere eligibility, without evidence, is not enough).
- patterns := []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)}
+// esmAptSourcePattern matches an uncommented apt source (classic or DEB822) or auth entry pointing at the
+// plain ESM streams esm.ubuntu.com/infra or /apps. it deliberately scopes to /(infra|apps) so the other
+// Pro streams served from the same host (/fips, /fips-updates, /realtime) are never folded into the base
+// esm channel. the [^#\n]* before the host ensures a leading `#` (comment) can never satisfy the match.
+var esmAptSourcePattern = regexp.MustCompile(`(?m)^[^#\n]*esm\.ubuntu\.com/(infra|apps)`)
+
+// esmVersionPattern matches a dpkg version carrying a numbered ESM pocket suffix, e.g. `...+esm1` or
+// `...~esm2`. the trailing \d scopes this to the ESM pocket and excludes other Pro pockets (e.g. +fips).
+var esmVersionPattern = regexp.MustCompile(`[~+]esm\d`)
+
+func findUbuntuFeatures(resolver file.Resolver, release *linux.Release, packages *pkg.Collection) error {
+ if release == nil || release.ID != "ubuntu" {
+ return nil
+ }
+
+ // an uncommented esm.ubuntu.com apt source or an active pro esm service both prove Pro/ESM was enabled.
+ // these are the durable, file-based signals present on attached hosts. a read error in one signal must
+ // not suppress the others, so errors are collected and detection continues to the next signal.
+ var errs error
+ for _, check := range []func() (bool, error){
+ func() (bool, error) { return hasUbuntuESMAptEvidence(resolver) },
+ func() (bool, error) { return hasActiveUbuntuESMService(resolver) },
+ } {
+ found, err := check()
+ if err != nil {
+ errs = errors.Join(errs, err)
+ continue
+ }
+ if found {
+ release.ExtendedSupport = true
+ return errs
+ }
+ }
+
+ // fallback for images (typically not pro-attached): an installed package whose version carries an
+ // +esm/~esm suffix was pulled from an ESM pocket, so ESM was in effect when it was installed.
+ if hasInstalledESMPackage(packages) {
+ release.ExtendedSupport = true
+ }
+ return errs
+}
+
+// hasUbuntuESMAptEvidence reports whether any apt source (classic .list or DEB822 .sources) or auth entry
+// references esm.ubuntu.com via an uncommented line.
+func hasUbuntuESMAptEvidence(resolver file.Resolver) (bool, error) {
+ // apt source files live in a directory (classic .list or DEB822 .sources), the credentials file is a fixed path.
+ sourceLocations, err := resolver.FilesByGlob("/etc/apt/sources.list.d/*")
+ if err != nil {
+ return false, fmt.Errorf("unable to find apt esm sources: %w", err)
+ }
+ authLocations, err := resolver.FilesByPath("/etc/apt/auth.conf.d/90ubuntu-advantage")
+ if err != nil {
+ return false, fmt.Errorf("unable to find apt esm auth: %w", err)
+ }
+
+ // ponytail: a DEB822 source with `Enabled: no` would still match; the pro client removes/comments the
+ // file on disable rather than toggling that key, so this is an accepted false-positive ceiling. the
+ // auth.conf.d entry is deleted by the pro client on disable, so it is a durable positive-only signal.
+ for _, location := range append(sourceLocations, authLocations...) {
+ match, err := fileMatchesAny(resolver, location, esmAptSourcePattern)
+ if err != nil {
+ return false, err
+ }
+ if match {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// hasActiveUbuntuESMService reports whether the ubuntu-advantage/pro status cache shows an enabled esm service.
+func hasActiveUbuntuESMService(resolver file.Resolver) (bool, error) {
+ locations, err := resolver.FilesByPath("/var/lib/ubuntu-advantage/status.json")
+ if err != nil {
+ return false, fmt.Errorf("unable to find ubuntu-advantage status: %w", err)
+ }
+ for _, location := range locations {
+ enabled, err := hasEnabledESMServiceInStatus(resolver, location)
+ if err != nil {
+ return false, err
+ }
+ if enabled {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func hasEnabledESMServiceInStatus(resolver file.Resolver, location file.Location) (bool, error) {
+ contents, err := resolver.FileContentsByLocation(location)
+ if err != nil {
+ return false, fmt.Errorf("unable to read ubuntu-advantage status from %s: %w", location.RealPath, err)
+ }
+ defer internal.CloseAndLogError(contents, location.RealPath)
+
+ var status struct {
+ Services []struct {
+ Name string `json:"name"`
+ Status string `json:"status"`
+ } `json:"services"`
+ }
+ if err := json.NewDecoder(contents).Decode(&status); err != nil {
+ return false, fmt.Errorf("unable to parse ubuntu-advantage status from %s: %w", location.RealPath, err)
+ }
+
+ // pro status values are enabled/disabled/n-a/warning/—; only "enabled" proves the stream is active.
+ // warning/expired states are intentionally excluded here and left to the apt-source and installed-package signals.
+ for _, svc := range status.Services {
+ if (svc.Name == "esm-infra" || svc.Name == "esm-apps") && svc.Status == "enabled" {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// hasInstalledESMPackage reports whether any installed dpkg package version carries an ESM pocket suffix.
+func hasInstalledESMPackage(packages *pkg.Collection) bool {
+ if packages == nil {
+ return false
+ }
+ for p := range packages.Enumerate(pkg.DebPkg) {
+ if esmVersionPattern.MatchString(p.Version) {
+ return true
+ }
+ }
+ return false
+}
+
+func fileMatchesAny(resolver file.Resolver, location file.Location, patterns ...*regexp.Regexp) (bool, error) {
+ contents, err := resolver.FileContentsByLocation(location)
+ if err != nil {
+ return false, fmt.Errorf("unable to read %s: %w", location.RealPath, err)
+ }
+ defer internal.CloseAndLogError(contents, location.RealPath)
return internal.MatchAnyFromReader(contents, patterns...)
}
diff --git a/internal/os/feature_detection_test.go b/internal/os/feature_detection_test.go
index 9a1fe32f9..bb8a30de1 100644
--- a/internal/os/feature_detection_test.go
+++ b/internal/os/feature_detection_test.go
@@ -2,6 +2,11 @@ package os_test
import (
"context"
+ "io"
+ stdos "os"
+ "path"
+ "path/filepath"
+ "slices"
"testing"
"github.com/stretchr/testify/require"
@@ -11,7 +16,9 @@ import (
"github.com/anchore/syft/internal/sbomsync"
"github.com/anchore/syft/internal/task"
"github.com/anchore/syft/syft"
+ "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
+ "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@@ -99,3 +106,210 @@ func Test_EnvironmentTask(t *testing.T) {
})
}
}
+
+func Test_findUbuntuFeatures(t *testing.T) {
+ tests := []struct {
+ name string
+ dir string
+ releaseID string
+ packages []pkg.Package
+ expected bool
+ }{
+ {
+ name: "esm apt source present",
+ dir: "testdata/ubuntu_esm_apt",
+ releaseID: "ubuntu",
+ expected: true,
+ },
+ {
+ name: "esm apt source in DEB822 .sources format",
+ dir: "testdata/ubuntu_esm_deb822",
+ releaseID: "ubuntu",
+ expected: true,
+ },
+ {
+ name: "esm evidence in apt auth.conf.d",
+ dir: "testdata/ubuntu_esm_authconf",
+ releaseID: "ubuntu",
+ expected: true,
+ },
+ {
+ name: "active esm service in ubuntu-advantage status",
+ dir: "testdata/ubuntu_esm_status",
+ releaseID: "ubuntu",
+ expected: true,
+ },
+ {
+ name: "only esm-infra enabled (esm-apps disabled)",
+ dir: "testdata/ubuntu_esm_infra_only",
+ releaseID: "ubuntu",
+ expected: true,
+ },
+ {
+ name: "malformed status.json does not panic or match",
+ dir: "testdata/ubuntu_esm_malformed",
+ releaseID: "ubuntu",
+ expected: false,
+ },
+ {
+ name: "installed +esm package version",
+ dir: "testdata/ubuntu_plain",
+ releaseID: "ubuntu",
+ packages: []pkg.Package{
+ {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
+ },
+ expected: true,
+ },
+ {
+ name: "installed ~esm package version",
+ dir: "testdata/ubuntu_plain",
+ releaseID: "ubuntu",
+ packages: []pkg.Package{
+ {Name: "libcap2", Version: "1:2.32-1ubuntu0.1~esm1", Type: pkg.DebPkg},
+ },
+ expected: true,
+ },
+ {
+ name: "plain ubuntu with no esm evidence",
+ dir: "testdata/ubuntu_plain",
+ releaseID: "ubuntu",
+ packages: []pkg.Package{
+ {Name: "openssl", Version: "1.1.1f-1ubuntu2.19", Type: pkg.DebPkg},
+ },
+ expected: false,
+ },
+ {
+ name: "commented esm source and disabled esm service",
+ dir: "testdata/ubuntu_esm_disabled",
+ releaseID: "ubuntu",
+ expected: false,
+ },
+ {
+ name: "esm disabled but +esm package remains installed",
+ dir: "testdata/ubuntu_esm_disabled",
+ releaseID: "ubuntu",
+ packages: []pkg.Package{
+ {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
+ },
+ expected: true, // durable proof: ESM content is on disk even though the channel is now off
+ },
+ {
+ name: "fips-only pro host is not plain esm",
+ dir: "testdata/ubuntu_fips_only",
+ releaseID: "ubuntu",
+ expected: false, // esm.ubuntu.com/fips must not be folded into the base esm channel
+ },
+ {
+ name: "non-ubuntu is unaffected by esm evidence",
+ dir: "testdata/ubuntu_esm_apt",
+ releaseID: "debian",
+ expected: false,
+ },
+ {
+ name: "non-ubuntu is unaffected by esm package version",
+ dir: "testdata/ubuntu_plain",
+ releaseID: "debian",
+ packages: []pkg.Package{
+ {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg},
+ },
+ expected: false,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ resolver := fixtureResolverForDir(t, test.dir)
+
+ s := sbom.SBOM{}
+ s.Artifacts.LinuxDistribution = &linux.Release{ID: test.releaseID}
+ s.Artifacts.Packages = pkg.NewCollection(test.packages...)
+
+ err := os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s))
+ require.NoError(t, err)
+
+ require.Equal(t, test.expected, s.Artifacts.LinuxDistribution.ExtendedSupport)
+ })
+ }
+}
+
+// a read error in the apt-source signal must not suppress the ubuntu-advantage status signal; the three
+// signals are meant to be independent fallbacks.
+func Test_findUbuntuFeatures_signalErrorDoesNotSuppressOthers(t *testing.T) {
+ resolver := errOnGlobResolver{
+ fixtureResolver: fixtureResolverForDir(t, "testdata/ubuntu_esm_status").(fixtureResolver),
+ errOnGlob: "/etc/apt/sources.list.d/*",
+ }
+
+ s := sbom.SBOM{}
+ s.Artifacts.LinuxDistribution = &linux.Release{ID: "ubuntu"}
+ s.Artifacts.Packages = pkg.NewCollection()
+
+ require.NoError(t, os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s)))
+
+ // the apt glob errors, but the status.json signal (esm-infra enabled) should still be consulted
+ require.True(t, s.Artifacts.LinuxDistribution.ExtendedSupport)
+}
+
+// errOnGlobResolver injects a failure for a specific glob to simulate an unreadable apt directory.
+type errOnGlobResolver struct {
+ fixtureResolver
+ errOnGlob string
+}
+
+func (r errOnGlobResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
+ if slices.Contains(patterns, r.errOnGlob) {
+ return nil, stdos.ErrPermission
+ }
+ return r.fixtureResolver.FilesByGlob(patterns...)
+}
+
+// fixtureResolver maps in-image logical paths to on-disk fixture files so DetectFeatures can be driven
+// without building a container image.
+type fixtureResolver struct {
+ file.Resolver // unused methods; nil is fine since only the three below are called
+ files map[string]string
+}
+
+func (r fixtureResolver) FilesByGlob(patterns ...string) ([]file.Location, error) {
+ var out []file.Location
+ for logical := range r.files {
+ for _, p := range patterns {
+ if ok, _ := path.Match(p, logical); ok {
+ out = append(out, file.NewLocation(logical))
+ break
+ }
+ }
+ }
+ return out, nil
+}
+
+func (r fixtureResolver) FilesByPath(paths ...string) ([]file.Location, error) {
+ var out []file.Location
+ for _, p := range paths {
+ if _, ok := r.files[p]; ok {
+ out = append(out, file.NewLocation(p))
+ }
+ }
+ return out, nil
+}
+
+func (r fixtureResolver) FileContentsByLocation(l file.Location) (io.ReadCloser, error) {
+ return stdos.Open(r.files[l.RealPath])
+}
+
+func fixtureResolverForDir(t *testing.T, dir string) file.Resolver {
+ t.Helper()
+ files := map[string]string{}
+ require.NoError(t, filepath.WalkDir(dir, func(p string, d stdos.DirEntry, err error) error {
+ if err != nil || d.IsDir() {
+ return err
+ }
+ rel, err := filepath.Rel(dir, p)
+ if err != nil {
+ return err
+ }
+ files["/"+filepath.ToSlash(rel)] = p
+ return nil
+ }))
+ return fixtureResolver{files: files}
+}
diff --git a/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list b/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list
new file mode 100644
index 000000000..8f43ca9a2
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list
@@ -0,0 +1,6 @@
+# Written by ubuntu-advantage-tools
+deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
+# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
+
+deb https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main
+# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main
diff --git a/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage b/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage
new file mode 100644
index 000000000..74c7c3018
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage
@@ -0,0 +1,2 @@
+machine esm.ubuntu.com/infra/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client
+machine esm.ubuntu.com/apps/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client
diff --git a/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources b/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources
new file mode 100644
index 000000000..12df00a6e
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources
@@ -0,0 +1,5 @@
+Types: deb
+URIs: https://esm.ubuntu.com/infra/ubuntu
+Suites: noble-infra-security noble-infra-updates
+Components: main
+Signed-By: /usr/share/keyrings/ubuntu-pro-esm-infra-keyring.gpg
diff --git a/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list b/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list
new file mode 100644
index 000000000..7351dd71c
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list
@@ -0,0 +1,3 @@
+# Written by ubuntu-advantage-tools
+# deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
+# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main
diff --git a/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json
new file mode 100644
index 000000000..3cd190426
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json
@@ -0,0 +1,18 @@
+{
+ "_schema_version": "0.1",
+ "attached": true,
+ "services": [
+ {
+ "name": "esm-apps",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "disabled"
+ },
+ {
+ "name": "esm-infra",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "disabled"
+ }
+ ]
+}
diff --git a/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json
new file mode 100644
index 000000000..98407f493
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json
@@ -0,0 +1,21 @@
+{
+ "_schema_version": "0.1",
+ "attached": true,
+ "execution_status": "success",
+ "services": [
+ {
+ "name": "esm-apps",
+ "description": "Expanded Security Maintenance for Applications",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "disabled"
+ },
+ {
+ "name": "esm-infra",
+ "description": "Expanded Security Maintenance for Infrastructure",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "enabled"
+ }
+ ]
+}
diff --git a/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json
new file mode 100644
index 000000000..dcf314808
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json
@@ -0,0 +1 @@
+{ this is not valid json
diff --git a/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json
new file mode 100644
index 000000000..597f97c09
--- /dev/null
+++ b/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json
@@ -0,0 +1,28 @@
+{
+ "_doc": "Content provided in json response is currently considered Experimental and may change",
+ "_schema_version": "0.1",
+ "attached": true,
+ "services": [
+ {
+ "name": "esm-apps",
+ "description": "Expanded Security Maintenance for Applications",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "enabled"
+ },
+ {
+ "name": "esm-infra",
+ "description": "Expanded Security Maintenance for Infrastructure",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "enabled"
+ },
+ {
+ "name": "fips",
+ "description": "NIST-certified FIPS crypto packages",
+ "available": "no",
+ "entitled": "no",
+ "status": "n/a"
+ }
+ ]
+}
diff --git a/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list b/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list
new file mode 100644
index 000000000..14c22bda4
--- /dev/null
+++ b/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list
@@ -0,0 +1,3 @@
+# Written by ubuntu-pro-client
+deb https://esm.ubuntu.com/fips/ubuntu focal main
+# deb-src https://esm.ubuntu.com/fips/ubuntu focal main
diff --git a/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json
new file mode 100644
index 000000000..6926f3151
--- /dev/null
+++ b/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json
@@ -0,0 +1,28 @@
+{
+ "_schema_version": "0.1",
+ "attached": true,
+ "execution_status": "success",
+ "services": [
+ {
+ "name": "esm-apps",
+ "description": "Expanded Security Maintenance for Applications",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "disabled"
+ },
+ {
+ "name": "esm-infra",
+ "description": "Expanded Security Maintenance for Infrastructure",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "disabled"
+ },
+ {
+ "name": "fips",
+ "description": "NIST-certified FIPS crypto packages",
+ "available": "yes",
+ "entitled": "yes",
+ "status": "enabled"
+ }
+ ]
+}
diff --git a/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources b/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources
new file mode 100644
index 000000000..7552b963f
--- /dev/null
+++ b/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources
@@ -0,0 +1,5 @@
+Types: deb
+URIs: http://archive.ubuntu.com/ubuntu/
+Suites: noble noble-updates noble-backports
+Components: main restricted universe multiverse
+Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
From e8abab2e775ffa844f4ce978c606769d23bdf703 Mon Sep 17 00:00:00 2001
From: Sai Asish Y
Date: Wed, 8 Jul 2026 09:55:13 -0700
Subject: [PATCH 054/168] swift: emit canonical purls without .git suffix or
repeated name (#4785)
* swift: emit canonical purls without .git suffix or repeated name
Reported in anchore/syft#3961: the purl syft emits for Swift packages
parsed out of Package.resolved keeps the repository URLs .git suffix
and then re-appends the package name, producing
pkg:swift/github.com/apple/swift-nio-ssl.git/swift-nio-ssl@2.0.0
NVD / Grype cannot match that purl against the known
swift-nio-ssl@2.0.0 CVE (GHSA-frg3-gpcx-968f), so every Swift SBOM
produced by syft silently loses vulnerability coverage. cdxgen and
the wider purl ecosystem use the shorter form
pkg:swift/github.com/apple/swift-nio-ssl@2.0.0
which Grype does match.
Replace the ad-hoc strings.Replace with swiftNamespaceFromSourceURL,
which trims the common URL schemes, strips the ".git" suffix, and
drops a trailing / segment so the namespace is only the
organisation path (e.g. github.com/apple). Existing test expectations
are updated to the new purl shape.
Fixes #3961
Signed-off-by: Sai Asish Y
Signed-off-by: Alex Goodman
* Revert unrelated consul binary classifier changes
The swift purl commit (ff5ffc37) accidentally bundled removal of the
consul GitDescribe and NUL-wrapped version matchers plus the 1.12.9 and
1.7.14 fixtures/test cases. That drops version detection for consul
binaries not carrying the CONSUL_VERSION string. Restore the binary
cataloger to its pre-commit state so this branch is swift-only.
Signed-off-by: Alex Goodman
* swift: strip repeated name segment case-insensitively
Package.resolved identities are lowercased while the repo URL path may be
mixed-case (e.g. github.com/Apple/Swift-NIO vs identity swift-nio). The
case-sensitive TrimSuffix left the repo segment in the namespace for such
repos, reintroducing the duplicated-name purl. Compare the trailing segment
with EqualFold and add unit coverage for swiftPackageManagerPackageURL.
Signed-off-by: Alex Goodman
---------
Signed-off-by: Sai Asish Y
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
syft/pkg/cataloger/swift/package.go | 36 ++++++++++++-
syft/pkg/cataloger/swift/package_test.go | 51 +++++++++++++++++++
.../swift/parse_package_resolved_test.go | 14 ++---
3 files changed, 93 insertions(+), 8 deletions(-)
diff --git a/syft/pkg/cataloger/swift/package.go b/syft/pkg/cataloger/swift/package.go
index c4370f5dc..17870370a 100644
--- a/syft/pkg/cataloger/swift/package.go
+++ b/syft/pkg/cataloger/swift/package.go
@@ -62,10 +62,44 @@ func swiftPackageManagerPackageURL(name, version, sourceURL string) string {
return packageurl.NewPackageURL(
packageurl.TypeSwift,
- strings.Replace(sourceURL, "https://", "", 1),
+ swiftNamespaceFromSourceURL(sourceURL, name),
name,
version,
qualifiers,
"",
).ToString()
}
+
+// swiftNamespaceFromSourceURL derives the purl namespace (e.g.
+// "github.com/apple") from a Swift Package.resolved source URL like
+// "https://github.com/apple/swift-nio-ssl.git".
+//
+// Two bits of cleanup are needed compared with the previous
+// strings.Replace(sourceURL, "https://", "", 1) behaviour:
+//
+// 1. Strip the ".git" suffix. Swift Package.resolved always carries the
+// repository URL with a trailing ".git", but the purl spec uses the
+// plain web path.
+// 2. Drop the trailing / segment. The previous code left the repo
+// name in the namespace and then appended the package name again, so
+// the emitted purl looked like
+// pkg:swift/github.com/apple/swift-nio-ssl.git/swift-nio-ssl@2.0.0
+// which Grype cannot match against NVD. The expected form is
+// pkg:swift/github.com/apple/swift-nio-ssl@2.0.0
+// which is also what cdxgen emits. See anchore/syft#3961.
+func swiftNamespaceFromSourceURL(sourceURL, name string) string {
+ ns := sourceURL
+ for _, prefix := range []string{"https://", "http://", "git+ssh://", "ssh://"} {
+ ns = strings.TrimPrefix(ns, prefix)
+ }
+ ns = strings.TrimSuffix(ns, ".git")
+ // drop a trailing "/" segment. Package.resolved identities are lowercased
+ // while the repo path may be mixed-case (e.g. github.com/Apple/Swift-NIO vs
+ // identity swift-nio), so compare case-insensitively.
+ if name != "" {
+ if idx := strings.LastIndex(ns, "/"); idx >= 0 && strings.EqualFold(ns[idx+1:], name) {
+ ns = ns[:idx]
+ }
+ }
+ return ns
+}
diff --git a/syft/pkg/cataloger/swift/package_test.go b/syft/pkg/cataloger/swift/package_test.go
index daa3132cd..abc8f3145 100644
--- a/syft/pkg/cataloger/swift/package_test.go
+++ b/syft/pkg/cataloger/swift/package_test.go
@@ -6,6 +6,57 @@ import (
"github.com/stretchr/testify/assert"
)
+func Test_swiftPackageManagerPackageURL(t *testing.T) {
+ tests := []struct {
+ name string
+ pkgName string
+ version string
+ sourceURL string
+ want string
+ }{
+ {
+ name: "strips .git suffix and repeated name segment",
+ pkgName: "swift-nio-ssl",
+ version: "2.0.0",
+ sourceURL: "https://github.com/apple/swift-nio-ssl.git",
+ want: "pkg:swift/github.com/apple/swift-nio-ssl@2.0.0",
+ },
+ {
+ name: "no .git suffix",
+ pkgName: "swift-numerics",
+ version: "1.0.2",
+ sourceURL: "https://github.com/apple/swift-numerics",
+ want: "pkg:swift/github.com/apple/swift-numerics@1.0.2",
+ },
+ {
+ name: "mixed-case repo path with lowercased identity",
+ pkgName: "swift-nio",
+ version: "2.0.0",
+ sourceURL: "https://github.com/Apple/Swift-NIO.git",
+ want: "pkg:swift/github.com/Apple/swift-nio@2.0.0",
+ },
+ {
+ name: "ssh scheme",
+ pkgName: "swift-nio",
+ version: "2.0.0",
+ sourceURL: "git+ssh://github.com/apple/swift-nio.git",
+ want: "pkg:swift/github.com/apple/swift-nio@2.0.0",
+ },
+ {
+ name: "trailing segment differs from name is preserved",
+ pkgName: "nio",
+ version: "2.0.0",
+ sourceURL: "https://github.com/apple/swift-nio.git",
+ want: "pkg:swift/github.com/apple/swift-nio/nio@2.0.0",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.want, swiftPackageManagerPackageURL(tt.pkgName, tt.version, tt.sourceURL))
+ })
+ }
+}
+
func Test_cocoaPodsPackageURL(t *testing.T) {
type args struct {
name string
diff --git a/syft/pkg/cataloger/swift/parse_package_resolved_test.go b/syft/pkg/cataloger/swift/parse_package_resolved_test.go
index cd2565924..494a164c1 100644
--- a/syft/pkg/cataloger/swift/parse_package_resolved_test.go
+++ b/syft/pkg/cataloger/swift/parse_package_resolved_test.go
@@ -20,7 +20,7 @@ func TestParsePackageResolved(t *testing.T) {
{
Name: "swift-algorithms",
Version: "1.0.0",
- PURL: "pkg:swift/github.com/apple/swift-algorithms.git/swift-algorithms@1.0.0",
+ PURL: "pkg:swift/github.com/apple/swift-algorithms@1.0.0",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
@@ -31,7 +31,7 @@ func TestParsePackageResolved(t *testing.T) {
{
Name: "swift-async-algorithms",
Version: "0.1.0",
- PURL: "pkg:swift/github.com/apple/swift-async-algorithms.git/swift-async-algorithms@0.1.0",
+ PURL: "pkg:swift/github.com/apple/swift-async-algorithms@0.1.0",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
@@ -42,7 +42,7 @@ func TestParsePackageResolved(t *testing.T) {
{
Name: "swift-atomics",
Version: "1.1.0",
- PURL: "pkg:swift/github.com/apple/swift-atomics.git/swift-atomics@1.1.0",
+ PURL: "pkg:swift/github.com/apple/swift-atomics@1.1.0",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
@@ -53,7 +53,7 @@ func TestParsePackageResolved(t *testing.T) {
{
Name: "swift-collections",
Version: "1.0.4",
- PURL: "pkg:swift/github.com/apple/swift-collections.git/swift-collections@1.0.4",
+ PURL: "pkg:swift/github.com/apple/swift-collections@1.0.4",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
@@ -64,7 +64,7 @@ func TestParsePackageResolved(t *testing.T) {
{
Name: "swift-numerics",
Version: "1.0.2",
- PURL: "pkg:swift/github.com/apple/swift-numerics/swift-numerics@1.0.2",
+ PURL: "pkg:swift/github.com/apple/swift-numerics@1.0.2",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
@@ -87,7 +87,7 @@ func TestParsePackageResolvedV3(t *testing.T) {
{
Name: "swift-mmio",
Version: "",
- PURL: "pkg:swift/github.com/apple/swift-mmio/swift-mmio",
+ PURL: "pkg:swift/github.com/apple/swift-mmio",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
@@ -98,7 +98,7 @@ func TestParsePackageResolvedV3(t *testing.T) {
{
Name: "swift-syntax",
Version: "509.1.1",
- PURL: "pkg:swift/github.com/apple/swift-syntax.git/swift-syntax@509.1.1",
+ PURL: "pkg:swift/github.com/apple/swift-syntax@509.1.1",
Locations: locations,
Language: pkg.Swift,
Type: pkg.SwiftPkg,
From 5b630ec61c7a3e98c51832fbab5fda79529b12a1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 13:29:34 +0000
Subject: [PATCH 055/168] chore(deps): bump docker/login-action from 4.2.0 to
4.4.0 (#5049)
Bumps [docker/login-action](https://github.com/docker/login-action) from 4.2.0 to 4.4.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...af1e73f918a031802d376d3c8bbc3fe56130a9b0)
---
updated-dependencies:
- dependency-name: docker/login-action
dependency-version: 4.4.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/release.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 90776175a..9d3981475 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -79,13 +79,13 @@ jobs:
uses: ./.github/actions/bootstrap
- name: Login to Docker Hub
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee #v4.2.0
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
with:
username: ${{ secrets.ANCHOREOSSWRITE_DH_USERNAME }}
password: ${{ secrets.ANCHOREOSSWRITE_DH_PAT }}
- name: Login to GitHub Container Registry
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee #v4.2.0
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
From c4a0b6f6470e628d5ea235029ab40d12f938bcd9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 10 Jul 2026 13:31:42 +0000
Subject: [PATCH 056/168] chore(deps): bump github.com/klauspost/compress from
1.18.6 to 1.19.0 (#5050)
Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.18.6 to 1.19.0.
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.18.6...v1.19.0)
---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
dependency-version: 1.19.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 5fc0388f1..dc3109696 100644
--- a/go.mod
+++ b/go.mod
@@ -62,7 +62,7 @@ require (
github.com/jedib0t/go-pretty/v6 v6.8.1
github.com/jinzhu/copier v0.4.0
github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953
- github.com/klauspost/compress v1.18.6
+ github.com/klauspost/compress v1.19.0
github.com/magiconair/properties v1.8.10
github.com/mholt/archives v0.1.5
github.com/moby/sys/mountinfo v0.7.2
diff --git a/go.sum b/go.sum
index e902f2d3c..bb20bd808 100644
--- a/go.sum
+++ b/go.sum
@@ -645,8 +645,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
-github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
-github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
+github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
From 3252b6f8063e5a2268eb3a2eb9dc42c271c1ab29 Mon Sep 17 00:00:00 2001
From: anchore-oss-update-bot
Date: Mon, 13 Jul 2026 10:07:00 -0400
Subject: [PATCH 057/168] chore(deps): update CPE dictionary index (#5063)
Signed-off-by: anchore-oss-update-bot
Co-authored-by: anchore-oss-update-bot
---
.../dictionary/data/cpe-index.json | 20 ++++++++++++++++++-
1 file changed, 19 insertions(+), 1 deletion(-)
diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
index 1d7a1dd69..694bf48a5 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
+++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
@@ -5875,6 +5875,9 @@
"three": [
"cpe:2.3:a:three_project:three:*:*:*:*:*:node.js:*:*"
],
+ "thumbler": [
+ "cpe:2.3:a:mmahrous:thumbler:*:*:*:*:*:node.js:*:*"
+ ],
"tianma-static": [
"cpe:2.3:a:tianma-static_project:tianma-static:*:*:*:*:*:node.js:*:*"
],
@@ -6961,7 +6964,7 @@
"cpe:2.3:a:google:slashify:*:*:*:*:*:node.js:*:*"
],
"snowflake-connector-python": [
- "cpe:2.3:a:snowflake:snowflake-connector-python:*:*:*:*:*:*:*:*"
+ "cpe:2.3:a:snowflake:snowflake_connector:*:*:*:*:*:python:*:*"
],
"sockeye": [
"cpe:2.3:a:amazon:sockeye:*:*:*:*:*:python:*:*"
@@ -7041,6 +7044,9 @@
"xmpp-http-upload": [
"cpe:2.3:a:xmpp-http-upload_project:xmpp-http-upload:*:*:*:*:*:*:*:*"
],
+ "zeep": [
+ "cpe:2.3:a:python-zeep:zeep:*:*:*:*:*:python:*:*"
+ ],
"zibalPlatform": [
"cpe:2.3:a:zibal_project:zibal:*:*:*:*:*:pypi:*:*"
]
@@ -10665,6 +10671,9 @@
"cpe:2.3:a:really-simple-plugins:complianz:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:really-simple-plugins:complianz:*:*:*:*:premium:wordpress:*:*"
],
+ "compress-then-upload": [
+ "cpe:2.3:a:eliehanna:compress_\\\u0026_upload:*:*:*:*:*:wordpress:*:*"
+ ],
"conditional-extra-fees-for-woocommerce": [
"cpe:2.3:a:piwebsolution:conditional_cart_fee_\\/_extra_charge_rule_for_woocommerce_extra_fees:*:*:*:*:*:wordpress:*:*"
],
@@ -11408,6 +11417,9 @@
"database-peek": [
"cpe:2.3:a:database_peek_project:database_peek:*:*:*:*:*:wordpress:*:*"
],
+ "db-access": [
+ "cpe:2.3:a:jimbob1953:db-access:*:*:*:*:*:wordpress:*:*"
+ ],
"dc-woocommerce-multi-vendor": [
"cpe:2.3:a:multivendorx:multivendorx:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wc-marketplace:multivendor_marketplace_solution_for_woocommerce_-_wc_marketplace:*:*:*:*:*:wordpress:*:*"
@@ -18282,6 +18294,9 @@
"simple-side-tab": [
"cpe:2.3:a:rumspeed:simple_side_tab:*:*:*:*:*:wordpress:*:*"
],
+ "simple-signup-form": [
+ "cpe:2.3:a:modalsurvey:simple_signup_form:*:*:*:*:*:wordpress:*:*"
+ ],
"simple-site-verify": [
"cpe:2.3:a:idoweb:simple_site_verify:*:*:*:*:*:wordpress:*:*"
],
@@ -22457,6 +22472,9 @@
"wpextended": [
"cpe:2.3:a:wpextended:wp_extended:*:*:*:*:*:wordpress:*:*"
],
+ "wpfavicon": [
+ "cpe:2.3:a:nihal:wpfavicon:*:*:*:*:*:wordpress:*:*"
+ ],
"wpforms": [
"cpe:2.3:a:wpforms:wpforms:*:*:*:*:pro:wordpress:*:*"
],
From 1dcac54b0e10b7f61685be32707fde1695e5553e Mon Sep 17 00:00:00 2001
From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
Date: Mon, 13 Jul 2026 10:30:23 -0400
Subject: [PATCH 058/168] feat: optionally capture golang binary symbols
(#4988)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
cmd/syft/internal/options/catalog.go | 3 +-
cmd/syft/internal/options/golang.go | 13 +-
cmd/syft/internal/options/golang_test.go | 57 +
internal/capabilities/appconfig.yaml | 2 +
internal/constants.go | 3 +-
schema/json/schema-16.1.9.json | 4520 +++++++++++++++++
schema/json/schema-latest.json | 39 +-
syft/cataloging/symbols.go | 28 +
syft/cataloging/symbols_test.go | 30 +
syft/pkg/cataloger/golang/capabilities.yaml | 3 +
syft/pkg/cataloger/golang/cataloger.go | 5 +-
syft/pkg/cataloger/golang/config.go | 12 +
syft/pkg/cataloger/golang/config_test.go | 3 +
syft/pkg/cataloger/golang/package.go | 3 +-
syft/pkg/cataloger/golang/parse_go_binary.go | 51 +-
.../cataloger/golang/parse_go_binary_test.go | 69 +
syft/pkg/cataloger/golang/scan_binary.go | 16 +-
syft/pkg/cataloger/golang/stdlib_package.go | 17 +-
.../cataloger/golang/stdlib_package_test.go | 6 +-
syft/pkg/cataloger/golang/symbols.go | 332 ++
syft/pkg/cataloger/golang/symbols_test.go | 201 +
syft/pkg/golang.go | 7 +
syft/pkg/vcpkg.go | 39 +-
23 files changed, 5414 insertions(+), 45 deletions(-)
create mode 100644 cmd/syft/internal/options/golang_test.go
create mode 100644 schema/json/schema-16.1.9.json
create mode 100644 syft/cataloging/symbols.go
create mode 100644 syft/cataloging/symbols_test.go
create mode 100644 syft/pkg/cataloger/golang/symbols.go
create mode 100644 syft/pkg/cataloger/golang/symbols_test.go
diff --git a/cmd/syft/internal/options/catalog.go b/cmd/syft/internal/options/catalog.go
index 4ffbfad56..0d7e94b50 100644
--- a/cmd/syft/internal/options/catalog.go
+++ b/cmd/syft/internal/options/catalog.go
@@ -198,7 +198,8 @@ func (cfg Catalog) ToPackagesConfig() pkgcataloging.Config {
WithFromBuildSettings(cfg.Golang.MainModuleVersion.FromBuildSettings).
WithFromLDFlags(cfg.Golang.MainModuleVersion.FromLDFlags),
).
- WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)),
+ WithUsePackagesLib(*multiLevelOption(true, enrichmentEnabled(cfg.Enrich, task.Go, task.Golang), cfg.Golang.UsePackagesLib)).
+ WithCaptureSymbols(cfg.Golang.CaptureSymbols),
JavaScript: javascript.DefaultCatalogerConfig().
WithIncludeDevDependencies(*multiLevelOption(false, cfg.JavaScript.IncludeDevDependencies)).
WithSearchRemoteLicenses(*multiLevelOption(false, enrichmentEnabled(cfg.Enrich, task.JavaScript, task.Node, task.NPM), cfg.JavaScript.SearchRemoteLicenses)).
diff --git a/cmd/syft/internal/options/golang.go b/cmd/syft/internal/options/golang.go
index 10539d79c..c811a34c3 100644
--- a/cmd/syft/internal/options/golang.go
+++ b/cmd/syft/internal/options/golang.go
@@ -4,6 +4,7 @@ import (
"strings"
"github.com/anchore/clio"
+ "github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/pkg/cataloger/golang"
)
@@ -17,10 +18,12 @@ type golangConfig struct {
NoProxy string `json:"no-proxy" yaml:"no-proxy" mapstructure:"no-proxy"`
MainModuleVersion golangMainModuleVersionConfig `json:"main-module-version" yaml:"main-module-version" mapstructure:"main-module-version"`
UsePackagesLib *bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
+ CaptureSymbols cataloging.SymbolScope `json:"capture-symbols" yaml:"capture-symbols" mapstructure:"capture-symbols"`
}
var _ interface {
clio.FieldDescriber
+ clio.PostLoader
} = (*golangConfig)(nil)
func (o *golangConfig) DescribeFields(descriptions clio.FieldDescriptionSet) {
@@ -39,12 +42,19 @@ if unset this defaults to $GONOPROXY`)
always show (devel) as the version. Use these options to control heuristics to guess
a more accurate version from the binary.`)
descriptions.Add(&o.UsePackagesLib, `use the golang.org/x/tools/go/packages library, which executes golang tooling found on the path in addition to potential network access to get the most accurate results`)
+ descriptions.Add(&o.CaptureSymbols, `capture function symbols from the binary symbol table (pclntab). valid values are:
+"none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)`)
descriptions.Add(&o.MainModuleVersion.FromLDFlags, `look for LD flags that appear to be setting a version (e.g. -X main.version=1.0.0)`)
- descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
+ descriptions.Add(&o.MainModuleVersion.FromBuildSettings, `use the build settings (e.g. vcs.version & vcs.time) to craft a v0 pseudo version
(e.g. v0.0.0-20220308212642-53e6d0aaf6fb) when a more accurate version cannot be found otherwise`)
descriptions.Add(&o.MainModuleVersion.FromContents, `search for semver-like strings in the binary contents`)
}
+func (o *golangConfig) PostLoad() error {
+ o.CaptureSymbols = o.CaptureSymbols.Parse()
+ return nil
+}
+
type golangMainModuleVersionConfig struct {
FromLDFlags bool `json:"from-ld-flags" yaml:"from-ld-flags" mapstructure:"from-ld-flags"`
FromContents bool `json:"from-contents" yaml:"from-contents" mapstructure:"from-contents"`
@@ -67,5 +77,6 @@ func defaultGolangConfig() golangConfig {
FromBuildSettings: def.MainModuleVersion.FromBuildSettings,
},
UsePackagesLib: nil, // this defaults to true, which is the API default
+ CaptureSymbols: def.CaptureSymbols,
}
}
diff --git a/cmd/syft/internal/options/golang_test.go b/cmd/syft/internal/options/golang_test.go
new file mode 100644
index 000000000..643349904
--- /dev/null
+++ b/cmd/syft/internal/options/golang_test.go
@@ -0,0 +1,57 @@
+package options
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/anchore/syft/syft/cataloging"
+)
+
+func Test_golangConfig_PostLoad(t *testing.T) {
+ tests := []struct {
+ name string
+ cfg golangConfig
+ expected cataloging.SymbolScope
+ wantErr assert.ErrorAssertionFunc
+ }{
+ {
+ name: "normalize all",
+ cfg: golangConfig{CaptureSymbols: "all"},
+ expected: cataloging.SymbolScopeAll,
+ },
+ {
+ name: "normalize stdlib",
+ cfg: golangConfig{CaptureSymbols: "stdlib"},
+ expected: cataloging.SymbolScopeStdlib,
+ },
+ {
+ name: "empty defaults to none",
+ cfg: golangConfig{CaptureSymbols: ""},
+ expected: cataloging.SymbolScopeNone,
+ },
+ {
+ name: "invalid value defaults to none",
+ cfg: golangConfig{CaptureSymbols: "bogus"},
+ expected: cataloging.SymbolScopeNone,
+ },
+ {
+ name: "boolean spellings default to none",
+ cfg: golangConfig{CaptureSymbols: "true"},
+ expected: cataloging.SymbolScopeNone,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if tt.wantErr == nil {
+ tt.wantErr = assert.NoError
+ }
+ err := tt.cfg.PostLoad()
+ tt.wantErr(t, err)
+ if err != nil {
+ return
+ }
+ assert.Equal(t, tt.expected, tt.cfg.CaptureSymbols)
+ })
+ }
+}
diff --git a/internal/capabilities/appconfig.yaml b/internal/capabilities/appconfig.yaml
index e073dd32c..295df39df 100644
--- a/internal/capabilities/appconfig.yaml
+++ b/internal/capabilities/appconfig.yaml
@@ -14,6 +14,8 @@ application: # AUTO-GENERATED - application-level config keys
description: treat DLL claims or on-disk evidence for child packages as DLL claims or on-disk evidence for any parent package
- key: dotnet.relax-dll-claims-when-bundling-detected
description: show all packages from the deps.json if bundling tooling is present as a dependency (e.g. ILRepack)
+ - key: golang.capture-symbols
+ description: 'capture function symbols from the binary symbol table (pclntab). valid values are: "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib)'
- key: golang.local-mod-cache-dir
description: specify an explicit go mod cache directory, if unset this defaults to $GOPATH/pkg/mod or $HOME/go/pkg/mod
- key: golang.local-vendor-dir
diff --git a/internal/constants.go b/internal/constants.go
index 0efb094e2..b21c5adda 100644
--- a/internal/constants.go
+++ b/internal/constants.go
@@ -3,7 +3,7 @@ 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.1.8"
+ JSONSchemaVersion = "16.1.9"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@@ -15,4 +15,5 @@ const (
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
+ // 16.1.9 - add Symbols to GolangBinaryBuildinfoEntry metadata
)
diff --git a/schema/json/schema-16.1.9.json b/schema/json/schema-16.1.9.json
new file mode 100644
index 000000000..e1363bf94
--- /dev/null
+++ b/schema/json/schema-16.1.9.json
@@ -0,0 +1,4520 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/syft/json/16.1.9/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",
+ "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman."
+ },
+ "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)."
+ },
+ "AppleAppBundleEntry": {
+ "properties": {
+ "bundleIdentifier": {
+ "type": "string",
+ "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the short name of the bundle"
+ },
+ "displayName": {
+ "type": "string",
+ "description": "DisplayName is the user-facing name of the bundle"
+ },
+ "executable": {
+ "type": "string",
+ "description": "Executable is the name of the executable within the bundle"
+ },
+ "shortVersion": {
+ "type": "string",
+ "description": "ShortVersion is the release (marketing) version of the bundle"
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the build version of the bundle, which often differs from the short version"
+ },
+ "packageType": {
+ "type": "string",
+ "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)"
+ },
+ "supportedPlatforms": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")"
+ },
+ "minimumSystemVersion": {
+ "type": "string",
+ "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle"
+ },
+ "minimumOSVersion": {
+ "type": "string",
+ "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)"
+ },
+ "copyright": {
+ "type": "string",
+ "description": "Copyright is the human-readable copyright notice for the bundle"
+ },
+ "platformName": {
+ "type": "string",
+ "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")"
+ },
+ "sdkName": {
+ "type": "string",
+ "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")"
+ }
+ },
+ "type": "object",
+ "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files."
+ },
+ "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",
+ "description": "Value is the CPE string identifier."
+ },
+ "source": {
+ "type": "string",
+ "description": "Source is the source where this CPE was obtained or generated from."
+ }
+ },
+ "type": "object",
+ "required": [
+ "cpe"
+ ],
+ "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases."
+ },
+ "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."
+ },
+ "DenoLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies is the list of package specifiers that this package depends on"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies"
+ ],
+ "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file"
+ },
+ "DenoRemoteLockEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the remote URL from which the module fetcef"
+ },
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "integrity"
+ ],
+ "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file"
+ },
+ "Descriptor": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the tool that generated this SBOM."
+ },
+ "configuration": {
+ "description": "Configuration contains the tool configuration used during SBOM generation."
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "version"
+ ],
+ "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation."
+ },
+ "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",
+ "description": "Artifacts is the list of packages discovered and placed into the catalog"
+ },
+ "artifactRelationships": {
+ "items": {
+ "$ref": "#/$defs/Relationship"
+ },
+ "type": "array"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/File"
+ },
+ "type": "array",
+ "description": "note: must have omitempty"
+ },
+ "source": {
+ "$ref": "#/$defs/Source",
+ "description": "Source represents the original object that was cataloged"
+ },
+ "distro": {
+ "$ref": "#/$defs/LinuxRelease",
+ "description": "Distro represents the Linux distribution that was detected from the source"
+ },
+ "descriptor": {
+ "$ref": "#/$defs/Descriptor",
+ "description": "Descriptor is a block containing self-describing information about syft"
+ },
+ "schema": {
+ "$ref": "#/$defs/Schema",
+ "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape"
+ }
+ },
+ "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\")"
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is type of entry could be package or project for internal refs"
+ },
+ "executables": {
+ "additionalProperties": {
+ "$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)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing."
+ },
+ "appCpe": {
+ "type": "string",
+ "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)"
+ },
+ "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)"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages."
+ }
+ },
+ "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",
+ "description": "ID is a unique identifier for this file within the SBOM."
+ },
+ "location": {
+ "$ref": "#/$defs/Coordinates",
+ "description": "Location is the file path and layer information where this file was found."
+ },
+ "metadata": {
+ "$ref": "#/$defs/FileMetadataEntry",
+ "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the file contents for small files."
+ },
+ "digests": {
+ "items": {
+ "$ref": "#/$defs/Digest"
+ },
+ "type": "array",
+ "description": "Digests contains cryptographic hashes of the file contents."
+ },
+ "licenses": {
+ "items": {
+ "$ref": "#/$defs/FileLicense"
+ },
+ "type": "array",
+ "description": "Licenses contains license information discovered within this file."
+ },
+ "executable": {
+ "$ref": "#/$defs/Executable",
+ "description": "Executable contains executable metadata if this file is a binary."
+ },
+ "unknowns": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Unknowns contains unknown fields for forward compatibility."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "location"
+ ],
+ "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages."
+ },
+ "FileLicense": {
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Value is the raw license identifier or text as found in the file."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "evidence": {
+ "$ref": "#/$defs/FileLicenseEvidence",
+ "description": "Evidence contains supporting evidence for this license detection."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type"
+ ],
+ "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression."
+ },
+ "FileLicenseEvidence": {
+ "properties": {
+ "confidence": {
+ "type": "integer",
+ "description": "Confidence is the confidence score for this license detection (0-100)."
+ },
+ "offset": {
+ "type": "integer",
+ "description": "Offset is the byte offset where the license text starts in the file."
+ },
+ "extent": {
+ "type": "integer",
+ "description": "Extent is the length of the license text in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "confidence",
+ "offset",
+ "extent"
+ ],
+ "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level."
+ },
+ "FileMetadataEntry": {
+ "properties": {
+ "mode": {
+ "type": "integer",
+ "description": "Mode is the Unix file permission mode in octal format."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")."
+ },
+ "linkDestination": {
+ "type": "string",
+ "description": "LinkDestination is the target path for symbolic links."
+ },
+ "userID": {
+ "type": "integer",
+ "description": "UserID is the file owner user ID."
+ },
+ "groupID": {
+ "type": "integer",
+ "description": "GroupID is the file owner group ID."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIMEType is the MIME type of the file contents."
+ },
+ "size": {
+ "type": "integer",
+ "description": "Size is the file size in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "mode",
+ "type",
+ "userID",
+ "groupID",
+ "mimeType",
+ "size"
+ ],
+ "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file."
+ },
+ "GgufFileHeader": {
+ "properties": {
+ "ggufVersion": {
+ "type": "integer",
+ "description": "GGUFVersion is the GGUF format version (e.g., 3)"
+ },
+ "fileSize": {
+ "type": "integer",
+ "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)"
+ },
+ "architecture": {
+ "type": "string",
+ "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")"
+ },
+ "quantization": {
+ "type": "string",
+ "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")"
+ },
+ "parameters": {
+ "type": "integer",
+ "description": "Parameters is the number of model parameters (if present in header)"
+ },
+ "tensorCount": {
+ "type": "integer",
+ "description": "TensorCount is the number of tensors in the model"
+ },
+ "header": {
+ "type": "object",
+ "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication."
+ },
+ "metadataHash": {
+ "type": "string",
+ "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames."
+ },
+ "parts": {
+ "items": {
+ "$ref": "#/$defs/GgufFileHeader"
+ },
+ "type": "array",
+ "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)."
+ }
+ },
+ "type": "object",
+ "required": [
+ "ggufVersion",
+ "tensorCount"
+ ],
+ "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file."
+ },
+ "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\")."
+ },
+ "symbols": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package."
+ }
+ },
+ "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",
+ "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field."
+ },
+ "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": {
+ "additionalProperties": {
+ "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."
+ },
+ "JavascriptBunLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of runtime dependencies and their version specifiers"
+ },
+ "optionalDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "OptionalDependencies is a map of optional dependencies and their version specifiers"
+ },
+ "peerDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "PeerDependencies is a map of peer dependencies and their version specifiers"
+ },
+ "bin": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Bin is a map of binary names to the paths they are installed to"
+ },
+ "os": {
+ "type": "string",
+ "description": "OS is the operating system constraint for the package (e.g. \"darwin\")"
+ },
+ "cpu": {
+ "type": "string",
+ "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies",
+ "optionalDependencies",
+ "peerDependencies",
+ "bin",
+ "os",
+ "cpu"
+ ],
+ "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file"
+ },
+ "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."
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\""
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file."
+ },
+ "JavascriptPnpmLockEntry": {
+ "properties": {
+ "resolution": {
+ "$ref": "#/$defs/PnpmLockResolution",
+ "description": "Resolution is the resolution information for the package"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolution",
+ "dependencies"
+ ],
+ "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml 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)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "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",
+ "description": "Value is the raw license identifier or expression as found."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "urls": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "URLs are URLs where license text or information can be found."
+ },
+ "locations": {
+ "items": {
+ "$ref": "#/$defs/Location"
+ },
+ "type": "array",
+ "description": "Locations are file locations where this license was discovered."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the full license text content."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type",
+ "urls",
+ "locations"
+ ],
+ "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence 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": {
+ "additionalProperties": {
+ "$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",
+ "description": "PrettyName is a human-readable operating system name with version."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the operating system name without version information."
+ },
+ "id": {
+ "type": "string",
+ "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")."
+ },
+ "idLike": {
+ "$ref": "#/$defs/IDLikes",
+ "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the operating system version including codename if available."
+ },
+ "versionID": {
+ "type": "string",
+ "description": "VersionID is the operating system version number or identifier."
+ },
+ "versionCodename": {
+ "type": "string",
+ "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")."
+ },
+ "buildID": {
+ "type": "string",
+ "description": "BuildID is a build identifier for the operating system."
+ },
+ "imageID": {
+ "type": "string",
+ "description": "ImageID is an identifier for container or cloud images."
+ },
+ "imageVersion": {
+ "type": "string",
+ "description": "ImageVersion is the version for container or cloud images."
+ },
+ "variant": {
+ "type": "string",
+ "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")."
+ },
+ "variantID": {
+ "type": "string",
+ "description": "VariantID is the lower-case operating system variant identifier."
+ },
+ "homeURL": {
+ "type": "string",
+ "description": "HomeURL is the homepage URL for the operating system."
+ },
+ "supportURL": {
+ "type": "string",
+ "description": "SupportURL is the support or help URL for the operating system."
+ },
+ "bugReportURL": {
+ "type": "string",
+ "description": "BugReportURL is the bug reporting URL for the operating system."
+ },
+ "privacyPolicyURL": {
+ "type": "string",
+ "description": "PrivacyPolicyURL is the privacy policy URL for the operating system."
+ },
+ "cpeName": {
+ "type": "string",
+ "description": "CPEName is the Common Platform Enumeration name for the operating system."
+ },
+ "supportEnd": {
+ "type": "string",
+ "description": "SupportEnd is the end of support date or version identifier."
+ },
+ "extendedSupport": {
+ "type": "boolean",
+ "description": "ExtendedSupport indicates whether extended security or support is available."
+ }
+ },
+ "type": "object",
+ "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files."
+ },
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Arbitrary key-value pairs that can be used to annotate a location"
+ }
+ },
+ "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": {
+ "additionalProperties": {
+ "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 represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)."
+ },
+ "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/AppleAppBundleEntry"
+ },
+ {
+ "$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/DenoLockEntry"
+ },
+ {
+ "$ref": "#/$defs/DenoRemoteLockEntry"
+ },
+ {
+ "$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/GgufFileHeader"
+ },
+ {
+ "$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/JavascriptBunLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackage"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackageLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptPnpmLockEntry"
+ },
+ {
+ "$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/VcpkgManifest"
+ },
+ {
+ "$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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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."
+ },
+ "PnpmLockResolution": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity"
+ ],
+ "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification."
+ },
+ "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/)."
+ },
+ "PythonPdmFileEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the file download URL"
+ },
+ "digest": {
+ "$ref": "#/$defs/PythonFileDigest",
+ "description": "Digest is the hash digest of the file hosted at the URL"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "digest"
+ ]
+ },
+ "PythonPdmLockEntry": {
+ "properties": {
+ "summary": {
+ "type": "string",
+ "description": "Summary provides a description of the package"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files with their paths and hash digests (for the base package without extras)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment"
+ },
+ "requiresPython": {
+ "type": "string",
+ "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")."
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependency specifications for the base package (without extras)"
+ },
+ "extras": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmLockExtraVariant"
+ },
+ "type": "array",
+ "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "summary",
+ "files"
+ ],
+ "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file."
+ },
+ "PythonPdmLockExtraVariant": {
+ "properties": {
+ "extras": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependencies specific to this extras variant"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files specific to this variant (only populated if different from base)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "extras"
+ ],
+ "description": "PythonPdmLockExtraVariant represents a specific extras combination variant 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",
+ "description": "Parent is the ID of the parent artifact in this relationship."
+ },
+ "child": {
+ "type": "string",
+ "description": "Child is the ID of the child artifact in this relationship."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional relationship-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "parent",
+ "child",
+ "type"
+ ],
+ "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package."
+ },
+ "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",
+ "description": "Version is the JSON schema version for this document format."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL is the URL to the JSON schema definition document."
+ }
+ },
+ "type": "object",
+ "required": [
+ "version",
+ "url"
+ ],
+ "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format."
+ },
+ "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",
+ "description": "ID is a unique identifier for the analyzed source artifact."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the analyzed artifact (e.g., image tag)."
+ },
+ "supplier": {
+ "type": "string",
+ "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional source-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "type",
+ "metadata"
+ ],
+ "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive."
+ },
+ "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)."
+ },
+ "VcpkgManifest": {
+ "properties": {
+ "description": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest)."
+ },
+ "documentation": {
+ "type": "string",
+ "description": "Documentation is the URL to the package's documentation."
+ },
+ "full-version": {
+ "type": "string",
+ "description": "FullVersion is the complete version string including the port-version suffix (e.g. \"1.2.3#2\")."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")."
+ },
+ "port-version": {
+ "type": "integer",
+ "description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version."
+ },
+ "maintainers": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Maintainers are the people responsible for maintaining the vcpkg port."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the package name as declared in the manifest."
+ },
+ "supports": {
+ "type": "string",
+ "description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")."
+ },
+ "registry": {
+ "$ref": "#/$defs/VcpkgRegistryEntry",
+ "description": "Registry indicates where the package definition came from."
+ },
+ "triplet": {
+ "type": "string",
+ "description": "Triplet is the build target discovered from the build folder (e.g. \"x64-linux\")."
+ }
+ },
+ "type": "object",
+ "required": [
+ "full-version",
+ "version",
+ "port-version",
+ "name"
+ ],
+ "description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package."
+ },
+ "VcpkgRegistryEntry": {
+ "properties": {
+ "baseline": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "packages": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "path": {
+ "type": "string"
+ },
+ "reference": {
+ "type": "string"
+ },
+ "repository": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "description": "Matches definition of Vcpkg \"Registry\"."
+ },
+ "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 1fc811850..e1363bf94 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.1.8/document",
+ "$id": "anchore.io/schema/syft/json/16.1.9/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -1653,6 +1653,13 @@
},
"type": "array",
"description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")."
+ },
+ "symbols": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package."
}
},
"type": "object",
@@ -4394,39 +4401,47 @@
"items": {
"type": "string"
},
- "type": "array"
+ "type": "array",
+ "description": "Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest)."
},
"documentation": {
- "type": "string"
+ "type": "string",
+ "description": "Documentation is the URL to the package's documentation."
},
"full-version": {
- "type": "string"
+ "type": "string",
+ "description": "FullVersion is the complete version string including the port-version suffix (e.g. \"1.2.3#2\")."
},
"version": {
- "type": "string"
+ "type": "string",
+ "description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")."
},
"port-version": {
- "type": "integer"
+ "type": "integer",
+ "description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version."
},
"maintainers": {
"items": {
"type": "string"
},
- "type": "array"
+ "type": "array",
+ "description": "Maintainers are the people responsible for maintaining the vcpkg port."
},
"name": {
- "type": "string"
+ "type": "string",
+ "description": "Name is the package name as declared in the manifest."
},
"supports": {
- "type": "string"
+ "type": "string",
+ "description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")."
},
"registry": {
"$ref": "#/$defs/VcpkgRegistryEntry",
- "description": "to show where it came from"
+ "description": "Registry indicates where the package definition came from."
},
"triplet": {
"type": "string",
- "description": "found by looking at build folder to find target. ex. \"x64-linux\""
+ "description": "Triplet is the build target discovered from the build folder (e.g. \"x64-linux\")."
}
},
"type": "object",
@@ -4436,7 +4451,7 @@
"port-version",
"name"
],
- "description": "used for metadata."
+ "description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package."
},
"VcpkgRegistryEntry": {
"properties": {
diff --git a/syft/cataloging/symbols.go b/syft/cataloging/symbols.go
new file mode 100644
index 000000000..4e892fcee
--- /dev/null
+++ b/syft/cataloging/symbols.go
@@ -0,0 +1,28 @@
+package cataloging
+
+import "strings"
+
+// SymbolScope controls which packages get function symbols (from a binary's symbol table) attached to their metadata.
+type SymbolScope string
+
+const (
+ // SymbolScopeNone disables symbol capture entirely.
+ SymbolScopeNone SymbolScope = "none"
+
+ // SymbolScopeStdlib captures symbols only for the synthetic "stdlib" package, leaving module packages without symbols.
+ SymbolScopeStdlib SymbolScope = "stdlib"
+
+ // SymbolScopeAll captures symbols for all module packages as well as the synthetic "stdlib" package.
+ SymbolScopeAll SymbolScope = "all"
+)
+
+// Parse normalizes a SymbolScope, treating empty (unset) and unrecognized values as SymbolScopeNone.
+func (s SymbolScope) Parse() SymbolScope {
+ switch strings.ToLower(strings.TrimSpace(string(s))) {
+ case string(SymbolScopeAll):
+ return SymbolScopeAll
+ case string(SymbolScopeStdlib):
+ return SymbolScopeStdlib
+ }
+ return SymbolScopeNone
+}
diff --git a/syft/cataloging/symbols_test.go b/syft/cataloging/symbols_test.go
new file mode 100644
index 000000000..307119a15
--- /dev/null
+++ b/syft/cataloging/symbols_test.go
@@ -0,0 +1,30 @@
+package cataloging
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func Test_SymbolScope_Parse(t *testing.T) {
+ tests := []struct {
+ input string
+ expected SymbolScope
+ }{
+ {"all", SymbolScopeAll},
+ {"ALL", SymbolScopeAll},
+ {" all ", SymbolScopeAll},
+ {"stdlib", SymbolScopeStdlib},
+ {"Stdlib", SymbolScopeStdlib},
+ {"none", SymbolScopeNone},
+ {"", SymbolScopeNone},
+ {"true", SymbolScopeNone},
+ {"false", SymbolScopeNone},
+ {"bogus", SymbolScopeNone},
+ }
+ for _, test := range tests {
+ t.Run(test.input, func(t *testing.T) {
+ assert.Equal(t, test.expected, SymbolScope(test.input).Parse())
+ })
+ }
+}
diff --git a/syft/pkg/cataloger/golang/capabilities.yaml b/syft/pkg/cataloger/golang/capabilities.yaml
index b0e55805c..01e24b146 100644
--- a/syft/pkg/cataloger/golang/capabilities.yaml
+++ b/syft/pkg/cataloger/golang/capabilities.yaml
@@ -24,6 +24,9 @@ configs: # AUTO-GENERATED - config structs and their fields
- key: NoProxy
description: 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_key: golang.no-proxy
+ - key: CaptureSymbols
+ description: CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib).
+ app_key: golang.capture-symbols
catalogers:
- ecosystem: go # MANUAL
name: go-module-binary-cataloger # AUTO-GENERATED
diff --git a/syft/pkg/cataloger/golang/cataloger.go b/syft/pkg/cataloger/golang/cataloger.go
index d52c210c0..f51dc8955 100644
--- a/syft/pkg/cataloger/golang/cataloger.go
+++ b/syft/pkg/cataloger/golang/cataloger.go
@@ -26,10 +26,11 @@ func NewGoModuleFileCataloger(opts CatalogerConfig) pkg.Cataloger {
// NewGoModuleBinaryCataloger returns a new cataloger object that searches within binaries built by the go compiler.
func NewGoModuleBinaryCataloger(opts CatalogerConfig) pkg.Cataloger {
+ c := newGoBinaryCataloger(opts)
return generic.NewCataloger(binaryCatalogerName).
WithParserByMimeTypes(
- newGoBinaryCataloger(opts).parseGoBinary,
+ c.parseGoBinary,
mimetype.ExecutableMIMETypeSet.List()...,
).
- WithResolvingProcessors(stdlibProcessor)
+ WithResolvingProcessors(c.stdlibProcessor)
}
diff --git a/syft/pkg/cataloger/golang/config.go b/syft/pkg/cataloger/golang/config.go
index c90767275..228cd67b1 100644
--- a/syft/pkg/cataloger/golang/config.go
+++ b/syft/pkg/cataloger/golang/config.go
@@ -7,6 +7,7 @@ import (
"github.com/anchore/go-homedir"
"github.com/anchore/syft/internal/log"
+ "github.com/anchore/syft/syft/cataloging"
)
const (
@@ -49,6 +50,11 @@ type CatalogerConfig struct {
MainModuleVersion MainModuleVersionConfig `yaml:"main-module-version" json:"main-module-version" mapstructure:"main-module-version"`
+ // CaptureSymbols controls extracting function symbols from the binary symbol table (pclntab). Valid values are
+ // "none" (disabled), "stdlib" (only the synthetic stdlib package), and "all" (all module packages plus stdlib).
+ // app-config: golang.capture-symbols
+ CaptureSymbols cataloging.SymbolScope `yaml:"capture-symbols" json:"capture-symbols" mapstructure:"capture-symbols"`
+
// Whether to use the golang.org/x/tools/go/packages, which executes golang tooling found on the path in addition to potential network access
UsePackagesLib bool `json:"use-packages-lib" yaml:"use-packages-lib" mapstructure:"use-packages-lib"`
}
@@ -76,6 +82,7 @@ func DefaultCatalogerConfig() CatalogerConfig {
UsePackagesLib: true,
MainModuleVersion: DefaultMainModuleVersionConfig(),
LocalModCacheDir: defaultGoModDir(),
+ CaptureSymbols: cataloging.SymbolScopeNone,
}
// first process the proxy settings
@@ -184,6 +191,11 @@ func (g CatalogerConfig) WithMainModuleVersion(input MainModuleVersionConfig) Ca
return g
}
+func (g CatalogerConfig) WithCaptureSymbols(input cataloging.SymbolScope) CatalogerConfig {
+ g.CaptureSymbols = input
+ return g
+}
+
func (g CatalogerConfig) WithUsePackagesLib(useLib bool) CatalogerConfig {
g.UsePackagesLib = useLib
return g
diff --git a/syft/pkg/cataloger/golang/config_test.go b/syft/pkg/cataloger/golang/config_test.go
index 4b563e656..1b16a190a 100644
--- a/syft/pkg/cataloger/golang/config_test.go
+++ b/syft/pkg/cataloger/golang/config_test.go
@@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/go-homedir"
+ "github.com/anchore/syft/syft/cataloging"
)
func Test_Config(t *testing.T) {
@@ -58,6 +59,7 @@ func Test_Config(t *testing.T) {
NoProxy: []string{"my.private", "no.proxy"},
MainModuleVersion: DefaultMainModuleVersionConfig(),
UsePackagesLib: true,
+ CaptureSymbols: cataloging.SymbolScopeNone,
},
},
{
@@ -86,6 +88,7 @@ func Test_Config(t *testing.T) {
NoProxy: []string{"alt.no.proxy"},
MainModuleVersion: DefaultMainModuleVersionConfig(),
UsePackagesLib: true,
+ CaptureSymbols: cataloging.SymbolScopeNone,
},
},
}
diff --git a/syft/pkg/cataloger/golang/package.go b/syft/pkg/cataloger/golang/package.go
index f689c1a40..4e1e39a80 100644
--- a/syft/pkg/cataloger/golang/package.go
+++ b/syft/pkg/cataloger/golang/package.go
@@ -45,7 +45,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi
return p
}
-func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments []string) pkg.GolangBinaryBuildinfoEntry {
+func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments, symbols []string) pkg.GolangBinaryBuildinfoEntry {
if dep.Replace != nil {
dep = dep.Replace
}
@@ -58,6 +58,7 @@ func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture st
MainModule: mainModule,
GoCryptoSettings: cryptoSettings,
GoExperiments: experiments,
+ Symbols: symbols,
}
}
diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go
index 9cb25337f..8553ec4be 100644
--- a/syft/pkg/cataloger/golang/parse_go_binary.go
+++ b/syft/pkg/cataloger/golang/parse_go_binary.go
@@ -13,6 +13,7 @@ import (
"runtime/debug"
"slices"
"strings"
+ "sync"
"time"
"golang.org/x/mod/module"
@@ -20,6 +21,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
+ "github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/pkg"
@@ -49,15 +51,45 @@ const devel = "(devel)"
type goBinaryCataloger struct {
licenseResolver goLicenseResolver
mainModuleVersion MainModuleVersionConfig
+ symbolScope cataloging.SymbolScope
+
+ // stdlibSymbols holds the standard-library function symbols discovered per binary (keyed by the
+ // binary's location), populated during parsing and consumed by stdlibProcessor when it builds the
+ // synthetic "stdlib" package. Guarded by stdlibSymbolsMu because parsers run concurrently.
+ stdlibSymbols map[file.Coordinates][]string
+ stdlibSymbolsMu sync.Mutex
}
func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger {
return &goBinaryCataloger{
licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts),
mainModuleVersion: opts.MainModuleVersion,
+ symbolScope: opts.CaptureSymbols,
+ stdlibSymbols: make(map[file.Coordinates][]string),
}
}
+// recordStdlibSymbols merges the standard-library symbols discovered for a binary location so the
+// stdlib processor can attach them to the synthetic stdlib package.
+func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols []string) {
+ if len(symbols) == 0 {
+ return
+ }
+ c.stdlibSymbolsMu.Lock()
+ defer c.stdlibSymbolsMu.Unlock()
+ merged := slices.Concat(c.stdlibSymbols[coord], symbols)
+ slices.Sort(merged)
+ c.stdlibSymbols[coord] = slices.Compact(merged)
+}
+
+// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a copy
+// so callers cannot alias (and later mutate or race on) the map's internal slice.
+func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) []string {
+ c.stdlibSymbolsMu.Lock()
+ defer c.stdlibSymbolsMu.Unlock()
+ return slices.Clone(c.stdlibSymbols[coord])
+}
+
// parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler.
func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
@@ -68,7 +100,7 @@ func (c *goBinaryCataloger) parseGoBinary(ctx context.Context, resolver file.Res
}
defer internal.CloseAndLogError(reader.ReadCloser, reader.RealPath)
- mods, errs := scanFile(reader.Location, unionReader)
+ mods, errs := scanFile(reader.Location, unionReader, c.symbolScope != cataloging.SymbolScopeNone)
var rels []artifact.Relationship
for _, mod := range mods {
@@ -128,6 +160,15 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
mod.Main = createMainModuleFromPath(mod)
}
+ symbolsByModule, stdlibSymbols := moduleSymbols(mod.symbols, &mod.Main, mod.Deps)
+ c.recordStdlibSymbols(location.Coordinates, stdlibSymbols)
+
+ if c.symbolScope != cataloging.SymbolScopeAll {
+ // only the "all" scope attaches per-module symbols; for the "stdlib" scope we keep just the
+ // recorded stdlib symbols. nil map lookups below then yield nil symbol lists for each module.
+ symbolsByModule = nil
+ }
+
var pkgs []pkg.Package
for _, dep := range mod.Deps {
if dep == nil {
@@ -147,6 +188,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
nil,
mod.cryptoSettings,
experiments,
+ symbolsByModule[dep.Path],
)
p := c.newGoBinaryPackage(
@@ -164,7 +206,7 @@ func (c *goBinaryCataloger) buildGoPkgInfo(ctx context.Context, resolver file.Re
return nil, pkgs
}
- main := c.makeGoMainPackage(ctx, resolver, mod, arch, location, reader)
+ main := c.makeGoMainPackage(ctx, resolver, mod, arch, location, reader, symbolsByModule[mod.Main.Path])
return &main, pkgs
}
@@ -179,7 +221,7 @@ func missingMainModule(mod *extendedBuildInfo) bool {
return mod.Main == moduleFromPartialPackageBuild
}
-func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser) pkg.Package {
+func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser, symbols []string) pkg.Package {
gbs := getBuildSettings(mod.Settings)
lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version)
gover, experiments := getExperimentsFromVersion(mod.GoVersion)
@@ -192,6 +234,7 @@ func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file
gbs,
mod.cryptoSettings,
experiments,
+ symbols,
)
if mod.Main.Version == devel {
@@ -387,7 +430,7 @@ func getExperimentsFromVersion(version string) (string, []string) {
version, rest, ok := strings.Cut(version, " ")
if ok {
// Assume they may add more non-version chunks in the future, so only look for "X:".
- for _, chunk := range strings.Split(rest, " ") {
+ for chunk := range strings.SplitSeq(rest, " ") {
if strings.HasPrefix(rest, "X:") {
csv := strings.TrimPrefix(chunk, "X:")
experiments = append(experiments, strings.Split(csv, ",")...)
diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go
index d0fda31fe..648068822 100644
--- a/syft/pkg/cataloger/golang/parse_go_binary_test.go
+++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go
@@ -18,6 +18,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/fileresolver"
"github.com/anchore/syft/syft/internal/unionreader"
@@ -1422,3 +1423,71 @@ type alwaysErrorReader struct{}
func (alwaysErrorReader) Read(_ []byte) (int, error) {
return 0, errors.New("read from always error reader")
}
+
+func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
+ location := file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"})
+
+ // the symbols a binary would carry once scanFile has extracted them: one main-package symbol, one
+ // dependency symbol, and one standard-library symbol. For the "none" scope scanFile never runs, so the
+ // build info carries no symbols at all.
+ populatedSymbols := []binarySymbol{
+ {packagePath: "main", name: "main.main"},
+ {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
+ {packagePath: "net/http", name: "net/http.(*Client).Do"},
+ }
+
+ tests := []struct {
+ name string
+ scope cataloging.SymbolScope
+ symbols []binarySymbol
+ wantMainSyms []string
+ wantDepSyms []string
+ wantStdlibSyms []string
+ }{
+ {
+ name: "none captures nothing",
+ scope: cataloging.SymbolScopeNone,
+ symbols: nil,
+ },
+ {
+ name: "stdlib captures only the stdlib package",
+ scope: cataloging.SymbolScopeStdlib,
+ symbols: populatedSymbols,
+ wantStdlibSyms: []string{"net/http.(*Client).Do"},
+ },
+ {
+ name: "all captures module and stdlib packages",
+ scope: cataloging.SymbolScopeAll,
+ symbols: populatedSymbols,
+ wantMainSyms: []string{"main.main"},
+ wantDepSyms: []string{"github.com/foo/bar.Parse"},
+ wantStdlibSyms: []string{"net/http.(*Client).Do"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ mod := &extendedBuildInfo{
+ BuildInfo: &debug.BuildInfo{
+ GoVersion: "go1.22.0",
+ Main: debug.Module{Path: "github.com/anchore/syft", Version: "v1.0.0"},
+ Deps: []*debug.Module{{Path: "github.com/foo/bar", Version: "v1.2.3"}},
+ },
+ arch: "amd64",
+ symbols: tt.symbols,
+ }
+
+ c := newGoBinaryCataloger(CatalogerConfig{CaptureSymbols: tt.scope})
+ reader, err := unionreader.GetUnionReader(io.NopCloser(strings.NewReader("")))
+ require.NoError(t, err)
+
+ mainPkg, pkgs := c.buildGoPkgInfo(context.Background(), fileresolver.Empty{}, location, mod, mod.arch, reader)
+ require.NotNil(t, mainPkg)
+ require.Len(t, pkgs, 1)
+
+ assert.Equal(t, tt.wantMainSyms, mainPkg.Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "main module symbols")
+ assert.Equal(t, tt.wantDepSyms, pkgs[0].Metadata.(pkg.GolangBinaryBuildinfoEntry).Symbols, "dependency symbols")
+ assert.Equal(t, tt.wantStdlibSyms, c.stdlibSymbolsFor(location.Coordinates), "recorded stdlib symbols")
+ })
+ }
+}
diff --git a/syft/pkg/cataloger/golang/scan_binary.go b/syft/pkg/cataloger/golang/scan_binary.go
index cc41d7190..84fd6d2b2 100644
--- a/syft/pkg/cataloger/golang/scan_binary.go
+++ b/syft/pkg/cataloger/golang/scan_binary.go
@@ -18,10 +18,11 @@ type extendedBuildInfo struct {
*debug.BuildInfo
cryptoSettings []string
arch string
+ symbols []binarySymbol
}
// scanFile scans file to try to report the Go and module versions.
-func scanFile(location file.Location, reader unionreader.UnionReader) ([]*extendedBuildInfo, error) {
+func scanFile(location file.Location, reader unionreader.UnionReader, captureSymbols bool) ([]*extendedBuildInfo, error) {
// NOTE: multiple readers are returned to cover universal binaries, which are files
// with more than one binary
readers, errs := unionreader.GetReaders(reader)
@@ -61,7 +62,18 @@ func scanFile(location file.Location, reader unionreader.UnionReader) ([]*extend
}
}
- builds = append(builds, &extendedBuildInfo{BuildInfo: bi, cryptoSettings: v, arch: arch})
+ var symbols []binarySymbol
+ if captureSymbols {
+ symbols, err = getSymbols(r)
+ if err != nil {
+ log.WithFields("file", location.RealPath, "error", err).Trace("unable to read golang symbol info")
+ // don't skip this build info.
+ // we can still catalog packages, even if we can't get the symbol information
+ errs = unknown.Appendf(errs, location, "unable to read golang symbol info: %w", err)
+ }
+ }
+
+ builds = append(builds, &extendedBuildInfo{BuildInfo: bi, cryptoSettings: v, arch: arch, symbols: symbols})
}
return builds, errs
}
diff --git a/syft/pkg/cataloger/golang/stdlib_package.go b/syft/pkg/cataloger/golang/stdlib_package.go
index b1c7ea67a..1743372e5 100644
--- a/syft/pkg/cataloger/golang/stdlib_package.go
+++ b/syft/pkg/cataloger/golang/stdlib_package.go
@@ -12,12 +12,12 @@ import (
"github.com/anchore/syft/syft/pkg"
)
-func stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
- compilerPkgs, newRelationships := stdlibPackageAndRelationships(ctx, pkgs)
+func (c *goBinaryCataloger) stdlibProcessor(ctx context.Context, _ file.Resolver, pkgs []pkg.Package, relationships []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
+ compilerPkgs, newRelationships := c.stdlibPackageAndRelationships(ctx, pkgs)
return append(pkgs, compilerPkgs...), append(relationships, newRelationships...), err
}
-func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) {
+func (c *goBinaryCataloger) stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]pkg.Package, []artifact.Relationship) {
var goCompilerPkgs []pkg.Package
var relationships []artifact.Relationship
totalLocations := file.NewLocationSet()
@@ -33,7 +33,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p
continue
}
- stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations)
+ stdLibPkg := newGoStdLib(ctx, mValue.GoCompiledVersion, goPkg.Locations, c.stdlibSymbolsFor(location.Coordinates))
if stdLibPkg == nil {
continue
}
@@ -50,7 +50,7 @@ func stdlibPackageAndRelationships(ctx context.Context, pkgs []pkg.Package) ([]p
return goCompilerPkgs, relationships
}
-func newGoStdLib(ctx context.Context, version string, location file.LocationSet) *pkg.Package {
+func newGoStdLib(ctx context.Context, version string, location file.LocationSet, symbols []string) *pkg.Package {
stdlibCpe, err := generateStdlibCpe(version)
if err != nil {
return nil
@@ -66,6 +66,7 @@ func newGoStdLib(ctx context.Context, version string, location file.LocationSet)
Type: pkg.GoModulePkg,
Metadata: pkg.GolangBinaryBuildinfoEntry{
GoCompiledVersion: version,
+ Symbols: symbols,
},
}
goCompilerPkg.SetID()
@@ -79,10 +80,10 @@ func generateStdlibCpe(version string) (stdlibCpe cpe.CPE, err error) {
// we also need to trim starting from the first + to
// correctly extract potential rc candidate information for cpe generation
- // ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found then + is returned
- after, _, found := strings.Cut("+", version)
+ // ex: 2.0.0-rc.1+build.123 -> 2.0.0-rc.1; if no + is found version is unchanged
+ before, _, found := strings.Cut(version, "+")
if found {
- version = after
+ version = before
}
// extracting and
diff --git a/syft/pkg/cataloger/golang/stdlib_package_test.go b/syft/pkg/cataloger/golang/stdlib_package_test.go
index 395f626cd..3c4e8ff75 100644
--- a/syft/pkg/cataloger/golang/stdlib_package_test.go
+++ b/syft/pkg/cataloger/golang/stdlib_package_test.go
@@ -88,7 +88,8 @@ func Test_stdlibPackageAndRelationships(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, tt.pkgs)
+ c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)}
+ gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, tt.pkgs)
assert.Len(t, gotPkgs, tt.wantPkgs)
assert.Len(t, gotRels, tt.wantRels)
})
@@ -137,7 +138,8 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) {
Type: artifact.DependencyOfRelationship,
}
- gotPkgs, gotRels := stdlibPackageAndRelationships(ctx, []pkg.Package{p})
+ c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)}
+ gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, []pkg.Package{p})
require.Len(t, gotPkgs, 1)
gotPkg := gotPkgs[0]
diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go
new file mode 100644
index 000000000..3d553a739
--- /dev/null
+++ b/syft/pkg/cataloger/golang/symbols.go
@@ -0,0 +1,332 @@
+package golang
+
+import (
+ "bytes"
+ "debug/elf"
+ "debug/gosym"
+ "debug/macho"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "runtime/debug"
+ "slices"
+ "strings"
+)
+
+// mainPackage is the import path the linker assigns to the binary's main package.
+const mainPackage = "main"
+
+// binarySymbol represents a single function symbol extracted from a go binary's pclntab.
+type binarySymbol struct {
+ // packagePath is the import path of the package that owns the symbol (e.g. "github.com/foo/bar/internal/baz")
+ packagePath string
+
+ // name is the fully qualified symbol name (e.g. "github.com/foo/bar/internal/baz.(*Type).Method")
+ name string
+}
+
+// getSymbols extracts all function symbols from the pclntab of a go binary. The pclntab is required by the
+// go runtime (for panic tracebacks and GC), so it is present even in binaries built with -ldflags="-s -w".
+func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ // the gosym package can panic on malformed pclntab data
+ syms = nil
+ err = fmt.Errorf("recovered from panic while reading pclntab: %v", r)
+ }
+ }()
+
+ pclntab, textStart, err := readPclntab(r)
+ if err != nil {
+ return nil, err
+ }
+
+ table, err := gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart))
+ if err != nil {
+ return nil, fmt.Errorf("unable to parse pclntab: %w", err)
+ }
+
+ seen := make(map[string]struct{})
+ for _, fn := range table.Funcs {
+ if fn.Sym == nil || isCompilerGeneratedName(fn.Name) {
+ continue
+ }
+ seen[fn.Name] = struct{}{}
+ syms = append(syms, binarySymbol{
+ packagePath: fn.PackageName(),
+ name: fn.Name,
+ })
+ }
+
+ // debug/gosym only exposes top-level functions; functions that the compiler inlined into their
+ // callers are absent from table.Funcs even though their names are recorded in the pclntab funcname
+ // table (used to reconstruct inlined frames in tracebacks). Recover those names so that a
+ // vulnerable-but-inlined function (e.g. a small stdlib wrapper) is still reported as present.
+ for _, name := range funcNameTable(pclntab) {
+ if _, ok := seen[name]; ok {
+ continue
+ }
+ pkgPath := packagePathFromSymbolName(name)
+ if pkgPath == "" {
+ continue
+ }
+ seen[name] = struct{}{}
+ syms = append(syms, binarySymbol{packagePath: pkgPath, name: name})
+ }
+
+ return syms, nil
+}
+
+// packagePathFromSymbolName derives the owning package import path from a fully qualified symbol name.
+// The package path is everything up to the first "." that follows the final "/" — e.g.
+// "path/filepath.IsLocal" -> "path/filepath" and "golang.org/x/net/html.(*Tokenizer).Next" ->
+// "golang.org/x/net/html". Returns "" when the name has no package-qualifying dot or is compiler-generated.
+func packagePathFromSymbolName(name string) string {
+ if isCompilerGeneratedName(name) {
+ return ""
+ }
+ name = nameWithoutTypeArgs(name)
+ slash := strings.LastIndex(name, "/")
+ dot := strings.IndexByte(name[slash+1:], '.')
+ if dot < 0 {
+ return ""
+ }
+ return name[:slash+1+dot]
+}
+
+// nameWithoutTypeArgs strips the type-argument portion from an instantiated generic symbol name, e.g.
+// "foo/bar.Do[net/url.Values]" -> "foo/bar.Do". The slashes and dots inside the brackets would otherwise
+// corrupt package-path derivation (yielding "foo/bar.Do[net" for the example above). Mirrors
+// debug/gosym's (*Sym).nameWithoutInst.
+func nameWithoutTypeArgs(name string) string {
+ start := strings.IndexByte(name, '[')
+ if start < 0 {
+ return name
+ }
+ end := strings.LastIndexByte(name, ']')
+ if end < 0 {
+ // malformed: an opening bracket should always have a closing one
+ return name
+ }
+ return name[:start] + name[end+1:]
+}
+
+// oldStyleCompilerGeneratedPrefixes match compiler/linker-generated symbols from toolchains older than
+// go1.20, which used "." where newer toolchains use ":" (e.g. "go.buildid" is now "go:buildid",
+// "go.type.*" is now "go:type.*"). These must be prefix (not substring) matches, and a bare "go." prefix
+// is not enough: legitimate module paths such as "go.uber.org/zap" also start with "go.".
+var oldStyleCompilerGeneratedPrefixes = []string{
+ "go.buildid",
+ "go.builtin.",
+ "go.constinfo.",
+ "go.cuinfo.",
+ "go.func.",
+ "go.importpath.",
+ "go.info.",
+ "go.interface.",
+ "go.itab.",
+ "go.itablink.",
+ "go.map.",
+ "go.shape.",
+ "go.string.",
+ "go.type.",
+ "go.typelink.",
+ "type.",
+}
+
+// isCompilerGeneratedName reports whether a symbol name was synthesized by the compiler or linker rather
+// than declared in Go source. Since go1.20 these names contain ':' or '..' (e.g. "type:.eq.*",
+// "go:string.*") — byte sequences that never appear in a real Go import path or identifier. Older
+// toolchains used '.' as the separator (e.g. "go.type.*", "type..hash.*"), which is matched against the
+// known reserved prefixes. Such names belong to no package and are dropped rather than mis-attributed
+// (e.g. bucketed under a bogus "type" stdlib package).
+func isCompilerGeneratedName(name string) bool {
+ if strings.Contains(name, ":") || strings.Contains(name, "..") {
+ return true
+ }
+ for _, prefix := range oldStyleCompilerGeneratedPrefixes {
+ if strings.HasPrefix(name, prefix) {
+ return true
+ }
+ }
+ return false
+}
+
+// funcNameTable returns every function name recorded in the pclntab's funcname table, including the
+// names of inlined functions that debug/gosym does not expose. It parses the pclntab header for the
+// Go 1.16+ layouts; on any unrecognized layout or out-of-bounds offset it returns nil (fail-soft), so
+// callers fall back to the debug/gosym function set. See the runtime's pcHeader / moduledata layout.
+func funcNameTable(pclntab []byte) []string {
+ if len(pclntab) < 8 {
+ return nil
+ }
+
+ magic := binary.LittleEndian.Uint32(pclntab[0:4])
+ // the field before funcnameOffset is textStart, which exists in the 1.18+ headers but not 1.16/1.17
+ var hasTextStart bool
+ switch magic {
+ case 0xfffffff1, 0xfffffff0: // go1.20+, go1.18/1.19
+ hasTextStart = true
+ case 0xfffffffa: // go1.16/1.17
+ hasTextStart = false
+ default:
+ return nil
+ }
+
+ ptrSize := int(pclntab[7])
+ if ptrSize != 4 && ptrSize != 8 {
+ return nil
+ }
+
+ readWord := func(idx int) (uint64, bool) {
+ off := 8 + idx*ptrSize
+ if off+ptrSize > len(pclntab) {
+ return 0, false
+ }
+ if ptrSize == 8 {
+ return binary.LittleEndian.Uint64(pclntab[off : off+8]), true
+ }
+ return uint64(binary.LittleEndian.Uint32(pclntab[off : off+4])), true
+ }
+
+ // header words after (nfunc, nfiles): [textStart,] funcnameOffset, cuOffset, ...
+ funcnameIdx := 2
+ if hasTextStart {
+ funcnameIdx = 3
+ }
+ funcnameOffset, ok1 := readWord(funcnameIdx)
+ cuOffset, ok2 := readWord(funcnameIdx + 1)
+ if !ok1 || !ok2 {
+ return nil
+ }
+
+ start, end := int(funcnameOffset), int(cuOffset)
+ if start < 0 || end > len(pclntab) || start >= end {
+ return nil
+ }
+
+ var names []string
+ for raw := range bytes.SplitSeq(pclntab[start:end], []byte{0}) {
+ if len(raw) == 0 {
+ continue
+ }
+ names = append(names, string(raw))
+ }
+ return names
+}
+
+// readPclntab locates the pclntab and the start address of the text segment within the binary.
+func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) {
+ ident := make([]byte, 16)
+ if n, err := r.ReadAt(ident, 0); n < len(ident) || err != nil {
+ return nil, 0, errUnrecognizedFormat
+ }
+
+ switch {
+ case strings.HasPrefix(string(ident), "\x7FELF"):
+ f, err := elf.NewFile(r)
+ if err != nil {
+ return nil, 0, fmt.Errorf("unable to parse ELF binary: %w", err)
+ }
+ sect := f.Section(".gopclntab")
+ if sect == nil {
+ return nil, 0, fmt.Errorf("no .gopclntab section found")
+ }
+ pclntab, err := sect.Data()
+ if err != nil {
+ return nil, 0, fmt.Errorf("unable to read .gopclntab section: %w", err)
+ }
+ text := f.Section(".text")
+ if text == nil {
+ return nil, 0, fmt.Errorf("no .text section found")
+ }
+ return pclntab, text.Addr, nil
+ case strings.HasPrefix(string(ident), "\xFE\xED\xFA") || strings.HasPrefix(string(ident[1:]), "\xFA\xED\xFE"):
+ f, err := macho.NewFile(r)
+ if err != nil {
+ return nil, 0, fmt.Errorf("unable to parse Mach-O binary: %w", err)
+ }
+ sect := f.Section("__gopclntab")
+ if sect == nil {
+ return nil, 0, fmt.Errorf("no __gopclntab section found")
+ }
+ pclntab, err := sect.Data()
+ if err != nil {
+ return nil, 0, fmt.Errorf("unable to read __gopclntab section: %w", err)
+ }
+ text := f.Section("__text")
+ if text == nil {
+ return nil, 0, fmt.Errorf("no __text section found")
+ }
+ return pclntab, text.Addr, nil
+ }
+
+ // note: PE and XCOFF binaries do not place the pclntab in a dedicated section; locating it requires
+ // walking the symbol table for runtime.pclntab markers, which is not yet supported here
+ return nil, 0, errUnrecognizedFormat
+}
+
+// moduleSymbols attributes each extracted symbol to the module that owns it (by longest module path prefix
+// of the symbol's package path) and returns a sorted, deduplicated list of symbol names per module path.
+// Symbols from the "main" package are attributed to the main module. Standard-library symbols (which belong
+// to no module) are collected separately and returned as the second value so they can be attached to the
+// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a
+// recognizable stdlib import path are dropped.
+func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string][]string, stdlib []string) {
+ if len(symbols) == 0 {
+ return nil, nil
+ }
+
+ var modulePaths []string
+ if main != nil && main.Path != "" {
+ modulePaths = append(modulePaths, main.Path)
+ }
+ for _, dep := range deps {
+ if dep != nil && dep.Path != "" {
+ modulePaths = append(modulePaths, dep.Path)
+ }
+ }
+
+ results := make(map[string][]string)
+ for _, sym := range symbols {
+ pkgPath := sym.packagePath
+ if pkgPath == mainPackage && main != nil {
+ // the linker renames the main package's import path to "main"
+ pkgPath = main.Path
+ }
+
+ var best string
+ for _, modPath := range modulePaths {
+ if len(modPath) > len(best) && (pkgPath == modPath || strings.HasPrefix(pkgPath, modPath+"/")) {
+ best = modPath
+ }
+ }
+ if best == "" {
+ if pkgPath != mainPackage && isStandardImportPath(pkgPath) {
+ stdlib = append(stdlib, sym.name)
+ }
+ continue
+ }
+ results[best] = append(results[best], sym.name)
+ }
+
+ for modPath, names := range results {
+ slices.Sort(names)
+ results[modPath] = slices.Compact(names)
+ }
+ if len(stdlib) > 0 {
+ slices.Sort(stdlib)
+ stdlib = slices.Compact(stdlib)
+ }
+
+ return results, stdlib
+}
+
+// isStandardImportPath reports whether path is a Go standard-library import path. This mirrors the rule
+// the Go toolchain uses: a path is standard if the element before its first slash contains no dot (e.g.
+// "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like
+// "github.com/foo/bar" whose leading element is a domain name.
+func isStandardImportPath(path string) bool {
+ first, _, _ := strings.Cut(path, "/")
+ return first != "" && !strings.Contains(first, ".")
+}
diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go
new file mode 100644
index 000000000..703c3210a
--- /dev/null
+++ b/syft/pkg/cataloger/golang/symbols_test.go
@@ -0,0 +1,201 @@
+package golang
+
+import (
+ "debug/gosym"
+ "os"
+ "runtime"
+ "runtime/debug"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func Test_moduleSymbols(t *testing.T) {
+ mainModule := &debug.Module{Path: "github.com/someorg/somecli"}
+ deps := []*debug.Module{
+ {Path: "github.com/foo/bar"},
+ {Path: "github.com/foo/bar/v2"},
+ nil,
+ }
+
+ tests := []struct {
+ name string
+ symbols []binarySymbol
+ expected map[string][]string
+ expectedStdlib []string
+ }{
+ {
+ name: "no symbols",
+ symbols: nil,
+ expected: nil,
+ },
+ {
+ name: "attribute symbols by longest module path prefix",
+ symbols: []binarySymbol{
+ {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
+ {packagePath: "github.com/foo/bar/internal/util", name: "github.com/foo/bar/internal/util.(*Helper).Do"},
+ {packagePath: "github.com/foo/bar/v2", name: "github.com/foo/bar/v2.Parse"},
+ },
+ expected: map[string][]string{
+ "github.com/foo/bar": {
+ "github.com/foo/bar.Parse",
+ "github.com/foo/bar/internal/util.(*Helper).Do",
+ },
+ "github.com/foo/bar/v2": {
+ "github.com/foo/bar/v2.Parse",
+ },
+ },
+ },
+ {
+ name: "main package symbols are attributed to the main module",
+ symbols: []binarySymbol{
+ {packagePath: "main", name: "main.main"},
+ {packagePath: "github.com/someorg/somecli/cmd", name: "github.com/someorg/somecli/cmd.Execute"},
+ },
+ expected: map[string][]string{
+ "github.com/someorg/somecli": {
+ "github.com/someorg/somecli/cmd.Execute",
+ "main.main",
+ },
+ },
+ },
+ {
+ name: "stdlib and runtime symbols are collected separately",
+ symbols: []binarySymbol{
+ {packagePath: "runtime", name: "runtime.main"},
+ {packagePath: "net/http", name: "net/http.(*Client).Do"},
+ {packagePath: "internal/abi", name: "internal/abi.(*Type).Kind"},
+ },
+ expected: map[string][]string{},
+ expectedStdlib: []string{
+ "internal/abi.(*Type).Kind",
+ "net/http.(*Client).Do",
+ "runtime.main",
+ },
+ },
+ {
+ name: "duplicate symbols are deduplicated",
+ symbols: []binarySymbol{
+ {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
+ {packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
+ },
+ expected: map[string][]string{
+ "github.com/foo/bar": {
+ "github.com/foo/bar.Parse",
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ gotByModule, gotStdlib := moduleSymbols(tt.symbols, mainModule, deps)
+ assert.Equal(t, tt.expected, gotByModule)
+ assert.Equal(t, tt.expectedStdlib, gotStdlib)
+ })
+ }
+}
+
+func Test_getSymbols(t *testing.T) {
+ if runtime.GOOS == "windows" {
+ t.Skip("PE binaries are not supported for symbol extraction")
+ }
+
+ // the test executable is itself a go binary with a pclntab, which makes for a hermetic fixture
+ exe, err := os.Executable()
+ require.NoError(t, err)
+
+ f, err := os.Open(exe)
+ require.NoError(t, err)
+ defer f.Close()
+
+ symbols, err := getSymbols(f)
+ require.NoError(t, err)
+ require.NotEmpty(t, symbols)
+
+ var foundRuntime, foundTesting bool
+ for _, sym := range symbols {
+ switch {
+ case sym.packagePath == "runtime" && sym.name == "runtime.main":
+ foundRuntime = true
+ case sym.packagePath == "testing" && sym.name == "testing.tRunner":
+ foundTesting = true
+ }
+ }
+ assert.True(t, foundRuntime, "expected to find runtime.main symbol")
+ assert.True(t, foundTesting, "expected to find testing.tRunner symbol")
+
+ // the recovery loop relies on packagePathFromSymbolName, so confirm at least one recovered name is
+ // present that debug/gosym's table.Funcs does not surface directly (i.e. an inlined function).
+ require.True(t, hasInlinedOnlySymbol(t, exe, symbols), "expected to recover at least one inlined-only symbol")
+}
+
+// hasInlinedOnlySymbol reports whether syms contains a function name that is absent from the raw
+// debug/gosym function table for the same binary — i.e. a name that could only have come from the
+// funcname-table recovery path.
+func hasInlinedOnlySymbol(t *testing.T, exe string, syms []binarySymbol) bool {
+ t.Helper()
+
+ f, err := os.Open(exe)
+ require.NoError(t, err)
+ defer f.Close()
+
+ pclntab, textStart, err := readPclntab(f)
+ require.NoError(t, err)
+
+ table, err := gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart))
+ require.NoError(t, err)
+
+ gosymNames := make(map[string]struct{}, len(table.Funcs))
+ for _, fn := range table.Funcs {
+ gosymNames[fn.Name] = struct{}{}
+ }
+
+ for _, sym := range syms {
+ if _, ok := gosymNames[sym.name]; !ok {
+ return true
+ }
+ }
+ return false
+}
+
+func Test_packagePathFromSymbolName(t *testing.T) {
+ tests := []struct {
+ name string
+ expected string
+ }{
+ {"path/filepath.IsLocal", "path/filepath"},
+ // pointer-receiver method
+ {"golang.org/x/net/html.(*Tokenizer).Next", "golang.org/x/net/html"},
+ // versioned (major-version-suffixed) module path
+ {"github.com/foo/bar/v2.Parse", "github.com/foo/bar/v2"},
+ {"github.com/foo/bar/v2.(*Client).Do", "github.com/foo/bar/v2"},
+ {"github.com/foo/bar.Parse.func1", "github.com/foo/bar"},
+ {"main.main", "main"},
+ {"runtime.gcBgMarkWorker", "runtime"},
+ // generic instantiations: type arguments must not corrupt the package path
+ {"foo/bar.Do[net/url.Values]", "foo/bar"},
+ {"github.com/foo/bar.Map[go.shape.int,go.shape.string]", "github.com/foo/bar"},
+ {"main.Do[go.shape.int]", "main"},
+ // no package-qualifying dot
+ {"runtime", ""},
+ // compiler/linker-generated symbols belong to no package
+ {"type:.eq.[]string", ""},
+ {"type..hash.runtime._type", ""},
+ {"go:string.\"foo\"", ""},
+ // pre-go1.20 toolchains generated symbols with "." where newer ones use ":"
+ {"go.buildid", ""},
+ {"go.type.*runtime._type", ""},
+ {"go.itab.*os.File,io.Reader", ""},
+ {"go.string.\"foo\"", ""},
+ // module paths that begin with "go." are not compiler-generated
+ {"go.uber.org/zap.(*Logger).Info", "go.uber.org/zap"},
+ {"go.opentelemetry.io/otel.Tracer", "go.opentelemetry.io/otel"},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expected, packagePathFromSymbolName(test.name))
+ })
+ }
+}
diff --git a/syft/pkg/golang.go b/syft/pkg/golang.go
index 9656f83b1..1a119e8dc 100644
--- a/syft/pkg/golang.go
+++ b/syft/pkg/golang.go
@@ -22,6 +22,13 @@ type GolangBinaryBuildinfoEntry struct {
// GoExperiments lists experimental Go features enabled during compilation (e.g., "arenas", "cgocheck2").
GoExperiments []string `json:"goExperiments,omitempty" cyclonedx:"goExperiments"`
+
+ // Symbols are the fully qualified function symbols from this module that are compiled into the binary
+ // (e.g., "github.com/foo/bar.(*Type).Method"), extracted from the binary symbol table (pclntab).
+ // Populated only when the golang cataloger's capture-symbols scope covers this package: the "all" scope
+ // populates every module package plus the synthetic stdlib package, while the "stdlib" scope populates
+ // only the stdlib package.
+ Symbols []string `json:"symbols,omitempty"`
}
// GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum
diff --git a/syft/pkg/vcpkg.go b/syft/pkg/vcpkg.go
index 165f846f1..23577c545 100644
--- a/syft/pkg/vcpkg.go
+++ b/syft/pkg/vcpkg.go
@@ -8,19 +8,36 @@ const (
Builtin VcpkgRegistryKind = "builtin"
)
-// used for metadata. Includes summary of data found in manifest (vcpkg.json file) relevant to just that vcpkg
+// VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package.
type VcpkgManifest struct {
- Description []string `json:"description,omitempty"`
- Documentation string `json:"documentation,omitempty"`
- FullVersion string `json:"full-version"`
- Version string `json:"version"`
- PortVersion int `json:"port-version"`
- Maintainers []string `json:"maintainers,omitempty"`
- Name string `json:"name"`
- Supports string `json:"supports,omitempty"`
- // to show where it came from
+ // Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest).
+ Description []string `json:"description,omitempty"`
+
+ // Documentation is the URL to the package's documentation.
+ Documentation string `json:"documentation,omitempty"`
+
+ // FullVersion is the complete version string including the port-version suffix (e.g. "1.2.3#2").
+ FullVersion string `json:"full-version"`
+
+ // Version is the upstream package version without the port-version suffix (e.g. "1.2.3").
+ Version string `json:"version"`
+
+ // PortVersion is the vcpkg-specific packaging revision for a given upstream version.
+ PortVersion int `json:"port-version"`
+
+ // Maintainers are the people responsible for maintaining the vcpkg port.
+ Maintainers []string `json:"maintainers,omitempty"`
+
+ // Name is the package name as declared in the manifest.
+ Name string `json:"name"`
+
+ // Supports is the platform expression describing which triplets the package can be built for (e.g. "!windows").
+ Supports string `json:"supports,omitempty"`
+
+ // Registry indicates where the package definition came from.
Registry *VcpkgRegistryEntry `json:"registry,omitempty"`
- // found by looking at build folder to find target. ex. "x64-linux"
+
+ // Triplet is the build target discovered from the build folder (e.g. "x64-linux").
Triplet string `json:"triplet,omitempty"`
}
From bd0917fe93e3e12bf02a22dd79b1a03f83d299f7 Mon Sep 17 00:00:00 2001
From: Synvoya
Date: Tue, 14 Jul 2026 00:38:32 +1000
Subject: [PATCH 059/168] fix(javascript): strip peer-dep suffix from deno.lock
npm keys (#5055)
deno.lock npm keys append resolved peer dependencies after the version,
separated by "_" (e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped
"@scope/name@1.2.3_@scope+peer@4.5.6"). parseDenoNpmNameVersion split on the
last "@", capturing a peer's "@" and producing a garbage package name plus
the peer's version (and a corrupt PURL that breaks vulnerability matching).
Strip the peer suffix (the first "_" after the version "@") before splitting
name@version; a semver version never contains "_".
Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
---
.../cataloger/javascript/parse_deno_lock.go | 30 ++++++++++++++-----
.../javascript/parse_deno_lock_test.go | 22 ++++++++++++++
.../javascript/test-fixtures/deno/deno.lock | 6 ++++
3 files changed, 51 insertions(+), 7 deletions(-)
diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock.go b/syft/pkg/cataloger/javascript/parse_deno_lock.go
index c11bc98f7..4ad2b8cec 100644
--- a/syft/pkg/cataloger/javascript/parse_deno_lock.go
+++ b/syft/pkg/cataloger/javascript/parse_deno_lock.go
@@ -96,19 +96,35 @@ func parseDenoJsrNameVersion(nameVersion string) (name, version string) {
}
func parseDenoNpmNameVersion(nameVersion string) (name, version string) {
+ // deno.lock npm keys append resolved peer dependencies after the version,
+ // separated by "_", e.g. "typedoc@0.28.19_typescript@6.0.3" or the scoped
+ // "@scope/name@1.2.3_@scope+peer@4.5.6". The peer suffix must be stripped
+ // before splitting name@version, otherwise LastIndex("@") splits on a peer's
+ // "@" and yields a garbage name and the peer's version.
+ //
+ // A semver version never contains "_", and it begins immediately after the
+ // package name's version "@", so the first "_" following that "@" starts the
+ // peer list.
+ var versionAt int
if strings.HasPrefix(nameVersion, "@") {
- rest := nameVersion[1:]
- idx := strings.LastIndex(rest, "@")
+ // scoped "@scope/name@version": the version "@" is the second "@"
+ idx := strings.Index(nameVersion[1:], "@")
if idx <= 0 {
return "", ""
}
- return nameVersion[:idx+1], rest[idx+1:]
+ versionAt = idx + 1
+ } else {
+ versionAt = strings.Index(nameVersion, "@")
+ if versionAt <= 0 {
+ return "", ""
+ }
}
- idx := strings.LastIndex(nameVersion, "@")
- if idx <= 0 {
- return "", ""
+
+ base := nameVersion
+ if underscore := strings.Index(nameVersion[versionAt:], "_"); underscore >= 0 {
+ base = nameVersion[:versionAt+underscore]
}
- return nameVersion[:idx], nameVersion[idx+1:]
+ return base[:versionAt], base[versionAt+1:]
}
func newDenoJsrPackage(location file.Location, name, version string, meta denoJsrPackage) pkg.Package {
diff --git a/syft/pkg/cataloger/javascript/parse_deno_lock_test.go b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go
index 2e70bc32a..83fe9abb7 100644
--- a/syft/pkg/cataloger/javascript/parse_deno_lock_test.go
+++ b/syft/pkg/cataloger/javascript/parse_deno_lock_test.go
@@ -55,6 +55,28 @@ func TestParseDenoLock(t *testing.T) {
Integrity: "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
},
},
+ {
+ // npm key carries a resolved peer dependency suffix ("_typescript@6.0.3")
+ Name: "typedoc",
+ Version: "0.28.19",
+ PURL: "pkg:npm/typedoc@0.28.19",
+ Language: pkg.JavaScript,
+ Type: pkg.NpmPkg,
+ Metadata: pkg.NpmPackageLockEntry{
+ Integrity: "sha512-TYPEDOCtestfixtureintegrityAAAA==",
+ },
+ },
+ {
+ // scoped npm key with a scope-encoded ("+") peer dependency suffix
+ Name: "@algolia/autocomplete-preset-algolia",
+ Version: "1.17.9",
+ PURL: "pkg:npm/%40algolia/autocomplete-preset-algolia@1.17.9",
+ Language: pkg.JavaScript,
+ Type: pkg.NpmPkg,
+ Metadata: pkg.NpmPackageLockEntry{
+ Integrity: "sha512-ALGOLIAtestfixtureintegrityBBBB==",
+ },
+ },
}
for i := range expectedPkgs {
diff --git a/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock
index c6ecf9efd..365a18a8c 100644
--- a/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock
+++ b/syft/pkg/cataloger/javascript/test-fixtures/deno/deno.lock
@@ -19,6 +19,12 @@
"npm": {
"chalk@5.3.0": {
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="
+ },
+ "typedoc@0.28.19_typescript@6.0.3": {
+ "integrity": "sha512-TYPEDOCtestfixtureintegrityAAAA=="
+ },
+ "@algolia/autocomplete-preset-algolia@1.17.9_@algolia+client-search@5.50.0": {
+ "integrity": "sha512-ALGOLIAtestfixtureintegrityBBBB=="
}
},
"remote": {
From 7745e450fad93ba0986d5e1b06bea5ff7c2d26e7 Mon Sep 17 00:00:00 2001
From: Arpit Jain <3242828+arpitjain099@users.noreply.github.com>
Date: Mon, 13 Jul 2026 23:45:12 +0900
Subject: [PATCH 060/168] fix panic parsing a rockspec comment that ends at EOF
(#5053)
The hand-written rockspec parser reads a byte past the end of the buffer
in two spots when a comment runs right up to the end of the file.
In parseRockspecBlock, when a block starts with a leading comment that
consumes the rest of the file, the SkipWhitespace afterward leaves the
index at len(data) and the following `c = data[*i]` reads out of range.
In parseComment, `data[*i]` is read after the index is advanced to check
for a CR/LF pair, so a bare carriage return as the last byte reads past
the end.
Both cases show up with a rockspec whose final line is a comment ending
in a lone \r with no trailing newline. That is malformed but harmless
input, and the panic aborts the whole Lua cataloger, so every valid Lua
package in the same scan gets dropped. Guard both reads with a length
check and return cleanly at EOF. Added table cases covering a
comment-only file and a trailing comment, both ending in a bare CR.
Signed-off-by: arpitjain099
---
syft/pkg/cataloger/lua/rockspec_parser.go | 5 ++++-
syft/pkg/cataloger/lua/rockspec_parser_test.go | 8 ++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/syft/pkg/cataloger/lua/rockspec_parser.go b/syft/pkg/cataloger/lua/rockspec_parser.go
index 56c9a87f2..be8630293 100644
--- a/syft/pkg/cataloger/lua/rockspec_parser.go
+++ b/syft/pkg/cataloger/lua/rockspec_parser.go
@@ -76,6 +76,9 @@ func parseRockspecBlock(data []byte, i *int, locals map[string]string) ([]rocksp
if c == '-' {
parseComment(data, i)
parsing.SkipWhitespace(data, i)
+ if *i >= len(data) {
+ return out, nil
+ }
c = data[*i]
}
@@ -424,7 +427,7 @@ func parseComment(data []byte, i *int) {
// Rest of a line is a comment. Deals with CR, LF and CR/LF
if c == '\n' {
break
- } else if c == '\r' && data[*i] == '\n' {
+ } else if c == '\r' && *i < len(data) && data[*i] == '\n' {
*i++
break
}
diff --git a/syft/pkg/cataloger/lua/rockspec_parser_test.go b/syft/pkg/cataloger/lua/rockspec_parser_test.go
index 0914d2695..aca545e00 100644
--- a/syft/pkg/cataloger/lua/rockspec_parser_test.go
+++ b/syft/pkg/cataloger/lua/rockspec_parser_test.go
@@ -185,6 +185,14 @@ end
test = "blah"
`,
},
+ {
+ name: "comment only ending in bare carriage return at EOF",
+ content: "--\r",
+ },
+ {
+ name: "trailing comment ending in bare carriage return at EOF",
+ content: "foo = \"bar\"\n-- x\r",
+ },
{
name: "invalid complex syntax",
wantErr: require.Error,
From 3cc3ecb003079b5a909ac1ff0e4c30d256997059 Mon Sep 17 00:00:00 2001
From: Synvoya
Date: Tue, 14 Jul 2026 00:57:32 +1000
Subject: [PATCH 061/168] Fix panic on cabal.project.freeze 'installed'
boot-lib constraints (#5040)
GHC boot-lib constraints (any.base installed) have no ' ==', so fields[1] panicked with index out of range and the recover dropped all Haskell packages. Skip constraint lines without a version.
Signed-off-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
Co-authored-by: Synvoya <16019863+Synvoya@users.noreply.github.com>
---
syft/pkg/cataloger/haskell/parse_cabal_freeze.go | 4 ++++
syft/pkg/cataloger/haskell/testdata/cabal.project.freeze | 2 ++
2 files changed, 6 insertions(+)
diff --git a/syft/pkg/cataloger/haskell/parse_cabal_freeze.go b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go
index 614524cae..b7f11093e 100644
--- a/syft/pkg/cataloger/haskell/parse_cabal_freeze.go
+++ b/syft/pkg/cataloger/haskell/parse_cabal_freeze.go
@@ -45,6 +45,10 @@ func parseCabalFreeze(_ context.Context, _ file.Resolver, _ *generic.Environment
line = line[startPkgEncoding:endPkgEncoding]
fields := strings.Split(line, " ==")
+ // skip constraints without a version (e.g. GHC boot libs: "any.base installed")
+ if len(fields) < 2 {
+ continue
+ }
pkgName, pkgVersion := fields[0], fields[1]
pkgs = append(
diff --git a/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze b/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze
index f20ce8cb5..6843f5d47 100644
--- a/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze
+++ b/syft/pkg/cataloger/haskell/testdata/cabal.project.freeze
@@ -1,5 +1,7 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Cabal ==3.2.1.0,
+ any.array installed,
+ any.base installed,
any.Diff ==0.4.1,
any.HTTP ==4000.3.16,
any.HUnit ==1.6.2.0,
From da77ea388a01943e5108cb1eafbe4b5c21cd0155 Mon Sep 17 00:00:00 2001
From: Ali Asghar <98263017+alliasgher@users.noreply.github.com>
Date: Mon, 13 Jul 2026 20:15:01 +0500
Subject: [PATCH 062/168] docs: fix stale commands in binary cataloger README
(#5047)
Update the binary classifier cataloger developer README to match the
current workflow:
- Testing section used the non-existent flag -must-use-full-binaries
against .../testdata/...; the real flag is -must-use-original-binaries
and Test_Cataloger_PositiveCases lives in the binary package. Fix the
command and add a single-fixture variant.
- Point add-snippet references at the make target and note the (Y/n/q)
prompt requires y to write the snippet.
- Fix the test file name (classifier_cataloger_test.go) and document the
go generate ./internal/capabilities step for new classifiers.
Fixes #4510
Signed-off-by: alliasgher
---
syft/pkg/cataloger/binary/README.md | 20 ++++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/syft/pkg/cataloger/binary/README.md b/syft/pkg/cataloger/binary/README.md
index f6b31290a..68de0723e 100644
--- a/syft/pkg/cataloger/binary/README.md
+++ b/syft/pkg/cataloger/binary/README.md
@@ -5,8 +5,9 @@
> 1. head to the correct directory: `cd testdata`
> 2. add a new entry to `config.yaml` to track where to get the binary from (verify the entry with `make list`)
> 3. run `make download` to get the binary
-> 4. run `make add-snippet` and follow the prompts (use `/` to search)
-> 5. add a new test case to `Test_Cataloger_PositiveCases` in `../cataloger_test.go`
+> 4. run `make add-snippet` and follow the prompts (use `/` to search; answer `y` at the `(Y/n/q)` prompt to write the snippet)
+> 5. add a new test case to `Test_Cataloger_PositiveCases` in `../classifier_cataloger_test.go`
+> 6. if you added a new classifier, regenerate the capabilities catalog from the repo root and commit the result: `go generate ./internal/capabilities`
To test the binary cataloger we run it against a set of files ("test fixtures"). There are two kinds of test fixtures:
@@ -32,7 +33,7 @@ And use tooling to list and manage the fixtures:
- `make list` - list all fixtures
- `make download` - download binaries that are not covered by a snippet
- `make download-all` - download all binaries
-- `go run ./manager add-snippet` - add a new snippet based off of a configured binary
+- `make add-snippet` - add a new snippet based off of a configured binary
- `capture-snippet.sh` - add a new snippet based off of a binary on your local machine (not recommended, but allowed)
There is a `config.yaml` that tracks all binaries that the tests can use. This makes it possible to download it at any time from a hosted source. Today the only method allowed is to download a container image and extract files out.
@@ -74,10 +75,17 @@ from-images:
## Testing
The test cases have been setup to allow testing against full binaries or a mix of both (default).
-To force running only against full binaries run with:
+To force running only against the original full binaries (instead of snippets), run with the
+`-must-use-original-binaries` flag:
```bash
-go test -must-use-full-binaries ./syft/pkg/cataloger/binary/testdata/...
+go test -v ./syft/pkg/cataloger/binary -run Test_Cataloger_PositiveCases/ -must-use-original-binaries
+```
+
+To run against a single fixture, append its name to the `-run` filter:
+
+```bash
+go test -v ./syft/pkg/cataloger/binary -run Test_Cataloger_PositiveCases/ -must-use-original-binaries
```
## Adding a new test fixture
@@ -91,7 +99,7 @@ Even if you are adding a snippet, it is best practice to:
1. Follow the steps above to [add a full binary](#adding-a-full-binary)
-2. Run `go run ./manager add-snippet` and follow the prompts to create a new snippet
+2. Run `make add-snippet` and follow the prompts to create a new snippet (answer `y` at the `(Y/n/q)` prompt to write the snippet)
- you should see your binary in the list of binaries to choose from. If not, check step 2
- if the search results in no matching snippets, you can specify your own search with `--search-for `
- you should see a new snippet file created in `snippets/`
From 76ede661db6d246424e8144f12323c3239945a1e Mon Sep 17 00:00:00 2001
From: Keith Zantow
Date: Mon, 13 Jul 2026 15:10:12 -0400
Subject: [PATCH 063/168] fix: allow more PEP440-compliant characters in python
versions (#4964)
Signed-off-by: Keith Zantow
---
.../cataloger/python/parse_requirements.go | 9 +-
.../python/parse_requirements_test.go | 135 ++++++++++++++++++
2 files changed, 142 insertions(+), 2 deletions(-)
diff --git a/syft/pkg/cataloger/python/parse_requirements.go b/syft/pkg/cataloger/python/parse_requirements.go
index 08d284c79..a7117bdee 100644
--- a/syft/pkg/cataloger/python/parse_requirements.go
+++ b/syft/pkg/cataloger/python/parse_requirements.go
@@ -29,8 +29,13 @@ const (
// namePattern matches: requests[security]
namePattern = `(?P\w[\w\[\],\s-_\.]+)`
- // versionConstraintPattern matches: == 2.8.* (including local version identifiers, e.g. == 1.2.3+gcr.2)
- versionConstraintPattern = `(?P([^\S\r\n]*[~=>!<]+\s*[0-9a-zA-Z.*+]+[^\S\r\n]*,?)+)?(@[^\S\r\n]*(?P[^;]*))?`
+ // versionConstraintPattern matches: == 2.8.*
+ // the version token accepts every character PEP 440 versions can contain: digits and letters,
+ // '.' (release/segment separator), '!' (epoch), '+' (local version), '*' (wildcard), and the
+ // alternate segment separators '-' and '_'. See:
+ // https://packaging.python.org/en/latest/specifications/version-specifiers/#version-scheme
+ // 1!2.0 1.0rc1 1.0-alpha-1 1.0_beta_2 1.0.post1 1.0.dev1 1.2.3+gcr.2 2.8.*
+ versionConstraintPattern = `(?P([^\S\r\n]*[~=>!<]+\s*[0-9a-zA-Z.*+!_-]+[^\S\r\n]*,?)+)?(@[^\S\r\n]*(?P[^;]*))?`
// markersPattern matches: python_version < "2.7" and sys_platform == "linux"
markersPattern = `(;(?P.*))?`
diff --git a/syft/pkg/cataloger/python/parse_requirements_test.go b/syft/pkg/cataloger/python/parse_requirements_test.go
index 8c9d4e54c..735804e50 100644
--- a/syft/pkg/cataloger/python/parse_requirements_test.go
+++ b/syft/pkg/cataloger/python/parse_requirements_test.go
@@ -386,6 +386,111 @@ func Test_newRequirement(t *testing.T) {
Markers: "sys_platform == 'linux'",
},
},
+ {
+ name: "epoch",
+ raw: "pkg == 1!2.0.0",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1!2.0.0",
+ },
+ },
+ {
+ name: "pre-release",
+ raw: "pkg == 1.0rc1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0rc1",
+ },
+ },
+ {
+ name: "pre-release with dash separators",
+ raw: "pkg == 1.0-alpha-1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0-alpha-1",
+ },
+ },
+ {
+ name: "pre-release with underscore separators",
+ raw: "pkg == 1.0_beta_2",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0_beta_2",
+ },
+ },
+ {
+ name: "post-release",
+ raw: "pkg == 1.0.post1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0.post1",
+ },
+ },
+ {
+ name: "implicit post-release",
+ raw: "pkg == 1.0-1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0-1",
+ },
+ },
+ {
+ name: "dev-release",
+ raw: "pkg == 1.0.dev1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0.dev1",
+ },
+ },
+ {
+ name: "local version with dash separator",
+ raw: "pkg == 1.0+ubuntu-1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0+ubuntu-1",
+ },
+ },
+ {
+ name: "local version with underscore separator",
+ raw: "pkg == 1.0+ubuntu_1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1.0+ubuntu_1",
+ },
+ },
+ {
+ name: "all segments combined",
+ raw: "pkg == 1!1.0a1.post2.dev3+local.1",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1!1.0a1.post2.dev3+local.1",
+ },
+ },
+ {
+ name: "release wildcard",
+ raw: "pkg == 2.*",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 2.*",
+ },
+ },
+ {
+ name: "epoch with markers and hashes",
+ raw: "pkg == 1!2.0 ; python_version < '3.8' --hash=sha256:abc123",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: "== 1!2.0",
+ Markers: "python_version < '3.8' --hash=sha256:abc123",
+ },
+ },
+ {
+ name: "compound constraint with exclusion is unaffected",
+ raw: "pkg >= 1.0.0, != 1.1.0, < 2.0.0",
+ want: &unprocessedRequirement{
+ Name: "pkg",
+ VersionConstraint: ">= 1.0.0, != 1.1.0, < 2.0.0",
+ },
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -427,6 +532,36 @@ func Test_parseVersion(t *testing.T) {
version: " === 1.2.3+ubuntu1 ",
want: "1.2.3+ubuntu1",
},
+ {
+ name: "pre-release",
+ version: "== 1.0rc1",
+ want: "1.0rc1",
+ },
+ {
+ name: "pre-release with dash separators",
+ version: "== 1.0-alpha-1",
+ want: "1.0-alpha-1",
+ },
+ {
+ name: "post-release",
+ version: "== 1.0.post1",
+ want: "1.0.post1",
+ },
+ {
+ name: "dev-release",
+ version: "== 1.0.dev1",
+ want: "1.0.dev1",
+ },
+ {
+ name: "local version with dash separator",
+ version: "== 1.0+ubuntu-1",
+ want: "1.0+ubuntu-1",
+ },
+ {
+ name: "all segments combined",
+ version: "== 1.0a1.post2.dev3+local.1",
+ want: "1.0a1.post2.dev3+local.1",
+ },
{
name: "resolve lowest, simple constraint",
version: " >= 1.0.0 ",
From 987ae7f26a3b29b5615d6de1be248d593468c19d Mon Sep 17 00:00:00 2001
From: Mike Zolotarov <96788373+activeobd@users.noreply.github.com>
Date: Mon, 13 Jul 2026 21:25:31 +0200
Subject: [PATCH 064/168] fix: PE case-insensitive extensions (Win32/ISO 9660
compatibility) (#4996)
* fix: PE case-insensitive extensions (Win32/ISO 9660 compatibility)
Signed-off-by: Alex Goodman
* add tests
Signed-off-by: Alex Goodman
* expand cases to bpl files and surrounding catalogers
Signed-off-by: Alex Goodman
---------
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
syft/pkg/cataloger/binary/capabilities.yaml | 3 +++
syft/pkg/cataloger/binary/pe_package_cataloger.go | 2 +-
syft/pkg/cataloger/binary/pe_package_cataloger_test.go | 6 +++++-
.../binary/testdata/glob-paths/src/winarchive.BPL | 1 +
.../binary/testdata/glob-paths/src/winlibrary.DLL | 1 +
.../binary/testdata/glob-paths/src/winprogram.EXE | 1 +
syft/pkg/cataloger/dotnet/capabilities.yaml | 8 ++++++++
syft/pkg/cataloger/dotnet/cataloger_test.go | 8 ++++++++
syft/pkg/cataloger/dotnet/deps_binary_cataloger.go | 6 +++++-
.../dotnet/testdata/glob-paths/src/winsomething.BPL | 1 +
.../dotnet/testdata/glob-paths/src/winsomething.DLL | 1 +
.../dotnet/testdata/glob-paths/src/winsomething.EXE | 1 +
12 files changed, 36 insertions(+), 3 deletions(-)
create mode 100644 syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL
create mode 100644 syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL
create mode 100644 syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE
create mode 100644 syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL
create mode 100644 syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL
create mode 100644 syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE
diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml
index a40db6e16..18e265ae0 100644
--- a/syft/pkg/cataloger/binary/capabilities.yaml
+++ b/syft/pkg/cataloger/binary/capabilities.yaml
@@ -1055,6 +1055,9 @@ catalogers:
detector: # AUTO-GENERATED
method: glob # AUTO-GENERATED
criteria: # AUTO-GENERATED
+ - '**/*.DLL'
+ - '**/*.EXE'
+ - '**/*.BPL'
- '**/*.dll'
- '**/*.exe'
- '**/*.bpl'
diff --git a/syft/pkg/cataloger/binary/pe_package_cataloger.go b/syft/pkg/cataloger/binary/pe_package_cataloger.go
index 16292be76..74abb379d 100644
--- a/syft/pkg/cataloger/binary/pe_package_cataloger.go
+++ b/syft/pkg/cataloger/binary/pe_package_cataloger.go
@@ -16,7 +16,7 @@ import (
// BPL (Borland Package Library) files are PE-format binaries used by Delphi and C++Builder.
func NewPEPackageCataloger() pkg.Cataloger {
return generic.NewCataloger("pe-binary-package-cataloger").
- WithParserByGlobs(parsePE, "**/*.dll", "**/*.exe", "**/*.bpl")
+ WithParserByGlobs(parsePE, "**/*.DLL", "**/*.EXE", "**/*.BPL", "**/*.dll", "**/*.exe", "**/*.bpl")
}
func parsePE(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
diff --git a/syft/pkg/cataloger/binary/pe_package_cataloger_test.go b/syft/pkg/cataloger/binary/pe_package_cataloger_test.go
index 7ba3eae3f..18b5c7d40 100644
--- a/syft/pkg/cataloger/binary/pe_package_cataloger_test.go
+++ b/syft/pkg/cataloger/binary/pe_package_cataloger_test.go
@@ -70,12 +70,16 @@ func Test_PEPackageCataloger_Globs(t *testing.T) {
expected []string
}{
{
- name: "obtain PE binary files (dll, exe, bpl)",
+ name: "obtain PE binary files (dll, exe, bpl), including uppercase extensions",
fixture: "testdata/glob-paths",
expected: []string{
"src/library.dll",
"src/program.exe",
"src/archive.bpl",
+ // uppercase extensions appear on Windows/ISO 9660 filesystems and must also match
+ "src/winlibrary.DLL",
+ "src/winprogram.EXE",
+ "src/winarchive.BPL",
},
},
}
diff --git a/syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL
new file mode 100644
index 000000000..159643c0f
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winarchive.BPL
@@ -0,0 +1 @@
+bogus PE contents
diff --git a/syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL
new file mode 100644
index 000000000..159643c0f
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winlibrary.DLL
@@ -0,0 +1 @@
+bogus PE contents
diff --git a/syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE
new file mode 100644
index 000000000..159643c0f
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/glob-paths/src/winprogram.EXE
@@ -0,0 +1 @@
+bogus PE contents
diff --git a/syft/pkg/cataloger/dotnet/capabilities.yaml b/syft/pkg/cataloger/dotnet/capabilities.yaml
index aa6b9c499..b6cf64d64 100644
--- a/syft/pkg/cataloger/dotnet/capabilities.yaml
+++ b/syft/pkg/cataloger/dotnet/capabilities.yaml
@@ -39,6 +39,10 @@ catalogers:
- '**/*.deps.json'
- '**/*.dll'
- '**/*.exe'
+ - '**/*.bpl'
+ - '**/*.DLL'
+ - '**/*.EXE'
+ - '**/*.BPL'
metadata_types: # AUTO-GENERATED
- pkg.DotnetDepsEntry
- pkg.DotnetPortableExecutableEntry
@@ -173,6 +177,10 @@ catalogers:
criteria:
- '**/*.dll'
- '**/*.exe'
+ - '**/*.bpl'
+ - '**/*.DLL'
+ - '**/*.EXE'
+ - '**/*.BPL'
metadata_types: # AUTO-GENERATED
- pkg.DotnetPortableExecutableEntry
package_types: # AUTO-GENERATED
diff --git a/syft/pkg/cataloger/dotnet/cataloger_test.go b/syft/pkg/cataloger/dotnet/cataloger_test.go
index 04b2caf20..dae8d9db9 100644
--- a/syft/pkg/cataloger/dotnet/cataloger_test.go
+++ b/syft/pkg/cataloger/dotnet/cataloger_test.go
@@ -36,6 +36,10 @@ func TestCataloger_Globs(t *testing.T) {
"src/something.bpl",
"src/something.dll",
"src/something.exe",
+ // uppercase extensions appear on Windows/ISO 9660 filesystems and must also match
+ "src/winsomething.DLL",
+ "src/winsomething.EXE",
+ "src/winsomething.BPL",
},
},
{
@@ -47,6 +51,10 @@ func TestCataloger_Globs(t *testing.T) {
"src/something.deps.json",
"src/something.dll",
"src/something.exe",
+ // uppercase extensions appear on Windows/ISO 9660 filesystems and must also match
+ "src/winsomething.DLL",
+ "src/winsomething.EXE",
+ "src/winsomething.BPL",
},
// the binary cataloger probes executables by MIME type to find embedded bundles,
// but the glob fixtures aren't real binaries so those queries go unfulfilled
diff --git a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go
index ea96b1710..15f19de22 100644
--- a/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go
+++ b/syft/pkg/cataloger/dotnet/deps_binary_cataloger.go
@@ -27,6 +27,10 @@ const (
dllGlob = "**/*.dll"
exeGlob = "**/*.exe"
bplGlob = "**/*.bpl"
+ // uppercase variants match PE files on case-preserving Windows/ISO 9660 filesystems (doublestar globs are case-sensitive)
+ dllGlobUpper = "**/*.DLL"
+ exeGlobUpper = "**/*.EXE"
+ bplGlobUpper = "**/*.BPL"
)
var elfMagic = []byte{0x7f, 'E', 'L', 'F'}
@@ -483,7 +487,7 @@ func readDepsJSON(resolver file.Resolver, loc file.Location) (*depsJSON, error)
// findPEFiles locates and parses all PE files (dll/exe).
func findPEFiles(resolver file.Resolver) ([]logicalPE, error, error) {
- peLocs, err := resolver.FilesByGlob(dllGlob, exeGlob, bplGlob)
+ peLocs, err := resolver.FilesByGlob(dllGlob, exeGlob, bplGlob, dllGlobUpper, exeGlobUpper, bplGlobUpper)
if err != nil {
return nil, nil, fmt.Errorf("unable to find PE files: %w", err)
}
diff --git a/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL
new file mode 100644
index 000000000..159643c0f
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.BPL
@@ -0,0 +1 @@
+bogus PE contents
diff --git a/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL
new file mode 100644
index 000000000..159643c0f
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.DLL
@@ -0,0 +1 @@
+bogus PE contents
diff --git a/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE
new file mode 100644
index 000000000..159643c0f
--- /dev/null
+++ b/syft/pkg/cataloger/dotnet/testdata/glob-paths/src/winsomething.EXE
@@ -0,0 +1 @@
+bogus PE contents
From e854078fb02b7704f5630818662144d12096dac2 Mon Sep 17 00:00:00 2001
From: M_C_J
Date: Mon, 13 Jul 2026 20:29:30 +0100
Subject: [PATCH 065/168] fix: use printf instead of echo to fix ANSI color
output (#4978)
Signed-off-by: Jouini Mohamed Chaker
---
install.sh | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/install.sh b/install.sh
index f4921c255..bf9222d06 100755
--- a/install.sh
+++ b/install.sh
@@ -35,7 +35,8 @@ is_command() (
)
echo_stderr() (
- echo "$@" 1>&2
+ IFS=" "
+ printf "%b\n" "$*" 1>&2
)
_logp=2
@@ -75,12 +76,12 @@ init_colors
log_tag() (
case $1 in
- 0) echo "${RED}${BOLD}[error]${RESET}" ;;
- 1) echo "${RED}[warn]${RESET}" ;;
- 2) echo "[info]${RESET}" ;;
- 3) echo "${BLUE}[debug]${RESET}" ;;
- 4) echo "${PURPLE}[trace]${RESET}" ;;
- *) echo "[$1]" ;;
+ 0) printf "%b" "${RED}${BOLD}[error]${RESET}" ;;
+ 1) printf "%b" "${RED}[warn]${RESET}" ;;
+ 2) printf "%b" "[info]${RESET}" ;;
+ 3) printf "%b" "${BLUE}[debug]${RESET}" ;;
+ 4) printf "%b" "${PURPLE}[trace]${RESET}" ;;
+ *) printf "[%s]" "$1" ;;
esac
)
From fabd7609146848a8489e1d7bd5308246227cd638 Mon Sep 17 00:00:00 2001
From: Jonas Boos
Date: Mon, 13 Jul 2026 21:31:35 +0200
Subject: [PATCH 066/168] fix: correct Spring LDAP group ID in purl generation
(#4908)
* fix: correct Spring LDAP group ID in purl generation
The Spring LDAP artifacts use the group ID org.springframework.ldap
(not org.springframework). This caused syft to generate incorrect purls
like pkg:maven/spring-ldap-core/spring-ldap-core@3.1.4 instead of the
correct pkg:maven/org.springframework.ldap/spring-ldap-core@3.1.4.
Add all Spring LDAP artifacts to the DefaultArtifactIDToGroupID map:
- spring-ldap
- spring-ldap-core
- spring-ldap-core-tiger
- spring-ldap-ldif-core
- spring-ldap-odm
- spring-ldap-test
Closes #4030
Signed-off-by: Jonas Boos
Signed-off-by: Alex Goodman
* fix formatting
Signed-off-by: Alex Goodman
* add test
Signed-off-by: Alex Goodman
---------
Signed-off-by: Jonas Boos
Signed-off-by: Alex Goodman
Co-authored-by: Jonas Boos
Co-authored-by: Alex Goodman
---
.../internal/cpegenerate/java_groupid_map.go | 9 ++++++++-
syft/pkg/cataloger/java/package_url_test.go | 14 ++++++++++++++
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go
index f607e8316..52c8ad250 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go
+++ b/syft/pkg/cataloger/internal/cpegenerate/java_groupid_map.go
@@ -381,7 +381,14 @@ var DefaultArtifactIDToGroupID = map[string]string{
"spring-jms": "org.springframework",
"spring-jmx": "org.springframework",
"spring-jpa": "org.springframework",
- "spring-ldap": "org.springframework",
+ "spring-ldap": "org.springframework.ldap",
+ "spring-ldap-core": "org.springframework.ldap",
+ "spring-ldap-core-tiger": "org.springframework.ldap",
+ "spring-ldap-ldif-batch": "org.springframework.ldap",
+ "spring-ldap-ldif-core": "org.springframework.ldap",
+ "spring-ldap-odm": "org.springframework.ldap",
+ "spring-ldap-test": "org.springframework.ldap",
+ "spring-ldap-tiger": "org.springframework.ldap",
"spring-messaging": "org.springframework",
"spring-mock": "org.springframework",
"spring-ojb": "org.springframework",
diff --git a/syft/pkg/cataloger/java/package_url_test.go b/syft/pkg/cataloger/java/package_url_test.go
index 1661f3fdb..0e0532ffc 100644
--- a/syft/pkg/cataloger/java/package_url_test.go
+++ b/syft/pkg/cataloger/java/package_url_test.go
@@ -171,6 +171,20 @@ func Test_groupIDFromJavaMetadata(t *testing.T) {
metadata: pkg.JavaArchive{},
expect: "org.apache.ant",
},
+ {
+ // regression for github.com/anchore/syft/issues/4030: spring-ldap artifacts
+ // live under org.springframework.ldap, not org.springframework
+ name: "known package list spring-ldap-core",
+ pkgName: "spring-ldap-core",
+ metadata: pkg.JavaArchive{},
+ expect: "org.springframework.ldap",
+ },
+ {
+ name: "known package list spring-ldap",
+ pkgName: "spring-ldap",
+ metadata: pkg.JavaArchive{},
+ expect: "org.springframework.ldap",
+ },
{
name: "java manifest",
metadata: pkg.JavaArchive{
From 4312699dd6f3d0d97e69a3a3fa6a0f2465239974 Mon Sep 17 00:00:00 2001
From: ChrisJr404
Date: Mon, 13 Jul 2026 15:33:51 -0400
Subject: [PATCH 067/168] cyclonedx: import firmware-typed components as
packages (#4855)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The CycloneDX decoder was filtering on a hard-coded set of component
types (application/framework/library/machine-learning-model). Anything
else — including "firmware" — fell through silently, so a BOM
describing u-boot or other firmware ended up with no packages and
grype reported no matches (anchore/grype#2537).
@kzantow confirmed in the issue thread that firmware should be
included and gave this section as the spot to update.
Add ComponentTypeFirmware to the case list and a regression test that
constructs a single-firmware-component BOM and asserts the decoded
SBOM contains the package.
Closes anchore/grype#2537
Signed-off-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com>
Co-authored-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com>
---
.../internal/cyclonedxutil/helpers/decoder.go | 2 +-
.../cyclonedxutil/helpers/decoder_test.go | 24 +++++++++++++++++++
2 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/syft/format/internal/cyclonedxutil/helpers/decoder.go b/syft/format/internal/cyclonedxutil/helpers/decoder.go
index 0f1783362..aff76f66a 100644
--- a/syft/format/internal/cyclonedxutil/helpers/decoder.go
+++ b/syft/format/internal/cyclonedxutil/helpers/decoder.go
@@ -62,7 +62,7 @@ func collectPackages(component *cyclonedx.Component, s *sbom.SBOM, idMap map[str
switch component.Type {
case cyclonedx.ComponentTypeOS:
case cyclonedx.ComponentTypeContainer:
- case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary, cyclonedx.ComponentTypeMachineLearningModel:
+ case cyclonedx.ComponentTypeApplication, cyclonedx.ComponentTypeFramework, cyclonedx.ComponentTypeLibrary, cyclonedx.ComponentTypeMachineLearningModel, cyclonedx.ComponentTypeFirmware:
p := decodeComponent(component)
idMap[component.BOMRef] = p
if component.BOMRef != "" {
diff --git a/syft/format/internal/cyclonedxutil/helpers/decoder_test.go b/syft/format/internal/cyclonedxutil/helpers/decoder_test.go
index b5c1f3c27..5092c3ff0 100644
--- a/syft/format/internal/cyclonedxutil/helpers/decoder_test.go
+++ b/syft/format/internal/cyclonedxutil/helpers/decoder_test.go
@@ -259,6 +259,30 @@ func Test_decode(t *testing.T) {
}
}
+func Test_decode_includesFirmwareComponents(t *testing.T) {
+ // CycloneDX BOMs that describe firmware (e.g. u-boot) use
+ // component type "firmware". Prior to issue #2537 the decoder skipped
+ // these and downstream tools (e.g. grype) reported zero matches.
+ bom := cyclonedx.BOM{
+ Components: &[]cyclonedx.Component{
+ {
+ BOMRef: "u-boot",
+ Type: cyclonedx.ComponentTypeFirmware,
+ Name: "u-boot",
+ Version: "2024.04",
+ PackageURL: "pkg:generic/u-boot@2024.04",
+ },
+ },
+ }
+ model, err := ToSyftModel(&bom)
+ require.NoError(t, err)
+
+ pkgs := model.Artifacts.Packages.Sorted()
+ require.Len(t, pkgs, 1, "firmware component should be imported as a package")
+ assert.Equal(t, "u-boot", pkgs[0].Name)
+ assert.Equal(t, "2024.04", pkgs[0].Version)
+}
+
func Test_relationshipDirection(t *testing.T) {
cyclonedx_bom := cyclonedx.BOM{Metadata: nil,
Components: &[]cyclonedx.Component{
From f6b5d3e736b837f71155620ed6495432cd5a020e Mon Sep 17 00:00:00 2001
From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
Date: Mon, 13 Jul 2026 16:40:16 -0400
Subject: [PATCH 068/168] perf: group golang symbols by package path to reduce
the sbom size (#5064)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
internal/constants.go | 2 +-
schema/json/schema-16.1.9.json | 11 ++-
schema/json/schema-latest.json | 11 ++-
syft/pkg/cataloger/golang/package.go | 2 +-
syft/pkg/cataloger/golang/parse_go_binary.go | 51 +++++++----
.../cataloger/golang/parse_go_binary_test.go | 69 ++++++++++++---
syft/pkg/cataloger/golang/stdlib_package.go | 2 +-
.../cataloger/golang/stdlib_package_test.go | 4 +-
syft/pkg/cataloger/golang/symbols.go | 67 ++++++++++-----
syft/pkg/cataloger/golang/symbols_test.go | 85 ++++++++++++++-----
syft/pkg/golang.go | 16 ++--
11 files changed, 235 insertions(+), 85 deletions(-)
diff --git a/internal/constants.go b/internal/constants.go
index b21c5adda..f1b50a324 100644
--- a/internal/constants.go
+++ b/internal/constants.go
@@ -15,5 +15,5 @@ const (
// 16.1.6 - add Dependencies to ElixirMixLockEntry metadata
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
- // 16.1.9 - add Symbols to GolangBinaryBuildinfoEntry metadata
+ // 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata
)
diff --git a/schema/json/schema-16.1.9.json b/schema/json/schema-16.1.9.json
index e1363bf94..75eea2f94 100644
--- a/schema/json/schema-16.1.9.json
+++ b/schema/json/schema-16.1.9.json
@@ -1655,11 +1655,14 @@
"description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")."
},
"symbols": {
- "items": {
- "type": "string"
+ "additionalProperties": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
- "type": "array",
- "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package."
+ "type": "object",
+ "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
}
},
"type": "object",
diff --git a/schema/json/schema-latest.json b/schema/json/schema-latest.json
index e1363bf94..75eea2f94 100644
--- a/schema/json/schema-latest.json
+++ b/schema/json/schema-latest.json
@@ -1655,11 +1655,14 @@
"description": "GoExperiments lists experimental Go features enabled during compilation (e.g., \"arenas\", \"cgocheck2\")."
},
"symbols": {
- "items": {
- "type": "string"
+ "additionalProperties": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
},
- "type": "array",
- "description": "Symbols are the fully qualified function symbols from this module that are compiled into the binary\n(e.g., \"github.com/foo/bar.(*Type).Method\"), extracted from the binary symbol table (pclntab).\nPopulated only when the golang cataloger's capture-symbols scope covers this package: the \"all\" scope\npopulates every module package plus the synthetic stdlib package, while the \"stdlib\" scope populates\nonly the stdlib package."
+ "type": "object",
+ "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
}
},
"type": "object",
diff --git a/syft/pkg/cataloger/golang/package.go b/syft/pkg/cataloger/golang/package.go
index 4e1e39a80..a08202ab3 100644
--- a/syft/pkg/cataloger/golang/package.go
+++ b/syft/pkg/cataloger/golang/package.go
@@ -45,7 +45,7 @@ func (c *goBinaryCataloger) newGoBinaryPackage(dep *debug.Module, m pkg.GolangBi
return p
}
-func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments, symbols []string) pkg.GolangBinaryBuildinfoEntry {
+func newBinaryMetadata(dep *debug.Module, mainModule, goVersion, architecture string, buildSettings pkg.KeyValues, cryptoSettings, experiments []string, symbols map[string][]string) pkg.GolangBinaryBuildinfoEntry {
if dep.Replace != nil {
dep = dep.Replace
}
diff --git a/syft/pkg/cataloger/golang/parse_go_binary.go b/syft/pkg/cataloger/golang/parse_go_binary.go
index 8553ec4be..a4d24c918 100644
--- a/syft/pkg/cataloger/golang/parse_go_binary.go
+++ b/syft/pkg/cataloger/golang/parse_go_binary.go
@@ -54,9 +54,10 @@ type goBinaryCataloger struct {
symbolScope cataloging.SymbolScope
// stdlibSymbols holds the standard-library function symbols discovered per binary (keyed by the
- // binary's location), populated during parsing and consumed by stdlibProcessor when it builds the
- // synthetic "stdlib" package. Guarded by stdlibSymbolsMu because parsers run concurrently.
- stdlibSymbols map[file.Coordinates][]string
+ // binary's location), grouped by import path, populated during parsing and consumed by stdlibProcessor
+ // when it builds the synthetic "stdlib" package. Guarded by stdlibSymbolsMu because parsers run
+ // concurrently.
+ stdlibSymbols map[file.Coordinates]map[string][]string
stdlibSymbolsMu sync.Mutex
}
@@ -65,29 +66,49 @@ func newGoBinaryCataloger(opts CatalogerConfig) *goBinaryCataloger {
licenseResolver: newGoLicenseResolver(binaryCatalogerName, opts),
mainModuleVersion: opts.MainModuleVersion,
symbolScope: opts.CaptureSymbols,
- stdlibSymbols: make(map[file.Coordinates][]string),
+ stdlibSymbols: make(map[file.Coordinates]map[string][]string),
}
}
-// recordStdlibSymbols merges the standard-library symbols discovered for a binary location so the
-// stdlib processor can attach them to the synthetic stdlib package.
-func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols []string) {
+// recordStdlibSymbols merges the standard-library symbols discovered for a binary location (grouped by
+// import path) so the stdlib processor can attach them to the synthetic stdlib package.
+func (c *goBinaryCataloger) recordStdlibSymbols(coord file.Coordinates, symbols map[string][]string) {
if len(symbols) == 0 {
return
}
c.stdlibSymbolsMu.Lock()
defer c.stdlibSymbolsMu.Unlock()
- merged := slices.Concat(c.stdlibSymbols[coord], symbols)
- slices.Sort(merged)
- c.stdlibSymbols[coord] = slices.Compact(merged)
+ existing := c.stdlibSymbols[coord]
+ if existing == nil {
+ existing = make(map[string][]string)
+ c.stdlibSymbols[coord] = existing
+ }
+ for path, names := range symbols {
+ merged := slices.Concat(existing[path], names)
+ slices.Sort(merged)
+ existing[path] = slices.Compact(merged)
+ }
}
-// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a copy
-// so callers cannot alias (and later mutate or race on) the map's internal slice.
-func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) []string {
+// stdlibSymbolsFor returns the standard-library symbols recorded for a binary location. It returns a deep
+// copy so callers cannot alias (and later mutate or race on) the map's internal state.
+func (c *goBinaryCataloger) stdlibSymbolsFor(coord file.Coordinates) map[string][]string {
c.stdlibSymbolsMu.Lock()
defer c.stdlibSymbolsMu.Unlock()
- return slices.Clone(c.stdlibSymbols[coord])
+ return cloneSymbolGroups(c.stdlibSymbols[coord])
+}
+
+// cloneSymbolGroups returns a deep copy of a symbol group map (import path -> local symbol names), or nil
+// when the input is empty.
+func cloneSymbolGroups(groups map[string][]string) map[string][]string {
+ if len(groups) == 0 {
+ return nil
+ }
+ out := make(map[string][]string, len(groups))
+ for path, names := range groups {
+ out[path] = slices.Clone(names)
+ }
+ return out
}
// parseGoBinary catalogs packages found in the "buildinfo" section of a binary built by the go compiler.
@@ -221,7 +242,7 @@ func missingMainModule(mod *extendedBuildInfo) bool {
return mod.Main == moduleFromPartialPackageBuild
}
-func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser, symbols []string) pkg.Package {
+func (c *goBinaryCataloger) makeGoMainPackage(ctx context.Context, resolver file.Resolver, mod *extendedBuildInfo, arch string, location file.Location, reader io.ReadSeekCloser, symbols map[string][]string) pkg.Package {
gbs := getBuildSettings(mod.Settings)
lics := c.licenseResolver.getLicenses(ctx, resolver, mod.Main.Path, mod.Main.Version)
gover, experiments := getExperimentsFromVersion(mod.GoVersion)
diff --git a/syft/pkg/cataloger/golang/parse_go_binary_test.go b/syft/pkg/cataloger/golang/parse_go_binary_test.go
index 648068822..9881f91cc 100644
--- a/syft/pkg/cataloger/golang/parse_go_binary_test.go
+++ b/syft/pkg/cataloger/golang/parse_go_binary_test.go
@@ -1427,12 +1427,14 @@ func (alwaysErrorReader) Read(_ []byte) (int, error) {
func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
location := file.NewLocationFromCoordinates(file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"})
- // the symbols a binary would carry once scanFile has extracted them: one main-package symbol, one
- // dependency symbol, and one standard-library symbol. For the "none" scope scanFile never runs, so the
- // build info carries no symbols at all.
+ // the symbols a binary would carry once scanFile has extracted them: one main-package symbol, two
+ // dependency symbols from distinct packages within the same module (so the per-package keying of the
+ // emitted map is visible), and one standard-library symbol. For the "none" scope scanFile never runs,
+ // so the build info carries no symbols at all.
populatedSymbols := []binarySymbol{
{packagePath: "main", name: "main.main"},
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
+ {packagePath: "github.com/foo/bar/baz", name: "github.com/foo/bar/baz.Helper"},
{packagePath: "net/http", name: "net/http.(*Client).Do"},
}
@@ -1440,9 +1442,9 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
name string
scope cataloging.SymbolScope
symbols []binarySymbol
- wantMainSyms []string
- wantDepSyms []string
- wantStdlibSyms []string
+ wantMainSyms map[string][]string
+ wantDepSyms map[string][]string
+ wantStdlibSyms map[string][]string
}{
{
name: "none captures nothing",
@@ -1453,15 +1455,18 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
name: "stdlib captures only the stdlib package",
scope: cataloging.SymbolScopeStdlib,
symbols: populatedSymbols,
- wantStdlibSyms: []string{"net/http.(*Client).Do"},
+ wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
},
{
- name: "all captures module and stdlib packages",
- scope: cataloging.SymbolScopeAll,
- symbols: populatedSymbols,
- wantMainSyms: []string{"main.main"},
- wantDepSyms: []string{"github.com/foo/bar.Parse"},
- wantStdlibSyms: []string{"net/http.(*Client).Do"},
+ name: "all captures module and stdlib packages",
+ scope: cataloging.SymbolScopeAll,
+ symbols: populatedSymbols,
+ wantMainSyms: map[string][]string{"main": {"main"}},
+ wantDepSyms: map[string][]string{
+ "github.com/foo/bar": {"Parse"},
+ "github.com/foo/bar/baz": {"Helper"},
+ },
+ wantStdlibSyms: map[string][]string{"net/http": {"(*Client).Do"}},
},
}
@@ -1491,3 +1496,41 @@ func Test_buildGoPkgInfo_symbolScope(t *testing.T) {
})
}
}
+
+// Test_recordStdlibSymbols_merge covers the merge path where the same binary location records stdlib
+// symbols more than once. This happens in production for universal/fat Mach-O binaries: scanFile yields
+// one build info per architecture and each is recorded under the same location coordinates.
+func Test_recordStdlibSymbols_merge(t *testing.T) {
+ coord := file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"}
+ c := newGoBinaryCataloger(CatalogerConfig{})
+
+ c.recordStdlibSymbols(coord, map[string][]string{
+ "net/http": {"(*Client).Do", "Get"},
+ "net/url": {"Parse"},
+ })
+ // a second architecture: overlapping names (must dedup), a new name for an existing package, and a new package
+ c.recordStdlibSymbols(coord, map[string][]string{
+ "net/http": {"Get", "Post"},
+ "os": {"Open"},
+ })
+
+ assert.Equal(t, map[string][]string{
+ "net/http": {"(*Client).Do", "Get", "Post"},
+ "net/url": {"Parse"},
+ "os": {"Open"},
+ }, c.stdlibSymbolsFor(coord))
+}
+
+// Test_stdlibSymbolsFor_isolation asserts the returned map is a deep copy: mutating it (or its slices)
+// must not corrupt the cataloger's guarded internal state.
+func Test_stdlibSymbolsFor_isolation(t *testing.T) {
+ coord := file.Coordinates{RealPath: "/a-path", FileSystemID: "layer-id"}
+ c := newGoBinaryCataloger(CatalogerConfig{})
+ c.recordStdlibSymbols(coord, map[string][]string{"net/http": {"Get"}})
+
+ got := c.stdlibSymbolsFor(coord)
+ got["net/http"] = append(got["net/http"], "MUTATED")
+ got["net/url"] = []string{"Parse"}
+
+ assert.Equal(t, map[string][]string{"net/http": {"Get"}}, c.stdlibSymbolsFor(coord), "internal state must be unaffected by caller mutation")
+}
diff --git a/syft/pkg/cataloger/golang/stdlib_package.go b/syft/pkg/cataloger/golang/stdlib_package.go
index 1743372e5..d310898f1 100644
--- a/syft/pkg/cataloger/golang/stdlib_package.go
+++ b/syft/pkg/cataloger/golang/stdlib_package.go
@@ -50,7 +50,7 @@ func (c *goBinaryCataloger) stdlibPackageAndRelationships(ctx context.Context, p
return goCompilerPkgs, relationships
}
-func newGoStdLib(ctx context.Context, version string, location file.LocationSet, symbols []string) *pkg.Package {
+func newGoStdLib(ctx context.Context, version string, location file.LocationSet, symbols map[string][]string) *pkg.Package {
stdlibCpe, err := generateStdlibCpe(version)
if err != nil {
return nil
diff --git a/syft/pkg/cataloger/golang/stdlib_package_test.go b/syft/pkg/cataloger/golang/stdlib_package_test.go
index 3c4e8ff75..b0f49dae4 100644
--- a/syft/pkg/cataloger/golang/stdlib_package_test.go
+++ b/syft/pkg/cataloger/golang/stdlib_package_test.go
@@ -88,7 +88,7 @@ func Test_stdlibPackageAndRelationships(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)}
+ c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates]map[string][]string)}
gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, tt.pkgs)
assert.Len(t, gotPkgs, tt.wantPkgs)
assert.Len(t, gotRels, tt.wantRels)
@@ -138,7 +138,7 @@ func Test_stdlibPackageAndRelationships_values(t *testing.T) {
Type: artifact.DependencyOfRelationship,
}
- c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates][]string)}
+ c := &goBinaryCataloger{stdlibSymbols: make(map[file.Coordinates]map[string][]string)}
gotPkgs, gotRels := c.stdlibPackageAndRelationships(ctx, []pkg.Package{p})
require.Len(t, gotPkgs, 1)
diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go
index 3d553a739..1d8067dff 100644
--- a/syft/pkg/cataloger/golang/symbols.go
+++ b/syft/pkg/cataloger/golang/symbols.go
@@ -267,12 +267,15 @@ func readPclntab(r io.ReaderAt) (pclntab []byte, textStart uint64, err error) {
}
// moduleSymbols attributes each extracted symbol to the module that owns it (by longest module path prefix
-// of the symbol's package path) and returns a sorted, deduplicated list of symbol names per module path.
-// Symbols from the "main" package are attributed to the main module. Standard-library symbols (which belong
-// to no module) are collected separately and returned as the second value so they can be attached to the
+// of the symbol's package path) and returns, per module path, the symbols grouped by the import path of the
+// owning package. Each inner value is a sorted, deduplicated list of symbol names local to that package
+// (the import path prefix stripped, e.g. "github.com/foo/bar.(*T).M" under key "github.com/foo/bar" becomes
+// "(*T).M"). Symbols from the "main" package are attributed to the main module and keyed by the "main"
+// import path the linker assigns. Standard-library symbols (which belong to no module) are collected
+// separately and returned as the second value, grouped by import path, so they can be attached to the
// synthetic "stdlib" package. Compiler/runtime-internal symbols that are neither module-owned nor a
// recognizable stdlib import path are dropped.
-func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string][]string, stdlib []string) {
+func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Module) (byModule map[string]map[string][]string, stdlib map[string][]string) {
if len(symbols) == 0 {
return nil, nil
}
@@ -287,41 +290,67 @@ func moduleSymbols(symbols []binarySymbol, main *debug.Module, deps []*debug.Mod
}
}
- results := make(map[string][]string)
+ results := make(map[string]map[string][]string)
+ stdlib = make(map[string][]string)
for _, sym := range symbols {
- pkgPath := sym.packagePath
- if pkgPath == mainPackage && main != nil {
- // the linker renames the main package's import path to "main"
- pkgPath = main.Path
+ importPath := sym.packagePath
+
+ // the linker renames the main package's import path to "main"; attribute it to the main module,
+ // but keep "main" as the group key since the original import path is not recoverable.
+ attrPath := importPath
+ if importPath == mainPackage && main != nil {
+ attrPath = main.Path
}
var best string
for _, modPath := range modulePaths {
- if len(modPath) > len(best) && (pkgPath == modPath || strings.HasPrefix(pkgPath, modPath+"/")) {
+ if len(modPath) > len(best) && (attrPath == modPath || strings.HasPrefix(attrPath, modPath+"/")) {
best = modPath
}
}
+
+ local := localSymbolName(sym.name, importPath)
if best == "" {
- if pkgPath != mainPackage && isStandardImportPath(pkgPath) {
- stdlib = append(stdlib, sym.name)
+ if importPath != mainPackage && isStandardImportPath(importPath) {
+ stdlib[importPath] = append(stdlib[importPath], local)
}
continue
}
- results[best] = append(results[best], sym.name)
+ if results[best] == nil {
+ results[best] = make(map[string][]string)
+ }
+ results[best][importPath] = append(results[best][importPath], local)
}
- for modPath, names := range results {
- slices.Sort(names)
- results[modPath] = slices.Compact(names)
+ for _, byImport := range results {
+ sortCompactGroups(byImport)
}
- if len(stdlib) > 0 {
- slices.Sort(stdlib)
- stdlib = slices.Compact(stdlib)
+ sortCompactGroups(stdlib)
+ if len(stdlib) == 0 {
+ stdlib = nil
}
return results, stdlib
}
+// localSymbolName strips the owning package's import path prefix from a fully qualified symbol name, e.g.
+// "github.com/foo/bar.(*T).M" with import path "github.com/foo/bar" becomes "(*T).M". The name is returned
+// unchanged when it does not carry the expected prefix.
+func localSymbolName(name, importPath string) string {
+ if importPath != "" && strings.HasPrefix(name, importPath+".") {
+ return name[len(importPath)+1:]
+ }
+ return name
+}
+
+// sortCompactGroups sorts and deduplicates each symbol list in a group keyed by import path, in place.
+func sortCompactGroups(groups map[string][]string) {
+ for path, names := range groups {
+ slices.Sort(names)
+ groups[path] = slices.Compact(names)
+ }
+}
+
// isStandardImportPath reports whether path is a Go standard-library import path. This mirrors the rule
// the Go toolchain uses: a path is standard if the element before its first slash contains no dot (e.g.
// "net/http", "runtime", "internal/abi"), which distinguishes it from module paths like
diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go
index 703c3210a..72ca613da 100644
--- a/syft/pkg/cataloger/golang/symbols_test.go
+++ b/syft/pkg/cataloger/golang/symbols_test.go
@@ -22,8 +22,8 @@ func Test_moduleSymbols(t *testing.T) {
tests := []struct {
name string
symbols []binarySymbol
- expected map[string][]string
- expectedStdlib []string
+ expected map[string]map[string][]string
+ expectedStdlib map[string][]string
}{
{
name: "no symbols",
@@ -31,47 +31,47 @@ func Test_moduleSymbols(t *testing.T) {
expected: nil,
},
{
- name: "attribute symbols by longest module path prefix",
+ name: "attribute symbols by longest module path prefix, grouped by import path",
symbols: []binarySymbol{
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
{packagePath: "github.com/foo/bar/internal/util", name: "github.com/foo/bar/internal/util.(*Helper).Do"},
{packagePath: "github.com/foo/bar/v2", name: "github.com/foo/bar/v2.Parse"},
},
- expected: map[string][]string{
+ expected: map[string]map[string][]string{
"github.com/foo/bar": {
- "github.com/foo/bar.Parse",
- "github.com/foo/bar/internal/util.(*Helper).Do",
+ "github.com/foo/bar": {"Parse"},
+ "github.com/foo/bar/internal/util": {"(*Helper).Do"},
},
"github.com/foo/bar/v2": {
- "github.com/foo/bar/v2.Parse",
+ "github.com/foo/bar/v2": {"Parse"},
},
},
},
{
- name: "main package symbols are attributed to the main module",
+ name: "main package symbols are attributed to the main module and keyed by the main import path",
symbols: []binarySymbol{
{packagePath: "main", name: "main.main"},
{packagePath: "github.com/someorg/somecli/cmd", name: "github.com/someorg/somecli/cmd.Execute"},
},
- expected: map[string][]string{
+ expected: map[string]map[string][]string{
"github.com/someorg/somecli": {
- "github.com/someorg/somecli/cmd.Execute",
- "main.main",
+ "github.com/someorg/somecli/cmd": {"Execute"},
+ "main": {"main"},
},
},
},
{
- name: "stdlib and runtime symbols are collected separately",
+ name: "stdlib and runtime symbols are collected separately, grouped by import path",
symbols: []binarySymbol{
{packagePath: "runtime", name: "runtime.main"},
{packagePath: "net/http", name: "net/http.(*Client).Do"},
{packagePath: "internal/abi", name: "internal/abi.(*Type).Kind"},
},
- expected: map[string][]string{},
- expectedStdlib: []string{
- "internal/abi.(*Type).Kind",
- "net/http.(*Client).Do",
- "runtime.main",
+ expected: map[string]map[string][]string{},
+ expectedStdlib: map[string][]string{
+ "internal/abi": {"(*Type).Kind"},
+ "net/http": {"(*Client).Do"},
+ "runtime": {"main"},
},
},
{
@@ -80,9 +80,9 @@ func Test_moduleSymbols(t *testing.T) {
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
{packagePath: "github.com/foo/bar", name: "github.com/foo/bar.Parse"},
},
- expected: map[string][]string{
+ expected: map[string]map[string][]string{
"github.com/foo/bar": {
- "github.com/foo/bar.Parse",
+ "github.com/foo/bar": {"Parse"},
},
},
},
@@ -97,6 +97,53 @@ func Test_moduleSymbols(t *testing.T) {
}
}
+func Test_localSymbolName(t *testing.T) {
+ tests := []struct {
+ name string
+ symbol string
+ importPath string
+ expected string
+ }{
+ {
+ name: "function",
+ symbol: "github.com/foo/bar.Parse",
+ importPath: "github.com/foo/bar",
+ expected: "Parse",
+ },
+ {
+ name: "pointer-receiver method",
+ symbol: "github.com/foo/bar.(*T).M",
+ importPath: "github.com/foo/bar",
+ expected: "(*T).M",
+ },
+ {
+ // type-argument brackets sit to the right of the stripped prefix and must be preserved
+ name: "generic instantiation",
+ symbol: "github.com/foo/bar.Do[net/url.Values]",
+ importPath: "github.com/foo/bar",
+ expected: "Do[net/url.Values]",
+ },
+ {
+ // a sibling package must not be stripped by a shorter import path (the "." boundary guards this)
+ name: "prefix mismatch is returned unchanged",
+ symbol: "github.com/foo/bar/sub.Func",
+ importPath: "github.com/foo/bar",
+ expected: "github.com/foo/bar/sub.Func",
+ },
+ {
+ name: "empty import path is returned unchanged",
+ symbol: "runtime.main",
+ importPath: "",
+ expected: "runtime.main",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.expected, localSymbolName(tt.symbol, tt.importPath))
+ })
+ }
+}
+
func Test_getSymbols(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("PE binaries are not supported for symbol extraction")
diff --git a/syft/pkg/golang.go b/syft/pkg/golang.go
index 1a119e8dc..8b133a8de 100644
--- a/syft/pkg/golang.go
+++ b/syft/pkg/golang.go
@@ -23,12 +23,16 @@ type GolangBinaryBuildinfoEntry struct {
// GoExperiments lists experimental Go features enabled during compilation (e.g., "arenas", "cgocheck2").
GoExperiments []string `json:"goExperiments,omitempty" cyclonedx:"goExperiments"`
- // Symbols are the fully qualified function symbols from this module that are compiled into the binary
- // (e.g., "github.com/foo/bar.(*Type).Method"), extracted from the binary symbol table (pclntab).
- // Populated only when the golang cataloger's capture-symbols scope covers this package: the "all" scope
- // populates every module package plus the synthetic stdlib package, while the "stdlib" scope populates
- // only the stdlib package.
- Symbols []string `json:"symbols,omitempty"`
+ // Symbols are the function symbols from this module that are compiled into the binary, extracted from
+ // the binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each
+ // value is the sorted, deduplicated list of symbol names local to that package, i.e. with the import
+ // path prefix stripped (e.g. import path "github.com/foo/bar" -> "(*Type).Method"). The fully qualified
+ // name is the import path, a ".", and the local name. One exception: the binary's main package appears
+ // under the key "main" (the name the linker assigns), not its original source import path, which is not
+ // recoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers
+ // this package: the "all" scope populates every module package plus the synthetic stdlib package, while
+ // the "stdlib" scope populates only the stdlib package.
+ Symbols map[string][]string `json:"symbols,omitempty"`
}
// GolangModuleEntry represents all captured data for a Golang source scan with go.mod/go.sum
From 552ddbfadb488c6f0e73fa418671a855845f768e Mon Sep 17 00:00:00 2001
From: Monibee-Fudgekins <148405685+Monibee-Fudgekins@users.noreply.github.com>
Date: Mon, 13 Jul 2026 16:43:34 -0400
Subject: [PATCH 069/168] fix: map NuGet PURLs to dotnet package type (#4861)
* test: use unique NuGet PURL subtest name
Signed-off-by: Monibee Fudgekins
* fix: emit canonical NuGet PURL type
Signed-off-by: Monibee Fudgekins
* chore: regenerate dotnet capabilities for nuget PURL type
regenerated after emitting canonical nuget PURL type for dotnet packages.
Signed-off-by: Alex Goodman
---------
Signed-off-by: Monibee Fudgekins
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
syft/pkg/cataloger/dotnet/capabilities.yaml | 8 ++++----
syft/pkg/type.go | 2 +-
syft/pkg/type_test.go | 11 ++++++++++-
3 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/syft/pkg/cataloger/dotnet/capabilities.yaml b/syft/pkg/cataloger/dotnet/capabilities.yaml
index b6cf64d64..2d1107ec2 100644
--- a/syft/pkg/cataloger/dotnet/capabilities.yaml
+++ b/syft/pkg/cataloger/dotnet/capabilities.yaml
@@ -50,8 +50,8 @@ catalogers:
- dotnet
- npm
purl_types: # AUTO-GENERATED
- - dotnet
- npm
+ - nuget
json_schema_types: # AUTO-GENERATED
- DotnetDepsEntry
- DotnetPortableExecutableEntry
@@ -91,7 +91,7 @@ catalogers:
package_types: # AUTO-GENERATED
- dotnet
purl_types: # AUTO-GENERATED
- - dotnet
+ - nuget
json_schema_types: # AUTO-GENERATED
- DotnetDepsEntry
capabilities: # MANUAL - edit capabilities here
@@ -137,7 +137,7 @@ catalogers:
package_types: # AUTO-GENERATED
- dotnet
purl_types: # AUTO-GENERATED
- - dotnet
+ - nuget
json_schema_types: # AUTO-GENERATED
- DotnetPackagesLockEntry
capabilities: # MANUAL - preserved across regeneration
@@ -186,7 +186,7 @@ catalogers:
package_types: # AUTO-GENERATED
- dotnet
purl_types: # AUTO-GENERATED
- - dotnet
+ - nuget
json_schema_types: # AUTO-GENERATED
- DotnetPortableExecutableEntry
capabilities: # MANUAL - edit capabilities here
diff --git a/syft/pkg/type.go b/syft/pkg/type.go
index b4d42853f..c354c949c 100644
--- a/syft/pkg/type.go
+++ b/syft/pkg/type.go
@@ -128,7 +128,7 @@ func (t Type) PackageURLType() string {
case DebPkg:
return "deb"
case DotnetPkg:
- return "dotnet"
+ return packageurl.TypeNuget
case ErlangOTPPkg:
return packageurl.TypeOTP
case GemPkg:
diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go
index 514d06b13..4c4fd7add 100644
--- a/syft/pkg/type_test.go
+++ b/syft/pkg/type_test.go
@@ -3,6 +3,7 @@ package pkg
import (
"testing"
+ "github.com/anchore/packageurl-go"
"github.com/scylladb/go-set/strset"
"github.com/stretchr/testify/assert"
)
@@ -166,7 +167,11 @@ func TestTypeFromPURL(t *testing.T) {
expectedTypes.Remove(string(VcpkgPkg))
for _, test := range tests {
- t.Run(string(test.expected), func(t *testing.T) {
+ name := test.name
+ if name == "" {
+ name = string(test.expected)
+ }
+ t.Run(name, func(t *testing.T) {
actual := TypeFromPURL(test.purl)
if actual != "" {
@@ -179,3 +184,7 @@ func TestTypeFromPURL(t *testing.T) {
assert.ElementsMatch(t, expectedTypes.List(), pkgTypes.List(), "missing one or more package types to test against (maybe a package type was added?)")
}
+
+func TestPackageURLType(t *testing.T) {
+ assert.Equal(t, packageurl.TypeNuget, DotnetPkg.PackageURLType())
+}
From 2805655ab04603446a5422905d32cb953b560556 Mon Sep 17 00:00:00 2001
From: ChrisJr404
Date: Tue, 14 Jul 2026 08:49:53 -0400
Subject: [PATCH 070/168] feat(cmd): log total scan duration at info level
(#4858)
Closes #4587
Adds a single info-level log line at the end of every `syft scan` run that reports the total scan wall-clock time, e.g.:
[0003] INFO scan completed in 2.017s
Per maintainer guidance on the issue, the line is logged at INFO so it surfaces with `-v` (alongside the existing per-cataloger timing) and stays out of the default TUI / stdout. Useful for users who leave long scans running and want the overall time at a glance, without having to wrap the invocation in `time`.
Signed-off-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com>
Co-authored-by: Chris (ChrisJr404) <11917633+ChrisJr404@users.noreply.github.com>
---
cmd/syft/internal/commands/scan.go | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/cmd/syft/internal/commands/scan.go b/cmd/syft/internal/commands/scan.go
index d9147dace..7db17fb7d 100644
--- a/cmd/syft/internal/commands/scan.go
+++ b/cmd/syft/internal/commands/scan.go
@@ -7,6 +7,7 @@ import (
"os"
"reflect"
"strings"
+ "time"
"github.com/hashicorp/go-multierror"
"github.com/spf13/cobra"
@@ -171,6 +172,11 @@ func validateArgs(cmd *cobra.Command, args []string, err string) error {
}
func runScan(ctx context.Context, id clio.Identification, opts *scanOptions, userInput string) error {
+ scanStart := time.Now()
+ defer func() {
+ log.Infof("scan completed in %s", time.Since(scanStart).Round(time.Millisecond))
+ }()
+
writer, err := opts.SBOMWriter()
if err != nil {
return err
From ad828d659fdb5b29766662821ebd1392e258f118 Mon Sep 17 00:00:00 2001
From: Rez Moss
Date: Wed, 15 Jul 2026 15:29:03 -0400
Subject: [PATCH 071/168] fix: conan libxml2 CPE (#5016)
Signed-off-by: Rez Moss
---
.../internal/cpegenerate/candidate_by_package_type.go | 5 +++++
.../internal/cpegenerate/candidate_by_package_type_test.go | 5 +++++
2 files changed, 10 insertions(+)
diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go
index 1b363c656..32c2131a1 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go
+++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go
@@ -508,6 +508,11 @@ var defaultCandidateAdditions = buildCandidateLookup(
candidateKey{PkgName: "poco"},
candidateAddition{AdditionalVendors: []string{"pocoproject"}},
},
+ {
+ pkg.ConanPkg,
+ candidateKey{PkgName: "libxml2"},
+ candidateAddition{AdditionalVendors: []string{"xmlsoft"}},
+ },
})
var defaultCandidateRemovals = buildCandidateRemovalLookup(
diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go
index c610167f8..4d903b14c 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go
+++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go
@@ -232,3 +232,8 @@ func Test_findProductsToRemove(t *testing.T) {
})
}
}
+
+func Test_defaultCandidateAdditions_conan_libxml2(t *testing.T) {
+ vendors := findAdditionalVendors(defaultCandidateAdditions, pkg.ConanPkg, "libxml2", "")
+ assert.Contains(t, vendors, "xmlsoft")
+}
From ed499fd2d0e23529bb3213cfe883be8068342ce4 Mon Sep 17 00:00:00 2001
From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
Date: Wed, 15 Jul 2026 16:52:46 -0400
Subject: [PATCH 072/168] chore: migrate `.goreleaser` config to use docker_v2
(#4608)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
---
.github/workflows/release.yaml | 9 +-
.github/workflows/validations.yaml | 10 +-
.goreleaser.yaml | 370 ++++++-----------------------
Dockerfile | 22 +-
Dockerfile.debug | 26 +-
Dockerfile.debug-nonroot | 12 +
Dockerfile.nonroot | 22 +-
Taskfile.yaml | 10 +-
8 files changed, 121 insertions(+), 360 deletions(-)
create mode 100644 Dockerfile.debug-nonroot
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 9d3981475..39e4fe7da 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -63,8 +63,13 @@ jobs:
# spot disabled: reliability for build workflows (used for releases too)
# goreleaser uses parallelism of 12, so we need more CPUs
# s3-cache: faster actions cache
- # tmpfs: faster io-intensive workflows
- runs-on: runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=s3-cache+tmpfs
+ # volume: enlarge dist/ disk -- the default 40GB EBS root fills up during the multi-arch
+ # image + binary build and OOMs (runs out of disk); size it well above peak usage.
+ # provisioned throughput/iops above the gp3 free baseline (125mbs/3000iops) approximate
+ # the tmpfs IO speed on disk, for ~1c/run.
+ # note: tmpfs intentionally omitted -- a RAM-backed dist/ never touches the enlarged
+ # volume and competes with the build's memory peak, so keep dist/ on disk.
+ runs-on: runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=s3-cache/volume=120gb:gp3:500mbs:4000iops
permissions:
contents: write # required for creating the GitHub release and pushing the version tag
packages: write # required for publishing release artifacts to GitHub packages
diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml
index c520a05b7..746319f30 100644
--- a/.github/workflows/validations.yaml
+++ b/.github/workflows/validations.yaml
@@ -92,10 +92,16 @@ jobs:
# runs-on.com: compute instances for parallel builds
# spot disabled: reliability for build workflows (used for releases too)
# goreleaser uses parallelism of 12, so we need more CPUs
- # tmpfs: faster io-intensive workflows
+ # tmpfs intentionally omitted -- a RAM-backed dist/ competes with the goreleaser
+ # build's memory peak (parallel binaries + multi-arch image assembly) and OOMs the
+ # instance; keep dist/ on disk.
# note: s3-cache intentionally omitted -- PR runs are untrusted and must not write to the
# shared cache backend that the trusted release workflow reads from (cache poisoning).
- runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/extras=tmpfs"
+ # volume: enlarge dist/ disk -- the default 40GB EBS root fills up during the multi-arch
+ # image + binary build and OOMs (runs out of disk); size it well above peak usage.
+ # provisioned throughput/iops above the gp3 free baseline (125mbs/3000iops) approximate
+ # the tmpfs IO speed on disk, for ~1c/run.
+ runs-on: "runs-on=${{ github.run_id }}/cpu=16+32/ram=32+128/family=c5+c6+c7+c8/spot=false/volume=120gb:gp3:500mbs:4000iops"
permissions:
contents: read
steps:
diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 3edce4f6d..d5bb7de18 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -73,306 +73,88 @@ brews:
description: *description
license: "Apache License 2.0"
-dockers:
- # production images...
- - image_templates:
- - anchore/syft:{{.Tag}}-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-amd64
- goarch: amd64
+dockers_v2:
+ # production images (scratch base, root)
+ - id: production
dockerfile: Dockerfile
- use: buildx
- build_flag_templates:
- - "--platform=linux/amd64"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
+ ids: &docker-ids
+ - linux-build
+ images: &docker-images
+ - anchore/syft
+ - ghcr.io/anchore/syft
+ platforms: &docker-platforms
+ - linux/amd64
+ - linux/arm64
+ - linux/ppc64le
+ - linux/riscv64
+ - linux/s390x
+ labels: &docker-labels
+ "org.opencontainers.image.created": "{{.Date}}"
+ "org.opencontainers.image.title": "syft"
+ "org.opencontainers.image.description": "CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
+ "org.opencontainers.image.source": "{{.GitURL}}"
+ "org.opencontainers.image.revision": "{{.FullCommit}}"
+ "org.opencontainers.image.vendor": "Anchore, Inc."
+ "org.opencontainers.image.version": "{{.Version}}"
+ "org.opencontainers.image.licenses": "Apache-2.0"
+ "io.artifacthub.package.readme-url": "https://raw.githubusercontent.com/anchore/syft/main/README.md"
+ "io.artifacthub.package.logo-url": "https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
+ "io.artifacthub.package.license": "Apache-2.0"
+ tags:
+ - latest
+ - "{{.Tag}}"
+ # DEBIAN_VERSION 13 (trixie) is the first distroless release with a riscv64 base image
+ # and is a superset of debian 12 for the other targeted arches; docker_v2 builds all
+ # platforms in a single buildx invocation, so build_args cannot vary per platform.
+ build_args: &docker-build-args
+ DEBIAN_VERSION: "13"
+ # disable provenance attestations to keep the manifest free of unknown/unknown entries
+ flags: &docker-flags
+ - "--provenance=false"
+ # SBOMs are produced separately for the archives (see the sboms section)
+ sbom: &docker-sbom "false"
- - image_templates:
- - anchore/syft:{{.Tag}}-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- goarch: arm64
- dockerfile: Dockerfile
- use: buildx
- build_flag_templates:
- - "--platform=linux/arm64/v8"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- goarch: ppc64le
- dockerfile: Dockerfile
- use: buildx
- build_flag_templates:
- - "--platform=linux/ppc64le"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-riscv64
- goarch: riscv64
- dockerfile: Dockerfile
- use: buildx
- build_flag_templates:
- - "--platform=linux/riscv64"
- - "--build-arg=DEBIAN_VERSION=13"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-s390x
- - ghcr.io/anchore/syft:{{.Tag}}-s390x
- goarch: s390x
- dockerfile: Dockerfile
- use: buildx
- build_flag_templates:
- - "--platform=linux/s390x"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- # nonroot images...
- - image_templates:
- - anchore/syft:{{.Tag}}-nonroot-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
- goarch: amd64
+ # nonroot images
+ - id: nonroot
dockerfile: Dockerfile.nonroot
- use: buildx
- build_flag_templates:
- - "--platform=linux/amd64"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
+ ids: *docker-ids
+ images: *docker-images
+ platforms: *docker-platforms
+ labels: *docker-labels
+ tags:
+ - nonroot
+ - "{{.Tag}}-nonroot"
+ build_args: *docker-build-args
+ flags: *docker-flags
+ sbom: *docker-sbom
- - image_templates:
- - anchore/syft:{{.Tag}}-nonroot-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
- goarch: arm64
- dockerfile: Dockerfile.nonroot
- use: buildx
- build_flag_templates:
- - "--platform=linux/arm64/v8"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-nonroot-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
- goarch: ppc64le
- dockerfile: Dockerfile.nonroot
- use: buildx
- build_flag_templates:
- - "--platform=linux/ppc64le"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-nonroot-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
- goarch: riscv64
- dockerfile: Dockerfile.nonroot
- use: buildx
- build_flag_templates:
- - "--platform=linux/riscv64"
- - "--build-arg=DEBIAN_VERSION=13"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-nonroot-s390x
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
- goarch: s390x
- dockerfile: Dockerfile.nonroot
- use: buildx
- build_flag_templates:
- - "--platform=linux/s390x"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- # debug images...
- - image_templates:
- - anchore/syft:{{.Tag}}-debug-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- goarch: amd64
+ # debug images (root)
+ - id: debug
dockerfile: Dockerfile.debug
- use: buildx
- build_flag_templates:
- - "--platform=linux/amd64"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
+ ids: *docker-ids
+ images: *docker-images
+ platforms: *docker-platforms
+ labels: *docker-labels
+ tags:
+ - debug
+ - "{{.Tag}}-debug"
+ build_args: *docker-build-args
+ flags: *docker-flags
+ sbom: *docker-sbom
- - image_templates:
- - anchore/syft:{{.Tag}}-debug-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- goarch: arm64
- dockerfile: Dockerfile.debug
- use: buildx
- build_flag_templates:
- - "--platform=linux/arm64/v8"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-debug-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- goarch: ppc64le
- dockerfile: Dockerfile.debug
- use: buildx
- build_flag_templates:
- - "--platform=linux/ppc64le"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-debug-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
- goarch: riscv64
- dockerfile: Dockerfile.debug
- use: buildx
- build_flag_templates:
- - "--platform=linux/riscv64"
- - "--build-arg=DEBIAN_VERSION=13"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
- - image_templates:
- - anchore/syft:{{.Tag}}-debug-s390x
- - ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
- goarch: s390x
- dockerfile: Dockerfile.debug
- use: buildx
- build_flag_templates:
- - "--platform=linux/s390x"
- - "--build-arg=BUILD_DATE={{.Date}}"
- - "--build-arg=BUILD_VERSION={{.Version}}"
- - "--build-arg=VCS_REF={{.FullCommit}}"
- - "--build-arg=VCS_URL={{.GitURL}}"
-
-docker_manifests:
- - name_template: anchore/syft:latest
- image_templates:
- - anchore/syft:{{.Tag}}-amd64
- - anchore/syft:{{.Tag}}-arm64v8
- - anchore/syft:{{.Tag}}-ppc64le
- - anchore/syft:{{.Tag}}-riscv64
- - anchore/syft:{{.Tag}}-s390x
-
- - name_template: ghcr.io/anchore/syft:latest
- image_templates:
- - ghcr.io/anchore/syft:{{.Tag}}-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-s390x
-
- - name_template: anchore/syft:{{.Tag}}
- image_templates:
- - anchore/syft:{{.Tag}}-amd64
- - anchore/syft:{{.Tag}}-arm64v8
- - anchore/syft:{{.Tag}}-ppc64le
- - anchore/syft:{{.Tag}}-riscv64
- - anchore/syft:{{.Tag}}-s390x
-
- - name_template: ghcr.io/anchore/syft:{{.Tag}}
- image_templates:
- - ghcr.io/anchore/syft:{{.Tag}}-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-s390x
-
- # nonroot images...
- - name_template: anchore/syft:nonroot
- image_templates:
- - anchore/syft:{{.Tag}}-nonroot-amd64
- - anchore/syft:{{.Tag}}-nonroot-arm64v8
- - anchore/syft:{{.Tag}}-nonroot-ppc64le
- - anchore/syft:{{.Tag}}-nonroot-riscv64
- - anchore/syft:{{.Tag}}-nonroot-s390x
-
- - name_template: ghcr.io/anchore/syft:nonroot
- image_templates:
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
-
- - name_template: anchore/syft:{{.Tag}}-nonroot
- image_templates:
- - anchore/syft:{{.Tag}}-nonroot-amd64
- - anchore/syft:{{.Tag}}-nonroot-arm64v8
- - anchore/syft:{{.Tag}}-nonroot-ppc64le
- - anchore/syft:{{.Tag}}-nonroot-riscv64
- - anchore/syft:{{.Tag}}-nonroot-s390x
-
- - name_template: ghcr.io/anchore/syft:{{.Tag}}-nonroot
- image_templates:
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-nonroot-s390x
-
- # debug images...
- - name_template: anchore/syft:debug
- image_templates:
- - anchore/syft:{{.Tag}}-debug-amd64
- - anchore/syft:{{.Tag}}-debug-arm64v8
- - anchore/syft:{{.Tag}}-debug-ppc64le
- - anchore/syft:{{.Tag}}-debug-riscv64
- - anchore/syft:{{.Tag}}-debug-s390x
-
- - name_template: ghcr.io/anchore/syft:debug
- image_templates:
- - ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
-
- - name_template: anchore/syft:{{.Tag}}-debug
- image_templates:
- - anchore/syft:{{.Tag}}-debug-amd64
- - anchore/syft:{{.Tag}}-debug-arm64v8
- - anchore/syft:{{.Tag}}-debug-ppc64le
- - anchore/syft:{{.Tag}}-debug-riscv64
- - anchore/syft:{{.Tag}}-debug-s390x
-
- - name_template: ghcr.io/anchore/syft:{{.Tag}}-debug
- image_templates:
- - ghcr.io/anchore/syft:{{.Tag}}-debug-amd64
- - ghcr.io/anchore/syft:{{.Tag}}-debug-arm64v8
- - ghcr.io/anchore/syft:{{.Tag}}-debug-ppc64le
- - ghcr.io/anchore/syft:{{.Tag}}-debug-riscv64
- - ghcr.io/anchore/syft:{{.Tag}}-debug-s390x
+ # debug-nonroot images
+ - id: debug-nonroot
+ dockerfile: Dockerfile.debug-nonroot
+ ids: *docker-ids
+ images: *docker-images
+ platforms: *docker-platforms
+ labels: *docker-labels
+ tags:
+ - debug-nonroot
+ - "{{.Tag}}-debug-nonroot"
+ build_args: *docker-build-args
+ flags: *docker-flags
+ sbom: *docker-sbom
sboms:
- artifacts: archive
diff --git a/Dockerfile b/Dockerfile
index 86ff23db2..4dc169ab5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-ARG DEBIAN_VERSION=12
+ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:latest AS build
FROM scratch
@@ -8,23 +8,7 @@ COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifica
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
-COPY syft /
-
-ARG BUILD_DATE
-ARG BUILD_VERSION
-ARG VCS_REF
-ARG VCS_URL
-
-LABEL org.opencontainers.image.created=$BUILD_DATE
-LABEL org.opencontainers.image.title="syft"
-LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
-LABEL org.opencontainers.image.source=$VCS_URL
-LABEL org.opencontainers.image.revision=$VCS_REF
-LABEL org.opencontainers.image.vendor="Anchore, Inc."
-LABEL org.opencontainers.image.version=$BUILD_VERSION
-LABEL org.opencontainers.image.licenses="Apache-2.0"
-LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
-LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
-LABEL io.artifacthub.package.license="Apache-2.0"
+ARG TARGETPLATFORM
+COPY ${TARGETPLATFORM}/syft /
ENTRYPOINT ["/syft"]
diff --git a/Dockerfile.debug b/Dockerfile.debug
index 936b5ffc1..697527b10 100644
--- a/Dockerfile.debug
+++ b/Dockerfile.debug
@@ -1,28 +1,10 @@
-ARG DEBIAN_VERSION=12
-FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot
+ARG DEBIAN_VERSION=13
+FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
-COPY syft /
-
-USER nonroot
-
-ARG BUILD_DATE
-ARG BUILD_VERSION
-ARG VCS_REF
-ARG VCS_URL
-
-LABEL org.opencontainers.image.created=$BUILD_DATE
-LABEL org.opencontainers.image.title="syft"
-LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
-LABEL org.opencontainers.image.source=$VCS_URL
-LABEL org.opencontainers.image.revision=$VCS_REF
-LABEL org.opencontainers.image.vendor="Anchore, Inc."
-LABEL org.opencontainers.image.version=$BUILD_VERSION
-LABEL org.opencontainers.image.licenses="Apache-2.0"
-LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
-LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
-LABEL io.artifacthub.package.license="Apache-2.0"
+ARG TARGETPLATFORM
+COPY ${TARGETPLATFORM}/syft /
ENTRYPOINT ["/syft"]
diff --git a/Dockerfile.debug-nonroot b/Dockerfile.debug-nonroot
new file mode 100644
index 000000000..0e152cc12
--- /dev/null
+++ b/Dockerfile.debug-nonroot
@@ -0,0 +1,12 @@
+ARG DEBIAN_VERSION=13
+FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:debug-nonroot
+
+# create the /tmp dir, which is needed for image content cache
+WORKDIR /tmp
+
+ARG TARGETPLATFORM
+COPY ${TARGETPLATFORM}/syft /
+
+USER nonroot
+
+ENTRYPOINT ["/syft"]
diff --git a/Dockerfile.nonroot b/Dockerfile.nonroot
index c6789d4c7..61c937c70 100644
--- a/Dockerfile.nonroot
+++ b/Dockerfile.nonroot
@@ -1,28 +1,12 @@
-ARG DEBIAN_VERSION=12
+ARG DEBIAN_VERSION=13
FROM gcr.io/distroless/static-debian${DEBIAN_VERSION}:nonroot
# create the /tmp dir, which is needed for image content cache
WORKDIR /tmp
-COPY syft /
+ARG TARGETPLATFORM
+COPY ${TARGETPLATFORM}/syft /
USER nonroot
-ARG BUILD_DATE
-ARG BUILD_VERSION
-ARG VCS_REF
-ARG VCS_URL
-
-LABEL org.opencontainers.image.created=$BUILD_DATE
-LABEL org.opencontainers.image.title="syft"
-LABEL org.opencontainers.image.description="CLI tool and library for generating a Software Bill of Materials from container images and filesystems"
-LABEL org.opencontainers.image.source=$VCS_URL
-LABEL org.opencontainers.image.revision=$VCS_REF
-LABEL org.opencontainers.image.vendor="Anchore, Inc."
-LABEL org.opencontainers.image.version=$BUILD_VERSION
-LABEL org.opencontainers.image.licenses="Apache-2.0"
-LABEL io.artifacthub.package.readme-url="https://raw.githubusercontent.com/anchore/syft/main/README.md"
-LABEL io.artifacthub.package.logo-url="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png"
-LABEL io.artifacthub.package.license="Apache-2.0"
-
ENTRYPOINT ["/syft"]
diff --git a/Taskfile.yaml b/Taskfile.yaml
index d0ae4f20d..7e671c38e 100644
--- a/Taskfile.yaml
+++ b/Taskfile.yaml
@@ -39,6 +39,12 @@ vars:
PROJECT_ROOT:
sh: echo $PWD
+ # docker platform suffix goreleaser appends to snapshot image tags (e.g. latest-amd64).
+ # snapshot builds load per-arch images rather than a multi-arch manifest, so the smoke
+ # test must run the host-native tag to avoid emulation (and red-local/green-CI splits).
+ DOCKER_ARCH:
+ sh: go env GOARCH
+
# note: the snapshot dir must be a relative path starting with ./
# e.g. when installing snapshot debs from a local path, ./ forces the deb to be installed in the current working directory instead of referencing a package name
SNAPSHOT_DIR: ./snapshot
@@ -129,8 +135,8 @@ tasks:
silent: true
- "{{ .SNAPSHOT_BIN }} version"
- "{{ .SNAPSHOT_BIN }} scan alpine:latest"
- - docker run --rm anchore/syft:latest version
- - docker run --rm anchore/syft:latest scan alpine:latest
+ - docker run --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} version
+ - docker run --pull=never --rm anchore/syft:latest-{{ .DOCKER_ARCH }} scan alpine:latest
## Test-fixture-related targets ###########################################
From aff2c247dbebf948534a510315cf4ae9a70299dd Mon Sep 17 00:00:00 2001
From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
Date: Wed, 15 Jul 2026 17:14:03 -0400
Subject: [PATCH 073/168] feat: add safe tensor model type to SBOM output
(#4844)
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
internal/constants.go | 3 +-
.../packagemetadata/discover_type_names.go | 1 +
internal/packagemetadata/generated.go | 1 +
internal/packagemetadata/names.go | 1 +
internal/task/package_tasks.go | 1 +
schema/json/schema-16.1.10.json | 4575 +++++++++++++++++
schema/json/schema-latest.json | 54 +-
syft/file/mock_resolver.go | 16 +
.../helpers/originator_supplier_test.go | 1 +
.../internal/spdxutil/helpers/source_info.go | 2 +-
.../fileresolver/container_image_model.go | 20 +-
.../container_image_model_test.go | 6 +-
syft/pkg/cataloger/ai/capabilities.yaml | 43 +
syft/pkg/cataloger/ai/cataloger.go | 30 +-
syft/pkg/cataloger/ai/gguf_processor.go | 56 +
syft/pkg/cataloger/ai/huggingface.go | 93 +
syft/pkg/cataloger/ai/identity_dir.go | 93 +
syft/pkg/cataloger/ai/identity_oci.go | 173 +
syft/pkg/cataloger/ai/merge.go | 139 +
syft/pkg/cataloger/ai/merge_test.go | 166 +
syft/pkg/cataloger/ai/naming.go | 22 +
syft/pkg/cataloger/ai/package.go | 14 +
syft/pkg/cataloger/ai/parse_safetensors.go | 202 +
.../cataloger/ai/parse_safetensors_model.go | 48 +
.../pkg/cataloger/ai/parse_safetensors_oci.go | 117 +
.../cataloger/ai/parse_safetensors_test.go | 1202 +++++
syft/pkg/cataloger/ai/processor.go | 191 +-
.../ai/testdata/safetensors/README.md | 28 +
.../ai/testdata/safetensors/extract_header.go | 149 +
.../nomic-embed-475M.header.safetensors | Bin 0 -> 15520 bytes
.../cataloger/golang/stdlib_package_test.go | 4 +-
syft/pkg/safetensors.go | 48 +
syft/pkg/type_test.go | 3 +-
.../source/ocimodelsource/oci_model_source.go | 131 +-
syft/source/ocimodelsource/registry_client.go | 118 +-
.../registry_client_integration_test.go | 112 +
.../registry_client_safetensors_test.go | 55 +
37 files changed, 7829 insertions(+), 89 deletions(-)
create mode 100644 schema/json/schema-16.1.10.json
create mode 100644 syft/pkg/cataloger/ai/gguf_processor.go
create mode 100644 syft/pkg/cataloger/ai/huggingface.go
create mode 100644 syft/pkg/cataloger/ai/identity_dir.go
create mode 100644 syft/pkg/cataloger/ai/identity_oci.go
create mode 100644 syft/pkg/cataloger/ai/merge.go
create mode 100644 syft/pkg/cataloger/ai/merge_test.go
create mode 100644 syft/pkg/cataloger/ai/naming.go
create mode 100644 syft/pkg/cataloger/ai/parse_safetensors.go
create mode 100644 syft/pkg/cataloger/ai/parse_safetensors_model.go
create mode 100644 syft/pkg/cataloger/ai/parse_safetensors_oci.go
create mode 100644 syft/pkg/cataloger/ai/parse_safetensors_test.go
create mode 100644 syft/pkg/cataloger/ai/testdata/safetensors/README.md
create mode 100644 syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go
create mode 100644 syft/pkg/cataloger/ai/testdata/safetensors/nomic-embed-475M.header.safetensors
create mode 100644 syft/pkg/safetensors.go
create mode 100644 syft/source/ocimodelsource/registry_client_integration_test.go
create mode 100644 syft/source/ocimodelsource/registry_client_safetensors_test.go
diff --git a/internal/constants.go b/internal/constants.go
index f1b50a324..4aa1849d3 100644
--- a/internal/constants.go
+++ b/internal/constants.go
@@ -3,7 +3,7 @@ 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.1.9"
+ JSONSchemaVersion = "16.1.10"
// Changelog
// 16.1.0 - reformulated the python pdm fields (added "URL" and removed the unused "path" field).
@@ -16,4 +16,5 @@ const (
// 16.1.7 - add AppleAppBundleEntry metadata type for the apple app bundle cataloger
// 16.1.8 - add VcpkgManifest metadata type for vcpkg manifest support
// 16.1.9 - add Symbols (grouped by owning package import path) to GolangBinaryBuildinfoEntry metadata
+ // 16.1.10 - add SafeTensorsModelInfo metadata type for the safetensors AI model cataloger
)
diff --git a/internal/packagemetadata/discover_type_names.go b/internal/packagemetadata/discover_type_names.go
index 704c98c0b..cdcefa184 100644
--- a/internal/packagemetadata/discover_type_names.go
+++ b/internal/packagemetadata/discover_type_names.go
@@ -27,6 +27,7 @@ var knownNonMetadataTypeNames = strset.New(
var knownMetadataTypeNames = strset.New(
"DotnetPortableExecutableEntry",
"GGUFFileHeader",
+ "SafeTensorsModelInfo",
)
func DiscoverTypeNames() ([]string, error) {
diff --git a/internal/packagemetadata/generated.go b/internal/packagemetadata/generated.go
index 78fe51347..65fc659c7 100644
--- a/internal/packagemetadata/generated.go
+++ b/internal/packagemetadata/generated.go
@@ -68,6 +68,7 @@ func AllTypes() []any {
pkg.RubyGemspec{},
pkg.RustBinaryAuditEntry{},
pkg.RustCargoLockEntry{},
+ pkg.SafeTensorsModelInfo{},
pkg.SnapEntry{},
pkg.SwiftPackageManagerResolvedEntry{},
pkg.SwiplPackEntry{},
diff --git a/internal/packagemetadata/names.go b/internal/packagemetadata/names.go
index e4370a897..98e5d47ae 100644
--- a/internal/packagemetadata/names.go
+++ b/internal/packagemetadata/names.go
@@ -130,6 +130,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.DotnetPackagesLockEntry{}, "dotnet-packages-lock-entry"),
jsonNames(pkg.CondaMetaPackage{}, "conda-metadata-entry", "CondaPackageMetadata"),
jsonNames(pkg.GGUFFileHeader{}, "gguf-file-header"),
+ jsonNames(pkg.SafeTensorsModelInfo{}, "safetensors-model-info"),
jsonNames(pkg.VcpkgManifest{}, "vcpkg-manifest"),
)
diff --git a/internal/task/package_tasks.go b/internal/task/package_tasks.go
index fa6e3a950..89b5bd201 100644
--- a/internal/task/package_tasks.go
+++ b/internal/task/package_tasks.go
@@ -191,6 +191,7 @@ func DefaultPackageTaskFactories() Factories {
newSimplePackageTaskFactory(conda.NewCondaMetaCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.PackageTag, "conda"),
newSimplePackageTaskFactory(snap.NewCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, "snap"),
newSimplePackageTaskFactory(ai.NewGGUFCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "gguf", "ml"),
+ newSimplePackageTaskFactory(ai.NewSafeTensorsCataloger, pkgcataloging.DirectoryTag, pkgcataloging.ImageTag, "ai", "model", "safetensors", "ml"),
// deprecated catalogers ////////////////////////////////////////
// these are catalogers that should not be selectable other than specific inclusion via name or "deprecated" tag (to remain backwards compatible)
diff --git a/schema/json/schema-16.1.10.json b/schema/json/schema-16.1.10.json
new file mode 100644
index 000000000..87756c843
--- /dev/null
+++ b/schema/json/schema-16.1.10.json
@@ -0,0 +1,4575 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "anchore.io/schema/syft/json/16.1.10/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",
+ "description": "AlpmFileRecord represents a single file entry within an Arch Linux package with its associated metadata tracked by pacman."
+ },
+ "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)."
+ },
+ "AppleAppBundleEntry": {
+ "properties": {
+ "bundleIdentifier": {
+ "type": "string",
+ "description": "BundleIdentifier is the unique identifier for the bundle (e.g. \"com.apple.Safari\")"
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the short name of the bundle"
+ },
+ "displayName": {
+ "type": "string",
+ "description": "DisplayName is the user-facing name of the bundle"
+ },
+ "executable": {
+ "type": "string",
+ "description": "Executable is the name of the executable within the bundle"
+ },
+ "shortVersion": {
+ "type": "string",
+ "description": "ShortVersion is the release (marketing) version of the bundle"
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the build version of the bundle, which often differs from the short version"
+ },
+ "packageType": {
+ "type": "string",
+ "description": "PackageType is the four-letter type code (e.g. \"APPL\" for apps, \"FMWK\" for frameworks, \"BNDL\" for bundles)"
+ },
+ "supportedPlatforms": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "SupportedPlatforms lists the platforms the bundle targets (e.g. \"MacOSX\", \"iPhoneOS\")"
+ },
+ "minimumSystemVersion": {
+ "type": "string",
+ "description": "MinimumSystemVersion is the minimum macOS version required to run the bundle"
+ },
+ "minimumOSVersion": {
+ "type": "string",
+ "description": "MinimumOSVersion is the minimum OS version required for non-macOS platforms (e.g. iOS)"
+ },
+ "copyright": {
+ "type": "string",
+ "description": "Copyright is the human-readable copyright notice for the bundle"
+ },
+ "platformName": {
+ "type": "string",
+ "description": "PlatformName is the platform name of the SDK used to build the bundle (e.g. \"macosx\")"
+ },
+ "sdkName": {
+ "type": "string",
+ "description": "SDKName is the name of the SDK used to build the bundle (e.g. \"macosx14.0\")"
+ }
+ },
+ "type": "object",
+ "description": "AppleAppBundleEntry represents metadata about an Apple application bundle (CFBundle) extracted from Info.plist files."
+ },
+ "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",
+ "description": "Value is the CPE string identifier."
+ },
+ "source": {
+ "type": "string",
+ "description": "Source is the source where this CPE was obtained or generated from."
+ }
+ },
+ "type": "object",
+ "required": [
+ "cpe"
+ ],
+ "description": "CPE represents a Common Platform Enumeration identifier used for matching packages to known vulnerabilities in security databases."
+ },
+ "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."
+ },
+ "DenoLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies is the list of package specifiers that this package depends on"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies"
+ ],
+ "description": "DenoLockEntry is a struct that rep a single entry found in the \"packages\" section of a Deno deno.lock file"
+ },
+ "DenoRemoteLockEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the remote URL from which the module fetcef"
+ },
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is the crpto hash of the package content for verification"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "integrity"
+ ],
+ "description": "DenoRemoteLockEntry is a struct that rep a single entry found in the \"remote\" section of a Deno deno.lock file"
+ },
+ "Descriptor": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the tool that generated this SBOM (e.g., \"syft\")."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the tool that generated this SBOM."
+ },
+ "configuration": {
+ "description": "Configuration contains the tool configuration used during SBOM generation."
+ }
+ },
+ "type": "object",
+ "required": [
+ "name",
+ "version"
+ ],
+ "description": "Descriptor identifies the tool that generated this SBOM document, including its name, version, and configuration used during catalog generation."
+ },
+ "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",
+ "description": "Artifacts is the list of packages discovered and placed into the catalog"
+ },
+ "artifactRelationships": {
+ "items": {
+ "$ref": "#/$defs/Relationship"
+ },
+ "type": "array"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/File"
+ },
+ "type": "array",
+ "description": "note: must have omitempty"
+ },
+ "source": {
+ "$ref": "#/$defs/Source",
+ "description": "Source represents the original object that was cataloged"
+ },
+ "distro": {
+ "$ref": "#/$defs/LinuxRelease",
+ "description": "Distro represents the Linux distribution that was detected from the source"
+ },
+ "descriptor": {
+ "$ref": "#/$defs/Descriptor",
+ "description": "Descriptor is a block containing self-describing information about syft"
+ },
+ "schema": {
+ "$ref": "#/$defs/Schema",
+ "description": "Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape"
+ }
+ },
+ "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\")"
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is type of entry could be package or project for internal refs"
+ },
+ "executables": {
+ "additionalProperties": {
+ "$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)\n\nDeprecated: in Syft 2.0 the struct tag will be corrected to `osCpe` to match the systemd spec casing."
+ },
+ "appCpe": {
+ "type": "string",
+ "description": "AppCpe is a CPE name for the upstream Application, as found in NVD CPE search (e.g. cpe:2.3:a:gnu:coreutils:5.0)"
+ },
+ "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)"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the names of the packages this entry depends on, as\ndeclared in the entry's dependency list within mix.lock. Used to derive\ndependency-of relationships between locked packages."
+ }
+ },
+ "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",
+ "description": "ID is a unique identifier for this file within the SBOM."
+ },
+ "location": {
+ "$ref": "#/$defs/Coordinates",
+ "description": "Location is the file path and layer information where this file was found."
+ },
+ "metadata": {
+ "$ref": "#/$defs/FileMetadataEntry",
+ "description": "Metadata contains filesystem metadata such as permissions, ownership, and file type."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the file contents for small files."
+ },
+ "digests": {
+ "items": {
+ "$ref": "#/$defs/Digest"
+ },
+ "type": "array",
+ "description": "Digests contains cryptographic hashes of the file contents."
+ },
+ "licenses": {
+ "items": {
+ "$ref": "#/$defs/FileLicense"
+ },
+ "type": "array",
+ "description": "Licenses contains license information discovered within this file."
+ },
+ "executable": {
+ "$ref": "#/$defs/Executable",
+ "description": "Executable contains executable metadata if this file is a binary."
+ },
+ "unknowns": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Unknowns contains unknown fields for forward compatibility."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "location"
+ ],
+ "description": "File represents a file discovered during cataloging with its metadata, content digests, licenses, and relationships to packages."
+ },
+ "FileLicense": {
+ "properties": {
+ "value": {
+ "type": "string",
+ "description": "Value is the raw license identifier or text as found in the file."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "evidence": {
+ "$ref": "#/$defs/FileLicenseEvidence",
+ "description": "Evidence contains supporting evidence for this license detection."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type"
+ ],
+ "description": "FileLicense represents license information discovered within a file's contents or metadata, including the matched license text and SPDX expression."
+ },
+ "FileLicenseEvidence": {
+ "properties": {
+ "confidence": {
+ "type": "integer",
+ "description": "Confidence is the confidence score for this license detection (0-100)."
+ },
+ "offset": {
+ "type": "integer",
+ "description": "Offset is the byte offset where the license text starts in the file."
+ },
+ "extent": {
+ "type": "integer",
+ "description": "Extent is the length of the license text in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "confidence",
+ "offset",
+ "extent"
+ ],
+ "description": "FileLicenseEvidence contains supporting evidence for a license detection in a file, including the byte offset, extent, and confidence level."
+ },
+ "FileMetadataEntry": {
+ "properties": {
+ "mode": {
+ "type": "integer",
+ "description": "Mode is the Unix file permission mode in octal format."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the file type (e.g., \"RegularFile\", \"Directory\", \"SymbolicLink\")."
+ },
+ "linkDestination": {
+ "type": "string",
+ "description": "LinkDestination is the target path for symbolic links."
+ },
+ "userID": {
+ "type": "integer",
+ "description": "UserID is the file owner user ID."
+ },
+ "groupID": {
+ "type": "integer",
+ "description": "GroupID is the file owner group ID."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIMEType is the MIME type of the file contents."
+ },
+ "size": {
+ "type": "integer",
+ "description": "Size is the file size in bytes."
+ }
+ },
+ "type": "object",
+ "required": [
+ "mode",
+ "type",
+ "userID",
+ "groupID",
+ "mimeType",
+ "size"
+ ],
+ "description": "FileMetadataEntry contains filesystem-level metadata attributes such as permissions, ownership, type, and size for a cataloged file."
+ },
+ "GgufFileHeader": {
+ "properties": {
+ "ggufVersion": {
+ "type": "integer",
+ "description": "GGUFVersion is the GGUF format version (e.g., 3)"
+ },
+ "fileSize": {
+ "type": "integer",
+ "description": "FileSize is the size of the GGUF file in bytes (best-effort if available from resolver)"
+ },
+ "architecture": {
+ "type": "string",
+ "description": "Architecture is the model architecture (from general.architecture, e.g., \"qwen3moe\", \"llama\")"
+ },
+ "quantization": {
+ "type": "string",
+ "description": "Quantization is the quantization type (e.g., \"IQ4_NL\", \"Q4_K_M\")"
+ },
+ "parameters": {
+ "type": "integer",
+ "description": "Parameters is the number of model parameters (if present in header)"
+ },
+ "tensorCount": {
+ "type": "integer",
+ "description": "TensorCount is the number of tensors in the model"
+ },
+ "header": {
+ "type": "object",
+ "description": "RemainingKeyValues contains the remaining key-value pairs from the GGUF header that are not already\nrepresented as typed fields above. This preserves additional metadata fields for reference\n(namespaced with general.*, llama.*, etc.) while avoiding duplication."
+ },
+ "metadataHash": {
+ "type": "string",
+ "description": "MetadataKeyValuesHash is a xx64 hash of all key-value pairs from the GGUF header metadata.\nThis hash is computed over the complete header metadata (including the fields extracted\ninto typed fields above) and provides a stable identifier for the model configuration\nacross different file locations or remotes. It allows matching identical models even\nwhen stored in different repositories or with different filenames."
+ },
+ "parts": {
+ "items": {
+ "$ref": "#/$defs/GgufFileHeader"
+ },
+ "type": "array",
+ "description": "Parts contains headers from additional GGUF files that were merged\ninto this package during post-processing (e.g., from OCI layers without model names)."
+ }
+ },
+ "type": "object",
+ "required": [
+ "ggufVersion",
+ "tensorCount"
+ ],
+ "description": "GGUFFileHeader represents metadata extracted from a GGUF (GPT-Generated Unified Format) model file."
+ },
+ "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\")."
+ },
+ "symbols": {
+ "additionalProperties": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "object",
+ "description": "Symbols are the function symbols from this module that are compiled into the binary, extracted from\nthe binary symbol table (pclntab) and grouped by the import path of the package that owns them. Each\nvalue is the sorted, deduplicated list of symbol names local to that package, i.e. with the import\npath prefix stripped (e.g. import path \"github.com/foo/bar\" -\u003e \"(*Type).Method\"). The fully qualified\nname is the import path, a \".\", and the local name. One exception: the binary's main package appears\nunder the key \"main\" (the name the linker assigns), not its original source import path, which is not\nrecoverable from the binary. Populated only when the golang cataloger's capture-symbols scope covers\nthis package: the \"all\" scope populates every module package plus the synthetic stdlib package, while\nthe \"stdlib\" scope populates only the stdlib package."
+ }
+ },
+ "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",
+ "description": "IDLikes represents a list of distribution IDs that this Linux distribution is similar to or derived from, as defined in os-release ID_LIKE field."
+ },
+ "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": {
+ "additionalProperties": {
+ "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."
+ },
+ "JavascriptBunLockEntry": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of runtime dependencies and their version specifiers"
+ },
+ "optionalDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "OptionalDependencies is a map of optional dependencies and their version specifiers"
+ },
+ "peerDependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "PeerDependencies is a map of peer dependencies and their version specifiers"
+ },
+ "bin": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Bin is a map of binary names to the paths they are installed to"
+ },
+ "os": {
+ "type": "string",
+ "description": "OS is the operating system constraint for the package (e.g. \"darwin\")"
+ },
+ "cpu": {
+ "type": "string",
+ "description": "CPU is the CPU architecture constraint for the package (e.g. \"arm64\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity",
+ "dependencies",
+ "optionalDependencies",
+ "peerDependencies",
+ "bin",
+ "os",
+ "cpu"
+ ],
+ "description": "BunLockEntry represents a single entry in the \"packages\" section of a bun.lock file"
+ },
+ "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."
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their version markers, i.e. \"lodash\": \"^1.0.0\""
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "description": "NpmPackageLockEntry represents a single entry within the \"packages\" section of a package-lock.json file."
+ },
+ "JavascriptPnpmLockEntry": {
+ "properties": {
+ "resolution": {
+ "$ref": "#/$defs/PnpmLockResolution",
+ "description": "Resolution is the resolution information for the package"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolution",
+ "dependencies"
+ ],
+ "description": "PnpmLockEntry represents a single entry in the \"packages\" section of a pnpm-lock.yaml 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)"
+ },
+ "dependencies": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Dependencies is a map of dependencies and their versions"
+ }
+ },
+ "type": "object",
+ "required": [
+ "resolved",
+ "integrity",
+ "dependencies"
+ ],
+ "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",
+ "description": "Value is the raw license identifier or expression as found."
+ },
+ "spdxExpression": {
+ "type": "string",
+ "description": "SPDXExpression is the parsed SPDX license expression."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the license type classification (e.g., declared, concluded, discovered)."
+ },
+ "urls": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "URLs are URLs where license text or information can be found."
+ },
+ "locations": {
+ "items": {
+ "$ref": "#/$defs/Location"
+ },
+ "type": "array",
+ "description": "Locations are file locations where this license was discovered."
+ },
+ "contents": {
+ "type": "string",
+ "description": "Contents is the full license text content."
+ }
+ },
+ "type": "object",
+ "required": [
+ "value",
+ "spdxExpression",
+ "type",
+ "urls",
+ "locations"
+ ],
+ "description": "License represents software license information discovered for a package, including SPDX expressions and supporting evidence 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": {
+ "additionalProperties": {
+ "$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",
+ "description": "PrettyName is a human-readable operating system name with version."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the operating system name without version information."
+ },
+ "id": {
+ "type": "string",
+ "description": "ID is the lower-case operating system identifier (e.g., \"ubuntu\", \"rhel\")."
+ },
+ "idLike": {
+ "$ref": "#/$defs/IDLikes",
+ "description": "IDLike is a list of operating system IDs this distribution is similar to or derived from."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the operating system version including codename if available."
+ },
+ "versionID": {
+ "type": "string",
+ "description": "VersionID is the operating system version number or identifier."
+ },
+ "versionCodename": {
+ "type": "string",
+ "description": "VersionCodename is the operating system release codename (e.g., \"jammy\", \"bullseye\")."
+ },
+ "buildID": {
+ "type": "string",
+ "description": "BuildID is a build identifier for the operating system."
+ },
+ "imageID": {
+ "type": "string",
+ "description": "ImageID is an identifier for container or cloud images."
+ },
+ "imageVersion": {
+ "type": "string",
+ "description": "ImageVersion is the version for container or cloud images."
+ },
+ "variant": {
+ "type": "string",
+ "description": "Variant is the operating system variant name (e.g., \"Server\", \"Workstation\")."
+ },
+ "variantID": {
+ "type": "string",
+ "description": "VariantID is the lower-case operating system variant identifier."
+ },
+ "homeURL": {
+ "type": "string",
+ "description": "HomeURL is the homepage URL for the operating system."
+ },
+ "supportURL": {
+ "type": "string",
+ "description": "SupportURL is the support or help URL for the operating system."
+ },
+ "bugReportURL": {
+ "type": "string",
+ "description": "BugReportURL is the bug reporting URL for the operating system."
+ },
+ "privacyPolicyURL": {
+ "type": "string",
+ "description": "PrivacyPolicyURL is the privacy policy URL for the operating system."
+ },
+ "cpeName": {
+ "type": "string",
+ "description": "CPEName is the Common Platform Enumeration name for the operating system."
+ },
+ "supportEnd": {
+ "type": "string",
+ "description": "SupportEnd is the end of support date or version identifier."
+ },
+ "extendedSupport": {
+ "type": "boolean",
+ "description": "ExtendedSupport indicates whether extended security or support is available."
+ }
+ },
+ "type": "object",
+ "description": "LinuxRelease contains Linux distribution identification and version information extracted from /etc/os-release or similar system files."
+ },
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Arbitrary key-value pairs that can be used to annotate a location"
+ }
+ },
+ "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": {
+ "additionalProperties": {
+ "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 represents a Windows Knowledge Base patch identifier associated with a specific Microsoft product from the MSRC (Microsoft Security Response Center)."
+ },
+ "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/AppleAppBundleEntry"
+ },
+ {
+ "$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/DenoLockEntry"
+ },
+ {
+ "$ref": "#/$defs/DenoRemoteLockEntry"
+ },
+ {
+ "$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/GgufFileHeader"
+ },
+ {
+ "$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/JavascriptBunLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackage"
+ },
+ {
+ "$ref": "#/$defs/JavascriptNpmPackageLockEntry"
+ },
+ {
+ "$ref": "#/$defs/JavascriptPnpmLockEntry"
+ },
+ {
+ "$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/SafetensorsModelInfo"
+ },
+ {
+ "$ref": "#/$defs/SnapEntry"
+ },
+ {
+ "$ref": "#/$defs/SwiftPackageManagerLockEntry"
+ },
+ {
+ "$ref": "#/$defs/SwiplpackPackage"
+ },
+ {
+ "$ref": "#/$defs/TerraformLockProviderEntry"
+ },
+ {
+ "$ref": "#/$defs/VcpkgManifest"
+ },
+ {
+ "$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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Require is runtime dependencies with version constraints (package will not install unless these requirements can be met)"
+ },
+ "provide": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "Provide is virtual packages/functionality provided by this package (allows other packages to depend on capabilities)"
+ },
+ "require-dev": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "type": "object",
+ "description": "RequireDev is development-only dependencies (not installed in production, only when developing this package or running tests)"
+ },
+ "suggest": {
+ "additionalProperties": {
+ "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."
+ },
+ "PnpmLockResolution": {
+ "properties": {
+ "integrity": {
+ "type": "string",
+ "description": "Integrity is Subresource Integrity hash for verification (SRI format)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "integrity"
+ ],
+ "description": "PnpmLockResolution contains package resolution metadata from pnpm lockfiles, including the integrity hash used for verification."
+ },
+ "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/)."
+ },
+ "PythonPdmFileEntry": {
+ "properties": {
+ "url": {
+ "type": "string",
+ "description": "URL is the file download URL"
+ },
+ "digest": {
+ "$ref": "#/$defs/PythonFileDigest",
+ "description": "Digest is the hash digest of the file hosted at the URL"
+ }
+ },
+ "type": "object",
+ "required": [
+ "url",
+ "digest"
+ ]
+ },
+ "PythonPdmLockEntry": {
+ "properties": {
+ "summary": {
+ "type": "string",
+ "description": "Summary provides a description of the package"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files with their paths and hash digests (for the base package without extras)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the \"environment\" --conditional expressions that determine whether a package should be installed based on the runtime environment"
+ },
+ "requiresPython": {
+ "type": "string",
+ "description": "RequiresPython specifies the Python version requirement (e.g., \"\u003e=3.6\")."
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependency specifications for the base package (without extras)"
+ },
+ "extras": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmLockExtraVariant"
+ },
+ "type": "array",
+ "description": "Extras contains variants for different extras combinations (PDM may have multiple entries per package)"
+ }
+ },
+ "type": "object",
+ "required": [
+ "summary",
+ "files"
+ ],
+ "description": "PythonPdmLockEntry represents a single package entry within a pdm.lock file."
+ },
+ "PythonPdmLockExtraVariant": {
+ "properties": {
+ "extras": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Extras are the optional extras enabled for this variant (e.g., [\"toml\"], [\"dev\"], or [\"toml\", \"dev\"])"
+ },
+ "dependencies": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Dependencies are the dependencies specific to this extras variant"
+ },
+ "files": {
+ "items": {
+ "$ref": "#/$defs/PythonPdmFileEntry"
+ },
+ "type": "array",
+ "description": "Files are the package files specific to this variant (only populated if different from base)"
+ },
+ "marker": {
+ "type": "string",
+ "description": "Marker is the environment conditional expression for this variant (e.g., \"python_version \u003c \\\"3.11\\\"\")"
+ }
+ },
+ "type": "object",
+ "required": [
+ "extras"
+ ],
+ "description": "PythonPdmLockExtraVariant represents a specific extras combination variant 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",
+ "description": "Parent is the ID of the parent artifact in this relationship."
+ },
+ "child": {
+ "type": "string",
+ "description": "Child is the ID of the child artifact in this relationship."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the relationship type (e.g., \"contains\", \"dependency-of\", \"ancestor-of\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional relationship-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "parent",
+ "child",
+ "type"
+ ],
+ "description": "Relationship represents a directed relationship between two artifacts in the SBOM, such as package-contains-file or package-depends-on-package."
+ },
+ "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."
+ },
+ "SafetensorsModelInfo": {
+ "properties": {
+ "format": {
+ "type": "string",
+ "description": "Format is the source format label (always \"safetensors\" for this metadata type).\nPresent because the Docker AI model config blob carries an explicit format field"
+ },
+ "architecture": {
+ "type": "string",
+ "description": "Architecture is the model architecture (e.g., \"LlamaForCausalLM\",\n\"Qwen3MoeForConditionalGeneration\"). It is not present in the SafeTensors\nheader itself; it is enriched from the companion config.json\n\"architectures\" array when one is found alongside the model."
+ },
+ "quantization": {
+ "type": "string",
+ "description": "Quantization describes tensor precision (e.g., \"BF16\", \"F16\", \"F32\", \"INT8\")."
+ },
+ "parameters": {
+ "type": "integer",
+ "description": "Parameters is the total number of model parameters, computed from the tensor\nshapes in the SafeTensors header(s). For a sharded model it is the sum across\nevery shard."
+ },
+ "tensorCount": {
+ "type": "integer",
+ "description": "TensorCount is the number of tensor entries in the file header."
+ },
+ "totalSize": {
+ "type": "string",
+ "description": "TotalSize is the total byte size of tensor data across all shards when known\n(from the Docker AI model config \"size\" field)."
+ },
+ "shardCount": {
+ "type": "integer",
+ "description": "ShardCount is the number of .safetensors shards for a sharded model (1 for a\nsingle-file model)."
+ },
+ "userMetadata": {
+ "$ref": "#/$defs/KeyValues",
+ "description": "UserMetadata is the optional \"__metadata__\" map from a .safetensors file header\n(string-to-string key/values set by the producer)."
+ },
+ "metadataHash": {
+ "type": "string",
+ "description": "MetadataHash is an xxhash over the on-disk SafeTensors header (sorted tensor\nentries + __metadata__). It is derived ONLY from the safetensors file bytes."
+ },
+ "parts": {
+ "items": {
+ "$ref": "#/$defs/SafetensorsModelInfo"
+ },
+ "type": "array",
+ "description": "Parts contains metadata from additional SafeTensors shards or OCI layers that\nwere merged into this package during post-processing."
+ }
+ },
+ "type": "object",
+ "description": "SafeTensorsModelInfo holds the model details extracted from SafeTensors content."
+ },
+ "Schema": {
+ "properties": {
+ "version": {
+ "type": "string",
+ "description": "Version is the JSON schema version for this document format."
+ },
+ "url": {
+ "type": "string",
+ "description": "URL is the URL to the JSON schema definition document."
+ }
+ },
+ "type": "object",
+ "required": [
+ "version",
+ "url"
+ ],
+ "description": "Schema specifies the JSON schema version and URL reference that defines the structure and validation rules for this document format."
+ },
+ "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",
+ "description": "ID is a unique identifier for the analyzed source artifact."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the name of the analyzed artifact (e.g., image name, directory path)."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the version of the analyzed artifact (e.g., image tag)."
+ },
+ "supplier": {
+ "type": "string",
+ "description": "Supplier is supplier information, which can be user-provided for NTIA minimum elements compliance."
+ },
+ "type": {
+ "type": "string",
+ "description": "Type is the source type (e.g., \"image\", \"directory\", \"file\")."
+ },
+ "metadata": {
+ "description": "Metadata contains additional source-specific metadata."
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "type",
+ "metadata"
+ ],
+ "description": "Source represents the artifact that was analyzed to generate this SBOM, such as a container image, directory, or file archive."
+ },
+ "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)."
+ },
+ "VcpkgManifest": {
+ "properties": {
+ "description": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Description is the human-readable description of the package (may be a single string or a list of paragraphs in the manifest)."
+ },
+ "documentation": {
+ "type": "string",
+ "description": "Documentation is the URL to the package's documentation."
+ },
+ "full-version": {
+ "type": "string",
+ "description": "FullVersion is the complete version string including the port-version suffix (e.g. \"1.2.3#2\")."
+ },
+ "version": {
+ "type": "string",
+ "description": "Version is the upstream package version without the port-version suffix (e.g. \"1.2.3\")."
+ },
+ "port-version": {
+ "type": "integer",
+ "description": "PortVersion is the vcpkg-specific packaging revision for a given upstream version."
+ },
+ "maintainers": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array",
+ "description": "Maintainers are the people responsible for maintaining the vcpkg port."
+ },
+ "name": {
+ "type": "string",
+ "description": "Name is the package name as declared in the manifest."
+ },
+ "supports": {
+ "type": "string",
+ "description": "Supports is the platform expression describing which triplets the package can be built for (e.g. \"!windows\")."
+ },
+ "registry": {
+ "$ref": "#/$defs/VcpkgRegistryEntry",
+ "description": "Registry indicates where the package definition came from."
+ },
+ "triplet": {
+ "type": "string",
+ "description": "Triplet is the build target discovered from the build folder (e.g. \"x64-linux\")."
+ }
+ },
+ "type": "object",
+ "required": [
+ "full-version",
+ "version",
+ "port-version",
+ "name"
+ ],
+ "description": "VcpkgManifest summarizes the data found in a vcpkg manifest (vcpkg.json) relevant to a single vcpkg package."
+ },
+ "VcpkgRegistryEntry": {
+ "properties": {
+ "baseline": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "packages": {
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "path": {
+ "type": "string"
+ },
+ "reference": {
+ "type": "string"
+ },
+ "repository": {
+ "type": "string"
+ }
+ },
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "description": "Matches definition of Vcpkg \"Registry\"."
+ },
+ "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 75eea2f94..87756c843 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.1.9/document",
+ "$id": "anchore.io/schema/syft/json/16.1.10/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@@ -2925,6 +2925,9 @@
{
"$ref": "#/$defs/RustCargoLockEntry"
},
+ {
+ "$ref": "#/$defs/SafetensorsModelInfo"
+ },
{
"$ref": "#/$defs/SnapEntry"
},
@@ -4216,6 +4219,55 @@
],
"description": "RustCargoLockEntry represents a locked dependency from a Cargo.lock file with precise version and checksum information."
},
+ "SafetensorsModelInfo": {
+ "properties": {
+ "format": {
+ "type": "string",
+ "description": "Format is the source format label (always \"safetensors\" for this metadata type).\nPresent because the Docker AI model config blob carries an explicit format field"
+ },
+ "architecture": {
+ "type": "string",
+ "description": "Architecture is the model architecture (e.g., \"LlamaForCausalLM\",\n\"Qwen3MoeForConditionalGeneration\"). It is not present in the SafeTensors\nheader itself; it is enriched from the companion config.json\n\"architectures\" array when one is found alongside the model."
+ },
+ "quantization": {
+ "type": "string",
+ "description": "Quantization describes tensor precision (e.g., \"BF16\", \"F16\", \"F32\", \"INT8\")."
+ },
+ "parameters": {
+ "type": "integer",
+ "description": "Parameters is the total number of model parameters, computed from the tensor\nshapes in the SafeTensors header(s). For a sharded model it is the sum across\nevery shard."
+ },
+ "tensorCount": {
+ "type": "integer",
+ "description": "TensorCount is the number of tensor entries in the file header."
+ },
+ "totalSize": {
+ "type": "string",
+ "description": "TotalSize is the total byte size of tensor data across all shards when known\n(from the Docker AI model config \"size\" field)."
+ },
+ "shardCount": {
+ "type": "integer",
+ "description": "ShardCount is the number of .safetensors shards for a sharded model (1 for a\nsingle-file model)."
+ },
+ "userMetadata": {
+ "$ref": "#/$defs/KeyValues",
+ "description": "UserMetadata is the optional \"__metadata__\" map from a .safetensors file header\n(string-to-string key/values set by the producer)."
+ },
+ "metadataHash": {
+ "type": "string",
+ "description": "MetadataHash is an xxhash over the on-disk SafeTensors header (sorted tensor\nentries + __metadata__). It is derived ONLY from the safetensors file bytes."
+ },
+ "parts": {
+ "items": {
+ "$ref": "#/$defs/SafetensorsModelInfo"
+ },
+ "type": "array",
+ "description": "Parts contains metadata from additional SafeTensors shards or OCI layers that\nwere merged into this package during post-processing."
+ }
+ },
+ "type": "object",
+ "description": "SafeTensorsModelInfo holds the model details extracted from SafeTensors content."
+ },
"Schema": {
"properties": {
"version": {
diff --git a/syft/file/mock_resolver.go b/syft/file/mock_resolver.go
index b9ed38420..67388796e 100644
--- a/syft/file/mock_resolver.go
+++ b/syft/file/mock_resolver.go
@@ -25,6 +25,7 @@ type MockResolver struct {
mediaTypeIndex map[string][]Location
extension map[string][]Location
basename map[string][]Location
+ ociRef string
}
// NewMockResolverForPaths creates a new MockResolver, where the only resolvable
@@ -74,6 +75,16 @@ func NewMockResolverForPathsWithMetadata(metadata map[Coordinates]Metadata) *Moc
}
}
+// NewMockResolverForOCIArtifact creates a MockResolver that can resolve files
+// by media type AND surfaces the given OCI ref via the ImageReference method.
+// Intended for tests that exercise the catalogers' OCI-artifact-aware naming
+// code paths.
+func NewMockResolverForOCIArtifact(ref string, mediaTypes map[string][]Location) *MockResolver {
+ r := NewMockResolverForMediaTypes(mediaTypes)
+ r.ociRef = ref
+ return r
+}
+
// NewMockResolverForMediaTypes creates a MockResolver that can resolve files by media type.
// The mediaTypes map specifies which locations should be returned for each media type.
func NewMockResolverForMediaTypes(mediaTypes map[string][]Location) *MockResolver {
@@ -102,6 +113,11 @@ func NewMockResolverForMediaTypes(mediaTypes map[string][]Location) *MockResolve
}
}
+// ImageReference returns the image reference associated with this mock, if any.
+func (r MockResolver) ImageReference() string {
+ return r.ociRef
+}
+
// HasPath indicates if the given path exists in the underlying source.
func (r MockResolver) HasPath(path string) bool {
for _, l := range r.locations {
diff --git a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
index dec9d06f0..cede5bf48 100644
--- a/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
+++ b/syft/format/internal/spdxutil/helpers/originator_supplier_test.go
@@ -58,6 +58,7 @@ func Test_OriginatorSupplier(t *testing.T) {
pkg.YarnLockEntry{},
pkg.TerraformLockProviderEntry{},
pkg.GGUFFileHeader{},
+ pkg.SafeTensorsModelInfo{},
pkg.DenoLockEntry{},
pkg.DenoRemoteLockEntry{},
pkg.VcpkgManifest{},
diff --git a/syft/format/internal/spdxutil/helpers/source_info.go b/syft/format/internal/spdxutil/helpers/source_info.go
index 5682e22da..dfe24324e 100644
--- a/syft/format/internal/spdxutil/helpers/source_info.go
+++ b/syft/format/internal/spdxutil/helpers/source_info.go
@@ -85,7 +85,7 @@ func SourceInfo(p pkg.Package) string {
case pkg.TerraformPkg:
answer = "acquired package info from Terraform dependency lock file"
case pkg.ModelPkg:
- answer = "acquired package info from AI artifact (e.g. GGUF File)"
+ answer = "acquired package info from AI model artifact"
case pkg.VcpkgPkg:
answer = "acquired package info from vcpkg manifest file"
default:
diff --git a/syft/internal/fileresolver/container_image_model.go b/syft/internal/fileresolver/container_image_model.go
index 51e6736fd..3cc4a7422 100644
--- a/syft/internal/fileresolver/container_image_model.go
+++ b/syft/internal/fileresolver/container_image_model.go
@@ -20,16 +20,22 @@ type LayerInfo struct {
}
// ContainerImageModel is a file.Resolver implementation that provides access to
-// GGUF header data fetched from OCI model artifacts via range-GET requests.
-// This does not fetch the entire model from the registry, only a sliver of it.
+// model header and metadata data (GGUF and SafeTensors headers, the model config
+// blob, and companion layers) fetched from OCI model artifacts via range-GET
+// requests. This does not fetch the entire model from the registry, only a
+// sliver of it.
type ContainerImageModel struct {
tempDir string // temp directory containing all layer files
layerFiles map[string]LayerInfo // digest -> layer info (temp path + media type)
locations map[string]file.Location // digest -> location
+ ref string // image reference the artifact was fetched with
}
-// NewContainerImageModel creates a new resolver with the given temp directory and layer files.
-func NewContainerImageModel(tempDir string, layerFiles map[string]LayerInfo) *ContainerImageModel {
+// NewContainerImageModel creates a new resolver with the given temp directory
+// and layer files. The ref is surfaced through the ImageReference method so
+// catalogers can derive context-level naming hints from the artifact reference
+// when the layer contents don't carry one.
+func NewContainerImageModel(tempDir string, layerFiles map[string]LayerInfo, ref string) *ContainerImageModel {
// Create locations for all layer files
// Each location has RealPath="/", FileSystemID=digest, AccessPath="/"
locations := make(map[string]file.Location, len(layerFiles))
@@ -43,9 +49,15 @@ func NewContainerImageModel(tempDir string, layerFiles map[string]LayerInfo) *Co
tempDir: tempDir,
layerFiles: layerFiles,
locations: locations,
+ ref: ref,
}
}
+// ImageReference returns the image reference the artifact was fetched with.
+func (r *ContainerImageModel) ImageReference() string {
+ return r.ref
+}
+
// FilesByMediaType returns locations for layers matching the given media type patterns.
// Patterns support glob-style matching (e.g., "application/vnd.docker.ai*").
func (r *ContainerImageModel) FilesByMediaType(types ...string) ([]file.Location, error) {
diff --git a/syft/internal/fileresolver/container_image_model_test.go b/syft/internal/fileresolver/container_image_model_test.go
index bd8c2efb7..d4a2084b5 100644
--- a/syft/internal/fileresolver/container_image_model_test.go
+++ b/syft/internal/fileresolver/container_image_model_test.go
@@ -60,7 +60,8 @@ func TestOCIModelResolver_FilesByMediaType(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- resolver := NewContainerImageModel(tempDir, test.layerFiles)
+ ref := ""
+ resolver := NewContainerImageModel(tempDir, test.layerFiles, ref)
locations, err := resolver.FilesByMediaType(test.patterns...)
require.NoError(t, err)
@@ -81,7 +82,8 @@ func TestOCIModelResolver_FileContentsByLocation(t *testing.T) {
digest: {TempPath: tempFile, MediaType: ggufLayerMediaType},
}
- resolver := NewContainerImageModel(tempDir, layerFiles)
+ ref := ""
+ resolver := NewContainerImageModel(tempDir, layerFiles, ref)
tests := []struct {
name string
diff --git a/syft/pkg/cataloger/ai/capabilities.yaml b/syft/pkg/cataloger/ai/capabilities.yaml
index fff9fa3a7..c7ad601df 100644
--- a/syft/pkg/cataloger/ai/capabilities.yaml
+++ b/syft/pkg/cataloger/ai/capabilities.yaml
@@ -44,3 +44,46 @@ catalogers:
default: true
evidence:
- GGUFFileHeader.MetadataKeyValuesHash
+ - ecosystem: ai # MANUAL
+ name: safetensors-cataloger # AUTO-GENERATED
+ type: generic # AUTO-GENERATED
+ source: # AUTO-GENERATED
+ file: syft/pkg/cataloger/ai/cataloger.go
+ function: NewSafeTensorsCataloger
+ selectors: # AUTO-GENERATED
+ - ai
+ - directory
+ - image
+ - ml
+ - model
+ - package
+ - safetensors
+ parsers: # AUTO-GENERATED structure
+ - function: parseSafeTensorsFile
+ detector: # AUTO-GENERATED
+ method: glob # AUTO-GENERATED
+ criteria: # AUTO-GENERATED
+ - '**/*.safetensors'
+ metadata_types: # AUTO-GENERATED
+ - pkg.SafeTensorsModelInfo
+ package_types: # AUTO-GENERATED
+ - model
+ json_schema_types: # AUTO-GENERATED
+ - SafetensorsModelInfo
+ capabilities: # MANUAL - preserved across regeneration
+ - name: license
+ default: true
+ - name: dependency.depth
+ default: []
+ - name: dependency.edges
+ default: ""
+ - name: dependency.kinds
+ default: []
+ - name: package_manager.files.listing
+ default: false
+ - name: package_manager.files.digests
+ default: false
+ - name: package_manager.package_integrity_hash
+ default: true
+ evidence:
+ - SafeTensorsModelInfo.MetadataHash
diff --git a/syft/pkg/cataloger/ai/cataloger.go b/syft/pkg/cataloger/ai/cataloger.go
index d566edba2..5b7a75882 100644
--- a/syft/pkg/cataloger/ai/cataloger.go
+++ b/syft/pkg/cataloger/ai/cataloger.go
@@ -1,6 +1,6 @@
/*
Package ai provides concrete Cataloger implementations for AI artifacts and machine learning models,
-including support for GGUF (GPT-Generated Unified Format) model files.
+including support for GGUF (GPT-Generated Unified Format) and SafeTensors model files.
*/
package ai
@@ -10,8 +10,9 @@ import (
)
const (
- catalogerName = "gguf-cataloger"
- ggufLayerMediaType = "application/vnd.docker.ai*"
+ catalogerName = "gguf-cataloger"
+ ggufLayerMediaType = "application/vnd.docker.ai*"
+ safeTensorsCatalogerName = "safetensors-cataloger"
)
// NewGGUFCataloger returns a new cataloger instance for GGUF model files.
@@ -23,3 +24,26 @@ func NewGGUFCataloger() pkg.Cataloger {
WithParserByMediaType(parseGGUFModel, ggufLayerMediaType).
WithProcessors(ggufMergeProcessor)
}
+
+// NewSafeTensorsCataloger returns a cataloger for SafeTensors model files,
+// covering three discovery paths:
+// - **/*.safetensors files (single-file models and individual shards;
+// header-only parse)
+// - application/vnd.docker.ai.model.config.v0.1+json / v0.2+json OCI layers
+// (Docker Model Runner artifacts whose config advertises format=="safetensors")
+// - application/vnd.docker.ai.safetensors OCI layers (per-shard JSON headers,
+// fetched as a prefix by the OCI model source; emitted as nameless
+// packages and merged into the config-derived package as Parts)
+//
+// model.safetensors.index.json files are intentionally not parsed today: the
+// index describes how tensors map to shards but contributes no metadata the
+// cataloger can't derive from the shard headers themselves. If a model is
+// distributed as just an index.json with no accompanying shard files, the
+// cataloger emits nothing for that directory.
+func NewSafeTensorsCataloger() pkg.Cataloger {
+ return generic.NewCataloger(safeTensorsCatalogerName).
+ WithParserByGlobs(parseSafeTensorsFile, "**/*.safetensors").
+ WithParserByMediaType(parseSafeTensorsOCIConfig, dockerAIModelConfigMediaTypes...).
+ WithParserByMediaType(parseSafeTensorsOCILayer, dockerAISafeTensorsMediaType).
+ WithResolvingProcessors(safeTensorsMergeProcessor)
+}
diff --git a/syft/pkg/cataloger/ai/gguf_processor.go b/syft/pkg/cataloger/ai/gguf_processor.go
new file mode 100644
index 000000000..fe31e3f8b
--- /dev/null
+++ b/syft/pkg/cataloger/ai/gguf_processor.go
@@ -0,0 +1,56 @@
+package ai
+
+import (
+ "github.com/anchore/syft/syft/artifact"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// ggufMergeProcessor consolidates multiple GGUF packages into a single package
+// representing the AI model. When scanning OCI images with multiple layers,
+// each layer may produce a separate package. This processor finds the package
+// with a name and merges metadata from nameless packages into its GGUFFileParts field.
+// Only packages with a non-empty name are returned in the final result.
+func ggufMergeProcessor(pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
+ if err != nil {
+ return pkgs, rels, err
+ }
+ if len(pkgs) == 0 {
+ return pkgs, rels, err
+ }
+
+ // Separate packages with names from those without
+ var namedPkgs []pkg.Package
+ var namelessHeaders []pkg.GGUFFileHeader
+
+ for _, p := range pkgs {
+ if p.Name != "" {
+ namedPkgs = append(namedPkgs, p)
+ } else {
+ if header, ok := p.Metadata.(pkg.GGUFFileHeader); ok {
+ // We do not want a kv hash for nameless headers
+ header.MetadataKeyValuesHash = ""
+ namelessHeaders = append(namelessHeaders, header)
+ }
+ }
+ }
+
+ // If there are no named packages, return nothing
+ if len(namedPkgs) == 0 {
+ return nil, rels, err
+ }
+
+ // merge nameless headers into a single named package;
+ // if there are multiple named packages, return them without trying to merge headers.
+ // we cannot determine which nameless headers belong to which package
+ // this is because the order we receive the gguf headers in is not guaranteed
+ // to match the layer order in the original oci image
+ if len(namedPkgs) == 1 && len(namelessHeaders) > 0 {
+ winner := &namedPkgs[0]
+ if header, ok := winner.Metadata.(pkg.GGUFFileHeader); ok {
+ header.Parts = namelessHeaders
+ winner.Metadata = header
+ }
+ }
+
+ return namedPkgs, rels, err
+}
diff --git a/syft/pkg/cataloger/ai/huggingface.go b/syft/pkg/cataloger/ai/huggingface.go
new file mode 100644
index 000000000..6c333835b
--- /dev/null
+++ b/syft/pkg/cataloger/ai/huggingface.go
@@ -0,0 +1,93 @@
+package ai
+
+import (
+ "bytes"
+
+ "gopkg.in/yaml.v3"
+
+ "github.com/anchore/syft/internal/log"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// hfConfig is a minimal projection of Hugging Face config.json fields.
+type hfConfig struct {
+ Architectures []string `json:"architectures"`
+ NameOrPath string `json:"_name_or_path"`
+}
+
+func applyHFConfig(md *pkg.SafeTensorsModelInfo, cfg *hfConfig) {
+ if md.Architecture == "" && len(cfg.Architectures) > 0 {
+ md.Architecture = cfg.Architectures[0]
+ }
+}
+
+// readmeFrontmatter holds the subset of YAML frontmatter fields we extract.
+type readmeFrontmatter struct {
+ License string `yaml:"license"`
+ BaseModel []string `yaml:"base_model"`
+}
+
+type licenseFrontmatter struct {
+ SPDXID string `yaml:"spdx-id"`
+}
+
+// extractFrontmatterBlock returns the YAML bytes between the first and second
+// "---" delimiters of a file
+func extractFrontmatterBlock(buf []byte) []byte {
+ trimmed := bytes.TrimLeft(buf, "\xef\xbb\xbf \t\r\n")
+ if !bytes.HasPrefix(trimmed, []byte("---")) {
+ return nil
+ }
+ rest := trimmed[3:]
+ if i := bytes.IndexByte(rest, '\n'); i >= 0 {
+ rest = rest[i+1:]
+ }
+ block, _, found := bytes.Cut(rest, []byte("\n---"))
+ if !found {
+ return nil
+ }
+ return block
+}
+
+// parseFrontmatter decodes a Hugging Face model card YAML frontmatter block
+// and returns the license and base_model fields.
+func parseFrontmatter(buf []byte) *readmeFrontmatter {
+ block := extractFrontmatterBlock(buf)
+ if block == nil {
+ return nil
+ }
+
+ var raw struct {
+ License string `yaml:"license"`
+ BaseModel yaml.Node `yaml:"base_model"`
+ }
+ if err := yaml.Unmarshal(block, &raw); err != nil {
+ log.Debugf("failed to parse README frontmatter: %v", err)
+ return nil
+ }
+
+ fm := readmeFrontmatter{License: raw.License}
+ switch raw.BaseModel.Kind {
+ case yaml.ScalarNode:
+ if raw.BaseModel.Value != "" {
+ fm.BaseModel = []string{raw.BaseModel.Value}
+ }
+ case yaml.SequenceNode:
+ _ = raw.BaseModel.Decode(&fm.BaseModel)
+ }
+ return &fm
+}
+
+// parseLicenseFrontmatter returns the producer-declared SPDX identifier
+func parseLicenseFrontmatter(buf []byte) string {
+ block := extractFrontmatterBlock(buf)
+ if block == nil {
+ return ""
+ }
+ var fm licenseFrontmatter
+ if err := yaml.Unmarshal(block, &fm); err != nil {
+ log.Debugf("failed to parse license frontmatter: %v", err)
+ return ""
+ }
+ return fm.SPDXID
+}
diff --git a/syft/pkg/cataloger/ai/identity_dir.go b/syft/pkg/cataloger/ai/identity_dir.go
new file mode 100644
index 000000000..eaa0d1efa
--- /dev/null
+++ b/syft/pkg/cataloger/ai/identity_dir.go
@@ -0,0 +1,93 @@
+package ai
+
+import (
+ "context"
+ "encoding/json"
+ "io"
+ "path"
+
+ "github.com/anchore/syft/internal"
+ "github.com/anchore/syft/internal/log"
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// resolveSafeTensorsDirIdentity handles the directory-scan case for safe tensors
+// find config.json beside the model files (walking up parent directories to the
+// scanned source root if no sibling exists) and a sibling README.md. It returns
+// the group's name candidates, resolved licenses, and supporting evidence.
+func resolveSafeTensorsDirIdentity(ctx context.Context, resolver file.Resolver, dir string, md *pkg.SafeTensorsModelInfo) safeTensorsIdentity {
+ id := safeTensorsIdentity{fallbackName: safeTensorsDirName(dir)}
+
+ if loc, cfg := findDirHFConfig(resolver, dir); cfg != nil {
+ applyHFConfig(md, cfg)
+ id.nameOrPath = cfg.NameOrPath
+ id.supporting = append(id.supporting, *loc)
+ }
+
+ if loc, fm := readDirReadmeFrontmatter(resolver, path.Join(dir, "README.md")); fm != nil {
+ if fm.License != "" {
+ id.licenses = pkg.NewLicensesFromValuesWithContext(ctx, fm.License)
+ }
+ if id.nameOrPath == "" && len(fm.BaseModel) > 0 {
+ id.nameOrPath = fm.BaseModel[0]
+ }
+ id.supporting = append(id.supporting, *loc)
+ }
+ return id
+}
+
+// findDirHFConfig looks for a config.json beside the model files
+func findDirHFConfig(resolver file.Resolver, dir string) (*file.Location, *hfConfig) {
+ for {
+ if loc, cfg := readDirHFConfig(resolver, path.Join(dir, "config.json")); cfg != nil {
+ return loc, cfg
+ }
+ parent := path.Dir(dir)
+ if parent == dir {
+ return nil, nil // reached the source root
+ }
+ dir = parent
+ }
+}
+
+func readDirHFConfig(resolver file.Resolver, p string) (*file.Location, *hfConfig) {
+ locations, err := resolver.FilesByPath(p)
+ if err != nil || len(locations) == 0 {
+ return nil, nil
+ }
+ rc, err := resolver.FileContentsByLocation(locations[0])
+ if err != nil {
+ return nil, nil
+ }
+ defer internal.CloseAndLogError(rc, p)
+
+ var cfg hfConfig
+ if err := json.NewDecoder(rc).Decode(&cfg); err != nil {
+ log.Debugf("failed to decode %s: %v", p, err)
+ return nil, nil
+ }
+ return &locations[0], &cfg
+}
+
+func readDirReadmeFrontmatter(resolver file.Resolver, p string) (*file.Location, *readmeFrontmatter) {
+ locations, err := resolver.FilesByPath(p)
+ if err != nil || len(locations) == 0 {
+ return nil, nil
+ }
+ rc, err := resolver.FileContentsByLocation(locations[0])
+ if err != nil {
+ return nil, nil
+ }
+ defer internal.CloseAndLogError(rc, p)
+
+ buf, err := io.ReadAll(io.LimitReader(rc, 1024*1024))
+ if err != nil {
+ return nil, nil
+ }
+ fm := parseFrontmatter(buf)
+ if fm == nil {
+ return nil, nil
+ }
+ return &locations[0], fm
+}
diff --git a/syft/pkg/cataloger/ai/identity_oci.go b/syft/pkg/cataloger/ai/identity_oci.go
new file mode 100644
index 000000000..6209a3f8b
--- /dev/null
+++ b/syft/pkg/cataloger/ai/identity_oci.go
@@ -0,0 +1,173 @@
+package ai
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "path"
+
+ gcrname "github.com/google/go-containerregistry/pkg/name"
+
+ "github.com/anchore/syft/internal"
+ "github.com/anchore/syft/internal/log"
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/pkg"
+ "github.com/anchore/syft/syft/pkg/cataloger/internal/licenses"
+)
+
+// resolveSafeTensorsOCIIdentity handles the OCI-artifact case
+func resolveSafeTensorsOCIIdentity(ctx context.Context, resolver file.Resolver, md *pkg.SafeTensorsModelInfo) safeTensorsIdentity {
+ ociResolver, ok := resolver.(file.OCIMediaTypeResolver)
+ if !ok {
+ return safeTensorsIdentity{}
+ }
+
+ modelFileLocs, err := ociResolver.FilesByMediaType(dockerAIModelFileMediaType)
+ if err != nil {
+ log.Debugf("failed to list docker AI model-file layers: %v", err)
+ }
+
+ // Collect config / readme candidates separately so the layer-iteration order
+ // returned by the resolver doesn't decide the precedence.
+ var configName, readmeName, readmeLicense string
+ var supporting []file.Location
+ for _, loc := range modelFileLocs {
+ cfg, fm := classifyOCIModelFileLayer(resolver, loc)
+ switch {
+ case cfg != nil:
+ applyHFConfig(md, cfg)
+ if configName == "" {
+ configName = cfg.NameOrPath
+ }
+ supporting = append(supporting, loc)
+ case fm != nil:
+ if readmeLicense == "" {
+ readmeLicense = fm.License
+ }
+ if readmeName == "" && len(fm.BaseModel) > 0 {
+ readmeName = fm.BaseModel[0]
+ }
+ supporting = append(supporting, loc)
+ }
+ }
+
+ // Precedence: config.json _name_or_path > README base_model.
+ nameOrPath := configName
+ if nameOrPath == "" {
+ nameOrPath = readmeName
+ }
+
+ id := safeTensorsIdentity{
+ nameOrPath: nameOrPath,
+ fallbackName: ociImageRefBasename(resolver),
+ supporting: supporting,
+ }
+
+ // License precedence: a dedicated vnd.docker.ai.license layer is a
+ // outranks the free-text license field in a model card's README frontmatter.
+ licLocs, err := ociResolver.FilesByMediaType(dockerAILicenseMediaType)
+ if err != nil {
+ log.Debugf("failed to list docker AI license layers: %v", err)
+ }
+ switch {
+ case len(licLocs) > 0:
+ id.licenses = identifyLicenseLayers(ctx, resolver, licLocs)
+ id.supporting = append(id.supporting, licLocs...)
+ case readmeLicense != "":
+ id.licenses = pkg.NewLicensesFromValuesWithContext(ctx, readmeLicense)
+ }
+
+ return id
+}
+
+// ociImageReferencer is the minimal capability ociImageRefBasename needs: a
+// resolver that can surface the OCI image reference it was built from. It is
+// kept local to this package (rather than exported from the file package) so the
+// assertion stays with its only consumer.
+type ociImageReferencer interface {
+ ImageReference() string
+}
+
+func ociImageRefBasename(resolver file.Resolver) string {
+ // TODO: we don't think this approach is generalizable quite yet, but we really do need this information.
+ // (Ideally we should be NOT be type asserting on the file resolver directly).
+ info, ok := resolver.(ociImageReferencer)
+ if !ok {
+ return ""
+ }
+ ref := info.ImageReference()
+ if ref == "" {
+ return ""
+ }
+ parsed, err := gcrname.ParseReference(ref)
+ if err != nil {
+ log.Debugf("failed to parse OCI ref %q: %v", ref, err)
+ return ""
+ }
+ return path.Base(parsed.Context().RepositoryStr())
+}
+
+// classifyOCIModelFileLayer reads up to 4 MiB of a model.file layer and decodes
+// it as either an HF config.json or a README model card's YAML frontmatter,
+// based on its leading bytes. It returns whichever it recognized; both are nil
+// when the layer is neither (or fails to decode). The caller owns precedence and
+// metadata enrichment.
+func classifyOCIModelFileLayer(resolver file.Resolver, loc file.Location) (*hfConfig, *readmeFrontmatter) {
+ rc, err := resolver.FileContentsByLocation(loc)
+ if err != nil {
+ return nil, nil
+ }
+ defer internal.CloseAndLogError(rc, loc.RealPath)
+
+ buf, err := io.ReadAll(io.LimitReader(rc, 4*1024*1024))
+ if err != nil {
+ return nil, nil
+ }
+ trimmed := bytes.TrimLeft(buf, "\xef\xbb\xbf \t\r\n")
+ switch {
+ case bytes.HasPrefix(trimmed, []byte("---")):
+ return nil, parseFrontmatter(buf)
+ case bytes.HasPrefix(trimmed, []byte("{")):
+ var cfg hfConfig
+ if err := json.Unmarshal(buf, &cfg); err != nil {
+ return nil, nil
+ }
+ return &cfg, nil
+ }
+ return nil, nil
+}
+
+// identifyLicenseLayers turns Docker AI license-layer locations into pkg.License values.
+func identifyLicenseLayers(ctx context.Context, resolver file.Resolver, locs []file.Location) []pkg.License {
+ var out []pkg.License
+ var scanFallback []file.Location
+ for i := range locs {
+ loc := locs[i]
+ if spdx := readLicenseSPDXIDFromFrontmatter(resolver, loc); spdx != "" {
+ out = append(out, pkg.NewLicenseFromFieldsWithContext(ctx, spdx, "", &loc))
+ continue
+ }
+ scanFallback = append(scanFallback, loc)
+ }
+ if len(scanFallback) > 0 {
+ out = append(out, licenses.FindAtLocations(ctx, resolver, scanFallback...)...)
+ }
+ return out
+}
+
+// readLicenseSPDXIDFromFrontmatter reads a bounded prefix of a license-layer
+// blob and returns the spdx-id declared in its YAML frontmatter
+func readLicenseSPDXIDFromFrontmatter(resolver file.Resolver, loc file.Location) string {
+ rc, err := resolver.FileContentsByLocation(loc)
+ if err != nil {
+ return ""
+ }
+ defer internal.CloseAndLogError(rc, loc.RealPath)
+
+ buf, err := io.ReadAll(io.LimitReader(rc, 64*1024))
+ if err != nil {
+ return ""
+ }
+ return parseLicenseFrontmatter(buf)
+}
diff --git a/syft/pkg/cataloger/ai/merge.go b/syft/pkg/cataloger/ai/merge.go
new file mode 100644
index 000000000..a8d2b4e74
--- /dev/null
+++ b/syft/pkg/cataloger/ai/merge.go
@@ -0,0 +1,139 @@
+package ai
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+
+ "github.com/cespare/xxhash/v2"
+
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// mergeSafeTensorsGroup folds a group's per-member metadata into a single package.
+func mergeSafeTensorsGroup(members []pkg.Package) pkg.Package {
+ locSet := unionLocations(members)
+ aggregates, shards := bucketSafeTensorsMembers(members)
+
+ merged := pkg.SafeTensorsModelInfo{Format: "safetensors"}
+ mergeAggregatesInto(&merged, aggregates)
+ shardTensorTotal, hashes := mergeShardsInto(&merged, shards)
+
+ // Keep merged UserMetadata globally key-sorted so the SBOM is stable
+ sort.Slice(merged.UserMetadata, func(i, j int) bool {
+ return merged.UserMetadata[i].Key < merged.UserMetadata[j].Key
+ })
+
+ if merged.TensorCount == 0 {
+ merged.TensorCount = shardTensorTotal
+ }
+ if merged.ShardCount == 0 {
+ if len(shards) > 0 {
+ merged.ShardCount = len(shards)
+ } else {
+ merged.ShardCount = 1
+ }
+ }
+ merged.MetadataHash = rollupHash(hashes)
+
+ // Parts only carry value for multi-shard models; for a single shard the
+ // outer view already exposes every per-shard field.
+ if len(shards) > 1 {
+ parts := append([]pkg.SafeTensorsModelInfo(nil), shards...)
+ sort.Slice(parts, func(i, j int) bool {
+ return parts[i].MetadataHash < parts[j].MetadataHash
+ })
+ merged.Parts = parts
+ }
+
+ return pkg.Package{
+ Locations: locSet,
+ Type: pkg.ModelPkg,
+ Metadata: merged,
+ }
+}
+
+func mergeAggregatesInto(merged *pkg.SafeTensorsModelInfo, aggregates []pkg.SafeTensorsModelInfo) {
+ for _, a := range aggregates {
+ if merged.TensorCount == 0 {
+ merged.TensorCount = a.TensorCount
+ }
+ firstNonEmpty(&merged.TotalSize, a.TotalSize)
+ firstNonEmpty(&merged.Quantization, a.Quantization)
+ }
+}
+
+// mergeShardsInto folds the per-shard header metadata into merged, returning
+// the summed shard TensorCount and the list of non-empty per-shard hashes for
+// the rollup. Shards carry only the content-derived fields (Quantization,
+// Parameters, UserMetadata), so those are the only fields folded in here.
+// TensorCount and Parameters are summed because each shard holds a distinct
+// slice of the model; Quantization takes the first value since all shards share
+// one precision.
+func mergeShardsInto(merged *pkg.SafeTensorsModelInfo, shards []pkg.SafeTensorsModelInfo) (shardTensorTotal uint64, hashes []string) {
+ seenKV := map[string]bool{}
+ for _, s := range shards {
+ shardTensorTotal += s.TensorCount
+ merged.Parameters += s.Parameters
+ firstNonEmpty(&merged.Quantization, s.Quantization)
+ for _, kv := range s.UserMetadata {
+ if seenKV[kv.Key] {
+ continue
+ }
+ seenKV[kv.Key] = true
+ merged.UserMetadata = append(merged.UserMetadata, kv)
+ }
+ if s.MetadataHash != "" {
+ hashes = append(hashes, s.MetadataHash)
+ }
+ }
+ return shardTensorTotal, hashes
+}
+
+func firstNonEmpty(dst *string, v string) {
+ if *dst == "" {
+ *dst = v
+ }
+}
+
+// unionLocations gathers every location from every member into a single set.
+func unionLocations(members []pkg.Package) file.LocationSet {
+ out := file.NewLocationSet()
+ for _, m := range members {
+ for _, l := range m.Locations.ToSlice() {
+ out.Add(l)
+ }
+ }
+ return out
+}
+
+// bucketSafeTensorsMembers splits group members into aggregate-flavored entries
+// (no MetadataHash — Docker AI config blob or sharded index) and shard-flavored
+// entries (carry a content-derived MetadataHash from a header parser).
+func bucketSafeTensorsMembers(members []pkg.Package) (aggregates, shards []pkg.SafeTensorsModelInfo) {
+ for _, m := range members {
+ md, ok := m.Metadata.(pkg.SafeTensorsModelInfo)
+ if !ok {
+ continue
+ }
+ if md.MetadataHash != "" {
+ shards = append(shards, md)
+ continue
+ }
+ aggregates = append(aggregates, md)
+ }
+ return aggregates, shards
+}
+
+func rollupHash(hashes []string) string {
+ if len(hashes) == 0 {
+ return ""
+ }
+ if len(hashes) == 1 {
+ return hashes[0]
+ }
+ sorted := append([]string(nil), hashes...)
+ sort.Strings(sorted)
+ return fmt.Sprintf("%016x", xxhash.Sum64String(strings.Join(sorted, "|")))
+}
diff --git a/syft/pkg/cataloger/ai/merge_test.go b/syft/pkg/cataloger/ai/merge_test.go
new file mode 100644
index 000000000..7276d8896
--- /dev/null
+++ b/syft/pkg/cataloger/ai/merge_test.go
@@ -0,0 +1,166 @@
+package ai
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// stPkg builds a model package carrying the given metadata, with each path
+// recorded as a primary-evidence location.
+func stPkg(md pkg.SafeTensorsModelInfo, paths ...string) pkg.Package {
+ locs := make([]file.Location, 0, len(paths))
+ for _, p := range paths {
+ locs = append(locs, file.NewLocation(p).WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
+ }
+ return pkg.Package{Type: pkg.ModelPkg, Metadata: md, Locations: file.NewLocationSet(locs...)}
+}
+
+// shardMeta is a content-derived shard entry: it carries a MetadataHash, which is
+// what marks a group member as a shard (vs. a hash-less aggregate config blob).
+func shardMeta(hash string, tensorCount uint64) pkg.SafeTensorsModelInfo {
+ return pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: tensorCount,
+ Quantization: "BF16",
+ Parameters: 1000,
+ MetadataHash: hash,
+ }
+}
+
+// TestMergeSafeTensorsGroup exercises the rollup contract directly (the cataloger
+// tests cover it only as a side effect of the merge processor). It locks how a
+// group's per-member metadata folds into one package: tensor-count summing,
+// aggregate-over-shard field precedence, UserMetadata dedup + sorting, Parts
+// rollup, ShardCount derivation, and the content MetadataHash rollup.
+func TestMergeSafeTensorsGroup(t *testing.T) {
+ t.Run("single shard: hash passes through, ShardCount 1, no Parts", func(t *testing.T) {
+ out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 5), "/m/a.safetensors")})
+
+ md := out.Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, pkg.ModelPkg, out.Type)
+ assert.Equal(t, 1, md.ShardCount)
+ assert.Equal(t, uint64(5), md.TensorCount)
+ assert.Equal(t, "aaaa", md.MetadataHash, "a single shard's hash passes through unchanged")
+ assert.Nil(t, md.Parts, "single-shard models do not populate Parts")
+ })
+
+ t.Run("multi-shard: tensors summed, Parts sorted by hash, rollup is order-independent", func(t *testing.T) {
+ in := []pkg.Package{
+ stPkg(shardMeta("cccc", 3), "/m/c.safetensors"),
+ stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"),
+ stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"),
+ }
+ out := mergeSafeTensorsGroup(in)
+
+ md := out.Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, 3, md.ShardCount)
+ assert.Equal(t, uint64(9), md.TensorCount, "tensor counts are summed across shards")
+ assert.Equal(t, uint64(3000), md.Parameters, "parameter counts are summed across shards")
+ require.Len(t, md.Parts, 3)
+ assert.Equal(t,
+ []string{"aaaa", "bbbb", "cccc"},
+ []string{md.Parts[0].MetadataHash, md.Parts[1].MetadataHash, md.Parts[2].MetadataHash},
+ "Parts are sorted by metadata hash",
+ )
+ assert.Equal(t, rollupHash([]string{"aaaa", "bbbb", "cccc"}), md.MetadataHash)
+
+ // the rollup hash must not depend on the order members arrive in
+ shuffled := []pkg.Package{
+ stPkg(shardMeta("bbbb", 3), "/m/b.safetensors"),
+ stPkg(shardMeta("cccc", 3), "/m/c.safetensors"),
+ stPkg(shardMeta("aaaa", 3), "/m/a.safetensors"),
+ }
+ out2 := mergeSafeTensorsGroup(shuffled)
+ assert.Equal(t, md.MetadataHash, out2.Metadata.(pkg.SafeTensorsModelInfo).MetadataHash)
+ })
+
+ t.Run("aggregate fields win over shard-derived fields", func(t *testing.T) {
+ // an aggregate (OCI config blob) carries no MetadataHash but declares the
+ // authoritative totals.
+ aggregate := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: 999,
+ TotalSize: "5.00GB",
+ Quantization: "Q4_K_M",
+ }
+ in := []pkg.Package{
+ stPkg(aggregate, "/"),
+ stPkg(shardMeta("aaaa", 3), "/"),
+ stPkg(shardMeta("bbbb", 3), "/"),
+ }
+ out := mergeSafeTensorsGroup(in)
+
+ md := out.Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, uint64(999), md.TensorCount, "aggregate TensorCount is authoritative; shard counts are not summed in")
+ assert.Equal(t, "5.00GB", md.TotalSize)
+ assert.Equal(t, uint64(2000), md.Parameters, "parameters are always measured from the shards (summed), not taken from the aggregate")
+ assert.Equal(t, "Q4_K_M", md.Quantization, "aggregate quantization wins over the shard dtype")
+ assert.Equal(t, 2, md.ShardCount, "ShardCount comes from the number of shards, not the aggregate")
+ assert.Equal(t, rollupHash([]string{"aaaa", "bbbb"}), md.MetadataHash, "the content hash still rolls up the shard hashes")
+ })
+
+ t.Run("aggregate-only group: ShardCount 1, empty hash, no Parts", func(t *testing.T) {
+ aggregate := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 42, TotalSize: "1.00GB"}
+ out := mergeSafeTensorsGroup([]pkg.Package{stPkg(aggregate, "/")})
+
+ md := out.Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, uint64(42), md.TensorCount)
+ assert.Equal(t, 1, md.ShardCount, "a group with no shards still reports a single shard")
+ assert.Equal(t, "", md.MetadataHash, "there are no shard hashes to roll up")
+ assert.Nil(t, md.Parts)
+ })
+
+ t.Run("UserMetadata: keys merged and sorted, first value wins on conflict", func(t *testing.T) {
+ // keys are intentionally unsorted within each shard so the assertion proves
+ // the merge re-sorts globally; "format" appears in both shards so dedup
+ // precedence (first wins) is exercised too.
+ s1 := shardMeta("aaaa", 1)
+ s1.UserMetadata = pkg.KeyValues{{Key: "format", Value: "pt"}, {Key: "author", Value: "alice"}}
+ s2 := shardMeta("bbbb", 1)
+ s2.UserMetadata = pkg.KeyValues{{Key: "format", Value: "gguf"}, {Key: "license", Value: "mit"}}
+
+ out := mergeSafeTensorsGroup([]pkg.Package{stPkg(s1, "/m/a.safetensors"), stPkg(s2, "/m/b.safetensors")})
+
+ md := out.Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, pkg.KeyValues{
+ {Key: "author", Value: "alice"},
+ {Key: "format", Value: "pt"}, // first shard's value wins over s2's "gguf"
+ {Key: "license", Value: "mit"},
+ }, md.UserMetadata)
+ })
+
+ t.Run("members without safetensors metadata are ignored in the rollup", func(t *testing.T) {
+ notST := pkg.Package{
+ Type: pkg.ModelPkg,
+ Metadata: pkg.GGUFFileHeader{},
+ Locations: file.NewLocationSet(file.NewLocation("/m/x.gguf")),
+ }
+ out := mergeSafeTensorsGroup([]pkg.Package{stPkg(shardMeta("aaaa", 2), "/m/a.safetensors"), notST})
+
+ md := out.Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, uint64(2), md.TensorCount, "only the safetensors shard contributes")
+ assert.Equal(t, 1, md.ShardCount)
+ assert.Equal(t, "aaaa", md.MetadataHash)
+ })
+}
+
+// TestRollupHash locks the cross-source content-fingerprint rollup: empty input
+// yields no hash, a lone shard's hash passes through unchanged (so a single-shard
+// model fingerprints identically across directory and OCI sources), and multiple
+// shards fold into one order-independent digest.
+func TestRollupHash(t *testing.T) {
+ assert.Equal(t, "", rollupHash(nil), "no hashes → empty")
+ assert.Equal(t, "solo", rollupHash([]string{"solo"}), "a single hash passes through unchanged")
+
+ ab := rollupHash([]string{"a", "b"})
+ ba := rollupHash([]string{"b", "a"})
+ assert.Equal(t, ab, ba, "the rollup is independent of input order")
+ assert.Len(t, ab, 16, "a multi-hash rollup is a 16-char xxhash")
+ assert.NotEqual(t, "a", ab)
+ assert.NotEqual(t, "b", ab)
+}
diff --git a/syft/pkg/cataloger/ai/naming.go b/syft/pkg/cataloger/ai/naming.go
new file mode 100644
index 000000000..ed2571dcb
--- /dev/null
+++ b/syft/pkg/cataloger/ai/naming.go
@@ -0,0 +1,22 @@
+package ai
+
+import "path"
+
+// pickSafeTensorsName implements the documented naming precedence chain:
+func pickSafeTensorsName(nameOrPath, fallbackName string) string {
+ if nameOrPath != "" {
+ return path.Base(nameOrPath)
+ }
+ return fallbackName
+}
+
+// safeTensorsDirName returns the directory-scan naming fallback: the base name
+// of the group's parent directory (the group key is already that directory).
+func safeTensorsDirName(directory string) string {
+ base := path.Base(directory)
+ switch base {
+ case "/", ".", "":
+ return ""
+ }
+ return base
+}
diff --git a/syft/pkg/cataloger/ai/package.go b/syft/pkg/cataloger/ai/package.go
index 67c6570ae..3abb42343 100644
--- a/syft/pkg/cataloger/ai/package.go
+++ b/syft/pkg/cataloger/ai/package.go
@@ -20,3 +20,17 @@ func newGGUFPackage(metadata *pkg.GGUFFileHeader, modelName, version, license st
return p
}
+
+// newSafeTensorsPackage creates a SafeTensors package with the given metadata
+// and locations. Name and Licenses are intentionally not set here and done at the processor level
+func newSafeTensorsPackage(metadata *pkg.SafeTensorsModelInfo, locations ...file.Location) pkg.Package {
+ p := pkg.Package{
+ Locations: file.NewLocationSet(locations...),
+ Type: pkg.ModelPkg,
+ Metadata: *metadata,
+ // PURL is intentionally not set: package-url has not yet finalized ML model support.
+ }
+ p.SetID()
+
+ return p
+}
diff --git a/syft/pkg/cataloger/ai/parse_safetensors.go b/syft/pkg/cataloger/ai/parse_safetensors.go
new file mode 100644
index 000000000..bc1ff1402
--- /dev/null
+++ b/syft/pkg/cataloger/ai/parse_safetensors.go
@@ -0,0 +1,202 @@
+package ai
+
+import (
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "github.com/cespare/xxhash/v2"
+
+ "github.com/anchore/syft/syft/pkg"
+)
+
+// SafeTensors file format: [8 bytes u64 LE header size] [N bytes JSON header] [tensor data].
+// Reference: https://github.com/huggingface/safetensors#format
+const (
+ maxSafeTensorsHeaderSize = 100 * 1024 * 1024 // 100MB ceiling on header JSON to prevent OOM
+)
+
+// safeTensorsHeader is the decoded JSON header. Tensor entries live alongside a
+// reserved "__metadata__" key holding a string-to-string producer map. We decode
+// tensor entries into a generic map so we can iterate and count without a fixed
+// schema for every field.
+type safeTensorsHeader struct {
+ metadata map[string]string
+ tensors map[string]safeTensorsEntry
+}
+
+// safeTensorsEntry describes a single tensor within the header JSON.
+type safeTensorsEntry struct {
+ DType string `json:"dtype"`
+ Shape []int64 `json:"shape"`
+ DataOffsets []int64 `json:"data_offsets"`
+}
+
+// readSafeTensorsHeader reads and parses the JSON header from a .safetensors
+// file (the leading `[8-byte LE length] [length bytes of JSON]` block) and
+// returns the decoded header.
+func readSafeTensorsHeader(r io.Reader) (*safeTensorsHeader, error) {
+ var lenBuf [8]byte
+ if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
+ return nil, fmt.Errorf("failed to read header length: %w", err)
+ }
+ headerLen := binary.LittleEndian.Uint64(lenBuf[:])
+ if headerLen == 0 {
+ return nil, fmt.Errorf("safetensors header length is zero")
+ }
+ if headerLen > maxSafeTensorsHeaderSize {
+ return nil, fmt.Errorf("safetensors header size %d exceeds maximum %d", headerLen, maxSafeTensorsHeaderSize)
+ }
+
+ // Read incrementally rather than pre-allocating headerLen up front
+ body, err := io.ReadAll(io.LimitReader(r, int64(headerLen)))
+ if err != nil {
+ return nil, fmt.Errorf("failed to read header body: %w", err)
+ }
+ if uint64(len(body)) != headerLen {
+ return nil, fmt.Errorf("safetensors header truncated: read %d of %d bytes", len(body), headerLen)
+ }
+
+ var raw map[string]json.RawMessage
+ if err := json.Unmarshal(body, &raw); err != nil {
+ return nil, fmt.Errorf("failed to decode safetensors header JSON: %w", err)
+ }
+
+ h := &safeTensorsHeader{tensors: make(map[string]safeTensorsEntry, len(raw))}
+ for key, val := range raw {
+ if key == "__metadata__" {
+ if err := json.Unmarshal(val, &h.metadata); err != nil {
+ return nil, fmt.Errorf("failed to decode __metadata__: %w", err)
+ }
+ continue
+ }
+ var entry safeTensorsEntry
+ if err := json.Unmarshal(val, &entry); err != nil {
+ // Not all entries must conform; skip anything we cannot decode rather than fail.
+ continue
+ }
+ h.tensors[key] = entry
+ }
+
+ return h, nil
+}
+
+// parameterCount sums the element counts across all tensors in the header.
+func (h *safeTensorsHeader) parameterCount() uint64 {
+ var total uint64
+ for _, t := range h.tensors {
+ count := uint64(1)
+ for _, dim := range t.Shape {
+ if dim <= 0 {
+ count = 0
+ break
+ }
+ count *= uint64(dim)
+ }
+ total += count
+ }
+ return total
+}
+
+// dominantDType returns the dtype that accounts for the largest fraction of parameters.
+// For mixed-precision models the "dominant" dtype is still a useful summary.
+func (h *safeTensorsHeader) dominantDType() string {
+ sizeByDType := make(map[string]uint64)
+ for _, t := range h.tensors {
+ count := uint64(1)
+ for _, dim := range t.Shape {
+ if dim <= 0 {
+ count = 0
+ break
+ }
+ count *= uint64(dim)
+ }
+ sizeByDType[t.DType] += count
+ }
+ var best string
+ var bestSize uint64
+ for dtype, size := range sizeByDType {
+ if size > bestSize || (size == bestSize && dtype < best) {
+ best = dtype
+ bestSize = size
+ }
+ }
+ return best
+}
+
+// metadataHash returns a stable xxhash64 over the logical tensor content
+// (name + dtype + shape) plus the __metadata__ map. Tensor keys are sorted to
+// keep the hash deterministic across producers.
+func (h *safeTensorsHeader) metadataHash() string {
+ type logicalEntry struct {
+ Name string `json:"name"`
+ DType string `json:"dtype"`
+ Shape []int64 `json:"shape"`
+ }
+ entries := make([]logicalEntry, 0, len(h.tensors))
+ for name, t := range h.tensors {
+ entries = append(entries, logicalEntry{Name: name, DType: t.DType, Shape: t.Shape})
+ }
+ sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
+
+ type hashInput struct {
+ Tensors []logicalEntry `json:"tensors"`
+ Metadata map[string]string `json:"metadata,omitempty"`
+ }
+ b, err := json.Marshal(hashInput{Tensors: entries, Metadata: h.metadata})
+ if err != nil {
+ return ""
+ }
+ return fmt.Sprintf("%016x", xxhash.Sum64(b))
+}
+
+// userMetadataKeyValues converts the safetensors __metadata__ map into a
+// KeyValues slice sorted by key, so SBOM output is stable across runs. Returns
+// nil for empty input (omitempty then drops the field).
+func userMetadataKeyValues(m map[string]string) pkg.KeyValues {
+ if len(m) == 0 {
+ return nil
+ }
+ keys := make([]string, 0, len(m))
+ for k := range m {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ out := make(pkg.KeyValues, 0, len(keys))
+ for _, k := range keys {
+ out = append(out, pkg.KeyValue{Key: k, Value: m[k]})
+ }
+ return out
+}
+
+// normalizeDType maps a safetensors/torch dtype label to an uppercase quantization
+// shorthand matching conventions used elsewhere in syft (e.g., BF16, F16, I8).
+func normalizeDType(dtype string) string {
+ switch strings.ToUpper(dtype) {
+ case "BF16":
+ return "BF16"
+ case "F16", "FP16", "FLOAT16", "HALF":
+ return "F16"
+ case "F32", "FP32", "FLOAT32", "FLOAT":
+ return "F32"
+ case "F64", "FP64", "FLOAT64", "DOUBLE":
+ return "F64"
+ case "I8", "INT8":
+ return "I8"
+ case "U8", "UINT8":
+ return "U8"
+ case "I16", "INT16":
+ return "I16"
+ case "I32", "INT32":
+ return "I32"
+ case "I64", "INT64":
+ return "I64"
+ case "BOOL":
+ return "BOOL"
+ default:
+ return strings.ToUpper(dtype)
+ }
+}
diff --git a/syft/pkg/cataloger/ai/parse_safetensors_model.go b/syft/pkg/cataloger/ai/parse_safetensors_model.go
new file mode 100644
index 000000000..a9a8f3c66
--- /dev/null
+++ b/syft/pkg/cataloger/ai/parse_safetensors_model.go
@@ -0,0 +1,48 @@
+package ai
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "github.com/anchore/syft/internal"
+ "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"
+)
+
+// parseSafeTensorsFile decodes the JSON header of a single .safetensors file
+// (also called once per shard for sharded models) and emits a nameless package
+// whose metadata is derived purely from the header bytes. Naming, license
+// resolution, sibling enrichment, and cross-shard rollup are all handled by
+// safeTensorsMergeProcessor.
+func parseSafeTensorsFile(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
+ defer internal.CloseAndLogError(reader, reader.Path())
+
+ header, err := readSafeTensorsHeader(&io.LimitedReader{R: reader, N: maxSafeTensorsHeaderSize + 8})
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read safetensors header: %w", err)
+ }
+
+ // ShardCount is intentionally not set here: the merge processor is the single
+ // owner of ShardCount and derives it from the number of shards in the group.
+ md := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: uint64(len(header.tensors)),
+ Parameters: header.parameterCount(),
+ Quantization: normalizeDType(header.dominantDType()),
+ UserMetadata: userMetadataKeyValues(header.metadata),
+ MetadataHash: header.metadataHash(),
+ }
+
+ p := newSafeTensorsPackage(
+ &md,
+ reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ )
+ return []pkg.Package{p}, nil, unknown.IfEmptyf([]pkg.Package{p}, "unable to parse safetensors file")
+}
+
+// integrity check
+var _ generic.Parser = parseSafeTensorsFile
diff --git a/syft/pkg/cataloger/ai/parse_safetensors_oci.go b/syft/pkg/cataloger/ai/parse_safetensors_oci.go
new file mode 100644
index 000000000..7fd9f3b3e
--- /dev/null
+++ b/syft/pkg/cataloger/ai/parse_safetensors_oci.go
@@ -0,0 +1,117 @@
+package ai
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "strings"
+
+ "github.com/anchore/syft/internal"
+ "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"
+)
+
+// Docker AI OCI media types used by Docker Model Runner artifacts.
+const (
+ dockerAIModelFileMediaType = "application/vnd.docker.ai.model.file"
+ dockerAILicenseMediaType = "application/vnd.docker.ai.license"
+ dockerAISafeTensorsMediaType = "application/vnd.docker.ai.safetensors"
+)
+
+// dockerAIModelConfigMediaTypes are the model-config schema versions this
+// cataloger understands. Versions are enumerated explicitly rather than matched
+// with a wildcard so that a future, potentially breaking, config schema is not
+// silently consumed; add a new version here only after confirming the fields we
+// parse still apply.
+var dockerAIModelConfigMediaTypes = []string{
+ "application/vnd.docker.ai.model.config.v0.1+json",
+ "application/vnd.docker.ai.model.config.v0.2+json",
+}
+
+// dockerAIModelConfig mirrors the JSON shape of the vnd.docker.ai.model.config
+// blob written by Docker Model Runner for AI artifacts. Only fields we use are
+// declared; unknown fields are ignored.
+type dockerAIModelConfig struct {
+ Config struct {
+ Format string `json:"format"`
+ Quantization string `json:"quantization"`
+ Size string `json:"size"`
+ SafeTensors struct {
+ TensorCount json.Number `json:"tensor_count"`
+ } `json:"safetensors"`
+ } `json:"config"`
+}
+
+// parseSafeTensorsOCIConfig decodes the Docker AI model-config blob
+func parseSafeTensorsOCIConfig(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
+ defer internal.CloseAndLogError(reader, reader.Path())
+
+ body, err := io.ReadAll(io.LimitReader(reader, 1024*1024))
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read docker AI model config: %w", err)
+ }
+
+ var cfg dockerAIModelConfig
+ if err := json.Unmarshal(body, &cfg); err != nil {
+ return nil, nil, fmt.Errorf("failed to decode docker AI model config: %w", err)
+ }
+
+ if !strings.EqualFold(cfg.Config.Format, "safetensors") {
+ return nil, nil, nil
+ }
+
+ // Parameters is intentionally not read from the config blob: we measure the
+ // true parameter count from the SafeTensors layer headers (parseSafeTensorsOCILayer)
+ // so OCI and directory scans of the same model agree, rather than trusting the
+ // producer-supplied label here.
+ md := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ Quantization: cfg.Config.Quantization,
+ TotalSize: cfg.Config.Size,
+ }
+ if n, err := cfg.Config.SafeTensors.TensorCount.Int64(); err == nil && n > 0 {
+ md.TensorCount = uint64(n)
+ }
+
+ p := newSafeTensorsPackage(
+ &md,
+ reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ )
+ return []pkg.Package{p}, nil, unknown.IfEmptyf([]pkg.Package{p}, "unable to parse docker AI safetensors config")
+}
+
+// parseSafeTensorsOCILayer decodes the JSON header of a SafeTensors weight
+// layer fetched from an OCI model artifact
+func parseSafeTensorsOCILayer(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
+ defer internal.CloseAndLogError(reader, reader.Path())
+
+ header, err := readSafeTensorsHeader(&io.LimitedReader{R: reader, N: maxSafeTensorsHeaderSize + 8})
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to read safetensors layer header: %w", err)
+ }
+
+ md := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: uint64(len(header.tensors)),
+ Parameters: header.parameterCount(),
+ Quantization: normalizeDType(header.dominantDType()),
+ UserMetadata: userMetadataKeyValues(header.metadata),
+ MetadataHash: header.metadataHash(),
+ }
+
+ p := newSafeTensorsPackage(
+ &md,
+ reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ )
+ return []pkg.Package{p}, nil, nil
+}
+
+// integrity checks
+var (
+ _ generic.Parser = parseSafeTensorsOCIConfig
+ _ generic.Parser = parseSafeTensorsOCILayer
+)
diff --git a/syft/pkg/cataloger/ai/parse_safetensors_test.go b/syft/pkg/cataloger/ai/parse_safetensors_test.go
new file mode 100644
index 000000000..56d50151a
--- /dev/null
+++ b/syft/pkg/cataloger/ai/parse_safetensors_test.go
@@ -0,0 +1,1202 @@
+package ai
+
+import (
+ "bytes"
+ "context"
+ "encoding/binary"
+ "encoding/json"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "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"
+)
+
+// buildSafeTensorsFile builds the on-disk bytes of a .safetensors file: an
+// 8-byte little-endian header length followed by the JSON header. Tensor data
+// is omitted because the parser only reads the header.
+func buildSafeTensorsFile(t *testing.T, metadata map[string]string, tensors map[string]safeTensorsEntry) []byte {
+ t.Helper()
+ raw := map[string]any{}
+ if metadata != nil {
+ raw["__metadata__"] = metadata
+ }
+ for name, entry := range tensors {
+ raw[name] = entry
+ }
+ body, err := json.Marshal(raw)
+ require.NoError(t, err)
+
+ out := make([]byte, 8+len(body))
+ binary.LittleEndian.PutUint64(out[:8], uint64(len(body)))
+ copy(out[8:], body)
+ return out
+}
+
+// TestSafeTensorsCataloger is the end-to-end `dir:` scan naming matrix: it walks
+// real on-disk directory trees and locks how a model package gets its name at
+// every depth a .safetensors file can appear (the scan root `./`, an immediate
+// child `./sometensor/`, and a grandchild `./dir/someothertensor/`).
+//
+// The naming precedence (owned by the merge processor's pickSafeTensorsName) is:
+// 1. config.json _name_or_path (path.Base applied), found beside the model or
+// by walking up parent directories to the scan root
+// 2. otherwise the model's immediate parent directory base name
+// → drop (no package) when neither yields a usable name
+//
+// Every model below is built from the same header bytes, so the header-derived
+// metadata (Quantization/Parameters/TensorCount/UserMetadata/MetadataHash) is
+// identical across rows and each row stays focused on naming.
+func TestSafeTensorsCataloger(t *testing.T) {
+ userMeta := map[string]string{"format": "pt"}
+ tensors := map[string]safeTensorsEntry{
+ "model.embed.weight": {DType: "BF16", Shape: []int64{1000, 16}, DataOffsets: []int64{0, 32000}},
+ "model.layer.weight": {DType: "BF16", Shape: []int64{16, 16}, DataOffsets: []int64{32000, 32512}},
+ }
+ // the dedicated hash test below locks the algorithm; here we only assert the
+ // cataloger wires the header hash through to the package metadata.
+ wantHash := (&safeTensorsHeader{metadata: userMeta, tensors: tensors}).metadataHash()
+
+ // model writes the shared .safetensors header into dir/.safetensors,
+ // creating dir if needed.
+ model := func(t *testing.T, dir string) {
+ t.Helper()
+ require.NoError(t, os.MkdirAll(dir, 0o755))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "model.safetensors"), buildSafeTensorsFile(t, userMeta, tensors), 0o644))
+ }
+ writeFile := func(t *testing.T, path, contents string) {
+ t.Helper()
+ require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
+ require.NoError(t, os.WriteFile(path, []byte(contents), 0o644))
+ }
+ // wantMetadata is the constant header-derived metadata; architecture is the
+ // only field that varies (it's enriched from config.json when present).
+ wantMetadata := func(architecture string) pkg.SafeTensorsModelInfo {
+ return pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ Architecture: architecture,
+ Quantization: "BF16",
+ Parameters: 16256,
+ TensorCount: 2,
+ ShardCount: 1,
+ UserMetadata: pkg.KeyValues{{Key: "format", Value: "pt"}},
+ MetadataHash: wantHash,
+ }
+ }
+
+ tests := []struct {
+ name string
+ setup func(t *testing.T) string
+ expectedPackages []pkg.Package
+ expectedRelationships []artifact.Relationship
+ }{
+ {
+ // rung 1: config.json _name_or_path (path.Base of "org/Llama-3-8B")
+ // wins over the "sometensor" directory fallback; license from README.
+ name: "config.json _name_or_path names the model and wins over the directory",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ modelDir := filepath.Join(dir, "sometensor")
+ model(t, modelDir)
+ writeFile(t, filepath.Join(modelDir, "config.json"),
+ `{"architectures":["LlamaForCausalLM"],"torch_dtype":"bfloat16","transformers_version":"4.40.0","_name_or_path":"meta-llama/Llama-3-8B"}`)
+ writeFile(t, filepath.Join(modelDir, "README.md"),
+ "---\nlicense: Apache-2.0\nbase_model:\n - meta-llama/Llama-3\n---\n# Llama 3\n")
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "Llama-3-8B",
+ Type: pkg.ModelPkg,
+ Licenses: pkg.NewLicenseSet(
+ pkg.NewLicenseFromFields("Apache-2.0", "", nil),
+ ),
+ Metadata: wantMetadata("LlamaForCausalLM"),
+ },
+ },
+ },
+ {
+ // rung 1 via README: with no config.json, the README model card's
+ // base_model names the model (path.Base applied), still beating the
+ // directory fallback ("readme-named").
+ name: "README base_model names the model when there is no config.json",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ modelDir := filepath.Join(dir, "readme-named")
+ model(t, modelDir)
+ writeFile(t, filepath.Join(modelDir, "README.md"),
+ "---\nbase_model:\n - org/base-model-name\n---\n# Card\n")
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "base-model-name",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata(""),
+ },
+ },
+ },
+ {
+ // rung 2: no config.json at all, so the model is named after its
+ // immediate parent directory.
+ name: "no config.json falls back to the parent directory name",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ model(t, filepath.Join(dir, "sometensor"))
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "sometensor",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata(""),
+ },
+ },
+ },
+ {
+ // rung 2: config.json exists but carries no _name_or_path (only
+ // non-identifying info), so we still fall back to the directory name
+ // while enriching architecture from the config.
+ name: "config.json without _name_or_path falls back to the parent directory name",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ modelDir := filepath.Join(dir, "sometensor")
+ model(t, modelDir)
+ writeFile(t, filepath.Join(modelDir, "config.json"),
+ `{"architectures":["LlamaForCausalLM"],"torch_dtype":"bfloat16"}`)
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "sometensor",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata("LlamaForCausalLM"),
+ },
+ },
+ },
+ {
+ // rung 2: the fallback is the IMMEDIATE parent ("someothertensor"),
+ // not an ancestor ("dir").
+ name: "nested model with no config.json is named by its immediate parent directory",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ model(t, filepath.Join(dir, "dir", "someothertensor"))
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "someothertensor",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata(""),
+ },
+ },
+ },
+ {
+ // rung 1 at the scan root: a model directly at `./` is unnameable by
+ // the directory fallback (its parent is the degenerate root "."), but
+ // a sibling config.json still names it.
+ name: "root-level model is named from a root config.json",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ model(t, dir)
+ writeFile(t, filepath.Join(dir, "config.json"), `{"_name_or_path":"org/RootModel"}`)
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "RootModel",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata(""),
+ },
+ },
+ },
+ {
+ // drop: a model directly at `./` with no config.json has no usable
+ // name — the parent is the degenerate root ".", which yields no
+ // directory fallback — so no package is emitted.
+ name: "root-level model with no config.json is dropped",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ model(t, dir)
+ return dir
+ },
+ expectedPackages: nil,
+ },
+ {
+ // rung 1 via parent-walk: findDirHFConfig walks up from the model
+ // directory, so a config.json in an ancestor names a nested model.
+ name: "config.json in an ancestor directory names a nested model",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ model(t, filepath.Join(dir, "dir", "someothertensor"))
+ writeFile(t, filepath.Join(dir, "dir", "config.json"), `{"_name_or_path":"org/Ancestor"}`)
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "Ancestor",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata(""),
+ },
+ },
+ },
+ {
+ // grouping: independent models in one scan are grouped by their own
+ // parent directory and each named from it.
+ name: "sibling models in one scan are each named by their own directory",
+ setup: func(t *testing.T) string {
+ dir := t.TempDir()
+ model(t, filepath.Join(dir, "sometensor"))
+ model(t, filepath.Join(dir, "dir", "someothertensor"))
+ return dir
+ },
+ expectedPackages: []pkg.Package{
+ {
+ Name: "sometensor",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata(""),
+ },
+ {
+ Name: "someothertensor",
+ Type: pkg.ModelPkg,
+ Metadata: wantMetadata(""),
+ },
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fixtureDir := tt.setup(t)
+
+ pkgtest.NewCatalogTester().
+ FromDirectory(t, fixtureDir).
+ Expects(tt.expectedPackages, tt.expectedRelationships).
+ IgnoreLocationLayer().
+ IgnorePackageFields("FoundBy", "Locations").
+ TestCataloger(t, NewSafeTensorsCataloger())
+ })
+ }
+}
+
+// TestSafeTensorsCataloger_shardedDirectory covers the primary multi-shard shape:
+// several `model-0000N-of-0000M.safetensors` files in one directory. The
+// cataloger must group the shards into a single package, sum their tensor counts,
+// record the shard count, and roll each shard up into Parts. (The OCI multi-shard
+// path is covered separately in TestSafeTensorsMergeProcessor.)
+func TestSafeTensorsCataloger_shardedDirectory(t *testing.T) {
+ userMeta := map[string]string{"format": "pt"}
+ // Two shards with distinct tensors → distinct per-shard metadata hashes, so
+ // the merge treats them as separate shards (3 tensors total across 2 shards).
+ shardA := map[string]safeTensorsEntry{
+ "layers.0.weight": {DType: "BF16", Shape: []int64{10, 10}, DataOffsets: []int64{0, 200}},
+ }
+ shardB := map[string]safeTensorsEntry{
+ "layers.1.weight": {DType: "BF16", Shape: []int64{10, 10}, DataOffsets: []int64{0, 200}},
+ "layers.2.weight": {DType: "BF16", Shape: []int64{10, 10}, DataOffsets: []int64{200, 400}},
+ }
+
+ dir := t.TempDir()
+ modelDir := filepath.Join(dir, "llama-sharded")
+ require.NoError(t, os.MkdirAll(modelDir, 0o755))
+ require.NoError(t, os.WriteFile(filepath.Join(modelDir, "model-00001-of-00002.safetensors"),
+ buildSafeTensorsFile(t, userMeta, shardA), 0o644))
+ require.NoError(t, os.WriteFile(filepath.Join(modelDir, "model-00002-of-00002.safetensors"),
+ buildSafeTensorsFile(t, userMeta, shardB), 0o644))
+
+ pkgtest.NewCatalogTester().
+ FromDirectory(t, dir).
+ ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, _ []artifact.Relationship) {
+ require.Len(t, pkgs, 1)
+ got := pkgs[0]
+ assert.Equal(t, "llama-sharded", got.Name, "a sharded model with no config.json is named by its directory")
+ md := got.Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, 2, md.ShardCount)
+ assert.Equal(t, uint64(3), md.TensorCount, "tensor counts are summed across shards")
+ assert.Len(t, md.Parts, 2, "each shard is rolled up into Parts")
+ assert.Equal(t, "BF16", md.Quantization)
+ }).
+ TestCataloger(t, NewSafeTensorsCataloger())
+}
+
+// TestParseSafeTensorsOCIConfig covers the parser in isolation: it should emit
+// a nameless package mirroring the config blob's producer-declared fields, and
+// emit nothing for non-safetensors formats so the GGUF cataloger can claim the
+// artifact. Naming and license resolution happen in the merge processor and are
+// tested separately under TestSafeTensorsMergeProcessor.
+func TestParseSafeTensorsOCIConfig(t *testing.T) {
+ tests := []struct {
+ name string
+ blob string
+ expectedPackages []pkg.Package // nil => parser must emit nothing
+ }{
+ {
+ name: "emits a nameless package with config-blob fields",
+ blob: `{"config":{"format":"safetensors","quantization":"Q4_K_M","parameters":"8B","size":"16.00GB","safetensors":{"tensor_count":291}}}`,
+ expectedPackages: []pkg.Package{
+ {
+ // nameless: the merge processor assigns the name and resolves
+ // licenses. Config blobs carry no header content, so
+ // MetadataHash stays empty. The "parameters" label in the blob is
+ // intentionally ignored: the true count is measured from the
+ // SafeTensors layer headers, so Parameters stays zero here.
+ Type: pkg.ModelPkg,
+ Metadata: pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ Quantization: "Q4_K_M",
+ TotalSize: "16.00GB",
+ TensorCount: 291,
+ },
+ },
+ },
+ },
+ {
+ // non-safetensors formats emit nothing so the GGUF cataloger can claim
+ // the artifact.
+ name: "ignores non-safetensors format",
+ blob: `{"config":{"format":"gguf","quantization":"Q4_K_M"}}`,
+ expectedPackages: nil,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ pkgtest.NewCatalogTester().
+ FromString("/config.json", tt.blob).
+ Expects(tt.expectedPackages, nil).
+ IgnorePackageFields("FoundBy", "Locations").
+ TestParser(t, parseSafeTensorsOCIConfig)
+ })
+ }
+}
+
+// TestSafeTensorsMergeProcessor exercises the merge processor directly with
+// synthetic input. The full-cataloger integration tests cover the realistic
+// happy paths; this focuses on grouping, the naming precedence chain, the
+// drop-when-unnameable rule, and cross-shard rollup.
+func TestSafeTensorsMergeProcessor(t *testing.T) {
+ dirPkg := func(realPath string, md pkg.SafeTensorsModelInfo) pkg.Package {
+ return pkg.Package{
+ Type: pkg.ModelPkg,
+ Metadata: md,
+ Locations: file.NewLocationSet(
+ file.NewLocation(realPath).
+ WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ ),
+ }
+ }
+ ociPkg := func(md pkg.SafeTensorsModelInfo) pkg.Package {
+ return pkg.Package{
+ Type: pkg.ModelPkg,
+ Metadata: md,
+ Locations: file.NewLocationSet(
+ file.NewLocation("/").
+ WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ ),
+ }
+ }
+
+ t.Run("dir scan: parent directory base name names the group when no config.json is present", func(t *testing.T) {
+ // Without a config.json the dir-scan path falls through to the
+ // parent directory base name. hugginface style model dir is named after the
+ // model, so "/models/tiny-llama/weights.safetensors" → "tiny-llama".
+ p := dirPkg("/models/tiny-llama/weights.safetensors", pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: 4,
+ Quantization: "BF16",
+ MetadataHash: "abc",
+ })
+ resolver := file.NewMockResolverForPaths() // no config.json / README available
+ out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, nil)
+ require.NoError(t, err)
+ require.Len(t, out, 1)
+ assert.Equal(t, "tiny-llama", out[0].Name, "rung 2: parent directory base name")
+ })
+
+ t.Run("dir scan: nested model dirs group and name by immediate parent", func(t *testing.T) {
+ top := dirPkg("/namea/1.safetensors", pkg.SafeTensorsModelInfo{
+ Format: "safetensors", TensorCount: 1, MetadataHash: "aaaa",
+ })
+ nested := dirPkg("/namea/nameb/2.safetensors", pkg.SafeTensorsModelInfo{
+ Format: "safetensors", TensorCount: 1, MetadataHash: "bbbb",
+ })
+ resolver := file.NewMockResolverForPaths()
+ out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{top, nested}, nil, nil)
+ require.NoError(t, err)
+ require.Len(t, out, 2)
+ names := []string{out[0].Name, out[1].Name}
+ assert.ElementsMatch(t, []string{"namea", "nameb"}, names)
+ })
+
+ t.Run("dir scan: config.json _name_or_path beats the parent directory fallback", func(t *testing.T) {
+ // When a sibling config.json carries _name_or_path
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"),
+ []byte(`{"_name_or_path":"org/preferred-name"}`), 0o644))
+ stPath := filepath.Join(dir, "weights.safetensors")
+ p := dirPkg(stPath, pkg.SafeTensorsModelInfo{
+ Format: "safetensors", TensorCount: 1, MetadataHash: "abc",
+ })
+ resolver := file.NewMockResolverForPaths(filepath.Join(dir, "config.json"))
+ out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, nil)
+ require.NoError(t, err)
+ require.Len(t, out, 1)
+ assert.Equal(t, "preferred-name", out[0].Name, "rung 1 (config.json) wins over rung 2 (parent dir)")
+ })
+
+ t.Run("OCI: dropped when no name source is available", func(t *testing.T) {
+ // The vllm-style shape: config-blob package + a weight-layer package,
+ // both at virtual path "/", no model.file companions on the resolver
+ // AND no image ref. With nothing to derive a name from, the package is
+ // dropped
+ configMd := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: 5,
+ TotalSize: "1GB",
+ }
+ shardMd := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: 5,
+ Quantization: "BF16",
+ MetadataHash: "deadbeef",
+ }
+ resolver := file.NewMockResolverForMediaTypes(nil)
+ out, _, err := safeTensorsMergeProcessor(
+ context.Background(), resolver,
+ []pkg.Package{ociPkg(configMd), ociPkg(shardMd)}, nil, nil,
+ )
+ require.NoError(t, err)
+ assert.Empty(t, out, "OCI group with no naming source must be dropped")
+ })
+
+ t.Run("OCI: image-ref last segment names the group when config.json is absent", func(t *testing.T) {
+ // vllm-style artifact: a repacked model whose embedded config.json has
+ // been stripped of _name_or_path.
+ configMd := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: 290,
+ TotalSize: "723MB",
+ }
+ shardMd := pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TensorCount: 290,
+ Quantization: "BF16",
+ MetadataHash: "deadbeef",
+ }
+ resolver := file.NewMockResolverForOCIArtifact(
+ "docker.io/ai/smollm2-vllm:360M", nil,
+ )
+ out, _, err := safeTensorsMergeProcessor(
+ context.Background(), resolver,
+ []pkg.Package{ociPkg(configMd), ociPkg(shardMd)}, nil, nil,
+ )
+ require.NoError(t, err)
+ require.Len(t, out, 1)
+ assert.Equal(t, "smollm2-vllm", out[0].Name, "rung 2: image-ref repository basename")
+ })
+
+ t.Run("OCI: multi-shard rollup hashes are stable and sorted", func(t *testing.T) {
+ dir := t.TempDir()
+ hfConfigPath := filepath.Join(dir, "config.json")
+ require.NoError(t, os.WriteFile(hfConfigPath,
+ []byte(`{"architectures":["X"],"_name_or_path":"org/multi"}`), 0o644))
+ resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{
+ dockerAIModelFileMediaType: {file.NewLocation(hfConfigPath)},
+ })
+
+ configMd := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 9, TotalSize: "3GB"}
+ shard := func(hash string, cnt uint64) pkg.SafeTensorsModelInfo {
+ return pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: cnt, Quantization: "BF16", MetadataHash: hash}
+ }
+ in := []pkg.Package{
+ ociPkg(configMd),
+ ociPkg(shard("cccc", 3)),
+ ociPkg(shard("aaaa", 3)),
+ ociPkg(shard("bbbb", 3)),
+ }
+ out1, _, err := safeTensorsMergeProcessor(context.Background(), resolver, in, nil, nil)
+ require.NoError(t, err)
+ require.Len(t, out1, 1)
+ md1 := out1[0].Metadata.(pkg.SafeTensorsModelInfo)
+ require.Len(t, md1.Parts, 3)
+ // Parts deterministically sorted by MetadataHash.
+ assert.Equal(t,
+ []string{"aaaa", "bbbb", "cccc"},
+ []string{md1.Parts[0].MetadataHash, md1.Parts[1].MetadataHash, md1.Parts[2].MetadataHash},
+ )
+ // Rollup hash is stable across input ordering.
+ shuffled := []pkg.Package{ociPkg(shard("bbbb", 3)), ociPkg(configMd), ociPkg(shard("aaaa", 3)), ociPkg(shard("cccc", 3))}
+ out2, _, err := safeTensorsMergeProcessor(context.Background(), resolver, shuffled, nil, nil)
+ require.NoError(t, err)
+ md2 := out2[0].Metadata.(pkg.SafeTensorsModelInfo)
+ assert.Equal(t, md1.MetadataHash, md2.MetadataHash, "rollup hash must not depend on input order")
+ })
+
+ t.Run("OCI: license layer SPDX comes from choosealicense frontmatter", func(t *testing.T) {
+ // The license layer's content carries a choosealicense.com-style YAML
+ // frontmatter block. The processor should prefer the cheap spdx-id read
+ // over invoking the full license scanner.
+ dir := t.TempDir()
+ licensePath := filepath.Join(dir, "LICENSE")
+ require.NoError(t, os.WriteFile(licensePath, []byte(`---
+title: Apache License 2.0
+spdx-id: Apache-2.0
+---
+
+ Apache License
+ Version 2.0, January 2004
+`), 0o644))
+ hfConfigPath := filepath.Join(dir, "config.json")
+ require.NoError(t, os.WriteFile(hfConfigPath,
+ []byte(`{"_name_or_path":"org/with-license-fm"}`), 0o644))
+ resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{
+ dockerAIModelFileMediaType: {file.NewLocation(hfConfigPath)},
+ dockerAILicenseMediaType: {file.NewLocation(licensePath)},
+ })
+
+ configMd := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 1}
+ out, _, err := safeTensorsMergeProcessor(
+ context.Background(), resolver,
+ []pkg.Package{ociPkg(configMd)}, nil, nil,
+ )
+ require.NoError(t, err)
+ require.Len(t, out, 1)
+ assert.Equal(t, "with-license-fm", out[0].Name)
+ assertHasLicense(t, out[0], "Apache-2.0")
+ })
+
+ t.Run("OCI: license layer wins over a README model-card license", func(t *testing.T) {
+ // When both a dedicated license layer and a README model-card license are
+ // present, the producer-curated license layer is authoritative. (If the
+ // README won, the resolved license would be MIT and this assertion fails.)
+ dir := t.TempDir()
+ licensePath := filepath.Join(dir, "LICENSE")
+ require.NoError(t, os.WriteFile(licensePath, []byte("---\nspdx-id: Apache-2.0\n---\n"), 0o644))
+ readmePath := filepath.Join(dir, "README.md")
+ require.NoError(t, os.WriteFile(readmePath,
+ []byte("---\nlicense: MIT\nbase_model:\n - org/base\n---\n# Card\n"), 0o644))
+ configPath := filepath.Join(dir, "config.json")
+ require.NoError(t, os.WriteFile(configPath, []byte(`{"_name_or_path":"org/precedence-model"}`), 0o644))
+ resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{
+ dockerAIModelFileMediaType: {file.NewLocation(configPath), file.NewLocation(readmePath)},
+ dockerAILicenseMediaType: {file.NewLocation(licensePath)},
+ })
+
+ configMd := pkg.SafeTensorsModelInfo{Format: "safetensors", TensorCount: 1}
+ out, _, err := safeTensorsMergeProcessor(
+ context.Background(), resolver,
+ []pkg.Package{ociPkg(configMd)}, nil, nil,
+ )
+ require.NoError(t, err)
+ require.Len(t, out, 1)
+ assert.Equal(t, "precedence-model", out[0].Name)
+ assertHasLicense(t, out[0], "Apache-2.0")
+ })
+
+ t.Run("names packages and still propagates an upstream error", func(t *testing.T) {
+ // A non-nil upstream error (e.g. another file in the run failed to parse)
+ // must NOT short-circuit naming. The package is still nameless at this
+ // point; skipping the merge would let it flow downstream where missing-name
+ // compliance silently drops it, turning one bad file into the loss of every
+ // valid model. So we name/drop as usual and pass the error through.
+ sentinel := assert.AnError
+ p := dirPkg("/models/x/y.safetensors", pkg.SafeTensorsModelInfo{Format: "safetensors", MetadataHash: "h"})
+ resolver := file.NewMockResolverForPaths() // no config.json -> parent directory base name
+ out, _, err := safeTensorsMergeProcessor(context.Background(), resolver, []pkg.Package{p}, nil, sentinel)
+ assert.Equal(t, sentinel, err, "the upstream error is still propagated")
+ require.Len(t, out, 1)
+ assert.Equal(t, "x", out[0].Name, "the model is still named (from its parent directory) despite the error")
+ })
+}
+
+// TestSafeTensorsNamingPrecedence codifies pickSafeTensorsName's two-rung
+// precedence chain. Each case sets the inputs that should activate one rung
+// (or neither, asserting the drop path).
+//
+// Precedence (highest → lowest):
+// 1. config.json _name_or_path (path.Base applied; both dir-scan and OCI)
+// 2. fallback name — OCI image-ref last segment, or dir-scan parent directory
+// base name (the merge processor computes the right one per group)
+// → drop (empty name) when nothing matches
+func TestSafeTensorsNamingPrecedence(t *testing.T) {
+ tests := []struct {
+ name string
+ nameOrPath string
+ fallbackName string
+ want string
+ }{
+ // rung 1
+ {
+ name: "rung 1: _name_or_path beats the fallback",
+ nameOrPath: "org/MyModel",
+ fallbackName: "fallback-name",
+ want: "MyModel",
+ },
+ {
+ name: "rung 1: applies path.Base to the raw value",
+ nameOrPath: "very/deep/checkpoint/path/leaf-model",
+ want: "leaf-model",
+ },
+ {
+ name: "rung 1: bare name without slashes is preserved",
+ nameOrPath: "OciModel",
+ want: "OciModel",
+ },
+
+ // rung 2
+ {
+ name: "rung 2: OCI image-ref last segment used when _name_or_path is empty",
+ fallbackName: "smollm2-vllm",
+ want: "smollm2-vllm",
+ },
+ {
+ name: "rung 2: dir-scan parent directory name used when _name_or_path is empty",
+ fallbackName: "tiny-llama",
+ want: "tiny-llama",
+ },
+
+ // drops
+ {
+ name: "drop: both rungs empty",
+ want: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := pickSafeTensorsName(tt.nameOrPath, tt.fallbackName)
+ assert.Equal(t, tt.want, got)
+ })
+ }
+}
+
+// TestSafeTensorsDirName covers the directory-scan fallback name derivation,
+// including the degenerate roots that must yield no name.
+func TestSafeTensorsDirName(t *testing.T) {
+ tests := []struct {
+ groupKey string
+ want string
+ }{
+ {groupKey: "/models/tiny-llama", want: "tiny-llama"},
+ {groupKey: "/namea", want: "namea"},
+ {groupKey: "/namea/nameb", want: "nameb"},
+ {groupKey: "/", want: ""},
+ {groupKey: ".", want: ""},
+ {groupKey: "", want: ""},
+ }
+ for _, tt := range tests {
+ t.Run(tt.groupKey, func(t *testing.T) {
+ assert.Equal(t, tt.want, safeTensorsDirName(tt.groupKey))
+ })
+ }
+}
+
+func TestParseSafeTensorsOCILayer(t *testing.T) {
+ tensors := map[string]safeTensorsEntry{
+ "layer.0.weight": {DType: "BF16", Shape: []int64{1024, 16}, DataOffsets: []int64{0, 32768}},
+ "layer.1.weight": {DType: "BF16", Shape: []int64{16, 16}, DataOffsets: []int64{32768, 33280}},
+ }
+ userMeta := map[string]string{"format": "pt"}
+ wantUserMetadata := pkg.KeyValues{{Key: "format", Value: "pt"}}
+ blob := buildSafeTensorsFile(t, userMeta, tensors)
+ wantHash := (&safeTensorsHeader{metadata: userMeta, tensors: tensors}).metadataHash()
+
+ t.Run("emits a nameless package with header-derived metadata", func(t *testing.T) {
+ // nameless: the merge processor assigns the name. Parameters is the
+ // summed element count of the two tensors (1024*16 + 16*16 = 16640).
+ expected := []pkg.Package{
+ {
+ Type: pkg.ModelPkg,
+ Metadata: pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ Parameters: 16640,
+ Quantization: "BF16",
+ TensorCount: 2,
+ UserMetadata: wantUserMetadata,
+ MetadataHash: wantHash,
+ },
+ },
+ }
+ pkgtest.NewCatalogTester().
+ FromString("/", string(blob)).
+ Expects(expected, nil).
+ IgnorePackageFields("FoundBy", "Locations").
+ TestParser(t, parseSafeTensorsOCILayer)
+ })
+
+ t.Run("merged via processor: aggregate fields preserved, hash lifted from single shard", func(t *testing.T) {
+ // Synthesize the OCI single-shard shape: a config-blob-derived nameless
+ // package + the weight-layer parser's nameless package (both at virtual
+ // path "/"). With a companion HF config.json on the resolver to provide
+ // _name_or_path, the merge processor produces a single named model.
+ dir := t.TempDir()
+ hfConfigPath := filepath.Join(dir, "config.json")
+ require.NoError(t, os.WriteFile(hfConfigPath,
+ []byte(`{"architectures":["Qwen3ForCausalLM"],"_name_or_path":"org/qwen-test"}`), 0o644))
+ resolver := file.NewMockResolverForMediaTypes(map[string][]file.Location{
+ dockerAIModelFileMediaType: {file.NewLocation(hfConfigPath)},
+ })
+
+ configPkg := pkg.Package{
+ Type: pkg.ModelPkg,
+ Metadata: pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ TotalSize: "5.00GB",
+ Quantization: "Q4_K_M", // raw producer string
+ TensorCount: 9999,
+ },
+ Locations: file.NewLocationSet(
+ file.NewLocation("/").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ ),
+ }
+ reader := file.NewLocationReadCloser(
+ file.NewLocation("/").WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
+ io.NopCloser(bytes.NewReader(blob)),
+ )
+ layerPkgs, _, err := parseSafeTensorsOCILayer(context.Background(), nil, nil, reader)
+ require.NoError(t, err)
+ require.Len(t, layerPkgs, 1)
+
+ out, _, err := safeTensorsMergeProcessor(
+ context.Background(), resolver,
+ append([]pkg.Package{configPkg}, layerPkgs...), nil, nil,
+ )
+ require.NoError(t, err)
+ require.Len(t, out, 1)
+
+ got := out[0]
+ assert.Equal(t, "qwen-test", got.Name, "name comes from the companion config.json _name_or_path")
+ md := got.Metadata.(pkg.SafeTensorsModelInfo)
+ // Aggregate-declared TensorCount/TotalSize win as authoritative totals; the
+ // per-shard TensorCount must NOT be summed into the aggregate. Parameters is
+ // the exception: it is always measured from the shard layer header (here the
+ // 16640-element blob), never taken from the config aggregate.
+ assert.Equal(t, uint64(9999), md.TensorCount)
+ assert.Equal(t, "5.00GB", md.TotalSize)
+ assert.Equal(t, uint64(16640), md.Parameters)
+ // Aggregate Quantization wins when set; shard's normalized dtype is the
+ // fallback (not exercised here because the config had Q4_K_M).
+ assert.Equal(t, "Q4_K_M", md.Quantization)
+ // Architecture comes from companion HF config.json enrichment.
+ assert.Equal(t, "Qwen3ForCausalLM", md.Architecture)
+ // Single-shard groups skip Parts; the rollup hash is the lone shard's hash.
+ assert.Nil(t, md.Parts)
+ assert.Equal(t, wantHash, md.MetadataHash)
+ assert.Equal(t, wantUserMetadata, md.UserMetadata)
+ assert.Equal(t, 1, md.ShardCount)
+ })
+}
+
+// TestParseSafeTensorsOCILayer_realFixture grounds the OCI layer parser
+// against a real `[prefix + JSON header]` captured from a public Docker AI
+// model artifact (docker.io/ai/nomic-embed-text-v2-moe-safetensors:475M).
+// The fixture and the tool that produced it live in
+// testdata/safetensors/; see the README there to refresh.
+//
+// Locking in the field values guards against changes to the header parser
+// silently breaking on real-world content shape.
+func TestParseSafeTensorsOCILayer_realFixture(t *testing.T) {
+ // nameless before the merge processor runs. The fixture is immutable on disk;
+ // the locked field values (notably MetadataHash) guard against the header
+ // parser silently breaking on real-world content shape — if MetadataHash
+ // changes, either the hash algorithm or the canonicalization changed, both of
+ // which callers may rely on for cross-source identity.
+ expected := []pkg.Package{
+ {
+ Type: pkg.ModelPkg,
+ Metadata: pkg.SafeTensorsModelInfo{
+ Format: "safetensors",
+ Parameters: 475292928, // exact element count summed across the 148 tensors in this shard
+ Quantization: "F32", // every tensor in the captured shard is F32
+ TensorCount: 148, // nomic-embed-v2-moe 475M ships 148 tensor entries in this shard
+ UserMetadata: pkg.KeyValues{{Key: "format", Value: "pt"}},
+ MetadataHash: "6026c28a883ab918",
+ },
+ },
+ }
+
+ pkgtest.NewCatalogTester().
+ FromFile(t, filepath.Join("testdata", "safetensors", "nomic-embed-475M.header.safetensors")).
+ Expects(expected, nil).
+ IgnorePackageFields("FoundBy", "Locations").
+ TestParser(t, parseSafeTensorsOCILayer)
+}
+
+func TestSafeTensorsCrossSourceHashParity(t *testing.T) {
+ // Same content, two paths: a directory scan via parseSafeTensorsFile, and an
+ // OCI weight-layer scan via parseSafeTensorsOCILayer. The MetadataHash of
+ // the dir-scan package must equal the per-shard hash captured before the
+ // merge processor absorbs it. This is the convergence point that lets a
+ // caller correlate the two source types.
+ tensors := map[string]safeTensorsEntry{
+ "a.weight": {DType: "BF16", Shape: []int64{8, 8}, DataOffsets: []int64{0, 128}},
+ "b.weight": {DType: "BF16", Shape: []int64{4, 4}, DataOffsets: []int64{128, 160}},
+ }
+ userMeta := map[string]string{"format": "pt", "producer": "test"}
+ blob := buildSafeTensorsFile(t, userMeta, tensors)
+
+ // dir-scan path
+ dir := t.TempDir()
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "model.safetensors"), blob, 0o644))
+ dirReader := func() file.LocationReadCloser {
+ f, err := os.Open(filepath.Join(dir, "model.safetensors"))
+ require.NoError(t, err)
+ return file.NewLocationReadCloser(file.NewLocation(filepath.Join(dir, "model.safetensors")), f)
+ }()
+ dirPkgs, _, err := parseSafeTensorsFile(context.Background(), nil, nil, dirReader)
+ require.NoError(t, err)
+ require.Len(t, dirPkgs, 1)
+ dirHash := dirPkgs[0].Metadata.(pkg.SafeTensorsModelInfo).MetadataHash
+ require.NotEmpty(t, dirHash)
+
+ // OCI weight-layer path
+ ociReader := file.NewLocationReadCloser(file.NewLocation("/"), io.NopCloser(bytes.NewReader(blob)))
+ ociPkgs, _, err := parseSafeTensorsOCILayer(context.Background(), nil, nil, ociReader)
+ require.NoError(t, err)
+ require.Len(t, ociPkgs, 1)
+ ociHash := ociPkgs[0].Metadata.(pkg.SafeTensorsModelInfo).MetadataHash
+
+ assert.Equal(t, dirHash, ociHash, "same content via dir scan and OCI weight-layer scan must hash equal")
+}
+
+func assertHasLicense(t *testing.T, p pkg.Package, value string) {
+ t.Helper()
+ for _, l := range p.Licenses.ToSlice() {
+ if l.Value == value {
+ return
+ }
+ }
+ t.Errorf("expected license %q, got %+v", value, p.Licenses.ToSlice())
+}
+
+func TestReadSafeTensorsHeader(t *testing.T) {
+ zeroLength := make([]byte, 8) // length prefix of 0
+
+ truncatedBody := make([]byte, 8)
+ binary.LittleEndian.PutUint64(truncatedBody, 100) // claims 100 bytes but supplies none
+
+ // length prefix declares a header larger than the OOM guard allows; the body
+ // is intentionally omitted because the cap must reject before any allocation.
+ overCap := make([]byte, 8)
+ binary.LittleEndian.PutUint64(overCap, uint64(maxSafeTensorsHeaderSize)+1)
+
+ malformedJSON := make([]byte, 8+len("not-json"))
+ binary.LittleEndian.PutUint64(malformedJSON[:8], uint64(len("not-json")))
+ copy(malformedJSON[8:], "not-json")
+
+ tests := []struct {
+ name string
+ data []byte
+ wantErr bool
+ assert func(t *testing.T, h *safeTensorsHeader)
+ }{
+ {
+ name: "valid header",
+ data: buildSafeTensorsFile(t, map[string]string{"format": "pt"}, map[string]safeTensorsEntry{
+ "w": {DType: "F32", Shape: []int64{2, 2}, DataOffsets: []int64{0, 16}},
+ }),
+ assert: func(t *testing.T, h *safeTensorsHeader) {
+ assert.Len(t, h.tensors, 1)
+ assert.Equal(t, "pt", h.metadata["format"])
+ },
+ },
+ {
+ name: "zero-length header",
+ data: zeroLength,
+ wantErr: true,
+ },
+ {
+ name: "truncated body",
+ data: truncatedBody,
+ wantErr: true,
+ },
+ {
+ name: "header exceeds max size cap",
+ data: overCap,
+ wantErr: true,
+ },
+ {
+ name: "malformed JSON body",
+ data: malformedJSON,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ h, err := readSafeTensorsHeader(bytes.NewReader(tt.data))
+ if tt.wantErr {
+ require.Error(t, err)
+ return
+ }
+ require.NoError(t, err)
+ tt.assert(t, h)
+ })
+ }
+}
+
+func TestSafeTensorsHeader_metadataHash(t *testing.T) {
+ base := &safeTensorsHeader{
+ metadata: map[string]string{"format": "pt"},
+ tensors: map[string]safeTensorsEntry{
+ "a.weight": {DType: "F32", Shape: []int64{2, 2}, DataOffsets: []int64{0, 16}},
+ "b.weight": {DType: "F16", Shape: []int64{4}, DataOffsets: []int64{16, 24}},
+ },
+ }
+
+ // deterministic across calls and independent of map insertion order
+ reordered := &safeTensorsHeader{
+ metadata: map[string]string{"format": "pt"},
+ tensors: map[string]safeTensorsEntry{
+ "b.weight": {DType: "F16", Shape: []int64{4}, DataOffsets: []int64{16, 24}},
+ "a.weight": {DType: "F32", Shape: []int64{2, 2}, DataOffsets: []int64{0, 16}},
+ },
+ }
+ assert.Equal(t, base.metadataHash(), reordered.metadataHash())
+ assert.Len(t, base.metadataHash(), 16)
+
+ // changing a tensor changes the hash
+ changed := &safeTensorsHeader{
+ metadata: base.metadata,
+ tensors: map[string]safeTensorsEntry{
+ "a.weight": {DType: "F32", Shape: []int64{2, 3}, DataOffsets: []int64{0, 24}},
+ "b.weight": {DType: "F16", Shape: []int64{4}, DataOffsets: []int64{24, 32}},
+ },
+ }
+ assert.NotEqual(t, base.metadataHash(), changed.metadataHash())
+
+ // changing __metadata__ changes the hash
+ differentMeta := &safeTensorsHeader{metadata: map[string]string{"format": "np"}, tensors: base.tensors}
+ assert.NotEqual(t, base.metadataHash(), differentMeta.metadataHash())
+}
+
+func TestSafeTensorsHeader_parameterCountAndDType(t *testing.T) {
+ h := &safeTensorsHeader{tensors: map[string]safeTensorsEntry{
+ "big": {DType: "BF16", Shape: []int64{1000, 16}},
+ "small": {DType: "F32", Shape: []int64{16, 16}},
+ "scalar": {DType: "F32", Shape: []int64{}}, // empty shape contributes 1
+ }}
+ assert.Equal(t, uint64(1000*16+16*16+1), h.parameterCount())
+ assert.Equal(t, "BF16", h.dominantDType())
+}
+
+func TestSafeTensorsHeader_parameterCount_nonPositiveDims(t *testing.T) {
+ // a zero or negative dim zeroes that tensor's contribution rather than
+ // wrapping the uint64 product (a negative int64 cast would otherwise become a
+ // huge value); other tensors are unaffected.
+ h := &safeTensorsHeader{tensors: map[string]safeTensorsEntry{
+ "good": {DType: "F32", Shape: []int64{4, 4}},
+ "zero": {DType: "F32", Shape: []int64{4, 0}},
+ "negative": {DType: "F32", Shape: []int64{-1, 8}},
+ }}
+ assert.Equal(t, uint64(16), h.parameterCount())
+}
+
+func TestSafeTensorsHeader_dominantDType_tieBreak(t *testing.T) {
+ // two dtypes tied on parameter count: the lexicographically smallest wins,
+ // independent of map iteration order.
+ h := &safeTensorsHeader{tensors: map[string]safeTensorsEntry{
+ "a": {DType: "F32", Shape: []int64{4, 4}},
+ "b": {DType: "BF16", Shape: []int64{4, 4}},
+ }}
+ assert.Equal(t, "BF16", h.dominantDType())
+}
+
+func TestNormalizeDType(t *testing.T) {
+ tests := []struct {
+ name string
+ in string
+ want string
+ }{
+ {name: "already canonical BF16", in: "BF16", want: "BF16"},
+ {name: "float16 alias", in: "float16", want: "F16"},
+ {name: "FP32 alias", in: "FP32", want: "F32"},
+ {name: "int8 alias", in: "int8", want: "I8"},
+ {name: "U8 passthrough", in: "U8", want: "U8"},
+ {name: "bool", in: "bool", want: "BOOL"},
+ {name: "unknown value uppercased", in: "weird", want: "WEIRD"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.want, normalizeDType(tt.in))
+ })
+ }
+}
+
+func TestParseFrontmatter(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ wantNil bool
+ wantLicense string
+ wantBaseModel []string
+ }{
+ {
+ name: "list base_model",
+ input: "---\nlicense: mit\nbase_model:\n - org/Model\n---\nbody",
+ wantLicense: "mit",
+ wantBaseModel: []string{"org/Model"},
+ },
+ {
+ name: "scalar base_model",
+ input: "---\nlicense: apache-2.0\nbase_model: org/Model\n---\n",
+ wantLicense: "apache-2.0",
+ wantBaseModel: []string{"org/Model"},
+ },
+ {
+ name: "leading BOM",
+ input: "\xef\xbb\xbf---\nlicense: mit\n---\n",
+ wantLicense: "mit",
+ },
+ {
+ name: "no frontmatter",
+ input: "# just a heading\n",
+ wantNil: true,
+ },
+ {
+ name: "unterminated frontmatter",
+ input: "---\nlicense: mit\n",
+ wantNil: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ fm := parseFrontmatter([]byte(tt.input))
+ if tt.wantNil {
+ assert.Nil(t, fm)
+ return
+ }
+ require.NotNil(t, fm)
+ assert.Equal(t, tt.wantLicense, fm.License)
+ if tt.wantBaseModel != nil {
+ assert.Equal(t, tt.wantBaseModel, fm.BaseModel)
+ }
+ })
+ }
+}
+
+// TestParseLicenseFrontmatter covers the choosealicense.com-style YAML
+// frontmatter Docker Model Runner uses for its license layers. Only spdx-id
+// is consumed; everything else in the block is ignored.
+func TestParseLicenseFrontmatter(t *testing.T) {
+ // The Apache-2.0 case is the exact frontmatter shape from
+ // https://github.com/github/choosealicense.com/blob/gh-pages/_licenses/apache-2.0.txt
+ // Docker AI license layers ship a near-identical block.
+ tests := []struct {
+ name string
+ input string
+ want string
+ }{
+ {
+ name: "Apache-2.0 (the canonical choosealicense.com shape)",
+ input: `---
+title: Apache License 2.0
+spdx-id: Apache-2.0
+redirect_from: /licenses/apache/
+featured: true
+hidden: false
+
+description: A permissive license whose main conditions require preservation of copyright and license notices.
+
+how: Create a text file (typically named LICENSE or LICENSE.txt) in the root of your source code and copy the text of the license into the file.
+
+using:
+ Kubernetes: https://github.com/kubernetes/kubernetes/blob/master/LICENSE
+ PDF.js: https://github.com/mozilla/pdf.js/blob/master/LICENSE
+ Swift: https://github.com/apple/swift/blob/main/LICENSE.txt
+
+permissions:
+ - commercial-use
+ - modifications
+ - distribution
+ - patent-use
+ - private-use
+
+conditions:
+ - include-copyright
+ - document-changes
+
+limitations:
+ - trademark-use
+ - liability
+ - warranty
+
+---
+
+ Apache License
+ Version 2.0, January 2004
+`,
+ want: "Apache-2.0",
+ },
+ {
+ name: "MIT with BOM prefix",
+ input: "\xef\xbb\xbf---\ntitle: MIT License\nspdx-id: MIT\n---\nThe MIT License...\n",
+ want: "MIT",
+ },
+ {
+ name: "frontmatter without spdx-id falls through (returns empty)",
+ input: "---\ntitle: Something\ndescription: no spdx-id here\n---\nbody\n",
+ want: "",
+ },
+ {
+ name: "plain license text without any frontmatter",
+ input: " Apache License\n Version 2.0, January 2004\n",
+ want: "",
+ },
+ {
+ name: "unterminated frontmatter block",
+ input: "---\nspdx-id: MIT\n(never closes)\n",
+ want: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.want, parseLicenseFrontmatter([]byte(tt.input)))
+ })
+ }
+}
+
+func TestDockerAIModelConfigMediaTypes(t *testing.T) {
+ // supported mirrors how the resolver matches: filepath.Match each registered
+ // media type against a layer's media type.
+ supported := func(mt string) bool {
+ for _, p := range dockerAIModelConfigMediaTypes {
+ if ok, err := filepath.Match(p, mt); err == nil && ok {
+ return true
+ }
+ }
+ return false
+ }
+
+ tests := []struct {
+ name string
+ mediaType string
+ want bool
+ }{
+ // the known, verified schema versions are consumed
+ {name: "known schema v0.1 is consumed", mediaType: "application/vnd.docker.ai.model.config.v0.1+json", want: true},
+ {name: "known schema v0.2 is consumed", mediaType: "application/vnd.docker.ai.model.config.v0.2+json", want: true},
+ // unknown/future schema versions are intentionally NOT consumed, to avoid
+ // silently ingesting a potentially breaking config change
+ {name: "unknown schema v0.3 is rejected", mediaType: "application/vnd.docker.ai.model.config.v0.3+json", want: false},
+ {name: "far-future schema v9.9 is rejected", mediaType: "application/vnd.docker.ai.model.config.v9.9+json", want: false},
+ // sibling layer media types are not matched either
+ {name: "sibling model.file layer is not matched", mediaType: "application/vnd.docker.ai.model.file", want: false},
+ {name: "sibling gguf layer is not matched", mediaType: "application/vnd.docker.ai.gguf.v3", want: false},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ assert.Equal(t, tt.want, supported(tt.mediaType))
+ })
+ }
+}
diff --git a/syft/pkg/cataloger/ai/processor.go b/syft/pkg/cataloger/ai/processor.go
index c3ca8c3e7..fd0e29292 100644
--- a/syft/pkg/cataloger/ai/processor.go
+++ b/syft/pkg/cataloger/ai/processor.go
@@ -1,59 +1,172 @@
package ai
import (
+ "context"
+ "path"
+ "sort"
+
+ "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
+ "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
)
-// ggufMergeProcessor consolidates multiple GGUF packages into a single package
-// representing the AI model. When scanning OCI images with multiple layers,
-// each layer may produce a separate package. This processor finds the package
-// with a name and merges metadata from nameless packages into its GGUFFileParts field.
-// Only packages with a non-empty name are returned in the final result.
-func ggufMergeProcessor(pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
- if err != nil {
- return pkgs, rels, err
- }
-
+// safeTensorsMergeProcessor owns naming, license resolution, and final package
+// assembly. SafeTensors packages reach it nameless from the parsers; it groups
+// them per model, merges the per-shard metadata, resolves a name + licenses, and
+// drops any model it cannot name.
+func safeTensorsMergeProcessor(ctx context.Context, resolver file.Resolver, pkgs []pkg.Package, rels []artifact.Relationship, err error) ([]pkg.Package, []artifact.Relationship, error) {
if len(pkgs) == 0 {
return pkgs, rels, err
}
- // Separate packages with names from those without
- var namedPkgs []pkg.Package
- var namelessHeaders []pkg.GGUFFileHeader
-
- for _, p := range pkgs {
- if p.Name != "" {
- namedPkgs = append(namedPkgs, p)
- } else {
- if header, ok := p.Metadata.(pkg.GGUFFileHeader); ok {
- // We do not want a kv hash for nameless headers
- header.MetadataKeyValuesHash = ""
- namelessHeaders = append(namelessHeaders, header)
- }
- }
+ // Note: we do NOT early-return when err != nil. A non-nil err here means some
+ // file in the run failed to parse, but the successfully-parsed packages are
+ // still nameless until this processor names or drops them. Skipping that work
+ // would let those nameless packages flow downstream, where they are silently
+ // dropped by missing-name compliance — turning one bad file into the loss of
+ // every otherwise-valid model. So we always name/drop and propagate err.
+ stPkgs, other := partitionSafeTensorsPackages(pkgs)
+ if len(stPkgs) == 0 {
+ return pkgs, rels, err
}
- // If there are no named packages, return nothing
- if len(namedPkgs) == 0 {
- return nil, rels, err
+ if fromOCIArtifact(stPkgs) {
+ return append(other, mergeOCIModel(ctx, resolver, stPkgs)...), rels, err
}
+ return append(other, mergeDirModels(ctx, resolver, stPkgs)...), rels, err
+}
- // merge nameless headers into a single named package;
- // if there are multiple named packages, return them without trying to merge headers.
- // we cannot determine which nameless headers belong to which package
- // this is because the order we receive the gguf headers in is not guaranteed
- // to match the layer order in the original oci image
- if len(namedPkgs) == 1 && len(namelessHeaders) > 0 {
- winner := &namedPkgs[0]
- if header, ok := winner.Metadata.(pkg.GGUFFileHeader); ok {
- header.Parts = namelessHeaders
- winner.Metadata = header
+// partitionSafeTensorsPackages separates safetensors packages from anything else
+// flowing through the processor.
+func partitionSafeTensorsPackages(pkgs []pkg.Package) (safeTensors, other []pkg.Package) {
+ for _, p := range pkgs {
+ if _, ok := p.Metadata.(pkg.SafeTensorsModelInfo); ok {
+ safeTensors = append(safeTensors, p)
+ continue
}
+ other = append(other, p)
}
+ return safeTensors, other
+}
- // Largest number of key value
+// fromOCIArtifact reports whether the packages came from an OCI model artifact.
+// That source (the ContainerImageModel resolver) presents every layer at the
+// virtual path "/", whereas a filesystem scan always carries a real file path. A
+// single scan is one source, so the first package is representative of the rest.
+func fromOCIArtifact(pkgs []pkg.Package) bool {
+ loc := primaryEvidenceLocation(pkgs[0])
+ return loc != nil && loc.RealPath == "/"
+}
- return namedPkgs, rels, err
+// mergeOCIModel treats the whole OCI artifact as a single model: every layer
+// merges into one package, named from the artifact's config.json/README or its
+// image reference.
+func mergeOCIModel(ctx context.Context, resolver file.Resolver, pkgs []pkg.Package) []pkg.Package {
+ merged := mergeSafeTensorsGroup(pkgs)
+
+ md := merged.Metadata.(pkg.SafeTensorsModelInfo)
+ id := resolveSafeTensorsOCIIdentity(ctx, resolver, &md)
+ merged.Metadata = md // write architecture enrichment back before assembly
+
+ if p, ok := assembleSafeTensorsPackage(merged, id); ok {
+ return []pkg.Package{p}
+ }
+ return nil
+}
+
+// mergeDirModels groups filesystem-scanned files by their parent directory and
+// emits one model per directory
+func mergeDirModels(ctx context.Context, resolver file.Resolver, pkgs []pkg.Package) []pkg.Package {
+ groups := groupByParentDir(pkgs)
+
+ // deterministic iteration order so the SBOM doesn't depend on map order
+ dirs := make([]string, 0, len(groups))
+ for dir := range groups {
+ dirs = append(dirs, dir)
+ }
+ sort.Strings(dirs)
+
+ var out []pkg.Package
+ for _, dir := range dirs {
+ merged := mergeSafeTensorsGroup(groups[dir])
+
+ md := merged.Metadata.(pkg.SafeTensorsModelInfo)
+ id := resolveSafeTensorsDirIdentity(ctx, resolver, dir, &md)
+ merged.Metadata = md // write architecture enrichment back before assembly
+
+ if p, ok := assembleSafeTensorsPackage(merged, id); ok {
+ out = append(out, p)
+ }
+ }
+ return out
+}
+
+// groupByParentDir buckets filesystem-scanned models by the directory their
+// primary-evidence file lives in.
+
+// This encodes a deliberate assumption: a directory holds one logical
+// model, so every .safetensors file in a directory is treated as a shard of the
+// same modeland merged into one package.
+// The trade-off is that if a directory happens to contain several unrelated models,
+// they are merged into one package rather than reported separately.
+// We accept that because the conventional on-disk layout gives each model
+// (with all of its shards) its own directory.
+// We have no reliable per-file signal to tell co-located-but-independent models apart.
+func groupByParentDir(pkgs []pkg.Package) map[string][]pkg.Package {
+ out := make(map[string][]pkg.Package)
+ for _, p := range pkgs {
+ loc := primaryEvidenceLocation(p)
+ if loc == nil {
+ continue
+ }
+ dir := path.Dir(loc.RealPath)
+ out[dir] = append(out[dir], p)
+ }
+ return out
+}
+
+func primaryEvidenceLocation(p pkg.Package) *file.Location {
+ locs := p.Locations.ToSlice()
+ for i, l := range locs {
+ if l.Annotations != nil && l.Annotations[pkg.EvidenceAnnotationKey] == pkg.PrimaryEvidenceAnnotation {
+ return &locs[i]
+ }
+ }
+ if len(locs) > 0 {
+ return &locs[0]
+ }
+ return nil
+}
+
+// safeTensorsIdentity is the fully-resolved naming/license result for a model.
+// Each source resolver (dir or OCI) populates it so assembly stays source-agnostic.
+type safeTensorsIdentity struct {
+ nameOrPath string
+ fallbackName string
+ licenses []pkg.License
+ supporting []file.Location
+}
+
+// assembleSafeTensorsPackage finalizes a merged model from its resolved identity:
+// it picks the name, attaches licenses and supporting evidence, and sets the ID.
+// A model with no name source is dropped (ok=false).
+func assembleSafeTensorsPackage(merged pkg.Package, id safeTensorsIdentity) (pkg.Package, bool) {
+ name := pickSafeTensorsName(id.nameOrPath, id.fallbackName)
+ if name == "" {
+ log.Debugf("dropped safetensors model package (metadata hash %q): no name source",
+ merged.Metadata.(pkg.SafeTensorsModelInfo).MetadataHash)
+ return pkg.Package{}, false
+ }
+
+ if len(id.licenses) > 0 {
+ merged.Licenses = pkg.NewLicenseSet(id.licenses...)
+ }
+ for _, loc := range id.supporting {
+ merged.Locations.Add(loc.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
+ }
+
+ merged.Name = name
+ merged.SetID()
+ return merged, true
}
diff --git a/syft/pkg/cataloger/ai/testdata/safetensors/README.md b/syft/pkg/cataloger/ai/testdata/safetensors/README.md
new file mode 100644
index 000000000..eecbe0577
--- /dev/null
+++ b/syft/pkg/cataloger/ai/testdata/safetensors/README.md
@@ -0,0 +1,28 @@
+# SafeTensors header fixtures
+
+These fixtures are `[8-byte length prefix + JSON header]` captures from
+public Docker AI model artifacts on the registry.
+
+`extract_header.go` does a range-GET of the first several MB of the layer,
+slices off just `[prefix + JSON header]`, and writes
+that to disk.
+
+## Refreshing a fixture
+
+```sh
+# from the package root
+go run ./testdata/safetensors/extract_header.go \
+ docker.io/ai/nomic-embed-text-v2-moe-safetensors:475M \
+ ./testdata/safetensors/nomic-embed-475M.header.safetensors
+```
+
+The tool prints the layer digest it selected and the number of top-level keys
+in the captured header. If you see `header length N does not fit in M fetched
+bytes`, raise `fetchBytes` in `extract_header.go` and rerun.
+
+## Notes
+
+- Pick one shard, not the full sharded set. The fixture is meant to exercise
+ the per-shard parser; merging across shards has its own tests.
+- Don't commit anything larger than ~1 MB. If a model has an unusually large
+ header, capture a smaller model instead.
diff --git a/syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go b/syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go
new file mode 100644
index 000000000..4f7d72a60
--- /dev/null
+++ b/syft/pkg/cataloger/ai/testdata/safetensors/extract_header.go
@@ -0,0 +1,149 @@
+// extract_header is a manual fixture tool that captures the real on-disk
+// safetensors header from a Docker AI OCI model artifact (a vnd.docker.ai.safetensors
+// layer) and writes just [8-byte length prefix + JSON header] to a destination
+// file. Tensor data following the header is never downloaded, so the resulting
+// fixture is a few KB to a few MB even for multi-GB models.
+//
+// This file lives under testdata/ so the Go build system ignores it. Run it
+// manually when refreshing fixtures:
+//
+// go run ./testdata/safetensors/extract_header.go \
+// docker.io/ai/nomic-embed-text-v2-moe-safetensors:475M \
+// ./testdata/safetensors/nomic-embed-475M.header.safetensors
+package main
+
+import (
+ "context"
+ "encoding/binary"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+)
+
+const (
+ safetensorsLayerMediaType = "application/vnd.docker.ai.safetensors"
+ // 8 MB matches maxHeaderBytes in the OCI model source. Real model headers
+ // are well under 1 MB; the extra slack covers outliers.
+ fetchBytes = 8 * 1024 * 1024
+)
+
+func main() {
+ if len(os.Args) != 3 {
+ fmt.Fprintf(os.Stderr, "usage: %s \n", os.Args[0])
+ os.Exit(2)
+ }
+ if err := run(os.Args[1], os.Args[2]); err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func run(refStr, outPath string) error {
+ ctx := context.Background()
+ ref, err := name.ParseReference(refStr)
+ if err != nil {
+ return fmt.Errorf("parse reference: %w", err)
+ }
+
+ opts := []remote.Option{
+ remote.WithAuthFromKeychain(authn.DefaultKeychain),
+ remote.WithContext(ctx),
+ }
+
+ desc, err := remote.Get(ref, opts...)
+ if err != nil {
+ return fmt.Errorf("fetch descriptor: %w", err)
+ }
+
+ manifest := &v1.Manifest{}
+ if err := json.Unmarshal(desc.Manifest, manifest); err != nil {
+ return fmt.Errorf("decode manifest: %w", err)
+ }
+
+ weightLayer := pickWeightLayer(manifest)
+ if weightLayer == nil {
+ return fmt.Errorf("no %q layer found in %s", safetensorsLayerMediaType, ref)
+ }
+ fmt.Fprintf(os.Stderr, "selected layer %s (%d bytes on-disk)\n", weightLayer.Digest, weightLayer.Size)
+
+ prefix, err := fetchPrefix(ctx, ref, weightLayer.Digest, opts)
+ if err != nil {
+ return fmt.Errorf("fetch layer prefix: %w", err)
+ }
+
+ header, err := sliceHeader(prefix)
+ if err != nil {
+ return fmt.Errorf("extract header: %w", err)
+ }
+
+ if err := os.WriteFile(outPath, header, 0o644); err != nil {
+ return fmt.Errorf("write fixture: %w", err)
+ }
+ fmt.Fprintf(os.Stderr, "wrote %d bytes to %s\n", len(header), outPath)
+ return nil
+}
+
+// pickWeightLayer returns the first vnd.docker.ai.safetensors layer in the
+// manifest, or nil if none exists. For sharded models we deliberately only
+// capture one shard: the fixture is meant to exercise the parser, not the
+// merge step.
+func pickWeightLayer(manifest *v1.Manifest) *v1.Descriptor {
+ for i := range manifest.Layers {
+ if string(manifest.Layers[i].MediaType) == safetensorsLayerMediaType {
+ return &manifest.Layers[i]
+ }
+ }
+ return nil
+}
+
+// fetchPrefix range-reads the first fetchBytes of a layer. Closing the reader
+// terminates the underlying HTTP body, so we never download the tensor data
+// that follows the header.
+func fetchPrefix(_ context.Context, ref name.Reference, digest v1.Hash, opts []remote.Option) ([]byte, error) {
+ layer, err := remote.Layer(ref.Context().Digest(digest.String()), opts...)
+ if err != nil {
+ return nil, err
+ }
+ reader, err := layer.Compressed()
+ if err != nil {
+ return nil, err
+ }
+ defer reader.Close()
+
+ buf := make([]byte, fetchBytes)
+ n, err := io.ReadFull(reader, buf)
+ if err != nil && err != io.ErrUnexpectedEOF {
+ return nil, err
+ }
+ return buf[:n], nil
+}
+
+// sliceHeader reads the 8-byte little-endian length prefix and returns just
+// [prefix + JSON header]. It also probes the JSON to make sure the captured
+// fixture is well-formed, so we never commit a half-truncated header.
+func sliceHeader(buf []byte) ([]byte, error) {
+ if len(buf) < 8 {
+ return nil, fmt.Errorf("short read: only %d bytes", len(buf))
+ }
+ headerLen := binary.LittleEndian.Uint64(buf[:8])
+ if headerLen == 0 {
+ return nil, fmt.Errorf("header length is zero")
+ }
+ if headerLen > uint64(len(buf)-8) {
+ return nil, fmt.Errorf("header length %d does not fit in %d fetched bytes; increase fetchBytes", headerLen, len(buf))
+ }
+
+ out := buf[:8+int(headerLen)]
+ var probe map[string]json.RawMessage
+ if err := json.Unmarshal(out[8:], &probe); err != nil {
+ return nil, fmt.Errorf("captured JSON does not parse: %w", err)
+ }
+ fmt.Fprintf(os.Stderr, "header parses cleanly: %d top-level keys\n", len(probe))
+ return out, nil
+}
diff --git a/syft/pkg/cataloger/ai/testdata/safetensors/nomic-embed-475M.header.safetensors b/syft/pkg/cataloger/ai/testdata/safetensors/nomic-embed-475M.header.safetensors
new file mode 100644
index 0000000000000000000000000000000000000000..8196899047b5b36b67712a58a4a5ad76a280aed8
GIT binary patch
literal 15520
zcmb`O+iqMn6oz@0x!xN2d_Y|D3S1x*rOl)*v`I(?3PODk9+!3S_Dm+r+rbW3rK$dJ
zmJh%G<#GP~;XC^8pWW5fm&5V;=K6Sjb+!BcpWW^K?)%-tarbT69lpG~`h2&4
z_v!ixe|mHL`{4jT{Lz}-vU~b?jsN#^4C$9;M;iCHw@-&7{M(<^VpTN1oHV{3K7IHI
z%Ku9RHI|^AD?*2xn@@Kip7zK4-wt;2=+|B*Ld
zDVJmh%HvP851|ap-TV8S!{h$*_1}lbr@h)=ACGtYpML-C&(3fgtK7@+uG&NiKBYwE
zv?;yOLXKPvvWT$6=#8^6^3aK_`#+9X50Cf1cB&F*rphxYB)3si1@#I8RHH7e1|fip
zLDhA4S=X1(5BuBqx>FIY{}=nBDicE|XL*yf7`hcpH+2GU<*ipu*?7Pw3@ooZyn
zD$Ix!8jFqx%i~t`{g3ahzGs8kqn)=I8<3F;MwTSi>aghL46psW}~qtScZn}@$14v)vD
z5eul|gb6Cb;e?0&*FAGcMN&jeXg(#|K{SQ#&HW(>DNmh3|Y4;2p}
z9>hmj9P4@Bb-Ko+RSNNvGDaTFVVgq$$uhv2EHA085TOW$-z1
zldh7;bUE8)U-!7e`nUa+j
zGs=n5E2tgrh(!Qk1#t8z_h2h)a67Ha46#b?!cLH0K{!LL!2=cnfEAq%5#~wg_{@yY
zt-6eeDl}FBTj?u`k!%hiq+B32h|MwAF*q%VK;41}7&Bfr$17r@4pc4>Xon;#wbLet
z#h^HIQdVLlKIilb+A=wy5dvsQn-Jt1+CrN%xmBN0*qKWW*d?l0+=j^kkSGYW!DK7i
zkDa}&JE=3s8Y^~6&p{JGkAOs3wDDORXb0q>M96v#Nx;FNS1F5zb}g7_#V(2~e3P74
zF--J2i~Wb4us=kQZb7UVE;HS1d%q{s@A~1(M*)Ou?HNGG>2-KwU-zIAvvLn#p(w
zA>{&r`ylB!ag><
ziRu-%VR8sW3IgGoiHBBQI9~m9#;fWDl;kfFpMz#H9s!ZEz(Xp0+93B$0&p
zHMpBrWr8@Erp5w91HXMg^EEA2!2n=+xQEE5v8mXBwl7*?B?vuc)nx<}*H{HpCI=s$
zlncbm&&R?<#=|-s>J~(DjcJ)OIfS4xLohH%P8z384mc#v9C%(OX5w>BZ`*8{9MA}X
zw3Ka}f-B9s
z#P0~Wd|!;ObvxLWq(z_pn{woE>XSW+V)34qyW%pc*z9UV8ZfFa~ZEn
zGX=&^j43Bdub_>Lhb#gB3%-+q4oz&zR@UHdT9peV%1zk`(klpOsBw=+RUiOZVCT`V
zzohm2WmcLoxmC9y;0d+73T84MeRx!62z-Cmm>3RVBk2JVq+1Zqh);%is9qJkC9_5d
zDi;VuFDc{YX_G?^iZcf;X++yO=k!+DmdOE)5J-dHcyjVpZlTSY+^SztQ4S`Ts9td!
zCWk8Tihk@oP5%aPS_lQWp5m6u$O>
zW=Fb&VBni%yozC>hXYt1Q}(72+du^AG6K3AvFXUgVPCNj1=~OfDHjO*W{Ny= 0 {
+ li, err := storeConfigBlobAsLayer(artifact, tempDir)
+ if err != nil {
+ cleanup()
+ return "", nil, err
+ }
+ layerFiles[artifact.Manifest.Config.Digest.String()] = li
+ }
+
+ // Companion layers (README, config.json, tokenizer.json, LICENSE). Small by
+ // convention; fetched in full up to maxCompanionBytes.
+ if artifact.Format == modelFormatSafeTensors {
+ for _, layer := range artifact.CompanionLayers {
+ li, err := fetchCompanionLayer(ctx, client, artifact.Reference, layer, tempDir)
+ if err != nil {
+ cleanup()
+ return "", nil, err
+ }
+ layerFiles[layer.Digest.String()] = li
+ }
+ }
+
+ // SafeTensors weight-layer headers. We only pull the leading prefix (same
+ // budget as a GGUF header)
+ if artifact.Format == modelFormatSafeTensors {
+ for _, layer := range artifact.SafeTensorsLayers {
+ li, err := fetchSafeTensorsLayerHeader(ctx, client, artifact.Reference, layer, tempDir)
+ if err != nil {
+ cleanup()
+ return "", nil, err
+ }
+ layerFiles[layer.Digest.String()] = li
+ }
+ }
+
+ resolver := fileresolver.NewContainerImageModel(
+ tempDir,
+ layerFiles,
+ artifact.Reference.String(),
+ )
return tempDir, resolver, nil
}
-// fetchSingleGGUFHeader fetches a single GGUF layer header and writes it to a temp file.
+// storeConfigBlobAsLayer writes the already-fetched raw config bytes to a temp
+// file so the resolver can serve them via media type
+func storeConfigBlobAsLayer(artifact *modelArtifact, tempDir string) (fileresolver.LayerInfo, error) {
+ digest := artifact.Manifest.Config.Digest.String()
+ safeDigest := strings.ReplaceAll(digest, ":", "-")
+ tempPath := filepath.Join(tempDir, safeDigest+".config.json")
+ if err := os.WriteFile(tempPath, artifact.RawConfig, 0600); err != nil {
+ return fileresolver.LayerInfo{}, fmt.Errorf("failed to write config blob: %w", err)
+ }
+ return fileresolver.LayerInfo{
+ TempPath: tempPath,
+ MediaType: string(artifact.Manifest.Config.MediaType),
+ }, nil
+}
+
+// fetchCompanionLayer downloads a companion (non-weight) layer to a temp file
+func fetchCompanionLayer(ctx context.Context, client *registryClient, ref name.Reference, layer v1.Descriptor, tempDir string) (fileresolver.LayerInfo, error) {
+ data, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxCompanionBytes)
+ if err != nil {
+ return fileresolver.LayerInfo{}, fmt.Errorf("failed to fetch companion layer: %w", err)
+ }
+ safeDigest := strings.ReplaceAll(layer.Digest.String(), ":", "-")
+ tempPath := filepath.Join(tempDir, safeDigest+".blob")
+ if err := os.WriteFile(tempPath, data, 0600); err != nil {
+ return fileresolver.LayerInfo{}, fmt.Errorf("failed to write companion temp file: %w", err)
+ }
+ return fileresolver.LayerInfo{
+ TempPath: tempPath,
+ MediaType: string(layer.MediaType),
+ }, nil
+}
+
+// fetchSingleGGUFHeader fetches a single GGUF layer header and writes it to a temp file
func fetchSingleGGUFHeader(ctx context.Context, client *registryClient, ref name.Reference, layer v1.Descriptor, tempDir string) (fileresolver.LayerInfo, error) {
- headerData, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxHeaderBytes)
+ headerData, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxWeightHeaderBytes)
if err != nil {
return fileresolver.LayerInfo{}, fmt.Errorf("failed to fetch GGUF layer header: %w", err)
}
@@ -129,7 +209,27 @@ func fetchSingleGGUFHeader(ctx context.Context, client *registryClient, ref name
}, nil
}
-// buildMetadata constructs OCIModelMetadata from a modelArtifact.
+// fetchSafeTensorsLayerHeader fetches the leading bytes of a SafeTensors weight
+// layer (enough to cover the JSON header) and writes them to a temp file
+func fetchSafeTensorsLayerHeader(ctx context.Context, client *registryClient, ref name.Reference, layer v1.Descriptor, tempDir string) (fileresolver.LayerInfo, error) {
+ headerData, err := client.fetchBlobRange(ctx, ref, layer.Digest, maxWeightHeaderBytes)
+ if err != nil {
+ return fileresolver.LayerInfo{}, fmt.Errorf("failed to fetch safetensors layer header: %w", err)
+ }
+
+ safeDigest := strings.ReplaceAll(layer.Digest.String(), ":", "-")
+ tempPath := filepath.Join(tempDir, safeDigest+".safetensors")
+ if err := os.WriteFile(tempPath, headerData, 0600); err != nil {
+ return fileresolver.LayerInfo{}, fmt.Errorf("failed to write temp file: %w", err)
+ }
+
+ return fileresolver.LayerInfo{
+ TempPath: tempPath,
+ MediaType: string(layer.MediaType),
+ }, nil
+}
+
+// buildMetadata constructs OCIModelMetadata from a modelArtifact
func buildMetadata(artifact *modelArtifact) source.OCIModelMetadata {
// layers
layers := make([]source.LayerMetadata, len(artifact.Manifest.Layers))
@@ -224,7 +324,8 @@ func (s *ociModelSource) Describe() source.Description {
}
}
-// FileResolver returns a file resolver for accessing header of GGUF files.
+// FileResolver returns a file resolver for accessing model headers and companion
+// metadata (GGUF/SafeTensors headers, the model config blob, and companion layers).
func (s *ociModelSource) FileResolver(_ source.Scope) (file.Resolver, error) {
return s.resolver, nil
}
diff --git a/syft/source/ocimodelsource/registry_client.go b/syft/source/ocimodelsource/registry_client.go
index 7574fda6c..8e6c6b9c7 100644
--- a/syft/source/ocimodelsource/registry_client.go
+++ b/syft/source/ocimodelsource/registry_client.go
@@ -26,9 +26,29 @@ const (
// Reference: https://www.docker.com/blog/oci-artifacts-for-ai-model-packaging/
modelConfigMediaTypePrefix = "application/vnd.docker.ai.model.config."
ggufLayerMediaType = "application/vnd.docker.ai.gguf.v3"
+ safetensorsLayerMediaType = "application/vnd.docker.ai.safetensors"
- // Maximum bytes to read/return for GGUF headers
- maxHeaderBytes = 8 * 1024 * 1024 // 8 MB
+ // Companion metadata layers packaged alongside the weight tensors.
+ // model.file covers README.md / config.json / tokenizer.json / generation_config.json.
+ modelFileMediaType = "application/vnd.docker.ai.model.file"
+ licenseMediaType = "application/vnd.docker.ai.license"
+
+ // Weight format labels surfaced on modelArtifact.Format.
+ modelFormatGGUF = "gguf"
+ modelFormatSafeTensors = "safetensors"
+
+ // maxWeightHeaderBytes is the leading slice we range-GET from a (multi-GB)
+ // weight layer — enough to cover the GGUF/safetensors header. Note this is
+ // smaller than the ai cataloger's own maxSafeTensorsHeaderSize (100 MB) parse
+ // ceiling: a safetensors header between the two is parseable from a directory
+ // scan but truncated here, so its shard would go uncounted on an OCI scan.
+ // Keep this comfortably above real-world header sizes.
+ maxWeightHeaderBytes = 8 * 1024 * 1024 // 8 MB
+
+ // maxCompanionBytes caps a whole companion blob (README, config.json,
+ // license); these are small by convention. Matches the 4 MB read cap in
+ // classifyOCIModelFileLayer.
+ maxCompanionBytes = 4 * 1024 * 1024 // 4 MB
)
// registryClient handles OCI registry interactions for model artifacts.
@@ -110,7 +130,20 @@ type modelArtifact struct {
RawManifest []byte
RawConfig []byte
ManifestDigest string
- GGUFLayers []v1.Descriptor
+
+ // Format identifies the weight storage format advertised by the manifest's
+ // layer media types. Empty means no recognized weight layers were found.
+ Format string
+
+ // GGUFLayers are descriptors for layers carrying GGUF-format weights.
+ // We fetch the first few MB of each to read the header data
+ GGUFLayers []v1.Descriptor
+
+ // SafeTensorsLayers are descriptors for layers carrying SafeTensors-format weights.
+ SafeTensorsLayers []v1.Descriptor
+
+ // CompanionLayers are non-weight layers (README, config.json, license)
+ CompanionLayers []v1.Descriptor
}
func (c *registryClient) fetchModelArtifact(ctx context.Context, refStr string) (*modelArtifact, error) {
@@ -151,18 +184,38 @@ func (c *registryClient) fetchModelArtifact(ctx context.Context, refStr string)
}
ggufLayers := extractGGUFLayers(manifest)
+ safetensorsLayers := extractSafeTensorsLayers(manifest)
+ companionLayers := extractCompanionLayers(manifest)
return &modelArtifact{
- Reference: ref,
- Manifest: manifest,
- Config: configFile,
- RawManifest: desc.Manifest,
- RawConfig: rawConfig,
- ManifestDigest: desc.Digest.String(),
- GGUFLayers: ggufLayers,
+ Reference: ref,
+ Manifest: manifest,
+ Config: configFile,
+ RawManifest: desc.Manifest,
+ RawConfig: rawConfig,
+ ManifestDigest: desc.Digest.String(),
+ Format: detectModelFormat(len(ggufLayers), len(safetensorsLayers)),
+ GGUFLayers: ggufLayers,
+ SafeTensorsLayers: safetensorsLayers,
+ CompanionLayers: companionLayers,
}, nil
}
+// detectModelFormat returns a single format string when either GGUF or
+// SafeTensors weight layers are present. GGUF wins if both somehow appear in one
+// artifact; Docker AI artifacts carry a single weight format in practice, so the
+// mixed case is not expected and the safetensors side would go uncataloged.
+func detectModelFormat(ggufCount, safetensorsCount int) string {
+ switch {
+ case ggufCount > 0:
+ return modelFormatGGUF
+ case safetensorsCount > 0:
+ return modelFormatSafeTensors
+ default:
+ return ""
+ }
+}
+
// isModelArtifact checks if the manifest represents a model artifact.
func isModelArtifact(manifest *v1.Manifest) bool {
return strings.HasPrefix(string(manifest.Config.MediaType), modelConfigMediaTypePrefix)
@@ -179,6 +232,33 @@ func extractGGUFLayers(manifest *v1.Manifest) []v1.Descriptor {
return ggufLayers
}
+// extractSafeTensorsLayers extracts SafeTensors weight-layer descriptors from
+// the manifest.
+func extractSafeTensorsLayers(manifest *v1.Manifest) []v1.Descriptor {
+ var out []v1.Descriptor
+ for _, layer := range manifest.Layers {
+ if string(layer.MediaType) == safetensorsLayerMediaType {
+ out = append(out, layer)
+ }
+ }
+ return out
+}
+
+// extractCompanionLayers extracts small, non-weight layers that carry
+// cataloger-relevant metadata: README.md / config.json / tokenizer.json /
+// generation_config.json under vnd.docker.ai.model.file, and the LICENSE under
+// vnd.docker.ai.license.
+func extractCompanionLayers(manifest *v1.Manifest) []v1.Descriptor {
+ var out []v1.Descriptor
+ for _, layer := range manifest.Layers {
+ switch string(layer.MediaType) {
+ case modelFileMediaType, licenseMediaType:
+ out = append(out, layer)
+ }
+ }
+ return out
+}
+
func (c *registryClient) fetchBlobRange(ctx context.Context, ref name.Reference, digest v1.Hash, maxBytes int64) ([]byte, error) {
repo := ref.Context()
@@ -189,18 +269,14 @@ func (c *registryClient) fetchBlobRange(ctx context.Context, ref name.Reference,
return nil, fmt.Errorf("failed to fetch layer: %w", err)
}
+ // Compressed() returns the raw stored blob. Docker AI weight/companion layers
+ // are stored uncompressed (their media types carry no +gzip suffix), so this
+ // is the header bytes as-is. A gzip-compressed layer would parse as garbage
+ // downstream rather than being transparently decompressed here.
reader, err := layer.Compressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer reader: %w", err)
}
- // this defer is what causes the download to stop
- // 1. io.ReadFull(reader, data) reads exactly 8MB into the buffer
- // 2. The function returns with data[:n]
- // 3. defer reader.Close() executes, closing the HTTP response body
- // 4. Closing the response body closes the underlying TCP connection
- // 5. The server receives TCP FIN/RST and stops sending
- // note: some data is already in flight when we close so we will see > 8mb over the wire
- // the full image will not download given we terminate the reader early here
defer reader.Close()
// Note: this is not some arbitrary number picked out of the blue.
@@ -208,8 +284,10 @@ func (c *registryClient) fetchBlobRange(ctx context.Context, ref name.Reference,
// https://github.com/ggml-org/ggml/blob/master/docs/gguf.md#file-structure
data := make([]byte, maxBytes)
n, err := io.ReadFull(reader, data)
- if err != nil && err != io.ErrUnexpectedEOF {
- // ErrUnexpectedEOF is okay - it means the file is smaller than maxBytes
+
+ // ErrUnexpectedEOF means the layer is smaller than maxBytes; EOF means it is
+ // empty. Both mean we read everything there was, not a failure.
+ if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && !errors.Is(err, io.EOF) {
return nil, fmt.Errorf("failed to read layer data: %w", err)
}
diff --git a/syft/source/ocimodelsource/registry_client_integration_test.go b/syft/source/ocimodelsource/registry_client_integration_test.go
new file mode 100644
index 000000000..9a554d340
--- /dev/null
+++ b/syft/source/ocimodelsource/registry_client_integration_test.go
@@ -0,0 +1,112 @@
+package ocimodelsource
+
+import (
+ "context"
+ "io"
+ stdlog "log"
+ "net/http/httptest"
+ "net/url"
+ "os"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/name"
+ "github.com/google/go-containerregistry/pkg/registry"
+ "github.com/google/go-containerregistry/pkg/v1/empty"
+ "github.com/google/go-containerregistry/pkg/v1/mutate"
+ "github.com/google/go-containerregistry/pkg/v1/remote"
+ "github.com/google/go-containerregistry/pkg/v1/static"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/stereoscope/pkg/image"
+ "github.com/anchore/syft/syft/internal/fileresolver"
+)
+
+// TestFetchAndStore_safetensors_inMemoryRegistry drives the full OCI fetch +
+// on-disk staging pipeline against a real (but in-process) registry, so nothing
+// touches the network. It pushes a synthetic Docker AI safetensors artifact —
+// config blob + one safetensors weight layer + a model.file companion + a
+// license layer — then asserts that fetchModelArtifact classifies the layers and
+// that fetchAndStoreModelHeaders stages every blob under the correct media type
+// with its bytes intact. This is the seam the mock-resolver merge tests assume is
+// correct; the staged content here is deliberately format-agnostic (header
+// parsing is covered in the ai package).
+func TestFetchAndStore_safetensors_inMemoryRegistry(t *testing.T) {
+ ctx := context.Background()
+
+ configMediaType := modelConfigMediaTypePrefix + "v0.2+json"
+ weightHeader := []byte("safetensors-header-bytes")
+ modelFile := []byte(`{"architectures":["LlamaForCausalLM"],"_name_or_path":"org/model"}`)
+ license := []byte("MIT license text")
+
+ img := mutate.ConfigMediaType(empty.Image, types.MediaType(configMediaType))
+ img = mutate.MediaType(img, types.OCIManifestSchema1)
+ img, err := mutate.Append(img,
+ layer(weightHeader, safetensorsLayerMediaType),
+ layer(modelFile, modelFileMediaType),
+ layer(license, licenseMediaType),
+ )
+ require.NoError(t, err)
+
+ // in-memory registry; localhost so go-containerregistry selects the http scheme
+ server := httptest.NewServer(registry.New(registry.Logger(stdlog.New(io.Discard, "", 0))))
+ defer server.Close()
+ u, err := url.Parse(server.URL)
+ require.NoError(t, err)
+ refStr := "localhost:" + u.Port() + "/testmodel:latest"
+
+ ref, err := name.ParseReference(refStr)
+ require.NoError(t, err)
+ require.NoError(t, remote.Write(ref, img, remote.WithContext(ctx)))
+
+ client := newRegistryClient(&image.RegistryOptions{InsecureUseHTTP: true})
+
+ art, err := client.fetchModelArtifact(ctx, refStr)
+ require.NoError(t, err)
+ assert.Equal(t, modelFormatSafeTensors, art.Format)
+ assert.Len(t, art.SafeTensorsLayers, 1)
+ assert.Len(t, art.CompanionLayers, 2) // model.file + license
+ assert.Empty(t, art.GGUFLayers)
+ assert.NotEmpty(t, art.RawConfig)
+ assert.NotEmpty(t, art.ManifestDigest)
+
+ tempDir, resolver, err := fetchAndStoreModelHeaders(ctx, client, art)
+ require.NoError(t, err)
+ defer os.RemoveAll(tempDir)
+
+ assert.Equal(t, refStr, resolver.ImageReference())
+
+ // every blob is staged under its own media type with bytes intact — this
+ // exercises fetchBlobRange's short-read (ErrUnexpectedEOF) branch, since each
+ // blob is far smaller than the fetch cap, plus all four staging helpers.
+ assertServesBytes(t, resolver, safetensorsLayerMediaType, weightHeader)
+ assertServesBytes(t, resolver, modelFileMediaType, modelFile)
+ assertServesBytes(t, resolver, licenseMediaType, license)
+
+ cfgLocs, err := resolver.FilesByMediaType(configMediaType)
+ require.NoError(t, err)
+ require.Len(t, cfgLocs, 1)
+}
+
+func layer(content []byte, mediaType string) mutate.Addendum {
+ return mutate.Addendum{
+ Layer: static.NewLayer(content, types.MediaType(mediaType)),
+ MediaType: types.MediaType(mediaType),
+ }
+}
+
+func assertServesBytes(t *testing.T, resolver *fileresolver.ContainerImageModel, mediaType string, want []byte) {
+ t.Helper()
+ locs, err := resolver.FilesByMediaType(mediaType)
+ require.NoError(t, err)
+ require.Len(t, locs, 1)
+
+ rc, err := resolver.FileContentsByLocation(locs[0])
+ require.NoError(t, err)
+ defer rc.Close()
+
+ got, err := io.ReadAll(rc)
+ require.NoError(t, err)
+ assert.Equal(t, want, got)
+}
diff --git a/syft/source/ocimodelsource/registry_client_safetensors_test.go b/syft/source/ocimodelsource/registry_client_safetensors_test.go
new file mode 100644
index 000000000..2ca91b4a2
--- /dev/null
+++ b/syft/source/ocimodelsource/registry_client_safetensors_test.go
@@ -0,0 +1,55 @@
+package ocimodelsource
+
+import (
+ "testing"
+
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+ "github.com/google/go-containerregistry/pkg/v1/types"
+ "github.com/stretchr/testify/assert"
+
+ "github.com/anchore/syft/syft/source"
+)
+
+func TestDetectModelFormat(t *testing.T) {
+ tests := []struct {
+ name string
+ gguf int
+ safetensors int
+ expected string
+ }{
+ {name: "gguf only", gguf: 2, safetensors: 0, expected: modelFormatGGUF},
+ {name: "safetensors only", gguf: 0, safetensors: 3, expected: modelFormatSafeTensors},
+ {name: "both prefers gguf", gguf: 1, safetensors: 1, expected: modelFormatGGUF},
+ {name: "neither", gguf: 0, safetensors: 0, expected: ""},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ assert.Equal(t, test.expected, detectModelFormat(test.gguf, test.safetensors))
+ })
+ }
+}
+
+func TestExtractSafeTensorsLayers(t *testing.T) {
+ manifest := &v1.Manifest{Layers: []v1.Descriptor{
+ {MediaType: types.MediaType(safetensorsLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "a"}},
+ {MediaType: types.MediaType(ggufLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "b"}},
+ {MediaType: types.MediaType(safetensorsLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "c"}},
+ }}
+ assert.Len(t, extractSafeTensorsLayers(manifest), 2)
+}
+
+func TestExtractCompanionLayers(t *testing.T) {
+ manifest := &v1.Manifest{Layers: []v1.Descriptor{
+ {MediaType: types.MediaType(modelFileMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "readme"}},
+ {MediaType: types.MediaType(licenseMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "license"}},
+ {MediaType: types.MediaType(safetensorsLayerMediaType), Digest: v1.Hash{Algorithm: "sha256", Hex: "weights"}},
+ {MediaType: types.DockerLayer, Digest: v1.Hash{Algorithm: "sha256", Hex: "other"}},
+ }}
+ // only the model.file and license layers should be selected (not weights or arbitrary layers)
+ assert.Len(t, extractCompanionLayers(manifest), 2)
+}
+
+func TestCalculateTotalSize(t *testing.T) {
+ layers := []source.LayerMetadata{{Size: 100}, {Size: 250}, {Size: 0}}
+ assert.Equal(t, int64(350), calculateTotalSize(layers))
+}
From 8ed534c32bc4b43fdc0111743c26ff9a328c53b5 Mon Sep 17 00:00:00 2001
From: Rez Moss
Date: Wed, 15 Jul 2026 20:55:42 -0400
Subject: [PATCH 074/168] fix: NPM redis.js CPE (#5012)
Signed-off-by: Rez Moss
---
.../internal/cpegenerate/candidate_by_package_type.go | 5 +++++
.../cpegenerate/candidate_by_package_type_test.go | 8 ++++++++
2 files changed, 13 insertions(+)
diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go
index 32c2131a1..b43906c8b 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go
+++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type.go
@@ -221,6 +221,11 @@ var defaultCandidateAdditions = buildCandidateLookup(
candidateKey{PkgName: "mustache"},
candidateAddition{AdditionalProducts: []string{"mustache.js"}},
},
+ {
+ pkg.NpmPkg,
+ candidateKey{PkgName: "redis"},
+ candidateAddition{AdditionalVendors: []string{"redis.js"}},
+ },
// Gem packages
{
diff --git a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go
index 4d903b14c..8f21eeae4 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go
+++ b/syft/pkg/cataloger/internal/cpegenerate/candidate_by_package_type_test.go
@@ -147,6 +147,14 @@ func Test_additionalVendors(t *testing.T) {
vendor: "my-vendor",
expected: []string{"awesome-vendor-addition", "one-good-addition", "another-good-addition"},
},
+ {
+ name: "default additionsnpm redis gets redisjs vendor",
+ allAdditions: defaultCandidateAdditions,
+ ty: pkg.NpmPkg,
+ pkgName: "redis",
+ vendor: "redis",
+ expected: []string{"redis.js"},
+ },
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
From 5e11775c8b76bd6cedc53e3c60cc02544575f0d5 Mon Sep 17 00:00:00 2001
From: Yashas Gunderia <68075205+ychampion@users.noreply.github.com>
Date: Thu, 16 Jul 2026 07:39:58 +0530
Subject: [PATCH 075/168] feat: detect unpackaged Firefox in software
inventories (#5068)
Signed-off-by: ychampion
---
syft/pkg/cataloger/binary/capabilities.yaml | 10 ++++++
.../binary/classifier_cataloger_test.go | 11 +++++++
syft/pkg/cataloger/binary/classifiers.go | 13 ++++++++
.../negative/firefox/application.ini | 3 ++
.../classifiers/negative/firefox/firefox | 1 +
.../firefox/151.0/linux-amd64/application.ini | 29 ++++++++++++++++++
.../firefox/151.0/linux-amd64/firefox | Bin 0 -> 325 bytes
.../pkg/cataloger/binary/testdata/config.yaml | 9 ++++++
8 files changed, 76 insertions(+)
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/firefox
diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml
index 18e265ae0..17f36be0b 100644
--- a/syft/pkg/cataloger/binary/capabilities.yaml
+++ b/syft/pkg/cataloger/binary/capabilities.yaml
@@ -666,6 +666,16 @@ catalogers:
cpes:
- cpe:2.3:a:google:chrome:*:*:*:*:*:*:*:*
type: BinaryPkg
+ - method: glob
+ criteria:
+ - '**/{firefox,firefox.exe}'
+ packages:
+ - class: firefox-binary
+ name: firefox
+ purl: pkg:generic/firefox
+ cpes:
+ - cpe:2.3:a:mozilla:firefox:*:*:*:*:*:*:*:*
+ type: BinaryPkg
- method: glob
criteria:
- '**/ffmpeg'
diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go
index ab5af2703..286699a76 100644
--- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go
+++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go
@@ -1951,6 +1951,17 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("chrome-binary"),
},
},
+ {
+ logicalFixture: "firefox/151.0/linux-amd64",
+ expected: pkg.Package{
+ Name: "firefox",
+ Version: "151.0",
+ Type: "binary",
+ PURL: "pkg:generic/firefox@151.0",
+ Locations: locations("firefox", "application.ini"),
+ Metadata: metadata("firefox-binary"),
+ },
+ },
{
logicalFixture: "ffmpeg/7.1.1/darwin-arm64",
expected: pkg.Package{
diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go
index 52a9be871..9c5adeea4 100644
--- a/syft/pkg/cataloger/binary/classifiers.go
+++ b/syft/pkg/cataloger/binary/classifiers.go
@@ -913,6 +913,19 @@ func DefaultClassifiers() []binutils.Classifier {
PURL: mustPURL("pkg:generic/chrome@version"),
CPEs: singleCPE("cpe:2.3:a:google:chrome:*:*:*:*:*:*:*:*"),
},
+ {
+ Class: "firefox-binary",
+ FileGlob: "**/{firefox,firefox.exe}",
+ EvidenceMatcher: binutils.SupportingEvidenceMatcher(
+ "application.ini",
+ m.FileContentsVersionMatcher(
+ `(?ms)^Name=Firefox\r?$.*?^Version=(?P[0-9]+(?:\.[0-9]+)+(?:[A-Za-z][0-9A-Za-z.-]*)?)\r?$`,
+ ),
+ ),
+ Package: "firefox",
+ PURL: mustPURL("pkg:generic/firefox@version"),
+ CPEs: singleCPE("cpe:2.3:a:mozilla:firefox:*:*:*:*:*:*:*:*"),
+ },
{
Class: "ffmpeg-binary",
FileGlob: "**/ffmpeg",
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini
new file mode 100644
index 000000000..1a917ca96
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/application.ini
@@ -0,0 +1,3 @@
+[App]
+Name=Thunderbird
+Version=151.0
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox
new file mode 100644
index 000000000..b1dc0970c
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/classifiers/negative/firefox/firefox
@@ -0,0 +1 @@
+not a Firefox executable
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini
new file mode 100644
index 000000000..eca8b2888
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/application.ini
@@ -0,0 +1,29 @@
+name: application.ini
+offset: 0
+length: 641
+snippetSha256: 286d4471ecf20596e8abde1ec5cb11456db06a6f73593c7b983f502b0b9b3011
+fileSha256: 286d4471ecf20596e8abde1ec5cb11456db06a6f73593c7b983f502b0b9b3011
+
+### byte snippet to follow ###
+; This file is not used. If you modify it and want the application to use
+; your modifications, move it under the browser/ subdirectory and start with
+; the "-app /path/to/browser/application.ini" argument.
+[App]
+Vendor=Mozilla
+Name=Firefox
+RemotingName=firefox-default
+CodeName=Nightly
+Version=151.0
+BuildID=20260611122700
+ID={ec8030f7-c20a-464f-9b0e-13a3a9e97384}
+
+[Gecko]
+MinVersion=151.0
+MaxVersion=151.0
+
+[XRE]
+EnableProfileMigrator=1
+
+
+[AppUpdate]
+URL=https://aus5.mozilla.org/update/6/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/firefox b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/firefox/151.0/linux-amd64/firefox
new file mode 100644
index 0000000000000000000000000000000000000000..09fd7009b13619b127e891ac959446c0a64ec46e
GIT binary patch
literal 325
zcmZ9GOG?B*5Qck35otVwHsk{RsD301$wpW1+zb7vnhC^Ya02Set9T;MWHV%3SWs2`
z|5yCf6ko9uG`u0rAABY{;?fDpN1S%cz7tAHew>EG0hi}})Yf&PMenRbWm55~P|5@f
z$U_dAN);Gm63!2Aqeis$-uZO3{?PkNyb;0i0Nxf`O
gR=)!AbA3KxJqzrn&O3I?uBv-kyI<-5{as^z0ZSc5od5s;
literal 0
HcmV?d00001
diff --git a/syft/pkg/cataloger/binary/testdata/config.yaml b/syft/pkg/cataloger/binary/testdata/config.yaml
index 5f8fa88aa..3bab3a99c 100644
--- a/syft/pkg/cataloger/binary/testdata/config.yaml
+++ b/syft/pkg/cataloger/binary/testdata/config.yaml
@@ -17,6 +17,15 @@ from-images:
paths:
- /bin/[
+ - name: firefox
+ version: "151.0"
+ images:
+ - ref: mcr.microsoft.com/playwright:v1.61.1-noble@sha256:cf0daee9b994042e011bc29f20cdff1a9f682a039b43fcd738f7d8a9d3bcd9d6
+ platform: linux/amd64
+ paths:
+ - /ms-playwright/firefox-1532/firefox/firefox
+ - /ms-playwright/firefox-1532/firefox/application.ini
+
- name: util-linux
version: 2.37.4
images:
From 3e2bc6ed095f7ec1a415fb38cfe1c319e95dfed6 Mon Sep 17 00:00:00 2001
From: Christopher Angelo Phillips <32073428+spiffcs@users.noreply.github.com>
Date: Thu, 16 Jul 2026 10:21:22 -0400
Subject: [PATCH 076/168] ci: add docker-container driver (#5071)
* ci: add docker-container driver
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
* fix: update to v4.2.0
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
---------
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
---
.github/workflows/release.yaml | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 39e4fe7da..35f09eeab 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -83,6 +83,11 @@ jobs:
- name: Bootstrap environment
uses: ./.github/actions/bootstrap
+ # dockers_v2 builds multi-platform manifests in a single buildx invocation, which requires a
+ # docker-container driver builder (the default builder on the runner cannot build multi-platform).
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
+
- name: Login to Docker Hub
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 #v4.4.0
with:
From 36a3af41d95e179c79901a3e205b41cacd55b72c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 17 Jul 2026 13:32:07 +0000
Subject: [PATCH 077/168] chore(deps): bump golang.org/x/net from 0.56.0 to
0.57.0 (#5076)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.56.0 to 0.57.0.
- [Commits](https://github.com/golang/net/compare/v0.56.0...v0.57.0)
---
updated-dependencies:
- dependency-name: golang.org/x/net
dependency-version: 0.57.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 | 12 ++++++------
go.sum | 24 ++++++++++++------------
2 files changed, 18 insertions(+), 18 deletions(-)
diff --git a/go.mod b/go.mod
index dc3109696..b220b7d54 100644
--- a/go.mod
+++ b/go.mod
@@ -94,7 +94,7 @@ require (
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/mod v0.37.0
- golang.org/x/net v0.56.0
+ golang.org/x/net v0.57.0
golang.org/x/time v0.15.0
golang.org/x/tools v0.47.0
gopkg.in/yaml.v3 v3.0.1
@@ -309,12 +309,12 @@ require (
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
- golang.org/x/crypto v0.53.0 // indirect
+ golang.org/x/crypto v0.54.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
- golang.org/x/sync v0.21.0 // indirect
- golang.org/x/sys v0.46.0 // indirect
- golang.org/x/term v0.44.0 // indirect
- golang.org/x/text v0.38.0 // indirect
+ golang.org/x/sync v0.22.0 // indirect
+ golang.org/x/sys v0.47.0 // indirect
+ golang.org/x/term v0.45.0 // indirect
+ golang.org/x/text v0.40.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.271.0 // indirect
diff --git a/go.sum b/go.sum
index bb20bd808..a18873431 100644
--- a/go.sum
+++ b/go.sum
@@ -1040,8 +1040,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
-golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
+golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
+golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1127,8 +1127,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
-golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
+golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
+golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1160,8 +1160,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
-golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1236,13 +1236,13 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
-golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
-golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
+golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
+golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1253,8 +1253,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
-golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
+golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
From dc1ef98237e6df35d4cbe3ea410bbeef33769788 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 17 Jul 2026 13:52:22 +0000
Subject: [PATCH 078/168] chore(deps): bump golang.org/x/tools from 0.47.0 to
0.48.0 (#5077)
Bumps [golang.org/x/tools](https://github.com/golang/tools) from 0.47.0 to 0.48.0.
- [Release notes](https://github.com/golang/tools/releases)
- [Commits](https://github.com/golang/tools/compare/v0.47.0...v0.48.0)
---
updated-dependencies:
- dependency-name: golang.org/x/tools
dependency-version: 0.48.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 | 4 ++--
go.sum | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/go.mod b/go.mod
index b220b7d54..ec554138d 100644
--- a/go.mod
+++ b/go.mod
@@ -93,10 +93,10 @@ require (
go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
- golang.org/x/mod v0.37.0
+ golang.org/x/mod v0.38.0
golang.org/x/net v0.57.0
golang.org/x/time v0.15.0
- golang.org/x/tools v0.47.0
+ golang.org/x/tools v0.48.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.53.0
)
diff --git a/go.sum b/go.sum
index a18873431..f677e0766 100644
--- a/go.sum
+++ b/go.sum
@@ -1081,8 +1081,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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
-golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
+golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
+golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
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=
@@ -1315,8 +1315,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.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
-golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
+golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
+golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
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 ae9534203d4a49f2b96a05dfc1383e2376993c72 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 17 Jul 2026 13:52:25 +0000
Subject: [PATCH 079/168] chore(deps): bump golang.org/x/mod from 0.37.0 to
0.38.0 (#5078)
Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/mod/compare/v0.37.0...v0.38.0)
---
updated-dependencies:
- dependency-name: golang.org/x/mod
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>
From d380ad08b5945058ca4c4662ca1f3a8dfd7db7f9 Mon Sep 17 00:00:00 2001
From: anchore-oss-update-bot
Date: Mon, 20 Jul 2026 11:21:24 -0400
Subject: [PATCH 080/168] chore(deps): update CPE dictionary index (#5082)
Signed-off-by: anchore-oss-update-bot
Co-authored-by: anchore-oss-update-bot
---
.../dictionary/data/cpe-index.json | 35 +++++++++++++++++--
1 file changed, 33 insertions(+), 2 deletions(-)
diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
index 694bf48a5..b9385c447 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
+++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
@@ -7,6 +7,9 @@
"chainguard.dev/apko": [
"cpe:2.3:a:chainguard:apko:*:*:*:*:*:go:*:*"
],
+ "github.com/Azure/go-ntlmssp": [
+ "cpe:2.3:a:microsoft:go-ntlmssp:*:*:*:*:*:go:*:*"
+ ],
"github.com/ClickHouse/ch-go": [
"cpe:2.3:a:clickhouse:ch:*:*:*:*:*:go:*:*"
],
@@ -223,6 +226,9 @@
"github.com/xddxdd/bird-lg-go": [
"cpe:2.3:a:xddxdd:bird-lg-go:*:*:*:*:*:go:*:*"
],
+ "github.com/xuri/excelize/v2": [
+ "cpe:2.3:a:excelize:excelize:*:*:*:*:*:go:*:*"
+ ],
"github.com/yuin/goldmark": [
"cpe:2.3:a:yuin:goldmark:*:*:*:*:*:go:*:*"
],
@@ -2349,6 +2355,9 @@
"@apollosproject/data-connector-rock": [
"cpe:2.3:a:apollosapp:data-connector-rock:*:*:*:*:*:node.js:*:*"
],
+ "@appium/base-driver": [
+ "cpe:2.3:a:appium:appium\\/base-driver:*:*:*:*:*:node.js:*:*"
+ ],
"@appium/support": [
"cpe:2.3:a:appium:appium\\/support:*:*:*:*:*:node.js:*:*"
],
@@ -2616,6 +2625,9 @@
"@octokit/webhooks": [
"cpe:2.3:a:octokit:webhooks:*:*:*:*:*:node.js:*:*"
],
+ "@openclaw/feishu": [
+ "cpe:2.3:a:openclaw:openclaw\\/feishu:*:*:*:*:*:node.js:*:*"
+ ],
"@openclaw/voice-call": [
"cpe:2.3:a:openclaw:openclaw\\/voice-call:*:*:*:*:*:node.js:*:*"
],
@@ -3086,6 +3098,9 @@
"cli": [
"cpe:2.3:a:cli_project:cli:*:*:*:*:*:node.js:*:*"
],
+ "client-certificate-auth": [
+ "cpe:2.3:a:tgies:client-certificate-auth:*:*:*:*:*:node.js:*:*"
+ ],
"closure-compiler-stream": [
"cpe:2.3:a:closure-compiler-stream_project:closure-compiler-stream:*:*:*:*:*:node.js:*:*"
],
@@ -5752,6 +5767,9 @@
"ssrf-agent": [
"cpe:2.3:a:ssrf-agent_project:ssrf-agent:*:*:*:*:*:node.js:*:*"
],
+ "ssrfcheck": [
+ "cpe:2.3:a:felippe-regazio:ssrf_check:*:*:*:*:*:node.js:*:*"
+ ],
"ssri": [
"cpe:2.3:a:ssri_project:ssri:*:*:*:*:*:node.js:*:*"
],
@@ -6978,6 +6996,9 @@
"sqlparse": [
"cpe:2.3:a:sqlparse_project:sqlparse:*:*:*:*:*:python:*:*"
],
+ "stanza": [
+ "cpe:2.3:a:stanford:stanza:*:*:*:*:*:python:*:*"
+ ],
"starlette": [
"cpe:2.3:a:encode:starlette:*:*:*:*:*:python:*:*"
],
@@ -7690,6 +7711,12 @@
"bzip2": [
"cpe:2.3:a:bzip2_project:bzip2:*:*:*:*:*:rust:*:*"
],
+ "c2pa": [
+ "cpe:2.3:a:adobe:c2pa:*:*:*:*:*:rust:*:*"
+ ],
+ "c2patool": [
+ "cpe:2.3:a:adobe:c2patool:*:*:*:*:*:*:*:*"
+ ],
"cache": [
"cpe:2.3:a:cache_project:cache:*:*:*:*:*:rust:*:*"
],
@@ -9175,7 +9202,8 @@
"cpe:2.3:a:article_directory_project:article_directory:*:*:*:*:*:wordpress:*:*"
],
"article-directory-redux": [
- "cpe:2.3:a:icontrolwp:article_directory_redux:*:*:*:*:*:wordpress:*:*"
+ "cpe:2.3:a:icontrolwp:article_directory_redux:*:*:*:*:*:wordpress:*:*",
+ "cpe:2.3:a:icontrolwp:icontrolwp:*:*:*:*:*:wordpress:*:*"
],
"article2pdf": [
"cpe:2.3:a:article2pdf_project:article2pdf:*:*:*:*:*:wordpress:*:*"
@@ -13831,6 +13859,9 @@
"ibtana-visual-editor": [
"cpe:2.3:a:vowelweb:ibtana:*:*:*:*:*:wordpress:*:*"
],
+ "ibuildapp": [
+ "cpe:2.3:a:ibuildapp:ibuildapp:*:*:*:*:*:wordpress:*:*"
+ ],
"icegram": [
"cpe:2.3:a:icegram:icegram_engage:*:*:*:*:*:wordpress:*:*"
],
@@ -15508,7 +15539,7 @@
"cpe:2.3:a:motor_racing_league_project:motor_racing_league:*:*:*:*:*:wordpress:*:*"
],
"motors-car-dealership-classified-listings": [
- "cpe:2.3:a:stylemixthemes:motors_-_car_dealer\\,_classifieds_\\\u0026_listing:*:*:*:*:*:wordpress:*:*"
+ "cpe:2.3:a:stylemixthemes:motors_-_car_dealer\\,_classifieds_\\\u0026_listing:*:*:*:*:starter:wordpress:*:*"
],
"mousewheel-smooth-scroll": [
"cpe:2.3:a:mousewheel_smooth_scroll_project:mousewheel_smooth_scroll:*:*:*:*:*:wordpress:*:*"
From a0852ebf987efdd44a5fa33e26cc12dbcff0ae1f Mon Sep 17 00:00:00 2001
From: Jason Paulos
Date: Mon, 20 Jul 2026 11:28:29 -0400
Subject: [PATCH 081/168] Add multi-platform OCI image support (#5074)
* Add multi-platform OCI image support
Signed-off-by: Jason Paulos
* Reduce calls to PrepareMultiplatformFixtureImage in TestMultiPlatformOCIImageSelection
Signed-off-by: Jason Paulos
* Use smaller image for testing & update stereoscope fork
Signed-off-by: Jason Paulos
* bump to stereoscope@main after 548 merge
Signed-off-by: Alex Goodman
---------
Signed-off-by: Jason Paulos
Signed-off-by: Alex Goodman
Co-authored-by: Alex Goodman
---
.../integration/multiplatform_oci_test.go | 145 ++++++++++++++++++
.../test/integration/testdata/.gitignore | 4 +-
go.mod | 14 +-
go.sum | 28 ++--
4 files changed, 169 insertions(+), 22 deletions(-)
create mode 100644 cmd/syft/internal/test/integration/multiplatform_oci_test.go
diff --git a/cmd/syft/internal/test/integration/multiplatform_oci_test.go b/cmd/syft/internal/test/integration/multiplatform_oci_test.go
new file mode 100644
index 000000000..94879a190
--- /dev/null
+++ b/cmd/syft/internal/test/integration/multiplatform_oci_test.go
@@ -0,0 +1,145 @@
+package integration
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "runtime"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/anchore/stereoscope/pkg/image"
+ "github.com/anchore/stereoscope/pkg/imagetest"
+ "github.com/anchore/syft/syft"
+ "github.com/anchore/syft/syft/source"
+)
+
+// TestMultiPlatformOCIImageSelection verifies that syft can load a single platform out of a
+// multi-platform OCI image on disk (both --from oci-dir and --from oci-archive) by selecting the
+// image that matches the requested platform.
+func TestMultiPlatformOCIImageSelection(t *testing.T) {
+ remoteImage := "docker.io/library/busybox:1.38.0"
+
+ // Per-platform image config digests within the multi-platform index, obtained from the OCI layout
+ // (see anchore/stereoscope integration tests: TestPlatformSelectionWithOciLocalSources).
+ expectedDigest := map[string]string{
+ "arm64": "sha256:e0e8b3cbfed68a90084781e2962f9c0deead51c5a3f11a488eef0283a4284bc2",
+ "s390x": "sha256:0cf160e720a8e4f20883b72276a6eaded83b79539f3f1d39e35a3336154b9960",
+ "amd64": "sha256:c6348fa86ba0fb2108c9334f5fe913ddc6d853313e655891f133a0127c30099f",
+ "ppc64le": "sha256:b144fc0e06537d07956cde340b878a81a717e394edb736d3110858a12a6635cb",
+ }
+
+ // syft --from source tag -> stereoscope OCI source used to prepare the local fixture
+ sources := map[string]image.Source{
+ "oci-dir": image.OciDirectorySource,
+ "oci-archive": image.OciTarballSource,
+ }
+
+ for from, imageSource := range sources {
+ t.Run(from, func(t *testing.T) {
+ localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
+ for _, arch := range []string{"amd64", "arm64", "s390x", "ppc64le"} {
+ t.Run(fmt.Sprintf("linux/%s", arch), func(t *testing.T) {
+ platform, err := image.NewPlatform("linux/" + arch)
+ require.NoError(t, err)
+
+ src, err := syft.GetSource(
+ context.Background(),
+ localPath,
+ syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, src.Close())
+ })
+
+ meta, ok := src.Describe().Metadata.(source.ImageMetadata)
+ require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
+
+ // The raw config of the selected image must match the requested platform...
+ assertConfigPlatform(t, meta.RawConfig, "linux", arch)
+ // ...and it must be the exact per-platform image from the multi-platform index.
+ assert.Equal(t, expectedDigest[arch], meta.ID)
+ })
+ }
+ })
+ }
+}
+
+// TestMultiPlatformOCIImageSelection_UnavailablePlatform verifies that requesting a platform not present
+// in the multi-platform OCI image results in an error rather than silently selecting the wrong image.
+func TestMultiPlatformOCIImageSelection_UnavailablePlatform(t *testing.T) {
+ remoteImage := "docker.io/library/busybox:1.38.0"
+
+ // windows/amd64 is not present in this linux-only multi-platform image
+ platform, err := image.NewPlatform("windows/amd64")
+ require.NoError(t, err)
+
+ sources := map[string]image.Source{
+ "oci-dir": image.OciDirectorySource,
+ "oci-archive": image.OciTarballSource,
+ }
+
+ for from, imageSource := range sources {
+ t.Run(from, func(t *testing.T) {
+ localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
+
+ _, err := syft.GetSource(
+ context.Background(),
+ localPath,
+ syft.DefaultGetSourceConfig().WithSources(from).WithPlatform(platform),
+ )
+ require.ErrorContains(t, err, "windows/amd64")
+ })
+ }
+}
+
+// TestMultiPlatformOCIImageSelection_DefaultPlatform verifies that not specifying a platform results
+// in the current platform being selected.
+func TestMultiPlatformOCIImageSelection_DefaultPlatform(t *testing.T) {
+ remoteImage := "docker.io/library/busybox:1.38.0"
+
+ sources := map[string]image.Source{
+ "oci-dir": image.OciDirectorySource,
+ "oci-archive": image.OciTarballSource,
+ }
+
+ for from, imageSource := range sources {
+ t.Run(from, func(t *testing.T) {
+ localPath := imagetest.PrepareMultiplatformFixtureImage(t, imageSource, remoteImage)
+
+ src, err := syft.GetSource(
+ context.Background(),
+ localPath,
+ syft.DefaultGetSourceConfig().WithSources(from),
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ require.NoError(t, src.Close())
+ })
+
+ meta, ok := src.Describe().Metadata.(source.ImageMetadata)
+ require.True(t, ok, "expected image metadata, got %T", src.Describe().Metadata)
+
+ // The raw config of the selected image must match the current platform
+ assertConfigPlatform(t, meta.RawConfig, "linux", runtime.GOARCH)
+ })
+ }
+}
+
+// assertConfigPlatform asserts the os/architecture recorded in a raw OCI image config document.
+func assertConfigPlatform(t *testing.T, rawConfig []byte, os, architecture string) {
+ t.Helper()
+ require.NotEmpty(t, rawConfig)
+
+ var cfg struct {
+ OS string `json:"os"`
+ Architecture string `json:"architecture"`
+ }
+ require.NoError(t, json.Unmarshal(rawConfig, &cfg))
+
+ assert.Equal(t, os, cfg.OS)
+ assert.Equal(t, architecture, cfg.Architecture)
+}
diff --git a/cmd/syft/internal/test/integration/testdata/.gitignore b/cmd/syft/internal/test/integration/testdata/.gitignore
index 48c7e0961..ddc9f73c0 100644
--- a/cmd/syft/internal/test/integration/testdata/.gitignore
+++ b/cmd/syft/internal/test/integration/testdata/.gitignore
@@ -7,4 +7,6 @@
!image-pkg-coverage/pkgs/java/*.hpi
**/go.sum
-!image-go-bin-arch-coverage/go.sum
\ No newline at end of file
+!image-go-bin-arch-coverage/go.sum
+
+cache/
\ No newline at end of file
diff --git a/go.mod b/go.mod
index ec554138d..0a29a383e 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
github.com/anchore/go-sync v0.1.1
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.2.0
- github.com/anchore/stereoscope v0.2.2
+ github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567
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
@@ -40,7 +40,7 @@ require (
github.com/elliotchance/phpserialize v1.4.0
github.com/facebookincubator/nvdtools v0.1.5
github.com/github/go-spdx/v2 v2.7.0
- github.com/gkampitakis/go-snaps v0.5.22
+ github.com/gkampitakis/go-snaps v0.5.23
github.com/go-git/go-billy/v5 v5.9.0
github.com/go-git/go-git/v5 v5.19.1
github.com/go-test/deep v1.1.1
@@ -171,7 +171,7 @@ require (
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/containerd/cgroups/v3 v3.1.3 // indirect
github.com/containerd/containerd/api v1.11.1 // indirect
- github.com/containerd/containerd/v2 v2.3.2 // indirect
+ github.com/containerd/containerd/v2 v2.3.3 // indirect
github.com/containerd/continuity v0.5.0 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
@@ -183,7 +183,7 @@ require (
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
- github.com/docker/cli v29.5.3+incompatible // indirect
+ github.com/docker/cli v29.6.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.7.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
@@ -238,8 +238,8 @@ require (
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/locker v1.0.1 // indirect
- github.com/moby/moby/api v1.54.2 // indirect
- github.com/moby/moby/client v0.4.1 // indirect
+ github.com/moby/moby/api v1.55.0 // indirect
+ github.com/moby/moby/client v0.5.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/signal v0.7.1 // indirect
github.com/moby/sys/user v0.4.0 // indirect
@@ -257,7 +257,7 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runtime-spec v1.3.0 // indirect
github.com/pborman/indent v1.2.1 // indirect
- github.com/pelletier/go-toml/v2 v2.3.1 // indirect
+ github.com/pelletier/go-toml/v2 v2.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.26 // indirect
github.com/piprate/json-gold v0.7.0 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
diff --git a/go.sum b/go.sum
index f677e0766..1c8d9df09 100644
--- a/go.sum
+++ b/go.sum
@@ -146,8 +146,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.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
-github.com/anchore/stereoscope v0.2.2 h1:SGTLGoF6GHmKEn9Bb6LYpzzgz9nhvMZgN/c4fTSQjYY=
-github.com/anchore/stereoscope v0.2.2/go.mod h1:ylJXJKebLctP7u9ewguB1d0zUETJxvAA7r2DiuljDTM=
+github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567 h1:ZklP3dHyXFUKPrZaQcqP6M86KzlbpayiEvUUeQRT/BI=
+github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567/go.mod h1:QIBWxa5WCrjtBqXH0+RIuECATrffcbaNdMWquZ83+OI=
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=
@@ -296,8 +296,8 @@ github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
github.com/containerd/containerd/api v1.11.1/go.mod h1:CaQFRu+N1MtbgL6JDOJLUB1hCKESU1lD6MuTJhgtdlw=
-github.com/containerd/containerd/v2 v2.3.2 h1:eLven1YxRMkeiKu7IcMrPKE+gn8sGR1DqHbbshMEvWM=
-github.com/containerd/containerd/v2 v2.3.2/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4=
+github.com/containerd/containerd/v2 v2.3.3 h1:MUNBVVBTBpPll7KPh5GTvkC3cfG03PQLAHVdsUoue9k=
+github.com/containerd/containerd/v2 v2.3.3/go.mod h1:rHKGm3VW6wNrINb3x8mNT+w7qYXFVElTt/8HTuxVhD4=
github.com/containerd/continuity v0.5.0 h1:7a85HZpCSs+1Zps0Ee3DPSuAWY+0SJM1JNM51nlEVDg=
github.com/containerd/continuity v0.5.0/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
@@ -339,8 +339,8 @@ 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 v29.5.3+incompatible h1:nbEFfz774vBwQ5KRYv7c/AghjReqnGISvrRhzjV0evs=
-github.com/docker/cli v29.5.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v29.6.1+incompatible h1:oO7F4nn3Ovr/5TlfTUWFbMwBSS/B7Xs6Epv26gBrUP8=
+github.com/docker/cli v29.6.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
@@ -410,8 +410,8 @@ github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XF
github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0=
github.com/gkampitakis/ciinfo v0.3.4 h1:5eBSibVuSMbb/H6Elc0IIEFbkzCJi3lm94n0+U7Z0KY=
github.com/gkampitakis/ciinfo v0.3.4/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
-github.com/gkampitakis/go-snaps v0.5.22 h1:xg9omphRnbDnimMCl1KqznC4krlxOGpkB0vDSfX2P7M=
-github.com/gkampitakis/go-snaps v0.5.22/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM=
+github.com/gkampitakis/go-snaps v0.5.23 h1:okh5QR48zpUjpWtu65AtqxdCY8huJq+dEDuUzd1PuKg=
+github.com/gkampitakis/go-snaps v0.5.23/go.mod h1:uy3lVzCCRRsAwYqSocyw5fY8xRLCYEfqoOJNxr8HonM=
github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4=
github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@@ -723,10 +723,10 @@ github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3N
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
-github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
-github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
-github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
-github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
+github.com/moby/moby/api v1.55.0 h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
+github.com/moby/moby/api v1.55.0/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
+github.com/moby/moby/client v0.5.0 h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
+github.com/moby/moby/client v0.5.0/go.mod h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=
github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
@@ -786,8 +786,8 @@ github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrp
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
-github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY=
+github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/piprate/json-gold v0.7.0 h1:bEMirgA5y8Z2loTQfxyIFfY+EflxH1CTP6r/KIlcJNw=
From 22ffd5209dbc5a4a2f0f4d732997f12c0830aac0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 20 Jul 2026 15:28:53 +0000
Subject: [PATCH 082/168] chore(deps): bump github.com/gkampitakis/go-snaps
from 0.5.22 to 0.5.23 (#5079)
Bumps [github.com/gkampitakis/go-snaps](https://github.com/gkampitakis/go-snaps) from 0.5.22 to 0.5.23.
- [Release notes](https://github.com/gkampitakis/go-snaps/releases)
- [Commits](https://github.com/gkampitakis/go-snaps/compare/v0.5.22...v0.5.23)
---
updated-dependencies:
- dependency-name: github.com/gkampitakis/go-snaps
dependency-version: 0.5.23
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>
From 289137ff3c101470597e1f53e566ed08b75ee651 Mon Sep 17 00:00:00 2001
From: Yashas Gunderia <68075205+ychampion@users.noreply.github.com>
Date: Mon, 20 Jul 2026 22:00:56 +0530
Subject: [PATCH 083/168] Avoid duplicate packages for replaced Go modules
(#5069)
* Prevent duplicate Go packages after source replacement resolution
Source analysis already applies module replacements through go/packages. Avoid synthesizing the same replacement again from go.mod while retaining fallback synthesis for modules that source analysis did not resolve.
Constraint: Preserve unimported and local-path replacement cataloging.
Rejected: Deduplicate only during final assembly | That retains redundant license lookup and ambiguous metadata ownership.
Confidence: high
Scope-risk: narrow
Directive: Keep go.mod fallback packages limited to modules absent from source analysis.
Tested: Focused replacement regression, related Go module parser tests, go vet, gofmt, and diff checks.
Not-tested: Docker-backed full cataloger fixtures; local root storage was exhausted by image generation.
Signed-off-by: ychampion
* Keep replacement fixtures with the Go module test data
Constraint: The maintainer reserves internal/gotestdata for fixtures that need special Go tooling discovery.
Rejected: Leave this fixture in gotestdata | The regression opens its module explicitly and does not need the special location.
Confidence: high
Scope-risk: narrow
Directive: Use internal/gotestdata only when a fixture must avoid Go testdata discovery rules.
Tested: replacement regression repeated 10 times; Go module parser table; go vet for the Go cataloger; gofmt; diff checks.
Not-tested: Full cataloger package; three unrelated parser fixtures fail identically on exact prior head in this environment.
Signed-off-by: ychampion
---------
Signed-off-by: ychampion
Co-authored-by: ychampion
---
syft/pkg/cataloger/golang/parse_go_mod.go | 7 ++-
.../golang/parse_go_mod_replace_test.go | 44 +++++++++++++++++++
.../testdata/go-source-replacements/go.mod | 15 +++++++
.../testdata/go-source-replacements/go.sum | 6 +++
.../testdata/go-source-replacements/main.go | 11 +++++
5 files changed, 81 insertions(+), 2 deletions(-)
create mode 100644 syft/pkg/cataloger/golang/parse_go_mod_replace_test.go
create mode 100644 syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod
create mode 100644 syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum
create mode 100644 syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go
diff --git a/syft/pkg/cataloger/golang/parse_go_mod.go b/syft/pkg/cataloger/golang/parse_go_mod.go
index d593ed299..4a40403b1 100644
--- a/syft/pkg/cataloger/golang/parse_go_mod.go
+++ b/syft/pkg/cataloger/golang/parse_go_mod.go
@@ -72,7 +72,7 @@ func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resol
// only use go.mod packages NOT found in source analysis
goModPackages := c.createGoModPackages(ctx, resolver, modFile, sourceModules, reader, digests)
- c.applyReplaceDirectives(ctx, resolver, modFile, goModPackages, reader, digests)
+ c.applyReplaceDirectives(ctx, resolver, modFile, sourceModules, goModPackages, reader, digests)
c.applyExcludeDirectives(modFile, goModPackages)
pkgs = c.assembleResults(catalogedModules, goModPackages)
@@ -365,8 +365,11 @@ func (c *goModCataloger) createGoModPackages(ctx context.Context, resolver file.
}
// applyReplaceDirectives processes replace directives from go.mod
-func (c *goModCataloger) applyReplaceDirectives(ctx context.Context, resolver file.Resolver, modFile *modfile.File, goModPackages map[string]pkg.Package, reader file.LocationReadCloser, digests map[string]string) {
+func (c *goModCataloger) applyReplaceDirectives(ctx context.Context, resolver file.Resolver, modFile *modfile.File, sourceModules map[string]*packages.Module, goModPackages map[string]pkg.Package, reader file.LocationReadCloser, digests map[string]string) {
for _, m := range modFile.Replace {
+ if sourceModules != nil && sourceModules[m.Old.Path] != nil {
+ continue
+ }
lics := c.licenseResolver.getLicenses(ctx, resolver, m.New.Path, m.New.Version)
var finalPath string
if !strings.HasPrefix(m.New.Path, ".") && !strings.HasPrefix(m.New.Path, "/") {
diff --git a/syft/pkg/cataloger/golang/parse_go_mod_replace_test.go b/syft/pkg/cataloger/golang/parse_go_mod_replace_test.go
new file mode 100644
index 000000000..8e51fd2a8
--- /dev/null
+++ b/syft/pkg/cataloger/golang/parse_go_mod_replace_test.go
@@ -0,0 +1,44 @@
+package golang
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ stereofile "github.com/anchore/stereoscope/pkg/file"
+ "github.com/anchore/syft/syft/file"
+ "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
+ "github.com/anchore/syft/syft/source"
+ "github.com/anchore/syft/syft/source/directorysource"
+)
+
+func Test_parseGoSource_replacedModulesAreNotDuplicated(t *testing.T) {
+ fixture := filepath.Join("testdata", "go-source-replacements")
+ s, err := directorysource.NewFromPath(fixture)
+ require.NoError(t, err)
+ resolver, err := s.FileResolver(source.AllLayersScope)
+ require.NoError(t, err)
+
+ modPath, err := filepath.Abs(filepath.Join(fixture, "go.mod"))
+ require.NoError(t, err)
+ contents, err := os.Open(modPath)
+ require.NoError(t, err)
+ t.Cleanup(func() { require.NoError(t, contents.Close()) })
+
+ reader := file.LocationReadCloser{
+ Location: file.NewVirtualLocationFromDirectory("go.mod", "go.mod", *stereofile.NewFileReference(stereofile.Path(modPath))),
+ ReadCloser: contents,
+ }
+ config := DefaultCatalogerConfig().WithUsePackagesLib(true).WithSearchRemoteLicenses(false)
+ pkgs, _, err := newGoModCataloger(config).parseGoModFile(pkgtest.Context(t), resolver, nil, reader)
+ require.NoError(t, err)
+
+ versions := make(map[string][]string)
+ for _, p := range pkgs {
+ versions[p.Name] = append(versions[p.Name], p.Version)
+ }
+ require.Equal(t, []string{"v1.5.2"}, versions["rsc.io/quote"])
+ require.Equal(t, []string{"v1.3.1"}, versions["rsc.io/sampler"])
+}
diff --git a/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod
new file mode 100644
index 000000000..b9066bb7c
--- /dev/null
+++ b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.mod
@@ -0,0 +1,15 @@
+module example.com/repro
+
+go 1.23
+
+require rsc.io/quote v1.5.1
+
+require (
+ golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
+ rsc.io/sampler v1.3.0 // indirect
+)
+
+replace (
+ rsc.io/quote => rsc.io/quote v1.5.2
+ rsc.io/sampler => rsc.io/sampler v1.3.1
+)
diff --git a/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum
new file mode 100644
index 000000000..715747812
--- /dev/null
+++ b/syft/pkg/cataloger/golang/testdata/go-source-replacements/go.sum
@@ -0,0 +1,6 @@
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
+rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
+rsc.io/sampler v1.3.1 h1:F0c3J2nQCdk9ODsNhU3sElnvPIxM/xV1c/qZuAeZmac=
+rsc.io/sampler v1.3.1/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go b/syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go
new file mode 100644
index 000000000..0a29866c4
--- /dev/null
+++ b/syft/pkg/cataloger/golang/testdata/go-source-replacements/main.go
@@ -0,0 +1,11 @@
+package main
+
+import (
+ "fmt"
+
+ "rsc.io/quote"
+)
+
+func main() {
+ fmt.Println(quote.Hello())
+}
From 29fd7d0dec81cf03e0a1194a1985c7c893bb2396 Mon Sep 17 00:00:00 2001
From: anchore-oss-update-bot
Date: Mon, 20 Jul 2026 14:04:57 -0400
Subject: [PATCH 084/168] chore(deps): update anchore dependencies (#5022)
Signed-off-by: anchore-oss-update-bot
Co-authored-by: anchore-oss-update-bot
---
go.mod | 4 ++--
go.sum | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/go.mod b/go.mod
index 0a29a383e..63d4e8378 100644
--- a/go.mod
+++ b/go.mod
@@ -18,11 +18,11 @@ require (
github.com/anchore/go-homedir v0.1.1
github.com/anchore/go-logger v0.1.1
github.com/anchore/go-macholibre v0.1.1
- github.com/anchore/go-rpmdb v0.1.0
+ github.com/anchore/go-rpmdb v0.2.0
github.com/anchore/go-sync v0.1.1
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.2.0
- github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567
+ github.com/anchore/stereoscope v0.3.0
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
diff --git a/go.sum b/go.sum
index 1c8d9df09..12fe1ee88 100644
--- a/go.sum
+++ b/go.sum
@@ -136,8 +136,8 @@ github.com/anchore/go-lzo v0.1.1 h1:IwL/fvkdtlIrYIXck6WxZ3nb8WjjHziYYmGxlooyOnM=
github.com/anchore/go-lzo v0.1.1/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=
github.com/anchore/go-macholibre v0.1.1 h1:sTBHi3S1WVIOCamc6NyT8Sn9mBmD30DCTfjfVm4k6/M=
github.com/anchore/go-macholibre v0.1.1/go.mod h1:YNq2610RlvGCK0Za+Klz/OCMtUaEnwwAGA/VfUAQaro=
-github.com/anchore/go-rpmdb v0.1.0 h1:Q8dc208/HYzCqhx0L1zurfm1UPil24hlo9NjkdFmLdE=
-github.com/anchore/go-rpmdb v0.1.0/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=
+github.com/anchore/go-rpmdb v0.2.0 h1:ixcyMg/guhBL9XjBVwhTv9CG48xO07Qkg2uDX2+zx/c=
+github.com/anchore/go-rpmdb v0.2.0/go.mod h1:ATsRlpCXstnoYfzqBfhwGw0U2dolx1BBQ9rAU8jWBAw=
github.com/anchore/go-struct-converter v0.2.0-rc2 h1:q+859fW2/jbHJHB2etbNfRlFwYpknyvbqqk1hUdamQ4=
github.com/anchore/go-struct-converter v0.2.0-rc2/go.mod h1:cDBA5vhcR62nXWo8QH9/Kk2807o65ISaHPNPX66L+Uw=
github.com/anchore/go-sync v0.1.1 h1:91SZ+YqUIIHmf2jPAYZPuHqM/ZqnK1pVJDWtET6+AJE=
@@ -146,8 +146,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.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T/EAErk=
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
-github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567 h1:ZklP3dHyXFUKPrZaQcqP6M86KzlbpayiEvUUeQRT/BI=
-github.com/anchore/stereoscope v0.2.3-0.20260720144826-e46db957a567/go.mod h1:QIBWxa5WCrjtBqXH0+RIuECATrffcbaNdMWquZ83+OI=
+github.com/anchore/stereoscope v0.3.0 h1:6mGGRxm4zRKj/jNiSM7g6XGwWihrnkQRJj11hcULDqY=
+github.com/anchore/stereoscope v0.3.0/go.mod h1:QIBWxa5WCrjtBqXH0+RIuECATrffcbaNdMWquZ83+OI=
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=
From c890e7f17f8db638d3f1558bf087b4cb2f3c2aad Mon Sep 17 00:00:00 2001
From: Alex Goodman
Date: Thu, 23 Jul 2026 15:03:36 -0400
Subject: [PATCH 085/168] decode golang symbols (#5089)
Signed-off-by: Alex Goodman
---
syft/pkg/cataloger/golang/symbols.go | 56 +++++++++++++++++++++--
syft/pkg/cataloger/golang/symbols_test.go | 52 +++++++++++++++++++++
2 files changed, 103 insertions(+), 5 deletions(-)
diff --git a/syft/pkg/cataloger/golang/symbols.go b/syft/pkg/cataloger/golang/symbols.go
index 1d8067dff..3b0987f2e 100644
--- a/syft/pkg/cataloger/golang/symbols.go
+++ b/syft/pkg/cataloger/golang/symbols.go
@@ -52,10 +52,7 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
continue
}
seen[fn.Name] = struct{}{}
- syms = append(syms, binarySymbol{
- packagePath: fn.PackageName(),
- name: fn.Name,
- })
+ syms = append(syms, makeBinarySymbol(fn.Name, fn.PackageName()))
}
// debug/gosym only exposes top-level functions; functions that the compiler inlined into their
@@ -71,7 +68,7 @@ func getSymbols(r io.ReaderAt) (syms []binarySymbol, err error) {
continue
}
seen[name] = struct{}{}
- syms = append(syms, binarySymbol{packagePath: pkgPath, name: name})
+ syms = append(syms, makeBinarySymbol(name, pkgPath))
}
return syms, nil
@@ -94,6 +91,55 @@ func packagePathFromSymbolName(name string) string {
return name[:slash+1+dot]
}
+// makeBinarySymbol builds a binarySymbol, unescaping the %xx sequences the go linker introduces in the
+// import-path portion of a symbol name (see unescapePackagePath). The local-symbol suffix (method and
+// function names) is left untouched, so only the path prefix shared by packagePath and name is rewritten.
+func makeBinarySymbol(name, pkgPath string) binarySymbol {
+ unescaped := unescapePackagePath(pkgPath)
+ if unescaped != pkgPath && strings.HasPrefix(name, pkgPath) {
+ name = unescaped + name[len(pkgPath):]
+ }
+ return binarySymbol{packagePath: unescaped, name: name}
+}
+
+// unescapePackagePath reverses the escaping cmd/internal/objabi.PathToPrefix applies to import paths in
+// symbol names: bytes like '.' (at or after the final '/'), '%', '"', control bytes, and high bytes are
+// written as lowercase "%xx". For example "gopkg.in/yaml.v2" is stored as "gopkg.in/yaml%2ev2", so this
+// restores it before matching against the (unescaped) module paths from build info. A lone or malformed
+// '%' sequence is left as-is.
+func unescapePackagePath(path string) string {
+ if !strings.Contains(path, "%") {
+ return path
+ }
+ var b strings.Builder
+ b.Grow(len(path))
+ for i := 0; i < len(path); i++ {
+ if path[i] == '%' && i+2 < len(path) {
+ if hi, ok1 := unhex(path[i+1]); ok1 {
+ if lo, ok2 := unhex(path[i+2]); ok2 {
+ b.WriteByte(hi<<4 | lo)
+ i += 2
+ continue
+ }
+ }
+ }
+ b.WriteByte(path[i])
+ }
+ return b.String()
+}
+
+func unhex(c byte) (byte, bool) {
+ switch {
+ case c >= '0' && c <= '9':
+ return c - '0', true
+ case c >= 'a' && c <= 'f':
+ return c - 'a' + 10, true
+ case c >= 'A' && c <= 'F':
+ return c - 'A' + 10, true
+ }
+ return 0, false
+}
+
// nameWithoutTypeArgs strips the type-argument portion from an instantiated generic symbol name, e.g.
// "foo/bar.Do[net/url.Values]" -> "foo/bar.Do". The slashes and dots inside the brackets would otherwise
// corrupt package-path derivation (yielding "foo/bar.Do[net" for the example above). Mirrors
diff --git a/syft/pkg/cataloger/golang/symbols_test.go b/syft/pkg/cataloger/golang/symbols_test.go
index 72ca613da..5d140a431 100644
--- a/syft/pkg/cataloger/golang/symbols_test.go
+++ b/syft/pkg/cataloger/golang/symbols_test.go
@@ -97,6 +97,58 @@ func Test_moduleSymbols(t *testing.T) {
}
}
+func Test_escapedImportPathAttribution(t *testing.T) {
+ // the go linker escapes the '.' in the last path element, so "gopkg.in/yaml.v2" appears in the symbol
+ // table as "gopkg.in/yaml%2ev2". attribution must still land these under the unescaped module path.
+ mainModule := &debug.Module{Path: "github.com/someorg/somecli"}
+ deps := []*debug.Module{{Path: "gopkg.in/yaml.v2"}}
+
+ symbols := []binarySymbol{
+ makeBinarySymbol("gopkg.in/yaml%2ev2.(*decoder).alias", "gopkg.in/yaml%2ev2"),
+ makeBinarySymbol("gopkg.in/yaml%2ev2.(*TypeError).Error", "gopkg.in/yaml%2ev2"),
+ }
+
+ gotByModule, gotStdlib := moduleSymbols(symbols, mainModule, deps)
+ assert.Nil(t, gotStdlib)
+ assert.Equal(t, map[string]map[string][]string{
+ "gopkg.in/yaml.v2": {
+ "gopkg.in/yaml.v2": {"(*TypeError).Error", "(*decoder).alias"},
+ },
+ }, gotByModule)
+}
+
+func Test_unescapePackagePath(t *testing.T) {
+ tests := []struct {
+ input string
+ expected string
+ }{
+ {"gopkg.in/yaml%2ev2", "gopkg.in/yaml.v2"},
+ {"foo%25bar", "foo%bar"},
+ {"foo%22bar", "foo\"bar"},
+ // no escapes: unchanged
+ {"github.com/foo/bar", "github.com/foo/bar"},
+ {"", ""},
+ // malformed sequences are left as-is
+ {"foo%2", "foo%2"},
+ {"foo%zz", "foo%zz"},
+ {"foo%", "foo%"},
+ // uppercase hex is accepted too
+ {"foo%2Ebar", "foo.bar"},
+ }
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ assert.Equal(t, tt.expected, unescapePackagePath(tt.input))
+ })
+ }
+}
+
+func Test_makeBinarySymbol(t *testing.T) {
+ // only the import-path prefix is unescaped; the local-symbol suffix is preserved verbatim
+ sym := makeBinarySymbol("gopkg.in/yaml%2ev2.(*decoder).alias", "gopkg.in/yaml%2ev2")
+ assert.Equal(t, "gopkg.in/yaml.v2", sym.packagePath)
+ assert.Equal(t, "gopkg.in/yaml.v2.(*decoder).alias", sym.name)
+}
+
func Test_localSymbolName(t *testing.T) {
tests := []struct {
name string
From 691b1357b2d11da21a8bbe26267f79552b969f15 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 13:27:48 +0000
Subject: [PATCH 086/168] chore(deps): bump actions/checkout from 7.0.0 to
7.0.1 (#5095)
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-version: 7.0.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>
---
.github/workflows/release.yaml | 2 +-
.github/workflows/validate-github-actions.yaml | 2 +-
.github/workflows/validations.yaml | 14 +++++++-------
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 35f09eeab..81370b372 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -75,7 +75,7 @@ jobs:
packages: write # required for publishing release artifacts to GitHub packages
id-token: write # required for keyless signing (cosign/sigstore OIDC)
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
fetch-depth: 0
persist-credentials: true
diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml
index 6aaa5b82c..89788772d 100644
--- a/.github/workflows/validate-github-actions.yaml
+++ b/.github/workflows/validate-github-actions.yaml
@@ -20,7 +20,7 @@ jobs:
contents: read
security-events: write # for uploading SARIF results
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml
index 746319f30..b4bb10ec2 100644
--- a/.github/workflows/validations.yaml
+++ b/.github/workflows/validations.yaml
@@ -28,7 +28,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -50,7 +50,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -72,7 +72,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -105,7 +105,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -135,7 +135,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -179,7 +179,7 @@ jobs:
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
@@ -213,7 +213,7 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7.0.1
with:
persist-credentials: false
From a6eedecd319229aa6fdab2e3ab593d54ddc24ee3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 13:29:04 +0000
Subject: [PATCH 087/168] chore(deps): bump github.com/gpustack/gguf-parser-go
(#5097)
Bumps [github.com/gpustack/gguf-parser-go](https://github.com/gpustack/gguf-parser-go) from 0.24.1 to 0.25.0.
- [Release notes](https://github.com/gpustack/gguf-parser-go/releases)
- [Commits](https://github.com/gpustack/gguf-parser-go/compare/v0.24.1...v0.25.0)
---
updated-dependencies:
- dependency-name: github.com/gpustack/gguf-parser-go
dependency-version: 0.25.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 63d4e8378..e7702dc2d 100644
--- a/go.mod
+++ b/go.mod
@@ -52,7 +52,7 @@ require (
github.com/google/licensecheck v0.3.1
github.com/google/uuid v1.6.0
github.com/gookit/color v1.6.1
- github.com/gpustack/gguf-parser-go v0.24.1
+ github.com/gpustack/gguf-parser-go v0.25.0
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-getter v1.8.6
github.com/hashicorp/go-multierror v1.1.1
diff --git a/go.sum b/go.sum
index 12fe1ee88..0b4e0ca5a 100644
--- a/go.sum
+++ b/go.sum
@@ -558,8 +558,8 @@ github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj
github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
-github.com/gpustack/gguf-parser-go v0.24.1 h1:nTYtL8HFK6ZhB90RKBu4oX2b3ZHpJLrMmKRfL9w9Cyc=
-github.com/gpustack/gguf-parser-go v0.24.1/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
+github.com/gpustack/gguf-parser-go v0.25.0 h1:1AMBhMKtI24nTtn588Bq53FqNiOvEw1x9Nb4HbRrThs=
+github.com/gpustack/gguf-parser-go v0.25.0/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 h1:vTCWu1wbdYo7PEZFem/rlr01+Un+wwVmI7wiegFdRLk=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72/go.mod h1:Vn+BBgKQHVQYdVQ4NZDICE1Brb+JfaONyDHr3q07oQc=
From b38e40e3e675a7e53d59ed65781a6c68d256dfb5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 13:29:21 +0000
Subject: [PATCH 088/168] chore(deps): bump zizmorcore/zizmor-action from 0.5.7
to 0.6.0 (#5096)
Bumps [zizmorcore/zizmor-action](https://github.com/zizmorcore/zizmor-action) from 0.5.7 to 0.6.0.
- [Release notes](https://github.com/zizmorcore/zizmor-action/releases)
- [Commits](https://github.com/zizmorcore/zizmor-action/compare/192e21d79ab29983730a13d1382995c2307fbcaa...6599ee8b7a49aef6a770f63d261d214911a7ce02)
---
updated-dependencies:
- dependency-name: zizmorcore/zizmor-action
dependency-version: 0.6.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/validate-github-actions.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/validate-github-actions.yaml b/.github/workflows/validate-github-actions.yaml
index 89788772d..0ed9051a9 100644
--- a/.github/workflows/validate-github-actions.yaml
+++ b/.github/workflows/validate-github-actions.yaml
@@ -25,7 +25,7 @@ jobs:
persist-credentials: false
- name: "Run zizmor"
- uses: zizmorcore/zizmor-action@192e21d79ab29983730a13d1382995c2307fbcaa # v0.5.7
+ uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
with:
# there is a pass/fail gate as a repo ruleset (if there is no ruleset configured then the action will pass by default)
advanced-security: true
From 2840ea6653ceb55807ff66427bfc8a1360f416a8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 13:29:49 +0000
Subject: [PATCH 089/168] chore(deps): bump modernc.org/sqlite from 1.53.0 to
1.54.0 (#5098)
Bumps [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) from 1.53.0 to 1.54.0.
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.53.0...v1.54.0)
---
updated-dependencies:
- dependency-name: modernc.org/sqlite
dependency-version: 1.54.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 | 4 ++--
go.sum | 20 ++++++++++----------
2 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/go.mod b/go.mod
index e7702dc2d..bd6b3bbad 100644
--- a/go.mod
+++ b/go.mod
@@ -98,7 +98,7 @@ require (
golang.org/x/time v0.15.0
golang.org/x/tools v0.48.0
gopkg.in/yaml.v3 v3.0.1
- modernc.org/sqlite v1.53.0
+ modernc.org/sqlite v1.54.0
)
require (
@@ -324,7 +324,7 @@ require (
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
- modernc.org/libc v1.73.4 // indirect
+ modernc.org/libc v1.74.1 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
diff --git a/go.sum b/go.sum
index 0b4e0ca5a..ac7240e01 100644
--- a/go.sum
+++ b/go.sum
@@ -1513,20 +1513,20 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
-modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c=
-modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
-modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws=
-modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE=
+modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
+modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
+modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
+modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
-modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
-modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
+modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
+modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
-modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA=
-modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8=
+modernc.org/libc v1.74.1 h1:bdR4VTKFMC4966QSNZ05XLGI/VwzVa2kTUX51Dm0riQ=
+modernc.org/libc v1.74.1/go.mod h1:uH4t5bOx3G3g9Xcmj10YKlTcVISlRDwv8VoQJG9n8Os=
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=
@@ -1535,8 +1535,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/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.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
-modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
+modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
+modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
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 9af0098a6847b42eb7b48222935cb501ae61ea09 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Jul 2026 13:48:24 +0000
Subject: [PATCH 090/168] chore(deps): bump google.golang.org/grpc from 1.80.0
to 1.82.1 (#5099)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.80.0 to 1.82.1.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.80.0...v1.82.1)
---
updated-dependencies:
- dependency-name: google.golang.org/grpc
dependency-version: 1.82.1
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
go.mod | 16 ++++++++--------
go.sum | 32 ++++++++++++++++----------------
2 files changed, 24 insertions(+), 24 deletions(-)
diff --git a/go.mod b/go.mod
index bd6b3bbad..aae9d3b51 100644
--- a/go.mod
+++ b/go.mod
@@ -118,7 +118,7 @@ require (
cloud.google.com/go/storage v1.61.3 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/DataDog/zstd v1.5.5 // indirect
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
@@ -168,7 +168,7 @@ require (
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
- github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
+ github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/cgroups/v3 v3.1.3 // indirect
github.com/containerd/containerd/api v1.11.1 // indirect
github.com/containerd/containerd/v2 v2.3.3 // indirect
@@ -189,8 +189,8 @@ require (
github.com/docker/go-units v0.5.0 // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
- github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
- github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
+ github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
@@ -299,7 +299,7 @@ require (
github.com/zclconf/go-cty v1.16.3 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
- go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
+ go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
@@ -319,9 +319,9 @@ require (
gonum.org/v1/gonum v0.17.0 // indirect
google.golang.org/api v0.271.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d // indirect
- google.golang.org/grpc v1.80.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
+ google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
modernc.org/libc v1.74.1 // indirect
diff --git a/go.sum b/go.sum
index ac7240e01..55b03b1f5 100644
--- a/go.sum
+++ b/go.sum
@@ -82,8 +82,8 @@ github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByX
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ=
github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
@@ -290,8 +290,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
-github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
+github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
+github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
github.com/containerd/cgroups/v3 v3.1.3 h1:eUNflyMddm18+yrDmZPn3jI7C5hJ9ahABE5q6dyLYXQ=
github.com/containerd/cgroups/v3 v3.1.3/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=
github.com/containerd/containerd/api v1.11.1 h1:h8nfoDW9+fNsC/9TwiAHj8B1GzXKtR4eFtkhi/X5RLU=
@@ -371,14 +371,14 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.
github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
-github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
-github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
+github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
+github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
-github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
-github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
+github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
+github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/erofs/go-erofs v0.3.0 h1:o/W5ABAA3sHYl97WL93dacKEfeDpJhdFf3c2snAti7I=
@@ -998,8 +998,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
-go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
+go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
+go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
@@ -1431,10 +1431,10 @@ google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
-google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
-google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec=
+google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1462,8 +1462,8 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
-google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
-google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
+google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
+google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
From 8a229b16c4ac2a3d3666848886d97bbff7d2a40a Mon Sep 17 00:00:00 2001
From: anchore-oss-update-bot
Date: Mon, 27 Jul 2026 10:04:27 -0400
Subject: [PATCH 091/168] chore(deps): update CPE dictionary index (#5109)
Signed-off-by: anchore-oss-update-bot
Co-authored-by: anchore-oss-update-bot
---
.../dictionary/data/cpe-index.json | 121 +++++++++++++++++-
1 file changed, 119 insertions(+), 2 deletions(-)
diff --git a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
index b9385c447..cc31a1c8c 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
+++ b/syft/pkg/cataloger/internal/cpegenerate/dictionary/data/cpe-index.json
@@ -2412,6 +2412,9 @@
"@better-auth/oauth-provider": [
"cpe:2.3:a:better-auth:better-auth\\/oauth-provider:*:*:*:*:*:node.js:*:*"
],
+ "@better-auth/sso": [
+ "cpe:2.3:a:better-auth:better-auth\\/sso:*:*:*:*:*:node.js:*:*"
+ ],
"@braintree/sanitize-url": [
"cpe:2.3:a:paypal:braintree\\/sanitize-url:*:*:*:*:*:node.js:*:*"
],
@@ -6837,6 +6840,9 @@
"pyanyapi": [
"cpe:2.3:a:pyanyapi_project:pyanyapi:*:*:*:*:*:*:*:*"
],
+ "pyasn1": [
+ "cpe:2.3:a:pyasn1:pyasn1:*:*:*:*:*:python:*:*"
+ ],
"pybluemonday": [
"cpe:2.3:a:python:pybluemonday:*:*:*:*:*:*:*:*"
],
@@ -8557,6 +8563,9 @@
"add-subtitle": [
"cpe:2.3:a:viitorcloud:add_subtitle:*:*:*:*:*:wordpress:*:*"
],
+ "add-svg-support-for-media-uploader-inventivo": [
+ "cpe:2.3:a:inventivo:inventivo:*:*:*:*:*:wordpress:*:*"
+ ],
"add-to-any": [
"cpe:2.3:a:addtoany:addtoany_share_buttons:*:*:*:*:*:wordpress:*:*"
],
@@ -8747,6 +8756,9 @@
"advanced-recent-posts": [
"cpe:2.3:a:advanced_recent_posts_project:advanced_recent_posts:*:*:*:*:*:wordpress:*:*"
],
+ "advanced-schedule-posts": [
+ "cpe:2.3:a:hijiriworld:advanced_schedule_posts:*:*:*:*:*:wordpress:*:*"
+ ],
"advanced-sermons": [
"cpe:2.3:a:wpcodeus:advanced_sermons:*:*:*:*:*:wordpress:*:*"
],
@@ -8811,6 +8823,7 @@
"cpe:2.3:a:ai_contact_us_form_project:ai_contact_us_form:*:*:*:*:*:wordpress:*:*"
],
"ai-engine": [
+ "cpe:2.3:a:jordymeow:ai_engine:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:meowapps:ai_engine:*:*:*:*:*:wordpress:*:*"
],
"ai-image-alt-text-generator-for-wp": [
@@ -11021,6 +11034,9 @@
"cost-calculator-builder": [
"cpe:2.3:a:stylemixthemes:cost_calculator_builder:*:*:*:*:free:wordpress:*:*"
],
+ "coub": [
+ "cpe:2.3:a:chartscss:coub:*:*:*:*:*:wordpress:*:*"
+ ],
"countdown-block": [
"cpe:2.3:a:wpdeveloper:countdown_block:*:*:*:*:*:wordpress:*:*"
],
@@ -12150,9 +12166,12 @@
"cpe:2.3:a:mandsconsulting:email_before_download:*:*:*:*:*:wordpress:*:*"
],
"email-encoder-bundle": [
- "cpe:2.3:a:jannisthuemmig:email_encoder:*:*:*:*:*:wordpress:*:*",
+ "cpe:2.3:a:onlineoptimisation:email_encoder:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:wp-webhooks:email_encoder:*:*:*:*:*:wordpress:*:*"
],
+ "email-keep": [
+ "cpe:2.3:a:intricateweb:email_keep:*:*:*:*:*:wordpress:*:*"
+ ],
"email-log": [
"cpe:2.3:a:email_log_project:email_log:*:*:*:*:*:wordpress:*:*"
],
@@ -13132,6 +13151,9 @@
"game-server-status": [
"cpe:2.3:a:game-server-status_project:game-server-status:*:*:*:*:*:wordpress:*:*"
],
+ "game-users-share-buttons": [
+ "cpe:2.3:a:gameusers:game_users_share_button:*:*:*:*:*:wordpress:*:*"
+ ],
"gamepress": [
"cpe:2.3:a:gamepress_project:gamepress:*:*:*:*:*:wordpress:*:*"
],
@@ -13141,6 +13163,9 @@
"gamipress-link": [
"cpe:2.3:a:gamipress:gamipress_-_link:*:*:*:*:*:wordpress:*:*"
],
+ "gamipress-reset-user": [
+ "cpe:2.3:a:gamipress:gamipress_-_reset_user:*:*:*:*:*:wordpress:*:*"
+ ],
"garden-gnome-package": [
"cpe:2.3:a:ggnome:garden_gnome_package:*:*:*:*:*:wordpress:*:*"
],
@@ -13225,6 +13250,9 @@
"gestpay-for-woocommerce": [
"cpe:2.3:a:fabrick:gestpay_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
+ "get-bookings-wp": [
+ "cpe:2.3:a:istmoplugins:get_bookings_wp:*:*:*:*:*:wordpress:*:*"
+ ],
"get-custom-field-values": [
"cpe:2.3:a:get_custom_field_values_project:get_custom_field_values:*:*:*:*:*:wordpress:*:*"
],
@@ -13255,6 +13283,15 @@
"gf-custom-style": [
"cpe:2.3:a:alefypimentel:gf_custom_style:*:*:*:*:*:wordpress:*:*"
],
+ "gf-hubspot": [
+ "cpe:2.3:a:crmperks:wp_gravity_forms_hubspot:*:*:*:*:*:wordpress:*:*"
+ ],
+ "gf-insightly": [
+ "cpe:2.3:a:crmperks:wp_gravity_forms_insightly:*:*:*:*:*:wordpress:*:*"
+ ],
+ "gf-salesforce-crmperks": [
+ "cpe:2.3:a:crmperks:wp_gravity_forms_salesforce:*:*:*:*:*:wordpress:*:*"
+ ],
"gf-zoho": [
"cpe:2.3:a:crmperks:wp_gravity_forms_zoho_crm_and_bigin:*:*:*:*:*:wordpress:*:*"
],
@@ -13687,6 +13724,9 @@
"heureka": [
"cpe:2.3:a:heureka:heureka:*:*:*:*:*:wordpress:*:*"
],
+ "hide-links": [
+ "cpe:2.3:a:avovkdesign:hide_links:*:*:*:*:*:wordpress:*:*"
+ ],
"hide-my-wp": [
"cpe:2.3:a:wpplugins:hide_my_wp_ghost:*:*:*:*:*:wordpress:*:*"
],
@@ -13711,6 +13751,9 @@
"hkdev-maintenance-mode": [
"cpe:2.3:a:helderk:maintenance_mode:*:*:*:*:*:wordpress:*:*"
],
+ "hmapsprem": [
+ "cpe:2.3:a:heroplugins:hero_maps_premium:*:*:*:*:*:wordpress:*:*"
+ ],
"hms-testimonials": [
"cpe:2.3:a:hitmyserver:hms_testimonials:*:*:*:*:*:wordpress:*:*"
],
@@ -14085,6 +14128,9 @@
"cpe:2.3:a:infusionsoft_project:infusionsoft:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:katz:infusionsoft_gravity_forms:*:*:*:*:*:wordpress:*:*"
],
+ "infusionsoft-official-opt-in-forms": [
+ "cpe:2.3:a:keap:keap_official_opt_in_forms:*:*:*:*:*:wordpress:*:*"
+ ],
"inline-google-spreadsheet-viewer": [
"cpe:2.3:a:inline_google_spreadsheet_viewer_project:inline_google_spreadsheet_viewer:*:*:*:*:*:wordpress:*:*"
],
@@ -14185,6 +14231,9 @@
"intelly-related-posts": [
"cpe:2.3:a:data443:inline_related_posts:*:*:*:*:*:wordpress:*:*"
],
+ "interact-quiz-embed": [
+ "cpe:2.3:a:tryinteract:embed_a_quiz_on_your_site:*:*:*:*:*:wordpress:*:*"
+ ],
"interactive-3d-flipbook-powered-physics-engine": [
"cpe:2.3:a:3dflipbook:3d_flipbook:*:*:*:*:*:wordpress:*:*"
],
@@ -14421,6 +14470,9 @@
"jvm-rich-text-icons": [
"cpe:2.3:a:jorisvm:jvm_gutenberg_rich_text_icons:*:*:*:*:*:wordpress:*:*"
],
+ "jwp-a11y": [
+ "cpe:2.3:a:jidaikobo:jwp-a11y:*:*:*:*:*:wordpress:*:*"
+ ],
"kadence-blocks": [
"cpe:2.3:a:kadencewp:gutenberg_blocks_with_ai:*:*:*:*:*:wordpress:*:*"
],
@@ -14910,6 +14962,9 @@
"logo-slider-wp": [
"cpe:2.3:a:logichunt:logo_slider:*:*:*:*:*:wordpress:*:*"
],
+ "logs-de-connexion": [
+ "cpe:2.3:a:floriansimunek:connexion_logs:*:*:*:*:*:wordpress:*:*"
+ ],
"logwpmail": [
"cpe:2.3:a:premierethemes:log_wp_mail:*:*:*:*:*:wordpress:*:*"
],
@@ -14982,6 +15037,9 @@
"cpe:2.3:a:custom_popup_builder_project:custom_popup_builder:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:popup_\\|_custom_popup_builder_project:popup_\\|_custom_popup_builder:*:*:*:*:*:wordpress:*:*"
],
+ "magayo-lottery-results": [
+ "cpe:2.3:a:magayo:magayo_lottery_results:*:*:*:*:*:wordpress:*:*"
+ ],
"magazine-blocks": [
"cpe:2.3:a:themegrill:magazine_blocks:*:*:*:*:*:wordpress:*:*"
],
@@ -15178,7 +15236,8 @@
"cpe:2.3:a:master-addons:master_addons:*:*:*:*:*:wordpress:*:*"
],
"master-slider": [
- "cpe:2.3:a:averta:master_slider:*:*:*:*:*:wordpress:*:*"
+ "cpe:2.3:a:averta:master_slider:*:*:*:*:*:wordpress:*:*",
+ "cpe:2.3:a:averta:master_slider:*:*:*:*:free:wordpress:*:*"
],
"masterstudy-lms-learning-management-system": [
"cpe:2.3:a:stylemixthemes:masterstudy_lms:*:*:*:*:*:wordpress:*:*"
@@ -15198,6 +15257,9 @@
"materialis-companion": [
"cpe:2.3:a:extendthemes:materialis_companion:*:*:*:*:*:wordpress:*:*"
],
+ "matomo": [
+ "cpe:2.3:a:matomo:matomo:*:*:*:*:*:wordpress:*:*"
+ ],
"matrix-pre-loader": [
"cpe:2.3:a:nkb-bd:preloader_matrix:*:*:*:*:*:wordpress:*:*"
],
@@ -15216,6 +15278,9 @@
"mdc-youtube-downloader": [
"cpe:2.3:a:mdc_youtube_downloader_project:mdc_youtube_downloader:*:*:*:*:*:wordpress:*:*"
],
+ "media-alt-renamer": [
+ "cpe:2.3:a:jordymeow:media_alt_renamer:*:*:*:*:*:wordpress:*:*"
+ ],
"media-downloader": [
"cpe:2.3:a:media_downloader_project:media_downloader:*:*:*:*:*:wordpress:*:*"
],
@@ -15547,6 +15612,9 @@
"move-addons": [
"cpe:2.3:a:moveaddons:move_addons_for_elementor:*:*:*:*:*:wordpress:*:*"
],
+ "movie-database": [
+ "cpe:2.3:a:heimkino-praxis:movie_database:*:*:*:*:*:wordpress:*:*"
+ ],
"movies": [
"cpe:2.3:a:movies_project:movies:*:*:*:*:*:wordpress:*:*"
],
@@ -16239,6 +16307,7 @@
"cpe:2.3:a:benhuson:page_parts:*:*:*:*:*:wordpress:*:*"
],
"page-scroll-to-id": [
+ "cpe:2.3:a:malihu:page_scroll_to_id:*:*:*:*:*:wordpress:*:*",
"cpe:2.3:a:page_scroll_to_id_project:page_scroll_to_id:*:*:*:*:*:wordpress:*:*"
],
"page-views-count": [
@@ -16672,6 +16741,9 @@
"popup-zyrex": [
"cpe:2.3:a:zyrex:popup:*:*:*:*:*:wordpress:*:*"
],
+ "popup4phone": [
+ "cpe:2.3:a:ivanweb:popup4phone:*:*:*:*:*:wordpress:*:*"
+ ],
"popupally": [
"cpe:2.3:a:accessally:popupally:*:*:*:*:*:wordpress:*:*"
],
@@ -16685,6 +16757,9 @@
"portfolio": [
"cpe:2.3:a:bestwebsoft:portfolio:*:*:*:*:*:wordpress:*:*"
],
+ "portfolio-and-projects": [
+ "cpe:2.3:a:essentialplugin:portfolio_and_projects:*:*:*:*:*:wordpress:*:*"
+ ],
"portfolio-by-lisa-westlund": [
"cpe:2.3:a:portfolio_project:portfolio:*:*:*:*:*:wordpress:*:*"
],
@@ -17472,6 +17547,9 @@
"responsive-css-editor": [
"cpe:2.3:a:wpwox:responsive_css_editor:*:*:*:*:*:wordpress:*:*"
],
+ "responsive-filterable-portfolio": [
+ "cpe:2.3:a:i13websolution:responsive_filterable_portfolio:*:*:*:*:*:wordpress:*:*"
+ ],
"responsive-food-and-drink-menu": [
"cpe:2.3:a:corporatezen:responsive_food_and_drink_menu:*:*:*:*:*:wordpress:*:*"
],
@@ -17501,6 +17579,12 @@
"responsive-vector-maps": [
"cpe:2.3:a:thinkupthemes:responsive_vector_maps:*:*:*:*:*:wordpress:*:*"
],
+ "responsive-video-embed": [
+ "cpe:2.3:a:lepileppanen:responsive_video_embed:*:*:*:*:*:wordpress:*:*"
+ ],
+ "rest-api-to-miniprogram": [
+ "cpe:2.3:a:jianbo:rest_api_to_miniprogram:*:*:*:*:*:wordpress:*:*"
+ ],
"restaurant-cafe-addon-for-elementor": [
"cpe:2.3:a:nicheaddons:restaurant_\\\u0026_cafe_addon_for_elementor:*:*:*:*:*:wordpress:*:*"
],
@@ -17686,6 +17770,9 @@
"safe-svg": [
"cpe:2.3:a:10up:safe_svg:*:*:*:*:*:wordpress:*:*"
],
+ "safetymails-forms": [
+ "cpe:2.3:a:henriquerodrigues:safetyforms:*:*:*:*:*:wordpress:*:*"
+ ],
"sagepay-server-gateway-for-woocommerce": [
"cpe:2.3:a:patsatech:sagepay_server_gateway_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
@@ -18576,6 +18663,9 @@
"slideshow-se": [
"cpe:2.3:a:slideshow_se_project:slideshow_se:*:*:*:*:*:wordpress:*:*"
],
+ "slingblocks": [
+ "cpe:2.3:a:funnelkit:slingblocks:*:*:*:*:*:wordpress:*:*"
+ ],
"small-package-quotes-fedex-edition": [
"cpe:2.3:a:eniture:small_package_quotes:*:*:*:*:fedex:wordpress:*:*"
],
@@ -18868,6 +18958,9 @@
"sportspress": [
"cpe:2.3:a:themeboy:sportspress:*:*:*:*:*:wordpress:*:*"
],
+ "spotbot": [
+ "cpe:2.3:a:jakehelbig:spotbot:*:*:*:*:*:wordpress:*:*"
+ ],
"spotify-play-button-for-wordpress": [
"cpe:2.3:a:followmedarling:spotify-play-button-for-wordpress:*:*:*:*:*:wordpress:*:*"
],
@@ -19970,6 +20063,9 @@
"user-login-log": [
"cpe:2.3:a:intechnosoftware:user_login_log:*:*:*:*:*:wordpress:*:*"
],
+ "user-messages": [
+ "cpe:2.3:a:marvinlabs:user_messages:*:*:*:*:*:wordpress:*:*"
+ ],
"user-meta": [
"cpe:2.3:a:user-meta:user_meta_user_profile_builder_and_user_management:*:*:*:*:*:wordpress:*:*"
],
@@ -19993,6 +20089,9 @@
"user-role": [
"cpe:2.3:a:bestwebsoft:user_role:*:*:*:*:*:wordpress:*:*"
],
+ "user-shortcodes-plus": [
+ "cpe:2.3:a:kylebjohnson:user_shortcodes_plus:*:*:*:*:*:wordpress:*:*"
+ ],
"user-spam-remover": [
"cpe:2.3:a:joelhardi:user_spam_remover:*:*:*:*:*:wordpress:*:*"
],
@@ -20605,6 +20704,9 @@
"woo-gutenberg-products-block": [
"cpe:2.3:a:automattic:woocommerce_blocks:*:*:*:*:*:wordpress:*:*"
],
+ "woo-mailerlite": [
+ "cpe:2.3:a:mailerlite:mailerlite:*:*:*:*:*:wordpress:*:*"
+ ],
"woo-moneybird": [
"cpe:2.3:a:techastha:integration_of_moneybird_for_woocommerce:*:*:*:*:*:wordpress:*:*"
],
@@ -20954,6 +21056,9 @@
"wordpress-users": [
"cpe:2.3:a:jonathonkemp:wordpress_users:*:*:*:*:*:wordpress:*:*"
],
+ "work-the-flow-file-upload": [
+ "cpe:2.3:a:lyntonreed:work_the_flow_file_upload:*:*:*:*:*:wordpress:*:*"
+ ],
"workreap": [
"cpe:2.3:a:amentotech:workreap:*:*:*:*:*:wordpress:*:*"
],
@@ -21185,6 +21290,9 @@
"wp-consultant": [
"cpe:2.3:a:wp_consultant_project:wp_consultant:*:*:-:*:-:wordpress:*:*"
],
+ "wp-contact-form": [
+ "cpe:2.3:a:ftwr:wp_contact_form:*:*:*:*:*:wordpress:*:*"
+ ],
"wp-contact-slider": [
"cpe:2.3:a:wpexperts:wp_contact_slider:*:*:*:*:*:wordpress:*:*"
],
@@ -21483,6 +21591,9 @@
"wp-fountain": [
"cpe:2.3:a:wp_fountain_project:wp_fountain:*:*:*:*:*:wordpress:*:*"
],
+ "wp-front-end-login-and-register": [
+ "cpe:2.3:a:hellomohsinkhan:wp_front-end_login_and_register:*:*:*:*:*:wordpress:*:*"
+ ],
"wp-front-end-profile": [
"cpe:2.3:a:wp_front_end_profile_project:wp_front_end_profile:*:*:*:*:*:wordpress:*:*"
],
@@ -21935,6 +22046,9 @@
"wp-product-gallery-lite": [
"cpe:2.3:a:accesspressthemes:wp_product_gallery_lite:*:*:*:*:*:wordpress:*:*"
],
+ "wp-programmmanager": [
+ "cpe:2.3:a:mantus667:wp-pmanager:*:*:*:*:*:wordpress:*:*"
+ ],
"wp-propagator": [
"cpe:2.3:a:widgilabs:plugin_propagator:*:*:*:*:*:wordpress:*:*"
],
@@ -23143,6 +23257,9 @@
"empowerwp": [
"cpe:2.3:a:extendthemes:empowerwp:*:*:*:*:*:wordpress:*:*"
],
+ "enfold": [
+ "cpe:2.3:a:kriesi:enfold:*:*:*:*:*:wordpress:*:*"
+ ],
"enlighten": [
"cpe:2.3:a:accesspressthemes:enlighten:*:*:*:*:*:wordpress:*:*"
],
From 138d9ce2a095755a04b06b7e7d9173c976502d9a Mon Sep 17 00:00:00 2001
From: Rez Moss
Date: Mon, 27 Jul 2026 10:18:50 -0400
Subject: [PATCH 092/168] added bun binary classifier (#5103)
* added bun binary classifier
Signed-off-by: Rez Moss
* added bun binary classifier
Signed-off-by: Rez Moss
---------
Signed-off-by: Rez Moss
---
syft/pkg/cataloger/binary/capabilities.yaml | 10 +++++
.../binary/classifier_cataloger_test.go | 44 +++++++++++++++++++
syft/pkg/cataloger/binary/classifiers.go | 14 ++++++
.../snippets/bun/0.5.9/linux-amd64/bun | 12 +++++
.../snippets/bun/1.0.0/linux-amd64/bun | 12 +++++
.../snippets/bun/1.1.0/linux-amd64/bun | 12 +++++
.../snippets/bun/1.3.5/linux-amd64/bun | 12 +++++
7 files changed, 116 insertions(+)
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun
create mode 100644 syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun
diff --git a/syft/pkg/cataloger/binary/capabilities.yaml b/syft/pkg/cataloger/binary/capabilities.yaml
index 17f36be0b..e8d68b254 100644
--- a/syft/pkg/cataloger/binary/capabilities.yaml
+++ b/syft/pkg/cataloger/binary/capabilities.yaml
@@ -433,6 +433,16 @@ catalogers:
cpes:
- cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*
type: BinaryPkg
+ - method: glob
+ criteria:
+ - '**/bun'
+ packages:
+ - class: bun-binary
+ name: bun
+ purl: pkg:generic/bun
+ cpes:
+ - cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*
+ type: BinaryPkg
- method: glob
criteria:
- '**/ghc*'
diff --git a/syft/pkg/cataloger/binary/classifier_cataloger_test.go b/syft/pkg/cataloger/binary/classifier_cataloger_test.go
index 286699a76..b620a8674 100644
--- a/syft/pkg/cataloger/binary/classifier_cataloger_test.go
+++ b/syft/pkg/cataloger/binary/classifier_cataloger_test.go
@@ -1565,6 +1565,50 @@ func Test_Cataloger_PositiveCases(t *testing.T) {
Metadata: metadata("deno-binary"),
},
},
+ {
+ logicalFixture: "bun/0.5.9/linux-amd64",
+ expected: pkg.Package{
+ Name: "bun",
+ Version: "0.5.9",
+ Type: "binary",
+ PURL: "pkg:generic/bun@0.5.9",
+ Locations: locations("bun"),
+ Metadata: metadata("bun-binary"),
+ },
+ },
+ {
+ logicalFixture: "bun/1.0.0/linux-amd64",
+ expected: pkg.Package{
+ Name: "bun",
+ Version: "1.0.0",
+ Type: "binary",
+ PURL: "pkg:generic/bun@1.0.0",
+ Locations: locations("bun"),
+ Metadata: metadata("bun-binary"),
+ },
+ },
+ {
+ logicalFixture: "bun/1.1.0/linux-amd64",
+ expected: pkg.Package{
+ Name: "bun",
+ Version: "1.1.0",
+ Type: "binary",
+ PURL: "pkg:generic/bun@1.1.0",
+ Locations: locations("bun"),
+ Metadata: metadata("bun-binary"),
+ },
+ },
+ {
+ logicalFixture: "bun/1.3.5/linux-amd64",
+ expected: pkg.Package{
+ Name: "bun",
+ Version: "1.3.5",
+ Type: "binary",
+ PURL: "pkg:generic/bun@1.3.5",
+ Locations: locations("bun"),
+ Metadata: metadata("bun-binary"),
+ },
+ },
{
logicalFixture: "haskell-ghc/7.10.3/linux-amd64",
expected: pkg.Package{
diff --git a/syft/pkg/cataloger/binary/classifiers.go b/syft/pkg/cataloger/binary/classifiers.go
index 9c5adeea4..212f244a2 100644
--- a/syft/pkg/cataloger/binary/classifiers.go
+++ b/syft/pkg/cataloger/binary/classifiers.go
@@ -611,6 +611,20 @@ func DefaultClassifiers() []binutils.Classifier {
PURL: mustPURL("pkg:generic/deno@version"),
CPEs: singleCPE("cpe:2.3:a:deno:deno:*:*:*:*:*:*:*:*", cpe.NVDDictionaryLookupSource),
},
+ {
+ Class: "bun-binary",
+ FileGlob: "**/bun",
+ EvidenceMatcher: binutils.MatchAny(
+ // bun 1.1.0
+ // Bun v1.0.0
+ m.FileContentsVersionMatcher(`(?m)[Bb]un v?(?P[0-9]+\.[0-9]+\.[0-9]+)`),
+ // bun-1.1.0
+ m.FileContentsVersionMatcher(`(?m)bun-(?P[0-9]+\.[0-9]+\.[0-9]+)`),
+ ),
+ Package: "bun",
+ PURL: mustPURL("pkg:generic/bun@version"),
+ CPEs: singleCPE("cpe:2.3:a:oven-sh:bun:*:*:*:*:*:*:*:*"),
+ },
{
Class: "haskell-ghc-binary",
FileGlob: "**/ghc*",
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun
new file mode 100644
index 000000000..a19be8c22
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/0.5.9/linux-amd64/bun
@@ -0,0 +1,12 @@
+name: bun
+offset: 1000000
+length: 100
+snippetSha256: 0000000000000000000000000000000000000000000000000000000000000001
+fileSha256: 0000000000000000000000000000000000000000000000000000000000000001
+
+### byte snippet to follow ###
+]
+
+Bun v0.5.9
+bun-0.5.9
+JavaScript runtime for developing, testing, and bundling
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun
new file mode 100644
index 000000000..163f6bf84
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.0.0/linux-amd64/bun
@@ -0,0 +1,12 @@
+name: bun
+offset: 1000000
+length: 100
+snippetSha256: 0000000000000000000000000000000000000000000000000000000000000002
+fileSha256: 0000000000000000000000000000000000000000000000000000000000000002
+
+### byte snippet to follow ###
+]
+
+Bun v1.0.0
+bun-1.0.0
+JavaScript runtime for developing, testing, and bundling
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun
new file mode 100644
index 000000000..719498ef3
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.1.0/linux-amd64/bun
@@ -0,0 +1,12 @@
+name: bun
+offset: 1000000
+length: 100
+snippetSha256: 0000000000000000000000000000000000000000000000000000000000000003
+fileSha256: 0000000000000000000000000000000000000000000000000000000000000003
+
+### byte snippet to follow ###
+]
+
+Bun v1.1.0
+bun-1.1.0
+JavaScript runtime for developing, testing, and bundling
diff --git a/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun
new file mode 100644
index 000000000..e68db67f7
--- /dev/null
+++ b/syft/pkg/cataloger/binary/testdata/classifiers/snippets/bun/1.3.5/linux-amd64/bun
@@ -0,0 +1,12 @@
+name: bun
+offset: 1000000
+length: 100
+snippetSha256: 0000000000000000000000000000000000000000000000000000000000000004
+fileSha256: 0000000000000000000000000000000000000000000000000000000000000004
+
+### byte snippet to follow ###
+]
+
+Bun v1.3.5
+bun-1.3.5
+JavaScript runtime for developing, testing, and bundling
From 12b8ba47fbdf146db769377559649b002810f37e Mon Sep 17 00:00:00 2001
From: Arpit Jain <3242828+arpitjain099@users.noreply.github.com>
Date: Mon, 27 Jul 2026 23:22:41 +0900
Subject: [PATCH 093/168] Fix inverted bounds check dropping every Erlang
string with a backslash (#5110)
parseErlangString advances past a backslash escape and then checks
len(data) >= *i before reading the escaped byte. That condition is
almost always true (it only turns false once *i runs off the end),
so the intended out-of-range guard fires on the very first escape
character it sees instead of only at EOF. Any rebar.lock or OTP
resource file containing a backslash in a quoted string (a Windows
git path, an escaped quote, anything) fails to parse and the whole
file, and every package in it, gets dropped.
Flip the comparison to *i >= len(data) so the guard only trips when
the escape is genuinely truncated, and add a regression test for a
string with an escaped quote.
Signed-off-by: Arpit Jain
---
syft/pkg/cataloger/erlang/erlang_parser.go | 2 +-
syft/pkg/cataloger/erlang/erlang_parser_test.go | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/syft/pkg/cataloger/erlang/erlang_parser.go b/syft/pkg/cataloger/erlang/erlang_parser.go
index ae22589b4..819c3bb41 100644
--- a/syft/pkg/cataloger/erlang/erlang_parser.go
+++ b/syft/pkg/cataloger/erlang/erlang_parser.go
@@ -156,7 +156,7 @@ func parseErlangString(data []byte, i *int) (erlangNode, error) {
}
if c == '\\' {
*i++
- if len(data) >= *i {
+ if *i >= len(data) {
return node(nil), fmt.Errorf("invalid escape without closed string at %d", *i)
}
c = data[*i]
diff --git a/syft/pkg/cataloger/erlang/erlang_parser_test.go b/syft/pkg/cataloger/erlang/erlang_parser_test.go
index adb031ddb..9b6950793 100644
--- a/syft/pkg/cataloger/erlang/erlang_parser_test.go
+++ b/syft/pkg/cataloger/erlang/erlang_parser_test.go
@@ -96,6 +96,11 @@ func Test_parseErlang(t *testing.T) {
{ foo, bar }
]}`,
},
+ {
+ name: "string with an escaped quote",
+ content: `
+{escaped, ["a\"b"]}`,
+ },
}
for _, test := range tests {
@@ -112,3 +117,14 @@ func Test_parseErlang(t *testing.T) {
})
}
}
+
+func Test_parseErlangString_escapedByte(t *testing.T) {
+ // a backslash escape mid-string used to always error out, since the bounds
+ // check at the escape site was inverted (len(data) >= *i is true for
+ // almost every position, not just an out-of-range one).
+ data := []byte(`"a\"b"`)
+ i := 0
+ got, err := parseErlangString(data, &i)
+ require.NoError(t, err)
+ assert.Equal(t, `a"b`, got.String())
+}
From 2dcf5163b8112c0029b7534df9b747db0e0c7b6a Mon Sep 17 00:00:00 2001
From: Rayan Salhab
Date: Mon, 27 Jul 2026 17:36:56 +0300
Subject: [PATCH 094/168] fix(apk): allow large installed db fields (#5100)
Signed-off-by: cyphercodes
Co-authored-by: cyphercodes
---
syft/pkg/cataloger/alpine/parse_apk_db.go | 3 +++
.../pkg/cataloger/alpine/parse_apk_db_test.go | 19 +++++++++++++++++++
2 files changed, 22 insertions(+)
diff --git a/syft/pkg/cataloger/alpine/parse_apk_db.go b/syft/pkg/cataloger/alpine/parse_apk_db.go
index 094d076f6..e661797a1 100644
--- a/syft/pkg/cataloger/alpine/parse_apk_db.go
+++ b/syft/pkg/cataloger/alpine/parse_apk_db.go
@@ -32,12 +32,15 @@ type parsedData struct {
pkg.ApkDBEntry
}
+const maxApkDBFieldSize = 10 * 1024 * 1024
+
// parseApkDB parses packages from a given APK "installed" flat-file DB. For more
// information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec.
//
//nolint:funlen
func parseApkDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
scanner := bufio.NewScanner(reader)
+ scanner.Buffer(nil, maxApkDBFieldSize)
var errs error
var apks []parsedData
diff --git a/syft/pkg/cataloger/alpine/parse_apk_db_test.go b/syft/pkg/cataloger/alpine/parse_apk_db_test.go
index 0354a5931..ee146d723 100644
--- a/syft/pkg/cataloger/alpine/parse_apk_db_test.go
+++ b/syft/pkg/cataloger/alpine/parse_apk_db_test.go
@@ -719,6 +719,25 @@ func Test_processChecksum(t *testing.T) {
}
}
+func TestParseApkDBAllowsLargeFieldValues(t *testing.T) {
+ contents := strings.Join([]string{
+ "P:large-description",
+ "V:1.0-r0",
+ "A:x86_64",
+ "S:1",
+ "I:1",
+ "T:" + strings.Repeat("a", 70*1024),
+ "",
+ }, "\n")
+ reader := file.NewLocationReadCloser(file.NewLocation("large-installed-db"), io.NopCloser(strings.NewReader(contents)))
+
+ pkgs, _, err := parseApkDB(context.Background(), nil, new(generic.Environment), reader)
+
+ require.NoError(t, err)
+ require.Len(t, pkgs, 1)
+ assert.Equal(t, "large-description", pkgs[0].Name)
+}
+
func Test_parseApkDB_expectedPkgNames(t *testing.T) {
tests := []struct {
fixture string
From 86baeeb481ce7de42841993fb4424a696a89d2ee Mon Sep 17 00:00:00 2001
From: Enes Deniz <142517728+3nesdeniz@users.noreply.github.com>
Date: Mon, 27 Jul 2026 17:50:13 +0300
Subject: [PATCH 095/168] fix(rust): omit Cargo PURLs for local packages
(#5105)
Signed-off-by: Enes Deniz <142517728+3nesdeniz@users.noreply.github.com>
---
syft/pkg/cataloger/rust/package.go | 14 +++-
syft/pkg/cataloger/rust/package_test.go | 33 +++++++++
.../cataloger/rust/parse_cargo_lock_test.go | 73 ++++++++++++++++++-
.../rust/testdata/path-deps/Cargo.lock | 21 ++++++
4 files changed, 139 insertions(+), 2 deletions(-)
create mode 100644 syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock
diff --git a/syft/pkg/cataloger/rust/package.go b/syft/pkg/cataloger/rust/package.go
index 9ed04696d..aebf04540 100644
--- a/syft/pkg/cataloger/rust/package.go
+++ b/syft/pkg/cataloger/rust/package.go
@@ -14,7 +14,7 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc
Name: m.Name,
Version: m.Version,
Locations: file.NewLocationSet(locations...),
- PURL: packageURL(m.Name, m.Version),
+ PURL: cargoLockPackageURL(m),
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: m,
@@ -25,6 +25,18 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc
return p
}
+func cargoLockPackageURL(m pkg.RustCargoLockEntry) string {
+ // Cargo omits source for packages resolved from the local workspace or a
+ // path dependency. A cargo PURL without a repository qualifier identifies
+ // a package on crates.io, so emitting one for a local package can cause
+ // downstream scanners to match unrelated registry vulnerabilities.
+ if m.Source == "" {
+ return ""
+ }
+
+ return packageURL(m.Name, m.Version)
+}
+
func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg.Package {
p := pkg.Package{
Name: dep.Name,
diff --git a/syft/pkg/cataloger/rust/package_test.go b/syft/pkg/cataloger/rust/package_test.go
index 01f1f9a2f..c1cc84125 100644
--- a/syft/pkg/cataloger/rust/package_test.go
+++ b/syft/pkg/cataloger/rust/package_test.go
@@ -4,6 +4,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
+
+ "github.com/anchore/syft/syft/pkg"
)
func Test_packageURL(t *testing.T) {
@@ -31,3 +33,34 @@ func Test_packageURL(t *testing.T) {
})
}
}
+
+func TestNewPackageFromCargoMetadataPURL(t *testing.T) {
+ tests := []struct {
+ name string
+ source string
+ want string
+ }{
+ {
+ name: "crates.io package",
+ source: "registry+https://github.com/rust-lang/crates.io-index",
+ want: "pkg:cargo/telemetry@0.1.0",
+ },
+ {
+ name: "local path package",
+ source: "",
+ want: "",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got := newPackageFromCargoMetadata(pkg.RustCargoLockEntry{
+ Name: "telemetry",
+ Version: "0.1.0",
+ Source: tt.source,
+ })
+
+ assert.Equal(t, tt.want, got.PURL)
+ })
+ }
+}
diff --git a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go
index aa13b3755..3b74e4b67 100644
--- a/syft/pkg/cataloger/rust/parse_cargo_lock_test.go
+++ b/syft/pkg/cataloger/rust/parse_cargo_lock_test.go
@@ -351,7 +351,7 @@ func TestCargoLockWithGitDependencies(t *testing.T) {
helloWorld := pkg.Package{
Name: "hello_world",
Version: "0.1.0",
- PURL: "pkg:cargo/hello_world@0.1.0",
+ PURL: "",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
@@ -673,6 +673,77 @@ func TestCargoLockWithGitDependencies(t *testing.T) {
pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships)
}
+func TestCargoLockWithPathDependencies(t *testing.T) {
+ fixture := "testdata/path-deps/Cargo.lock"
+ locations := file.NewLocationSet(file.NewLocation(fixture))
+
+ app := pkg.Package{
+ Name: "app",
+ Version: "1.0.0",
+ Locations: locations,
+ Language: pkg.Rust,
+ Type: pkg.RustPkg,
+ Licenses: pkg.NewLicenseSet(),
+ Metadata: pkg.RustCargoLockEntry{
+ Name: "app",
+ Version: "1.0.0",
+ Dependencies: []string{
+ "memchr",
+ "telemetry",
+ },
+ },
+ }
+ memchr := pkg.Package{
+ Name: "memchr",
+ Version: "2.7.4",
+ PURL: "pkg:cargo/memchr@2.7.4",
+ Locations: locations,
+ Language: pkg.Rust,
+ Type: pkg.RustPkg,
+ Licenses: pkg.NewLicenseSet(),
+ Metadata: pkg.RustCargoLockEntry{
+ Name: "memchr",
+ Version: "2.7.4",
+ Source: "registry+https://github.com/rust-lang/crates.io-index",
+ Checksum: "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3",
+ Dependencies: []string{},
+ },
+ }
+ telemetry := pkg.Package{
+ Name: "telemetry",
+ Version: "0.1.0",
+ Locations: locations,
+ Language: pkg.Rust,
+ Type: pkg.RustPkg,
+ Licenses: pkg.NewLicenseSet(),
+ Metadata: pkg.RustCargoLockEntry{
+ Name: "telemetry",
+ Version: "0.1.0",
+ Dependencies: []string{},
+ },
+ }
+
+ expectedPkgs := []pkg.Package{
+ app,
+ memchr,
+ telemetry,
+ }
+ expectedRelationships := []artifact.Relationship{
+ {
+ From: memchr,
+ To: app,
+ Type: artifact.DependencyOfRelationship,
+ },
+ {
+ From: telemetry,
+ To: app,
+ Type: artifact.DependencyOfRelationship,
+ },
+ }
+
+ pkgtest.TestFileParser(t, fixture, parseCargoLock, expectedPkgs, expectedRelationships)
+}
+
func TestCargoLockDependencySpecification(t *testing.T) {
tests := []struct {
name string
diff --git a/syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock b/syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock
new file mode 100644
index 000000000..388954608
--- /dev/null
+++ b/syft/pkg/cataloger/rust/testdata/path-deps/Cargo.lock
@@ -0,0 +1,21 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "app"
+version = "1.0.0"
+dependencies = [
+ "memchr",
+ "telemetry",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "telemetry"
+version = "0.1.0"
From 295454945ca6b725d73b44da9b1dc263a91b878b Mon Sep 17 00:00:00 2001
From: Eljees <57435526+Eljees@users.noreply.github.com>
Date: Mon, 27 Jul 2026 22:18:27 +0300
Subject: [PATCH 096/168] fix: strip publisher URL from RPM CPE vendor (#5081)
Signed-off-by: Eljees
---
.../pkg/cataloger/internal/cpegenerate/rpm.go | 49 +++++++++++----
.../internal/cpegenerate/rpm_test.go | 60 +++++++++++++++++++
2 files changed, 96 insertions(+), 13 deletions(-)
create mode 100644 syft/pkg/cataloger/internal/cpegenerate/rpm_test.go
diff --git a/syft/pkg/cataloger/internal/cpegenerate/rpm.go b/syft/pkg/cataloger/internal/cpegenerate/rpm.go
index ecaea15f5..54a1e8c42 100644
--- a/syft/pkg/cataloger/internal/cpegenerate/rpm.go
+++ b/syft/pkg/cataloger/internal/cpegenerate/rpm.go
@@ -1,26 +1,49 @@
package cpegenerate
-import "github.com/anchore/syft/syft/pkg"
+import (
+ "net/url"
+ "strings"
+
+ "github.com/anchore/syft/syft/pkg"
+)
func candidateVendorsForRPM(p pkg.Package) fieldCandidateSet {
vendors := newFieldCandidateSet()
+ var vendor string
switch m := p.Metadata.(type) {
case pkg.RpmDBEntry:
- if m.Vendor != "" {
- vendors.add(fieldCandidate{
- value: normalizeName(m.Vendor),
- disallowSubSelections: true,
- })
- }
+ vendor = m.Vendor
case pkg.RpmArchive:
- if m.Vendor != "" {
- vendors.add(fieldCandidate{
- value: normalizeName(m.Vendor),
- disallowSubSelections: true,
- })
- }
+ vendor = m.Vendor
+ }
+
+ vendor = stripTrailingURL(vendor)
+ if vendor != "" {
+ vendors.add(fieldCandidate{
+ value: normalizeName(vendor),
+ disallowSubSelections: true,
+ })
}
return vendors
}
+
+func stripTrailingURL(value string) string {
+ trimmed := strings.TrimSpace(value)
+ if !strings.HasSuffix(trimmed, ">") {
+ return value
+ }
+
+ open := strings.LastIndex(trimmed, "<")
+ if open == -1 {
+ return value
+ }
+
+ parsed, err := url.Parse(trimmed[open+1 : len(trimmed)-1])
+ if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
+ return value
+ }
+
+ return strings.TrimSpace(trimmed[:open])
+}
diff --git a/syft/pkg/cataloger/internal/cpegenerate/rpm_test.go b/syft/pkg/cataloger/internal/cpegenerate/rpm_test.go
new file mode 100644
index 000000000..e5dc059d3
--- /dev/null
+++ b/syft/pkg/cataloger/internal/cpegenerate/rpm_test.go
@@ -0,0 +1,60 @@
+package cpegenerate
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/anchore/syft/syft/pkg"
+)
+
+func TestCandidateVendorsForRPM(t *testing.T) {
+ tests := []struct {
+ name string
+ metadata any
+ expected []string
+ }{
+ {
+ name: "database vendor with publisher URL",
+ metadata: pkg.RpmDBEntry{
+ Vendor: "SUSE LLC ",
+ },
+ expected: []string{"susellc"},
+ },
+ {
+ name: "archive vendor with publisher URL",
+ metadata: pkg.RpmArchive{
+ Vendor: "SUSE LLC ",
+ },
+ expected: []string{"susellc"},
+ },
+ {
+ name: "plain vendor",
+ metadata: pkg.RpmDBEntry{
+ Vendor: "Red Hat, Inc.",
+ },
+ expected: []string{"redhat"},
+ },
+ {
+ name: "non-URL angle-bracket suffix",
+ metadata: pkg.RpmDBEntry{
+ Vendor: "Example ",
+ },
+ expected: []string{"example