From d12a57ef43f056f84b5e4016334594f15e659abb Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Tue, 17 Dec 2024 00:10:56 -0800 Subject: [PATCH] =?UTF-8?q?=E2=AD=90=EF=B8=8F=20Add=20Purl=20to=20macOS=20?= =?UTF-8?q?and=20Windows=20systems=20(#4996)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/mondoohq/cnquery/issues/4957 🛠️ Refactored the `purl` package. **Basic usage.** No platform information. ```go purl.NewPackageURL(nil, purl.TypeDebian, "curl", "7.50.3-1").String() ``` Will produce the purl: `pkg:deb/curl@7.50.3-1` **Main usage. (with platform info)** We rely on the `inventory.Platform` that already has information like the architecture that can't change. Also we guess the linux distribution from here. ```go platform := &inventory.Platform{ Arch: "x86_64", Version: "22.04", Labels: map[string]string{ "distro-id": "jessie", }, } purl.NewPackageURL(platform, purl.TypeDebian, "curl", "7.50.3-1").String() ``` Will produce the purl: `pkg:deb/debian/curl@7.50.3-1?arch=x86_64&distro=jessie` **Extended usage. (with modifiers)** We can override optional attributes, like the architecture, epoch and namespace. This is useful for non-linux systems. ```go platform := &inventory.Platform{ Name: "windows", Version: "10.0.18363", Family: []string{"windows"}, } purl.NewPackageURL(platform, purl.TypeAppx, "Microsoft.Windows.Cortana", "1.11.5.17763", purl.WithArch("x86"), purl.WithNamespace("windows"), ).String() ``` Will produce the purl: `pkg:appx/windows/Microsoft.Windows.Cortana@1.11.5.17763?arch=x86` --------- Signed-off-by: Salim Afiune Maya --- cli/reporter/cnquery_report.pb.go | 4 +- explorer/cnquery_explorer.pb.go | 2 +- .../cnquery_resources_explorer.pb.go | 4 +- explorer/scan/cnquery_explorer_scan.pb.go | 4 +- llx/llx.pb.go | 4 +- providers-sdk/v1/plugin/plugin_grpc.pb.go | 44 +++- providers-sdk/v1/upstream/health/errors.pb.go | 4 +- providers-sdk/v1/upstream/health/health.pb.go | 4 +- providers-sdk/v1/upstream/mvd/cvss/cvss.pb.go | 4 +- providers-sdk/v1/upstream/mvd/mvd.pb.go | 4 +- providers-sdk/v1/upstream/upstream.pb.go | 4 +- .../os/resources/packages/aix_packages.go | 7 +- .../resources/packages/aix_packages_test.go | 2 +- .../os/resources/packages/apk_packages.go | 6 +- .../os/resources/packages/dpkg_packages.go | 6 +- .../os/resources/packages/macos_packages.go | 12 +- .../resources/packages/macos_packages_test.go | 10 +- providers/os/resources/packages/packages.go | 2 +- .../os/resources/packages/pacman_packages.go | 8 +- .../packages/pacman_packages_test.go | 11 +- .../os/resources/packages/rpm_packages.go | 6 +- .../os/resources/packages/windows_packages.go | 30 ++- .../packages/windows_packages_test.go | 33 ++- providers/os/resources/purl/purl.go | 121 ++++++++--- providers/os/resources/purl/purl_test.go | 194 ++++++++++++++++++ providers/os/resources/purl/purl_types.go | 48 +++++ .../os/resources/purl/purl_types_test.go | 73 +++++++ sbom/sbom.pb.go | 4 +- shared/proto/cnquery.pb.go | 4 +- shared/proto/cnquery_grpc.pb.go | 2 +- 30 files changed, 562 insertions(+), 99 deletions(-) create mode 100644 providers/os/resources/purl/purl_test.go create mode 100644 providers/os/resources/purl/purl_types.go create mode 100644 providers/os/resources/purl/purl_types_test.go diff --git a/cli/reporter/cnquery_report.pb.go b/cli/reporter/cnquery_report.pb.go index aa90de6129..f2fe7e78ca 100644 --- a/cli/reporter/cnquery_report.pb.go +++ b/cli/reporter/cnquery_report.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: cnquery_report.proto package reporter diff --git a/explorer/cnquery_explorer.pb.go b/explorer/cnquery_explorer.pb.go index bd802cfe5b..69e9c55056 100644 --- a/explorer/cnquery_explorer.pb.go +++ b/explorer/cnquery_explorer.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.35.2 -// protoc v5.28.3 +// protoc v5.29.0 // source: cnquery_explorer.proto package explorer diff --git a/explorer/resources/cnquery_resources_explorer.pb.go b/explorer/resources/cnquery_resources_explorer.pb.go index 958e593f48..37fd0827c0 100644 --- a/explorer/resources/cnquery_resources_explorer.pb.go +++ b/explorer/resources/cnquery_resources_explorer.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: cnquery_resources_explorer.proto package resources diff --git a/explorer/scan/cnquery_explorer_scan.pb.go b/explorer/scan/cnquery_explorer_scan.pb.go index a4d84dbb1c..c697805d24 100644 --- a/explorer/scan/cnquery_explorer_scan.pb.go +++ b/explorer/scan/cnquery_explorer_scan.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: cnquery_explorer_scan.proto package scan diff --git a/llx/llx.pb.go b/llx/llx.pb.go index 71f7ab2fe6..fd9b1d00b4 100644 --- a/llx/llx.pb.go +++ b/llx/llx.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: llx.proto package llx diff --git a/providers-sdk/v1/plugin/plugin_grpc.pb.go b/providers-sdk/v1/plugin/plugin_grpc.pb.go index 81b221fc9e..63e57fc937 100644 --- a/providers-sdk/v1/plugin/plugin_grpc.pb.go +++ b/providers-sdk/v1/plugin/plugin_grpc.pb.go @@ -3,7 +3,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.4.0 +// - protoc-gen-go-grpc v1.5.1 // - protoc v5.29.0 // source: plugin.proto @@ -18,8 +18,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.62.0 or later. -const _ = grpc.SupportPackageIsVersion8 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( ProviderPlugin_Heartbeat_FullMethodName = "/cnquery.providers.v1.ProviderPlugin/Heartbeat" @@ -136,7 +136,7 @@ func (c *providerPluginClient) StoreData(ctx context.Context, in *StoreReq, opts // ProviderPluginServer is the server API for ProviderPlugin service. // All implementations must embed UnimplementedProviderPluginServer -// for forward compatibility +// for forward compatibility. type ProviderPluginServer interface { Heartbeat(context.Context, *HeartbeatReq) (*HeartbeatRes, error) ParseCLI(context.Context, *ParseCLIReq) (*ParseCLIRes, error) @@ -149,9 +149,12 @@ type ProviderPluginServer interface { mustEmbedUnimplementedProviderPluginServer() } -// UnimplementedProviderPluginServer must be embedded to have forward compatible implementations. -type UnimplementedProviderPluginServer struct { -} +// UnimplementedProviderPluginServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedProviderPluginServer struct{} func (UnimplementedProviderPluginServer) Heartbeat(context.Context, *HeartbeatReq) (*HeartbeatRes, error) { return nil, status.Errorf(codes.Unimplemented, "method Heartbeat not implemented") @@ -178,6 +181,7 @@ func (UnimplementedProviderPluginServer) StoreData(context.Context, *StoreReq) ( return nil, status.Errorf(codes.Unimplemented, "method StoreData not implemented") } func (UnimplementedProviderPluginServer) mustEmbedUnimplementedProviderPluginServer() {} +func (UnimplementedProviderPluginServer) testEmbeddedByValue() {} // UnsafeProviderPluginServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ProviderPluginServer will @@ -187,6 +191,13 @@ type UnsafeProviderPluginServer interface { } func RegisterProviderPluginServer(s grpc.ServiceRegistrar, srv ProviderPluginServer) { + // If the following call pancis, it indicates UnimplementedProviderPluginServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&ProviderPlugin_ServiceDesc, srv) } @@ -433,7 +444,7 @@ func (c *providerCallbackClient) GetData(ctx context.Context, in *DataReq, opts // ProviderCallbackServer is the server API for ProviderCallback service. // All implementations must embed UnimplementedProviderCallbackServer -// for forward compatibility +// for forward compatibility. type ProviderCallbackServer interface { Collect(context.Context, *DataRes) (*CollectRes, error) GetRecording(context.Context, *DataReq) (*ResourceData, error) @@ -441,9 +452,12 @@ type ProviderCallbackServer interface { mustEmbedUnimplementedProviderCallbackServer() } -// UnimplementedProviderCallbackServer must be embedded to have forward compatible implementations. -type UnimplementedProviderCallbackServer struct { -} +// UnimplementedProviderCallbackServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedProviderCallbackServer struct{} func (UnimplementedProviderCallbackServer) Collect(context.Context, *DataRes) (*CollectRes, error) { return nil, status.Errorf(codes.Unimplemented, "method Collect not implemented") @@ -455,6 +469,7 @@ func (UnimplementedProviderCallbackServer) GetData(context.Context, *DataReq) (* return nil, status.Errorf(codes.Unimplemented, "method GetData not implemented") } func (UnimplementedProviderCallbackServer) mustEmbedUnimplementedProviderCallbackServer() {} +func (UnimplementedProviderCallbackServer) testEmbeddedByValue() {} // UnsafeProviderCallbackServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to ProviderCallbackServer will @@ -464,6 +479,13 @@ type UnsafeProviderCallbackServer interface { } func RegisterProviderCallbackServer(s grpc.ServiceRegistrar, srv ProviderCallbackServer) { + // If the following call pancis, it indicates UnimplementedProviderCallbackServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&ProviderCallback_ServiceDesc, srv) } diff --git a/providers-sdk/v1/upstream/health/errors.pb.go b/providers-sdk/v1/upstream/health/errors.pb.go index 3b3906c226..392f24e7d2 100644 --- a/providers-sdk/v1/upstream/health/errors.pb.go +++ b/providers-sdk/v1/upstream/health/errors.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: errors.proto package health diff --git a/providers-sdk/v1/upstream/health/health.pb.go b/providers-sdk/v1/upstream/health/health.pb.go index 4d5917bb30..01526cf4d8 100644 --- a/providers-sdk/v1/upstream/health/health.pb.go +++ b/providers-sdk/v1/upstream/health/health.pb.go @@ -17,8 +17,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: health.proto package health diff --git a/providers-sdk/v1/upstream/mvd/cvss/cvss.pb.go b/providers-sdk/v1/upstream/mvd/cvss/cvss.pb.go index 411934c581..99a0e3505b 100644 --- a/providers-sdk/v1/upstream/mvd/cvss/cvss.pb.go +++ b/providers-sdk/v1/upstream/mvd/cvss/cvss.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: cvss.proto package cvss diff --git a/providers-sdk/v1/upstream/mvd/mvd.pb.go b/providers-sdk/v1/upstream/mvd/mvd.pb.go index a7c631b812..9c87f8e8fd 100644 --- a/providers-sdk/v1/upstream/mvd/mvd.pb.go +++ b/providers-sdk/v1/upstream/mvd/mvd.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: mvd.proto package mvd diff --git a/providers-sdk/v1/upstream/upstream.pb.go b/providers-sdk/v1/upstream/upstream.pb.go index 699653e03a..39d7475118 100644 --- a/providers-sdk/v1/upstream/upstream.pb.go +++ b/providers-sdk/v1/upstream/upstream.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: upstream.proto package upstream diff --git a/providers/os/resources/packages/aix_packages.go b/providers/os/resources/packages/aix_packages.go index a863e98339..6e8a3e01d5 100644 --- a/providers/os/resources/packages/aix_packages.go +++ b/providers/os/resources/packages/aix_packages.go @@ -9,7 +9,6 @@ import ( "io" "strings" - "github.com/package-url/packageurl-go" "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" cpe2 "go.mondoo.com/cnquery/v11/providers/os/resources/cpe" "go.mondoo.com/cnquery/v11/providers/os/resources/purl" @@ -43,8 +42,10 @@ func parseAixPackages(pf *inventory.Platform, r io.Reader) ([]Package, error) { Version: record[2], Description: strings.TrimSpace(record[6]), Format: AixPkgFormat, - PUrl: purl.NewPackageUrl(pf, record[1], record[2], "", "", packageurl.TypeGeneric), - CPEs: cpes, + PUrl: purl.NewPackageURL( + pf, purl.TypeGeneric, record[1], record[2], purl.WithNamespace(pf.Name), + ).String(), + CPEs: cpes, }) } diff --git a/providers/os/resources/packages/aix_packages_test.go b/providers/os/resources/packages/aix_packages_test.go index 467121faef..9fb0371f42 100644 --- a/providers/os/resources/packages/aix_packages_test.go +++ b/providers/os/resources/packages/aix_packages_test.go @@ -31,7 +31,7 @@ func TestParseAixPackages(t *testing.T) { Name: "X11.apps.msmit", Version: "7.3.0.0", Description: "AIXwindows msmit Application", - PUrl: "pkg:generic/aix/X11.apps.msmit@7.3.0.0?distro=aix-7.2", + PUrl: "pkg:generic/aix/X11.apps.msmit@7.3.0.0?arch=powerpc&distro=aix-7.2", CPEs: []string{ "cpe:2.3:a:x11.apps.msmit:x11.apps.msmit:7.3.0.0:*:*:*:*:*:powerpc:*", "cpe:2.3:a:x11.apps.msmit:x11.apps.msmit:7.3.0:*:*:*:*:*:powerpc:*", diff --git a/providers/os/resources/packages/apk_packages.go b/providers/os/resources/packages/apk_packages.go index 793e70e905..0b1b144675 100644 --- a/providers/os/resources/packages/apk_packages.go +++ b/providers/os/resources/packages/apk_packages.go @@ -10,7 +10,6 @@ import ( "path/filepath" "regexp" - "github.com/package-url/packageurl-go" "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" cpe2 "go.mondoo.com/cnquery/v11/providers/os/resources/cpe" "go.mondoo.com/cnquery/v11/providers/os/resources/purl" @@ -44,7 +43,10 @@ func ParseApkDbPackages(pf *inventory.Platform, input io.Reader) []Package { } pkg.Format = AlpinePkgFormat - pkg.PUrl = purl.NewPackageUrl(pf, pkg.Name, pkg.Version, pkg.Arch, pkg.Epoch, packageurl.TypeApk) + pkg.PUrl = purl.NewPackageURL(pf, purl.TypeApk, pkg.Name, pkg.Version, + purl.WithArch(pkg.Arch), + purl.WithEpoch(pkg.Epoch), + ).String() cpes, _ := cpe2.NewPackage2Cpe(pkg.Vendor, pkg.Name, pkg.Version, "", pf.Arch) pkg.CPEs = cpes diff --git a/providers/os/resources/packages/dpkg_packages.go b/providers/os/resources/packages/dpkg_packages.go index af8130c8b2..361bc26e56 100644 --- a/providers/os/resources/packages/dpkg_packages.go +++ b/providers/os/resources/packages/dpkg_packages.go @@ -11,7 +11,6 @@ import ( "regexp" "strings" - "github.com/package-url/packageurl-go" "github.com/rs/zerolog/log" "github.com/spf13/afero" "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" @@ -39,7 +38,10 @@ func ParseDpkgPackages(pf *inventory.Platform, input io.Reader) ([]Package, erro add := func(pkg Package) { // do sanitization checks to ensure we have minimal information if pkg.Name != "" && pkg.Version != "" { - pkg.PUrl = purl.NewPackageUrl(pf, pkg.Name, pkg.Version, pkg.Arch, pkg.Epoch, packageurl.TypeDebian) + pkg.PUrl = purl.NewPackageURL(pf, purl.TypeDebian, pkg.Name, pkg.Version, + purl.WithArch(pkg.Arch), + purl.WithEpoch(pkg.Epoch), + ).String() cpes, _ := cpe.NewPackage2Cpe(pkg.Name, pkg.Name, pkg.Version, pkg.Epoch, pkg.Arch) cpesWithoutArch, _ := cpe.NewPackage2Cpe(pkg.Name, pkg.Name, pkg.Version, pkg.Epoch, "") cpes = append(cpes, cpesWithoutArch...) diff --git a/providers/os/resources/packages/macos_packages.go b/providers/os/resources/packages/macos_packages.go index 8cfbf1406a..0b0221735a 100644 --- a/providers/os/resources/packages/macos_packages.go +++ b/providers/os/resources/packages/macos_packages.go @@ -9,7 +9,9 @@ import ( "strings" "github.com/cockroachdb/errors" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v11/providers/os/connection/shared" + "go.mondoo.com/cnquery/v11/providers/os/resources/purl" plist "howett.net/plist" ) @@ -18,7 +20,7 @@ const ( ) // parse macos system version property list -func ParseMacOSPackages(input io.Reader) ([]Package, error) { +func ParseMacOSPackages(platform *inventory.Platform, input io.Reader) ([]Package, error) { var r io.ReadSeeker r, ok := input.(io.ReadSeeker) @@ -58,6 +60,9 @@ func ParseMacOSPackages(input io.Reader) ([]Package, error) { pkgs[i].Version = entry.Version pkgs[i].Format = MacosPkgFormat pkgs[i].FilesAvailable = PkgFilesIncluded + pkgs[i].PUrl = purl.NewPackageURL( + platform, purl.TypeMacos, entry.Name, entry.Version, + ).String() if entry.Path != "" { pkgs[i].Files = []FileRecord{ { @@ -72,7 +77,8 @@ func ParseMacOSPackages(input io.Reader) ([]Package, error) { // MacOS type MacOSPkgManager struct { - conn shared.Connection + conn shared.Connection + platform *inventory.Platform } func (mpm *MacOSPkgManager) Name() string { @@ -89,7 +95,7 @@ func (mpm *MacOSPkgManager) List() ([]Package, error) { return nil, fmt.Errorf("could not read package list") } - return ParseMacOSPackages(cmd.Stdout) + return ParseMacOSPackages(mpm.platform, cmd.Stdout) } func (mpm *MacOSPkgManager) Available() (map[string]PackageUpdate, error) { diff --git a/providers/os/resources/packages/macos_packages_test.go b/providers/os/resources/packages/macos_packages_test.go index 6c147c17ed..70635a0ae5 100644 --- a/providers/os/resources/packages/macos_packages_test.go +++ b/providers/os/resources/packages/macos_packages_test.go @@ -23,7 +23,13 @@ func TestMacOsXPackageParser(t *testing.T) { } assert.Nil(t, err) - m, err := packages.ParseMacOSPackages(c.Stdout) + pf := &inventory.Platform{ + Name: "macos", + Version: "15.2", + Arch: "x86_64", + Family: []string{"darwin", "bsd", "unix", "os"}, + } + m, err := packages.ParseMacOSPackages(pf, c.Stdout) assert.Nil(t, err) assert.Equal(t, 2, len(m), "detected the right amount of packages") @@ -31,11 +37,13 @@ func TestMacOsXPackageParser(t *testing.T) { assert.Equal(t, "10.0", m[0].Version, "pkg version detected") assert.Equal(t, packages.MacosPkgFormat, m[0].Format, "pkg format detected") assert.Equal(t, packages.PkgFilesIncluded, m[0].FilesAvailable) + assert.Equal(t, "pkg:macos/Preview@10.0?arch=x86_64", m[0].PUrl) assert.Equal(t, []packages.FileRecord{{Path: "/Applications/Preview.app"}}, m[0].Files) assert.Equal(t, "Contacts", m[1].Name, "pkg name detected") assert.Equal(t, "11.0", m[1].Version, "pkg version detected") assert.Equal(t, packages.MacosPkgFormat, m[1].Format, "pkg format detected") assert.Equal(t, packages.PkgFilesIncluded, m[1].FilesAvailable) + assert.Equal(t, "pkg:macos/Contacts@11.0?arch=x86_64", m[1].PUrl) assert.Equal(t, []packages.FileRecord{{Path: "/Applications/Contacts.app"}}, m[1].Files) } diff --git a/providers/os/resources/packages/packages.go b/providers/os/resources/packages/packages.go index 1867b45266..b9c965f18f 100644 --- a/providers/os/resources/packages/packages.go +++ b/providers/os/resources/packages/packages.go @@ -111,7 +111,7 @@ func ResolveSystemPkgManager(conn shared.Connection) (OperatingSystemPkgManager, case asset.Platform.Name == "alpine" || asset.Platform.Name == "wolfi": // alpine & wolfi share apk pm = &AlpinePkgManager{conn: conn, platform: asset.Platform} case asset.Platform.Name == "macos": // mac os family - pm = &MacOSPkgManager{conn: conn} + pm = &MacOSPkgManager{conn: conn, platform: asset.Platform} case asset.Platform.Name == "windows": pm = &WinPkgManager{conn: conn, platform: asset.Platform} case asset.Platform.Name == "scratch" || asset.Platform.Name == "coreos": diff --git a/providers/os/resources/packages/pacman_packages.go b/providers/os/resources/packages/pacman_packages.go index 36725b2c58..4fc752522d 100644 --- a/providers/os/resources/packages/pacman_packages.go +++ b/providers/os/resources/packages/pacman_packages.go @@ -6,12 +6,12 @@ package packages import ( "bufio" "fmt" - "github.com/package-url/packageurl-go" - "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" - "go.mondoo.com/cnquery/v11/providers/os/resources/purl" "io" "regexp" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v11/providers/os/resources/purl" + "github.com/cockroachdb/errors" "go.mondoo.com/cnquery/v11/providers/os/connection/shared" ) @@ -35,7 +35,7 @@ func ParsePacmanPackages(pf *inventory.Platform, input io.Reader) []Package { Name: name, Version: version, Format: PacmanPkgFormat, - PUrl: purl.NewPackageUrl(pf, name, version, "", "", packageurl.TypeAlpm), + PUrl: purl.NewPackageURL(pf, purl.TypeAlpm, name, version).String(), }) } } diff --git a/providers/os/resources/packages/pacman_packages_test.go b/providers/os/resources/packages/pacman_packages_test.go index 40f85db006..52a78d23c0 100644 --- a/providers/os/resources/packages/pacman_packages_test.go +++ b/providers/os/resources/packages/pacman_packages_test.go @@ -4,10 +4,11 @@ package packages_test import ( - "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "strings" "testing" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" + "github.com/stretchr/testify/assert" "go.mondoo.com/cnquery/v11/providers/os/resources/packages" ) @@ -38,7 +39,7 @@ zziplib 0.13.67-1` p := packages.Package{ Name: "qpdfview", Version: "0.4.17beta1-4.1", - PUrl: "pkg:alpm/arch/qpdfview@0.4.17beta1-4.1?distro=arch", + PUrl: "pkg:alpm/arch/qpdfview@0.4.17beta1-4.1?arch=x86_64&distro=arch", Format: packages.PacmanPkgFormat, } assert.Contains(t, m, p, "pkg detected") @@ -46,7 +47,7 @@ zziplib 0.13.67-1` p = packages.Package{ Name: "vertex-maia-themes", Version: "20171114-1", - PUrl: "pkg:alpm/arch/vertex-maia-themes@20171114-1?distro=arch", + PUrl: "pkg:alpm/arch/vertex-maia-themes@20171114-1?arch=x86_64&distro=arch", Format: packages.PacmanPkgFormat, } assert.Contains(t, m, p, "pkg detected") @@ -54,7 +55,7 @@ zziplib 0.13.67-1` p = packages.Package{ Name: "xfce4-pulseaudio-plugin", Version: "0.3.2.r13.g553691a-1", - PUrl: "pkg:alpm/arch/xfce4-pulseaudio-plugin@0.3.2.r13.g553691a-1?distro=arch", + PUrl: "pkg:alpm/arch/xfce4-pulseaudio-plugin@0.3.2.r13.g553691a-1?arch=x86_64&distro=arch", Format: packages.PacmanPkgFormat, } assert.Contains(t, m, p, "pkg detected") @@ -84,7 +85,7 @@ argon2 20190702-2` p := packages.Package{ Name: "acl", Version: "2.2.53-2", - PUrl: "pkg:alpm/arch/acl@2.2.53-2?distro=arch", + PUrl: "pkg:alpm/arch/acl@2.2.53-2?arch=x86_64&distro=arch", Format: packages.PacmanPkgFormat, } assert.Contains(t, m, p, "pkg detected") diff --git a/providers/os/resources/packages/rpm_packages.go b/providers/os/resources/packages/rpm_packages.go index 876669be88..92ec540d13 100644 --- a/providers/os/resources/packages/rpm_packages.go +++ b/providers/os/resources/packages/rpm_packages.go @@ -13,7 +13,6 @@ import ( "strconv" "strings" - "github.com/package-url/packageurl-go" "go.mondoo.com/cnquery/v11/providers/os/resources/cpe" "go.mondoo.com/cnquery/v11/providers/os/resources/purl" @@ -94,9 +93,12 @@ func newRpmPackage(pf *inventory.Platform, name, version, arch, epoch, vendor, d Arch: arch, Description: description, Format: RpmPkgFormat, - PUrl: purl.NewPackageUrl(pf, name, version, arch, epoch, packageurl.TypeRPM), CPEs: cpes, Vendor: vendor, + PUrl: purl.NewPackageURL(pf, purl.TypeRPM, name, version, + purl.WithArch(arch), + purl.WithEpoch(epoch), + ).String(), } } diff --git a/providers/os/resources/packages/windows_packages.go b/providers/os/resources/packages/windows_packages.go index 714b8df191..4d4b3c8b50 100644 --- a/providers/os/resources/packages/windows_packages.go +++ b/providers/os/resources/packages/windows_packages.go @@ -22,6 +22,7 @@ import ( "go.mondoo.com/cnquery/v11/providers/os/registry" "go.mondoo.com/cnquery/v11/providers/os/resources/cpe" "go.mondoo.com/cnquery/v11/providers/os/resources/powershell" + "go.mondoo.com/cnquery/v11/providers/os/resources/purl" ) // ProcessorArchitecture Enum @@ -112,7 +113,7 @@ type winAppxPackages struct { arch string `json:"-"` } -func (p winAppxPackages) toPackage() Package { +func (p winAppxPackages) toPackage(platform *inventory.Platform) Package { if p.arch == "" { arch, ok := appxArchitecture[p.Architecture] if !ok { @@ -128,12 +129,16 @@ func (p winAppxPackages) toPackage() Package { Arch: p.arch, Format: "windows/appx", Vendor: p.Publisher, + PUrl: purl.NewPackageURL(platform, purl.TypeAppx, p.Name, p.Version).String(), } if p.Name != "" && p.Version != "" { cpeWfns, err := cpe.NewPackage2Cpe(p.Publisher, p.Name, p.Version, "", "") if err != nil { - log.Debug().Err(err).Str("name", p.Name).Str("version", p.Version).Msg("could not create cpe for windows appx package") + log.Debug().Err(err). + Str("name", p.Name). + Str("version", p.Version). + Msg("could not create cpe for windows appx package") } else { pkg.CPEs = cpeWfns } @@ -145,7 +150,7 @@ func (p winAppxPackages) toPackage() Package { } // Good read: https://www.wintips.org/view-installed-apps-and-packages-in-windows-10-8-1-8-from-powershell/ -func ParseWindowsAppxPackages(input io.Reader) ([]Package, error) { +func ParseWindowsAppxPackages(platform *inventory.Platform, input io.Reader) ([]Package, error) { data, err := io.ReadAll(input) if err != nil { return nil, err @@ -165,8 +170,7 @@ func ParseWindowsAppxPackages(input io.Reader) ([]Package, error) { pkgs := make([]Package, len(appxPackages)) for i, p := range appxPackages { - pkg := p.toPackage() - pkgs[i] = pkg + pkgs[i] = p.toPackage(platform) } return pkgs, nil } @@ -281,7 +285,7 @@ func (w *WinPkgManager) getInstalledApps() ([]Package, error) { return nil, errors.New("failed to retrieve installed apps: " + string(stderr)) } - return ParseWindowsAppPackages(cmd.Stdout) + return ParseWindowsAppPackages(w.platform, cmd.Stdout) } func (w *WinPkgManager) getAppxPackages() ([]Package, error) { @@ -309,7 +313,7 @@ func (w *WinPkgManager) getPwshAppxPackages() ([]Package, error) { if err != nil { return nil, fmt.Errorf("could not read appx package list") } - return ParseWindowsAppxPackages(cmd.Stdout) + return ParseWindowsAppxPackages(w.platform, cmd.Stdout) } func (w *WinPkgManager) getFsInstalledApps() ([]Package, error) { @@ -394,7 +398,7 @@ func (w *WinPkgManager) getFsAppxPackages() ([]Package, error) { log.Debug().Err(err).Str("path", p).Msg("could not parse appx manifest") continue } - pkg := winAppxPkg.toPackage() + pkg := winAppxPkg.toPackage(w.platform) pkgs = append(pkgs, pkg) } @@ -504,7 +508,7 @@ func (w *WinPkgManager) List() ([]Package, error) { return pkgs, nil } -func ParseWindowsAppPackages(input io.Reader) ([]Package, error) { +func ParseWindowsAppPackages(platform *inventory.Platform, input io.Reader) ([]Package, error) { data, err := io.ReadAll(input) if err != nil { return nil, err @@ -540,7 +544,10 @@ func ParseWindowsAppPackages(input io.Reader) ([]Package, error) { if entry.DisplayName != "" && entry.DisplayVersion != "" { cpeWfns, err = cpe.NewPackage2Cpe(entry.Publisher, entry.DisplayName, entry.DisplayVersion, "", "") if err != nil { - log.Debug().Err(err).Str("name", entry.DisplayName).Str("version", entry.DisplayVersion).Msg("could not create cpe for windows app package") + log.Debug().Err(err). + Str("name", entry.DisplayName). + Str("version", entry.DisplayVersion). + Msg("could not create cpe for windows app package") } } else { log.Debug().Msg("ignored package since information is missing") @@ -551,6 +558,9 @@ func ParseWindowsAppPackages(input io.Reader) ([]Package, error) { Format: "windows/app", CPEs: cpeWfns, Vendor: entry.Publisher, + PUrl: purl.NewPackageURL( + platform, purl.TypeWindows, entry.DisplayName, entry.DisplayVersion, + ).String(), }) } diff --git a/providers/os/resources/packages/windows_packages_test.go b/providers/os/resources/packages/windows_packages_test.go index 3feb0f4414..eff9f7d829 100644 --- a/providers/os/resources/packages/windows_packages_test.go +++ b/providers/os/resources/packages/windows_packages_test.go @@ -22,7 +22,13 @@ func TestWindowsAppPackagesParser(t *testing.T) { require.NoError(t, err) defer f.Close() - pkgs, err := ParseWindowsAppPackages(f) + pf := &inventory.Platform{ + Name: "windows", + Version: "10.0.18363", + Arch: "x86", + Family: []string{"windows"}, + } + pkgs, err := ParseWindowsAppPackages(pf, f) assert.Nil(t, err) assert.Equal(t, 19, len(pkgs), "detected the right amount of packages") @@ -32,6 +38,7 @@ func TestWindowsAppPackagesParser(t *testing.T) { Version: "14.28.29913.0", Arch: "", Format: "windows/app", + PUrl: `pkg:windows/Microsoft%20Visual%20C%2B%2B%202015-2019%20Redistributable%20%28x86%29%20-%2014.28.29913@14.28.29913.0?arch=x86`, CPEs: []string{ "cpe:2.3:a:microsoft_corporation:microsoft_visual_c\\+\\+_2015-2019_redistributable_\\(x86\\)_-_14.28.29913:14.28.29913.0:*:*:*:*:*:*:*", "cpe:2.3:a:microsoft:microsoft_visual_c\\+\\+_2015-2019_redistributable_\\(x86\\)_-_14.28.29913:14.28.29913.0:*:*:*:*:*:*:*", @@ -41,7 +48,7 @@ func TestWindowsAppPackagesParser(t *testing.T) { }, p) // check empty return - pkgs, err = ParseWindowsAppxPackages(strings.NewReader("")) + pkgs, err = ParseWindowsAppxPackages(pf, strings.NewReader("")) assert.Nil(t, err) assert.Equal(t, 0, len(pkgs), "detected the right amount of packages") } @@ -61,7 +68,14 @@ func TestWindowsAppxPackagesParser(t *testing.T) { t.Fatal(err) } - pkgs, err := ParseWindowsAppxPackages(c.Stdout) + pf := &inventory.Platform{ + Name: "windows", + Version: "10.0.18363", + Arch: "x86", + Family: []string{"windows"}, + } + + pkgs, err := ParseWindowsAppxPackages(pf, c.Stdout) assert.Nil(t, err) assert.Equal(t, 28, len(pkgs), "detected the right amount of packages") @@ -71,6 +85,7 @@ func TestWindowsAppxPackagesParser(t *testing.T) { Version: "1.11.5.17763", Arch: "neutral", Format: "windows/appx", + PUrl: "pkg:appx/Microsoft.Windows.Cortana@1.11.5.17763?arch=x86", // TODO: this is a bug in the CPE generation, we need to extract the publisher from the package CPEs: []string{ "cpe:2.3:a:cn\\=microsoft_corporation\\,_o\\=microsoft_corporation\\,_l\\=redmond\\,_s\\=washington\\,_c\\=us:microsoft.windows.cortana:1.11.5.17763:*:*:*:*:*:*:*", @@ -80,7 +95,7 @@ func TestWindowsAppxPackagesParser(t *testing.T) { }, p) // check empty return - pkgs, err = ParseWindowsAppxPackages(strings.NewReader("")) + pkgs, err = ParseWindowsAppxPackages(pf, strings.NewReader("")) assert.Nil(t, err) assert.Equal(t, 0, len(pkgs), "detected the right amount of packages") } @@ -203,13 +218,21 @@ func TestToPackage(t *testing.T) { Architecture: 0, } - pkg := winAppxPkg.toPackage() + pf := &inventory.Platform{ + Name: "windows", + Version: "10.0.18363", + Arch: "x86", + Family: []string{"windows"}, + } + + pkg := winAppxPkg.toPackage(pf) expected := Package{ Name: "Microsoft.Windows.Cortana", Version: "1.11.5.17763", Arch: "x86", Format: "windows/appx", + PUrl: "pkg:appx/Microsoft.Windows.Cortana@1.11.5.17763?arch=x86", Vendor: "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", CPEs: []string{ "cpe:2.3:a:cn\\=microsoft_corporation\\,_o\\=microsoft_corporation\\,_l\\=redmond\\,_s\\=washington\\,_c\\=us:microsoft.windows.cortana:1.11.5.17763:*:*:*:*:*:*:*", diff --git a/providers/os/resources/purl/purl.go b/providers/os/resources/purl/purl.go index 1ec4146d63..1edd9e18ae 100644 --- a/providers/os/resources/purl/purl.go +++ b/providers/os/resources/purl/purl.go @@ -4,11 +4,12 @@ package purl import ( + "sort" + "strings" + "github.com/package-url/packageurl-go" "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" "go.mondoo.com/cnquery/v11/providers/os/detector" - "sort" - "strings" ) const ( @@ -17,6 +18,23 @@ const ( QualifierEpoch = "epoch" ) +// PackageURL is a helper struct that renters a package url based of an inventory +// platform, purl type, and modifiers. +type PackageURL struct { + // Required: minimal attributes to render a PURL. + Type Type + Name string + Version string + + // Optional: can be set via modifiers. + Namespace string + Arch string + Epoch string + + // Used as metadata to fetch things like the architecture or linux distribution. + platform *inventory.Platform +} + // NewQualifiers creates a new Qualifiers slice from a map of key/value pairs. // see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst for more information func NewQualifiers(qualifier map[string]string) packageurl.Qualifiers { @@ -42,39 +60,92 @@ func NewQualifiers(qualifier map[string]string) packageurl.Qualifiers { return list } -// NewPackageUrl creates a new package url for a given platform, name, version, arch, epoch and purlType -// see https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst for more information -func NewPackageUrl(pf *inventory.Platform, name string, version string, arch string, epoch string, purlType string) string { - qualifiers := map[string]string{} - if arch != "" { - qualifiers[QualifierArch] = arch +// NewPackageURL creates a new package url for a given platform, name, version, and type. +// +// For more information, see: +// https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst +func NewPackageURL(pf *inventory.Platform, t Type, name, version string, modifiers ...Modifier) *PackageURL { + purl := &PackageURL{ + Type: t, + Name: name, + Version: version, + platform: pf, } - if epoch != "" && epoch != "0" { - qualifiers[QualifierEpoch] = epoch + // if a platform was provided + if pf != nil { + // use the platform architecture for the package + purl.Arch = pf.Arch + + // and if the distro is set via labels, set it as a namespace + if pf.Labels != nil && pf.Labels[detector.LabelDistroID] != "" { + purl.Namespace = pf.Labels[detector.LabelDistroID] + } } - namespace := pf.Name - if pf.Labels != nil && pf.Labels[detector.LabelDistroID] != "" { - namespace = pf.Labels[detector.LabelDistroID] + // apply modifiers + for _, modifier := range modifiers { + modifier(purl) } - // generate distro qualifier - distroQualifiers := []string{} - distroQualifiers = append(distroQualifiers, namespace) - if pf.Version != "" { - distroQualifiers = append(distroQualifiers, pf.Version) - } else if pf.Build != "" { - distroQualifiers = append(distroQualifiers, pf.Build) + return purl +} + +func (purl PackageURL) String() string { + qualifiers := map[string]string{} + if purl.Arch != "" { + qualifiers[QualifierArch] = purl.Arch + } + + if purl.Epoch != "" && purl.Epoch != "0" { + qualifiers[QualifierEpoch] = purl.Epoch + } + + if distroQualifiers, ok := purl.distroQualifiers(); ok { + qualifiers[QualifierDistro] = distroQualifiers } - qualifiers[QualifierDistro] = strings.Join(distroQualifiers, "-") return packageurl.NewPackageURL( - purlType, - namespace, - name, - version, + string(purl.Type), + purl.Namespace, + purl.Name, + purl.Version, NewQualifiers(qualifiers), "", ).ToString() } + +// generate distro qualifier +func (purl PackageURL) distroQualifiers() (string, bool) { + if purl.Namespace == "" { + return "", false + } + + distroQualifiers := []string{} + distroQualifiers = append(distroQualifiers, purl.Namespace) + if purl.platform.Version != "" { + distroQualifiers = append(distroQualifiers, purl.platform.Version) + } else if purl.platform.Build != "" { + distroQualifiers = append(distroQualifiers, purl.platform.Build) + } + + return strings.Join(distroQualifiers, "-"), true +} + +type Modifier func(*PackageURL) + +func WithArch(arch string) Modifier { + return func(purl *PackageURL) { + purl.Arch = arch + } +} +func WithEpoch(epoch string) Modifier { + return func(purl *PackageURL) { + purl.Epoch = epoch + } +} +func WithNamespace(namespace string) Modifier { + return func(purl *PackageURL) { + purl.Namespace = namespace + } +} diff --git a/providers/os/resources/purl/purl_test.go b/providers/os/resources/purl/purl_test.go new file mode 100644 index 0000000000..11de374ae4 --- /dev/null +++ b/providers/os/resources/purl/purl_test.go @@ -0,0 +1,194 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package purl_test + +import ( + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" + "go.mondoo.com/cnquery/v11/providers/os/resources/purl" +) + +func TestNewQualifiers(t *testing.T) { + t.Run("Empty qualifiers map", func(t *testing.T) { + result := purl.NewQualifiers(map[string]string{}) + assert.Empty(t, result) + }) + + t.Run("Valid qualifiers map", func(t *testing.T) { + qualifiers := map[string]string{ + "arch": "x86_64", + "os": "linux", + } + expected := packageurl.Qualifiers{ + {Key: "arch", Value: "x86_64"}, + {Key: "os", Value: "linux"}, + } + + result := purl.NewQualifiers(qualifiers) + assert.Equal(t, expected, result) + }) + + t.Run("Qualifiers map with empty values", func(t *testing.T) { + qualifiers := map[string]string{ + "arch": "x86_64", + "os": "", + } + expected := packageurl.Qualifiers{ + {Key: "arch", Value: "x86_64"}, + } + + result := purl.NewQualifiers(qualifiers) + assert.Equal(t, expected, result) + }) + + t.Run("Qualifiers map with unsorted keys", func(t *testing.T) { + qualifiers := map[string]string{ + "os": "linux", + "arch": "x86_64", + } + expected := packageurl.Qualifiers{ + {Key: "arch", Value: "x86_64"}, + {Key: "os", Value: "linux"}, + } + + result := purl.NewQualifiers(qualifiers) + assert.Equal(t, expected, result) + }) +} + +func TestNewPackageURL(t *testing.T) { + platform := &inventory.Platform{ + Arch: "x86_64", + Version: "22.04", + Labels: map[string]string{ + "distro-id": "ubuntu", + }, + } + + t.Run("Basic PackageURL", func(t *testing.T) { + p := purl.NewPackageURL(platform, purl.TypeApk, "testpkg", "1.0.0") + assert.Equal(t, purl.TypeApk, p.Type) + assert.Equal(t, "testpkg", p.Name) + assert.Equal(t, "1.0.0", p.Version) + assert.Equal(t, "x86_64", p.Arch) + assert.Equal(t, "ubuntu", p.Namespace) + }) + + t.Run("Modifiers applied", func(t *testing.T) { + p := purl.NewPackageURL(platform, purl.TypeRPM, "testpkg", "1.0.0", + purl.WithArch("arm64"), + purl.WithEpoch("1"), + ) + assert.Equal(t, "arm64", p.Arch) + assert.Equal(t, "1", p.Epoch) + }) + + t.Run("Nil platform won't discover optional attributes", func(t *testing.T) { + p := purl.NewPackageURL(nil, purl.TypeDebian, "testpkg", "1.0.0") + assert.Equal(t, purl.TypeDebian, p.Type) + assert.Equal(t, "testpkg", p.Name) + assert.Equal(t, "1.0.0", p.Version) + assert.Empty(t, p.Arch) + assert.Empty(t, p.Namespace) + }) +} + +func TestPackageURLString(t *testing.T) { + platform := &inventory.Platform{ + Arch: "x86_64", + Version: "22.04", + Labels: map[string]string{ + "distro-id": "ubuntu", + }, + } + + t.Run("Basic PackageURL string", func(t *testing.T) { + p := purl.NewPackageURL(platform, purl.TypeDebian, "testpkg", "1.0.0") + expected := "pkg:deb/ubuntu/testpkg@1.0.0?arch=x86_64&distro=ubuntu-22.04" + assert.Equal(t, expected, p.String()) + }) + + t.Run("With Epoch", func(t *testing.T) { + p := purl.NewPackageURL(platform, purl.TypeDebian, "testpkg", "1.0.0", + purl.WithEpoch("2"), + ) + expected := "pkg:deb/ubuntu/testpkg@1.0.0?arch=x86_64&distro=ubuntu-22.04&epoch=2" + assert.Equal(t, expected, p.String()) + }) + + t.Run("Without Namespace from platform", func(t *testing.T) { + platform := &inventory.Platform{ + Arch: "x86_64", + Version: "22.04", + Labels: nil, + } + p := purl.NewPackageURL(platform, purl.TypeDebian, "testpkg", "1.0.0") + expected := "pkg:deb/testpkg@1.0.0?arch=x86_64" + assert.Equal(t, expected, p.String()) + + t.Run("But Namespace from modifiers", func(t *testing.T) { + platform := &inventory.Platform{ + Arch: "x86_64", + Version: "22.04", + Labels: nil, + } + p := purl.NewPackageURL(platform, purl.TypeDebian, "testpkg", "1.0.0", + purl.WithNamespace("debian"), + ) + expected := "pkg:deb/debian/testpkg@1.0.0?arch=x86_64&distro=debian-22.04" + assert.Equal(t, expected, p.String()) + }) + }) + + t.Run("Modifiers overriding platform values", func(t *testing.T) { + p := purl.NewPackageURL(platform, purl.TypeDebian, "testpkg", "1.0.0", + purl.WithArch("arm64"), + ) + expected := "pkg:deb/ubuntu/testpkg@1.0.0?arch=arm64&distro=ubuntu-22.04" + assert.Equal(t, expected, p.String()) + }) + + t.Run("Empty Platform and Qualifiers", func(t *testing.T) { + p := purl.NewPackageURL(nil, purl.TypeApk, "testpkg", "1.0.0") + expected := "pkg:apk/testpkg@1.0.0" + assert.Equal(t, expected, p.String()) + }) + + t.Run("Non-standard Type", func(t *testing.T) { + p := purl.NewPackageURL(nil, "customtype", "testpkg", "1.0.0") + expected := "pkg:customtype/testpkg@1.0.0" + assert.Equal(t, expected, p.String()) + }) + + t.Run("Special characters in fields", func(t *testing.T) { + p := purl.NewPackageURL(nil, purl.TypeApk, "pkg@123", "1.0.0") + expected := "pkg:apk/pkg%40123@1.0.0" + assert.Equal(t, expected, p.String()) + }) + + t.Run("Empty name and version", func(t *testing.T) { + p := purl.NewPackageURL(nil, purl.TypeGeneric, "", "") + assert.Equal(t, purl.TypeGeneric, p.Type) + assert.Empty(t, p.Name) + assert.Empty(t, p.Version) + assert.Empty(t, p.Namespace) + assert.Empty(t, p.Arch) + }) + + t.Run("Both version and build specified, we prefer version", func(t *testing.T) { + platform.Build = "20.04" // just for testing + p := purl.NewPackageURL(platform, purl.TypeDebian, "testpkg", "1.0.0") + expected := "pkg:deb/ubuntu/testpkg@1.0.0?arch=x86_64&distro=ubuntu-22.04" + assert.Equal(t, expected, p.String()) + t.Run("Only build specified", func(t *testing.T) { + platform.Version = "" + p := purl.NewPackageURL(platform, purl.TypeDebian, "testpkg", "1.0.0") + expected := "pkg:deb/ubuntu/testpkg@1.0.0?arch=x86_64&distro=ubuntu-20.04" + assert.Equal(t, expected, p.String()) + }) + }) +} diff --git a/providers/os/resources/purl/purl_types.go b/providers/os/resources/purl/purl_types.go new file mode 100644 index 0000000000..3b5707c3bf --- /dev/null +++ b/providers/os/resources/purl/purl_types.go @@ -0,0 +1,48 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package purl + +import "github.com/package-url/packageurl-go" + +type Type string + +// These are only an extension of the known purl types defined at: +// +// https://github.com/package-url/purl-spec#known-purl-types +var ( + // TypeWindows is a pkg:windows purl. + TypeWindows Type = "windows" + // TypeAppx is a pkg:appx purl. + TypeAppx Type = "appx" + // TypeMacos is a pkg:macos purl. + TypeMacos Type = "macos" + + // Types we use coming from: + // https://github.com/package-url/packageurl-go/blob/master/packageurl.go#L54 + TypeGeneric = Type(packageurl.TypeGeneric) + TypeApk = Type(packageurl.TypeApk) + TypeDebian = Type(packageurl.TypeDebian) + TypeAlpm = Type(packageurl.TypeAlpm) + TypeRPM = Type(packageurl.TypeRPM) + + KnownTypes = map[Type]struct{}{ + TypeAppx: {}, + TypeWindows: {}, + TypeMacos: {}, + TypeGeneric: {}, + TypeApk: {}, + TypeDebian: {}, + TypeAlpm: {}, + TypeRPM: {}, + } +) + +func ValidTypeString(t string) bool { + return ValidType(Type(t)) +} + +func ValidType(t Type) bool { + _, ok := KnownTypes[t] + return ok +} diff --git a/providers/os/resources/purl/purl_types_test.go b/providers/os/resources/purl/purl_types_test.go new file mode 100644 index 0000000000..ba329b7c7e --- /dev/null +++ b/providers/os/resources/purl/purl_types_test.go @@ -0,0 +1,73 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package purl_test + +import ( + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "go.mondoo.com/cnquery/v11/providers/os/resources/purl" +) + +func TestValidType(t *testing.T) { + t.Run("Valid types should return true", func(t *testing.T) { + validTypes := []purl.Type{ + purl.TypeWindows, purl.TypeAppx, purl.TypeMacos, purl.TypeGeneric, + purl.TypeApk, purl.TypeDebian, purl.TypeAlpm, purl.TypeRPM, + } + + for _, validType := range validTypes { + assert.True(t, + purl.ValidType(validType), + "Expected type %s to be valid", validType) + } + }) + + t.Run("Invalid types should return false", func(t *testing.T) { + invalidTypes := []purl.Type{"invalid", "unknown", purl.Type("random")} + + for _, invalidType := range invalidTypes { + assert.False(t, + purl.ValidType(invalidType), + "Expected type %s to be invalid", invalidType) + } + }) + + t.Run("Empty type should return false", func(t *testing.T) { + assert.False(t, + purl.ValidType(purl.Type("")), + "Expected empty type to be invalid") + }) +} + +func TestValidTypeString(t *testing.T) { + t.Run("Valid type strings should return true", func(t *testing.T) { + validTypes := []string{ + string(purl.TypeWindows), string(purl.TypeAppx), string(purl.TypeMacos), + packageurl.TypeGeneric, packageurl.TypeApk, packageurl.TypeDebian, + packageurl.TypeAlpm, packageurl.TypeRPM, "windows", "appx", "macos", + } + + for _, validType := range validTypes { + assert.True(t, + purl.ValidTypeString(validType), + "Expected type string %s to be valid", validType) + } + }) + + t.Run("Invalid type strings should return false", func(t *testing.T) { + invalidTypes := []string{"invalid", "unknown", "random"} + + for _, invalidType := range invalidTypes { + assert.False(t, purl.ValidTypeString(invalidType), "Expected type string %s to be invalid", invalidType) + } + }) + + t.Run("Empty type string should return false", func(t *testing.T) { + assert.False(t, + purl.ValidTypeString(""), + "Expected empty type string to be invalid") + }) +} diff --git a/sbom/sbom.pb.go b/sbom/sbom.pb.go index 236d147cea..c07a8f5b95 100644 --- a/sbom/sbom.pb.go +++ b/sbom/sbom.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: sbom.proto package sbom diff --git a/shared/proto/cnquery.pb.go b/shared/proto/cnquery.pb.go index 7a88682d30..0ecb330e8a 100644 --- a/shared/proto/cnquery.pb.go +++ b/shared/proto/cnquery.pb.go @@ -3,8 +3,8 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.35.1 -// protoc v5.28.3 +// protoc-gen-go v1.35.2 +// protoc v5.29.0 // source: cnquery.proto package proto diff --git a/shared/proto/cnquery_grpc.pb.go b/shared/proto/cnquery_grpc.pb.go index 3ef3aece8c..6c9d541e29 100644 --- a/shared/proto/cnquery_grpc.pb.go +++ b/shared/proto/cnquery_grpc.pb.go @@ -4,7 +4,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.28.3 +// - protoc v5.29.0 // source: cnquery.proto package proto