From 4e1406af2c1db426f083472e95dc77502bf29b8b Mon Sep 17 00:00:00 2001 From: Christian Zunker <827818+czunker@users.noreply.github.com> Date: Tue, 29 Oct 2024 05:43:52 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20platform=20PURLs=20(#4785)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the asset platform in package URL format. Signed-off-by: Christian Zunker --- providers/os/resources/asset_purl.go | 23 +++ providers/os/resources/os.lr | 3 + providers/os/resources/os.lr.go | 14 ++ providers/os/resources/os.lr.manifest.yaml | 2 + providers/os/resources/purl/platform_purl.go | 42 +++++ .../os/resources/purl/platform_purl_test.go | 178 ++++++++++++++++++ 6 files changed, 262 insertions(+) create mode 100644 providers/os/resources/asset_purl.go create mode 100644 providers/os/resources/purl/platform_purl.go create mode 100644 providers/os/resources/purl/platform_purl_test.go diff --git a/providers/os/resources/asset_purl.go b/providers/os/resources/asset_purl.go new file mode 100644 index 0000000000..89d6edaa04 --- /dev/null +++ b/providers/os/resources/asset_purl.go @@ -0,0 +1,23 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package resources + +import ( + "go.mondoo.com/cnquery/v11/providers/os/connection/shared" + "go.mondoo.com/cnquery/v11/providers/os/resources/purl" +) + +func (a *mqlAsset) purl() (string, error) { + // use platform and version to generate the purl + conn, ok := a.MqlRuntime.Connection.(shared.Connection) + if ok && conn.Asset() != nil && conn.Asset().Platform != nil { + purlString, err := purl.NewPlatformPurl(conn.Asset().Platform) + if err != nil { + return "", err + } + return purlString, nil + } + + return "", nil +} diff --git a/providers/os/resources/os.lr b/providers/os/resources/os.lr index 45f67df8f6..5c538a83db 100644 --- a/providers/os/resources/os.lr +++ b/providers/os/resources/os.lr @@ -23,6 +23,9 @@ extend asset { // Deprecated; will be removed in version 12.0 // use vulnmgmt instead vulnerabilityReport() dict + // Platform URL in the Package URL format + // That's a URL as alternative to the CPE format + purl() string } asset.eol @defaults("date") { diff --git a/providers/os/resources/os.lr.go b/providers/os/resources/os.lr.go index 8811206110..2708b29bf3 100644 --- a/providers/os/resources/os.lr.go +++ b/providers/os/resources/os.lr.go @@ -552,6 +552,9 @@ var getDataFields = map[string]func(r plugin.Resource) *plugin.DataRes{ "asset.vulnerabilityReport": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlAsset).GetVulnerabilityReport()).ToDataRes(types.Dict) }, + "asset.purl": func(r plugin.Resource) *plugin.DataRes { + return (r.(*mqlAsset).GetPurl()).ToDataRes(types.String) + }, "asset.eol.docsUrl": func(r plugin.Resource) *plugin.DataRes { return (r.(*mqlAssetEol).GetDocsUrl()).ToDataRes(types.String) }, @@ -2301,6 +2304,10 @@ var setDataFields = map[string]func(r plugin.Resource, v *llx.RawData) bool { r.(*mqlAsset).VulnerabilityReport, ok = plugin.RawToTValue[interface{}](v.Value, v.Error) return }, + "asset.purl": func(r plugin.Resource, v *llx.RawData) (ok bool) { + r.(*mqlAsset).Purl, ok = plugin.RawToTValue[string](v.Value, v.Error) + return + }, "asset.eol.__id": func(r plugin.Resource, v *llx.RawData) (ok bool) { r.(*mqlAssetEol).__id, ok = v.Value.(string) return @@ -5088,6 +5095,7 @@ type mqlAsset struct { // optional: if you define mqlAssetInternal it will be used here Cpes plugin.TValue[[]interface{}] VulnerabilityReport plugin.TValue[interface{}] + Purl plugin.TValue[string] } // createAsset creates a new instance of this resource @@ -5144,6 +5152,12 @@ func (c *mqlAsset) GetVulnerabilityReport() *plugin.TValue[interface{}] { }) } +func (c *mqlAsset) GetPurl() *plugin.TValue[string] { + return plugin.GetOrCompute[string](&c.Purl, func() (string, error) { + return c.purl() + }) +} + // mqlAssetEol for the asset.eol resource type mqlAssetEol struct { MqlRuntime *plugin.Runtime diff --git a/providers/os/resources/os.lr.manifest.yaml b/providers/os/resources/os.lr.manifest.yaml index 4f0ea3c0ac..cbfc765482 100644 --- a/providers/os/resources/os.lr.manifest.yaml +++ b/providers/os/resources/os.lr.manifest.yaml @@ -6,6 +6,8 @@ resources: fields: cpe: {} cpes: {} + purl: + min_mondoo_version: 9.0.0 vulnerabilityReport: {} min_mondoo_version: latest asset.eol: diff --git a/providers/os/resources/purl/platform_purl.go b/providers/os/resources/purl/platform_purl.go new file mode 100644 index 0000000000..dc93227cda --- /dev/null +++ b/providers/os/resources/purl/platform_purl.go @@ -0,0 +1,42 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package purl + +import ( + "fmt" + "strings" + + "github.com/package-url/packageurl-go" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" +) + +func NewPlatformPurl(platform *inventory.Platform) (string, error) { + if platform == nil { + return "", fmt.Errorf("platform is required") + } + + qualifiers := map[string]string{} + if platform.Arch != "" { + qualifiers[QualifierArch] = platform.Arch + } + + // generate distro qualifier + distroQualifiers := []string{} + distroQualifiers = append(distroQualifiers, platform.Name) + if platform.Version != "" { + distroQualifiers = append(distroQualifiers, platform.Version) + } else if platform.Build != "" { + distroQualifiers = append(distroQualifiers, platform.Build) + } + qualifiers[QualifierDistro] = strings.Join(distroQualifiers, "-") + + return packageurl.NewPackageURL( + "platform", + platform.Name, + "", + platform.Version, + NewQualifiers(qualifiers), + "", + ).ToString(), nil +} diff --git a/providers/os/resources/purl/platform_purl_test.go b/providers/os/resources/purl/platform_purl_test.go new file mode 100644 index 0000000000..947b2ef912 --- /dev/null +++ b/providers/os/resources/purl/platform_purl_test.go @@ -0,0 +1,178 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package purl + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.mondoo.com/cnquery/v11/providers-sdk/v1/inventory" +) + +func TestNewPlatformPurl(t *testing.T) { + tests := []struct { + name string + platform *inventory.Platform + want string + wantErr string + }{ + { + name: "valid ubuntu platform", + platform: &inventory.Platform{ + Name: "ubuntu", + Version: "22.04", + }, + want: "pkg:platform/ubuntu/@22.04?distro=ubuntu-22.04", + wantErr: "", + }, + { + name: "valid windows platform", + platform: &inventory.Platform{ + Name: "windows", + Version: "19045", + }, + want: "pkg:platform/windows/@19045?distro=windows-19045", + wantErr: "", + }, + { + name: "platform with arch", + platform: &inventory.Platform{ + Name: "ubuntu", + Version: "22.04", + Arch: "amd64", + }, + want: "pkg:platform/ubuntu/@22.04?arch=amd64&distro=ubuntu-22.04", + wantErr: "", + }, + { + name: "platform with x86_64 arch", + platform: &inventory.Platform{ + Name: "ubuntu", + Version: "22.04", + Arch: "x86_64", + }, + want: "pkg:platform/ubuntu/@22.04?arch=x86_64&distro=ubuntu-22.04", + wantErr: "", + }, + { + name: "platform with arm64 arch", + platform: &inventory.Platform{ + Name: "ubuntu", + Version: "22.04", + Arch: "arm64", + }, + want: "pkg:platform/ubuntu/@22.04?arch=arm64&distro=ubuntu-22.04", + wantErr: "", + }, + { + name: "macplatform with apple silicon", + platform: &inventory.Platform{ + Name: "macplatform", + Version: "14.5.1", + Arch: "arm64", + }, + want: "pkg:platform/macplatform/@14.5.1?arch=arm64&distro=macplatform-14.5.1", + wantErr: "", + }, + { + name: "windows with x86 arch", + platform: &inventory.Platform{ + Name: "windows", + Version: "19045", + Arch: "x86", + }, + want: "pkg:platform/windows/@19045?arch=x86&distro=windows-19045", + wantErr: "", + }, + { + name: "vsphere platform", + platform: &inventory.Platform{ + Name: "vsphere", + Version: "7.0.3", + }, + want: "pkg:platform/vsphere/@7.0.3?distro=vsphere-7.0.3", + wantErr: "", + }, + { + name: "esxi platform", + platform: &inventory.Platform{ + Name: "esxi", + Version: "7.0.3", + Arch: "x86_64", + }, + want: "pkg:platform/esxi/@7.0.3?arch=x86_64&distro=esxi-7.0.3", + wantErr: "", + }, + { + name: "kubernetes deployment", + platform: &inventory.Platform{ + Name: "k8s-deployment", + Version: "1.27", + }, + want: "pkg:platform/k8s-deployment/@1.27?distro=k8s-deployment-1.27", + wantErr: "", + }, + { + name: "aws platform", + platform: &inventory.Platform{ + Name: "aws", + }, + want: "pkg:platform/aws/?distro=aws", + wantErr: "", + }, + { + name: "gcp platform", + platform: &inventory.Platform{ + Name: "gcp", + }, + want: "pkg:platform/gcp/?distro=gcp", + wantErr: "", + }, + { + name: "azure platform", + platform: &inventory.Platform{ + Name: "azure", + }, + want: "pkg:platform/azure/?distro=azure", + wantErr: "", + }, + { + name: "platform with build instead of version", + platform: &inventory.Platform{ + Name: "centplatform", + Build: "8.5", + }, + want: "pkg:platform/centplatform/?distro=centplatform-8.5", + wantErr: "", + }, + { + name: "nil platform", + platform: nil, + want: "", + wantErr: "platform is required", + }, + { + name: "debian bookworm platform", + platform: &inventory.Platform{ + Name: "debian", + Version: "12", + Title: "Debian GNU/Linux 12 (bookworm)", + }, + want: "pkg:platform/debian/@12?distro=debian-12", + wantErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewPlatformPurl(tt.platform) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +}