From ac91c0e39d6d9cbfa4a4ab476267aa845f901f5b Mon Sep 17 00:00:00 2001 From: Cristian Glavan Date: Tue, 10 Nov 2026 20:08:42 +0200 Subject: [PATCH 1/6] feat(sbomexport): export node SBOMs to Dependency-Track With the new sbomExport capability, the operator sends the SBOMs that node-agent stores (SBOMSyft, from nodeSbomGeneration) to Dependency-Track, one project per running image and namespace: : current collection, adds up its namespaces / : current collection, latest version of each image // : SBOMWatch now also runs when kubevuln is off and sbomExport is on, and passes its pod and SBOM events to the exporter. An image is exported when its SBOM becomes ready, when a pod starts with it, and on a resync (1h by default) that compares every running image with Dependency-Track. It is sent again only when its SBOMSyft is newer than the project's last BOM import, and converted to CycloneDX once even when it runs in several namespaces, with syft's own syft-json decoder and CycloneDX encoder, as `syft convert` does. syft is pinned to the kubescape/syft fork node-agent and kubevuln build against, with node-agent's containerd pins. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristian Glavan --- README.md | 24 +- config/config.go | 38 ++ config/config_test.go | 64 +++ docs/features/sbom-export.md | 98 ++++ go.mod | 233 +++++---- go.sum | 566 +++++++++++++++------- mainhandler/handlerequests.go | 29 +- mainhandler/sbomexport.go | 45 ++ mainhandler/sbomexport_test.go | 87 ++++ sbomexport/convert.go | 32 ++ sbomexport/convert_test.go | 38 ++ sbomexport/dtrack.go | 200 ++++++++ sbomexport/dtrack_test.go | 113 +++++ sbomexport/exporter.go | 412 ++++++++++++++++ sbomexport/exporter_test.go | 322 ++++++++++++ sbomexport/fakedtrack_test.go | 189 ++++++++ sbomexport/image.go | 57 +++ sbomexport/image_test.go | 74 +++ sbomexport/index.go | 181 +++++++ sbomexport/index_test.go | 84 ++++ sbomexport/store.go | 105 ++++ sbomexport/store_test.go | 48 ++ sbomexport/testdata/curl-8.11.1.cdx.json | 1 + sbomexport/testdata/curl-8.11.1.syft.json | 1 + watcher/sbomobserver_test.go | 157 ++++++ watcher/sbomwatcher.go | 14 + watcher/watchhandler.go | 23 + 27 files changed, 2954 insertions(+), 281 deletions(-) create mode 100644 docs/features/sbom-export.md create mode 100644 mainhandler/sbomexport.go create mode 100644 mainhandler/sbomexport_test.go create mode 100644 sbomexport/convert.go create mode 100644 sbomexport/convert_test.go create mode 100644 sbomexport/dtrack.go create mode 100644 sbomexport/dtrack_test.go create mode 100644 sbomexport/exporter.go create mode 100644 sbomexport/exporter_test.go create mode 100644 sbomexport/fakedtrack_test.go create mode 100644 sbomexport/image.go create mode 100644 sbomexport/image_test.go create mode 100644 sbomexport/index.go create mode 100644 sbomexport/index_test.go create mode 100644 sbomexport/store.go create mode 100644 sbomexport/store_test.go create mode 100644 sbomexport/testdata/curl-8.11.1.cdx.json create mode 100644 sbomexport/testdata/curl-8.11.1.syft.json create mode 100644 watcher/sbomobserver_test.go diff --git a/README.md b/README.md index 82a9d47c..6e78a9a1 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ The **Kubescape Operator** is the central orchestration engine for [Kubescape](h - [Features](#features) - [Continuous Scanning](#continuous-scanning) - [Admission Controller](#admission-controller) + - [SBOM Export to Dependency-Track](#sbom-export-to-dependency-track) - [Development](#development) - [Building from Source](#building-from-source) - [VS Code Configuration](#vs-code-configuration) @@ -239,7 +240,8 @@ The Operator reads configuration from `/etc/config/`. When running locally, set "nodeSbomGeneration": "disable", "seccomp": "disable", "otel": "enable", - "admissionController": "disable" + "admissionController": "disable", + "sbomExport": "disable" }, "components": { "operator": { "enabled": true }, @@ -321,6 +323,10 @@ The Operator reads configuration from `/etc/config/`. When running locally, set | `excludeControls` | `[]` | Control IDs or legacy CIS section numbers omitted from operator posture scans | | `excludeNamespacesRegex` | `""` | Comma-separated string or array of RE2 regex patterns for namespaces to exclude | | `namespaceFilterConfigMapName` | `""` | Optional ConfigMap name in the operator namespace for live namespace filtering | +| `sbomExport.resyncInterval` | `1h` | How often the [SBOM export](docs/features/sbom-export.md) compares every running image with Dependency-Track | +| `sbomExport.dependencyTrack.url` | `""` | Dependency-Track API server the SBOM export sends to | +| `sbomExport.dependencyTrack.apiKeyFile` | `/etc/sbom-export/api-key` | File holding the Dependency-Track API key, read on every request | +| `sbomExport.dependencyTrack.rootProject` | cluster name | Root of the SBOM export's project tree in Dependency-Track | ### Exclude controls from posture scans @@ -807,6 +813,22 @@ helm upgrade kubescape kubescape/kubescape-operator \ When enabled, the Operator runs an HTTPS webhook server on port `8443` that integrates with Kubernetes admission control. +### SBOM Export to Dependency-Track + +The Operator sends the SBOM of every running image, as node-agent built it, to [Dependency-Track](https://dependencytrack.org): one project per image and namespace, kept current as workloads change. It needs `nodeSbomGeneration`, but neither kubevuln nor registry access. + +**Enable via Helm**, with the API key in a Secret you create in the Kubescape namespace: +```bash +helm upgrade kubescape kubescape/kubescape-operator \ + -n kubescape \ + --set capabilities.nodeSbomGeneration=enable \ + --set capabilities.sbomExport=enable \ + --set sbomExport.dependencyTrack.url=https://dtrack.example.com \ + --set sbomExport.dependencyTrack.apiKeySecret.name=dependency-track +``` + +See [SBOM export to Dependency-Track](docs/features/sbom-export.md) for the project tree, the settings and the permissions the API key needs. + --- ## Development diff --git a/config/config.go b/config/config.go index ce11ceb0..c7062b96 100644 --- a/config/config.go +++ b/config/config.go @@ -34,6 +34,7 @@ type Capabilities struct { VulnerabilityScan string `json:"vulnerabilityScan"` AdmissionController string `json:"admissionController"` RiskAcceptance string `json:"riskAcceptance"` + SBOMExport string `json:"sbomExport"` } type Components struct { @@ -156,6 +157,27 @@ type Config struct { RulesUpdateConfig rulesupdate.RulesUpdaterConfig `mapstructure:"rulesUpdateConfig"` SkipProfilesWithoutInstances bool `mapstructure:"skipProfilesWithoutInstances"` NodeAgentAutoscaler NodeAgentAutoscalerConfig `mapstructure:"nodeAgentAutoscaler"` + SBOMExport SBOMExportConfig `mapstructure:"sbomExport"` +} + +// SBOMExportConfig configures the export of node SBOMs to Dependency-Track, +// turned on by the sbomExport capability. +type SBOMExportConfig struct { + // ResyncInterval is how often the exporter compares what runs in the + // cluster with what Dependency-Track holds. + ResyncInterval time.Duration `mapstructure:"resyncInterval"` + DependencyTrack DependencyTrackConfig `mapstructure:"dependencyTrack"` +} + +// DependencyTrackConfig is where the exporter sends the SBOMs. +type DependencyTrackConfig struct { + // URL is the base URL of the Dependency-Track API server. + URL string `mapstructure:"url"` + // APIKeyFile is the file the API key is read from, on every request, so a + // rotated Secret needs no restart. + APIKeyFile string `mapstructure:"apiKeyFile"` + // RootProject is the root of the project tree; empty means the cluster name. + RootProject string `mapstructure:"rootProject"` } // IConfig is an interface for all config types used in the operator @@ -170,6 +192,8 @@ type IConfig interface { ContinuousScanEnabled() bool NodeSbomGenerationEnabled() bool RiskAcceptanceEnabled() bool + SBOMExportEnabled() bool + SBOMExportConfig() SBOMExportConfig CleanUpRoutineInterval() time.Duration MatchingRulesFilename() string TriggerSecurityFramework() bool @@ -250,6 +274,16 @@ func (c *OperatorConfig) NodeSbomGenerationEnabled() bool { return c.components.Capabilities.NodeSbomGeneration == "enable" } +// SBOMExportEnabled reports whether the sbomExport capability is on. +func (c *OperatorConfig) SBOMExportEnabled() bool { + return c.components.Capabilities.SBOMExport == "enable" +} + +// SBOMExportConfig returns where and how often to export SBOMs. +func (c *OperatorConfig) SBOMExportConfig() SBOMExportConfig { + return c.serviceConfig.SBOMExport +} + // RiskAcceptanceEnabled reports whether the riskAcceptance capability is on. The // SecurityException watcher must gate on this because the operator's RBAC for // securityexceptions/clustersecurityexceptions is only granted by the Helm chart @@ -432,6 +466,10 @@ func LoadConfig(path string) (Config, error) { viper.SetDefault("nodeAgentAutoscaler.seLinuxType", "spc_t") viper.SetDefault("nodeAgentAutoscaler.bottlerocketAutoDetect", true) + // SBOM export defaults + viper.SetDefault("sbomExport.resyncInterval", time.Hour) + viper.SetDefault("sbomExport.dependencyTrack.apiKeyFile", "/etc/sbom-export/api-key") + viper.AutomaticEnv() err := viper.ReadInConfig() diff --git a/config/config_test.go b/config/config_test.go index ddd7ee4e..cd5d01bd 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -179,6 +179,12 @@ func TestLoadConfig(t *testing.T) { SELinuxType: "spc_t", BottlerocketAutoDetect: true, }, + SBOMExport: SBOMExportConfig{ + ResyncInterval: time.Hour, + DependencyTrack: DependencyTrackConfig{ + APIKeyFile: "/etc/sbom-export/api-key", + }, + }, }, }, } @@ -619,3 +625,61 @@ func TestNewOperatorConfig_RegexValidation(t *testing.T) { assert.False(t, opCfg.SkipNamespace("default")) }) } + +func TestSBOMExportEnabled(t *testing.T) { + tests := []struct { + value string + want bool + }{ + {"enable", true}, + {"disable", false}, + {"", false}, + } + for _, tt := range tests { + t.Run(tt.value, func(t *testing.T) { + cfg, err := NewOperatorConfig( + CapabilitiesConfig{Capabilities: Capabilities{SBOMExport: tt.value}}, + armometadata.ClusterConfig{}, + &utils.Credentials{}, + Config{}, + ) + require.NoError(t, err) + assert.Equal(t, tt.want, cfg.SBOMExportEnabled()) + }) + } +} + +func TestLoadConfigSBOMExportDefaults(t *testing.T) { + viper.Reset() + defer viper.Reset() + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{}`), 0600)) + cfg, err := LoadConfig(dir) + require.NoError(t, err) + assert.Equal(t, time.Hour, cfg.SBOMExport.ResyncInterval) + assert.Equal(t, "/etc/sbom-export/api-key", cfg.SBOMExport.DependencyTrack.APIKeyFile) + assert.Empty(t, cfg.SBOMExport.DependencyTrack.URL) + assert.Empty(t, cfg.SBOMExport.DependencyTrack.RootProject) +} + +func TestLoadConfigSBOMExport(t *testing.T) { + viper.Reset() + defer viper.Reset() + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{ + "sbomExport": { + "resyncInterval": "30m", + "dependencyTrack": {"url": "https://dtrack.example.com", "apiKeyFile": "/keys/dtrack", "rootProject": "prod"} + } + }`), 0600)) + cfg, err := LoadConfig(dir) + require.NoError(t, err) + assert.Equal(t, SBOMExportConfig{ + ResyncInterval: 30 * time.Minute, + DependencyTrack: DependencyTrackConfig{ + URL: "https://dtrack.example.com", + APIKeyFile: "/keys/dtrack", + RootProject: "prod", + }, + }, cfg.SBOMExport) +} diff --git a/docs/features/sbom-export.md b/docs/features/sbom-export.md new file mode 100644 index 00000000..5eb5c2d8 --- /dev/null +++ b/docs/features/sbom-export.md @@ -0,0 +1,98 @@ +# SBOM export to Dependency-Track + +## Overview + +With the `sbomExport` capability, the operator sends the SBOMs node-agent +stores (`SBOMSyft` objects, from `nodeSbomGeneration`) to +[Dependency-Track](https://dependencytrack.org), one project per running image +and namespace, and keeps them current as workloads change. It needs neither +kubevuln nor registry access: the SBOMs are the ones node-agent builds from the +image layers on each node. + +## How it works + +The exporter is fed by `SBOMWatch`, the operator's watch of running pods and +`SBOMSyft` objects, which also starts when kubevuln is off but `sbomExport` is +on. An image is exported when its SBOM becomes ready, when a pod starts with an +image whose SBOM is ready, and on every resync (`resyncInterval`, one hour by +default), which compares every running image with Dependency-Track. The resync +catches up after Dependency-Track was unreachable or the operator restarted. + +For each namespace the image runs in, the exporter looks up its project. If the +project's last BOM import is not older than the `SBOMSyft`, the image is only +marked as the project's latest version. Otherwise the exporter reads the +`SBOMSyft`, converts its syft document to CycloneDX JSON (1.6) with syft, the +way `syft convert` does, and uploads it. An image that runs in several +namespaces is converted once. + +An image stays counted as running in a namespace for one resync interval after +its last pod there ended, so the image of a short Job is still exported once +its SBOM is ready. Namespaces the operator skips (`excludeNamespaces`, +`includeNamespaces`, their regex forms) are not exported. + +## Projects in Dependency-Track + +```text + : current collection, adds up its namespaces +└── / : current collection, adds up the latest version of each image + └── // : +``` + +`` is `dependencyTrack.rootProject`, by default the cluster name. The two +collection levels use the fixed version `current`, because Dependency-Track 5 +refuses a project without a version. An image referenced only by digest gets +the first 12 characters of the digest as its version. Projects of images that +stopped running are left as they are. + +The tree needs Dependency-Track 4.13 or later (collection projects). + +## Configuration + +`capabilities.json`: + +```json +{ + "capabilities": { + "nodeSbomGeneration": "enable", + "sbomExport": "enable" + } +} +``` + +`config.json`: + +```json +{ + "sbomExport": { + "resyncInterval": "1h", + "dependencyTrack": { + "url": "https://dtrack.example.com", + "apiKeyFile": "/etc/sbom-export/api-key", + "rootProject": "" + } + } +} +``` + +| Setting | Default | Description | +|---------|---------|-------------| +| `sbomExport.resyncInterval` | `1h` | How often every running image is compared with Dependency-Track; also how long an image counts as running after its last pod ended | +| `sbomExport.dependencyTrack.url` | *required* | Base URL of the Dependency-Track API server | +| `sbomExport.dependencyTrack.apiKeyFile` | `/etc/sbom-export/api-key` | File holding the API key, read on every request, so a rotated Secret needs no restart | +| `sbomExport.dependencyTrack.rootProject` | cluster name | Root project of the tree | + +Without `nodeSbomGeneration`, or with an invalid configuration, the operator +logs why and runs without the export. + +The team that owns the API key needs `BOM_UPLOAD`, `PROJECT_CREATION_UPLOAD`, +`VIEW_PORTFOLIO` and `PORTFOLIO_MANAGEMENT`; on Dependency-Track 5, +`PORTFOLIO_MANAGEMENT_CREATE` and `PORTFOLIO_MANAGEMENT_UPDATE` replace the +last one. They create the collection projects and set their collection logic. + +## Logs + +Every image sent logs `sent SBOM to Dependency-Track` with its project and +version. Each resync logs what happened since the previous one: sent, +unchanged, skipped because no ready SBOM exists yet, and failed. A failed +export is retried with backoff, up to five times, and then at the next resync. +A refused API key is logged once per resync, with the permissions above. diff --git a/go.mod b/go.mod index f31759b8..552bcb1c 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,20 @@ module github.com/kubescape/operator go 1.26.9 require ( + github.com/anchore/syft v1.55.0 github.com/armosec/armoapi-go v0.0.773 github.com/armosec/registryx v0.0.36 github.com/armosec/utils-go v0.0.58 github.com/armosec/utils-k8s-go v0.0.35 - github.com/aws/aws-sdk-go-v2 v1.41.2 - github.com/aws/aws-sdk-go-v2/config v1.32.10 - github.com/aws/aws-sdk-go-v2/credentials v1.19.10 + github.com/aws/aws-sdk-go-v2 v1.47.1 + github.com/aws/aws-sdk-go-v2/config v1.33.6 + github.com/aws/aws-sdk-go-v2/credentials v1.20.6 github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff/v4 v4.3.0 github.com/deckarep/golang-set/v2 v2.8.0 github.com/distribution/reference v0.6.0 - github.com/fsnotify/fsnotify v1.9.0 + github.com/fsnotify/fsnotify v1.10.1 github.com/go-logr/stdr v1.2.2 github.com/go-openapi/runtime v0.33.2 github.com/google/cel-go v0.29.0 @@ -41,7 +42,7 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.47.0 go.opentelemetry.io/otel/trace v1.47.0 go.opentelemetry.io/proto/otlp v1.11.1 - golang.org/x/sync v0.23.0 + golang.org/x/sync v0.24.0 google.golang.org/grpc v1.84.0 google.golang.org/protobuf v1.36.12 istio.io/pkg v0.0.0-20231221211216-7635388a563e @@ -54,13 +55,13 @@ require ( ) require ( - cloud.google.com/go/auth v0.20.0 // indirect + cloud.google.com/go/auth v0.23.3 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/container v1.46.0 // indirect + cloud.google.com/go/compute/metadata v0.9.1 // indirect + cloud.google.com/go/container v1.51.0 // indirect github.com/IBM/sarama v1.42.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.6.0 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/eapache/go-resiliency v1.4.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect @@ -93,10 +94,12 @@ require ( require ( cel.dev/expr v0.25.2 // indirect cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/artifactregistry v1.20.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/longrunning v0.8.0 // indirect - cyphar.com/go-pathrs v0.2.1 // indirect + cloud.google.com/go/artifactregistry v1.26.0 // indirect + cloud.google.com/go/iam v1.12.0 // indirect + cloud.google.com/go/longrunning v1.2.0 // indirect + cloud.google.com/go/monitoring v1.30.0 // indirect + cloud.google.com/go/storage v1.68.0 // indirect + cyphar.com/go-pathrs v0.2.5 // indirect dario.cat/mergo v1.0.2 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20250520111509-a70c2aa677fa // indirect @@ -109,80 +112,102 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerservice/armcontainerservice/v2 v2.4.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/Microsoft/go-winio v0.6.2 // indirect - github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect + github.com/CycloneDX/cyclonedx-go v0.12.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 // indirect + github.com/Microsoft/hcsshim v0.15.0-rc.4 // indirect github.com/NYTimes/gziphandler v1.1.1 // indirect + github.com/STARRY-S/zip v0.2.3 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect + github.com/adrg/xdg v0.5.3 // indirect github.com/agnivade/levenshtein v1.2.1 // indirect - github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 // indirect - github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 // indirect - github.com/anchore/stereoscope v0.1.22 // indirect - github.com/anchore/syft v1.42.3 // indirect + github.com/anchore/clio v0.1.1 // indirect + github.com/anchore/fangs v0.1.1 // indirect + github.com/anchore/go-homedir v0.1.1 // indirect + github.com/anchore/go-logger v0.2.0 // indirect + github.com/anchore/go-sync v0.1.2 // indirect + github.com/anchore/packageurl-go v0.2.0 // indirect + github.com/anchore/stereoscope v0.3.4 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/armosec/gojay v1.2.17 // indirect github.com/aws/aws-sdk-go v1.55.7 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4 // indirect github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 // indirect github.com/aws/aws-sdk-go-v2/service/eks v1.48.5 // indirect - github.com/aws/aws-sdk-go-v2/service/iam v1.35.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect - github.com/aws/smithy-go v1.24.1 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.56.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.10.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.38.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.51.1 // indirect + github.com/aws/smithy-go v1.28.1 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect - github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.2 // indirect + github.com/bodgit/plumbing v1.3.0 // indirect + github.com/bodgit/sevenzip v1.6.1 // indirect + github.com/bodgit/windows v1.0.1 // indirect github.com/briandowns/spinner v1.23.2 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cilium/cilium v1.17.17 // indirect github.com/cilium/ebpf v0.22.0 // indirect - github.com/containerd/cgroups/v3 v3.1.2 // 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 v1.7.36 // indirect - github.com/containerd/containerd/api v1.10.0 // indirect - github.com/containerd/continuity v0.4.5 // indirect + github.com/containerd/containerd/api v1.12.0 // 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 github.com/containerd/fifo v1.1.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v1.0.0-rc.2 // indirect - github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect - github.com/containerd/ttrpc v1.2.7 // indirect - github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/containerd/log v0.2.0 // indirect + github.com/containerd/platforms v1.0.0-rc.5 // indirect + github.com/containerd/ttrpc v1.2.9 // indirect + github.com/containerd/typeurl/v2 v2.3.0 // indirect github.com/containers/common v0.64.2 // indirect github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/cyphar/filepath-securejoin v0.6.0 // indirect + github.com/cyphar/filepath-securejoin v0.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/docker/cli v29.3.0+incompatible // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect + github.com/diskfs/go-diskfs v1.9.4 // indirect + github.com/docker/cli v29.8.2+incompatible // indirect github.com/docker/docker v28.5.2+incompatible // indirect - github.com/docker/docker-credential-helpers v0.9.5 // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/docker-credential-helpers v0.9.8 // indirect + github.com/docker/go-connections v0.8.1 // indirect github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/ebitengine/purego v0.8.4 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/facebookincubator/nvdtools v0.1.5 // indirect github.com/fatih/color v1.19.0 // indirect + github.com/felixge/fgprof v0.9.5 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.13 // indirect - github.com/github/go-spdx/v2 v2.4.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.15 // indirect + github.com/github/go-spdx/v2 v2.7.1 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gota/gota v0.12.0 // indirect - github.com/go-ini/ini v1.67.0 // indirect - github.com/go-jose/go-jose/v4 v4.1.4 // indirect + github.com/go-jose/go-jose/v4 v4.1.5 // indirect github.com/go-logr/logr v1.4.4 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/analysis v1.0.0 // indirect @@ -208,43 +233,61 @@ require ( github.com/go-openapi/validate v1.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/gohugoio/hashstructure v0.6.0 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/go-containerregistry v0.21.2 // indirect + github.com/google/go-containerregistry v0.22.1 // indirect github.com/google/licensecheck v0.3.1 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect - github.com/googleapis/gax-go/v2 v2.22.0 // indirect - github.com/gorilla/mux v1.8.1 // indirect + github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 // indirect + github.com/google/s2a-go v0.1.10 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.22 // indirect + github.com/googleapis/gax-go/v2 v2.24.1 // indirect + github.com/gookit/color v1.6.2 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect + github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-getter v1.8.10 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-version v1.8.0 // indirect + github.com/hashicorp/go-version v1.9.0 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inspektor-gadget/inspektor-gadget v0.45.1-0.20251020222545-c91c23581ebf // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.18.7 // indirect + github.com/klauspost/compress v1.20.1 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect github.com/kubescape/kubescape/v3 v3.0.4 // indirect github.com/kubescape/rbac-utils v0.0.21-0.20230806101615-07e36f555520 // indirect github.com/kubescape/regolibrary v1.0.317-0.20240320124840-1d84ac7186ea // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.2.1 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.5 // indirect + github.com/lestrrat-go/jwx/v3 v3.1.1 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mackerelio/go-osstat v0.2.5 // indirect - github.com/magiconair/properties v1.8.10 // indirect + github.com/magiconair/properties v1.18.12 // indirect github.com/mattn/go-colorable v0.1.16 // indirect github.com/mattn/go-isatty v0.0.24 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mholt/archives v0.1.5 // indirect + github.com/mikelolasagasti/xz v1.0.1 // indirect + github.com/minio/minlz v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -255,46 +298,53 @@ require ( github.com/moby/sys/sequential v0.7.0 // indirect github.com/moby/sys/signal v0.7.1 // indirect github.com/moby/sys/user v0.4.1 // indirect - github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/sys/userns v0.2.1 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nwaples/rardecode/v2 v2.4.1 // indirect github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/olvrng/ujson v1.1.0 // indirect - github.com/open-policy-agent/opa v1.4.0 // indirect + github.com/open-policy-agent/opa v1.19.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opencontainers/runtime-spec v1.3.0 // indirect - github.com/opencontainers/selinux v1.13.1 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/opencontainers/selinux v1.15.1 // indirect + github.com/pborman/indent v1.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/petermattis/goid v0.0.0-20241211131331-93ee7e083c43 // indirect github.com/picatz/xcel v0.0.0-20250816143731-885b5f678a12 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_golang v1.24.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.4 // indirect - github.com/prometheus/procfs v0.19.2 // indirect - github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/prometheus/common v0.70.0 // indirect + github.com/prometheus/procfs v0.21.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20240229202106-bca3154da60a // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sasha-s/go-deadlock v0.3.5 // indirect github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect - github.com/seccomp/libseccomp-golang v0.11.0 // indirect + github.com/seccomp/libseccomp-golang v0.11.1 // indirect + github.com/segmentio/asm v1.2.1 // indirect github.com/shirou/gopsutil/v4 v4.25.6 // indirect - github.com/sirupsen/logrus v1.9.4 // indirect + github.com/sirupsen/logrus v1.10.2 // indirect + github.com/sorairolake/lzip-go v0.3.8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/spiffe/go-spiffe/v2 v2.8.1 // indirect github.com/stripe/stripe-go/v74 v74.30.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/sylabs/squashfs v1.0.6 // indirect @@ -303,11 +353,12 @@ require ( github.com/therootcompany/xz v1.0.1 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.11.0 // indirect - github.com/ulikunitz/xz v0.5.15 // indirect + github.com/ulikunitz/xz v0.5.17 // indirect github.com/uptrace/opentelemetry-go-extra/otelutil v0.4.0 // indirect github.com/uptrace/opentelemetry-go-extra/otelzap v0.4.0 // indirect github.com/uptrace/uptrace-go v1.47.0 // indirect - github.com/vbatts/tar-split v0.12.2 // indirect + github.com/valyala/fastjson v1.6.10 // indirect + github.com/vektah/gqlparser/v2 v2.5.36 // indirect github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netns v0.0.5 // indirect github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect @@ -316,13 +367,14 @@ require ( github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xlab/treeprint v1.2.0 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yl2chen/cidranger v1.0.2 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.mongodb.org/mongo-driver v1.17.7 // indirect - go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/bridges/otelslog v0.21.0 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.44.0 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 // indirect go.opentelemetry.io/contrib/instrumentation/runtime v0.72.0 // indirect @@ -338,21 +390,22 @@ require ( go.opentelemetry.io/otel/sdk/log v1.47.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect + go4.org v0.0.0-20230225012048-214862532bf5 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.57.0 // indirect + golang.org/x/crypto v0.58.0 // indirect golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect golang.org/x/mod v0.41.0 // indirect - golang.org/x/net v0.59.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/term v0.46.0 // indirect - golang.org/x/text v0.42.0 // indirect - golang.org/x/time v0.15.0 // indirect + golang.org/x/net v0.61.0 // indirect + golang.org/x/oauth2 v0.37.0 // indirect + golang.org/x/sys v0.49.0 // indirect + golang.org/x/term v0.47.0 // indirect + golang.org/x/text v0.43.0 // indirect + golang.org/x/time v0.16.0 // indirect gonum.org/v1/gonum v0.17.0 // indirect - google.golang.org/api v0.278.0 // indirect - google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/api v0.299.0 // indirect + google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect @@ -388,3 +441,15 @@ replace github.com/opencontainers/runtime-spec => github.com/opencontainers/runt // Temporary concurrency fix: https://github.com/goradd/maps/pull/22 // Remove this replacement when an upstream release includes the fix. replace github.com/goradd/maps => github.com/matthyx/maps v0.0.0-20260915111345-e9181ad40421 + +// The SBOM export decodes the documents node-agent's syft writes, so it builds +// against the same syft as node-agent and kubevuln. +replace github.com/anchore/syft => github.com/kubescape/syft v1.55.0-ks.3 + +// syft v1.55.0 declares containerd/v2 v2.4.1, whose containerd/api v1.12.0 no +// longer compiles with the containerd v1.7.36 that inspektor-gadget imports. +// syft compiles against v2.2.0. Same pins as node-agent's go.mod, which explains +// them in full. +replace github.com/containerd/containerd/v2 => github.com/containerd/containerd/v2 v2.2.0 + +replace github.com/containerd/containerd/api => github.com/containerd/containerd/api v1.10.0 diff --git a/go.sum b/go.sum index 306100eb..c3c5049a 100644 --- a/go.sum +++ b/go.sum @@ -33,10 +33,10 @@ cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0c cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/artifactregistry v1.20.0 h1:j/XQiQfaeTyQeNj3HNk4iDFREVnY/fxkHIjsxpaDs8A= -cloud.google.com/go/artifactregistry v1.20.0/go.mod h1:0G9wdbGyDFkvrYH+2AlQs9MuTJdbY8Vg45M8VjlI8rc= -cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= -cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= +cloud.google.com/go/artifactregistry v1.26.0 h1:iq5kkdY2FJY8RkkNghUelb3EOcqyy7dVhfDGVk/Xw9g= +cloud.google.com/go/artifactregistry v1.26.0/go.mod h1:c5FPi5GtDBP+OAr5kKhCBNQDT9ZgAyobXQjekx93VWs= +cloud.google.com/go/auth v0.23.3 h1:UMK+oBtuNGMCR/6i6mmySUItqjOazpJrbmZyhGbGBWo= +cloud.google.com/go/auth v0.23.3/go.mod h1:fClbry28fo7XkxhSeT6AQtAVAp6Jy0fW9N99PoPNPFM= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= @@ -45,17 +45,21 @@ cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvf cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/container v1.46.0 h1:xX94Lo3xrS5OkdMWKvpEVAbBwjN9uleVv6vOi02fL4s= -cloud.google.com/go/container v1.46.0/go.mod h1:A7gMqdQduTk46+zssWDTKbGS2z46UsJNXfKqvMI1ZO4= +cloud.google.com/go/compute/metadata v0.9.1 h1:CTE1OWBQ0vnF5uHwdFAQJvMQ0Fi/KRcqqKTo9V0F8Ik= +cloud.google.com/go/compute/metadata v0.9.1/go.mod h1:NtnlvB6X3t4R6xSWyVX/ZWk493PCxGQlhI/iqxh4M8I= +cloud.google.com/go/container v1.51.0 h1:KdeWHqwlOvABdhkhSTSb4QFySMOuXnc3UUoQMmq9lic= +cloud.google.com/go/container v1.51.0/go.mod h1:EvqoT2eXfxLweXXUlhAMGR0sOAB00XPzEjoL01esSDs= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= +cloud.google.com/go/iam v1.12.0 h1:Aki3bX9aHUDKPHfnRJfDcTdVedvy6quGBQcTqx3DRXk= +cloud.google.com/go/iam v1.12.0/go.mod h1:FEZ4lXpADAC2AIpQY7LANNjjwyQ2jK439CI2VaD+sLY= +cloud.google.com/go/logging v1.19.0 h1:NCqhdVUg3wQ8Cobdf16FDSuTGi3+6+hdSBHrY5TsR6Q= +cloud.google.com/go/logging v1.19.0/go.mod h1:i40NZCHC9Gqvod4yE+yQfDWwlgwW/SrshkkGibCHxcA= +cloud.google.com/go/longrunning v1.2.0 h1:WjYH3YHBGCxGJP9M4dWGHBfXr/cFIjMkNgWcJj7/iMM= +cloud.google.com/go/longrunning v1.2.0/go.mod h1:5KMQALFGOCtFoi2xSOA1u3H7WKlhmckgiyFw7+LGQp0= +cloud.google.com/go/monitoring v1.30.0 h1:r/d+JUbyKmJ8b07iznuKfzVzrIXTWxHQ3lBRm3x2LlY= +cloud.google.com/go/monitoring v1.30.0/go.mod h1:htlUR0QWVMrjFzZmN4LGnMAve9xB/eduwjmINxVZ8RM= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -65,8 +69,12 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= -cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +cloud.google.com/go/storage v1.68.0 h1:gqrAMJ51OZjYgU6AJ2U60um90YQhSjq8HEIQNtJ4C/8= +cloud.google.com/go/storage v1.68.0/go.mod h1:UsS9OgFg/XHOSYakQ8ZtLWWeyGkk1WnmD/GsGfN0BHM= +cloud.google.com/go/trace v1.16.0 h1:GmQovzFc5F0CNfl0VLgL64aoTtu7xsM0YajW2GlG9+E= +cloud.google.com/go/trace v1.16.0/go.mod h1:r+bdAn16dKLSV1G2D5v3e58IlQlizfxWrUfjx7kM7X0= +cyphar.com/go-pathrs v0.2.5 h1:SnX9FBvnoyn3lUs1dkMgZ52bAETpirNu3FTRh5HlRik= +cyphar.com/go-pathrs v0.2.5/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= @@ -107,19 +115,39 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CycloneDX/cyclonedx-go v0.12.0 h1:/7Jum36UA6V043tQZ/fE3jf+Nf9gn/qxUFfd7QReMy8= +github.com/CycloneDX/cyclonedx-go v0.12.0/go.mod h1:V2577HhxDDCDLYfkm55WJrz16nHTfyQZwcWUBSG7Z28= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0 h1:yzIYdwuro811Z27D3T80Wkd3rqZzb0K43nner7Eh1yE= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.34.0/go.mod h1:pJTkW8hEUIIi3Pf65lPZOnn4Y81yCllX6IWk2jNXdkM= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0 h1:jLdiS1vO+XJFyDSWRHBx56r4s/NNtcl5J6KyCcWUX/w= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.57.0/go.mod h1:8lmpHY+1VRoteiOwyrQMDt1YGXOrFKCz+1wJW7n3ODY= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0 h1:cSjUzZ7KU8hicTgzaSv9NmSyM9fTVK3y5lsBUl3wOis= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.57.0/go.mod h1:dzcEjy1WJ0Q4u9twNR3LcLhNoYMRCrMCMafpxa0TjPQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0 h1:RoO5+d7uCmDqovLrHCr2/BuViUXvdcrNxyNM1pN9dDQ= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.57.0/go.mod h1:YqwkQPrWSC7+byyc1VlKbWLBF5JsW5IoL6xUkemYSXk= github.com/IBM/sarama v1.42.1 h1:wugyWa15TDEHh2kvq2gAy1IHLjEjuYOYgXz/ruC/OSQ= github.com/IBM/sarama v1.42.1/go.mod h1:Xxho9HkHd4K/MDUo/T/sOqwtX/17D33++E9Wib6hUdQ= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= -github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ= -github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29 h1:0kQAzHq8vLs7Pptv+7TxjdETLf/nIqJpIB4oC6Ba4vY= +github.com/Microsoft/go-winio v0.6.3-0.20251027160822-ad3df93bed29/go.mod h1:ZWa7ssZJT30CCDGJ7fk/2SBTq9BIQrrVjrcss0UW2s0= +github.com/Microsoft/hcsshim v0.15.0-rc.4 h1:aZFX4LH0S20Lgjq0wG61StIClj7im4yzrxIClkaR8Z8= +github.com/Microsoft/hcsshim v0.15.0-rc.4/go.mod h1:BA9CBztgu4h/6Jsvo1O1M4qjWw09PoYpaEYgexPE578= github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= +github.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4= +github.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= +github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78= @@ -131,21 +159,27 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084 h1:7DUAXEdAxoANPlDgxYiaSRKnWnTygvdrrWhnmvEjNLg= -github.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084/go.mod h1:42dWox8z4//b898OIELsQnSdYq9q1aCXkwp5fKF+BEU= -github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 h1:aVC6r9h5wGNh8BYTW3CXxOdPoZzY/bBRWne1NvSTlO8= -github.com/anchore/fangs v0.0.0-20250716230140-94c22408c232/go.mod h1:Zees1AEKNpXIRgdVAMYWITncarLFiPOtEQ7rl45V/h0= -github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc= -github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50= -github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM= -github.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw= -github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY= -github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI= -github.com/anchore/stereoscope v0.1.22 h1:L807G/kk0WZzOCGuRGF7knxMKzwW2PGdbPVRystryd8= -github.com/anchore/stereoscope v0.1.22/go.mod h1:FikPtAb/WnbqwgLHAvQA9O+fWez0K4pbjxzghz++iy4= -github.com/anchore/syft v1.42.3 h1:eIeeGyqfXm/C8wpBWU50xFbOjdL37VbLatMj9nEJ6n4= -github.com/anchore/syft v1.42.3/go.mod h1:i2PZ+276IdPcnd/n32aeIv849iO/QqdjRknbIc39yL0= +github.com/anchore/clio v0.1.1 h1:9bkYnaFbpT0J3G0qSHyQqa/lpK5zV0C9Gtgh82H/jj0= +github.com/anchore/clio v0.1.1/go.mod h1:CFPIopJlakJbBpKtAL/EPpj4W9hgBO7PfFc8N2FAtlE= +github.com/anchore/fangs v0.1.1 h1:NBTHocmWM2/0gNhebpoE/wEB0Z0nxh2lESww2o/Uf5M= +github.com/anchore/fangs v0.1.1/go.mod h1:82oZuk4+RynOvAoxnXKmI1zO5xk/kbOw+5t4fAAPSsk= +github.com/anchore/go-collections v0.1.1 h1:kGlmSehs+ctTBZr+98F2+taHeolHy+XDx1Nczl8GZc8= +github.com/anchore/go-collections v0.1.1/go.mod h1:zgDbPSNDpfU47tVo7aW+2suG7+vXB1Sq1qtDhRJknQ0= +github.com/anchore/go-homedir v0.1.1 h1:KpaO5BPrVT7cVMcEZ9KfFfEi0iuo3DtagPGl0C744X0= +github.com/anchore/go-homedir v0.1.1/go.mod h1:9B0DGhbmMAJVGGEbrlb+PkORM5eDFCEOtZ3xQ22qaLA= +github.com/anchore/go-logger v0.2.0 h1:V8Gazz+F8xwWEWz+whqt8/5163IkO4XrAie+1qbK10k= +github.com/anchore/go-logger v0.2.0/go.mod h1:omqLWBwT/B5/aTwIH6pl5WBylBzeH3BGHUTS6OFeFvY= +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-sync v0.1.2 h1:RDD5RCanrWqhbtBulqPSZpERadUlgcYjpybYzYd3Uk8= +github.com/anchore/go-sync v0.1.2/go.mod h1:grjZH059bCHM6f6jh+Y7VYvX/q8YcjkDJqG6nplHGkE= +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.3.4 h1:dT5AfjCTXUJowMiRIc+VBQVwvwnIEwnxxwA1GwAkMxw= +github.com/anchore/stereoscope v0.3.4/go.mod h1:+HxqHTt7dvH8mea0mf3ulqkf084VvHiT1grpAQf5uEs= 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= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -169,48 +203,58 @@ github.com/armosec/utils-k8s-go v0.0.35 h1:CliNObhAca5UYl84m5OQecOTm9ZfMFI8648pY github.com/armosec/utils-k8s-go v0.0.35/go.mod h1:iHwR/KhMFtdd8Px1oYexLZYOHqmdknfGTZ8b7sZS0Ms= github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= -github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= -github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= -github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= -github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2 v1.47.1 h1:uOIZnp4PK3ZhKI0dNrJrhTEsLxbpXHTAJlwoS1pvAtw= +github.com/aws/aws-sdk-go-v2 v1.47.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8= +github.com/aws/aws-sdk-go-v2/config v1.33.6 h1:MBjkSTLczek/UgiK+EYPIoRTqE7gP8vtW3OFbFo7Nug= +github.com/aws/aws-sdk-go-v2/config v1.33.6/go.mod h1:grRAFzdAZJrwcbasJRg2MPvIrVjtlfXllHssN6+E1JE= +github.com/aws/aws-sdk-go-v2/credentials v1.20.6 h1:NpAFXCU7NzXNkdGK3zQTtsRJ+3v9tZQV0xcdRw8uBdw= +github.com/aws/aws-sdk-go-v2/credentials v1.20.6/go.mod h1:mcZCoiPnyMvP8VMNbygNX5lLqSlkYJIMPODylQMurOk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1 h1:8gALAAmacnIXh+z6VkdDanv4/IkG5APdg4DZLDTmLog= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.20.1/go.mod h1:Z7IJhJU+poOdJjUR2wpyY21ossQ1XS/R3Lk9Msq5kM4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4 h1:CLq4+8UHCI+ZZYl/EuJxXovaIVN2xeeT8JV+dsApQ5E= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.4/go.mod h1:Wv4q5sAM04xAMkoOedxLx2inVf6K5FdxYp+A61L+q/0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4 h1:dD4MR81I7YkpEBRk6UP9rocC2QnT3qVuXwzlYTtfGEs= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4/go.mod h1:EcXV1kAFd5XwSkDHlj94gnF3q5CkJyYiIJfH8N0VmrE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4 h1:7Wo47d/xn/7KttCSBd8EGYeZ7ULRFRkUHr6vkZPBzVQ= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4/go.mod h1:tDB2IVC1xC3vX8o+6uRlzhTxP3g1b77CZXFX/oD2FnQ= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3 h1:RtGctYMmkTerGClvdY6bHXdtly4FeYw9wz/NPz62LF8= github.com/aws/aws-sdk-go-v2/service/ecr v1.55.3/go.mod h1:vBfBu24Ka3/5UZtepbTV0gnc9VPLT8ok+0oDDaYAzn4= github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0 h1:MzP/ElwTpINq+hS80ZQz4epKVnUTlz8Sz+P/AFORCKM= github.com/aws/aws-sdk-go-v2/service/ecs v1.71.0/go.mod h1:pMlGFDpHoLTJOIZHGdJOAWmi+xeIlQXuFTuQxs1epYE= github.com/aws/aws-sdk-go-v2/service/eks v1.48.5 h1:vMwwdzKoUBt7vMHNkF16Hh7+8ndVGOAAEgqcrbz17M4= github.com/aws/aws-sdk-go-v2/service/eks v1.48.5/go.mod h1:9dn8p15siUL80NCTPVNd+YvEpVTmWO+rboGx6qOMBa0= -github.com/aws/aws-sdk-go-v2/service/iam v1.35.3 h1:bWFkGGea2UoD/m229uuRfT0mu+6pKNB0Kq4U6j/Qz3U= -github.com/aws/aws-sdk-go-v2/service/iam v1.35.3/go.mod h1:PpmEOH3ZTQlDAezieBVdFMjPO1jovUMNPA4OpCtnwbY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= -github.com/aws/smithy-go v1.24.1 h1:VbyeNfmYkWoxMVpGUAbQumkODcYmfMRfZ8yQiH30SK0= -github.com/aws/smithy-go v1.24.1/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go-v2/service/iam v1.56.1 h1:SLGzTAo/8J81zHUisaiKY6gF804fPsHZBxAZr0uquHM= +github.com/aws/aws-sdk-go-v2/service/iam v1.56.1/go.mod h1:Cc31Ak8Ps/0Vi4QqN3qIuqSHXi/MGLw/Cl6vzUwuqwk= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 h1:bAdDl/HkGCcGPoe25ToSHEw23VIxt6CT5fLcg111BKg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19/go.mod h1:KaUzbLxv4CeSxh6ZCl9B4m7CuFenS8kUEaDs+f/DQr4= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5 h1:/TYsZXdA8UTa+WCtCYSAJIr1vwl0+eho6TUgJGwFFO8= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.11.5/go.mod h1:qPqp1Uwd/BqdhPufv6oem9j5J7HNsgc2V22dUiDPn+s= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4 h1:29SvnfGhXjTl8ONxFwbj2rs6lbhiFXD2CgFQmbT/bXY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.14.4/go.mod h1:wm04I5DMuNVvZHFe/dHnUxincvNbbK7AiNBbYsQivek= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4 h1:pPiWfgeNxqluKEph7hvU88kuGKBPOWzO+Dk9t2zqqNs= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.20.4/go.mod h1:YlwGoIUDG/3kBQbdNOVs/xKZ9J01G8e/6D1mRBj9uTk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0 h1:VMAdYqr4Jn/8ATs9BHC5riwrs0d6m1Z2ohFriSwZwm0= +github.com/aws/aws-sdk-go-v2/service/s3 v1.114.0/go.mod h1:9APRWGLFITKD+xzWSIyT9V7QV4bNlEuIieWlzXgGFlI= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.1 h1:DzCCWLzcIRQ77F3DEUljud7bEjTgFOIKXP52NmVRyhU= +github.com/aws/aws-sdk-go-v2/service/signin v1.10.1/go.mod h1:xpo/geVldu8payT375WekctUzopG/hBU7miiqItMUlw= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.1 h1:Umtl/0YZhng4xndfW3lKJrYYP7NLEjI6bGXVomwLcs0= +github.com/aws/aws-sdk-go-v2/service/sso v1.38.1/go.mod h1:rRD/dnm7q0HYE/I5TMaPgkWyyUGLcwuxHLABsLnQ3e0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1 h1:orIWdNiLgzrhu/11RcPPKO/SBzUUymbUQuZbSPImghg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.43.1/go.mod h1:skwM/xsbR/1ReUTesv9BhpJp1VjajR7DWQnuVLwiXsQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.1 h1:0HOqZXRvMytH6bFHVIc0oJX07sZjfhz0zXtjs6gdE8s= +github.com/aws/aws-sdk-go-v2/service/sts v1.51.1/go.mod h1:26zA0GhDrLo+yiLI2yXWxqB1PdsShfLikoI7GOEgugM= +github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= +github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= @@ -218,17 +262,23 @@ github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdn github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= -github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.2 h1:eF7W7HWKg3z9NrWV9pTLnNeoXaqq3Tq9DNKXVMfoCnw= +github.com/bmatcuk/doublestar/v4 v4.10.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU= +github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs= +github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4= +github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= +github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= +github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= -github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -242,9 +292,15 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= +github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cilium/cilium v1.17.17 h1:lm2loJwLu9jDjC5Izk71Y5Zkr//rY7UXqvQjNT2KwFE= github.com/cilium/cilium v1.17.17/go.mod h1:h1VXfDHk2lnb02j9QwsTugs2dQw+7eBx03ELeUg7gJE= github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= @@ -252,6 +308,10 @@ github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10 github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -264,32 +324,40 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 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.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= -github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= +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 v1.7.36 h1:HyMsOG5kmp1LQsGzqwI6Ts06T4VpYZbszfDZYB0bW5w= github.com/containerd/containerd v1.7.36/go.mod h1:ozI//0TomTCLPhQREnx0IXDIQMg+Fk7yTtg9fNvU8EQ= github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= -github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= -github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/containerd/v2 v2.2.0 h1:K7TqcXy+LnFmZaui2DgHsnp2gAHhVNWYaHlx7HXfys8= +github.com/containerd/containerd/v2 v2.2.0/go.mod h1:YCMjKjA4ZA7egdHNi3/93bJR1+2oniYlnS+c0N62HdE= +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= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= -github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/log v0.2.0 h1:BewD/umNgVnoczglOpX8eRMyEy5t5iPlu5AIpnWDONc= +github.com/containerd/log v0.2.0/go.mod h1:/M7L7CXKcPTfNC74XzaK+5H5KbO5+4lJVpuVI6vRLoM= +github.com/containerd/platforms v1.0.0-rc.5 h1:vXd569rDrz8LeMXzAnBsy6LADV5YtsD8oyaRarxdmSU= +github.com/containerd/platforms v1.0.0-rc.5/go.mod h1:lKlMXyLybmBedS/JJm11uDofzI8L2v0J2ZbYvNsbq1A= +github.com/containerd/plugin v1.1.0 h1:O+7lczNJVMy8rz0YNx3xGB8tTf5qY4i5abF041Ew19U= +github.com/containerd/plugin v1.1.0/go.mod h1:qBTum+A8lJ6lO44A19Eo7y1OlcLj4OWFH1DA/vnHmcc= github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= -github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= -github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= -github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= -github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/containerd/ttrpc v1.2.9 h1:ha0ak962T0s3CA/RoZ6S6xiWZQF24GrBaEpiGX1uihg= +github.com/containerd/ttrpc v1.2.9/go.mod h1:jjtQRwXm4DL3KsHKW8vDiUOV6wO0hi6IPhmJhxU7aEs= +github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= +github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/containers/common v0.64.2 h1:1xepE7QwQggUXxmyQ1Dbh6Cn0yd7ktk14sN3McSWf5I= github.com/containers/common v0.64.2/go.mod h1:o29GfYy4tefUuShm8mOn2AiL5Mpzdio+viHI7n24KJ4= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 h1:Qzk5C6cYglewc+UyGf6lc8Mj2UaPTHy/iF2De0/77CA= +github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY= +github.com/containers/ocicrypt v1.3.2 h1:MuqHSfiPpGzoAQdgDSX85FgixSgIm9mSDXTLTgugY5E= +github.com/containers/ocicrypt v1.3.2/go.mod h1:ntBZabYG0rlvstB/1rK/ba2laaU5EHE0pD5ioHoPTq4= github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -297,24 +365,28 @@ github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= -github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= -github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= +github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= -github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgraph-io/badger/v4 v4.9.4 h1:bcw+waCpzRZ2nmcSPbnPvDVhiEsn98TKmvnAhK7r7LM= +github.com/dgraph-io/badger/v4 v4.9.4/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= @@ -322,25 +394,28 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/diskfs/go-diskfs v1.9.4 h1:0j2d7eG4IjyxL6+ChWbDPocdBCF6HQ4HBWU2WDYWVnc= +github.com/diskfs/go-diskfs v1.9.4/go.mod h1:TePJORO83Adh5pb2SqsxAwaP0fofFxKLkxctiS/9OQc= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk= -github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/cli v29.8.2+incompatible h1:2zgdFuoFst2T80oS42vhKGxkd0JRo305eIEKTsxR7pQ= +github.com/docker/cli v29.8.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -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.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/docker-credential-helpers v0.9.8 h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q= +github.com/docker/docker-credential-helpers v0.9.8/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.8.1 h1:JibmG5hULs5qXSr/cp/w3Pw5fZuStt4MOHMUExb29/M= +github.com/docker/go-connections v0.8.1/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 h1:EHZfspsnLAz8Hzccd67D5abwLiqoqym2jz/jOS39mCk= github.com/docker/go-events v0.0.0-20250114142523-c867878c5e32/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/dustin/go-humanize v1.1.0 h1:dbKTrvD0klcbBV/h4AWJdMuZogJACoMlvWIWZ5b2xWg= +github.com/dustin/go-humanize v1.1.0/go.mod h1:hc1CvRkJMsgxqjmjMQF3QNRAZBwY8AXBAzKYoSX9sFI= github.com/eapache/go-resiliency v1.4.0 h1:3OK9bWpPk5q6pbFAaYSEwD9CLUSHG8bnZuqX2yMt3B0= github.com/eapache/go-resiliency v1.4.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= @@ -365,8 +440,11 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= 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.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.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= @@ -381,6 +459,7 @@ github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= @@ -390,23 +469,23 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= -github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= -github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= +github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/github/go-spdx/v2 v2.4.0 h1:+4IwVwJJbm3rzvrQ6P1nI9BDMcy3la4RchRy5uehV/M= -github.com/github/go-spdx/v2 v2.4.0/go.mod h1:/5rwgS0txhGtRdUZwc02bTglzg6HK3FfuEbECKlK2Sg= +github.com/github/go-spdx/v2 v2.7.1 h1:+siV9wnsncI5pg4otH/sKvqGL8UkcLdPh16TSJYY1V4= +github.com/github/go-spdx/v2 v2.7.1/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= @@ -420,10 +499,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gota/gota v0.12.0 h1:T5BDg1hTf5fZ/CO+T/N0E+DDqUhvoKBl+UVckgcAAQg= github.com/go-gota/gota v0.12.0/go.mod h1:UT+NsWpZC/FhaOyWb9Hui0jXg0Iq8e/YugZHTbyW/34= -github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= -github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= -github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= -github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-jose/go-jose/v4 v4.1.5 h1:RjgjO2LOtWOJKUC5wpwY9LR3B3vwVAz6JS2YHfYU6eA= +github.com/go-jose/go-jose/v4 v4.1.5/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= @@ -504,6 +581,11 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gocql/gocql v1.6.0 h1:IdFdOTbnpbd0pDhl4REKQDM+Q0SzKXQ1Yh+YZZ8T/qU= github.com/gocql/gocql v1.6.0/go.mod h1:3gM2c4D3AnkISwBxGnMMsS8Oy4y2lhbPRsH4xnJrHG8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -512,16 +594,14 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg= github.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -578,17 +658,22 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-containerregistry v0.21.2 h1:vYaMU4nU55JJGFC9JR/s8NZcTjbE9DBBbvusTW9NeS0= -github.com/google/go-containerregistry v0.21.2/go.mod h1:ctO5aCaewH4AK1AumSF5DPW+0+R+d2FmylMJdp5G7p0= +github.com/google/go-containerregistry v0.22.1 h1:RZuuSYhTvlDvtsK+NkutoCZ//C0X2ebLK8X8l3ULs84= +github.com/google/go-containerregistry v0.22.1/go.mod h1:bJR35SK8XgisYmhg/FMQ/5RK0S/XrOAqLBV5/LR2XE0= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-intervals v0.0.2 h1:FGrVEiUnTRKR8yE04qzXYaJMtnIYqobR5QbblK3ixcM= +github.com/google/go-intervals v0.0.2/go.mod h1:MkaR3LNRfeKLPmqgJYs4E66z5InYjmCjbbr4TQlcT6Y= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -603,30 +688,32 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= -github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo= +github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/s2a-go v0.1.10 h1:EMp+aOuXN6l8cE/gjF5Bt+vyZxsUuyCWe9chDWR/+uU= +github.com/google/s2a-go v0.1.10/go.mod h1:pz4tyvwXvJLLbyrkh6FW1eS2zPUXMaTmyNhYtyP2tNw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= -github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.22 h1:NU4XpII6jD+Dxcot94fqjE+AfJoE/lQP9q3faYGzC/c= +github.com/googleapis/enterprise-certificate-proxy v0.3.22/go.mod h1:L3D/IQExI6LqEjBdXcZQ1WluSgigQmSwBboFstVPM4w= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= -github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= +github.com/googleapis/gax-go/v2 v2.24.1 h1:AtqTN21IXMMWo99LiEVAiBfNNQmO40d8xUfZI640mc0= +github.com/googleapis/gax-go/v2 v2.24.1/go.mod h1:bWeBei0NVwaNZKb2y1HUBS7gLXIF3/Tu3pq7j8D2Tb0= +github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= +github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= -github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= -github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= +github.com/gookit/color v1.6.2 h1:M6TJRJMuDFPYkrWmjOvMj+E0xIjcHq8lMPy7+vv0z1Y= +github.com/gookit/color v1.6.2/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= @@ -647,6 +734,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -654,7 +743,10 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.8.10 h1:uufJp+uacA0fSDRxclsHkiE6yUQTKgwv5b25QexgVM8= +github.com/hashicorp/go-getter v1.8.10/go.mod h1:5HyKGCQmT+I3MGAM76vk/fWpktjwxn7xruCUr9CrVuo= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -673,11 +765,13 @@ github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/b github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= +github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= @@ -691,6 +785,8 @@ github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSAS github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= +github.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -715,6 +811,7 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= @@ -731,8 +828,12 @@ github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 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.18.7 h1:aUyZsS4kH3QTKurYhAOwAHxllVPnOthb3vPfnF1Ehjw= -github.com/klauspost/compress v1.18.7/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ= +github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -767,8 +868,25 @@ github.com/kubescape/storage v0.0.301 h1:SsyS1Xdq8ClIOqCB5gNg7X82lGfrSpWLI+J+Ven github.com/kubescape/storage v0.0.301/go.mod h1:d/1hqWPda2clsjx2wmQgysnB5dThIo3rDKP7RWx+v+M= github.com/kubescape/storage/pkg/apiauth v0.1.0 h1:lkmcuT18mb1hi0R0Bz1MwWpJIpyVo1Ppuwo26fVh9WY= github.com/kubescape/storage/pkg/apiauth v0.1.0/go.mod h1:WiGMS7DCUl6a3SPwuFjxXHosdkNCw8F+JqRtGhBwQF8= +github.com/kubescape/syft v1.55.0-ks.3 h1:GL7gfj3zohpTB23YU6idVP+NuKKG5GfV1r2Fu5FhN8Q= +github.com/kubescape/syft v1.55.0-ks.3/go.mod h1:mBfgoR36a4J2XG7L6EPThwJjUjdQb/fUyxghW3U72Zk= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc= +github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM= +github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw= +github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= @@ -780,9 +898,10 @@ github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc8 github.com/mackerelio/go-osstat v0.2.5 h1:+MqTbZUhoIt4m8qzkVoXUJg1EuifwlAJSk4Yl2GXh+o= github.com/mackerelio/go-osstat v0.2.5/go.mod h1:atxwWF+POUZcdtR1wnsUcQxTytoHG4uhl2AKKzrOajY= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/magiconair/properties v1.18.12 h1:sT9zQpvTB3B4gzrX0tmZNTEaGyg8Zw55MFYRE32Mr9I= +github.com/magiconair/properties v1.18.12/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matthyx/inspektor-gadget v0.0.0-20260203101533-6ef87216d3dd h1:n8zR1L5t5UWzmQ/DgQ98DF/NrYJL7gUI57GkiDlyu9Y= github.com/matthyx/inspektor-gadget v0.0.0-20260203101533-6ef87216d3dd/go.mod h1:V4TgEmWo37K72pQvC7XuRQssysrxIIkrNX4TtEkgiE0= github.com/matthyx/maps v0.0.0-20260915111345-e9181ad40421 h1:9XVp2iZdJ7AVWvlNb7e9AOgGzXUyZmX79GzNJDNOCLM= @@ -802,6 +921,10 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0= +github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= @@ -809,12 +932,22 @@ github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ= +github.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= +github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0= +github.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc= +github.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A= +github.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec= +github.com/mistifyio/go-zfs/v4 v4.0.0 h1:sU0+5dX45tdDK5xNZ3HBi95nxUc48FS92qbIZEvpAg4= +github.com/mistifyio/go-zfs/v4 v4.0.0/go.mod h1:weotFtXTHvBwhr9Mv96KYnDkTPBOHFUbm9cBmQpesL0= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -832,6 +965,8 @@ 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 v28.5.2+incompatible h1:hIn6qcenb3JY1E3STwqEbBvJ8bha+u1LpqjX4CBvNCk= github.com/moby/moby v28.5.2+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/moby/client v0.6.0 h1:AJjEB21QPbXSXjDsZorFBoDZPhMrfbpaPLgSMAW9Bgs= +github.com/moby/moby/client v0.6.0/go.mod h1:OCo00wNRyA3m4lmJ228W3JbyCN4ZNNYjpOXiJydBdcQ= github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -848,8 +983,8 @@ github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0 github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= github.com/moby/sys/user v0.4.1 h1:RgjRlaDKi/Xmyrz4t8lyzXT6v2ooFeO/7xtchmhVWE0= github.com/moby/sys/user v0.4.1/go.mod h1:E9QsW5WRe1kUAf7kW8hXKwu1uhsZEAdPLYHYSDudF4Y= -github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= -github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/sys/userns v0.2.1 h1:4OvdM7BcPkASbuouHsbW3aeMJSFlYDldBRnXVZhaRk8= +github.com/moby/sys/userns v0.2.1/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -871,6 +1006,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nwaples/rardecode/v2 v2.4.1 h1:F7zNW2LdAuuBThHWXQaiFUGVD/sef299NfWSB1nHAl4= +github.com/nwaples/rardecode/v2 v2.4.1/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid/v2 v2.1.2 h1:IEclFb9JNvzYA6MW2SCxbLzcHTVsfqm3PrqGQJH5zec= @@ -883,8 +1020,8 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/open-policy-agent/opa v1.4.0 h1:IGO3xt5HhQKQq2axfa9memIFx5lCyaBlG+fXcgHpd3A= -github.com/open-policy-agent/opa v1.4.0/go.mod h1:DNzZPKqKh4U0n0ANxcCVlw8lCSv2c+h5G/3QvSYdWZ8= +github.com/open-policy-agent/opa v1.19.0 h1:+j2OCsjMezZEML2T1lI9giJdGJS/PL1XFKgkHPGIhpo= +github.com/open-policy-agent/opa v1.19.0/go.mod h1:pb6Y6klyf7X7X8uXNDflruA9dQC2gMqWROXI5w/kvv0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -893,9 +1030,10 @@ github.com/opencontainers/runtime-spec v1.2.1 h1:S4k4ryNgEpxW1dzyqffOmhI1BHYcjzU github.com/opencontainers/runtime-spec v1.2.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2 h1:2xZEHOdeQBV6PW8ZtimN863bIOl7OCW/X10K0cnxKeA= github.com/opencontainers/runtime-tools v0.9.1-0.20250523060157-0ea5ed0382a2/go.mod h1:MXdPzqAA8pHC58USHqNCSjyLnRQ6D+NjbpP+02Z1U/0= -github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= -github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= +github.com/opencontainers/selinux v1.15.1 h1:ERxeh5caJvCzNAKdI8WQbJmB1LDTn4BuaAg8wihLBpA= +github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZKumHcKMQtnaDlQQ= github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= @@ -904,8 +1042,8 @@ github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwp github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= @@ -915,8 +1053,8 @@ github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2 github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/picatz/xcel v0.0.0-20250816143731-885b5f678a12 h1:RS7RxrC+OtnYpgI0li0NwvpE0cqYewsZGXUb6wAe0oQ= github.com/picatz/xcel v0.0.0-20250816143731-885b5f678a12/go.mod h1:jxNaYyVlWe+WPV3G45KzlMLvplS3PQdHLUsFePIcaEg= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +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/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -926,6 +1064,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM= +github.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -935,12 +1075,14 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/proglottis/gpgme v0.1.6 h1:8WpQ8VWggLdxkuTnW+sZ1r1t92XBNd8GZNDhQ4Rz+98= +github.com/proglottis/gpgme v0.1.6/go.mod h1:5LoXMgpE4bttgwwdv9bLs/vwqv3qV7F4glEEZ7mRKrM= github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI= +github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -950,23 +1092,24 @@ github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvM github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI= +github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= -github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= +github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20240229202106-bca3154da60a h1:4VFls9SuqkqeioVevnaeTXrYKQ7JiEsxqKHfxp+/ovA= github.com/s3rj1k/go-fanotify/fanotify v0.0.0-20240229202106-bca3154da60a/go.mod h1:2zG1g57bc+D6FpNc68gsRXJgkidteqTMhWiiUP3m8UE= @@ -978,8 +1121,12 @@ github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6v github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/seccomp/libseccomp-golang v0.11.0 h1:SDkcBRqGLP+sezmMACkxO1EfgbghxIxnRKfd6mHUEis= -github.com/seccomp/libseccomp-golang v0.11.0/go.mod h1:5m1Lk8E9OwgZTTVz4bBOer7JuazaBa+xTkM895tDiWc= +github.com/seccomp/libseccomp-golang v0.11.1 h1:wuk4ZjSx6kyQII4rj6G6fvVzRHQaSiPvccJazDagu4g= +github.com/seccomp/libseccomp-golang v0.11.1/go.mod h1:5m1Lk8E9OwgZTTVz4bBOer7JuazaBa+xTkM895tDiWc= +github.com/secure-systems-lab/go-securesystemslib v0.11.1 h1:ayahDPjfSIqKegyt5YVGEvQ7SAi72GHXTy+b2YjUd5w= +github.com/secure-systems-lab/go-securesystemslib v0.11.1/go.mod h1:UyOjhoZLi76ir63u1ptubEevVe6dlNPmNxwgVvKqFFY= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -1009,12 +1156,22 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sigstore/fulcio v1.8.7 h1:d7QJQxukgXarqPCT9uDNeyzt1Bbt/biw58/VfOnwI/o= +github.com/sigstore/fulcio v1.8.7/go.mod h1:7fX+QyigZxRGKTgHvTfOfxwwniCDxK02PVloHy5/vg4= +github.com/sigstore/protobuf-specs v0.5.2 h1:RSWWUY8QrVTxbYH00jY/jg2e7YnjzrpwP+PeHTMll0E= +github.com/sigstore/protobuf-specs v0.5.2/go.mod h1:DRBzpFuE+LnvQMN10/dU6nBeKwVLGEQ6o2FovN2Rats= +github.com/sigstore/sigstore v1.10.11 h1:fjrwDfR0AWmRONYV/hKGJyqbT0osMtaBz9FTZ6MH9XI= +github.com/sigstore/sigstore v1.10.11/go.mod h1:ecPk+ftLqP8/TmzQFJmn0+QrL2yUQIJvyzVKNmDiw6U= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/sirupsen/logrus v1.10.2 h1:G2SED73/qrAu6YwbdxOD6peLkCBI3z7L+ykJFTXJBBo= +github.com/sirupsen/logrus v1.10.2/go.mod h1:SLEg8TqYulVKKfIGHldVp2K2aYz2DKSVBq4g/H5bR7Q= +github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA= +github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik= +github.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= @@ -1038,6 +1195,10 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.8.1 h1:eXZMLsu+3MLEPJyGJkolqtVrteZfQdUpOWj6LTiDl/E= +github.com/spiffe/go-spiffe/v2 v2.8.1/go.mod h1:47Q0Q9/AqGha8QLHp+kxpH4Wca7X7EnOtlIJy3mxZ3U= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 h1:pnnLyeX7o/5aX8qUQ69P/mLojDqwda8hFOCBTmP/6hw= +github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6/go.mod h1:39R/xuhNgVhi+K0/zst4TLrJrVmbm6LVgl4A0+ZFS5M= github.com/streadway/amqp v1.1.0 h1:py12iX8XSyI7aN/3dUT8DFIDJazNJsVJdxNVEpnQTZM= github.com/streadway/amqp v1.1.0/go.mod h1:WYSrTEYHOXHd0nwFeUXAe2G2hRnQT+deZJJf88uS9Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1062,15 +1223,21 @@ github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaB github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/sylabs/sif/v2 v2.24.2 h1:rdkFf6z3xLC6yKweU7S9marlKfkS91Ht0ruIri6YWFY= +github.com/sylabs/sif/v2 v2.24.2/go.mod h1:bbrkk0JhTX+xNJcnUtmDkivoO2r4DGWM3LxNR0U2ooA= github.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI= github.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/terminalstatic/go-xsd-validate v0.1.8 h1:UVrTCy1j3DhwaYTTUF+QYO/Nan13S0tf+Jwi+p45Bf0= +github.com/terminalstatic/go-xsd-validate v0.1.8/go.mod h1:1kb47fi2c6onlf+B7UrrQ9VYraOhcYwFm3iG+J6F4Zo= github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= github.com/testcontainers/testcontainers-go/modules/k3s v0.40.0 h1:3w6SjtIp/+FdpjWJCyPqaGWknG2iU6MacEWA7hl0IqQ= github.com/testcontainers/testcontainers-go/modules/k3s v0.40.0/go.mod h1:1xJwmfO2g+XKox9LiJXKGCm1vWp7LozX+78UjXVRbF0= +github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= +github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -1081,16 +1248,25 @@ github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZ github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.17 h1:flR0y/x1hgM8EGV1AW3Xll6T413G0glV8UfBwR617V4= +github.com/ulikunitz/xz v0.5.17/go.mod h1:H9Rt/W6/Qj27PGauhQc6nfCDy7vHpzsOThBSaYDoEhw= github.com/uptrace/opentelemetry-go-extra/otelutil v0.4.0 h1:ZC08i0whcTGacAS7iDFxYL5D/bHdtdzxAjMJD8M15Pg= github.com/uptrace/opentelemetry-go-extra/otelutil v0.4.0/go.mod h1:j5Tnr2zA8UTDRzfIAZUaseE5/QPQCRv8JShSuGZek1A= github.com/uptrace/opentelemetry-go-extra/otelzap v0.4.0 h1:nPz2yF9qIJyuYDWhD7+ieSlNTi4OHgoOQ6MpR7jwNa8= github.com/uptrace/opentelemetry-go-extra/otelzap v0.4.0/go.mod h1:ubWgbUJc+h0o4gvsov82+s98XLu6rVQ//tQ47nINT2A= github.com/uptrace/uptrace-go v1.47.0 h1:FQgpAx/DdfDEaJ9xX03RaAc7BXVraBz0qC15oynFDpI= github.com/uptrace/uptrace-go v1.47.0/go.mod h1:xVJgZjczsSa94/pdg5HrtWTY/HyfCXxL7f+MienUh7Y= -github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= -github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= +github.com/vbatts/tar-split v0.12.3 h1:Cd46rkGXI3Td4yrVNwU8ripbxFaQbmesqhjBUUYAJSw= +github.com/vbatts/tar-split v0.12.3/go.mod h1:sQOc6OlqGCr7HkGx/IDBeKiTIvqhmj8KffNhEXG4Nq0= +github.com/vbauerster/cupwriter v0.0.4 h1:9sBPe0uXWLZuWQU5lqVbhyFlxX6c09asST/YfatFAys= +github.com/vbauerster/cupwriter v0.0.4/go.mod h1:IFyzS6Xis5dnBH/rdAhrnuzg3c+KkUqEN6yE8lhJlDw= +github.com/vbauerster/mpb/v8 v8.14.0 h1:55SR80dptMfASxIG/oCEkBXgBhxeSu4GrVsjl16oKmA= +github.com/vbauerster/mpb/v8 v8.14.0/go.mod h1:HgpQPKfcWe3kbuGGPmi+jatHreMase5C3Fp5dpdAy0Q= +github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s= +github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= @@ -1113,12 +1289,16 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMc github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU= @@ -1133,8 +1313,8 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= -go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU= +go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= @@ -1160,12 +1340,12 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -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/bridges/otelslog v0.21.0 h1:/CdgE52i1OEPHhsGUqrggYNRaZy4v4YgVenGd52dRBI= go.opentelemetry.io/contrib/bridges/otelslog v0.21.0/go.mod h1:9pBdbVbuehIDzxN4accnzzxEUPT8Nq7grbZMo+J1VN4= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0 h1:NmLfL734pJhM0JKaYd2Y28+nY9dPRWYAAbxhRCrKXPw= +go.opentelemetry.io/contrib/detectors/gcp v1.44.0/go.mod h1:tNAsgd8avTGke1+MndXlU5Cru4PQ9Ai/cCNWQv/ZJ/s= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 h1:B2h3uqicet1CT2N5TOFhS+Gq++9i0/CLmaxvhmhtP5s= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0/go.mod h1:dylvB+ZiiwMvsDij9O84Uy7SijLgHMX4mbkncds+4Sw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg= @@ -1190,6 +1370,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0 h1:UFpxO go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.47.0/go.mod h1:YBGjxe3lt0jtXoEXxGrGhv/jM7oUBUDVK7zhAvNyEjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0 h1:hqxVTu/GtBF+vJ8d1fzW7fRxZFvgoDjWcxwwCaFDYpU= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.44.0/go.mod h1:z5fVEF4X5v0ESvlJqBrrFlBVoj5EQuefZpzsu7R+x5Q= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0 h1:N3YQCxjxQ/bMjyc3heladfRm9t9RTksGQH8z4w6yU/0= go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.47.0/go.mod h1:Mp8HOFqcaUyypCuGv9IhDdTHnJ56lSudSHMd+pVSCEA= go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI= @@ -1211,6 +1393,10 @@ go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08u go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw= go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg= +go.podman.io/image/v5 v5.41.2 h1:YnVaFrGLPWSBmipKfNvlOsGcBtk5bUxlPxQ+A6CS/uo= +go.podman.io/image/v5 v5.41.2/go.mod h1:T/bzxEFpdpAzyHabPmcq5K+my3wBsht22sFkFjJSKT4= +go.podman.io/storage v1.64.1 h1:DqbG8scW/bD3xHiSRzV7O5fzn46qVC+SwmUN6C6Fr5w= +go.podman.io/storage v1.64.1/go.mod h1:ft0DzCRMZs1yn0rszmLYXEEwgd7IIc66JRwSRPUDwwc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= @@ -1220,12 +1406,14 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8 go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc= +go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= @@ -1243,8 +1431,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.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= +golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1348,8 +1536,8 @@ golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= -golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= +golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1369,8 +1557,8 @@ golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= +golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1384,8 +1572,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.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= +golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= 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= @@ -1457,21 +1645,23 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= +golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= +golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= 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.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= -golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM= +golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY= 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= @@ -1483,14 +1673,14 @@ 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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= +golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= +golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 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= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1551,8 +1741,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.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= -golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= +golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI= +golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA= 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= @@ -1600,8 +1790,8 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= -google.golang.org/api v0.278.0 h1:W7jiRvRi53VYFfZ/HoZjQBtJk7gOFbHD8ot1RzVZU6E= -google.golang.org/api v0.278.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= +google.golang.org/api v0.299.0 h1:b3K+ydSMd0kh6TQI6bJyApRQfqQX2MfSOaVkpM59mJw= +google.golang.org/api v0.299.0/go.mod h1:zlR3GVA8b2R5nv5Ij9UWe37StVB3cxDD7DBFi4ZFsHw= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1678,8 +1868,8 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= +google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d h1:C9v1o0/4quuhOAfmRXA2j+we0PqZIp8traLdeogF3Ms= +google.golang.org/genproto v0.0.0-20260715232425-e75dac1f907d/go.mod h1:Wz2wFJntZFmLGo7pLDXZ3wYk5hyc0Mb+SkHhDDXT+lU= google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8 h1:cFbkNigDInw+wVTXc+CyhUsoHGJCw0BR6mlNoRVfre0= google.golang.org/genproto/googleapis/api v0.0.0-20261005182115-fad411399dd8/go.mod h1:QK6p7IgD7IZEZJBhhSUCib5X2vMOzlrGCGUw2XyVDh0= google.golang.org/genproto/googleapis/rpc v0.0.0-20261005182115-fad411399dd8 h1:0SXBqli5dnuCKMjI/X3FM226zEaEa0GUcSCJtN1vL2o= diff --git a/mainhandler/handlerequests.go b/mainhandler/handlerequests.go index a11b0c42..996fd942 100644 --- a/mainhandler/handlerequests.go +++ b/mainhandler/handlerequests.go @@ -151,17 +151,28 @@ func (mainHandler *MainHandler) HandleWatchers(ctx context.Context) { commandWatchHandler := watcher.NewCommandWatchHandler(mainHandler.k8sAPI, mainHandler.config) operatorCommandsHandler := watcher.NewOperatorCommandsHandler(ctx, mainHandler.eventWorkerPool, mainHandler.k8sAPI, commandWatchHandler, mainHandler.config) - if mainHandler.config.Components().Kubevuln.Enabled { + kubevulnEnabled := mainHandler.config.Components().Kubevuln.Enabled + sbomExporter := mainHandler.newSBOMExporter() + if kubevulnEnabled || sbomExporter != nil { eventQueue := watcher.NewCooldownQueue() watchHandler := watcher.NewWatchHandler(mainHandler.config, mainHandler.k8sAPI, mainHandler.ksStorageClient, eventQueue) - registryCommandsHandler := watcher.NewRegistryCommandsHandler(ctx, mainHandler.k8sAPI, commandWatchHandler, mainHandler.config) - go registryCommandsHandler.Start() + if sbomExporter != nil { + watchHandler.SetSBOMObserver(sbomExporter) + go sbomExporter.Start(ctx) + } + + if kubevulnEnabled { + registryCommandsHandler := watcher.NewRegistryCommandsHandler(ctx, mainHandler.k8sAPI, commandWatchHandler, mainHandler.config) + go registryCommandsHandler.Start() - // wait for the kubevuln component to be ready - logger.L().Info("Waiting for vuln scan to be ready") - waitFunc := isActionNeedToWait(apis.Command{CommandName: apis.TypeScanImages}) - waitFunc(mainHandler.config) + // wait for the kubevuln component to be ready + logger.L().Info("Waiting for vuln scan to be ready") + waitFunc := isActionNeedToWait(apis.Command{CommandName: apis.TypeScanImages}) + waitFunc(mainHandler.config) + } else { + watchHandler.DisableImageScans() + } // start watching if mainHandler.config.NodeSbomGenerationEnabled() { @@ -169,7 +180,9 @@ func (mainHandler *MainHandler) HandleWatchers(ctx context.Context) { } else { go watchHandler.PodWatch(ctx, mainHandler.eventWorkerPool) } - go watchHandler.ContainerProfileWatch(ctx, mainHandler.eventWorkerPool) + if kubevulnEnabled { + go watchHandler.ContainerProfileWatch(ctx, mainHandler.eventWorkerPool) + } } // Watch SecurityException/ClusterSecurityException CRDs and rescan on diff --git a/mainhandler/sbomexport.go b/mainhandler/sbomexport.go new file mode 100644 index 00000000..13a94850 --- /dev/null +++ b/mainhandler/sbomexport.go @@ -0,0 +1,45 @@ +package mainhandler + +import ( + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + "github.com/kubescape/operator/config" + "github.com/kubescape/operator/sbomexport" +) + +// newSBOMExporter returns the SBOM exporter when the sbomExport capability is +// on, and nil when it is off or cannot run. It needs nodeSbomGeneration: it +// exports the SBOMs node-agent makes, and SBOMWatch, which feeds it, only runs +// with it. +func (mainHandler *MainHandler) newSBOMExporter() *sbomexport.Exporter { + if !mainHandler.config.SBOMExportEnabled() { + return nil + } + if !mainHandler.config.NodeSbomGenerationEnabled() { + logger.L().Warning("SBOM export is off: the sbomExport capability needs nodeSbomGeneration") + return nil + } + exporter, err := sbomexport.New(sbomExportOptions(mainHandler.config), mainHandler.k8sAPI.KubernetesClient, mainHandler.ksStorageClient) + if err != nil { + logger.L().Error("SBOM export is off: invalid configuration", helpers.Error(err)) + return nil + } + return exporter +} + +// sbomExportOptions reads the exporter's options from the operator +// configuration. The root project defaults to the cluster name. +func sbomExportOptions(cfg config.IConfig) sbomexport.Options { + export := cfg.SBOMExportConfig() + rootProject := export.DependencyTrack.RootProject + if rootProject == "" { + rootProject = cfg.ClusterName() + } + return sbomexport.Options{ + URL: export.DependencyTrack.URL, + APIKeyFile: export.DependencyTrack.APIKeyFile, + RootProject: rootProject, + ResyncInterval: export.ResyncInterval, + SkipNamespace: cfg.SkipNamespace, + } +} diff --git a/mainhandler/sbomexport_test.go b/mainhandler/sbomexport_test.go new file mode 100644 index 00000000..99811e8f --- /dev/null +++ b/mainhandler/sbomexport_test.go @@ -0,0 +1,87 @@ +package mainhandler + +import ( + "testing" + "time" + + "github.com/armosec/utils-k8s-go/armometadata" + beUtils "github.com/kubescape/backend/pkg/utils" + "github.com/kubescape/operator/config" + "github.com/kubescape/operator/utils" + kssfake "github.com/kubescape/storage/pkg/generated/clientset/versioned/fake" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + k8sfake "k8s.io/client-go/kubernetes/fake" +) + +func sbomExportConfig(t *testing.T, capabilities config.Capabilities, export config.SBOMExportConfig) config.IConfig { + t.Helper() + cfg, err := config.NewOperatorConfig( + config.CapabilitiesConfig{Capabilities: capabilities}, + armometadata.ClusterConfig{ClusterName: "prod-cluster"}, + &beUtils.Credentials{}, + config.Config{SBOMExport: export}, + ) + require.NoError(t, err) + return cfg +} + +func TestNewSBOMExporter(t *testing.T) { + valid := config.SBOMExportConfig{ + ResyncInterval: time.Hour, + DependencyTrack: config.DependencyTrackConfig{URL: "https://dtrack.example.com", APIKeyFile: "/etc/sbom-export/api-key"}, + } + tt := []struct { + name string + capabilities config.Capabilities + export config.SBOMExportConfig + wantExporter bool + }{ + { + name: "capability off", + capabilities: config.Capabilities{NodeSbomGeneration: "enable"}, + export: valid, + }, + { + name: "without nodeSbomGeneration", + capabilities: config.Capabilities{SBOMExport: "enable"}, + export: valid, + }, + { + name: "without a Dependency-Track URL", + capabilities: config.Capabilities{SBOMExport: "enable", NodeSbomGeneration: "enable"}, + export: config.SBOMExportConfig{ResyncInterval: time.Hour}, + }, + { + name: "capability on and configured", + capabilities: config.Capabilities{SBOMExport: "enable", NodeSbomGeneration: "enable"}, + export: valid, + wantExporter: true, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + mainHandler := &MainHandler{ + config: sbomExportConfig(t, tc.capabilities, tc.export), + k8sAPI: utils.NewK8sInterfaceFake(k8sfake.NewClientset()), + ksStorageClient: kssfake.NewClientset(), + } + assert.Equal(t, tc.wantExporter, mainHandler.newSBOMExporter() != nil) + }) + } +} + +func TestSBOMExportOptionsDefaultTheRootProjectToTheClusterName(t *testing.T) { + export := config.SBOMExportConfig{ + ResyncInterval: 30 * time.Minute, + DependencyTrack: config.DependencyTrackConfig{URL: "https://dtrack.example.com", APIKeyFile: "/keys/dtrack"}, + } + opts := sbomExportOptions(sbomExportConfig(t, config.Capabilities{}, export)) + assert.Equal(t, "prod-cluster", opts.RootProject) + assert.Equal(t, "https://dtrack.example.com", opts.URL) + assert.Equal(t, "/keys/dtrack", opts.APIKeyFile) + assert.Equal(t, 30*time.Minute, opts.ResyncInterval) + + export.DependencyTrack.RootProject = "prod" + assert.Equal(t, "prod", sbomExportOptions(sbomExportConfig(t, config.Capabilities{}, export)).RootProject) +} diff --git a/sbomexport/convert.go b/sbomexport/convert.go new file mode 100644 index 00000000..10e08bb6 --- /dev/null +++ b/sbomexport/convert.go @@ -0,0 +1,32 @@ +package sbomexport + +import ( + "bytes" + "fmt" + + "github.com/anchore/syft/syft/format/cyclonedxjson" + "github.com/anchore/syft/syft/format/syftjson" +) + +// cycloneDXVersion is the CycloneDX specification version of the uploaded +// BOMs. +const cycloneDXVersion = "1.6" + +// toCycloneDX converts a syft JSON document, the `.spec.syft` of an SBOMSyft, +// to CycloneDX JSON, the way `syft convert` does: syft's own syft-json +// decoder, then its CycloneDX encoder. +func toCycloneDX(syftJSON []byte) ([]byte, error) { + s, _, _, err := syftjson.NewFormatDecoder().Decode(bytes.NewReader(syftJSON)) + if err != nil { + return nil, fmt.Errorf("decode syft document: %w", err) + } + enc, err := cyclonedxjson.NewFormatEncoderWithConfig(cyclonedxjson.EncoderConfig{Version: cycloneDXVersion}) + if err != nil { + return nil, err + } + var buf bytes.Buffer + if err := enc.Encode(&buf, *s); err != nil { + return nil, fmt.Errorf("encode CycloneDX: %w", err) + } + return buf.Bytes(), nil +} diff --git a/sbomexport/convert_test.go b/sbomexport/convert_test.go new file mode 100644 index 00000000..744017ec --- /dev/null +++ b/sbomexport/convert_test.go @@ -0,0 +1,38 @@ +package sbomexport + +import ( + _ "embed" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// curl-8.11.1.syft.json is the `.spec.syft` that node-agent v0.3.310 stored +// for docker.io/curlimages/curl:8.11.1; curl-8.11.1.cdx.json is what +// `syft convert -o cyclonedx-json@1.6` (syft v1.55.0) makes of it. +var ( + //go:embed testdata/curl-8.11.1.syft.json + curlSyftJSON []byte + //go:embed testdata/curl-8.11.1.cdx.json + curlCycloneDX []byte +) + +func TestToCycloneDXMatchesSyftConvert(t *testing.T) { + got, err := toCycloneDX(curlSyftJSON) + require.NoError(t, err) + + var gotBOM, wantBOM map[string]any + require.NoError(t, json.Unmarshal(got, &gotBOM)) + require.NoError(t, json.Unmarshal(curlCycloneDX, &wantBOM)) + assert.Equal(t, cycloneDXVersion, gotBOM["specVersion"]) + assert.Equal(t, wantBOM["components"], gotBOM["components"]) + assert.Equal(t, wantBOM["dependencies"], gotBOM["dependencies"]) + assert.Equal(t, wantBOM["metadata"].(map[string]any)["component"], gotBOM["metadata"].(map[string]any)["component"]) +} + +func TestToCycloneDXRejectsADocumentThatIsNotSyftJSON(t *testing.T) { + _, err := toCycloneDX(curlCycloneDX) + assert.Error(t, err) +} diff --git a/sbomexport/dtrack.go b/sbomexport/dtrack.go new file mode 100644 index 00000000..2199babd --- /dev/null +++ b/sbomexport/dtrack.go @@ -0,0 +1,200 @@ +package sbomexport + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "strings" + "time" +) + +// project is the part of a Dependency-Track project the exporter reads. +type project struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Version string `json:"version"` + IsLatest bool `json:"isLatest"` + CollectionLogic string `json:"collectionLogic"` + LastBOMImport epochMillis `json:"lastBomImport"` +} + +// epochMillis is a Dependency-Track timestamp. It decodes from epoch +// milliseconds or from an RFC 3339 string. +type epochMillis int64 + +func (m *epochMillis) UnmarshalJSON(b []byte) error { + if string(b) == "null" { + *m = 0 + return nil + } + var n int64 + if err := json.Unmarshal(b, &n); err == nil { + *m = epochMillis(n) + return nil + } + var s string + if err := json.Unmarshal(b, &s); err != nil { + return fmt.Errorf("timestamp is neither a number nor a string: %s", b) + } + t, err := time.Parse(time.RFC3339Nano, s) + if err != nil { + return err + } + *m = epochMillis(t.UnixMilli()) + return nil +} + +// Time returns the timestamp as a time.Time; zero when Dependency-Track sent none. +func (m epochMillis) Time() time.Time { + if m == 0 { + return time.Time{} + } + return time.UnixMilli(int64(m)) +} + +// dtClient calls the Dependency-Track API v1. +type dtClient struct { + baseURL string + apiKeyFile string + http *http.Client +} + +// errBody is how much of an error response goes into an error message. +const errBody = 300 + +// do sends one request. The API key is read from apiKeyFile on every call, so +// a rotated Secret takes effect without a restart. A status in ok is a +// success, and a 2xx body is decoded into out when out is not nil; any other +// status is an error. +func (c *dtClient) do(ctx context.Context, method, path string, body any, out any, ok ...int) (int, error) { + key, err := os.ReadFile(c.apiKeyFile) + if err != nil { + return 0, fmt.Errorf("read Dependency-Track API key: %w", err) + } + if len(bytes.TrimSpace(key)) == 0 { + return 0, fmt.Errorf("the Dependency-Track API key file %s is empty", c.apiKeyFile) + } + + var reqBody io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return 0, err + } + reqBody = bytes.NewReader(b) + } + req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, reqBody) + if err != nil { + return 0, err + } + req.Header.Set("X-Api-Key", string(bytes.TrimSpace(key))) + req.Header.Set("Accept", "application/json") + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := c.http.Do(req) + if err != nil { + return 0, fmt.Errorf("%s %s: %w", method, path, err) + } + defer func() { _ = resp.Body.Close() }() + data, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) + if err != nil { + return resp.StatusCode, fmt.Errorf("%s %s: read response: %w", method, path, err) + } + + for _, s := range ok { + if resp.StatusCode != s { + continue + } + if out != nil && len(data) > 0 && resp.StatusCode < 300 { // a 404 body is plain text + if err := json.Unmarshal(data, out); err != nil { + return resp.StatusCode, fmt.Errorf("%s %s: decode response: %w", method, path, err) + } + } + return resp.StatusCode, nil + } + msg := strings.TrimSpace(string(data)) + if len(msg) > errBody { + msg = msg[:errBody] + } + return resp.StatusCode, &httpError{method: method, path: path, status: resp.StatusCode, body: msg} +} + +// httpError is a response with an unexpected status. +type httpError struct { + method, path string + status int + body string +} + +func (e *httpError) Error() string { + return fmt.Sprintf("%s %s: HTTP %d: %s", e.method, e.path, e.status, e.body) +} + +// isAuthError reports whether err is Dependency-Track refusing the API key or +// one of its permissions. +func isAuthError(err error) bool { + var h *httpError + return errors.As(err, &h) && (h.status == http.StatusUnauthorized || h.status == http.StatusForbidden) +} + +// lookup returns the project name:version, or nil when it does not exist. +func (c *dtClient) lookup(ctx context.Context, name, version string) (*project, error) { + q := url.Values{"name": {name}, "version": {version}} + var p project + status, err := c.do(ctx, http.MethodGet, "/api/v1/project/lookup?"+q.Encode(), nil, &p, http.StatusOK, http.StatusNotFound) + if err != nil { + return nil, err + } + if status == http.StatusNotFound { + return nil, nil + } + return &p, nil +} + +// createCollection creates the collection project name:version with the given +// collection logic, under parentUUID unless it is empty. +func (c *dtClient) createCollection(ctx context.Context, name, version, parentUUID, logic string) (*project, error) { + body := map[string]any{"name": name, "version": version, "collectionLogic": logic} + if parentUUID != "" { + body["parent"] = map[string]string{"uuid": parentUUID} + } + var p project + if _, err := c.do(ctx, http.MethodPut, "/api/v1/project", body, &p, http.StatusCreated, http.StatusOK); err != nil { + return nil, err + } + return &p, nil +} + +// patch changes the given fields of the project with uuid. Dependency-Track +// answers 304 when nothing changed. +func (c *dtClient) patch(ctx context.Context, uuid string, fields map[string]any) error { + _, err := c.do(ctx, http.MethodPatch, "/api/v1/project/"+url.PathEscape(uuid), fields, nil, http.StatusOK, http.StatusNotModified) + return err +} + +// uploadBOM uploads bom as the latest version of name:version, creating the +// project under parentUUID when it does not exist. Dependency-Track processes +// the BOM asynchronously after answering. +func (c *dtClient) uploadBOM(ctx context.Context, name, version, parentUUID string, bom []byte) error { + body := map[string]any{ + "projectName": name, + "projectVersion": version, + "autoCreate": true, + "isLatest": true, + "bom": base64.StdEncoding.EncodeToString(bom), + } + if parentUUID != "" { + body["parentUUID"] = parentUUID + } + _, err := c.do(ctx, http.MethodPut, "/api/v1/bom", body, nil, http.StatusOK) + return err +} diff --git a/sbomexport/dtrack_test.go b/sbomexport/dtrack_test.go new file mode 100644 index 00000000..2ca96e09 --- /dev/null +++ b/sbomexport/dtrack_test.go @@ -0,0 +1,113 @@ +package sbomexport + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDTClientLooksUpCreatesAndPatchesProjects(t *testing.T) { + ctx := context.Background() + f, srv, keyFile := newFakeDT(t, time.Now) + c := &dtClient{baseURL: srv.URL, apiKeyFile: keyFile, http: srv.Client()} + + p, err := c.lookup(ctx, "cluster", "current") + require.NoError(t, err) + assert.Nil(t, p, "a missing project is not an error") + + root, err := c.createCollection(ctx, "cluster", "current", "", clusterLogic) + require.NoError(t, err) + ns, err := c.createCollection(ctx, "cluster/apps", "current", root.UUID, namespaceLogic) + require.NoError(t, err) + require.NoError(t, c.patch(ctx, ns.UUID, map[string]any{"collectionLogic": clusterLogic})) + + got, parent, ok := f.project("cluster/apps", "current") + require.True(t, ok) + assert.Equal(t, "cluster", parent) + assert.Equal(t, clusterLogic, got.CollectionLogic) + + p, err = c.lookup(ctx, "cluster/apps", "current") + require.NoError(t, err) + assert.Equal(t, ns.UUID, p.UUID) +} + +func TestDTClientUploadsTheBOMUnderItsParent(t *testing.T) { + ctx := context.Background() + now := time.Date(2026, 11, 10, 12, 0, 0, 0, time.UTC) + f, srv, keyFile := newFakeDT(t, func() time.Time { return now }) + c := &dtClient{baseURL: srv.URL, apiKeyFile: keyFile, http: srv.Client()} + ns, err := c.createCollection(ctx, "cluster/apps", "current", "", namespaceLogic) + require.NoError(t, err) + + require.NoError(t, c.uploadBOM(ctx, "cluster/apps/redis", "8", ns.UUID, curlCycloneDX)) + + got, parent, ok := f.project("cluster/apps/redis", "8") + require.True(t, ok) + assert.Equal(t, "cluster/apps", parent) + assert.True(t, got.IsLatest) + assert.Equal(t, now, got.LastBOMImport.Time().UTC()) +} + +func TestDTClientReadsTheAPIKeyOnEveryRequest(t *testing.T) { + ctx := context.Background() + _, srv, keyFile := newFakeDT(t, time.Now) + c := &dtClient{baseURL: srv.URL, apiKeyFile: keyFile, http: srv.Client()} + + require.NoError(t, os.WriteFile(keyFile, []byte("revoked-key"), 0600)) + _, err := c.lookup(ctx, "cluster", "current") + require.Error(t, err) + assert.True(t, isAuthError(err)) + + require.NoError(t, os.WriteFile(keyFile, []byte(testAPIKey), 0600)) + _, err = c.lookup(ctx, "cluster", "current") + assert.NoError(t, err, "the rotated key is used without a restart") + + require.NoError(t, os.WriteFile(keyFile, []byte(" \n"), 0600)) + _, err = c.lookup(ctx, "cluster", "current") + assert.ErrorContains(t, err, "is empty") +} + +func TestDTClientErrorCarriesTheStatusAndTheStartOfTheBody(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, strings.Repeat("x", 2*errBody), http.StatusInternalServerError) + })) + defer srv.Close() + keyFile := t.TempDir() + "/api-key" + require.NoError(t, os.WriteFile(keyFile, []byte(testAPIKey), 0600)) + c := &dtClient{baseURL: srv.URL, apiKeyFile: keyFile, http: srv.Client()} + + err := c.uploadBOM(context.Background(), "cluster/apps/redis", "8", "", curlCycloneDX) + + require.Error(t, err) + assert.Contains(t, err.Error(), "PUT /api/v1/bom: HTTP 500: "+strings.Repeat("x", errBody)) + assert.NotContains(t, err.Error(), strings.Repeat("x", errBody+1)) + assert.False(t, isAuthError(err)) +} + +func TestEpochMillisDecodesNumbersAndRFC3339(t *testing.T) { + tt := []struct { + name string + json string + want time.Time + }{ + {"epoch milliseconds", `{"lastBomImport":1762776000123}`, time.UnixMilli(1762776000123)}, + {"RFC 3339", `{"lastBomImport":"2025-11-10T12:00:00.123Z"}`, time.UnixMilli(1762776000123)}, + {"null", `{"lastBomImport":null}`, time.Time{}}, + {"missing", `{}`, time.Time{}}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + var p project + require.NoError(t, json.Unmarshal([]byte(tc.json), &p)) + assert.True(t, tc.want.Equal(p.LastBOMImport.Time()), "got %s", p.LastBOMImport.Time()) + }) + } +} diff --git a/sbomexport/exporter.go b/sbomexport/exporter.go new file mode 100644 index 00000000..d567c189 --- /dev/null +++ b/sbomexport/exporter.go @@ -0,0 +1,412 @@ +// Package sbomexport sends the SBOMs that node-agent stores (SBOMSyft +// objects) to Dependency-Track, one project per running image and namespace, +// and keeps them current as workloads change. +// +// The projects form a tree under a root project, by default the cluster name: +// +// : current collection, adds up its namespaces +// └── / : current collection, adds up the latest version of each image +// └── // : +// +// Two things trigger an export: SBOMWatch's events (an SBOM becomes ready, a +// pod starts with an image whose SBOM is ready), and a resync that compares +// every running image with Dependency-Track, so nothing is lost while +// Dependency-Track is unreachable or the operator restarts. An image is sent +// again only when its SBOMSyft is newer than the project's last BOM import. +package sbomexport + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + kssc "github.com/kubescape/storage/pkg/generated/clientset/versioned" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/pager" + "k8s.io/client-go/util/workqueue" +) + +const ( + // collectionVersion is the version of the two collection levels: + // Dependency-Track 5 refuses a project without a version. + collectionVersion = "current" + + // The cluster adds up its namespaces; a namespace adds up the latest + // version of each of its images, so the running tag is what counts. + clusterLogic = "AGGREGATE_DIRECT_CHILDREN" + namespaceLogic = "AGGREGATE_LATEST_VERSION_CHILDREN" + + // maxRetries bounds the retries of one image between two resyncs. + maxRetries = 5 + + requestTimeout = 2 * time.Minute +) + +// Options configures an Exporter. +type Options struct { + // URL is the base URL of the Dependency-Track API server. + URL string + // APIKeyFile holds the API key; it is read on every request. + APIKeyFile string + // RootProject is the root of the project tree. + RootProject string + // ResyncInterval is how often every running image is compared with + // Dependency-Track. It is also how long an image counts as running in a + // namespace after its last pod there ended. + ResyncInterval time.Duration + // SkipNamespace reports the namespaces whose images are not exported. + SkipNamespace func(namespace string) bool +} + +func (o Options) validate() error { + u, err := url.Parse(o.URL) + if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" { + return fmt.Errorf("sbomExport.dependencyTrack.url %q is not an http(s) URL", o.URL) + } + if o.APIKeyFile == "" { + return errors.New("sbomExport.dependencyTrack.apiKeyFile is empty") + } + if o.RootProject == "" { + return errors.New("sbomExport.dependencyTrack.rootProject is empty and the cluster has no name") + } + if o.ResyncInterval <= 0 { + return fmt.Errorf("sbomExport.resyncInterval %s must be positive", o.ResyncInterval) + } + return nil +} + +// Exporter sends SBOMs to Dependency-Track. Create it with New, feed it +// SBOMWatch's events with ObservePod and ObserveSBOM, and run it with Start. +type Exporter struct { + opts Options + pods kubernetes.Interface + sboms sbomStore + dt *dtClient + running *index + queue workqueue.TypedRateLimitingInterface[string] // image digests + + mu sync.Mutex + sbomByImage map[string]sbomMeta // digest → its SBOMSyft + collections map[string]string // collection project name → uuid, emptied on every resync + + stats stats +} + +// New returns an Exporter that lists pods with pods and reads SBOMSyft +// objects with storage. +func New(opts Options, pods kubernetes.Interface, storage kssc.Interface) (*Exporter, error) { + return newExporter(opts, pods, storageSBOMs{client: storage}, &http.Client{Timeout: requestTimeout}, time.Now) +} + +func newExporter(opts Options, pods kubernetes.Interface, sboms sbomStore, httpClient *http.Client, now func() time.Time) (*Exporter, error) { + if err := opts.validate(); err != nil { + return nil, err + } + if opts.SkipNamespace == nil { + opts.SkipNamespace = func(string) bool { return false } + } + return &Exporter{ + opts: opts, + pods: pods, + sboms: sboms, + dt: &dtClient{baseURL: strings.TrimRight(opts.URL, "/"), apiKeyFile: opts.APIKeyFile, http: httpClient}, + running: newIndex(opts.ResyncInterval, now), + queue: workqueue.NewTypedRateLimitingQueueWithConfig( + workqueue.NewTypedItemExponentialFailureRateLimiter[string](5*time.Second, 5*time.Minute), + workqueue.TypedRateLimitingQueueConfig[string]{Name: "sbom-export"}, + ), + sbomByImage: map[string]sbomMeta{}, + collections: map[string]string{}, + }, nil +} + +// ObservePod records a pod event from SBOMWatch's pod watch, which only sees +// running pods: a pod that stops running arrives as Deleted. +func (e *Exporter) ObservePod(eventType watch.EventType, pod *corev1.Pod) { + if e.opts.SkipNamespace(pod.Namespace) { + return + } + key := pod.Namespace + "/" + pod.Name + switch eventType { + case watch.Added, watch.Modified: + for _, d := range e.running.setPod(key, pod.Namespace, runningDigests(pod)) { + if m, ok := e.sbom(d); ok && m.ready() { + e.queue.Add(d) + } + } + case watch.Deleted: + e.running.removePod(key) + } +} + +// ObserveSBOM records an SBOMSyft event from SBOMWatch. +func (e *Exporter) ObserveSBOM(eventType watch.EventType, obj *spdxv1beta1.SBOMSyft) { + m, ok := metaOf(obj) + if !ok { + return + } + switch eventType { + case watch.Added, watch.Modified: + e.mu.Lock() + e.sbomByImage[m.digest] = m + e.mu.Unlock() + if m.ready() { + e.queue.Add(m.digest) + } + case watch.Deleted: + e.mu.Lock() + if cur, ok := e.sbomByImage[m.digest]; ok && cur.name == m.name && cur.namespace == m.namespace { + delete(e.sbomByImage, m.digest) + } + e.mu.Unlock() + } +} + +// Start exports until ctx is done: one worker drains the queue, and a resync +// runs now and every ResyncInterval. +func (e *Exporter) Start(ctx context.Context) { + go func() { + <-ctx.Done() + e.queue.ShutDown() + }() + go e.work(ctx) + + logger.L().Info("starting SBOM export to Dependency-Track", + helpers.String("url", e.opts.URL), + helpers.String("rootProject", e.opts.RootProject), + helpers.String("resyncInterval", e.opts.ResyncInterval.String())) + + ticker := time.NewTicker(e.opts.ResyncInterval) + defer ticker.Stop() + for { + if err := e.resync(ctx); err != nil && ctx.Err() == nil { + logger.L().Ctx(ctx).Error("SBOM export resync failed, retrying at the next one", helpers.Error(err)) + } + select { + case <-ctx.Done(): + return + case <-ticker.C: + } + } +} + +func (e *Exporter) work(ctx context.Context) { + for { + digest, shutdown := e.queue.Get() + if shutdown { + return + } + err := e.process(ctx, digest) + switch { + case err == nil: + e.queue.Forget(digest) + case e.queue.NumRequeues(digest) < maxRetries && ctx.Err() == nil: + e.queue.AddRateLimited(digest) + default: + e.queue.Forget(digest) + logger.L().Ctx(ctx).Warning("giving up on exporting this SBOM until the next resync", + helpers.String("digest", digest), helpers.Error(err)) + } + e.queue.Done(digest) + } +} + +// resync rebuilds the running index from one paged list of running pods and +// the SBOM metadata from one list of SBOMSyft objects, then queues every +// running image. If either list fails, nothing changes. +func (e *Exporter) resync(ctx context.Context) error { + logger.L().Info("SBOM export since the last resync", e.stats.takeFields()...) + + pods := map[string]podEntry{} + err := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + return e.pods.CoreV1().Pods("").List(ctx, opts) + }).EachListItem(ctx, metav1.ListOptions{FieldSelector: "status.phase=Running"}, func(obj runtime.Object) error { + pod := obj.(*corev1.Pod) + if e.opts.SkipNamespace(pod.Namespace) { + return nil + } + if digests := runningDigests(pod); len(digests) > 0 { + pods[pod.Namespace+"/"+pod.Name] = podEntry{namespace: pod.Namespace, digests: digests} + } + return nil + }) + if err != nil { + return fmt.Errorf("list running pods: %w", err) + } + sboms, err := e.sboms.listSBOMs(ctx) + if err != nil { + return fmt.Errorf("list SBOMSyft objects: %w", err) + } + + e.running.replace(pods) + byImage := make(map[string]sbomMeta, len(sboms)) + for _, m := range sboms { + byImage[m.digest] = m + } + e.mu.Lock() + e.sbomByImage = byImage + e.collections = map[string]string{} + e.mu.Unlock() + + for _, d := range e.running.digests() { + e.queue.Add(d) + } + return nil +} + +func (e *Exporter) sbom(digest string) (sbomMeta, bool) { + e.mu.Lock() + defer e.mu.Unlock() + m, ok := e.sbomByImage[digest] + return m, ok +} + +// process exports the SBOM of one image to the project of every namespace it +// runs in. One namespace failing does not stop the others; the image is +// retried as a whole, and namespaces already done are then only looked up. +func (e *Exporter) process(ctx context.Context, digest string) error { + namespaces := e.running.namespaces(digest) + if len(namespaces) == 0 { + return nil + } + m, ok := e.sbom(digest) + if !ok || !m.ready() { + e.stats.skipped.Add(1) + return nil + } + + root, err := e.collection(ctx, e.opts.RootProject, "", clusterLogic) + if err != nil { + e.stats.failed.Add(1) + e.logFailure(ctx, err, m.imageTag, e.opts.RootProject, collectionVersion) + return err + } + repository, version := splitRef(m.imageTag) + var bom []byte // converted once, for every namespace + var errs []error + for _, ns := range namespaces { + nsProject := e.opts.RootProject + "/" + ns + name := nsProject + "/" + repository + sent, err := e.exportTo(ctx, m, name, version, nsProject, root, &bom) + switch { + case err != nil: + e.stats.failed.Add(1) + e.logFailure(ctx, err, m.imageTag, name, version) + errs = append(errs, err) + case sent: + e.stats.sent.Add(1) + logger.L().Info("sent SBOM to Dependency-Track", helpers.String("project", name), helpers.String("version", version)) + default: + e.stats.unchanged.Add(1) + } + } + return errors.Join(errs...) +} + +// exportTo makes name:version hold m's SBOM as its latest version. It reports +// whether it uploaded: an image whose project imported a BOM no older than the +// SBOMSyft is only marked latest. +func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, name, version, nsProject, rootUUID string, bom *[]byte) (bool, error) { + p, err := e.dt.lookup(ctx, name, version) + if err != nil { + return false, err + } + if p != nil && !p.LastBOMImport.Time().Before(m.created) { + if !p.IsLatest { + if err := e.dt.patch(ctx, p.UUID, map[string]any{"isLatest": true}); err != nil { + return false, err + } + } + return false, nil + } + nsUUID, err := e.collection(ctx, nsProject, rootUUID, namespaceLogic) + if err != nil { + return false, err + } + if *bom == nil { + doc, err := e.sboms.syftDocument(ctx, m.namespace, m.name) + if err != nil { + return false, err + } + if *bom, err = toCycloneDX(doc); err != nil { + return false, fmt.Errorf("convert SBOMSyft %s/%s: %w", m.namespace, m.name, err) + } + } + return true, e.dt.uploadBOM(ctx, name, version, nsUUID, *bom) +} + +// collection returns the uuid of the collection project name:current, +// creating it under parentUUID, or setting its collection logic, when needed. +func (e *Exporter) collection(ctx context.Context, name, parentUUID, logic string) (string, error) { + e.mu.Lock() + uuid, ok := e.collections[name] + e.mu.Unlock() + if ok { + return uuid, nil + } + + p, err := e.dt.lookup(ctx, name, collectionVersion) + if err != nil { + return "", err + } + switch { + case p == nil: + if p, err = e.dt.createCollection(ctx, name, collectionVersion, parentUUID, logic); err != nil { + return "", err + } + logger.L().Info("created Dependency-Track collection project", helpers.String("project", name), helpers.String("collectionLogic", logic)) + case p.CollectionLogic != logic: + if err := e.dt.patch(ctx, p.UUID, map[string]any{"collectionLogic": logic}); err != nil { + return "", err + } + logger.L().Info("set collection logic of Dependency-Track project", helpers.String("project", name), helpers.String("collectionLogic", logic)) + } + + e.mu.Lock() + e.collections[name] = p.UUID + e.mu.Unlock() + return p.UUID, nil +} + +// logFailure logs a failed export of image, at project name:version. A refused +// API key fails every image the same way, so it is logged once per resync. +func (e *Exporter) logFailure(ctx context.Context, err error, image, name, version string) { + fields := []helpers.IDetails{helpers.String("image", image), helpers.String("project", name), helpers.String("version", version), helpers.Error(err)} + if isAuthError(err) { + if e.stats.authLogged.Swap(true) { + return + } + fields = append(fields, helpers.String("hint", "the API key's team needs BOM_UPLOAD, PROJECT_CREATION_UPLOAD, VIEW_PORTFOLIO and PORTFOLIO_MANAGEMENT (on 5.x: PORTFOLIO_MANAGEMENT_CREATE and PORTFOLIO_MANAGEMENT_UPDATE)")) + } + logger.L().Ctx(ctx).Error("failed to export SBOM to Dependency-Track", fields...) +} + +// stats counts what the exporter did between two resyncs. +type stats struct { + sent, unchanged, skipped, failed atomic.Int64 + authLogged atomic.Bool // a refused API key was logged +} + +// takeFields returns the counts as log fields and resets them. +func (s *stats) takeFields() []helpers.IDetails { + s.authLogged.Store(false) + return []helpers.IDetails{ + helpers.Int("sent", int(s.sent.Swap(0))), + helpers.Int("unchanged", int(s.unchanged.Swap(0))), + helpers.Int("skippedNoReadySBOM", int(s.skipped.Swap(0))), + helpers.Int("failed", int(s.failed.Swap(0))), + } +} diff --git a/sbomexport/exporter_test.go b/sbomexport/exporter_test.go new file mode 100644 index 00000000..cec5e761 --- /dev/null +++ b/sbomexport/exporter_test.go @@ -0,0 +1,322 @@ +package sbomexport + +import ( + "context" + "errors" + "os" + "slices" + "sync" + "testing" + "time" + + helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + k8sfake "k8s.io/client-go/kubernetes/fake" + ktesting "k8s.io/client-go/testing" +) + +const ( + redisImageID = "docker.io/library/redis@sha256:1111" + redisTag = "docker.io/library/redis:8" + redisProject = "docker.io/library/redis" +) + +// fakeStore serves SBOMSyft objects from memory; every document is the curl +// fixture. +type fakeStore struct { + mu sync.Mutex + sboms []*spdxv1beta1.SBOMSyft + gets int +} + +func (s *fakeStore) listSBOMs(context.Context) ([]sbomMeta, error) { + s.mu.Lock() + defer s.mu.Unlock() + var out []sbomMeta + for _, obj := range s.sboms { + if m, ok := metaOf(obj); ok { + out = append(out, m) + } + } + return out, nil +} + +func (s *fakeStore) syftDocument(context.Context, string, string) ([]byte, error) { + s.mu.Lock() + defer s.mu.Unlock() + s.gets++ + return curlSyftJSON, nil +} + +func (s *fakeStore) set(sboms ...*spdxv1beta1.SBOMSyft) { + s.mu.Lock() + defer s.mu.Unlock() + s.sboms = sboms +} + +func (s *fakeStore) documentReads() int { + s.mu.Lock() + defer s.mu.Unlock() + return s.gets +} + +type testExporter struct { + *Exporter + dt *fakeDT + store *fakeStore + clock *fakeClock + client *k8sfake.Clientset +} + +// newTestExporter returns an Exporter with root project "cluster", a one-hour +// resync and kube-system excluded, over a fake Dependency-Track, a fake SBOM +// store and a fake clientset holding pods. +func newTestExporter(t *testing.T, pods ...runtime.Object) *testExporter { + t.Helper() + clock := &fakeClock{t: time.Date(2026, 11, 10, 12, 0, 0, 0, time.UTC)} + dt, srv, keyFile := newFakeDT(t, clock.now) + store := &fakeStore{} + client := k8sfake.NewClientset(pods...) + e, err := newExporter(Options{ + URL: srv.URL, + APIKeyFile: keyFile, + RootProject: "cluster", + ResyncInterval: time.Hour, + SkipNamespace: func(ns string) bool { return ns == "kube-system" }, + }, client, store, srv.Client(), clock.now) + require.NoError(t, err) + t.Cleanup(e.queue.ShutDown) + return &testExporter{Exporter: e, dt: dt, store: store, clock: clock, client: client} +} + +// drain processes every queued image once, as Start's worker would, and +// returns the errors. +func (te *testExporter) drain() []error { + var errs []error + for te.queue.Len() > 0 { + digest, _ := te.queue.Get() + if err := te.process(context.Background(), digest); err != nil { + errs = append(errs, err) + } + te.queue.Forget(digest) + te.queue.Done(digest) + } + return errs +} + +func runningPod(namespace, name, imageID string) *corev1.Pod { + return &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{{Name: "main", ImageID: imageID}}, + }, + } +} + +func sbomObject(imageID, imageTag, status string, created time.Time) *spdxv1beta1.SBOMSyft { + return &spdxv1beta1.SBOMSyft{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "kubescape", + Name: "sbom-" + digestOf(imageID), + CreationTimestamp: metav1.NewTime(created), + Annotations: map[string]string{ + helpersv1.ImageIDMetadataKey: imageID, + helpersv1.ImageTagMetadataKey: imageTag, + helpersv1.StatusMetadataKey: status, + }, + }, + } +} + +func TestNewRejectsAnInvalidConfiguration(t *testing.T) { + valid := Options{URL: "https://dtrack.example.com", APIKeyFile: "/etc/sbom-export/api-key", RootProject: "cluster", ResyncInterval: time.Hour} + tt := []struct { + name string + change func(*Options) + }{ + {"no URL", func(o *Options) { o.URL = "" }}, + {"a URL without a scheme", func(o *Options) { o.URL = "dtrack.example.com" }}, + {"no API key file", func(o *Options) { o.APIKeyFile = "" }}, + {"no root project", func(o *Options) { o.RootProject = "" }}, + {"no resync interval", func(o *Options) { o.ResyncInterval = 0 }}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + opts := valid + tc.change(&opts) + _, err := New(opts, k8sfake.NewClientset(), nil) + assert.Error(t, err) + }) + } + _, err := New(valid, k8sfake.NewClientset(), nil) + assert.NoError(t, err) +} + +func TestExporterSendsAnImageToTheProjectOfEveryNamespaceItRunsIn(t *testing.T) { + te := newTestExporter(t, + runningPod("apps", "web", redisImageID), + runningPod("jobs", "worker", redisImageID), + runningPod("kube-system", "cache", redisImageID), + ) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.ElementsMatch(t, []string{ + "cluster/apps/" + redisProject + ":8", + "cluster/jobs/" + redisProject + ":8", + }, te.dt.takeUploads(), "nothing for the excluded kube-system") + assert.Equal(t, 1, te.store.documentReads(), "converted once for both namespaces") + + image, parent, _ := te.dt.project("cluster/apps/"+redisProject, "8") + assert.Equal(t, "cluster/apps", parent) + assert.True(t, image.IsLatest) + namespace, parent, _ := te.dt.project("cluster/apps", collectionVersion) + assert.Equal(t, "cluster", parent) + assert.Equal(t, namespaceLogic, namespace.CollectionLogic) + root, parent, _ := te.dt.project("cluster", collectionVersion) + assert.Empty(t, parent) + assert.Equal(t, clusterLogic, root.CollectionLogic) +} + +func TestExporterSendsAnSBOMAgainOnlyWhenItIsNewerThanTheLastImport(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + te.dt.takeUploads() + + te.clock.t = te.clock.t.Add(time.Hour) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Empty(t, te.dt.takeUploads(), "unchanged") + + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Minute))) + te.clock.t = te.clock.t.Add(time.Hour) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Equal(t, []string{"cluster/apps/" + redisProject + ":8"}, te.dt.takeUploads(), "the SBOMSyft is newer than the last import") +} + +func TestExporterMarksTheRunningTagAsLatestWithoutSendingIt(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + te.dt.takeUploads() + + // another version of the image became the latest one + p, _, _ := te.dt.project("cluster/apps/"+redisProject, "8") + require.NoError(t, te.dt.patchForTest(p.UUID, false)) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Empty(t, te.dt.takeUploads()) + p, _, _ = te.dt.project("cluster/apps/"+redisProject, "8") + assert.True(t, p.IsLatest) +} + +func TestExporterWaitsForTheSBOMToBeReady(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Initializing, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Empty(t, te.dt.takeUploads()) + + te.ObserveSBOM(watch.Modified, sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.Empty(t, te.drain()) + assert.Equal(t, []string{"cluster/apps/" + redisProject + ":8"}, te.dt.takeUploads()) +} + +func TestExporterSendsTheImageOfAJobThatEndedBeforeItsSBOMWasReady(t *testing.T) { + te := newTestExporter(t) + job := runningPod("jobs", "migrate-28937", redisImageID) + + te.ObservePod(watch.Added, job) + te.ObservePod(watch.Deleted, job) + te.clock.t = te.clock.t.Add(10 * time.Minute) + te.ObserveSBOM(watch.Added, sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t)) + require.Empty(t, te.drain()) + assert.Equal(t, []string{"cluster/jobs/" + redisProject + ":8"}, te.dt.takeUploads()) + + te.clock.t = te.clock.t.Add(time.Hour) + require.NoError(t, te.resync(context.Background())) + assert.Zero(t, te.queue.Len(), "the grace period is over") +} + +func TestExporterRetriesWhatDependencyTrackRefused(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + te.dt.failNext(1) + + assert.Len(t, te.drain(), 1) + assert.Empty(t, te.dt.takeUploads()) + + te.queue.Add(digestOf(redisImageID)) + require.Empty(t, te.drain()) + assert.Equal(t, []string{"cluster/apps/" + redisProject + ":8"}, te.dt.takeUploads()) +} + +func TestExporterResyncKeepsWhatItKnewWhenThePodListFails(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + require.NoError(t, te.resync(context.Background())) + listErr := errors.New("pods are unavailable") + te.client.PrependReactor("list", "pods", func(ktesting.Action) (bool, runtime.Object, error) { + return true, nil, listErr + }) + + assert.ErrorIs(t, te.resync(context.Background()), listErr) + assert.Equal(t, []string{"apps"}, te.running.namespaces(digestOf(redisImageID))) +} + +func TestStartExportsUntilTheContextEnds(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + ctx, cancel := context.WithCancel(context.Background()) + done := make(chan struct{}) + go func() { + defer close(done) + te.Start(ctx) + }() + + var uploads []string + assert.Eventually(t, func() bool { + uploads = append(uploads, te.dt.takeUploads()...) + return slices.Contains(uploads, "cluster/apps/"+redisProject+":8") + }, 10*time.Second, 50*time.Millisecond) + + cancel() + select { + case <-done: + case <-time.After(10 * time.Second): + t.Fatal("Start did not return after the context ended") + } +} + +func TestExporterCountsARefusedAPIKeyAsAFailureAndLogsItOncePerResync(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID), runningPod("apps", "api", "docker.io/library/nginx@sha256:2222")) + te.store.set( + sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour)), + sbomObject("docker.io/library/nginx@sha256:2222", "docker.io/library/nginx:1.27", helpersv1.Learning, te.clock.t.Add(-time.Hour)), + ) + require.NoError(t, os.WriteFile(te.dt.keyFile, []byte("revoked-key"), 0600)) + require.NoError(t, te.resync(context.Background())) + + errs := te.drain() + require.Len(t, errs, 2) + assert.True(t, isAuthError(errs[0])) + assert.Equal(t, int64(2), te.stats.failed.Load()) + assert.True(t, te.stats.authLogged.Load()) + + te.stats.takeFields() + assert.False(t, te.stats.authLogged.Load(), "the next resync logs it again") +} diff --git a/sbomexport/fakedtrack_test.go b/sbomexport/fakedtrack_test.go new file mode 100644 index 00000000..2c7ab5bf --- /dev/null +++ b/sbomexport/fakedtrack_test.go @@ -0,0 +1,189 @@ +package sbomexport + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync" + "testing" + "time" +) + +const testAPIKey = "test-api-key" + +// fakeDT is an in-memory Dependency-Track that serves the calls dtClient +// makes: project lookup, create, patch, and BOM upload with autoCreate. +type fakeDT struct { + mu sync.Mutex + now func() time.Time + projects map[string]*project // "name:version" → project + parents map[string]string // project uuid → parent uuid + uploads []string // "name:version" of every BOM upload + nextID int + fail int // answer the next fail requests with 503 + keyFile string // holds testAPIKey +} + +// newFakeDT starts a fakeDT and returns it with an API key file holding +// testAPIKey. +func newFakeDT(t *testing.T, now func() time.Time) (*fakeDT, *httptest.Server, string) { + t.Helper() + keyFile := filepath.Join(t.TempDir(), "api-key") + if err := os.WriteFile(keyFile, []byte(testAPIKey+"\n"), 0600); err != nil { + t.Fatal(err) + } + f := &fakeDT{now: now, projects: map[string]*project{}, parents: map[string]string{}, keyFile: keyFile} + srv := httptest.NewServer(f) + t.Cleanup(srv.Close) + return f, srv, keyFile +} + +func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { + f.mu.Lock() + defer f.mu.Unlock() + if r.Header.Get("X-Api-Key") != testAPIKey { + http.Error(w, "The API key could not be validated", http.StatusUnauthorized) + return + } + if f.fail > 0 { + f.fail-- + http.Error(w, "unavailable", http.StatusServiceUnavailable) + return + } + switch { + case r.Method == http.MethodGet && r.URL.Path == "/api/v1/project/lookup": + p, ok := f.projects[r.URL.Query().Get("name")+":"+r.URL.Query().Get("version")] + if !ok { + http.Error(w, "The project could not be found.", http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, p) + case r.Method == http.MethodPut && r.URL.Path == "/api/v1/project": + var req struct { + Name, Version, CollectionLogic string + Parent *struct{ UUID string } + } + _ = json.NewDecoder(r.Body).Decode(&req) + parent := "" + if req.Parent != nil { + parent = req.Parent.UUID + } + p := f.addLocked(req.Name, req.Version, parent) + p.CollectionLogic = req.CollectionLogic + writeJSON(w, http.StatusCreated, p) + case r.Method == http.MethodPatch && strings.HasPrefix(r.URL.Path, "/api/v1/project/"): + p := f.byUUIDLocked(strings.TrimPrefix(r.URL.Path, "/api/v1/project/")) + if p == nil { + http.Error(w, "The project could not be found.", http.StatusNotFound) + return + } + var req map[string]any + _ = json.NewDecoder(r.Body).Decode(&req) + if v, ok := req["isLatest"].(bool); ok { + p.IsLatest = v + } + if v, ok := req["collectionLogic"].(string); ok { + p.CollectionLogic = v + } + writeJSON(w, http.StatusOK, p) + case r.Method == http.MethodPut && r.URL.Path == "/api/v1/bom": + var req struct { + ProjectName, ProjectVersion, ParentUUID, BOM string + AutoCreate, IsLatest bool + } + _ = json.NewDecoder(r.Body).Decode(&req) + bom, err := base64.StdEncoding.DecodeString(req.BOM) + if err != nil || !json.Valid(bom) || !req.AutoCreate { + http.Error(w, "bad BOM upload", http.StatusBadRequest) + return + } + key := req.ProjectName + ":" + req.ProjectVersion + p, ok := f.projects[key] + if !ok { + p = f.addLocked(req.ProjectName, req.ProjectVersion, req.ParentUUID) + } + if req.IsLatest { + for _, other := range f.projects { + if other.Name == p.Name { + other.IsLatest = false + } + } + p.IsLatest = true + } + p.LastBOMImport = epochMillis(f.now().UnixMilli()) + f.uploads = append(f.uploads, key) + writeJSON(w, http.StatusOK, map[string]string{"token": fmt.Sprintf("token-%d", len(f.uploads))}) + default: + http.NotFound(w, r) + } +} + +func (f *fakeDT) addLocked(name, version, parentUUID string) *project { + f.nextID++ + p := &project{UUID: fmt.Sprintf("uuid-%d", f.nextID), Name: name, Version: version} + f.projects[name+":"+version] = p + f.parents[p.UUID] = parentUUID + return p +} + +func (f *fakeDT) byUUIDLocked(uuid string) *project { + for _, p := range f.projects { + if p.UUID == uuid { + return p + } + } + return nil +} + +// project returns a copy of name:version, and the name of its parent. +func (f *fakeDT) project(name, version string) (project, string, bool) { + f.mu.Lock() + defer f.mu.Unlock() + p, ok := f.projects[name+":"+version] + if !ok { + return project{}, "", false + } + parent := "" + if pp := f.byUUIDLocked(f.parents[p.UUID]); pp != nil { + parent = pp.Name + } + return *p, parent, true +} + +// failNext makes the next n requests fail with 503. +func (f *fakeDT) failNext(n int) { + f.mu.Lock() + defer f.mu.Unlock() + f.fail = n +} + +// patchForTest sets isLatest of the project with uuid, as another upload would. +func (f *fakeDT) patchForTest(uuid string, isLatest bool) error { + f.mu.Lock() + defer f.mu.Unlock() + p := f.byUUIDLocked(uuid) + if p == nil { + return fmt.Errorf("no project %s", uuid) + } + p.IsLatest = isLatest + return nil +} + +func (f *fakeDT) takeUploads() []string { + f.mu.Lock() + defer f.mu.Unlock() + u := f.uploads + f.uploads = nil + return u +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) +} diff --git a/sbomexport/image.go b/sbomexport/image.go new file mode 100644 index 00000000..f4c6a321 --- /dev/null +++ b/sbomexport/image.go @@ -0,0 +1,57 @@ +package sbomexport + +import ( + "strings" + + corev1 "k8s.io/api/core/v1" +) + +// digestOf returns the digest of an image ID, such as "sha256:abc" for +// "docker.io/library/redis@sha256:abc". An ID without "@" is returned as is. +// Pod container statuses and the kubescape.io/image-id annotation of an +// SBOMSyft both go through it, so the two sides compare equal. +func digestOf(imageID string) string { + if i := strings.LastIndex(imageID, "@"); i >= 0 { + return imageID[i+1:] + } + return imageID +} + +// splitRef splits an image reference into the repository and the version its +// Dependency-Track project gets: the tag; without a tag, the first 12 +// characters of the digest; with neither, "latest". +// +// docker.io/library/redis:8-alpine → docker.io/library/redis, 8-alpine +// docker.io/library/redis@sha256:0123… → docker.io/library/redis, 0123456789ab +// localhost:5000/app → localhost:5000/app, latest +func splitRef(ref string) (repository, version string) { + base, digest, hasDigest := strings.Cut(ref, "@") + if i := strings.LastIndex(base, ":"); i > strings.LastIndex(base, "/") { + return base[:i], base[i+1:] + } + if hasDigest { + hex := digest[strings.LastIndex(digest, ":")+1:] + return base, hex[:min(12, len(hex))] + } + return base, "latest" +} + +// runningDigests returns the image digests of the containers that run in pod: +// its containers, and its init containers that are running (sidecars). +func runningDigests(pod *corev1.Pod) []string { + if pod.Status.Phase != corev1.PodRunning { + return nil + } + var digests []string + for _, cs := range pod.Status.ContainerStatuses { + if cs.ImageID != "" { + digests = append(digests, digestOf(cs.ImageID)) + } + } + for _, cs := range pod.Status.InitContainerStatuses { + if cs.ImageID != "" && cs.State.Running != nil { + digests = append(digests, digestOf(cs.ImageID)) + } + } + return digests +} diff --git a/sbomexport/image_test.go b/sbomexport/image_test.go new file mode 100644 index 00000000..ca996b4c --- /dev/null +++ b/sbomexport/image_test.go @@ -0,0 +1,74 @@ +package sbomexport + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" +) + +func TestSplitRef(t *testing.T) { + tt := []struct { + ref string + repository string + version string + }{ + {"docker.io/library/redis:8-alpine", "docker.io/library/redis", "8-alpine"}, + {"docker.io/library/redis@sha256:0123456789abcdef", "docker.io/library/redis", "0123456789ab"}, + {"docker.io/library/redis:8-alpine@sha256:0123456789abcdef", "docker.io/library/redis", "8-alpine"}, + {"localhost:5000/app", "localhost:5000/app", "latest"}, + {"localhost:5000/app:1.2", "localhost:5000/app", "1.2"}, + {"localhost:5000/app@sha256:abc", "localhost:5000/app", "abc"}, + {"nginx", "nginx", "latest"}, + } + for _, tc := range tt { + t.Run(tc.ref, func(t *testing.T) { + repository, version := splitRef(tc.ref) + assert.Equal(t, tc.repository, repository) + assert.Equal(t, tc.version, version) + }) + } +} + +func TestDigestOf(t *testing.T) { + assert.Equal(t, "sha256:abc", digestOf("docker.io/library/redis@sha256:abc")) + assert.Equal(t, "sha256:abc", digestOf("sha256:abc")) +} + +func TestRunningDigests(t *testing.T) { + running := corev1.ContainerState{Running: &corev1.ContainerStateRunning{}} + terminated := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}} + tt := []struct { + name string + pod corev1.PodStatus + want []string + }{ + { + name: "containers and running init containers (sidecars) count", + pod: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{ + {Name: "app", ImageID: "docker.io/library/app@sha256:app"}, + {Name: "starting"}, // no image ID yet + }, + InitContainerStatuses: []corev1.ContainerStatus{ + {Name: "sidecar", ImageID: "docker.io/library/proxy@sha256:proxy", State: running}, + {Name: "migrate", ImageID: "docker.io/library/migrate@sha256:migrate", State: terminated}, + }, + }, + want: []string{"sha256:app", "sha256:proxy"}, + }, + { + name: "a pod that is not running runs nothing", + pod: corev1.PodStatus{ + Phase: corev1.PodSucceeded, + ContainerStatuses: []corev1.ContainerStatus{{Name: "app", ImageID: "docker.io/library/app@sha256:app"}}, + }, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.want, runningDigests(&corev1.Pod{Status: tc.pod})) + }) + } +} diff --git a/sbomexport/index.go b/sbomexport/index.go new file mode 100644 index 00000000..a9515efb --- /dev/null +++ b/sbomexport/index.go @@ -0,0 +1,181 @@ +package sbomexport + +import ( + "slices" + "sync" + "time" +) + +// podEntry is what the index keeps of one running pod. +type podEntry struct { + namespace string + digests []string +} + +// index records, per image digest, the namespaces where a pod runs that +// image. A namespace stays listed for one grace period after its last such pod +// ended, so the image of a short Job is still exported once its SBOM is ready, +// which can take longer than the Job runs. +// +// It is safe for concurrent use. +type index struct { + mu sync.Mutex + grace time.Duration + now func() time.Time + + pods map[string]podEntry // pod key → what it runs + refs map[string]map[string]int // digest → namespace → running pods + ended map[string]map[string]time.Time // digest → namespace → when its last pod ended +} + +func newIndex(grace time.Duration, now func() time.Time) *index { + return &index{ + grace: grace, + now: now, + pods: map[string]podEntry{}, + refs: map[string]map[string]int{}, + ended: map[string]map[string]time.Time{}, + } +} + +// setPod records that the pod with key runs digests in namespace, replacing +// what was recorded for it before. It returns the digests that now run in a +// namespace where they did not run before. +func (x *index) setPod(key, namespace string, digests []string) []string { + x.mu.Lock() + defer x.mu.Unlock() + x.removeLocked(key) + if len(digests) == 0 { + return nil + } + digests = slices.Compact(slices.Sorted(slices.Values(digests))) + x.pods[key] = podEntry{namespace: namespace, digests: digests} + var started []string + for _, d := range digests { + if x.incLocked(d, namespace) { + started = append(started, d) + } + } + return started +} + +// removePod forgets the pod with key. +func (x *index) removePod(key string) { + x.mu.Lock() + defer x.mu.Unlock() + x.removeLocked(key) +} + +// replace rebuilds the index from a full list of running pods. An image that +// no longer runs in a namespace starts its grace period there. +func (x *index) replace(pods map[string]podEntry) { + x.mu.Lock() + defer x.mu.Unlock() + for key := range x.pods { + if _, ok := pods[key]; !ok { + x.removeLocked(key) + } + } + for key, p := range pods { + x.removeLocked(key) + x.pods[key] = p + for _, d := range p.digests { + x.incLocked(d, p.namespace) + } + } +} + +// namespaces returns, sorted, the namespaces where digest runs or ran less +// than a grace period ago. +func (x *index) namespaces(digest string) []string { + x.mu.Lock() + defer x.mu.Unlock() + x.pruneLocked() + var out []string + for ns := range x.refs[digest] { + out = append(out, ns) + } + for ns := range x.ended[digest] { + out = append(out, ns) + } + slices.Sort(out) + return out +} + +// digests returns every digest that namespaces would return a namespace for. +func (x *index) digests() []string { + x.mu.Lock() + defer x.mu.Unlock() + x.pruneLocked() + var out []string + for d := range x.refs { + out = append(out, d) + } + for d := range x.ended { + if _, running := x.refs[d]; !running { + out = append(out, d) + } + } + slices.Sort(out) + return out +} + +func (x *index) removeLocked(key string) { + p, ok := x.pods[key] + if !ok { + return + } + delete(x.pods, key) + for _, d := range p.digests { + x.decLocked(d, p.namespace) + } +} + +// incLocked counts one more pod running digest in ns, and reports whether it +// is the first. +func (x *index) incLocked(digest, ns string) bool { + if x.refs[digest] == nil { + x.refs[digest] = map[string]int{} + } + x.refs[digest][ns]++ + if e := x.ended[digest]; e != nil { + delete(e, ns) + if len(e) == 0 { + delete(x.ended, digest) + } + } + return x.refs[digest][ns] == 1 +} + +func (x *index) decLocked(digest, ns string) { + r := x.refs[digest] + if r == nil || r[ns] == 0 { + return + } + r[ns]-- + if r[ns] > 0 { + return + } + delete(r, ns) + if len(r) == 0 { + delete(x.refs, digest) + } + if x.ended[digest] == nil { + x.ended[digest] = map[string]time.Time{} + } + x.ended[digest][ns] = x.now() +} + +func (x *index) pruneLocked() { + now := x.now() + for d, e := range x.ended { + for ns, at := range e { + if now.Sub(at) >= x.grace { + delete(e, ns) + } + } + if len(e) == 0 { + delete(x.ended, d) + } + } +} diff --git a/sbomexport/index_test.go b/sbomexport/index_test.go new file mode 100644 index 00000000..ae24eb90 --- /dev/null +++ b/sbomexport/index_test.go @@ -0,0 +1,84 @@ +package sbomexport + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// fakeClock is a time source a test moves by hand. +type fakeClock struct{ t time.Time } + +func (c *fakeClock) now() time.Time { return c.t } + +func newTestIndex() (*index, *fakeClock) { + clock := &fakeClock{t: time.Date(2026, 11, 10, 12, 0, 0, 0, time.UTC)} + return newIndex(time.Hour, clock.now), clock +} + +func TestIndexListsEveryNamespaceAnImageRunsIn(t *testing.T) { + x, _ := newTestIndex() + + assert.Equal(t, []string{"sha256:a"}, x.setPod("apps/web", "apps", []string{"sha256:a"})) + assert.Equal(t, []string{"sha256:a", "sha256:b"}, x.setPod("jobs/batch", "jobs", []string{"sha256:b", "sha256:a", "sha256:a"})) + assert.Empty(t, x.setPod("apps/web-2", "apps", []string{"sha256:a"}), "already runs in apps") + + assert.Equal(t, []string{"apps", "jobs"}, x.namespaces("sha256:a")) + assert.Equal(t, []string{"jobs"}, x.namespaces("sha256:b")) + assert.Equal(t, []string{"sha256:a", "sha256:b"}, x.digests()) +} + +func TestIndexKeepsANamespaceForOneGracePeriodAfterItsLastPod(t *testing.T) { + x, clock := newTestIndex() + x.setPod("apps/web", "apps", []string{"sha256:a"}) + x.setPod("apps/web-2", "apps", []string{"sha256:a"}) + + x.removePod("apps/web") + clock.t = clock.t.Add(2 * time.Hour) + assert.Equal(t, []string{"apps"}, x.namespaces("sha256:a"), "web-2 still runs") + + x.removePod("apps/web-2") + clock.t = clock.t.Add(59 * time.Minute) + assert.Equal(t, []string{"apps"}, x.namespaces("sha256:a"), "inside the grace period") + assert.Equal(t, []string{"sha256:a"}, x.digests()) + + clock.t = clock.t.Add(time.Minute) + assert.Empty(t, x.namespaces("sha256:a")) + assert.Empty(t, x.digests()) +} + +func TestIndexForgetsTheEndOfAnImageThatRunsAgain(t *testing.T) { + x, clock := newTestIndex() + x.setPod("apps/web", "apps", []string{"sha256:a"}) + x.removePod("apps/web") + + assert.Equal(t, []string{"sha256:a"}, x.setPod("apps/web", "apps", []string{"sha256:a"}), "runs again") + clock.t = clock.t.Add(2 * time.Hour) + assert.Equal(t, []string{"apps"}, x.namespaces("sha256:a")) +} + +func TestIndexMovesAPodToItsNewImage(t *testing.T) { + x, _ := newTestIndex() + x.setPod("apps/web", "apps", []string{"sha256:old"}) + + assert.Equal(t, []string{"sha256:new"}, x.setPod("apps/web", "apps", []string{"sha256:new"})) + assert.Equal(t, []string{"apps"}, x.namespaces("sha256:old"), "the old image is in its grace period") + assert.Equal(t, []string{"apps"}, x.namespaces("sha256:new")) +} + +func TestIndexReplaceStartsTheGracePeriodOfWhatStopped(t *testing.T) { + x, clock := newTestIndex() + x.setPod("apps/web", "apps", []string{"sha256:a"}) + x.setPod("jobs/batch", "jobs", []string{"sha256:b"}) + + x.replace(map[string]podEntry{ + "apps/web": {namespace: "apps", digests: []string{"sha256:a"}}, + "apps/api": {namespace: "apps", digests: []string{"sha256:c"}}, + }) + assert.Equal(t, []string{"sha256:a", "sha256:b", "sha256:c"}, x.digests()) + + clock.t = clock.t.Add(time.Hour) + assert.Equal(t, []string{"sha256:a", "sha256:c"}, x.digests(), "batch stopped an hour ago") + assert.Equal(t, []string{"apps"}, x.namespaces("sha256:a")) +} diff --git a/sbomexport/store.go b/sbomexport/store.go new file mode 100644 index 00000000..4998fab1 --- /dev/null +++ b/sbomexport/store.go @@ -0,0 +1,105 @@ +package sbomexport + +import ( + "context" + "encoding/json" + "fmt" + "time" + + helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + kssc "github.com/kubescape/storage/pkg/generated/clientset/versioned" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/pager" +) + +// sbomMeta is what the exporter needs to know about an SBOMSyft without its +// document. +type sbomMeta struct { + namespace, name string + digest string + imageTag string + status string + created time.Time +} + +// ready reports whether the SBOM is complete: the statuses the operator's +// image scans accept too (empty for SBOMs written before the annotation). +func (m sbomMeta) ready() bool { + switch m.status { + case "", helpersv1.Learning, helpersv1.Completed: + return true + } + return false +} + +// metaOf reads sbomMeta from an SBOMSyft's metadata. It reports false when +// the object names no image. +func metaOf(obj *spdxv1beta1.SBOMSyft) (sbomMeta, bool) { + ann := obj.GetAnnotations() + imageID, imageTag := ann[helpersv1.ImageIDMetadataKey], ann[helpersv1.ImageTagMetadataKey] + if imageID == "" || imageTag == "" { + return sbomMeta{}, false + } + return sbomMeta{ + namespace: obj.GetNamespace(), + name: obj.GetName(), + digest: digestOf(imageID), + imageTag: imageTag, + status: ann[helpersv1.StatusMetadataKey], + created: obj.GetCreationTimestamp().Time, + }, true +} + +// sbomStore reads SBOMSyft objects. +type sbomStore interface { + // listSBOMs returns the metadata of every SBOMSyft. + listSBOMs(ctx context.Context) ([]sbomMeta, error) + // syftDocument returns the `.spec.syft` of one SBOMSyft as JSON. + syftDocument(ctx context.Context, namespace, name string) ([]byte, error) +} + +// storageSBOMs reads SBOMSyft objects from Kubescape's storage. +type storageSBOMs struct { + client kssc.Interface +} + +func (s storageSBOMs) listSBOMs(ctx context.Context) ([]sbomMeta, error) { + var out []sbomMeta + err := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { + return s.client.SpdxV1beta1().SBOMSyfts("").List(ctx, opts) + }).EachListItem(ctx, metav1.ListOptions{}, func(obj runtime.Object) error { + if m, ok := metaOf(obj.(*spdxv1beta1.SBOMSyft)); ok { + out = append(out, m) + } + return nil + }) + return out, err +} + +// syftDocument GETs the object as raw JSON rather than through the typed +// client: the document goes to syft's own decoder, and the storage types this +// module builds against may lack fields that a newer node-agent writes. The +// operator's storage client asks for protobuf, so this request asks for JSON. +func (s storageSBOMs) syftDocument(ctx context.Context, namespace, name string) ([]byte, error) { + raw, err := s.client.SpdxV1beta1().RESTClient().Get(). + Namespace(namespace).Resource("sbomsyfts").Name(name). + SetHeader("Accept", "application/json"). + Do(ctx).Raw() + if err != nil { + return nil, fmt.Errorf("get SBOMSyft %s/%s: %w", namespace, name, err) + } + var obj struct { + Spec struct { + Syft json.RawMessage `json:"syft"` + } `json:"spec"` + } + if err := json.Unmarshal(raw, &obj); err != nil { + return nil, fmt.Errorf("decode SBOMSyft %s/%s: %w", namespace, name, err) + } + if len(obj.Spec.Syft) == 0 { + return nil, fmt.Errorf("SBOMSyft %s/%s has no syft document", namespace, name) + } + return obj.Spec.Syft, nil +} diff --git a/sbomexport/store_test.go b/sbomexport/store_test.go new file mode 100644 index 00000000..91256cd3 --- /dev/null +++ b/sbomexport/store_test.go @@ -0,0 +1,48 @@ +package sbomexport + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + kssc "github.com/kubescape/storage/pkg/generated/clientset/versioned" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/client-go/rest" +) + +// The operator's storage client asks for protobuf (main.go). The SBOM document +// is read as JSON for syft's decoder, so that one request must ask for JSON. +func TestStorageSBOMsReadsTheSyftDocumentAsJSONFromAProtobufClient(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/apis/spdx.softwarecomposition.kubescape.io/v1beta1/namespaces/kubescape/sbomsyfts/nginx-sbom" { + http.NotFound(w, r) + return + } + if !strings.Contains(r.Header.Get("Accept"), "application/json") { + w.Header().Set("Content-Type", "application/vnd.kubernetes.protobuf") + _, _ = w.Write([]byte("k8s\x00\x0a\x0f")) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"apiVersion":"spdx.softwarecomposition.kubescape.io/v1beta1","kind":"SBOMSyft","spec":{"syft":` + string(curlSyftJSON) + `}}`)) + })) + defer srv.Close() + client, err := kssc.NewForConfig(&rest.Config{ + Host: srv.URL, + ContentConfig: rest.ContentConfig{ + AcceptContentTypes: "application/vnd.kubernetes.protobuf", + ContentType: "application/vnd.kubernetes.protobuf", + }, + }) + require.NoError(t, err) + + doc, err := storageSBOMs{client: client}.syftDocument(context.Background(), "kubescape", "nginx-sbom") + + require.NoError(t, err) + assert.JSONEq(t, string(curlSyftJSON), string(doc)) + _, err = toCycloneDX(doc) + assert.NoError(t, err) +} diff --git a/sbomexport/testdata/curl-8.11.1.cdx.json b/sbomexport/testdata/curl-8.11.1.cdx.json new file mode 100644 index 00000000..e36e27e5 --- /dev/null +++ b/sbomexport/testdata/curl-8.11.1.cdx.json @@ -0,0 +1 @@ +{"$schema":"http://cyclonedx.org/schema/bom-1.6.schema.json","bomFormat":"CycloneDX","specVersion":"1.6","serialNumber":"urn:uuid:9c3a8bc3-5e1e-4073-8a43-9520535efe88","version":1,"metadata":{"timestamp":"2026-11-10T17:36:49Z","tools":{"components":[{"type":"application","author":"anchore","name":"syft","version":"v1.55.0"}]},"component":{"bom-ref":"d450b75eaf61d760","type":"container","name":"docker.io/curlimages/curl:8.11.1","version":"docker.io/curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69"},"properties":[{"name":"syft:image:labels:docker.cmd","value":"podman run -it quay.io/curl/:8.11.1"},{"name":"syft:image:labels:io.buildah.version","value":"1.23.1"},{"name":"syft:image:labels:maintainer","value":"James Fuller "},{"name":"syft:image:labels:name","value":"curl-linux-amd64:8.11.1"},{"name":"syft:image:labels:org.opencontainers.image.description","value":"minimal image for curl"},{"name":"syft:image:labels:org.opencontainers.image.licenses","value":"MIT"},{"name":"syft:image:labels:org.opencontainers.image.source","value":"https://github.com/curl/curl-container"},{"name":"syft:image:labels:version","value":"8.11.1"}]},"components":[{"bom-ref":"pkg:apk/alpine/alpine-baselayout@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0&package-id=ceeb874c4a135dac","type":"library","name":"alpine-baselayout","version":"3.6.8-r0","licenses":[{"license":{"id":"GPL-2.0-only"}}],"cpe":"cpe:2.3:a:alpine-baselayout:alpine-baselayout:3.6.8-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/alpine-baselayout@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine-baselayout:alpine_baselayout:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_baselayout:alpine-baselayout:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_baselayout:alpine_baselayout:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine-baselayout:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine_baselayout:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"alpine-baselayout"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/alpine-baselayout-data@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0&package-id=1a41f8b73cec7362&upstream=alpine-baselayout","type":"library","name":"alpine-baselayout-data","version":"3.6.8-r0","licenses":[{"license":{"id":"GPL-2.0-only"}}],"cpe":"cpe:2.3:a:alpine-baselayout-data:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/alpine-baselayout-data@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0&upstream=alpine-baselayout","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine-baselayout-data:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_baselayout_data:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_baselayout_data:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine-baselayout:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine-baselayout:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_baselayout:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_baselayout:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"alpine-baselayout"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/alpine-keys@2.5-r0?arch=x86_64&distro=alpine-3.21.0&package-id=f3faf4bc1f174ce9","type":"library","name":"alpine-keys","version":"2.5-r0","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:alpine-keys:alpine-keys:2.5-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/alpine-keys@2.5-r0?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine-keys:alpine_keys:2.5-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_keys:alpine-keys:2.5-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_keys:alpine_keys:2.5-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine-keys:2.5-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine_keys:2.5-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"alpine-keys"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/alpine-release@3.21.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=456e920c4331bde6&upstream=alpine-base","type":"library","name":"alpine-release","version":"3.21.0-r0","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:alpine-release:alpine-release:3.21.0-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/alpine-release@3.21.0-r0?arch=x86_64&distro=alpine-3.21.0&upstream=alpine-base","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine-release:alpine_release:3.21.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_release:alpine-release:3.21.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine_release:alpine_release:3.21.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine-release:3.21.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:alpine:alpine_release:3.21.0-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"alpine-base"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/apk-tools@2.14.6-r2?arch=x86_64&distro=alpine-3.21.0&package-id=1095a39f80569c66","type":"library","name":"apk-tools","version":"2.14.6-r2","licenses":[{"license":{"id":"GPL-2.0-only"}}],"cpe":"cpe:2.3:a:apk-tools:apk-tools:2.14.6-r2:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/apk-tools@2.14.6-r2?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:apk-tools:apk_tools:2.14.6-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:apk_tools:apk-tools:2.14.6-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:apk_tools:apk_tools:2.14.6-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:apk:apk-tools:2.14.6-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:apk:apk_tools:2.14.6-r2:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"apk-tools"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/brotli@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=e3ed65b6dc9144b9","type":"library","name":"brotli","version":"1.1.0-r2","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:brotli:brotli:1.1.0-r2:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/brotli@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:google:brotli:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"brotli"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/brotli-dev@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=037ba9595fe77699&upstream=brotli","type":"library","name":"brotli-dev","version":"1.1.0-r2","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:brotli-dev:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/brotli-dev@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&upstream=brotli","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli-dev:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli_dev:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli_dev:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:google:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:google:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"brotli"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/brotli-libs@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=8f69c879d6b38e38&upstream=brotli","type":"library","name":"brotli-libs","version":"1.1.0-r2","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:brotli-libs:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/brotli-libs@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&upstream=brotli","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli-libs:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli_libs:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli_libs:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:brotli:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:google:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:google:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"brotli"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/busybox@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=7cbfd25df10d2724","type":"library","name":"busybox","version":"1.37.0-r8","licenses":[{"license":{"id":"GPL-2.0-only"}}],"cpe":"cpe:2.3:a:busybox:busybox:1.37.0-r8:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/busybox@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"busybox"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/busybox-binsh@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=3a2e3359d51d0045&upstream=busybox","type":"library","name":"busybox-binsh","version":"1.37.0-r8","licenses":[{"license":{"id":"GPL-2.0-only"}}],"cpe":"cpe:2.3:a:busybox-binsh:busybox-binsh:1.37.0-r8:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/busybox-binsh@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&upstream=busybox","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:busybox-binsh:busybox_binsh:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:busybox_binsh:busybox-binsh:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:busybox_binsh:busybox_binsh:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:busybox:busybox-binsh:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:busybox:busybox_binsh:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"busybox"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/ca-certificates-bundle@20241010-r0?arch=x86_64&distro=alpine-3.21.0&package-id=a906f1b13dd9bf14&upstream=ca-certificates","type":"library","name":"ca-certificates-bundle","version":"20241010-r0","licenses":[{"expression":"MPL-2.0 AND MIT"}],"cpe":"cpe:2.3:a:ca-certificates-bundle:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/ca-certificates-bundle@20241010-r0?arch=x86_64&distro=alpine-3.21.0&upstream=ca-certificates","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca-certificates-bundle:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca_certificates_bundle:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca_certificates_bundle:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca-certificates:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca-certificates:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca_certificates:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca_certificates:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:mozilla:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:mozilla:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ca:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"ca-certificates"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:generic/curl@8.11.1?package-id=3e249a6f2317504e","type":"application","name":"curl","version":"8.11.1","cpe":"cpe:2.3:a:haxx:curl:8.11.1:*:*:*:*:*:*:*","purl":"pkg:generic/curl@8.11.1","properties":[{"name":"syft:package:type","value":"binary"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/usr/bin/curl"}]},{"bom-ref":"pkg:apk/alpine/e2fsprogs-libs@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=412b42ad943bf8cb&upstream=e2fsprogs","type":"library","name":"e2fsprogs-libs","version":"1.47.1-r1","licenses":[{"expression":"GPL-2.0-or-later AND LGPL-2.0-or-later AND BSD-3-Clause AND MIT"}],"cpe":"cpe:2.3:a:e2fsprogs-libs:e2fsprogs-libs:1.47.1-r1:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/e2fsprogs-libs@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&upstream=e2fsprogs","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:e2fsprogs-libs:e2fsprogs_libs:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:e2fsprogs_libs:e2fsprogs-libs:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:e2fsprogs_libs:e2fsprogs_libs:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:e2fsprogs:e2fsprogs-libs:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:e2fsprogs:e2fsprogs_libs:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"e2fsprogs"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/keyutils-libs@1.6.3-r4?arch=x86_64&distro=alpine-3.21.0&package-id=5c84d53d49f221e1&upstream=keyutils","type":"library","name":"keyutils-libs","version":"1.6.3-r4","licenses":[{"expression":"GPL-2.0-or-later AND LGPL-2.0-or-later"}],"cpe":"cpe:2.3:a:keyutils-libs:keyutils-libs:1.6.3-r4:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/keyutils-libs@1.6.3-r4?arch=x86_64&distro=alpine-3.21.0&upstream=keyutils","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:keyutils-libs:keyutils_libs:1.6.3-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:keyutils_libs:keyutils-libs:1.6.3-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:keyutils_libs:keyutils_libs:1.6.3-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:keyutils:keyutils-libs:1.6.3-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:keyutils:keyutils_libs:1.6.3-r4:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"keyutils"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/krb5@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0&package-id=e13769c76f74134e","type":"library","name":"krb5","version":"1.21.3-r0","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:krb5:krb5:1.21.3-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/krb5@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb5:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"krb5"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/krb5-conf@1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=5449192ad1853f9c","type":"library","name":"krb5-conf","version":"1.0-r2","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:krb5-conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/krb5-conf@1.0-r2?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5-conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5-conf:krb-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5-conf:krb_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_conf:krb-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_conf:krb_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-conf:krb-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-conf:krb_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_conf:krb-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_conf:krb_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb5-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb5_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb5-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb5_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb-conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb_conf:1.0-r2:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"krb5-conf"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/krb5-libs@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0&package-id=67d8ec97d517d9c2&upstream=krb5","type":"library","name":"krb5-libs","version":"1.21.3-r0","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:krb5-libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/krb5-libs@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0&upstream=krb5","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5-libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5-libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5-libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5_libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb-libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb_libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb5:krb_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb-libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:krb:krb_libs:1.21.3-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"krb5"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libcom_err@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=dbfb4d1dbecb422f&upstream=e2fsprogs","type":"library","name":"libcom_err","version":"1.47.1-r1","licenses":[{"expression":"GPL-2.0-or-later AND LGPL-2.0-or-later AND BSD-3-Clause AND MIT"}],"cpe":"cpe:2.3:a:libcom-err:libcom-err:1.47.1-r1:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libcom_err@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&upstream=e2fsprogs","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcom-err:libcom_err:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcom_err:libcom-err:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcom_err:libcom_err:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcom:libcom-err:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcom:libcom_err:1.47.1-r1:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"e2fsprogs"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=88109714e4e7213c&upstream=openssl","type":"library","name":"libcrypto3","version":"3.3.2-r4","licenses":[{"license":{"id":"Apache-2.0"}}],"cpe":"cpe:2.3:a:libcrypto3:libcrypto3:3.3.2-r4:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&upstream=openssl","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcrypto3:libcrypto:3.3.2-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcrypto:libcrypto3:3.3.2-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libcrypto:libcrypto:3.3.2-r4:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"openssl"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libgcc@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&package-id=3d4d4f53df422ab4&upstream=gcc","type":"library","name":"libgcc","version":"14.2.0-r4","licenses":[{"expression":"GPL-2.0-or-later AND LGPL-2.1-or-later"}],"cpe":"cpe:2.3:a:libgcc:libgcc:14.2.0-r4:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libgcc@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&upstream=gcc","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"gcc"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libidn2@2.3.7-r0?arch=x86_64&distro=alpine-3.21.0&package-id=97c06e6e1903e776","type":"library","name":"libidn2","version":"2.3.7-r0","licenses":[{"expression":"GPL-2.0-or-later OR LGPL-3.0-or-later"}],"cpe":"cpe:2.3:a:libidn2:libidn2:2.3.7-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libidn2@2.3.7-r0?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:libidn2:libidn:2.3.7-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libidn:libidn2:2.3.7-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libidn:libidn:2.3.7-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:gnu:libidn2:2.3.7-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:gnu:libidn:2.3.7-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"libidn2"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libpsl@0.21.5-r3?arch=x86_64&distro=alpine-3.21.0&package-id=fc2164ebd15bcb82","type":"library","name":"libpsl","version":"0.21.5-r3","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:libpsl:libpsl:0.21.5-r3:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libpsl@0.21.5-r3?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"libpsl"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libssh2@1.11.1-r0?arch=x86_64&distro=alpine-3.21.0&package-id=ba1753b9ff7d9303","type":"library","name":"libssh2","version":"1.11.1-r0","licenses":[{"license":{"id":"BSD-3-Clause"}}],"cpe":"cpe:2.3:a:libssh2:libssh2:1.11.1-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libssh2@1.11.1-r0?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:libssh2:libssh:1.11.1-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libssh:libssh2:1.11.1-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libssh:libssh:1.11.1-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"libssh2"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libssl3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=0f1bae216faefb4b&upstream=openssl","type":"library","name":"libssl3","version":"3.3.2-r4","licenses":[{"license":{"id":"Apache-2.0"}}],"cpe":"cpe:2.3:a:libssl3:libssl3:3.3.2-r4:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libssl3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&upstream=openssl","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:libssl3:libssl:3.3.2-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libssl:libssl3:3.3.2-r4:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:libssl:libssl:3.3.2-r4:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"openssl"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libstdc%2B%2B@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&package-id=f579a97329d24094&upstream=gcc","type":"library","name":"libstdc++","version":"14.2.0-r4","licenses":[{"expression":"GPL-2.0-or-later AND LGPL-2.1-or-later"}],"cpe":"cpe:2.3:a:libstdc\\+\\+:libstdc\\+\\+:14.2.0-r4:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libstdc%2B%2B@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&upstream=gcc","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"gcc"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libunistring@1.2-r0?arch=x86_64&distro=alpine-3.21.0&package-id=07c4841b11e2910b","type":"library","name":"libunistring","version":"1.2-r0","licenses":[{"expression":"GPL-2.0-or-later OR LGPL-3.0-or-later"}],"cpe":"cpe:2.3:a:libunistring:libunistring:1.2-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libunistring@1.2-r0?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:gnu:libunistring:1.2-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"libunistring"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/libverto@0.3.2-r2?arch=x86_64&distro=alpine-3.21.0&package-id=00fb4f5e90a57319","type":"library","name":"libverto","version":"0.3.2-r2","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:latchset:libverto:0.3.2-r2:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/libverto@0.3.2-r2?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:libverto:libverto:0.3.2-r2:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"libverto"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba","type":"library","name":"musl","version":"1.2.5-r8","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:musl-libc:musl:1.2.5-r8:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl_libc:musl:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl:musl:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"musl"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/musl-utils@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=9ed24d5185052508&upstream=musl","type":"library","name":"musl-utils","version":"1.2.5-r8","licenses":[{"expression":"MIT AND BSD-2-Clause AND GPL-2.0-or-later"}],"cpe":"cpe:2.3:a:musl-utils:musl-utils:1.2.5-r8:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/musl-utils@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&upstream=musl","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl-utils:musl_utils:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl_utils:musl-utils:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl_utils:musl_utils:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl-libc:musl-utils:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl-libc:musl_utils:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl:musl-utils:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:musl:musl_utils:1.2.5-r8:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"musl"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/nghttp2-dev@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c9d137d8d21db486&upstream=nghttp2","type":"library","name":"nghttp2-dev","version":"1.64.0-r0","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:nghttp2-dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/nghttp2-dev@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&upstream=nghttp2","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2-dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2-dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2-dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"nghttp2"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/nghttp2-libs@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c16da0382f03c90a&upstream=nghttp2","type":"library","name":"nghttp2-libs","version":"1.64.0-r0","licenses":[{"license":{"id":"MIT"}}],"cpe":"cpe:2.3:a:nghttp2-libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/nghttp2-libs@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&upstream=nghttp2","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2-libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2-libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2-libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2_libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp-libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp_libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp2:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:nghttp:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"nghttp2"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/pkgconf@2.3.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c86c3c8f976d31b1","type":"library","name":"pkgconf","version":"2.3.0-r0","licenses":[{"license":{"id":"ISC"}}],"cpe":"cpe:2.3:a:pkgconf:pkgconf:2.3.0-r0:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/pkgconf@2.3.0-r0?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"pkgconf"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/scanelf@1.3.8-r1?arch=x86_64&distro=alpine-3.21.0&package-id=e70542a40cdca454&upstream=pax-utils","type":"library","name":"scanelf","version":"1.3.8-r1","licenses":[{"license":{"id":"GPL-2.0-only"}}],"cpe":"cpe:2.3:a:scanelf:scanelf:1.3.8-r1:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/scanelf@1.3.8-r1?arch=x86_64&distro=alpine-3.21.0&upstream=pax-utils","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"pax-utils"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/ssl_client@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=3b2d5f71701ee5a7&upstream=busybox","type":"library","name":"ssl_client","version":"1.37.0-r8","licenses":[{"license":{"id":"GPL-2.0-only"}}],"cpe":"cpe:2.3:a:ssl-client:ssl-client:1.37.0-r8:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/ssl_client@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&upstream=busybox","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:ssl-client:ssl_client:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ssl_client:ssl-client:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ssl_client:ssl_client:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ssl:ssl-client:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:ssl:ssl_client:1.37.0-r8:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"busybox"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/zlib@1.3.1-r2?arch=x86_64&distro=alpine-3.21.0&package-id=bb1801fcfd6835ff","type":"library","name":"zlib","version":"1.3.1-r2","licenses":[{"license":{"id":"Zlib"}}],"cpe":"cpe:2.3:a:zlib:zlib:1.3.1-r2:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/zlib@1.3.1-r2?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"zlib"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/zstd@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0&package-id=986677a068b531ae","type":"library","name":"zstd","version":"1.5.6-r1","licenses":[{"expression":"BSD-3-Clause OR GPL-2.0-or-later"}],"cpe":"cpe:2.3:a:zstd:zstd:1.5.6-r1:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/zstd@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"zstd"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"pkg:apk/alpine/zstd-libs@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0&package-id=9661e4d8b6fc57db&upstream=zstd","type":"library","name":"zstd-libs","version":"1.5.6-r1","licenses":[{"expression":"BSD-3-Clause OR GPL-2.0-or-later"}],"cpe":"cpe:2.3:a:zstd-libs:zstd-libs:1.5.6-r1:*:*:*:*:*:*:*","purl":"pkg:apk/alpine/zstd-libs@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0&upstream=zstd","properties":[{"name":"syft:package:type","value":"apk"},{"name":"syft:package:metadataType","value":"apk-db-entry"},{"name":"syft:cpe23","value":"cpe:2.3:a:zstd-libs:zstd_libs:1.5.6-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:zstd_libs:zstd-libs:1.5.6-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:zstd_libs:zstd_libs:1.5.6-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:zstd:zstd-libs:1.5.6-r1:*:*:*:*:*:*:*"},{"name":"syft:cpe23","value":"cpe:2.3:a:zstd:zstd_libs:1.5.6-r1:*:*:*:*:*:*:*"},{"name":"syft:location:0:layerID","value":"sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"},{"name":"syft:location:0:path","value":"/lib/apk/db/installed"},{"name":"syft:metadata:installedSize","value":"0"},{"name":"syft:metadata:originPackage","value":"zstd"},{"name":"syft:metadata:size","value":"0"}]},{"bom-ref":"os:alpine@3.21.0","type":"operating-system","name":"alpine","version":"3.21.0","description":"Alpine Linux v3.21","swid":{"tagId":"alpine","name":"alpine","version":"3.21.0"},"externalReferences":[{"url":"https://gitlab.alpinelinux.org/alpine/aports/-/issues","type":"issue-tracker"},{"url":"https://alpinelinux.org/","type":"website"}],"properties":[{"name":"syft:distro:extendedSupport","value":"false"},{"name":"syft:distro:id","value":"alpine"},{"name":"syft:distro:prettyName","value":"Alpine Linux v3.21"},{"name":"syft:distro:versionID","value":"3.21.0"}]}],"dependencies":[{"ref":"pkg:apk/alpine/alpine-baselayout@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0&package-id=ceeb874c4a135dac","dependsOn":["pkg:apk/alpine/alpine-baselayout-data@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0&package-id=1a41f8b73cec7362&upstream=alpine-baselayout","pkg:apk/alpine/busybox-binsh@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=3a2e3359d51d0045&upstream=busybox"]},{"ref":"pkg:apk/alpine/alpine-release@3.21.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=456e920c4331bde6&upstream=alpine-base","dependsOn":["pkg:apk/alpine/alpine-keys@2.5-r0?arch=x86_64&distro=alpine-3.21.0&package-id=f3faf4bc1f174ce9"]},{"ref":"pkg:apk/alpine/apk-tools@2.14.6-r2?arch=x86_64&distro=alpine-3.21.0&package-id=1095a39f80569c66","dependsOn":["pkg:apk/alpine/ca-certificates-bundle@20241010-r0?arch=x86_64&distro=alpine-3.21.0&package-id=a906f1b13dd9bf14&upstream=ca-certificates","pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=88109714e4e7213c&upstream=openssl","pkg:apk/alpine/libssl3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=0f1bae216faefb4b&upstream=openssl","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba","pkg:apk/alpine/zlib@1.3.1-r2?arch=x86_64&distro=alpine-3.21.0&package-id=bb1801fcfd6835ff"]},{"ref":"pkg:apk/alpine/brotli-dev@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=037ba9595fe77699&upstream=brotli","dependsOn":["pkg:apk/alpine/brotli-libs@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=8f69c879d6b38e38&upstream=brotli","pkg:apk/alpine/brotli@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=e3ed65b6dc9144b9","pkg:apk/alpine/pkgconf@2.3.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c86c3c8f976d31b1"]},{"ref":"pkg:apk/alpine/brotli-libs@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=8f69c879d6b38e38&upstream=brotli","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/brotli@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=e3ed65b6dc9144b9","dependsOn":["pkg:apk/alpine/brotli-libs@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=8f69c879d6b38e38&upstream=brotli","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/busybox-binsh@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=3a2e3359d51d0045&upstream=busybox","dependsOn":["pkg:apk/alpine/busybox@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=7cbfd25df10d2724"]},{"ref":"pkg:apk/alpine/busybox@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=7cbfd25df10d2724","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/e2fsprogs-libs@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=412b42ad943bf8cb&upstream=e2fsprogs","dependsOn":["pkg:apk/alpine/libcom_err@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=dbfb4d1dbecb422f&upstream=e2fsprogs","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/keyutils-libs@1.6.3-r4?arch=x86_64&distro=alpine-3.21.0&package-id=5c84d53d49f221e1&upstream=keyutils","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/krb5-libs@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0&package-id=67d8ec97d517d9c2&upstream=krb5","dependsOn":["pkg:apk/alpine/keyutils-libs@1.6.3-r4?arch=x86_64&distro=alpine-3.21.0&package-id=5c84d53d49f221e1&upstream=keyutils","pkg:apk/alpine/krb5-conf@1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=5449192ad1853f9c","pkg:apk/alpine/libcom_err@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=dbfb4d1dbecb422f&upstream=e2fsprogs","pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=88109714e4e7213c&upstream=openssl","pkg:apk/alpine/libssl3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=0f1bae216faefb4b&upstream=openssl","pkg:apk/alpine/libverto@0.3.2-r2?arch=x86_64&distro=alpine-3.21.0&package-id=00fb4f5e90a57319","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/krb5@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0&package-id=e13769c76f74134e","dependsOn":["pkg:apk/alpine/e2fsprogs-libs@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=412b42ad943bf8cb&upstream=e2fsprogs","pkg:apk/alpine/krb5-conf@1.0-r2?arch=x86_64&distro=alpine-3.21.0&package-id=5449192ad1853f9c","pkg:apk/alpine/krb5-libs@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0&package-id=67d8ec97d517d9c2&upstream=krb5","pkg:apk/alpine/libcom_err@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=dbfb4d1dbecb422f&upstream=e2fsprogs","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libcom_err@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&package-id=dbfb4d1dbecb422f&upstream=e2fsprogs","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=88109714e4e7213c&upstream=openssl","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libgcc@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&package-id=3d4d4f53df422ab4&upstream=gcc","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libidn2@2.3.7-r0?arch=x86_64&distro=alpine-3.21.0&package-id=97c06e6e1903e776","dependsOn":["pkg:apk/alpine/libunistring@1.2-r0?arch=x86_64&distro=alpine-3.21.0&package-id=07c4841b11e2910b","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libpsl@0.21.5-r3?arch=x86_64&distro=alpine-3.21.0&package-id=fc2164ebd15bcb82","dependsOn":["pkg:apk/alpine/libidn2@2.3.7-r0?arch=x86_64&distro=alpine-3.21.0&package-id=97c06e6e1903e776","pkg:apk/alpine/libunistring@1.2-r0?arch=x86_64&distro=alpine-3.21.0&package-id=07c4841b11e2910b","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libssh2@1.11.1-r0?arch=x86_64&distro=alpine-3.21.0&package-id=ba1753b9ff7d9303","dependsOn":["pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=88109714e4e7213c&upstream=openssl","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba","pkg:apk/alpine/zlib@1.3.1-r2?arch=x86_64&distro=alpine-3.21.0&package-id=bb1801fcfd6835ff"]},{"ref":"pkg:apk/alpine/libssl3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=0f1bae216faefb4b&upstream=openssl","dependsOn":["pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=88109714e4e7213c&upstream=openssl","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libstdc%2B%2B@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&package-id=f579a97329d24094&upstream=gcc","dependsOn":["pkg:apk/alpine/libgcc@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&package-id=3d4d4f53df422ab4&upstream=gcc","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libunistring@1.2-r0?arch=x86_64&distro=alpine-3.21.0&package-id=07c4841b11e2910b","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/libverto@0.3.2-r2?arch=x86_64&distro=alpine-3.21.0&package-id=00fb4f5e90a57319","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/musl-utils@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=9ed24d5185052508&upstream=musl","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba","pkg:apk/alpine/scanelf@1.3.8-r1?arch=x86_64&distro=alpine-3.21.0&package-id=e70542a40cdca454&upstream=pax-utils"]},{"ref":"pkg:apk/alpine/nghttp2-dev@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c9d137d8d21db486&upstream=nghttp2","dependsOn":["pkg:apk/alpine/nghttp2-libs@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c16da0382f03c90a&upstream=nghttp2","pkg:apk/alpine/pkgconf@2.3.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c86c3c8f976d31b1"]},{"ref":"pkg:apk/alpine/nghttp2-libs@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c16da0382f03c90a&upstream=nghttp2","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/pkgconf@2.3.0-r0?arch=x86_64&distro=alpine-3.21.0&package-id=c86c3c8f976d31b1","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/scanelf@1.3.8-r1?arch=x86_64&distro=alpine-3.21.0&package-id=e70542a40cdca454&upstream=pax-utils","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/ssl_client@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&package-id=3b2d5f71701ee5a7&upstream=busybox","dependsOn":["pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=88109714e4e7213c&upstream=openssl","pkg:apk/alpine/libssl3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&package-id=0f1bae216faefb4b&upstream=openssl","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/zlib@1.3.1-r2?arch=x86_64&distro=alpine-3.21.0&package-id=bb1801fcfd6835ff","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/zstd-libs@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0&package-id=9661e4d8b6fc57db&upstream=zstd","dependsOn":["pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba"]},{"ref":"pkg:apk/alpine/zstd@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0&package-id=986677a068b531ae","dependsOn":["pkg:apk/alpine/libgcc@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&package-id=3d4d4f53df422ab4&upstream=gcc","pkg:apk/alpine/libstdc%2B%2B@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&package-id=f579a97329d24094&upstream=gcc","pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&package-id=25a411dcbf4c73ba","pkg:apk/alpine/zstd-libs@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0&package-id=9661e4d8b6fc57db&upstream=zstd"]}]} diff --git a/sbomexport/testdata/curl-8.11.1.syft.json b/sbomexport/testdata/curl-8.11.1.syft.json new file mode 100644 index 00000000..6679ceaf --- /dev/null +++ b/sbomexport/testdata/curl-8.11.1.syft.json @@ -0,0 +1 @@ +{"artifacts": [{"id": "ceeb874c4a135dac", "name": "alpine-baselayout", "version": "3.6.8-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-only", "spdxExpression": "GPL-2.0-only", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:alpine-baselayout:alpine-baselayout:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine-baselayout:alpine_baselayout:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_baselayout:alpine-baselayout:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_baselayout:alpine_baselayout:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine-baselayout:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine_baselayout:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/alpine-baselayout@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "alpine-baselayout", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "1a41f8b73cec7362", "name": "alpine-baselayout-data", "version": "3.6.8-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-only", "spdxExpression": "GPL-2.0-only", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:alpine-baselayout-data:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine-baselayout-data:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_baselayout_data:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_baselayout_data:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine-baselayout:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine-baselayout:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_baselayout:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_baselayout:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine-baselayout-data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine_baselayout_data:3.6.8-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/alpine-baselayout-data@3.6.8-r0?arch=x86_64&distro=alpine-3.21.0&upstream=alpine-baselayout", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "alpine-baselayout", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "f3faf4bc1f174ce9", "name": "alpine-keys", "version": "2.5-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:alpine-keys:alpine-keys:2.5-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine-keys:alpine_keys:2.5-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_keys:alpine-keys:2.5-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_keys:alpine_keys:2.5-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine-keys:2.5-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine_keys:2.5-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/alpine-keys@2.5-r0?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "alpine-keys", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "456e920c4331bde6", "name": "alpine-release", "version": "3.21.0-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:alpine-release:alpine-release:3.21.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine-release:alpine_release:3.21.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_release:alpine-release:3.21.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine_release:alpine_release:3.21.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine-release:3.21.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:alpine:alpine_release:3.21.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/alpine-release@3.21.0-r0?arch=x86_64&distro=alpine-3.21.0&upstream=alpine-base", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "alpine-base", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "1095a39f80569c66", "name": "apk-tools", "version": "2.14.6-r2", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-only", "spdxExpression": "GPL-2.0-only", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:apk-tools:apk-tools:2.14.6-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:apk-tools:apk_tools:2.14.6-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:apk_tools:apk-tools:2.14.6-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:apk_tools:apk_tools:2.14.6-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:apk:apk-tools:2.14.6-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:apk:apk_tools:2.14.6-r2:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/apk-tools@2.14.6-r2?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "apk-tools", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "e3ed65b6dc9144b9", "name": "brotli", "version": "1.1.0-r2", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:brotli:brotli:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:google:brotli:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/brotli@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "brotli", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "037ba9595fe77699", "name": "brotli-dev", "version": "1.1.0-r2", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:brotli-dev:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli-dev:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli_dev:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli_dev:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:google:brotli-dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:google:brotli_dev:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/brotli-dev@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&upstream=brotli", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "brotli", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "8f69c879d6b38e38", "name": "brotli-libs", "version": "1.1.0-r2", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:brotli-libs:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli-libs:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli_libs:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli_libs:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:brotli:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:google:brotli-libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:google:brotli_libs:1.1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/brotli-libs@1.1.0-r2?arch=x86_64&distro=alpine-3.21.0&upstream=brotli", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "brotli", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "7cbfd25df10d2724", "name": "busybox", "version": "1.37.0-r8", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-only", "spdxExpression": "GPL-2.0-only", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:busybox:busybox:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/busybox@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "busybox", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "3a2e3359d51d0045", "name": "busybox-binsh", "version": "1.37.0-r8", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-only", "spdxExpression": "GPL-2.0-only", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:busybox-binsh:busybox-binsh:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:busybox-binsh:busybox_binsh:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:busybox_binsh:busybox-binsh:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:busybox_binsh:busybox_binsh:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:busybox:busybox-binsh:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:busybox:busybox_binsh:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/busybox-binsh@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&upstream=busybox", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "busybox", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "a906f1b13dd9bf14", "name": "ca-certificates-bundle", "version": "20241010-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MPL-2.0 AND MIT", "spdxExpression": "MPL-2.0 AND MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:ca-certificates-bundle:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca-certificates-bundle:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca_certificates_bundle:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca_certificates_bundle:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca-certificates:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca-certificates:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca_certificates:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca_certificates:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:mozilla:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:mozilla:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca:ca-certificates-bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ca:ca_certificates_bundle:20241010-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/ca-certificates-bundle@20241010-r0?arch=x86_64&distro=alpine-3.21.0&upstream=ca-certificates", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "ca-certificates", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "3e249a6f2317504e", "name": "curl", "version": "8.11.1", "type": "binary", "foundBy": "", "locations": [{"path": "/usr/bin/curl", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": null, "language": "", "cpes": [{"cpe": "cpe:2.3:a:haxx:curl:8.11.1:*:*:*:*:*:*:*", "source": "nvd-cpe-dictionary"}], "purl": "pkg:generic/curl@8.11.1", "metadata": null}, {"id": "412b42ad943bf8cb", "name": "e2fsprogs-libs", "version": "1.47.1-r1", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-or-later AND LGPL-2.0-or-later AND BSD-3-Clause AND MIT", "spdxExpression": "GPL-2.0-or-later AND LGPL-2.0-or-later AND BSD-3-Clause AND MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:e2fsprogs-libs:e2fsprogs-libs:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:e2fsprogs-libs:e2fsprogs_libs:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:e2fsprogs_libs:e2fsprogs-libs:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:e2fsprogs_libs:e2fsprogs_libs:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:e2fsprogs:e2fsprogs-libs:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:e2fsprogs:e2fsprogs_libs:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/e2fsprogs-libs@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&upstream=e2fsprogs", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "e2fsprogs", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "5c84d53d49f221e1", "name": "keyutils-libs", "version": "1.6.3-r4", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-or-later AND LGPL-2.0-or-later", "spdxExpression": "GPL-2.0-or-later AND LGPL-2.0-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:keyutils-libs:keyutils-libs:1.6.3-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:keyutils-libs:keyutils_libs:1.6.3-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:keyutils_libs:keyutils-libs:1.6.3-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:keyutils_libs:keyutils_libs:1.6.3-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:keyutils:keyutils-libs:1.6.3-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:keyutils:keyutils_libs:1.6.3-r4:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/keyutils-libs@1.6.3-r4?arch=x86_64&distro=alpine-3.21.0&upstream=keyutils", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "keyutils", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "e13769c76f74134e", "name": "krb5", "version": "1.21.3-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:krb5:krb5:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb5:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/krb5@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "krb5", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "5449192ad1853f9c", "name": "krb5-conf", "version": "1.0-r2", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:krb5-conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5-conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5-conf:krb-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5-conf:krb_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_conf:krb-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_conf:krb_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_conf:krb5-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_conf:krb5_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-conf:krb-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-conf:krb_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_conf:krb-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_conf:krb_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb5-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb5_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb5-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb5_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb-conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb_conf:1.0-r2:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/krb5-conf@1.0-r2?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "krb5-conf", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "67d8ec97d517d9c2", "name": "krb5-libs", "version": "1.21.3-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:krb5-libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5-libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5-libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5-libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5_libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_libs:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_libs:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb-libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_libs:krb-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb_libs:krb_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb5:krb_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb5-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb5_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb-libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:krb:krb_libs:1.21.3-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/krb5-libs@1.21.3-r0?arch=x86_64&distro=alpine-3.21.0&upstream=krb5", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "krb5", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "dbfb4d1dbecb422f", "name": "libcom_err", "version": "1.47.1-r1", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-or-later AND LGPL-2.0-or-later AND BSD-3-Clause AND MIT", "spdxExpression": "GPL-2.0-or-later AND LGPL-2.0-or-later AND BSD-3-Clause AND MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libcom-err:libcom-err:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcom-err:libcom_err:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcom_err:libcom-err:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcom_err:libcom_err:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcom:libcom-err:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcom:libcom_err:1.47.1-r1:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libcom_err@1.47.1-r1?arch=x86_64&distro=alpine-3.21.0&upstream=e2fsprogs", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "e2fsprogs", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "88109714e4e7213c", "name": "libcrypto3", "version": "3.3.2-r4", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "Apache-2.0", "spdxExpression": "Apache-2.0", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libcrypto3:libcrypto3:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcrypto3:libcrypto:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcrypto:libcrypto3:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libcrypto:libcrypto:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libcrypto3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&upstream=openssl", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "openssl", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "3d4d4f53df422ab4", "name": "libgcc", "version": "14.2.0-r4", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-or-later AND LGPL-2.1-or-later", "spdxExpression": "GPL-2.0-or-later AND LGPL-2.1-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libgcc:libgcc:14.2.0-r4:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libgcc@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&upstream=gcc", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "gcc", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "97c06e6e1903e776", "name": "libidn2", "version": "2.3.7-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-or-later OR LGPL-3.0-or-later", "spdxExpression": "GPL-2.0-or-later OR LGPL-3.0-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libidn2:libidn2:2.3.7-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libidn2:libidn:2.3.7-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libidn:libidn2:2.3.7-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libidn:libidn:2.3.7-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:gnu:libidn2:2.3.7-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:gnu:libidn:2.3.7-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libidn2@2.3.7-r0?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "libidn2", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "fc2164ebd15bcb82", "name": "libpsl", "version": "0.21.5-r3", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libpsl:libpsl:0.21.5-r3:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libpsl@0.21.5-r3?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "libpsl", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "ba1753b9ff7d9303", "name": "libssh2", "version": "1.11.1-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "BSD-3-Clause", "spdxExpression": "BSD-3-Clause", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libssh2:libssh2:1.11.1-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libssh2:libssh:1.11.1-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libssh:libssh2:1.11.1-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libssh:libssh:1.11.1-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libssh2@1.11.1-r0?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "libssh2", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "0f1bae216faefb4b", "name": "libssl3", "version": "3.3.2-r4", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "Apache-2.0", "spdxExpression": "Apache-2.0", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libssl3:libssl3:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libssl3:libssl:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libssl:libssl3:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libssl:libssl:3.3.2-r4:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libssl3@3.3.2-r4?arch=x86_64&distro=alpine-3.21.0&upstream=openssl", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "openssl", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "f579a97329d24094", "name": "libstdc++", "version": "14.2.0-r4", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-or-later AND LGPL-2.1-or-later", "spdxExpression": "GPL-2.0-or-later AND LGPL-2.1-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libstdc\\+\\+:libstdc\\+\\+:14.2.0-r4:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libstdc%2B%2B@14.2.0-r4?arch=x86_64&distro=alpine-3.21.0&upstream=gcc", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "gcc", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "07c4841b11e2910b", "name": "libunistring", "version": "1.2-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-or-later OR LGPL-3.0-or-later", "spdxExpression": "GPL-2.0-or-later OR LGPL-3.0-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:libunistring:libunistring:1.2-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:gnu:libunistring:1.2-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libunistring@1.2-r0?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "libunistring", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "00fb4f5e90a57319", "name": "libverto", "version": "0.3.2-r2", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:latchset:libverto:0.3.2-r2:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:libverto:libverto:0.3.2-r2:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/libverto@0.3.2-r2?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "libverto", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "25a411dcbf4c73ba", "name": "musl", "version": "1.2.5-r8", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:musl-libc:musl:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl_libc:musl:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl:musl:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/musl@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "musl", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "9ed24d5185052508", "name": "musl-utils", "version": "1.2.5-r8", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT AND BSD-2-Clause AND GPL-2.0-or-later", "spdxExpression": "MIT AND BSD-2-Clause AND GPL-2.0-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:musl-utils:musl-utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl-utils:musl_utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl_utils:musl-utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl_utils:musl_utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl-libc:musl-utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl-libc:musl_utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl:musl-utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:musl:musl_utils:1.2.5-r8:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/musl-utils@1.2.5-r8?arch=x86_64&distro=alpine-3.21.0&upstream=musl", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "musl", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "c9d137d8d21db486", "name": "nghttp2-dev", "version": "1.64.0-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:nghttp2-dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2-dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2-dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2-dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_dev:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_dev:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_dev:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_dev:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp2-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp2_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp-dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp_dev:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/nghttp2-dev@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&upstream=nghttp2", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "nghttp2", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "c16da0382f03c90a", "name": "nghttp2-libs", "version": "1.64.0-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "MIT", "spdxExpression": "MIT", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:nghttp2-libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2-libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2-libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2-libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2_libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_libs:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_libs:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp-libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_libs:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp_libs:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp2:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp2-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp2_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp-libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:nghttp:nghttp_libs:1.64.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/nghttp2-libs@1.64.0-r0?arch=x86_64&distro=alpine-3.21.0&upstream=nghttp2", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "nghttp2", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "c86c3c8f976d31b1", "name": "pkgconf", "version": "2.3.0-r0", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "ISC", "spdxExpression": "ISC", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:pkgconf:pkgconf:2.3.0-r0:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/pkgconf@2.3.0-r0?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "pkgconf", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "e70542a40cdca454", "name": "scanelf", "version": "1.3.8-r1", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-only", "spdxExpression": "GPL-2.0-only", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:scanelf:scanelf:1.3.8-r1:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/scanelf@1.3.8-r1?arch=x86_64&distro=alpine-3.21.0&upstream=pax-utils", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "pax-utils", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "3b2d5f71701ee5a7", "name": "ssl_client", "version": "1.37.0-r8", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "GPL-2.0-only", "spdxExpression": "GPL-2.0-only", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:ssl-client:ssl-client:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ssl-client:ssl_client:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ssl_client:ssl-client:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ssl_client:ssl_client:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ssl:ssl-client:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:ssl:ssl_client:1.37.0-r8:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/ssl_client@1.37.0-r8?arch=x86_64&distro=alpine-3.21.0&upstream=busybox", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "busybox", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "bb1801fcfd6835ff", "name": "zlib", "version": "1.3.1-r2", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "Zlib", "spdxExpression": "Zlib", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:zlib:zlib:1.3.1-r2:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/zlib@1.3.1-r2?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "zlib", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "986677a068b531ae", "name": "zstd", "version": "1.5.6-r1", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "BSD-3-Clause OR GPL-2.0-or-later", "spdxExpression": "BSD-3-Clause OR GPL-2.0-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:zstd:zstd:1.5.6-r1:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/zstd@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "zstd", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}, {"id": "9661e4d8b6fc57db", "name": "zstd-libs", "version": "1.5.6-r1", "type": "apk", "foundBy": "", "locations": [{"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "accessPath": ""}], "licenses": [{"value": "BSD-3-Clause OR GPL-2.0-or-later", "spdxExpression": "BSD-3-Clause OR GPL-2.0-or-later", "type": "declared", "urls": null, "locations": null}], "language": "", "cpes": [{"cpe": "cpe:2.3:a:zstd-libs:zstd-libs:1.5.6-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:zstd-libs:zstd_libs:1.5.6-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:zstd_libs:zstd-libs:1.5.6-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:zstd_libs:zstd_libs:1.5.6-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:zstd:zstd-libs:1.5.6-r1:*:*:*:*:*:*:*", "source": "syft-generated"}, {"cpe": "cpe:2.3:a:zstd:zstd_libs:1.5.6-r1:*:*:*:*:*:*:*", "source": "syft-generated"}], "purl": "pkg:apk/alpine/zstd-libs@1.5.6-r1?arch=x86_64&distro=alpine-3.21.0&upstream=zstd", "metadataType": "apk-db-entry", "metadata": {"package": "", "originPackage": "zstd", "maintainer": "", "version": "", "architecture": "", "url": "", "description": "", "size": 0, "installedSize": 0, "pullDependencies": null, "provides": null, "pullChecksum": "", "gitCommitOfApkPort": "", "files": null}}], "artifactRelationships": [{"parent": "00fb4f5e90a57319", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "00fb4f5e90a57319", "child": "550be067c8009c6f", "type": "contains"}, {"parent": "00fb4f5e90a57319", "child": "67d8ec97d517d9c2", "type": "dependency-of"}, {"parent": "037ba9595fe77699", "child": "00c4e2d01d480665", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "037ba9595fe77699", "child": "29c097271e009ca3", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "43c67ab0a2c85c2b", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "6a9ff817a6522527", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "70c81ce36219cef2", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "72fca5accef8f2a0", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "75f32f96c2d07d86", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "a1e5642bfd332967", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "ab50f8a62f73e17c", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "e1780308bd2745ec", "type": "contains"}, {"parent": "037ba9595fe77699", "child": "e580594caad1a9a9", "type": "contains"}, {"parent": "07c4841b11e2910b", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "07c4841b11e2910b", "child": "472a25f3f0ecdbd7", "type": "contains"}, {"parent": "07c4841b11e2910b", "child": "97c06e6e1903e776", "type": "dependency-of"}, {"parent": "07c4841b11e2910b", "child": "fc2164ebd15bcb82", "type": "dependency-of"}, {"parent": "0f1bae216faefb4b", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "0f1bae216faefb4b", "child": "1095a39f80569c66", "type": "dependency-of"}, {"parent": "0f1bae216faefb4b", "child": "3b2d5f71701ee5a7", "type": "dependency-of"}, {"parent": "0f1bae216faefb4b", "child": "67d8ec97d517d9c2", "type": "dependency-of"}, {"parent": "0f1bae216faefb4b", "child": "69cc85b55482eba1", "type": "contains"}, {"parent": "1095a39f80569c66", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "1095a39f80569c66", "child": "0c90571c31cc24d4", "type": "contains"}, {"parent": "1095a39f80569c66", "child": "4e2deeb47f39a10e", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "1a41f8b73cec7362", "child": "2ebaf2d5275a87c3", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "4d414d1737405098", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "4d8d5f0587154018", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "5e64bee7d0512520", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "5f3d3b1ea52cd203", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "5f409797065e8a24", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "668425e791126652", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "773e11b623fa2206", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "86a05755e77e07fc", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "a3196be7d01a0aec", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "b3633bc811ad48c9", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "b80c3cb56c980b08", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "c068f4c2abc469f7", "type": "contains"}, {"parent": "1a41f8b73cec7362", "child": "ceeb874c4a135dac", "type": "dependency-of"}, {"parent": "1a41f8b73cec7362", "child": "e7d0d8adb0625ce4", "type": "contains"}, {"parent": "25a411dcbf4c73ba", "child": "00fb4f5e90a57319", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "25a411dcbf4c73ba", "child": "07c4841b11e2910b", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "0f1bae216faefb4b", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "1095a39f80569c66", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "3b2d5f71701ee5a7", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "3d4d4f53df422ab4", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "412b42ad943bf8cb", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "5c84d53d49f221e1", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "67d8ec97d517d9c2", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "6e7e3004534b86d1", "type": "contains"}, {"parent": "25a411dcbf4c73ba", "child": "7cbfd25df10d2724", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "88109714e4e7213c", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "8f69c879d6b38e38", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "9661e4d8b6fc57db", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "97c06e6e1903e776", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "986677a068b531ae", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "9ed24d5185052508", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "ba1753b9ff7d9303", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "bb1801fcfd6835ff", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "c16da0382f03c90a", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "c86c3c8f976d31b1", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "dbfb4d1dbecb422f", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "e13769c76f74134e", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "e3ed65b6dc9144b9", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "e70542a40cdca454", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "f579a97329d24094", "type": "dependency-of"}, {"parent": "25a411dcbf4c73ba", "child": "fc2164ebd15bcb82", "type": "dependency-of"}, {"parent": "3a2e3359d51d0045", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "3a2e3359d51d0045", "child": "468f4889734362f5", "type": "contains"}, {"parent": "3a2e3359d51d0045", "child": "ceeb874c4a135dac", "type": "dependency-of"}, {"parent": "3b2d5f71701ee5a7", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "3b2d5f71701ee5a7", "child": "b4bab8e79035429e", "type": "contains"}, {"parent": "3d4d4f53df422ab4", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "3d4d4f53df422ab4", "child": "986677a068b531ae", "type": "dependency-of"}, {"parent": "3d4d4f53df422ab4", "child": "c394fbc850ea8753", "type": "contains"}, {"parent": "3d4d4f53df422ab4", "child": "f579a97329d24094", "type": "dependency-of"}, {"parent": "3e249a6f2317504e", "child": "0607fb48d6a867d9", "type": "evident-by"}, {"parent": "412b42ad943bf8cb", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "412b42ad943bf8cb", "child": "16db6b7867c5e19b", "type": "contains"}, {"parent": "412b42ad943bf8cb", "child": "55e808d1502ce1f5", "type": "contains"}, {"parent": "412b42ad943bf8cb", "child": "c08f16a4a723e201", "type": "contains"}, {"parent": "412b42ad943bf8cb", "child": "e13769c76f74134e", "type": "dependency-of"}, {"parent": "456e920c4331bde6", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "456e920c4331bde6", "child": "31f4ab357226cd60", "type": "contains"}, {"parent": "456e920c4331bde6", "child": "b413295f2e1636b9", "type": "contains"}, {"parent": "456e920c4331bde6", "child": "c36f73b65f306d73", "type": "contains"}, {"parent": "456e920c4331bde6", "child": "fd2f197c9c548c66", "type": "contains"}, {"parent": "5449192ad1853f9c", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "5449192ad1853f9c", "child": "614c65d7d5699747", "type": "contains"}, {"parent": "5449192ad1853f9c", "child": "67d8ec97d517d9c2", "type": "dependency-of"}, {"parent": "5449192ad1853f9c", "child": "e13769c76f74134e", "type": "dependency-of"}, {"parent": "5c84d53d49f221e1", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "5c84d53d49f221e1", "child": "67d8ec97d517d9c2", "type": "dependency-of"}, {"parent": "5c84d53d49f221e1", "child": "d500a230de1b6100", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "67d8ec97d517d9c2", "child": "1b4fe1c27bee3d6f", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "319aab2b06d5c638", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "3c1e7db652a4d4c1", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "4e51d5fe7f02b74f", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "5ae7577620ec6cc6", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "6a3ad7d0df5ad0b8", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "74a405952a8bacd5", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "803e7fbfe5b428cc", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "8663ae9af4fed258", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "a490c4f82f21f144", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "a6ee5aa1579ea184", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "aa1f2ef0278a32b8", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "bf5b8a5792ae8ce3", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "c042c0248a70b846", "type": "contains"}, {"parent": "67d8ec97d517d9c2", "child": "e13769c76f74134e", "type": "dependency-of"}, {"parent": "7cbfd25df10d2724", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "7cbfd25df10d2724", "child": "378d389540b30911", "type": "contains"}, {"parent": "7cbfd25df10d2724", "child": "3a2e3359d51d0045", "type": "dependency-of"}, {"parent": "7cbfd25df10d2724", "child": "3cbfe16761873575", "type": "contains"}, {"parent": "7cbfd25df10d2724", "child": "431ddc0b26fa5372", "type": "contains"}, {"parent": "7cbfd25df10d2724", "child": "468f4889734362f5", "type": "contains"}, {"parent": "7cbfd25df10d2724", "child": "ae53711b4c2ae508", "type": "contains"}, {"parent": "7cbfd25df10d2724", "child": "e680a539c74e700d", "type": "contains"}, {"parent": "7cbfd25df10d2724", "child": "f5ddc6e8bf552536", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "88109714e4e7213c", "child": "0f1bae216faefb4b", "type": "dependency-of"}, {"parent": "88109714e4e7213c", "child": "1095a39f80569c66", "type": "dependency-of"}, {"parent": "88109714e4e7213c", "child": "2230852154a764a9", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "2cac453818c05153", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "3b2d5f71701ee5a7", "type": "dependency-of"}, {"parent": "88109714e4e7213c", "child": "51e3b429173363b7", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "67d8ec97d517d9c2", "type": "dependency-of"}, {"parent": "88109714e4e7213c", "child": "70e74fc481bbd7b7", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "7386b7ea31c8254f", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "97ee0f53ad135fdd", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "9afb2be8c7429f0d", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "a36b11ef91e748da", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "b2bd5535e09327d5", "type": "contains"}, {"parent": "88109714e4e7213c", "child": "ba1753b9ff7d9303", "type": "dependency-of"}, {"parent": "88109714e4e7213c", "child": "ff43617ec381da18", "type": "contains"}, {"parent": "8f69c879d6b38e38", "child": "00c4e2d01d480665", "type": "contains"}, {"parent": "8f69c879d6b38e38", "child": "037ba9595fe77699", "type": "dependency-of"}, {"parent": "8f69c879d6b38e38", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "8f69c879d6b38e38", "child": "29c097271e009ca3", "type": "contains"}, {"parent": "8f69c879d6b38e38", "child": "75f32f96c2d07d86", "type": "contains"}, {"parent": "8f69c879d6b38e38", "child": "e3ed65b6dc9144b9", "type": "dependency-of"}, {"parent": "9661e4d8b6fc57db", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "9661e4d8b6fc57db", "child": "986677a068b531ae", "type": "dependency-of"}, {"parent": "9661e4d8b6fc57db", "child": "bf9888330a19323c", "type": "contains"}, {"parent": "97c06e6e1903e776", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "97c06e6e1903e776", "child": "9c5437be4d2bd035", "type": "contains"}, {"parent": "97c06e6e1903e776", "child": "fc2164ebd15bcb82", "type": "dependency-of"}, {"parent": "986677a068b531ae", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "986677a068b531ae", "child": "1c988daa02be9ff4", "type": "contains"}, {"parent": "986677a068b531ae", "child": "1d49d01d1338ec6e", "type": "contains"}, {"parent": "986677a068b531ae", "child": "94931d779ffec08e", "type": "contains"}, {"parent": "986677a068b531ae", "child": "e9a806dcd1a078d7", "type": "contains"}, {"parent": "9ed24d5185052508", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "9ed24d5185052508", "child": "485cab4248b11e3f", "type": "contains"}, {"parent": "9ed24d5185052508", "child": "b33e7a49b30565e7", "type": "contains"}, {"parent": "9ed24d5185052508", "child": "cb10741d9c217d07", "type": "contains"}, {"parent": "9ed24d5185052508", "child": "d36d4602a8c0ad50", "type": "contains"}, {"parent": "9ed24d5185052508", "child": "f058d85fa2770b4f", "type": "contains"}, {"parent": "a906f1b13dd9bf14", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "a906f1b13dd9bf14", "child": "1095a39f80569c66", "type": "dependency-of"}, {"parent": "a906f1b13dd9bf14", "child": "860b9d53645feb47", "type": "contains"}, {"parent": "ba1753b9ff7d9303", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "ba1753b9ff7d9303", "child": "ca2554e971e8d3da", "type": "contains"}, {"parent": "bb1801fcfd6835ff", "child": "014a43ace0388eab", "type": "contains"}, {"parent": "bb1801fcfd6835ff", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "bb1801fcfd6835ff", "child": "1095a39f80569c66", "type": "dependency-of"}, {"parent": "bb1801fcfd6835ff", "child": "ba1753b9ff7d9303", "type": "dependency-of"}, {"parent": "c16da0382f03c90a", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "c16da0382f03c90a", "child": "43052306802633cb", "type": "contains"}, {"parent": "c16da0382f03c90a", "child": "c9d137d8d21db486", "type": "dependency-of"}, {"parent": "c86c3c8f976d31b1", "child": "037ba9595fe77699", "type": "dependency-of"}, {"parent": "c86c3c8f976d31b1", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "c86c3c8f976d31b1", "child": "6f304251929469c4", "type": "contains"}, {"parent": "c86c3c8f976d31b1", "child": "affcf55f6df321b5", "type": "contains"}, {"parent": "c86c3c8f976d31b1", "child": "c9d137d8d21db486", "type": "dependency-of"}, {"parent": "c86c3c8f976d31b1", "child": "e70de53d7f62bd47", "type": "contains"}, {"parent": "c86c3c8f976d31b1", "child": "ffcacae116b11153", "type": "contains"}, {"parent": "c9d137d8d21db486", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "c9d137d8d21db486", "child": "43052306802633cb", "type": "contains"}, {"parent": "c9d137d8d21db486", "child": "4eb2fae82928af57", "type": "contains"}, {"parent": "c9d137d8d21db486", "child": "b8714ac0c5e52f87", "type": "contains"}, {"parent": "c9d137d8d21db486", "child": "f62213b368d29ad6", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "ceeb874c4a135dac", "child": "22834df769d2095e", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "4eb653645b7f13ee", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "86d9785f3abff156", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "96f77289dc1c6105", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "b29978812ba855fa", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "b58b1a80d90b95b5", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "bae009a41fd01a1d", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "d70650009c2bb036", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "f54933ed0cb86bd2", "type": "contains"}, {"parent": "ceeb874c4a135dac", "child": "ff541b2c108c7e57", "type": "contains"}, {"parent": "dbfb4d1dbecb422f", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "dbfb4d1dbecb422f", "child": "412b42ad943bf8cb", "type": "dependency-of"}, {"parent": "dbfb4d1dbecb422f", "child": "67d8ec97d517d9c2", "type": "dependency-of"}, {"parent": "dbfb4d1dbecb422f", "child": "e13769c76f74134e", "type": "dependency-of"}, {"parent": "dbfb4d1dbecb422f", "child": "ef996e1f6676f341", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "00fb4f5e90a57319", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "037ba9595fe77699", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "07c4841b11e2910b", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "0f1bae216faefb4b", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "1095a39f80569c66", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "1a41f8b73cec7362", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "25a411dcbf4c73ba", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "3a2e3359d51d0045", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "3b2d5f71701ee5a7", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "3d4d4f53df422ab4", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "3e249a6f2317504e", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "412b42ad943bf8cb", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "456e920c4331bde6", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "5449192ad1853f9c", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "5c84d53d49f221e1", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "67d8ec97d517d9c2", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "7cbfd25df10d2724", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "88109714e4e7213c", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "8f69c879d6b38e38", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "9661e4d8b6fc57db", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "97c06e6e1903e776", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "986677a068b531ae", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "9ed24d5185052508", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "a906f1b13dd9bf14", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "ba1753b9ff7d9303", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "bb1801fcfd6835ff", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "c16da0382f03c90a", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "c86c3c8f976d31b1", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "c9d137d8d21db486", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "ceeb874c4a135dac", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "dbfb4d1dbecb422f", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "e13769c76f74134e", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "e3ed65b6dc9144b9", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "e70542a40cdca454", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "f3faf4bc1f174ce9", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "f579a97329d24094", "type": "contains"}, {"parent": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "child": "fc2164ebd15bcb82", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "e13769c76f74134e", "child": "0c04ff01e9c104c2", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "4f2c93dd51c19e51", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "6bd9499f1bd4c600", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "86cfbda7088c3928", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "a7f7ae0f1df842f0", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "a81fad2d2679f3f4", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "adf89964d7825b5e", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "c218e0319aa46823", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "cdd9a493d13f00a7", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "d078169da5d9c73c", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "d7f92369591e398c", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "df66d7815e8ae92d", "type": "contains"}, {"parent": "e13769c76f74134e", "child": "f331aed8094b0bc8", "type": "contains"}, {"parent": "e3ed65b6dc9144b9", "child": "037ba9595fe77699", "type": "dependency-of"}, {"parent": "e3ed65b6dc9144b9", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "e3ed65b6dc9144b9", "child": "c373d9de6a379d23", "type": "contains"}, {"parent": "e70542a40cdca454", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "e70542a40cdca454", "child": "97db8aae69dc1643", "type": "contains"}, {"parent": "e70542a40cdca454", "child": "9ed24d5185052508", "type": "dependency-of"}, {"parent": "f3faf4bc1f174ce9", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "f3faf4bc1f174ce9", "child": "0c253a006a51a055", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "1e36d9959ceb1540", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "20c139d549b80964", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "3104c985f7217a91", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "33dbeb08a729965e", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "41bcca0722df483b", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "456e920c4331bde6", "type": "dependency-of"}, {"parent": "f3faf4bc1f174ce9", "child": "58dfe972c27db41d", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "5d2bce7c30d4842e", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "60f3660667c882d5", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "6277d3a7f9f78fa0", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "7430793c9843ac71", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "8503eca0329855ac", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "9174b40bd64a4afa", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "9d58e602c9201b03", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "9ee7f6bfafbd04f5", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "9fc2b76378071420", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "abac01a20548e31f", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "c0f8875bed907f16", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "dd82cc7a531a4f8a", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "e174ab22f2beeb60", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "e31ca5e7df53db52", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "e8e728f7eb9ac206", "type": "contains"}, {"parent": "f3faf4bc1f174ce9", "child": "ee960619cbfa52da", "type": "contains"}, {"parent": "f579a97329d24094", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "f579a97329d24094", "child": "6ddbc46067f19656", "type": "contains"}, {"parent": "f579a97329d24094", "child": "986677a068b531ae", "type": "dependency-of"}, {"parent": "fc2164ebd15bcb82", "child": "05d2eab904a84e7e", "type": "evident-by"}, {"parent": "fc2164ebd15bcb82", "child": "fd6f398d3af29611", "type": "contains"}], "files": [{"id": "468f4889734362f5", "location": {"path": "/bin/busybox", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "fd2f197c9c548c66", "location": {"path": "/etc/alpine-release", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "e31ca5e7df53db52", "location": {"path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "dd82cc7a531a4f8a", "location": {"path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "8503eca0329855ac", "location": {"path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "9fc2b76378071420", "location": {"path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "e174ab22f2beeb60", "location": {"path": "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-61666e3f.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "3cbfe16761873575", "location": {"path": "/etc/busybox-paths.d/busybox", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "86d9785f3abff156", "location": {"path": "/etc/crontabs/root", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b80c3cb56c980b08", "location": {"path": "/etc/fstab", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "e7d0d8adb0625ce4", "location": {"path": "/etc/hostname", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "4d8d5f0587154018", "location": {"path": "/etc/inittab", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b413295f2e1636b9", "location": {"path": "/etc/issue", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "e680a539c74e700d", "location": {"path": "/etc/logrotate.d/acpid", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b29978812ba855fa", "location": {"path": "/etc/modprobe.d/aliases.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b58b1a80d90b95b5", "location": {"path": "/etc/modprobe.d/blacklist.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "f54933ed0cb86bd2", "location": {"path": "/etc/modprobe.d/i386.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "bae009a41fd01a1d", "location": {"path": "/etc/modprobe.d/kms.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "a3196be7d01a0aec", "location": {"path": "/etc/modules", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "4eb653645b7f13ee", "location": {"path": "/etc/motd", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "ae53711b4c2ae508", "location": {"path": "/etc/network/if-up.d/dad", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "668425e791126652", "location": {"path": "/etc/nsswitch.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "773e11b623fa2206", "location": {"path": "/etc/profile", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "ff541b2c108c7e57", "location": {"path": "/etc/profile.d/20locale.sh", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "22834df769d2095e", "location": {"path": "/etc/profile.d/README", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "96f77289dc1c6105", "location": {"path": "/etc/profile.d/color_prompt.sh.disabled", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "2ebaf2d5275a87c3", "location": {"path": "/etc/protocols", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "31f4ab357226cd60", "location": {"path": "/etc/secfixes.d/alpine", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "f5ddc6e8bf552536", "location": {"path": "/etc/securetty", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "5e64bee7d0512520", "location": {"path": "/etc/services", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "5f409797065e8a24", "location": {"path": "/etc/shells", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "860b9d53645feb47", "location": {"path": "/etc/ssl/certs/ca-certificates.crt", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "97ee0f53ad135fdd", "location": {"path": "/etc/ssl/ct_log_list.cnf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "7386b7ea31c8254f", "location": {"path": "/etc/ssl/ct_log_list.cnf.dist", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "9afb2be8c7429f0d", "location": {"path": "/etc/ssl/openssl.cnf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "70e74fc481bbd7b7", "location": {"path": "/etc/ssl/openssl.cnf.dist", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "c068f4c2abc469f7", "location": {"path": "/etc/sysctl.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "378d389540b30911", "location": {"path": "/etc/udhcpc/udhcpc.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "6e7e3004534b86d1", "location": {"path": "/lib/ld-musl-x86_64.so.1", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "0c90571c31cc24d4", "location": {"path": "/sbin/apk", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "f058d85fa2770b4f", "location": {"path": "/sbin/ldconfig", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "cb10741d9c217d07", "location": {"path": "/usr/bin/getconf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "d36d4602a8c0ad50", "location": {"path": "/usr/bin/getent", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b33e7a49b30565e7", "location": {"path": "/usr/bin/iconv", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "485cab4248b11e3f", "location": {"path": "/usr/bin/ldd", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "97db8aae69dc1643", "location": {"path": "/usr/bin/scanelf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b4bab8e79035429e", "location": {"path": "/usr/bin/ssl_client", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "51e3b429173363b7", "location": {"path": "/usr/lib/engines-3/afalg.so", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "2cac453818c05153", "location": {"path": "/usr/lib/engines-3/capi.so", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b2bd5535e09327d5", "location": {"path": "/usr/lib/engines-3/loader_attic.so", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "2230852154a764a9", "location": {"path": "/usr/lib/engines-3/padlock.so", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "4e2deeb47f39a10e", "location": {"path": "/usr/lib/libapk.so.2.14.0", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "a36b11ef91e748da", "location": {"path": "/usr/lib/libcrypto.so.3", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "69cc85b55482eba1", "location": {"path": "/usr/lib/libssl.so.3", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "014a43ace0388eab", "location": {"path": "/usr/lib/libz.so.1.3.1", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "c36f73b65f306d73", "location": {"path": "/usr/lib/os-release", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "ff43617ec381da18", "location": {"path": "/usr/lib/ossl-modules/legacy.so", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "d70650009c2bb036", "location": {"path": "/usr/lib/sysctl.d/00-alpine.conf", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "0c253a006a51a055", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "7430793c9843ac71", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "9174b40bd64a4afa", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "41bcca0722df483b", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "33dbeb08a729965e", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58199dcc.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "ee960619cbfa52da", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58cbb476.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "3104c985f7217a91", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58e4f17d.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "1e36d9959ceb1540", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5e69ca50.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "abac01a20548e31f", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-60ac2099.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "60f3660667c882d5", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-6165ee59.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "20c139d549b80964", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-61666e3f.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "9ee7f6bfafbd04f5", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-616a9724.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "c0f8875bed907f16", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-616abc23.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "5d2bce7c30d4842e", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-616ac3bc.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "6277d3a7f9f78fa0", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-616adfeb.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "e8e728f7eb9ac206", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-616ae350.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "58dfe972c27db41d", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-616db30d.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "9d58e602c9201b03", "location": {"path": "/usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-66ba20fe.rsa.pub", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "431ddc0b26fa5372", "location": {"path": "/usr/share/udhcpc/default.script", "layerID": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7"}}, {"id": "b3633bc811ad48c9", "location": {"path": "/etc/group", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "4d414d1737405098", "location": {"path": "/etc/hosts", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "614c65d7d5699747", "location": {"path": "/etc/krb5.conf", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "86a05755e77e07fc", "location": {"path": "/etc/passwd", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "5f3d3b1ea52cd203", "location": {"path": "/etc/shadow", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "05d2eab904a84e7e", "location": {"path": "/lib/apk/db/installed", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "affcf55f6df321b5", "location": {"path": "/usr/bin/bomtool", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "c373d9de6a379d23", "location": {"path": "/usr/bin/brotli", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "0607fb48d6a867d9", "location": {"path": "/usr/bin/curl", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "c218e0319aa46823", "location": {"path": "/usr/bin/gss-client", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "d078169da5d9c73c", "location": {"path": "/usr/bin/k5srvutil", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "f331aed8094b0bc8", "location": {"path": "/usr/bin/kadmin", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "a7f7ae0f1df842f0", "location": {"path": "/usr/bin/kdestroy", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "86cfbda7088c3928", "location": {"path": "/usr/bin/kinit", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "d7f92369591e398c", "location": {"path": "/usr/bin/klist", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "0c04ff01e9c104c2", "location": {"path": "/usr/bin/kpasswd", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "4f2c93dd51c19e51", "location": {"path": "/usr/bin/ksu", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "a81fad2d2679f3f4", "location": {"path": "/usr/bin/kswitch", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "df66d7815e8ae92d", "location": {"path": "/usr/bin/ktutil", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "6bd9499f1bd4c600", "location": {"path": "/usr/bin/kvno", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "e70de53d7f62bd47", "location": {"path": "/usr/bin/pkgconf", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "e9a806dcd1a078d7", "location": {"path": "/usr/bin/pzstd", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "adf89964d7825b5e", "location": {"path": "/usr/bin/sim_client", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "cdd9a493d13f00a7", "location": {"path": "/usr/bin/uuclient", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "94931d779ffec08e", "location": {"path": "/usr/bin/zstd", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "1d49d01d1338ec6e", "location": {"path": "/usr/bin/zstdgrep", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "1c988daa02be9ff4", "location": {"path": "/usr/bin/zstdless", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "e580594caad1a9a9", "location": {"path": "/usr/include/brotli/decode.h", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "e1780308bd2745ec", "location": {"path": "/usr/include/brotli/encode.h", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "70c81ce36219cef2", "location": {"path": "/usr/include/brotli/port.h", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "6a9ff817a6522527", "location": {"path": "/usr/include/brotli/shared_dictionary.h", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "72fca5accef8f2a0", "location": {"path": "/usr/include/brotli/types.h", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "b8714ac0c5e52f87", "location": {"path": "/usr/include/nghttp2/nghttp2.h", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "f62213b368d29ad6", "location": {"path": "/usr/include/nghttp2/nghttp2ver.h", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "bf5b8a5792ae8ce3", "location": {"path": "/usr/lib/krb5/plugins/kdb/db2.so", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "74a405952a8bacd5", "location": {"path": "/usr/lib/krb5/plugins/preauth/otp.so", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "1b4fe1c27bee3d6f", "location": {"path": "/usr/lib/krb5/plugins/preauth/spake.so", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "5ae7577620ec6cc6", "location": {"path": "/usr/lib/krb5/plugins/preauth/test.so", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "aa1f2ef0278a32b8", "location": {"path": "/usr/lib/krb5/plugins/tls/k5tls.so", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "29c097271e009ca3", "location": {"path": "/usr/lib/libbrotlicommon.so.1.1.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "00c4e2d01d480665", "location": {"path": "/usr/lib/libbrotlidec.so.1.1.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "75f32f96c2d07d86", "location": {"path": "/usr/lib/libbrotlienc.so.1.1.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "ef996e1f6676f341", "location": {"path": "/usr/lib/libcom_err.so.2.1", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "16db6b7867c5e19b", "location": {"path": "/usr/lib/libe2p.so.2.3", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "c08f16a4a723e201", "location": {"path": "/usr/lib/libext2fs.so.2.4", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "c394fbc850ea8753", "location": {"path": "/usr/lib/libgcc_s.so.1", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "4e51d5fe7f02b74f", "location": {"path": "/usr/lib/libgssapi_krb5.so.2.2", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "319aab2b06d5c638", "location": {"path": "/usr/lib/libgssrpc.so.4.2", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "9c5437be4d2bd035", "location": {"path": "/usr/lib/libidn2.so.0.4.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "a6ee5aa1579ea184", "location": {"path": "/usr/lib/libk5crypto.so.3.1", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "a490c4f82f21f144", "location": {"path": "/usr/lib/libkadm5clnt_mit.so.12.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "c042c0248a70b846", "location": {"path": "/usr/lib/libkadm5srv_mit.so.12.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "6a3ad7d0df5ad0b8", "location": {"path": "/usr/lib/libkdb5.so.10.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "d500a230de1b6100", "location": {"path": "/usr/lib/libkeyutils.so.1.10", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "8663ae9af4fed258", "location": {"path": "/usr/lib/libkrad.so.0.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "3c1e7db652a4d4c1", "location": {"path": "/usr/lib/libkrb5.so.3.3", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "803e7fbfe5b428cc", "location": {"path": "/usr/lib/libkrb5support.so.0.1", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "43052306802633cb", "location": {"path": "/usr/lib/libnghttp2.so.14.28.3", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "ffcacae116b11153", "location": {"path": "/usr/lib/libpkgconf.so.5.0.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "fd6f398d3af29611", "location": {"path": "/usr/lib/libpsl.so.5.3.5", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "55e808d1502ce1f5", "location": {"path": "/usr/lib/libss.so.2.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "ca2554e971e8d3da", "location": {"path": "/usr/lib/libssh2.so.1.0.1", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "6ddbc46067f19656", "location": {"path": "/usr/lib/libstdc++.so.6.0.33", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "472a25f3f0ecdbd7", "location": {"path": "/usr/lib/libunistring.so.5.1.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "550be067c8009c6f", "location": {"path": "/usr/lib/libverto.so.1.0.0", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "bf9888330a19323c", "location": {"path": "/usr/lib/libzstd.so.1.5.6", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "ab50f8a62f73e17c", "location": {"path": "/usr/lib/pkgconfig/libbrotlicommon.pc", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "43c67ab0a2c85c2b", "location": {"path": "/usr/lib/pkgconfig/libbrotlidec.pc", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "a1e5642bfd332967", "location": {"path": "/usr/lib/pkgconfig/libbrotlienc.pc", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "4eb2fae82928af57", "location": {"path": "/usr/lib/pkgconfig/libnghttp2.pc", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}, {"id": "6f304251929469c4", "location": {"path": "/usr/share/aclocal/pkg.m4", "layerID": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d"}}], "source": {"id": "docker.io/curlimages/curl@c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "name": "docker.io/curlimages/curl:8.11.1", "version": "docker.io/curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "type": "image", "metadata": {"userInput": "docker.io/curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69", "imageID": "sha256:7551dbeefe0d9a88a7134dd903f97727315e9fd150b4aefa6da5dcf3a4757f2a", "manifestDigest": "", "mediaType": "", "tags": ["docker.io/curlimages/curl:8.11.1"], "imageSize": 21753027, "layers": [{"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:3e01818d79cd3467f1d60e54224f3f6ce5170eceb54e265d96bb82344b8c24e7", "size": 7837773}, {"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:47f700d46e8a78ec5278107460349b8392e454014ed0bb48a1cd04fe1781f69d", "size": 13915254}, {"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", "digest": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", "size": 0}], "manifest": null, "config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNyZWF0ZWQiOiIyMDI0LTEyLTExVDA4OjQ2OjU4LjIzMTMyNjg3WiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDI0LTEyLTA1VDEyOjQ5OjA0WiIsImNyZWF0ZWRfYnkiOiJBREQgYWxwaW5lLW1pbmlyb290ZnMtMy4yMS4wLXg4Nl82NC50YXIuZ3ogLyAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifSx7ImNyZWF0ZWQiOiIyMDI0LTEyLTA1VDEyOjQ5OjA0WiIsImNyZWF0ZWRfYnkiOiJDTUQgW1wiL2Jpbi9zaFwiXSIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZCI6IjIwMjQtMTItMTFUMDg6NDY6NTcuNjUyMzgyODkxWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIiwiY29tbWVudCI6IkZST00gZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lOjMuMjEuMCJ9LHsiY3JlYXRlZCI6IjIwMjQtMTItMTFUMDg6NDY6NTguMjMxOTQxMDExWiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIiwiY29tbWVudCI6IkZST00gbG9jYWxob3N0L2N1cmwtYmFzZS1saW51eC1hbWQ2NDo4LjExLjEifV0sIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjozZTAxODE4ZDc5Y2QzNDY3ZjFkNjBlNTQyMjRmM2Y2Y2U1MTcwZWNlYjU0ZTI2NWQ5NmJiODIzNDRiOGMyNGU3Iiwic2hhMjU2OjQ3ZjcwMGQ0NmU4YTc4ZWM1Mjc4MTA3NDYwMzQ5YjgzOTJlNDU0MDE0ZWQwYmI0OGExY2QwNGZlMTc4MWY2OWQiLCJzaGEyNTY6NWY3MGJmMThhMDg2MDA3MDE2ZTk0OGIwNGFlZDNiODIxMDNhMzZiZWE0MTc1NWI2Y2RkZmFmMTBhY2UzYzZlZiJdfSwiY29uZmlnIjp7IkNtZCI6WyJjdXJsIl0sIkhlYWx0aGNoZWNrIjp7fSwiRW50cnlwb2ludCI6WyIvZW50cnlwb2ludC5zaCJdLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iLCJDVVJMX0NBX0JVTkRMRT0vY2FjZXJ0LnBlbSJdLCJMYWJlbHMiOnsiZG9ja2VyLmNtZCI6InBvZG1hbiBydW4gLWl0IHF1YXkuaW8vY3VybC86OC4xMS4xIiwiaW8uYnVpbGRhaC52ZXJzaW9uIjoiMS4yMy4xIiwibWFpbnRhaW5lciI6IkphbWVzIEZ1bGxlciBcdTAwM2NqaW0uZnVsbGVyQHdlYmNvbXBvc2l0ZS5jb21cdTAwM2UiLCJuYW1lIjoiY3VybC1saW51eC1hbWQ2NDo4LjExLjEiLCJvcmcub3BlbmNvbnRhaW5lcnMuaW1hZ2UuZGVzY3JpcHRpb24iOiJtaW5pbWFsIGltYWdlIGZvciBjdXJsIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLmxpY2Vuc2VzIjoiTUlUIiwib3JnLm9wZW5jb250YWluZXJzLmltYWdlLnNvdXJjZSI6Imh0dHBzOi8vZ2l0aHViLmNvbS9jdXJsL2N1cmwtY29udGFpbmVyIiwidmVyc2lvbiI6IjguMTEuMSJ9LCJXb3JraW5nRGlyIjoiL2hvbWUvY3VybF91c2VyIn19", "repoDigests": ["docker.io/curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69"], "architecture": "amd64", "os": "linux", "labels": {"docker.cmd": "podman run -it quay.io/curl/:8.11.1", "io.buildah.version": "1.23.1", "maintainer": "James Fuller ", "name": "curl-linux-amd64:8.11.1", "org.opencontainers.image.description": "minimal image for curl", "org.opencontainers.image.licenses": "MIT", "org.opencontainers.image.source": "https://github.com/curl/curl-container", "version": "8.11.1"}}}, "distro": {"prettyName": "Alpine Linux v3.21", "name": "Alpine Linux", "id": "alpine", "versionID": "3.21.0", "homeURL": "https://alpinelinux.org/", "bugReportURL": "https://gitlab.alpinelinux.org/alpine/aports/-/issues"}, "descriptor": {"name": "syft", "version": "v1.55.0", "configuration": null}, "schema": {"version": "16.1.11", "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-16.1.11.json"}} \ No newline at end of file diff --git a/watcher/sbomobserver_test.go b/watcher/sbomobserver_test.go new file mode 100644 index 00000000..32bcce05 --- /dev/null +++ b/watcher/sbomobserver_test.go @@ -0,0 +1,157 @@ +package watcher + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/armosec/armoapi-go/apis" + helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/kubescape/operator/utils" + spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" + "github.com/panjf2000/ants/v2" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" +) + +// recordingObserver records what SBOMWatch tells an SBOMObserver. +type recordingObserver struct { + mu sync.Mutex + pods []string + sboms []string +} + +func (o *recordingObserver) ObservePod(eventType watch.EventType, pod *corev1.Pod) { + o.mu.Lock() + defer o.mu.Unlock() + o.pods = append(o.pods, string(eventType)+" "+pod.Namespace+"/"+pod.Name) +} + +func (o *recordingObserver) ObserveSBOM(eventType watch.EventType, sbom *spdxv1beta1.SBOMSyft) { + o.mu.Lock() + defer o.mu.Unlock() + o.sboms = append(o.sboms, string(eventType)+" "+sbom.Name) +} + +func (o *recordingObserver) seen() (pods, sboms []string) { + o.mu.Lock() + defer o.mu.Unlock() + return append([]string(nil), o.pods...), append([]string(nil), o.sboms...) +} + +func testSBOM(name string) *spdxv1beta1.SBOMSyft { + return &spdxv1beta1.SBOMSyft{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "kubescape", + Annotations: map[string]string{ + helpersv1.ImageIDMetadataKey: testImageID, + helpersv1.ImageTagMetadataKey: testImageTag, + helpersv1.StatusMetadataKey: helpersv1.Learning, + }, + }, + } +} + +// startHandleSBOMEvents runs HandleSBOMEvents until the test ends. +func startHandleSBOMEvents(t *testing.T, wh *WatchHandler) (*CooldownQueue, chan *apis.Command, chan error) { + t.Helper() + eventQueue := NewCooldownQueueWithParams(50*time.Millisecond, 10*time.Millisecond) + cmdCh := make(chan *apis.Command, 4) + errorCh := make(chan error, 4) + handlerDone := make(chan struct{}) + go func() { + defer close(handlerDone) + wh.HandleSBOMEvents(eventQueue, cmdCh, errorCh) + }() + t.Cleanup(func() { + eventQueue.Stop() + <-handlerDone + }) + return eventQueue, cmdCh, errorCh +} + +// With kubevuln off, SBOMWatch runs only for the observer: it is told about +// the SBOM, and no scan command goes out, although the Wlid is known. +func TestHandleSBOMEvents_DisabledImageScansOnlyTellTheObserver(t *testing.T) { + wh := newTestHandler(t) + observer := &recordingObserver{} + wh.SetSBOMObserver(observer) + wh.DisableImageScans() + wh.ImageToContainerData.Set(testImageHashOnly, utils.ContainerData{ContainerName: testContainerName, Wlid: testWlid}) + eventQueue, cmdCh, errorCh := startHandleSBOMEvents(t, wh) + + eventQueue.Enqueue(watch.Event{Type: watch.Added, Object: testSBOM("nginx-sbom")}) + + assert.Eventually(t, func() bool { + _, sboms := observer.seen() + return len(sboms) == 1 + }, 3*time.Second, 10*time.Millisecond) + time.Sleep(100 * time.Millisecond) + assert.Empty(t, cmdCh) + assert.Empty(t, errorCh) +} + +// An SBOM re-enqueued while its Wlid is unknown reaches the observer once, not +// once per retry. +func TestHandleSBOMEvents_TellsTheObserverOnceWhileWaitingForTheWlid(t *testing.T) { + origInit, origMax, origAttempts := sbomRetryInitialDelay, sbomRetryMaxDelay, sbomRetryMaxAttempts + sbomRetryInitialDelay = 20 * time.Millisecond + sbomRetryMaxDelay = 40 * time.Millisecond + sbomRetryMaxAttempts = 3 + t.Cleanup(func() { + sbomRetryInitialDelay = origInit + sbomRetryMaxDelay = origMax + sbomRetryMaxAttempts = origAttempts + }) + wh := newTestHandler(t) + observer := &recordingObserver{} + wh.SetSBOMObserver(observer) + eventQueue, _, errorCh := startHandleSBOMEvents(t, wh) + + eventQueue.Enqueue(watch.Event{Type: watch.Added, Object: testSBOM("nginx-sbom")}) + + select { + case err := <-errorCh: + assert.ErrorIs(t, err, ErrMissingWlid) + case <-time.After(5 * time.Second): + t.Fatal("expected the retries to run out") + } + _, sboms := observer.seen() + assert.Equal(t, []string{"ADDED nginx-sbom"}, sboms) +} + +// SBOMWatch passes its pod events to the observer, and with image scans off +// it does not resolve their workloads. +func TestSBOMWatch_TellsTheObserverAboutPods(t *testing.T) { + wh := newTestHandler(t) + observer := &recordingObserver{} + wh.SetSBOMObserver(observer) + wh.DisableImageScans() + wh.eventQueue = NewCooldownQueueWithParams(50*time.Millisecond, 10*time.Millisecond) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "nginx", Namespace: "default"}, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + ContainerStatuses: []corev1.ContainerStatus{{Name: testContainerName, ImageID: testImageID}}, + }, + } + _, err := wh.k8sAPI.KubernetesClient.CoreV1().Pods(pod.Namespace).Create(context.Background(), pod, metav1.CreateOptions{}) + assert.NoError(t, err) + + pool, err := ants.NewPoolWithFunc(1, func(interface{}) {}) + assert.NoError(t, err) + defer pool.Release() + + go wh.SBOMWatch(context.Background(), pool) + + assert.Eventually(t, func() bool { + pods, _ := observer.seen() + return len(pods) > 0 && pods[0] == "ADDED default/nginx" + }, 3*time.Second, 20*time.Millisecond) + assert.Empty(t, wh.ImageToContainerData.Get(testImageHashOnly).Wlid) +} diff --git a/watcher/sbomwatcher.go b/watcher/sbomwatcher.go index fe27de72..f0fc9cea 100644 --- a/watcher/sbomwatcher.go +++ b/watcher/sbomwatcher.go @@ -91,6 +91,12 @@ func (wh *WatchHandler) SBOMWatch(ctx context.Context, workerPool *ants.PoolWith if !ok { continue } + if wh.sbomObserver != nil { + wh.sbomObserver.ObservePod(event.Type, pod) + } + if wh.imageScansDisabled { + continue + } wlid, err := utils.GetParentIDForPod(wh.k8sAPI, pod, wh.cfg.ClusterName()) if err != nil { logger.L().Ctx(ctx).Error("failed to get wlid for pod", helpers.Error(err), helpers.String("pod", pod.Name), helpers.String("namespace", pod.Namespace)) @@ -149,6 +155,14 @@ func (wh *WatchHandler) HandleSBOMEvents(eventQueue *CooldownQueue, producedComm continue } + // a re-enqueued event (waiting for its Wlid below) was observed already + if wh.sbomObserver != nil && wh.sbomRetryAttempts.Get(obj.Namespace+"/"+obj.Name) == 0 { + wh.sbomObserver.ObserveSBOM(e.Type, obj) + } + if wh.imageScansDisabled { + continue + } + switch e.Type { case watch.Added: // diff --git a/watcher/watchhandler.go b/watcher/watchhandler.go index 861d98d3..91c3b883 100644 --- a/watcher/watchhandler.go +++ b/watcher/watchhandler.go @@ -10,7 +10,10 @@ import ( "github.com/kubescape/k8s-interface/k8sinterface" "github.com/kubescape/operator/config" "github.com/kubescape/operator/utils" + spdxv1beta1 "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1" kssc "github.com/kubescape/storage/pkg/generated/clientset/versioned" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/watch" ) const retryInterval = 1 * time.Second @@ -35,6 +38,26 @@ type WatchHandler struct { cfg config.IConfig k8sAPI *k8sinterface.KubernetesApi eventQueue *CooldownQueue + sbomObserver SBOMObserver // also told about SBOMWatch's events, when set + imageScansDisabled bool // SBOMWatch sends no scan commands (kubevuln is off) +} + +// SBOMObserver is told about the pod and SBOM events SBOMWatch receives, such +// as the SBOM exporter. +type SBOMObserver interface { + ObservePod(eventType watch.EventType, pod *corev1.Pod) + ObserveSBOM(eventType watch.EventType, sbom *spdxv1beta1.SBOMSyft) +} + +// SetSBOMObserver makes SBOMWatch pass its pod and SBOM events to o. +func (wh *WatchHandler) SetSBOMObserver(o SBOMObserver) { + wh.sbomObserver = o +} + +// DisableImageScans stops SBOMWatch from sending scan commands, for when it +// runs only for an SBOMObserver. +func (wh *WatchHandler) DisableImageScans() { + wh.imageScansDisabled = true } // NewWatchHandler creates a new WatchHandler, initializes the maps and returns it From b293ae465638eeb71be3261e826455e46e80a48b Mon Sep 17 00:00:00 2001 From: Cristian Glavan Date: Wed, 11 Nov 2026 09:03:01 +0200 Subject: [PATCH 2/6] docs(sbomexport): note that a restart forgets recently ended Jobs Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristian Glavan --- docs/features/sbom-export.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/features/sbom-export.md b/docs/features/sbom-export.md index 5eb5c2d8..850d4c7c 100644 --- a/docs/features/sbom-export.md +++ b/docs/features/sbom-export.md @@ -27,8 +27,10 @@ namespaces is converted once. An image stays counted as running in a namespace for one resync interval after its last pod there ended, so the image of a short Job is still exported once -its SBOM is ready. Namespaces the operator skips (`excludeNamespaces`, -`includeNamespaces`, their regex forms) are not exported. +its SBOM is ready. The operator keeps this in memory: the image of a Job that +ended shortly before an operator restart is not exported, unless it runs again. +Namespaces the operator skips (`excludeNamespaces`, `includeNamespaces`, their +regex forms) are not exported. ## Projects in Dependency-Track From 5219e34aa62c96055dbed96c29e7a4e8235e911e Mon Sep 17 00:00:00 2001 From: Cristian Glavan Date: Wed, 11 Nov 2026 11:24:56 +0200 Subject: [PATCH 3/6] feat(sbomexport): configurable Dependency-Track layout and an SBOM size limit Where images go in Dependency-Track is now configurable. The root project, the image project's parent and the image project are Go templates over .Cluster, .Namespace, .Repository, .Tag and .Labels (the namespace's labels), and the defaults keep the tree //:. A namespace can override them with kubescape.io/dtrack-root-project, -root-project-create, -parent, -parent-create and -project-name annotations, for example to send its images under a project that already exists. The exporter tags what it creates with managed-by:kubescape and only ever changes those projects: a root or parent with create false must exist, and a project it did not create is never given a BOM, a collection logic or a move. It records each project's image digest in a project property, so two running images that resolve to the same project and version cannot overwrite each other; the second is skipped with a log line. When a namespace's labels or layout annotations change, the exporter moves its projects there with a PATCH, keeping their history. SBOMs larger than sbomExport.maxSBOMSize (10 MiB by default) are skipped: converting takes about nine times the SBOM's size in memory. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristian Glavan --- README.md | 8 +- config/config.go | 43 ++- config/config_test.go | 35 ++- docs/features/sbom-export.md | 116 ++++++-- mainhandler/sbomexport.go | 20 +- mainhandler/sbomexport_test.go | 43 ++- sbomexport/dtrack.go | 94 +++++- sbomexport/dtrack_test.go | 10 +- sbomexport/exporter.go | 462 +++++++++++++++++++++++------ sbomexport/exporter_layout_test.go | 221 ++++++++++++++ sbomexport/exporter_test.go | 76 +++-- sbomexport/fakedtrack_test.go | 177 ++++++++--- sbomexport/index.go | 21 ++ sbomexport/layout.go | 256 ++++++++++++++++ sbomexport/layout_test.go | 129 ++++++++ sbomexport/store.go | 4 + 16 files changed, 1510 insertions(+), 205 deletions(-) create mode 100644 sbomexport/exporter_layout_test.go create mode 100644 sbomexport/layout.go create mode 100644 sbomexport/layout_test.go diff --git a/README.md b/README.md index 6e78a9a1..23251620 100644 --- a/README.md +++ b/README.md @@ -324,9 +324,11 @@ The Operator reads configuration from `/etc/config/`. When running locally, set | `excludeNamespacesRegex` | `""` | Comma-separated string or array of RE2 regex patterns for namespaces to exclude | | `namespaceFilterConfigMapName` | `""` | Optional ConfigMap name in the operator namespace for live namespace filtering | | `sbomExport.resyncInterval` | `1h` | How often the [SBOM export](docs/features/sbom-export.md) compares every running image with Dependency-Track | +| `sbomExport.maxSBOMSize` | `10485760` | Largest SBOM, in bytes, the SBOM export converts | | `sbomExport.dependencyTrack.url` | `""` | Dependency-Track API server the SBOM export sends to | | `sbomExport.dependencyTrack.apiKeyFile` | `/etc/sbom-export/api-key` | File holding the Dependency-Track API key, read on every request | -| `sbomExport.dependencyTrack.rootProject` | cluster name | Root of the SBOM export's project tree in Dependency-Track | +| `sbomExport.dependencyTrack.rootProject` | `{{ .Cluster }}` | Root project, where created parents go; see [changing the layout](docs/features/sbom-export.md#changing-the-layout) | +| `sbomExport.dependencyTrack.project` | `{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}` : `{{ .Tag }}`, parent `{{ .Cluster }}/{{ .Namespace }}` | Where each image goes; namespaces can override it with `kubescape.io/dtrack-*` annotations | ### Exclude controls from posture scans @@ -815,7 +817,7 @@ When enabled, the Operator runs an HTTPS webhook server on port `8443` that inte ### SBOM Export to Dependency-Track -The Operator sends the SBOM of every running image, as node-agent built it, to [Dependency-Track](https://dependencytrack.org): one project per image and namespace, kept current as workloads change. It needs `nodeSbomGeneration`, but neither kubevuln nor registry access. +The Operator sends the SBOM of every running image, as node-agent built it, to [Dependency-Track](https://dependencytrack.org): one project per image and namespace, kept current as workloads change. It needs `nodeSbomGeneration`, but neither kubevuln nor registry access. Where projects go is configurable, and namespaces can send their images under projects that already exist with `kubescape.io/dtrack-*` annotations. **Enable via Helm**, with the API key in a Secret you create in the Kubescape namespace: ```bash @@ -827,7 +829,7 @@ helm upgrade kubescape kubescape/kubescape-operator \ --set sbomExport.dependencyTrack.apiKeySecret.name=dependency-track ``` -See [SBOM export to Dependency-Track](docs/features/sbom-export.md) for the project tree, the settings and the permissions the API key needs. +See [SBOM export to Dependency-Track](docs/features/sbom-export.md) for the project layout and its annotations, the size limit, the settings and the permissions the API key needs. --- diff --git a/config/config.go b/config/config.go index c7062b96..4adfb423 100644 --- a/config/config.go +++ b/config/config.go @@ -165,19 +165,43 @@ type Config struct { type SBOMExportConfig struct { // ResyncInterval is how often the exporter compares what runs in the // cluster with what Dependency-Track holds. - ResyncInterval time.Duration `mapstructure:"resyncInterval"` + ResyncInterval time.Duration `mapstructure:"resyncInterval"` + // MaxSBOMSize is the largest SBOM, in bytes, the exporter converts; larger + // ones are skipped, which bounds the operator's memory. + MaxSBOMSize int64 `mapstructure:"maxSBOMSize"` DependencyTrack DependencyTrackConfig `mapstructure:"dependencyTrack"` } -// DependencyTrackConfig is where the exporter sends the SBOMs. +// DependencyTrackConfig is where the exporter sends the SBOMs, and where in +// Dependency-Track's project tree they go. Names and versions are Go +// templates over the cluster, the namespace, its labels, and the image's +// repository and tag; Namespace annotations can override them. type DependencyTrackConfig struct { // URL is the base URL of the Dependency-Track API server. URL string `mapstructure:"url"` // APIKeyFile is the file the API key is read from, on every request, so a // rotated Secret needs no restart. APIKeyFile string `mapstructure:"apiKeyFile"` - // RootProject is the root of the project tree; empty means the cluster name. - RootProject string `mapstructure:"rootProject"` + // RootProject is where the exporter creates the parents it creates. + RootProject DTCollectionConfig `mapstructure:"rootProject"` + // Project is the project of one image in one namespace. + Project DTProjectConfig `mapstructure:"project"` +} + +// DTCollectionConfig is a collection project: the root, or an image's parent. +type DTCollectionConfig struct { + Name string `mapstructure:"name"` + Version string `mapstructure:"version"` + // Create lets the exporter create the project when it is missing. Without + // it the project must exist, and the exporter never changes it. + Create bool `mapstructure:"create"` +} + +// DTProjectConfig is the project an image is uploaded to, and its parent. +type DTProjectConfig struct { + Name string `mapstructure:"name"` + Version string `mapstructure:"version"` + Parent DTCollectionConfig `mapstructure:"parent"` } // IConfig is an interface for all config types used in the operator @@ -466,9 +490,18 @@ func LoadConfig(path string) (Config, error) { viper.SetDefault("nodeAgentAutoscaler.seLinuxType", "spc_t") viper.SetDefault("nodeAgentAutoscaler.bottlerocketAutoDetect", true) - // SBOM export defaults + // SBOM export defaults: the project tree is //: viper.SetDefault("sbomExport.resyncInterval", time.Hour) + viper.SetDefault("sbomExport.maxSBOMSize", 10<<20) viper.SetDefault("sbomExport.dependencyTrack.apiKeyFile", "/etc/sbom-export/api-key") + viper.SetDefault("sbomExport.dependencyTrack.rootProject.name", "{{ .Cluster }}") + viper.SetDefault("sbomExport.dependencyTrack.rootProject.version", "current") + viper.SetDefault("sbomExport.dependencyTrack.rootProject.create", true) + viper.SetDefault("sbomExport.dependencyTrack.project.name", "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}") + viper.SetDefault("sbomExport.dependencyTrack.project.version", "{{ .Tag }}") + viper.SetDefault("sbomExport.dependencyTrack.project.parent.name", "{{ .Cluster }}/{{ .Namespace }}") + viper.SetDefault("sbomExport.dependencyTrack.project.parent.version", "current") + viper.SetDefault("sbomExport.dependencyTrack.project.parent.create", true) viper.AutomaticEnv() diff --git a/config/config_test.go b/config/config_test.go index cd5d01bd..5cbf9fae 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -181,8 +181,15 @@ func TestLoadConfig(t *testing.T) { }, SBOMExport: SBOMExportConfig{ ResyncInterval: time.Hour, + MaxSBOMSize: 10 << 20, DependencyTrack: DependencyTrackConfig{ - APIKeyFile: "/etc/sbom-export/api-key", + APIKeyFile: "/etc/sbom-export/api-key", + RootProject: DTCollectionConfig{Name: "{{ .Cluster }}", Version: "current", Create: true}, + Project: DTProjectConfig{ + Name: "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}", + Version: "{{ .Tag }}", + Parent: DTCollectionConfig{Name: "{{ .Cluster }}/{{ .Namespace }}", Version: "current", Create: true}, + }, }, }, }, @@ -657,9 +664,15 @@ func TestLoadConfigSBOMExportDefaults(t *testing.T) { cfg, err := LoadConfig(dir) require.NoError(t, err) assert.Equal(t, time.Hour, cfg.SBOMExport.ResyncInterval) + assert.Equal(t, int64(10<<20), cfg.SBOMExport.MaxSBOMSize) assert.Equal(t, "/etc/sbom-export/api-key", cfg.SBOMExport.DependencyTrack.APIKeyFile) assert.Empty(t, cfg.SBOMExport.DependencyTrack.URL) - assert.Empty(t, cfg.SBOMExport.DependencyTrack.RootProject) + assert.Equal(t, DTCollectionConfig{Name: "{{ .Cluster }}", Version: "current", Create: true}, cfg.SBOMExport.DependencyTrack.RootProject) + assert.Equal(t, DTProjectConfig{ + Name: "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}", + Version: "{{ .Tag }}", + Parent: DTCollectionConfig{Name: "{{ .Cluster }}/{{ .Namespace }}", Version: "current", Create: true}, + }, cfg.SBOMExport.DependencyTrack.Project) } func TestLoadConfigSBOMExport(t *testing.T) { @@ -669,17 +682,29 @@ func TestLoadConfigSBOMExport(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(dir, "config.json"), []byte(`{ "sbomExport": { "resyncInterval": "30m", - "dependencyTrack": {"url": "https://dtrack.example.com", "apiKeyFile": "/keys/dtrack", "rootProject": "prod"} + "maxSBOMSize": 5242880, + "dependencyTrack": { + "url": "https://dtrack.example.com", + "apiKeyFile": "/keys/dtrack", + "rootProject": {"name": "Finance", "version": "current", "create": false}, + "project": {"name": "{{ .Repository }}", "parent": {"name": "Payments Team", "create": false}} + } } }`), 0600)) cfg, err := LoadConfig(dir) require.NoError(t, err) assert.Equal(t, SBOMExportConfig{ ResyncInterval: 30 * time.Minute, + MaxSBOMSize: 5 << 20, DependencyTrack: DependencyTrackConfig{ URL: "https://dtrack.example.com", APIKeyFile: "/keys/dtrack", - RootProject: "prod", + RootProject: DTCollectionConfig{Name: "Finance", Version: "current", Create: false}, + Project: DTProjectConfig{ + Name: "{{ .Repository }}", + Version: "{{ .Tag }}", + Parent: DTCollectionConfig{Name: "Payments Team", Version: "current", Create: false}, + }, }, - }, cfg.SBOMExport) + }, cfg.SBOMExport, "unset fields keep their defaults") } diff --git a/docs/features/sbom-export.md b/docs/features/sbom-export.md index 850d4c7c..a2b6d08c 100644 --- a/docs/features/sbom-export.md +++ b/docs/features/sbom-export.md @@ -34,19 +34,81 @@ regex forms) are not exported. ## Projects in Dependency-Track +By default each cluster gets a tree: + +```text + : current collection, adds up its namespaces +└── / : current collection, adds up the latest version of each image + └── // : +``` + +The collection levels use the fixed version `current`, because +Dependency-Track 5 refuses a project without a version. An image referenced +only by digest gets the first 12 characters of the digest as its version. +Projects of images that stopped running are left as they are. The tree needs +Dependency-Track 4.13 or later (collection projects). + +### Changing the layout + +The tree is built from three projects per image, each a Go template, in the +same style as the node-agent autoscaler's DaemonSet template: + +- the **project** of the image, uploaded with its BOM; +- its **parent**, a collection; +- the **root project**, under which the operator creates parents it creates. + +Templates can use `.Cluster`, `.Namespace`, `.Repository`, `.Tag` and +`.Labels`, the namespace's labels, for example +`{{ .Cluster }}/{{ index .Labels "team" }}`. + +A namespace can override any of them with annotations. Each one replaces only +the setting it names: + +```yaml +apiVersion: v1 +kind: Namespace +metadata: + name: payments + annotations: + kubescape.io/dtrack-root-project: "Finance:current" # name:version, or a project UUID + kubescape.io/dtrack-root-project-create: "false" + kubescape.io/dtrack-parent: "Payments Team:current" + kubescape.io/dtrack-parent-create: "true" + kubescape.io/dtrack-project-name: "{{ .Repository }}" +``` + +With the default settings for every other namespace, this gives: + ```text - : current collection, adds up its namespaces -└── / : current collection, adds up the latest version of each image - └── // : +prod-cluster : current ← created by the operator (default root) +└── prod-cluster/team-a : current + └── prod-cluster/team-a/nginx : 1.27 +Finance : current ← yours, never modified +└── Payments Team : current ← created by the operator under Finance + ├── payments-api : 1.4 + └── payments-worker : 2.0 ``` -`` is `dependencyTrack.rootProject`, by default the cluster name. The two -collection levels use the fixed version `current`, because Dependency-Track 5 -refuses a project without a version. An image referenced only by digest gets -the first 12 characters of the digest as its version. Projects of images that -stopped running are left as they are. +Rules: + +| Situation | What the operator does | +|-----------|------------------------| +| The parent exists | Uploads the image's project under it. The root is not used. | +| The parent is missing, `create` is true | Creates it as a collection under the root, creating the root too when it is missing and may be created. | +| A project is missing and `create` is false, or it is named by a UUID that does not exist | Logs it once per resync and skips that namespace's images. Nothing is created. | +| A project exists that the operator did not create | Uses it, but never changes it: no collection logic, no BOM, no move. The operator tags what it creates with `managed-by:kubescape`; to let it take over projects created another way, for example by an earlier exporter, add that tag to them. | +| The image's project exists and was not created by the operator | Skips the image and logs a conflict, instead of replacing someone's BOM. | +| Two running images resolve to the same project and version | Uploads the first and skips the second, logging a conflict: one project version holds one BOM. The operator records each project's image digest in the project property `kubescape/image-digest`. When the first image stops running, the second takes the project over. | +| A namespace's labels or `kubescape.io/dtrack-*` annotations change | Moves the projects it created for that namespace's running images to their new place with a PATCH, so their history in Dependency-Track goes with them. A change made while the operator is not running is not seen; those images start new projects. | +| A template or annotation cannot be used (a typo, an unknown field, an empty result, a create flag that is not `true` or `false`) | Logs it once per resync and skips that namespace's images. Invalid default templates in `config.json` turn the export off at startup, with the reason in the log. | -The tree needs Dependency-Track 4.13 or later (collection projects). +## SBOM size + +Converting an SBOM takes about nine times its size in memory, one image at a +time. SBOMs larger than `maxSBOMSize` (10 MiB by default) are skipped and +logged once per resync. The operator checks the `kubescape.io/resource-size` +annotation before it reads the SBOM, and the document's length after. node-agent +itself stores SBOMs up to 20 MiB. ## Configuration @@ -61,16 +123,22 @@ The tree needs Dependency-Track 4.13 or later (collection projects). } ``` -`config.json`: +`config.json`, with the defaults: ```json { "sbomExport": { "resyncInterval": "1h", + "maxSBOMSize": 10485760, "dependencyTrack": { "url": "https://dtrack.example.com", "apiKeyFile": "/etc/sbom-export/api-key", - "rootProject": "" + "rootProject": {"name": "{{ .Cluster }}", "version": "current", "create": true}, + "project": { + "name": "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}", + "version": "{{ .Tag }}", + "parent": {"name": "{{ .Cluster }}/{{ .Namespace }}", "version": "current", "create": true} + } } } } @@ -79,22 +147,32 @@ The tree needs Dependency-Track 4.13 or later (collection projects). | Setting | Default | Description | |---------|---------|-------------| | `sbomExport.resyncInterval` | `1h` | How often every running image is compared with Dependency-Track; also how long an image counts as running after its last pod ended | +| `sbomExport.maxSBOMSize` | `10485760` (10 MiB) | Largest SBOM, in bytes, the operator converts | | `sbomExport.dependencyTrack.url` | *required* | Base URL of the Dependency-Track API server | | `sbomExport.dependencyTrack.apiKeyFile` | `/etc/sbom-export/api-key` | File holding the API key, read on every request, so a rotated Secret needs no restart | -| `sbomExport.dependencyTrack.rootProject` | cluster name | Root project of the tree | +| `sbomExport.dependencyTrack.rootProject.{name,version,create}` | `{{ .Cluster }}`, `current`, `true` | The root project, where created parents go | +| `sbomExport.dependencyTrack.project.{name,version}` | `{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}`, `{{ .Tag }}` | The image's project | +| `sbomExport.dependencyTrack.project.parent.{name,version,create}` | `{{ .Cluster }}/{{ .Namespace }}`, `current`, `true` | The image project's parent | Without `nodeSbomGeneration`, or with an invalid configuration, the operator logs why and runs without the export. -The team that owns the API key needs `BOM_UPLOAD`, `PROJECT_CREATION_UPLOAD`, -`VIEW_PORTFOLIO` and `PORTFOLIO_MANAGEMENT`; on Dependency-Track 5, -`PORTFOLIO_MANAGEMENT_CREATE` and `PORTFOLIO_MANAGEMENT_UPDATE` replace the -last one. They create the collection projects and set their collection logic. +## Permissions in Dependency-Track + +The team that owns the API key needs `BOM_UPLOAD`, `PROJECT_CREATION_UPLOAD` +and `VIEW_PORTFOLIO` to upload. To create root and parent projects, set their +collection logic, mark the running version latest, record the image digest and +move projects, it also needs `PORTFOLIO_MANAGEMENT`; on Dependency-Track 5, +`PORTFOLIO_MANAGEMENT_CREATE` and `PORTFOLIO_MANAGEMENT_UPDATE` replace it. With +portfolio access control on, the team also needs access to the existing +projects the layout names. A refused call is logged with the reason +Dependency-Track gave. ## Logs Every image sent logs `sent SBOM to Dependency-Track` with its project and version. Each resync logs what happened since the previous one: sent, -unchanged, skipped because no ready SBOM exists yet, and failed. A failed -export is retried with backoff, up to five times, and then at the next resync. -A refused API key is logged once per resync, with the permissions above. +unchanged, skipped because no ready SBOM exists yet, skipped as too large, +conflicts, skipped because the layout could not be used, moved, and failed. A +failed export is retried with backoff, up to five times, and then at the next +resync. A refused API key is logged once per resync, with the permissions above. diff --git a/mainhandler/sbomexport.go b/mainhandler/sbomexport.go index 13a94850..fadff038 100644 --- a/mainhandler/sbomexport.go +++ b/mainhandler/sbomexport.go @@ -28,17 +28,21 @@ func (mainHandler *MainHandler) newSBOMExporter() *sbomexport.Exporter { } // sbomExportOptions reads the exporter's options from the operator -// configuration. The root project defaults to the cluster name. +// configuration. func sbomExportOptions(cfg config.IConfig) sbomexport.Options { export := cfg.SBOMExportConfig() - rootProject := export.DependencyTrack.RootProject - if rootProject == "" { - rootProject = cfg.ClusterName() - } + dt := export.DependencyTrack return sbomexport.Options{ - URL: export.DependencyTrack.URL, - APIKeyFile: export.DependencyTrack.APIKeyFile, - RootProject: rootProject, + URL: dt.URL, + APIKeyFile: dt.APIKeyFile, + Cluster: cfg.ClusterName(), + Layout: sbomexport.Layout{ + Root: sbomexport.Collection{Name: dt.RootProject.Name, Version: dt.RootProject.Version, Create: dt.RootProject.Create}, + Parent: sbomexport.Collection{Name: dt.Project.Parent.Name, Version: dt.Project.Parent.Version, Create: dt.Project.Parent.Create}, + Name: dt.Project.Name, + Version: dt.Project.Version, + }, + MaxSBOMSize: export.MaxSBOMSize, ResyncInterval: export.ResyncInterval, SkipNamespace: cfg.SkipNamespace, } diff --git a/mainhandler/sbomexport_test.go b/mainhandler/sbomexport_test.go index 99811e8f..4de5e239 100644 --- a/mainhandler/sbomexport_test.go +++ b/mainhandler/sbomexport_test.go @@ -7,6 +7,7 @@ import ( "github.com/armosec/utils-k8s-go/armometadata" beUtils "github.com/kubescape/backend/pkg/utils" "github.com/kubescape/operator/config" + "github.com/kubescape/operator/sbomexport" "github.com/kubescape/operator/utils" kssfake "github.com/kubescape/storage/pkg/generated/clientset/versioned/fake" "github.com/stretchr/testify/assert" @@ -28,8 +29,18 @@ func sbomExportConfig(t *testing.T, capabilities config.Capabilities, export con func TestNewSBOMExporter(t *testing.T) { valid := config.SBOMExportConfig{ - ResyncInterval: time.Hour, - DependencyTrack: config.DependencyTrackConfig{URL: "https://dtrack.example.com", APIKeyFile: "/etc/sbom-export/api-key"}, + ResyncInterval: time.Hour, + MaxSBOMSize: 10 << 20, + DependencyTrack: config.DependencyTrackConfig{ + URL: "https://dtrack.example.com", + APIKeyFile: "/etc/sbom-export/api-key", + RootProject: config.DTCollectionConfig{Name: "{{ .Cluster }}", Version: "current", Create: true}, + Project: config.DTProjectConfig{ + Name: "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}", + Version: "{{ .Tag }}", + Parent: config.DTCollectionConfig{Name: "{{ .Cluster }}/{{ .Namespace }}", Version: "current", Create: true}, + }, + }, } tt := []struct { name string @@ -71,17 +82,31 @@ func TestNewSBOMExporter(t *testing.T) { } } -func TestSBOMExportOptionsDefaultTheRootProjectToTheClusterName(t *testing.T) { +func TestSBOMExportOptionsCarryTheLayoutAndTheClusterName(t *testing.T) { export := config.SBOMExportConfig{ - ResyncInterval: 30 * time.Minute, - DependencyTrack: config.DependencyTrackConfig{URL: "https://dtrack.example.com", APIKeyFile: "/keys/dtrack"}, + ResyncInterval: 30 * time.Minute, + MaxSBOMSize: 5 << 20, + DependencyTrack: config.DependencyTrackConfig{ + URL: "https://dtrack.example.com", + APIKeyFile: "/keys/dtrack", + RootProject: config.DTCollectionConfig{Name: "{{ .Cluster }}", Version: "current", Create: true}, + Project: config.DTProjectConfig{ + Name: "{{ .Repository }}", + Version: "{{ .Tag }}", + Parent: config.DTCollectionConfig{Name: "Payments Team", Version: "current", Create: false}, + }, + }, } opts := sbomExportOptions(sbomExportConfig(t, config.Capabilities{}, export)) - assert.Equal(t, "prod-cluster", opts.RootProject) + assert.Equal(t, "prod-cluster", opts.Cluster) assert.Equal(t, "https://dtrack.example.com", opts.URL) assert.Equal(t, "/keys/dtrack", opts.APIKeyFile) + assert.Equal(t, int64(5<<20), opts.MaxSBOMSize) assert.Equal(t, 30*time.Minute, opts.ResyncInterval) - - export.DependencyTrack.RootProject = "prod" - assert.Equal(t, "prod", sbomExportOptions(sbomExportConfig(t, config.Capabilities{}, export)).RootProject) + assert.Equal(t, sbomexport.Layout{ + Root: sbomexport.Collection{Name: "{{ .Cluster }}", Version: "current", Create: true}, + Parent: sbomexport.Collection{Name: "Payments Team", Version: "current", Create: false}, + Name: "{{ .Repository }}", + Version: "{{ .Tag }}", + }, opts.Layout) } diff --git a/sbomexport/dtrack.go b/sbomexport/dtrack.go index 2199babd..b7834495 100644 --- a/sbomexport/dtrack.go +++ b/sbomexport/dtrack.go @@ -15,6 +15,17 @@ import ( "time" ) +const ( + // managedTag marks the projects the exporter created. It changes no other + // project: no collection logic, no BOM, no move. + managedTag = "managed-by:kubescape" + + // The project property that records which image digest a project's BOM + // came from, so two images cannot overwrite each other's project. + digestPropertyGroup = "kubescape" + digestPropertyName = "image-digest" +) + // project is the part of a Dependency-Track project the exporter reads. type project struct { UUID string `json:"uuid"` @@ -23,6 +34,21 @@ type project struct { IsLatest bool `json:"isLatest"` CollectionLogic string `json:"collectionLogic"` LastBOMImport epochMillis `json:"lastBomImport"` + Tags []tag `json:"tags"` +} + +type tag struct { + Name string `json:"name"` +} + +// managed reports whether the exporter created p. +func (p *project) managed() bool { + for _, t := range p.Tags { + if strings.EqualFold(t.Name, managedTag) { + return true + } + } + return false } // epochMillis is a Dependency-Track timestamp. It decodes from epoch @@ -160,10 +186,33 @@ func (c *dtClient) lookup(ctx context.Context, name, version string) (*project, return &p, nil } +// getByUUID returns the project with uuid, or nil when it does not exist. +// Dependency-Track answers 400 for some UUIDs it does not know. +func (c *dtClient) getByUUID(ctx context.Context, uuid string) (*project, error) { + var p project + status, err := c.do(ctx, http.MethodGet, "/api/v1/project/"+url.PathEscape(uuid), nil, &p, http.StatusOK, http.StatusNotFound, http.StatusBadRequest) + if err != nil { + return nil, err + } + if status != http.StatusOK { + return nil, nil + } + return &p, nil +} + +// find returns the project ref points at, or nil when it does not exist. +func (c *dtClient) find(ctx context.Context, ref projectRef) (*project, error) { + if ref.uuid != "" { + return c.getByUUID(ctx, ref.uuid) + } + return c.lookup(ctx, ref.name, ref.version) +} + // createCollection creates the collection project name:version with the given -// collection logic, under parentUUID unless it is empty. +// collection logic, under parentUUID unless it is empty, tagged as created by +// the exporter. func (c *dtClient) createCollection(ctx context.Context, name, version, parentUUID, logic string) (*project, error) { - body := map[string]any{"name": name, "version": version, "collectionLogic": logic} + body := map[string]any{"name": name, "version": version, "collectionLogic": logic, "tags": []tag{{Name: managedTag}}} if parentUUID != "" { body["parent"] = map[string]string{"uuid": parentUUID} } @@ -182,14 +231,15 @@ func (c *dtClient) patch(ctx context.Context, uuid string, fields map[string]any } // uploadBOM uploads bom as the latest version of name:version, creating the -// project under parentUUID when it does not exist. Dependency-Track processes -// the BOM asynchronously after answering. +// project under parentUUID, tagged as created by the exporter, when it does +// not exist. Dependency-Track processes the BOM asynchronously after answering. func (c *dtClient) uploadBOM(ctx context.Context, name, version, parentUUID string, bom []byte) error { body := map[string]any{ "projectName": name, "projectVersion": version, "autoCreate": true, "isLatest": true, + "projectTags": []tag{{Name: managedTag}}, "bom": base64.StdEncoding.EncodeToString(bom), } if parentUUID != "" { @@ -198,3 +248,39 @@ func (c *dtClient) uploadBOM(ctx context.Context, name, version, parentUUID stri _, err := c.do(ctx, http.MethodPut, "/api/v1/bom", body, nil, http.StatusOK) return err } + +// projectProperty is a Dependency-Track project property. +type projectProperty struct { + GroupName string `json:"groupName"` + PropertyName string `json:"propertyName"` + PropertyValue string `json:"propertyValue"` + PropertyType string `json:"propertyType"` +} + +// digestOwner returns the image digest recorded on the project with uuid, or +// "" when none is. +func (c *dtClient) digestOwner(ctx context.Context, uuid string) (string, error) { + var props []projectProperty + if _, err := c.do(ctx, http.MethodGet, "/api/v1/project/"+url.PathEscape(uuid)+"/property", nil, &props, http.StatusOK); err != nil { + return "", err + } + for _, p := range props { + if p.GroupName == digestPropertyGroup && p.PropertyName == digestPropertyName { + return p.PropertyValue, nil + } + } + return "", nil +} + +// setDigestOwner records digest on the project with uuid: PUT creates the +// property, and answers 409 when it exists, which POST then updates. +func (c *dtClient) setDigestOwner(ctx context.Context, uuid, digest string) error { + body := projectProperty{GroupName: digestPropertyGroup, PropertyName: digestPropertyName, PropertyValue: digest, PropertyType: "STRING"} + path := "/api/v1/project/" + url.PathEscape(uuid) + "/property" + status, err := c.do(ctx, http.MethodPut, path, body, nil, http.StatusCreated, http.StatusOK, http.StatusConflict) + if err != nil || status != http.StatusConflict { + return err + } + _, err = c.do(ctx, http.MethodPost, path, body, nil, http.StatusOK, http.StatusNotModified) + return err +} diff --git a/sbomexport/dtrack_test.go b/sbomexport/dtrack_test.go index 2ca96e09..c131357c 100644 --- a/sbomexport/dtrack_test.go +++ b/sbomexport/dtrack_test.go @@ -23,16 +23,16 @@ func TestDTClientLooksUpCreatesAndPatchesProjects(t *testing.T) { require.NoError(t, err) assert.Nil(t, p, "a missing project is not an error") - root, err := c.createCollection(ctx, "cluster", "current", "", clusterLogic) + root, err := c.createCollection(ctx, "cluster", "current", "", rootLogic) require.NoError(t, err) - ns, err := c.createCollection(ctx, "cluster/apps", "current", root.UUID, namespaceLogic) + ns, err := c.createCollection(ctx, "cluster/apps", "current", root.UUID, parentLogic) require.NoError(t, err) - require.NoError(t, c.patch(ctx, ns.UUID, map[string]any{"collectionLogic": clusterLogic})) + require.NoError(t, c.patch(ctx, ns.UUID, map[string]any{"collectionLogic": rootLogic})) got, parent, ok := f.project("cluster/apps", "current") require.True(t, ok) assert.Equal(t, "cluster", parent) - assert.Equal(t, clusterLogic, got.CollectionLogic) + assert.Equal(t, rootLogic, got.CollectionLogic) p, err = c.lookup(ctx, "cluster/apps", "current") require.NoError(t, err) @@ -44,7 +44,7 @@ func TestDTClientUploadsTheBOMUnderItsParent(t *testing.T) { now := time.Date(2026, 11, 10, 12, 0, 0, 0, time.UTC) f, srv, keyFile := newFakeDT(t, func() time.Time { return now }) c := &dtClient{baseURL: srv.URL, apiKeyFile: keyFile, http: srv.Client()} - ns, err := c.createCollection(ctx, "cluster/apps", "current", "", namespaceLogic) + ns, err := c.createCollection(ctx, "cluster/apps", "current", "", parentLogic) require.NoError(t, err) require.NoError(t, c.uploadBOM(ctx, "cluster/apps/redis", "8", ns.UUID, curlCycloneDX)) diff --git a/sbomexport/exporter.go b/sbomexport/exporter.go index d567c189..a0b5db85 100644 --- a/sbomexport/exporter.go +++ b/sbomexport/exporter.go @@ -2,11 +2,16 @@ // objects) to Dependency-Track, one project per running image and namespace, // and keeps them current as workloads change. // -// The projects form a tree under a root project, by default the cluster name: +// By default the projects form a tree under the cluster: // -// : current collection, adds up its namespaces -// └── / : current collection, adds up the latest version of each image -// └── // : +// : current collection, adds up its namespaces +// └── / : current collection, adds up the latest version of each image +// └── // : +// +// The names, versions and parents are templates (Layout), and a namespace can +// override them with annotations, for example to send its images under a +// project that already exists. The exporter only ever creates or changes +// projects it created itself, which it tags with "managed-by:kubescape". // // Two things trigger an export: SBOMWatch's events (an SBOM becomes ready, a // pod starts with an image whose SBOM is ready), and a resync that compares @@ -19,6 +24,7 @@ import ( "context" "errors" "fmt" + "maps" "net/http" "net/url" "strings" @@ -34,25 +40,28 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" + coreinformers "k8s.io/client-go/informers/core/v1" "k8s.io/client-go/kubernetes" + corelisters "k8s.io/client-go/listers/core/v1" + "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/pager" "k8s.io/client-go/util/workqueue" ) const ( - // collectionVersion is the version of the two collection levels: - // Dependency-Track 5 refuses a project without a version. - collectionVersion = "current" - - // The cluster adds up its namespaces; a namespace adds up the latest - // version of each of its images, so the running tag is what counts. - clusterLogic = "AGGREGATE_DIRECT_CHILDREN" - namespaceLogic = "AGGREGATE_LATEST_VERSION_CHILDREN" + // A root adds up its children; a parent adds up the latest version of each + // of its images, so the running tag is what counts. + rootLogic = "AGGREGATE_DIRECT_CHILDREN" + parentLogic = "AGGREGATE_LATEST_VERSION_CHILDREN" - // maxRetries bounds the retries of one image between two resyncs. + // maxRetries bounds the retries of one item between two resyncs. maxRetries = 5 requestTimeout = 2 * time.Minute + + // moveKeyPrefix marks queue items that move a namespace's projects after + // its layout changed; every other item is an image digest. + moveKeyPrefix = "namespace/" ) // Options configures an Exporter. @@ -61,8 +70,13 @@ type Options struct { URL string // APIKeyFile holds the API key; it is read on every request. APIKeyFile string - // RootProject is the root of the project tree. - RootProject string + // Cluster is the cluster name the layout templates see as .Cluster. + Cluster string + // Layout is where images go by default. + Layout Layout + // MaxSBOMSize is the largest SBOM, in bytes, that is converted. Converting + // takes about nine times the SBOM's size in memory, so this bounds it. + MaxSBOMSize int64 // ResyncInterval is how often every running image is compared with // Dependency-Track. It is also how long an image counts as running in a // namespace after its last pod there ended. @@ -79,8 +93,8 @@ func (o Options) validate() error { if o.APIKeyFile == "" { return errors.New("sbomExport.dependencyTrack.apiKeyFile is empty") } - if o.RootProject == "" { - return errors.New("sbomExport.dependencyTrack.rootProject is empty and the cluster has no name") + if o.MaxSBOMSize <= 0 { + return fmt.Errorf("sbomExport.maxSBOMSize %d must be positive", o.MaxSBOMSize) } if o.ResyncInterval <= 0 { return fmt.Errorf("sbomExport.resyncInterval %s must be positive", o.ResyncInterval) @@ -95,18 +109,24 @@ type Exporter struct { pods kubernetes.Interface sboms sbomStore dt *dtClient + layout *layout running *index - queue workqueue.TypedRateLimitingInterface[string] // image digests + queue workqueue.TypedRateLimitingInterface[string] // image digests and move keys + + namespaceInformer cache.SharedIndexInformer + namespaceLister corelisters.NamespaceLister - mu sync.Mutex - sbomByImage map[string]sbomMeta // digest → its SBOMSyft - collections map[string]string // collection project name → uuid, emptied on every resync + mu sync.Mutex + sbomByImage map[string]sbomMeta // digest → its SBOMSyft + projects map[string]string // collection reference → uuid, emptied on every resync + pendingMoves map[string]namespaceInfo // namespace → what it was before its layout changed + logged map[string]bool // problems logged since the last resync stats stats } -// New returns an Exporter that lists pods with pods and reads SBOMSyft -// objects with storage. +// New returns an Exporter that lists pods and watches namespaces with pods, +// and reads SBOMSyft objects with storage. func New(opts Options, pods kubernetes.Interface, storage kssc.Interface) (*Exporter, error) { return newExporter(opts, pods, storageSBOMs{client: storage}, &http.Client{Timeout: requestTimeout}, time.Now) } @@ -115,22 +135,44 @@ func newExporter(opts Options, pods kubernetes.Interface, sboms sbomStore, httpC if err := opts.validate(); err != nil { return nil, err } + l, err := newLayout(opts.Cluster, opts.Layout) + if err != nil { + return nil, fmt.Errorf("sbomExport.dependencyTrack layout: %w", err) + } if opts.SkipNamespace == nil { opts.SkipNamespace = func(string) bool { return false } } - return &Exporter{ + informer := coreinformers.NewNamespaceInformer(pods, 0, cache.Indexers{}) + e := &Exporter{ opts: opts, pods: pods, sboms: sboms, dt: &dtClient{baseURL: strings.TrimRight(opts.URL, "/"), apiKeyFile: opts.APIKeyFile, http: httpClient}, + layout: l, running: newIndex(opts.ResyncInterval, now), queue: workqueue.NewTypedRateLimitingQueueWithConfig( workqueue.NewTypedItemExponentialFailureRateLimiter[string](5*time.Second, 5*time.Minute), workqueue.TypedRateLimitingQueueConfig[string]{Name: "sbom-export"}, ), - sbomByImage: map[string]sbomMeta{}, - collections: map[string]string{}, - }, nil + namespaceInformer: informer, + namespaceLister: corelisters.NewNamespaceLister(informer.GetIndexer()), + sbomByImage: map[string]sbomMeta{}, + projects: map[string]string{}, + pendingMoves: map[string]namespaceInfo{}, + logged: map[string]bool{}, + } + if _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + UpdateFunc: func(oldObj, newObj any) { + oldNS, oldOK := oldObj.(*corev1.Namespace) + newNS, newOK := newObj.(*corev1.Namespace) + if oldOK && newOK { + e.namespaceChanged(oldNS, newNS) + } + }, + }); err != nil { + return nil, err + } + return e, nil } // ObservePod records a pod event from SBOMWatch's pod watch, which only sees @@ -175,18 +217,59 @@ func (e *Exporter) ObserveSBOM(eventType watch.EventType, obj *spdxv1beta1.SBOMS } } -// Start exports until ctx is done: one worker drains the queue, and a resync -// runs now and every ResyncInterval. +// namespaceChanged queues a move of the namespace's projects when a change of +// its labels or of its kubescape.io/dtrack-* annotations can change where its +// images go. The first state before the move runs is the one moved from. +func (e *Exporter) namespaceChanged(oldNS, newNS *corev1.Namespace) { + if maps.Equal(oldNS.Labels, newNS.Labels) && maps.Equal(layoutAnnotations(oldNS), layoutAnnotations(newNS)) { + return + } + e.mu.Lock() + if _, ok := e.pendingMoves[newNS.Name]; !ok { + e.pendingMoves[newNS.Name] = infoOf(oldNS) + } + e.mu.Unlock() + e.queue.Add(moveKeyPrefix + newNS.Name) +} + +func layoutAnnotations(ns *corev1.Namespace) map[string]string { + out := map[string]string{} + for k, v := range ns.Annotations { + if strings.HasPrefix(k, "kubescape.io/dtrack-") { + out[k] = v + } + } + return out +} + +func infoOf(ns *corev1.Namespace) namespaceInfo { + return namespaceInfo{name: ns.Name, labels: maps.Clone(ns.Labels), annotations: maps.Clone(ns.Annotations)} +} + +// namespaceInfo returns the namespace's labels and annotations, or none when +// it is not known (yet). +func (e *Exporter) namespaceInfo(name string) namespaceInfo { + if ns, err := e.namespaceLister.Get(name); err == nil { + return infoOf(ns) + } + return namespaceInfo{name: name} +} + +// Start exports until ctx is done: it watches namespaces, one worker drains +// the queue, and a resync runs now and every ResyncInterval. func (e *Exporter) Start(ctx context.Context) { go func() { <-ctx.Done() e.queue.ShutDown() }() + go e.namespaceInformer.Run(ctx.Done()) + if !cache.WaitForCacheSync(ctx.Done(), e.namespaceInformer.HasSynced) { + return + } go e.work(ctx) logger.L().Info("starting SBOM export to Dependency-Track", helpers.String("url", e.opts.URL), - helpers.String("rootProject", e.opts.RootProject), helpers.String("resyncInterval", e.opts.ResyncInterval.String())) ticker := time.NewTicker(e.opts.ResyncInterval) @@ -205,22 +288,27 @@ func (e *Exporter) Start(ctx context.Context) { func (e *Exporter) work(ctx context.Context) { for { - digest, shutdown := e.queue.Get() + key, shutdown := e.queue.Get() if shutdown { return } - err := e.process(ctx, digest) + var err error + if ns, ok := strings.CutPrefix(key, moveKeyPrefix); ok { + err = e.move(ctx, ns) + } else { + err = e.process(ctx, key) + } switch { case err == nil: - e.queue.Forget(digest) - case e.queue.NumRequeues(digest) < maxRetries && ctx.Err() == nil: - e.queue.AddRateLimited(digest) + e.queue.Forget(key) + case e.queue.NumRequeues(key) < maxRetries && ctx.Err() == nil: + e.queue.AddRateLimited(key) default: - e.queue.Forget(digest) - logger.L().Ctx(ctx).Warning("giving up on exporting this SBOM until the next resync", - helpers.String("digest", digest), helpers.Error(err)) + e.queue.Forget(key) + logger.L().Ctx(ctx).Warning("giving up on this SBOM export item until the next resync", + helpers.String("item", key), helpers.Error(err)) } - e.queue.Done(digest) + e.queue.Done(key) } } @@ -229,6 +317,9 @@ func (e *Exporter) work(ctx context.Context) { // running image. If either list fails, nothing changes. func (e *Exporter) resync(ctx context.Context) error { logger.L().Info("SBOM export since the last resync", e.stats.takeFields()...) + e.mu.Lock() + e.logged = map[string]bool{} + e.mu.Unlock() pods := map[string]podEntry{} err := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { @@ -258,7 +349,7 @@ func (e *Exporter) resync(ctx context.Context) error { } e.mu.Lock() e.sbomByImage = byImage - e.collections = map[string]string{} + e.projects = map[string]string{} e.mu.Unlock() for _, d := range e.running.digests() { @@ -274,6 +365,17 @@ func (e *Exporter) sbom(digest string) (sbomMeta, bool) { return m, ok } +// result is what exporting one image to one place did. +type result int + +const ( + resultSent result = iota + resultUnchanged + resultConflict // the project belongs to another image or was not created by the exporter + resultSkipped // the layout cannot be resolved, or a project it names is missing + resultTooLarge +) + // process exports the SBOM of one image to the project of every namespace it // runs in. One namespace failing does not stop the others; the image is // retried as a whole, and namespaces already done are then only looked up. @@ -287,100 +389,280 @@ func (e *Exporter) process(ctx context.Context, digest string) error { e.stats.skipped.Add(1) return nil } - - root, err := e.collection(ctx, e.opts.RootProject, "", clusterLogic) - if err != nil { - e.stats.failed.Add(1) - e.logFailure(ctx, err, m.imageTag, e.opts.RootProject, collectionVersion) - return err + if m.size > e.opts.MaxSBOMSize { + e.stats.tooLarge.Add(1) + e.logTooLarge(ctx, m, m.size) + return nil } - repository, version := splitRef(m.imageTag) + repository, tag := splitRef(m.imageTag) var bom []byte // converted once, for every namespace var errs []error for _, ns := range namespaces { - nsProject := e.opts.RootProject + "/" + ns - name := nsProject + "/" + repository - sent, err := e.exportTo(ctx, m, name, version, nsProject, root, &bom) + p, err := e.layout.resolve(e.namespaceInfo(ns), repository, tag) + if err != nil { + e.stats.skippedLayout.Add(1) + e.logOnce(ctx, "layout/"+ns, "cannot resolve the Dependency-Track layout of this namespace, skipping its images", + helpers.String("namespace", ns), helpers.Error(err)) + continue + } + res, err := e.exportTo(ctx, m, ns, p, &bom) switch { case err != nil: e.stats.failed.Add(1) - e.logFailure(ctx, err, m.imageTag, name, version) + e.logFailure(ctx, err, m.imageTag, p.name, p.version) errs = append(errs, err) - case sent: + case res == resultSent: e.stats.sent.Add(1) - logger.L().Info("sent SBOM to Dependency-Track", helpers.String("project", name), helpers.String("version", version)) - default: + logger.L().Info("sent SBOM to Dependency-Track", helpers.String("project", p.name), helpers.String("version", p.version)) + case res == resultUnchanged: e.stats.unchanged.Add(1) + case res == resultConflict: + e.stats.conflicts.Add(1) + case res == resultSkipped: + e.stats.skippedLayout.Add(1) + case res == resultTooLarge: + e.stats.tooLarge.Add(1) + return nil } } return errors.Join(errs...) } -// exportTo makes name:version hold m's SBOM as its latest version. It reports -// whether it uploaded: an image whose project imported a BOM no older than the -// SBOMSyft is only marked latest. -func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, name, version, nsProject, rootUUID string, bom *[]byte) (bool, error) { - p, err := e.dt.lookup(ctx, name, version) +// exportTo makes p's project hold m's SBOM as its latest version. An image +// whose project imported a BOM no older than its SBOMSyft is only marked +// latest. It never uploads to a project it did not create, nor to one that +// holds the BOM of another image that still runs. +func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placement, bom *[]byte) (result, error) { + existing, err := e.dt.lookup(ctx, p.name, p.version) if err != nil { - return false, err + return 0, err } - if p != nil && !p.LastBOMImport.Time().Before(m.created) { - if !p.IsLatest { - if err := e.dt.patch(ctx, p.UUID, map[string]any{"isLatest": true}); err != nil { - return false, err + parentUUID := "" + if existing != nil { + if !existing.managed() { + e.logOnce(ctx, "conflict/"+p.name+":"+p.version, "a Dependency-Track project with this name and version exists and was not created by Kubescape, skipping the image", + helpers.String("project", p.name), helpers.String("version", p.version), helpers.String("image", m.imageTag), helpers.String("namespace", ns)) + return resultConflict, nil + } + owner, err := e.dt.digestOwner(ctx, existing.UUID) + if err != nil { + return 0, err + } + if owner != "" && owner != m.digest && len(e.running.namespaces(owner)) > 0 { + e.logOnce(ctx, "conflict/"+p.name+":"+p.version, "two running images map to the same Dependency-Track project and version, skipping the second; include the repository in the project name template", + helpers.String("project", p.name), helpers.String("version", p.version), helpers.String("image", m.imageTag), helpers.String("namespace", ns), + helpers.String("ownerDigest", owner), helpers.String("digest", m.digest)) + return resultConflict, nil + } + if owner == m.digest && !existing.LastBOMImport.Time().Before(m.created) { + if !existing.IsLatest { + if err := e.dt.patch(ctx, existing.UUID, map[string]any{"isLatest": true}); err != nil { + return 0, err + } } + return resultUnchanged, nil + } + } else { + parentUUID, err = e.parent(ctx, p) + var missing *missingProjectError + if errors.As(err, &missing) { + e.logOnce(ctx, "missing/"+missing.ref.String(), "a Dependency-Track project the layout names does not exist and may not be created, skipping the namespace's images", + helpers.String("project", missing.ref.String()), helpers.String("reason", missing.reason), helpers.String("namespace", ns)) + return resultSkipped, nil + } + if err != nil { + return 0, err } - return false, nil - } - nsUUID, err := e.collection(ctx, nsProject, rootUUID, namespaceLogic) - if err != nil { - return false, err } + if *bom == nil { doc, err := e.sboms.syftDocument(ctx, m.namespace, m.name) if err != nil { - return false, err + return 0, err + } + if int64(len(doc)) > e.opts.MaxSBOMSize { + e.logTooLarge(ctx, m, int64(len(doc))) + return resultTooLarge, nil } if *bom, err = toCycloneDX(doc); err != nil { - return false, fmt.Errorf("convert SBOMSyft %s/%s: %w", m.namespace, m.name, err) + return 0, fmt.Errorf("convert SBOMSyft %s/%s: %w", m.namespace, m.name, err) } } - return true, e.dt.uploadBOM(ctx, name, version, nsUUID, *bom) + if err := e.dt.uploadBOM(ctx, p.name, p.version, parentUUID, *bom); err != nil { + return 0, err + } + + // Record which image the project holds. The upload created the project + // when it was missing; tag it in case Dependency-Track ignored projectTags. + uploaded, err := e.dt.lookup(ctx, p.name, p.version) + if err != nil { + return 0, err + } + if uploaded == nil { + return 0, fmt.Errorf("project %s:%s is missing after the upload", p.name, p.version) + } + if !uploaded.managed() { + if err := e.dt.patch(ctx, uploaded.UUID, map[string]any{"tags": append(uploaded.Tags, tag{Name: managedTag})}); err != nil { + return 0, err + } + } + return resultSent, e.dt.setDigestOwner(ctx, uploaded.UUID, m.digest) +} + +// missingProjectError is a project the layout names that does not exist and +// may not be created. +type missingProjectError struct { + ref projectRef + reason string +} + +func (e *missingProjectError) Error() string { + return fmt.Sprintf("project %s %s", e.ref, e.reason) +} + +// parent returns the uuid of p's parent, creating it, and the root above it, +// when they are missing and may be created. +func (e *Exporter) parent(ctx context.Context, p placement) (string, error) { + return e.collection(ctx, p.parent, p.parentCreate, parentLogic, func() (string, error) { + return e.collection(ctx, p.root, p.rootCreate, rootLogic, nil) + }) } -// collection returns the uuid of the collection project name:current, -// creating it under parentUUID, or setting its collection logic, when needed. -func (e *Exporter) collection(ctx context.Context, name, parentUUID, logic string) (string, error) { +// collection returns the uuid of the collection project ref names. A missing +// one is created, under the project parentOf returns, only when create allows +// it. The collection logic is set only on projects the exporter created. +func (e *Exporter) collection(ctx context.Context, ref projectRef, create bool, logic string, parentOf func() (string, error)) (string, error) { + key := ref.String() e.mu.Lock() - uuid, ok := e.collections[name] + uuid, ok := e.projects[key] e.mu.Unlock() if ok { return uuid, nil } - p, err := e.dt.lookup(ctx, name, collectionVersion) + p, err := e.dt.find(ctx, ref) if err != nil { return "", err } switch { - case p == nil: - if p, err = e.dt.createCollection(ctx, name, collectionVersion, parentUUID, logic); err != nil { - return "", err + case p != nil: + if p.managed() && p.CollectionLogic != logic { + if err := e.dt.patch(ctx, p.UUID, map[string]any{"collectionLogic": logic}); err != nil { + return "", err + } + logger.L().Info("set collection logic of Dependency-Track project", helpers.String("project", key), helpers.String("collectionLogic", logic)) + } + case ref.uuid != "": + return "", &missingProjectError{ref: ref, reason: "does not exist; a project named by UUID is never created"} + case !create: + return "", &missingProjectError{ref: ref, reason: "does not exist and create is false"} + default: + parentUUID := "" + if parentOf != nil { + if parentUUID, err = parentOf(); err != nil { + return "", err + } } - logger.L().Info("created Dependency-Track collection project", helpers.String("project", name), helpers.String("collectionLogic", logic)) - case p.CollectionLogic != logic: - if err := e.dt.patch(ctx, p.UUID, map[string]any{"collectionLogic": logic}); err != nil { + if p, err = e.dt.createCollection(ctx, ref.name, ref.version, parentUUID, logic); err != nil { return "", err } - logger.L().Info("set collection logic of Dependency-Track project", helpers.String("project", name), helpers.String("collectionLogic", logic)) + logger.L().Info("created Dependency-Track collection project", helpers.String("project", key), helpers.String("collectionLogic", logic)) } e.mu.Lock() - e.collections[name] = p.UUID + e.projects[key] = p.UUID e.mu.Unlock() return p.UUID, nil } +// move moves the projects of the images running in ns from where the +// namespace's previous labels and annotations put them to where the current +// ones do, so their history in Dependency-Track moves with them. It moves only +// projects the exporter created, and never onto a project that exists; the +// images are then exported as usual. A change made while the operator was not +// running is not seen, so those images start new projects. +func (e *Exporter) move(ctx context.Context, ns string) error { + e.mu.Lock() + before, ok := e.pendingMoves[ns] + e.mu.Unlock() + if !ok { + return nil + } + now := e.namespaceInfo(ns) + digests := e.running.digestsIn(ns) + var errs []error + for _, d := range digests { + m, ok := e.sbom(d) + if !ok { + continue + } + repository, tag := splitRef(m.imageTag) + from, err1 := e.layout.resolve(before, repository, tag) + to, err2 := e.layout.resolve(now, repository, tag) + if err1 != nil || err2 != nil || (from.name == to.name && from.version == to.version && from.parent == to.parent) { + continue + } + if err := e.moveProject(ctx, ns, from, to); err != nil { + errs = append(errs, err) + } + } + if err := errors.Join(errs...); err != nil { + return err + } + e.mu.Lock() + delete(e.pendingMoves, ns) + e.mu.Unlock() + for _, d := range digests { + e.queue.Add(d) + } + return nil +} + +func (e *Exporter) moveProject(ctx context.Context, ns string, from, to placement) error { + p, err := e.dt.lookup(ctx, from.name, from.version) + if err != nil || p == nil || !p.managed() { + return err + } + target, err := e.dt.lookup(ctx, to.name, to.version) + if err != nil || target != nil { + return err + } + parentUUID, err := e.parent(ctx, to) + var missing *missingProjectError + if errors.As(err, &missing) { + e.logOnce(ctx, "missing/"+missing.ref.String(), "a Dependency-Track project the layout names does not exist and may not be created, skipping the namespace's images", + helpers.String("project", missing.ref.String()), helpers.String("reason", missing.reason), helpers.String("namespace", ns)) + return nil + } + if err != nil { + return err + } + if err := e.dt.patch(ctx, p.UUID, map[string]any{"name": to.name, "version": to.version, "parent": map[string]string{"uuid": parentUUID}}); err != nil { + return err + } + e.stats.moved.Add(1) + logger.L().Info("moved Dependency-Track project after the namespace's layout changed", + helpers.String("namespace", ns), helpers.String("from", from.name+":"+from.version), helpers.String("to", to.name+":"+to.version)) + return nil +} + +func (e *Exporter) logTooLarge(ctx context.Context, m sbomMeta, size int64) { + e.logOnce(ctx, "too-large/"+m.digest, "SBOM is larger than sbomExport.maxSBOMSize, skipping it", + helpers.String("image", m.imageTag), helpers.Int("size", int(size)), helpers.Int("maxSBOMSize", int(e.opts.MaxSBOMSize))) +} + +// logOnce logs a warning the first time key comes up after a resync, so a +// problem that affects every image is logged once, not once per image. +func (e *Exporter) logOnce(ctx context.Context, key, msg string, fields ...helpers.IDetails) { + e.mu.Lock() + seen := e.logged[key] + e.logged[key] = true + e.mu.Unlock() + if !seen { + logger.L().Ctx(ctx).Warning(msg, fields...) + } +} + // logFailure logs a failed export of image, at project name:version. A refused // API key fails every image the same way, so it is logged once per resync. func (e *Exporter) logFailure(ctx context.Context, err error, image, name, version string) { @@ -389,15 +671,15 @@ func (e *Exporter) logFailure(ctx context.Context, err error, image, name, versi if e.stats.authLogged.Swap(true) { return } - fields = append(fields, helpers.String("hint", "the API key's team needs BOM_UPLOAD, PROJECT_CREATION_UPLOAD, VIEW_PORTFOLIO and PORTFOLIO_MANAGEMENT (on 5.x: PORTFOLIO_MANAGEMENT_CREATE and PORTFOLIO_MANAGEMENT_UPDATE)")) + fields = append(fields, helpers.String("hint", "the API key's team needs BOM_UPLOAD, PROJECT_CREATION_UPLOAD and VIEW_PORTFOLIO, plus PORTFOLIO_MANAGEMENT (on 5.x: PORTFOLIO_MANAGEMENT_CREATE and PORTFOLIO_MANAGEMENT_UPDATE) to create and update projects; with portfolio access control on, access to the projects the layout names")) } logger.L().Ctx(ctx).Error("failed to export SBOM to Dependency-Track", fields...) } // stats counts what the exporter did between two resyncs. type stats struct { - sent, unchanged, skipped, failed atomic.Int64 - authLogged atomic.Bool // a refused API key was logged + sent, unchanged, skipped, tooLarge, conflicts, skippedLayout, moved, failed atomic.Int64 + authLogged atomic.Bool // a refused API key was logged } // takeFields returns the counts as log fields and resets them. @@ -407,6 +689,10 @@ func (s *stats) takeFields() []helpers.IDetails { helpers.Int("sent", int(s.sent.Swap(0))), helpers.Int("unchanged", int(s.unchanged.Swap(0))), helpers.Int("skippedNoReadySBOM", int(s.skipped.Swap(0))), + helpers.Int("skippedTooLarge", int(s.tooLarge.Swap(0))), + helpers.Int("conflicts", int(s.conflicts.Swap(0))), + helpers.Int("skippedLayout", int(s.skippedLayout.Swap(0))), + helpers.Int("moved", int(s.moved.Swap(0))), helpers.Int("failed", int(s.failed.Swap(0))), } } diff --git a/sbomexport/exporter_layout_test.go b/sbomexport/exporter_layout_test.go new file mode 100644 index 00000000..354486fe --- /dev/null +++ b/sbomexport/exporter_layout_test.go @@ -0,0 +1,221 @@ +package sbomexport + +import ( + "context" + "strconv" + "testing" + "time" + + helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/watch" +) + +const nginxImageID = "docker.io/library/nginx@sha256:2222" + +func TestExporterSendsANamespaceUnderAnExistingParentAndLeavesItAlone(t *testing.T) { + te := newTestExporter(t, + namespace("payments", map[string]string{ + AnnotationParent: "Payments Team:current", + AnnotationParentCreate: "false", + AnnotationProjectName: "{{ .Repository }}", + }), + runningPod("payments", "api", redisImageID), + ) + finance := te.dt.addProject("Finance", "current", "") + te.dt.addProject("Payments Team", "current", finance) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.Equal(t, []string{redisProject + ":8"}, te.dt.takeUploads()) + image, parent, _ := te.dt.project(redisProject, "8") + assert.Equal(t, "Payments Team", parent) + assert.True(t, image.managed()) + team, _, _ := te.dt.project("Payments Team", "current") + assert.Empty(t, team.CollectionLogic, "an existing parent keeps its settings") + assert.Empty(t, team.Tags) + _, _, rootCreated := te.dt.project("cluster", "current") + assert.False(t, rootCreated, "the root is only needed to create a parent") +} + +func TestExporterCreatesTheParentUnderAnExistingRoot(t *testing.T) { + te := newTestExporter(t, + namespace("payments", map[string]string{ + AnnotationRootProject: "Finance:current", + AnnotationRootProjectCreate: "false", + AnnotationParent: "Payments Team:current", + }), + runningPod("payments", "api", redisImageID), + ) + te.dt.addProject("Finance", "current", "") + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + team, parent, ok := te.dt.project("Payments Team", "current") + require.True(t, ok) + assert.Equal(t, "Finance", parent) + assert.Equal(t, parentLogic, team.CollectionLogic) + assert.True(t, team.managed()) + finance, _, _ := te.dt.project("Finance", "current") + assert.Empty(t, finance.CollectionLogic, "an existing root keeps its settings") + _, imageParent, _ := te.dt.project("cluster/payments/"+redisProject, "8") + assert.Equal(t, "Payments Team", imageParent) +} + +func TestExporterSkipsANamespaceWhoseProjectIsMissingAndMayNotBeCreated(t *testing.T) { + tt := []struct { + name string + annotations map[string]string + }{ + {"parent with create false", map[string]string{AnnotationParent: "Payments Team:current", AnnotationParentCreate: "false"}}, + {"parent by an unknown UUID", map[string]string{AnnotationParent: "0a1b2c3d-0000-4000-8000-000000000099"}}, + {"root with create false", map[string]string{AnnotationRootProject: "Finance:current", AnnotationRootProjectCreate: "false"}}, + {"a layout that does not resolve", map[string]string{AnnotationProjectName: "{{ .Digest }}"}}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + te := newTestExporter(t, namespace("payments", tc.annotations), runningPod("payments", "api", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.Empty(t, te.dt.takeUploads()) + assert.Zero(t, te.dt.count(), "nothing is created") + assert.Equal(t, int64(1), te.stats.skippedLayout.Load()) + }) + } +} + +func TestExporterNeverUploadsToAProjectItDidNotCreate(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.dt.addProject("cluster/apps/"+redisProject, "8", "") + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.Empty(t, te.dt.takeUploads()) + assert.Equal(t, int64(1), te.stats.conflicts.Load()) +} + +// Two images whose layout gives the same project and version would overwrite +// each other's BOM. The second is skipped while the first runs, and takes the +// project over once the first stopped. +func TestExporterKeepsTwoRunningImagesFromSharingOneProject(t *testing.T) { + shared := map[string]string{AnnotationProjectName: "{{ .Namespace }}"} + te := newTestExporter(t, namespace("apps", shared), runningPod("apps", "cache", redisImageID), runningPod("apps", "web", nginxImageID)) + te.store.set( + sbomObject(redisImageID, "docker.io/library/redis:1.0", helpersv1.Learning, te.clock.t.Add(-time.Hour)), + sbomObject(nginxImageID, "docker.io/library/nginx:1.0", helpersv1.Learning, te.clock.t.Add(-time.Hour)), + ) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Equal(t, []string{"apps:1.0"}, te.dt.takeUploads()) + assert.Equal(t, int64(1), te.stats.conflicts.Load()) + + p, _, _ := te.dt.project("apps", "1.0") + owner, err := te.dt.digestOwnerForTest(p.UUID) + require.NoError(t, err) + stopped, other := "cache", digestOf(nginxImageID) + if owner == digestOf(nginxImageID) { + stopped, other = "web", digestOf(redisImageID) + } + require.NoError(t, te.client.CoreV1().Pods("apps").Delete(context.Background(), stopped, metav1.DeleteOptions{})) + require.NoError(t, te.resync(context.Background())) + te.clock.t = te.clock.t.Add(2 * time.Hour) + te.stats.takeFields() + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Equal(t, []string{"apps:1.0"}, te.dt.takeUploads(), "the image that still runs takes the project over") + owner, err = te.dt.digestOwnerForTest(p.UUID) + require.NoError(t, err) + assert.Equal(t, other, owner) +} + +func TestExporterSkipsAnSBOMLargerThanTheLimit(t *testing.T) { + t.Run("by its size annotation, without reading it", func(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + big := sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour)) + big.Annotations[helpersv1.ResourceSizeMetadataKey] = strconv.Itoa(11 << 20) + te.store.set(big) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.Empty(t, te.dt.takeUploads()) + assert.Zero(t, te.store.documentReads()) + assert.Equal(t, int64(1), te.stats.tooLarge.Load()) + }) + t.Run("by its document, when the annotation is missing", func(t *testing.T) { + te := newTestExporterWith(t, func(o *Options) { o.MaxSBOMSize = 1000 }, runningPod("apps", "web", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.Empty(t, te.dt.takeUploads()) + assert.Equal(t, int64(1), te.stats.tooLarge.Load()) + }) +} + +func TestExporterMovesItsProjectsWhenANamespaceLayoutChanges(t *testing.T) { + te := newTestExporter(t, namespace("payments", nil), runningPod("payments", "api", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + before, _, ok := te.dt.project("cluster/payments/"+redisProject, "8") + require.True(t, ok) + te.dt.takeUploads() + + ns := namespace("payments", map[string]string{AnnotationParent: "Payments Team:current", AnnotationProjectName: "{{ .Repository }}"}) + _, err := te.client.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{}) + require.NoError(t, err) + require.Eventually(t, func() bool { return te.queue.Len() > 0 }, 5*time.Second, 10*time.Millisecond) + require.Empty(t, te.drain()) + + after, parent, ok := te.dt.project(redisProject, "8") + require.True(t, ok) + assert.Equal(t, before.UUID, after.UUID, "the same project moved, with its history") + assert.Equal(t, "Payments Team", parent) + _, _, stillThere := te.dt.project("cluster/payments/"+redisProject, "8") + assert.False(t, stillThere) + assert.Empty(t, te.dt.takeUploads(), "nothing is uploaded again") + assert.Equal(t, int64(1), te.stats.moved.Load()) +} + +func TestExporterTagsItsProjectWhenDependencyTrackIgnoresProjectTags(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.dt.ignoreUpTags = true + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + image, _, _ := te.dt.project("cluster/apps/"+redisProject, "8") + assert.True(t, image.managed()) + te.clock.t = te.clock.t.Add(time.Hour) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Equal(t, []string{"cluster/apps/" + redisProject + ":8"}, te.dt.takeUploads(), "the second resync finds it its own and unchanged") +} + +// The namespace a pod event names is resolved when the image is exported, so +// an image seen before its Namespace object still lands in the right place. +func TestExporterUsesTheNamespaceAnnotationsWhenItExports(t *testing.T) { + te := newTestExporter(t, namespace("payments", map[string]string{AnnotationProjectName: "{{ .Repository }}"})) + te.ObserveSBOM(watch.Added, sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + te.ObservePod(watch.Added, runningPod("payments", "api", redisImageID)) + + require.Empty(t, te.drain()) + + assert.Equal(t, []string{redisProject + ":8"}, te.dt.takeUploads()) +} diff --git a/sbomexport/exporter_test.go b/sbomexport/exporter_test.go index cec5e761..23205faa 100644 --- a/sbomexport/exporter_test.go +++ b/sbomexport/exporter_test.go @@ -5,6 +5,7 @@ import ( "errors" "os" "slices" + "strings" "sync" "testing" "time" @@ -19,6 +20,7 @@ import ( "k8s.io/apimachinery/pkg/watch" k8sfake "k8s.io/client-go/kubernetes/fake" ktesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/cache" ) const ( @@ -74,42 +76,67 @@ type testExporter struct { client *k8sfake.Clientset } -// newTestExporter returns an Exporter with root project "cluster", a one-hour -// resync and kube-system excluded, over a fake Dependency-Track, a fake SBOM -// store and a fake clientset holding pods. -func newTestExporter(t *testing.T, pods ...runtime.Object) *testExporter { +// newTestExporter returns an Exporter for cluster "cluster" with the default +// layout, a one-hour resync and kube-system excluded, over a fake +// Dependency-Track, a fake SBOM store and a fake clientset holding objects +// (pods and namespaces). +func newTestExporter(t *testing.T, objects ...runtime.Object) *testExporter { + t.Helper() + return newTestExporterWith(t, func(*Options) {}, objects...) +} + +// newTestExporterWith is newTestExporter with options changed by change. +func newTestExporterWith(t *testing.T, change func(*Options), objects ...runtime.Object) *testExporter { t.Helper() clock := &fakeClock{t: time.Date(2026, 11, 10, 12, 0, 0, 0, time.UTC)} dt, srv, keyFile := newFakeDT(t, clock.now) store := &fakeStore{} - client := k8sfake.NewClientset(pods...) - e, err := newExporter(Options{ + client := k8sfake.NewClientset(objects...) + opts := Options{ URL: srv.URL, APIKeyFile: keyFile, - RootProject: "cluster", + Cluster: "cluster", + Layout: defaultLayout, + MaxSBOMSize: 10 << 20, ResyncInterval: time.Hour, SkipNamespace: func(ns string) bool { return ns == "kube-system" }, - }, client, store, srv.Client(), clock.now) + } + change(&opts) + e, err := newExporter(opts, client, store, srv.Client(), clock.now) require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) t.Cleanup(e.queue.ShutDown) + go e.namespaceInformer.Run(ctx.Done()) + require.True(t, cache.WaitForCacheSync(ctx.Done(), e.namespaceInformer.HasSynced)) return &testExporter{Exporter: e, dt: dt, store: store, clock: clock, client: client} } -// drain processes every queued image once, as Start's worker would, and +// drain processes every queued item once, as Start's worker would, and // returns the errors. func (te *testExporter) drain() []error { var errs []error for te.queue.Len() > 0 { - digest, _ := te.queue.Get() - if err := te.process(context.Background(), digest); err != nil { + key, _ := te.queue.Get() + var err error + if ns, ok := strings.CutPrefix(key, moveKeyPrefix); ok { + err = te.move(context.Background(), ns) + } else { + err = te.process(context.Background(), key) + } + if err != nil { errs = append(errs, err) } - te.queue.Forget(digest) - te.queue.Done(digest) + te.queue.Forget(key) + te.queue.Done(key) } return errs } +func namespace(name string, annotations map[string]string) *corev1.Namespace { + return &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name, Annotations: annotations}} +} + func runningPod(namespace, name, imageID string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: name}, @@ -135,8 +162,16 @@ func sbomObject(imageID, imageTag, status string, created time.Time) *spdxv1beta } } +// defaultLayout is the operator's default: //:. +var defaultLayout = Layout{ + Root: Collection{Name: "{{ .Cluster }}", Version: "current", Create: true}, + Parent: Collection{Name: "{{ .Cluster }}/{{ .Namespace }}", Version: "current", Create: true}, + Name: "{{ .Cluster }}/{{ .Namespace }}/{{ .Repository }}", + Version: "{{ .Tag }}", +} + func TestNewRejectsAnInvalidConfiguration(t *testing.T) { - valid := Options{URL: "https://dtrack.example.com", APIKeyFile: "/etc/sbom-export/api-key", RootProject: "cluster", ResyncInterval: time.Hour} + valid := Options{URL: "https://dtrack.example.com", APIKeyFile: "/etc/sbom-export/api-key", Cluster: "cluster", Layout: defaultLayout, MaxSBOMSize: 10 << 20, ResyncInterval: time.Hour} tt := []struct { name string change func(*Options) @@ -144,7 +179,10 @@ func TestNewRejectsAnInvalidConfiguration(t *testing.T) { {"no URL", func(o *Options) { o.URL = "" }}, {"a URL without a scheme", func(o *Options) { o.URL = "dtrack.example.com" }}, {"no API key file", func(o *Options) { o.APIKeyFile = "" }}, - {"no root project", func(o *Options) { o.RootProject = "" }}, + {"no cluster name for the default root project", func(o *Options) { o.Cluster = "" }}, + {"a template that does not parse", func(o *Options) { o.Layout.Name = "{{ .Namespace" }}, + {"a template with an unknown field", func(o *Options) { o.Layout.Version = "{{ .Digest }}" }}, + {"no size limit", func(o *Options) { o.MaxSBOMSize = 0 }}, {"no resync interval", func(o *Options) { o.ResyncInterval = 0 }}, } for _, tc := range tt { @@ -179,12 +217,12 @@ func TestExporterSendsAnImageToTheProjectOfEveryNamespaceItRunsIn(t *testing.T) image, parent, _ := te.dt.project("cluster/apps/"+redisProject, "8") assert.Equal(t, "cluster/apps", parent) assert.True(t, image.IsLatest) - namespace, parent, _ := te.dt.project("cluster/apps", collectionVersion) + namespace, parent, _ := te.dt.project("cluster/apps", "current") assert.Equal(t, "cluster", parent) - assert.Equal(t, namespaceLogic, namespace.CollectionLogic) - root, parent, _ := te.dt.project("cluster", collectionVersion) + assert.Equal(t, parentLogic, namespace.CollectionLogic) + root, parent, _ := te.dt.project("cluster", "current") assert.Empty(t, parent) - assert.Equal(t, clusterLogic, root.CollectionLogic) + assert.Equal(t, rootLogic, root.CollectionLogic) } func TestExporterSendsAnSBOMAgainOnlyWhenItIsNewerThanTheLastImport(t *testing.T) { diff --git a/sbomexport/fakedtrack_test.go b/sbomexport/fakedtrack_test.go index 2c7ab5bf..3a769ab4 100644 --- a/sbomexport/fakedtrack_test.go +++ b/sbomexport/fakedtrack_test.go @@ -17,16 +17,19 @@ import ( const testAPIKey = "test-api-key" // fakeDT is an in-memory Dependency-Track that serves the calls dtClient -// makes: project lookup, create, patch, and BOM upload with autoCreate. +// makes: project lookup, get by UUID, create, patch (including a move), BOM +// upload with autoCreate and projectTags, and project properties. type fakeDT struct { - mu sync.Mutex - now func() time.Time - projects map[string]*project // "name:version" → project - parents map[string]string // project uuid → parent uuid - uploads []string // "name:version" of every BOM upload - nextID int - fail int // answer the next fail requests with 503 - keyFile string // holds testAPIKey + mu sync.Mutex + now func() time.Time + projects map[string]*project // uuid → project + parents map[string]string // project uuid → parent uuid + properties map[string]map[string]string // project uuid → "group/name" → value + uploads []string // "name:version" of every BOM upload + nextID int + fail int // answer the next fail requests with 503 + ignoreUpTags bool // drop projectTags on upload, as a Dependency-Track that does not support them + keyFile string // holds testAPIKey } // newFakeDT starts a fakeDT and returns it with an API key file holding @@ -37,7 +40,7 @@ func newFakeDT(t *testing.T, now func() time.Time) (*fakeDT, *httptest.Server, s if err := os.WriteFile(keyFile, []byte(testAPIKey+"\n"), 0600); err != nil { t.Fatal(err) } - f := &fakeDT{now: now, projects: map[string]*project{}, parents: map[string]string{}, keyFile: keyFile} + f := &fakeDT{now: now, projects: map[string]*project{}, parents: map[string]string{}, properties: map[string]map[string]string{}, keyFile: keyFile} srv := httptest.NewServer(f) t.Cleanup(srv.Close) return f, srv, keyFile @@ -55,46 +58,77 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "unavailable", http.StatusServiceUnavailable) return } + path := r.URL.Path switch { - case r.Method == http.MethodGet && r.URL.Path == "/api/v1/project/lookup": - p, ok := f.projects[r.URL.Query().Get("name")+":"+r.URL.Query().Get("version")] - if !ok { + case r.Method == http.MethodGet && path == "/api/v1/project/lookup": + p := f.byNameLocked(r.URL.Query().Get("name"), r.URL.Query().Get("version")) + if p == nil { http.Error(w, "The project could not be found.", http.StatusNotFound) return } writeJSON(w, http.StatusOK, p) - case r.Method == http.MethodPut && r.URL.Path == "/api/v1/project": + case r.Method == http.MethodPut && path == "/api/v1/project": var req struct { Name, Version, CollectionLogic string Parent *struct{ UUID string } + Tags []tag } _ = json.NewDecoder(r.Body).Decode(&req) parent := "" if req.Parent != nil { parent = req.Parent.UUID } - p := f.addLocked(req.Name, req.Version, parent) + p := f.addLocked(req.Name, req.Version, parent, req.Tags) p.CollectionLogic = req.CollectionLogic writeJSON(w, http.StatusCreated, p) - case r.Method == http.MethodPatch && strings.HasPrefix(r.URL.Path, "/api/v1/project/"): - p := f.byUUIDLocked(strings.TrimPrefix(r.URL.Path, "/api/v1/project/")) + case strings.HasSuffix(path, "/property") && strings.HasPrefix(path, "/api/v1/project/"): + f.serveProperty(w, r, strings.TrimSuffix(strings.TrimPrefix(path, "/api/v1/project/"), "/property")) + case r.Method == http.MethodGet && strings.HasPrefix(path, "/api/v1/project/"): + p := f.projects[strings.TrimPrefix(path, "/api/v1/project/")] + if p == nil { + http.Error(w, "The project could not be found.", http.StatusNotFound) + return + } + writeJSON(w, http.StatusOK, p) + case r.Method == http.MethodPatch && strings.HasPrefix(path, "/api/v1/project/"): + uuid := strings.TrimPrefix(path, "/api/v1/project/") + p := f.projects[uuid] if p == nil { http.Error(w, "The project could not be found.", http.StatusNotFound) return } - var req map[string]any + var req struct { + IsLatest *bool + CollectionLogic *string + Name, Version *string + Parent *struct{ UUID string } + Tags *[]tag + } _ = json.NewDecoder(r.Body).Decode(&req) - if v, ok := req["isLatest"].(bool); ok { - p.IsLatest = v + if req.IsLatest != nil { + p.IsLatest = *req.IsLatest + } + if req.CollectionLogic != nil { + p.CollectionLogic = *req.CollectionLogic + } + if req.Name != nil { + p.Name = *req.Name + } + if req.Version != nil { + p.Version = *req.Version + } + if req.Parent != nil { + f.parents[uuid] = req.Parent.UUID } - if v, ok := req["collectionLogic"].(string); ok { - p.CollectionLogic = v + if req.Tags != nil { + p.Tags = *req.Tags } writeJSON(w, http.StatusOK, p) - case r.Method == http.MethodPut && r.URL.Path == "/api/v1/bom": + case r.Method == http.MethodPut && path == "/api/v1/bom": var req struct { ProjectName, ProjectVersion, ParentUUID, BOM string AutoCreate, IsLatest bool + ProjectTags []tag } _ = json.NewDecoder(r.Body).Decode(&req) bom, err := base64.StdEncoding.DecodeString(req.BOM) @@ -102,10 +136,13 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad BOM upload", http.StatusBadRequest) return } - key := req.ProjectName + ":" + req.ProjectVersion - p, ok := f.projects[key] - if !ok { - p = f.addLocked(req.ProjectName, req.ProjectVersion, req.ParentUUID) + p := f.byNameLocked(req.ProjectName, req.ProjectVersion) + if p == nil { + tags := req.ProjectTags + if f.ignoreUpTags { + tags = nil + } + p = f.addLocked(req.ProjectName, req.ProjectVersion, req.ParentUUID, tags) } if req.IsLatest { for _, other := range f.projects { @@ -116,45 +153,105 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.IsLatest = true } p.LastBOMImport = epochMillis(f.now().UnixMilli()) - f.uploads = append(f.uploads, key) + f.uploads = append(f.uploads, req.ProjectName+":"+req.ProjectVersion) writeJSON(w, http.StatusOK, map[string]string{"token": fmt.Sprintf("token-%d", len(f.uploads))}) default: http.NotFound(w, r) } } -func (f *fakeDT) addLocked(name, version, parentUUID string) *project { - f.nextID++ - p := &project{UUID: fmt.Sprintf("uuid-%d", f.nextID), Name: name, Version: version} - f.projects[name+":"+version] = p - f.parents[p.UUID] = parentUUID - return p +func (f *fakeDT) serveProperty(w http.ResponseWriter, r *http.Request, uuid string) { + if f.projects[uuid] == nil { + http.Error(w, "The project could not be found.", http.StatusNotFound) + return + } + if r.Method == http.MethodGet { + var out []projectProperty + for k, v := range f.properties[uuid] { + group, name, _ := strings.Cut(k, "/") + out = append(out, projectProperty{GroupName: group, PropertyName: name, PropertyValue: v, PropertyType: "STRING"}) + } + writeJSON(w, http.StatusOK, out) + return + } + var p projectProperty + _ = json.NewDecoder(r.Body).Decode(&p) + key := p.GroupName + "/" + p.PropertyName + if f.properties[uuid] == nil { + f.properties[uuid] = map[string]string{} + } + _, exists := f.properties[uuid][key] + switch { + case r.Method == http.MethodPut && exists: + http.Error(w, "A property with the specified project/group/name combination already exists.", http.StatusConflict) + case r.Method == http.MethodPut: + f.properties[uuid][key] = p.PropertyValue + writeJSON(w, http.StatusCreated, p) + case r.Method == http.MethodPost && exists: + f.properties[uuid][key] = p.PropertyValue + writeJSON(w, http.StatusOK, p) + default: + http.Error(w, "The property could not be found.", http.StatusNotFound) + } } -func (f *fakeDT) byUUIDLocked(uuid string) *project { +func (f *fakeDT) byNameLocked(name, version string) *project { for _, p := range f.projects { - if p.UUID == uuid { + if p.Name == name && p.Version == version { return p } } return nil } +func (f *fakeDT) addLocked(name, version, parentUUID string, tags []tag) *project { + f.nextID++ + p := &project{UUID: fmt.Sprintf("00000000-0000-0000-0000-%012d", f.nextID), Name: name, Version: version, Tags: tags} + f.projects[p.UUID] = p + f.parents[p.UUID] = parentUUID + return p +} + +// addProject creates a project the way a person would, without the +// exporter's tag, and returns its uuid. +func (f *fakeDT) addProject(name, version, parentUUID string) string { + f.mu.Lock() + defer f.mu.Unlock() + return f.addLocked(name, version, parentUUID, nil).UUID +} + // project returns a copy of name:version, and the name of its parent. func (f *fakeDT) project(name, version string) (project, string, bool) { f.mu.Lock() defer f.mu.Unlock() - p, ok := f.projects[name+":"+version] - if !ok { + p := f.byNameLocked(name, version) + if p == nil { return project{}, "", false } parent := "" - if pp := f.byUUIDLocked(f.parents[p.UUID]); pp != nil { + if pp := f.projects[f.parents[p.UUID]]; pp != nil { parent = pp.Name } return *p, parent, true } +// digestOwnerForTest returns the image digest recorded on the project with uuid. +func (f *fakeDT) digestOwnerForTest(uuid string) (string, error) { + f.mu.Lock() + defer f.mu.Unlock() + if f.projects[uuid] == nil { + return "", fmt.Errorf("no project %s", uuid) + } + return f.properties[uuid][digestPropertyGroup+"/"+digestPropertyName], nil +} + +// count returns how many projects exist. +func (f *fakeDT) count() int { + f.mu.Lock() + defer f.mu.Unlock() + return len(f.projects) +} + // failNext makes the next n requests fail with 503. func (f *fakeDT) failNext(n int) { f.mu.Lock() @@ -166,7 +263,7 @@ func (f *fakeDT) failNext(n int) { func (f *fakeDT) patchForTest(uuid string, isLatest bool) error { f.mu.Lock() defer f.mu.Unlock() - p := f.byUUIDLocked(uuid) + p := f.projects[uuid] if p == nil { return fmt.Errorf("no project %s", uuid) } diff --git a/sbomexport/index.go b/sbomexport/index.go index a9515efb..e7ff6bbb 100644 --- a/sbomexport/index.go +++ b/sbomexport/index.go @@ -102,6 +102,27 @@ func (x *index) namespaces(digest string) []string { return out } +// digestsIn returns, sorted, the digests that run in ns or ran there less than +// a grace period ago. +func (x *index) digestsIn(ns string) []string { + x.mu.Lock() + defer x.mu.Unlock() + x.pruneLocked() + var out []string + for d, r := range x.refs { + if r[ns] > 0 { + out = append(out, d) + } + } + for d, e := range x.ended { + if _, ok := e[ns]; ok { + out = append(out, d) + } + } + slices.Sort(out) + return slices.Compact(out) +} + // digests returns every digest that namespaces would return a namespace for. func (x *index) digests() []string { x.mu.Lock() diff --git a/sbomexport/layout.go b/sbomexport/layout.go new file mode 100644 index 00000000..e736046f --- /dev/null +++ b/sbomexport/layout.go @@ -0,0 +1,256 @@ +package sbomexport + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "sync" + "text/template" +) + +// Namespace annotations that override the layout for one namespace. Each one +// is optional; what a namespace does not set comes from the defaults. +const ( + // AnnotationRootProject is the root project, as "name:version" or a project UUID. + AnnotationRootProject = "kubescape.io/dtrack-root-project" + // AnnotationRootProjectCreate is "true" or "false". + AnnotationRootProjectCreate = "kubescape.io/dtrack-root-project-create" + // AnnotationParent is the parent project, as "name:version" or a project UUID. + AnnotationParent = "kubescape.io/dtrack-parent" + // AnnotationParentCreate is "true" or "false". + AnnotationParentCreate = "kubescape.io/dtrack-parent-create" + // AnnotationProjectName is a template for the image project's name. + AnnotationProjectName = "kubescape.io/dtrack-project-name" +) + +// TemplateData is what the layout templates can use, e.g. +// "{{ .Cluster }}/{{ .Namespace }}" or `{{ index .Labels "team" }}`. +type TemplateData struct { + Cluster string + Namespace string + Repository string + Tag string + // Labels are the namespace's labels. + Labels map[string]string +} + +// Collection is a collection project in the layout: the root, or the parent of +// an image's project. Name and Version are templates. +type Collection struct { + Name string + Version string + // Create lets the exporter create the project when it is missing. + Create bool +} + +// Layout is where images go in Dependency-Track, by default. Name and Version +// are templates for the image's project. +type Layout struct { + Root Collection + Parent Collection + Name string + Version string +} + +// projectRef points at a Dependency-Track project by UUID, or by name and version. +type projectRef struct { + uuid string + name, version string +} + +func (r projectRef) String() string { + if r.uuid != "" { + return r.uuid + } + return r.name + ":" + r.version +} + +// placement is where one image of one namespace goes. +type placement struct { + root projectRef + rootCreate bool + parent projectRef + parentCreate bool + name, version string // the image's project +} + +// namespaceInfo is what the layout needs to know about a namespace. +type namespaceInfo struct { + name string + labels map[string]string + annotations map[string]string +} + +var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) + +// parseRef reads an annotation's project reference: a UUID, or "name:version", +// split at the last colon. +func parseRef(s string) (projectRef, error) { + s = strings.TrimSpace(s) + if uuidPattern.MatchString(s) { + return projectRef{uuid: strings.ToLower(s)}, nil + } + i := strings.LastIndex(s, ":") + if i <= 0 || i == len(s)-1 { + return projectRef{}, fmt.Errorf("%q is neither a project UUID nor name:version", s) + } + return projectRef{name: s[:i], version: s[i+1:]}, nil +} + +// layout resolves placements from the default Layout and namespace annotations. +type layout struct { + cluster string + root [2]*template.Template // name, version + parent [2]*template.Template + project [2]*template.Template + defs Layout + + mu sync.Mutex + nameByText map[string]*template.Template // parsed AnnotationProjectName values +} + +func parseTemplate(field, text string) (*template.Template, error) { + t, err := template.New(field).Option("missingkey=error").Parse(text) + if err != nil { + return nil, fmt.Errorf("%s %q: %w", field, text, err) + } + return t, nil +} + +// newLayout parses the default templates and checks that they render. +func newLayout(cluster string, l Layout) (*layout, error) { + x := &layout{cluster: cluster, defs: l, nameByText: map[string]*template.Template{}} + var errs []error + for _, f := range []struct { + dst **template.Template + field string + text string + }{ + {&x.root[0], "rootProject.name", l.Root.Name}, + {&x.root[1], "rootProject.version", l.Root.Version}, + {&x.parent[0], "project.parent.name", l.Parent.Name}, + {&x.parent[1], "project.parent.version", l.Parent.Version}, + {&x.project[0], "project.name", l.Name}, + {&x.project[1], "project.version", l.Version}, + } { + t, err := parseTemplate(f.field, f.text) + if err != nil { + errs = append(errs, err) + continue + } + *f.dst = t + } + if err := errors.Join(errs...); err != nil { + return nil, err + } + sample := TemplateData{Cluster: cluster, Namespace: "default", Repository: "docker.io/library/nginx", Tag: "1.27", Labels: map[string]string{}} + for _, t := range []*template.Template{x.root[0], x.root[1], x.parent[0], x.parent[1], x.project[0], x.project[1]} { + if _, err := render(t, sample); err != nil { + errs = append(errs, err) + } + } + return x, errors.Join(errs...) +} + +// render executes t and rejects an empty result. +func render(t *template.Template, data TemplateData) (string, error) { + var b bytes.Buffer + if err := t.Execute(&b, data); err != nil { + return "", fmt.Errorf("render %s: %w", t.Name(), err) + } + out := strings.TrimSpace(b.String()) + if out == "" { + return "", fmt.Errorf("render %s: empty result", t.Name()) + } + return out, nil +} + +func (x *layout) projectNameTemplate(text string) (*template.Template, error) { + x.mu.Lock() + defer x.mu.Unlock() + if t, ok := x.nameByText[text]; ok { + return t, nil + } + t, err := parseTemplate(AnnotationProjectName, text) + if err != nil { + return nil, err + } + x.nameByText[text] = t + return t, nil +} + +// boolAnnotation reads a "true"/"false" annotation, or def when it is not set. +func boolAnnotation(ann map[string]string, key string, def bool) (bool, error) { + v, ok := ann[key] + if !ok { + return def, nil + } + b, err := strconv.ParseBool(strings.TrimSpace(v)) + if err != nil { + return false, fmt.Errorf("annotation %s: %q is not true or false", key, v) + } + return b, nil +} + +// resolve returns where repository:tag, running in ns, goes. Each annotation +// replaces only the setting it names. +func (x *layout) resolve(ns namespaceInfo, repository, tag string) (placement, error) { + data := TemplateData{Cluster: x.cluster, Namespace: ns.name, Repository: repository, Tag: tag, Labels: ns.labels} + if data.Labels == nil { + data.Labels = map[string]string{} + } + ann := ns.annotations + var p placement + var err error + + if p.root, err = x.collectionRef(ann, AnnotationRootProject, x.root, data); err != nil { + return placement{}, err + } + if p.rootCreate, err = boolAnnotation(ann, AnnotationRootProjectCreate, x.defs.Root.Create); err != nil { + return placement{}, err + } + if p.parent, err = x.collectionRef(ann, AnnotationParent, x.parent, data); err != nil { + return placement{}, err + } + if p.parentCreate, err = boolAnnotation(ann, AnnotationParentCreate, x.defs.Parent.Create); err != nil { + return placement{}, err + } + + nameTmpl := x.project[0] + if text, ok := ann[AnnotationProjectName]; ok { + if nameTmpl, err = x.projectNameTemplate(text); err != nil { + return placement{}, err + } + } + if p.name, err = render(nameTmpl, data); err != nil { + return placement{}, err + } + if p.version, err = render(x.project[1], data); err != nil { + return placement{}, err + } + return p, nil +} + +// collectionRef reads the reference from the annotation key, or renders the +// default name and version templates. +func (x *layout) collectionRef(ann map[string]string, key string, defs [2]*template.Template, data TemplateData) (projectRef, error) { + if v, ok := ann[key]; ok { + r, err := parseRef(v) + if err != nil { + return projectRef{}, fmt.Errorf("annotation %s: %w", key, err) + } + return r, nil + } + name, err := render(defs[0], data) + if err != nil { + return projectRef{}, err + } + version, err := render(defs[1], data) + if err != nil { + return projectRef{}, err + } + return projectRef{name: name, version: version}, nil +} diff --git a/sbomexport/layout_test.go b/sbomexport/layout_test.go new file mode 100644 index 00000000..b8654c10 --- /dev/null +++ b/sbomexport/layout_test.go @@ -0,0 +1,129 @@ +package sbomexport + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseRef(t *testing.T) { + tt := []struct { + in string + want projectRef + wantErr bool + }{ + {in: "Payments Team:current", want: projectRef{name: "Payments Team", version: "current"}}, + {in: "a:b:c", want: projectRef{name: "a:b", version: "c"}}, + {in: "0A1B2C3D-0000-4000-8000-000000000001", want: projectRef{uuid: "0a1b2c3d-0000-4000-8000-000000000001"}}, + {in: "Payments Team", wantErr: true}, + {in: ":current", wantErr: true}, + {in: "Payments Team:", wantErr: true}, + } + for _, tc := range tt { + t.Run(tc.in, func(t *testing.T) { + got, err := parseRef(tc.in) + if tc.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestLayoutDefaultsBuildTheClusterNamespaceTree(t *testing.T) { + l, err := newLayout("prod", defaultLayout) + require.NoError(t, err) + + p, err := l.resolve(namespaceInfo{name: "apps"}, "docker.io/library/redis", "8") + + require.NoError(t, err) + assert.Equal(t, placement{ + root: projectRef{name: "prod", version: "current"}, + rootCreate: true, + parent: projectRef{name: "prod/apps", version: "current"}, + parentCreate: true, + name: "prod/apps/docker.io/library/redis", + version: "8", + }, p) +} + +func TestLayoutTemplatesSeeTheNamespaceLabels(t *testing.T) { + byTeam := defaultLayout + byTeam.Parent.Name = `{{ .Cluster }}/{{ index .Labels "team" }}` + l, err := newLayout("prod", byTeam) + require.NoError(t, err) + + p, err := l.resolve(namespaceInfo{name: "apps", labels: map[string]string{"team": "payments"}}, "docker.io/library/redis", "8") + + require.NoError(t, err) + assert.Equal(t, projectRef{name: "prod/payments", version: "current"}, p.parent) +} + +func TestLayoutAnnotationsOverrideOneSettingEach(t *testing.T) { + l, err := newLayout("prod", defaultLayout) + require.NoError(t, err) + tt := []struct { + name string + annotations map[string]string + check func(t *testing.T, p placement) + }{ + { + name: "root project", + annotations: map[string]string{AnnotationRootProject: "Finance:current", AnnotationRootProjectCreate: "false"}, + check: func(t *testing.T, p placement) { + assert.Equal(t, projectRef{name: "Finance", version: "current"}, p.root) + assert.False(t, p.rootCreate) + assert.Equal(t, projectRef{name: "prod/apps", version: "current"}, p.parent, "the parent keeps its default") + assert.True(t, p.parentCreate) + }, + }, + { + name: "parent by UUID", + annotations: map[string]string{AnnotationParent: "0a1b2c3d-0000-4000-8000-000000000001", AnnotationParentCreate: "false"}, + check: func(t *testing.T, p placement) { + assert.Equal(t, projectRef{uuid: "0a1b2c3d-0000-4000-8000-000000000001"}, p.parent) + assert.False(t, p.parentCreate) + assert.Equal(t, projectRef{name: "prod", version: "current"}, p.root, "the root keeps its default") + }, + }, + { + name: "project name", + annotations: map[string]string{AnnotationProjectName: "{{ .Namespace }}-{{ .Repository }}"}, + check: func(t *testing.T, p placement) { + assert.Equal(t, "apps-docker.io/library/redis", p.name) + assert.Equal(t, "8", p.version, "the version keeps its default") + }, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + p, err := l.resolve(namespaceInfo{name: "apps", annotations: tc.annotations}, "docker.io/library/redis", "8") + require.NoError(t, err) + tc.check(t, p) + }) + } +} + +func TestLayoutRejectsAnnotationsItCannotUse(t *testing.T) { + l, err := newLayout("prod", defaultLayout) + require.NoError(t, err) + tt := []struct { + name string + annotations map[string]string + }{ + {"a reference without a version", map[string]string{AnnotationParent: "Payments Team"}}, + {"a create flag that is not a boolean", map[string]string{AnnotationParentCreate: "maybe"}}, + {"a template that does not parse", map[string]string{AnnotationProjectName: "{{ .Repository"}}, + {"a template with an unknown field", map[string]string{AnnotationProjectName: "{{ .Digest }}"}}, + {"a template that renders nothing", map[string]string{AnnotationProjectName: `{{ index .Labels "team" }}`}}, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + _, err := l.resolve(namespaceInfo{name: "apps", annotations: tc.annotations}, "docker.io/library/redis", "8") + assert.Error(t, err) + }) + } +} diff --git a/sbomexport/store.go b/sbomexport/store.go index 4998fab1..58108e62 100644 --- a/sbomexport/store.go +++ b/sbomexport/store.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strconv" "time" helpersv1 "github.com/kubescape/k8s-interface/instanceidhandler/v1/helpers" @@ -22,6 +23,7 @@ type sbomMeta struct { imageTag string status string created time.Time + size int64 // bytes, from the kubescape.io/resource-size annotation; 0 when unknown } // ready reports whether the SBOM is complete: the statuses the operator's @@ -42,7 +44,9 @@ func metaOf(obj *spdxv1beta1.SBOMSyft) (sbomMeta, bool) { if imageID == "" || imageTag == "" { return sbomMeta{}, false } + size, _ := strconv.ParseInt(ann[helpersv1.ResourceSizeMetadataKey], 10, 64) return sbomMeta{ + size: size, namespace: obj.GetNamespace(), name: obj.GetName(), digest: digestOf(imageID), From 5a72df77e6b27d3ffe45b5df132dba609bd5225a Mon Sep 17 00:00:00 2001 From: Cristian Glavan Date: Wed, 11 Nov 2026 13:01:08 +0200 Subject: [PATCH 4/6] fix(sbomexport): address review findings and document SBOM sizing - index: a pod that keeps its images is not reported as started again. - The exporter records the image digest on a project before it uploads, and creates the image's project itself (tagged managed-by:kubescape) instead of letting the upload auto-create it, so PROJECT_CREATION_UPLOAD is no longer needed. - The collection cache is keyed by the project reference, not a joined string, so "a:b"/"c" and "a"/"b:c" no longer collide. - An existing managed parent is moved when only the root changes; an existing image project is moved when only its parent changes. - A namespace change that arrives during a move is kept for the next pass. - Default templates that need a namespace label ({{ .Labels.team }} or {{ index .Labels "team" }}) are accepted at startup; only unknown fields are rejected there. - docs: SBOM size section with the measurements and the sizing rule (memory limit at least nine times sbomExport.maxSBOMSize plus 64 MiB), and the corrected permissions. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristian Glavan --- README.md | 2 +- docs/features/sbom-export.md | 41 ++++-- sbomexport/dtrack.go | 47 +++++-- sbomexport/dtrack_test.go | 8 +- sbomexport/exporter.go | 195 +++++++++++++++++++++-------- sbomexport/exporter_layout_test.go | 109 ++++++++++++++-- sbomexport/exporter_test.go | 17 ++- sbomexport/fakedtrack_test.go | 53 +++++--- sbomexport/index.go | 12 +- sbomexport/index_test.go | 10 ++ sbomexport/layout.go | 17 ++- sbomexport/layout_test.go | 28 +++++ sbomexport/store.go | 1 + 13 files changed, 424 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 23251620..58355acc 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,7 @@ The Operator reads configuration from `/etc/config/`. When running locally, set | `excludeNamespacesRegex` | `""` | Comma-separated string or array of RE2 regex patterns for namespaces to exclude | | `namespaceFilterConfigMapName` | `""` | Optional ConfigMap name in the operator namespace for live namespace filtering | | `sbomExport.resyncInterval` | `1h` | How often the [SBOM export](docs/features/sbom-export.md) compares every running image with Dependency-Track | -| `sbomExport.maxSBOMSize` | `10485760` | Largest SBOM, in bytes, the SBOM export converts | +| `sbomExport.maxSBOMSize` | `10485760` | Largest SBOM, in bytes, the SBOM export converts; the operator needs about nine times this in memory, see [SBOM size](docs/features/sbom-export.md#sbom-size) | | `sbomExport.dependencyTrack.url` | `""` | Dependency-Track API server the SBOM export sends to | | `sbomExport.dependencyTrack.apiKeyFile` | `/etc/sbom-export/api-key` | File holding the Dependency-Track API key, read on every request | | `sbomExport.dependencyTrack.rootProject` | `{{ .Cluster }}` | Root project, where created parents go; see [changing the layout](docs/features/sbom-export.md#changing-the-layout) | diff --git a/docs/features/sbom-export.md b/docs/features/sbom-export.md index a2b6d08c..6497b6d7 100644 --- a/docs/features/sbom-export.md +++ b/docs/features/sbom-export.md @@ -105,10 +105,33 @@ Rules: ## SBOM size Converting an SBOM takes about nine times its size in memory, one image at a -time. SBOMs larger than `maxSBOMSize` (10 MiB by default) are skipped and +time. Measured with node-agent's cataloger settings, the SBOM of +`nvcr.io/nvidia/pytorch:26.10-py3` (24.9 MiB) peaked at 176 MiB of heap and the +one of `quay.io/jupyter/all-spark-notebook` (33.9 MiB) at 291 MiB, both without +a `GOMEMLIMIT`. + +SBOMs larger than `sbomExport.maxSBOMSize` (10 MiB by default) are skipped and logged once per resync. The operator checks the `kubescape.io/resource-size` -annotation before it reads the SBOM, and the document's length after. node-agent -itself stores SBOMs up to 20 MiB. +annotation before it reads the SBOM, and the document's length after. + +node-agent has its own `maxSBOMSize`, 20 MiB by default: it marks a larger SBOM +`too-large` and stores it without its content, so there is nothing to export. +To export bigger SBOMs, raise three settings together: + +1. node-agent's `maxSBOMSize`, so it keeps them; +2. `sbomExport.maxSBOMSize`, so the operator converts them; +3. the operator's memory limit, to at least nine times `sbomExport.maxSBOMSize` + plus 64 MiB for the rest of the operator. The Helm chart sets `GOMEMLIMIT` + from the limit (`operator.gomemlimitPercentage`, 80% by default). + +| `sbomExport.maxSBOMSize` | Memory limit it needs | Operator limit | +|--------------------------|-----------------------|----------------| +| 10 MiB (default) | 154 MiB | 300Mi, the chart's default | +| 20 MiB | 244 MiB | 300Mi, the chart's default | +| 32 MiB | 352 MiB | 512Mi | + +The Helm chart warns in the `ks-capabilities` ConfigMap when the operator's +memory limit is below this rule. ## Configuration @@ -147,7 +170,7 @@ itself stores SBOMs up to 20 MiB. | Setting | Default | Description | |---------|---------|-------------| | `sbomExport.resyncInterval` | `1h` | How often every running image is compared with Dependency-Track; also how long an image counts as running after its last pod ended | -| `sbomExport.maxSBOMSize` | `10485760` (10 MiB) | Largest SBOM, in bytes, the operator converts | +| `sbomExport.maxSBOMSize` | `10485760` (10 MiB) | Largest SBOM, in bytes, the operator converts; see [SBOM size](#sbom-size) for the memory it needs | | `sbomExport.dependencyTrack.url` | *required* | Base URL of the Dependency-Track API server | | `sbomExport.dependencyTrack.apiKeyFile` | `/etc/sbom-export/api-key` | File holding the API key, read on every request, so a rotated Secret needs no restart | | `sbomExport.dependencyTrack.rootProject.{name,version,create}` | `{{ .Cluster }}`, `current`, `true` | The root project, where created parents go | @@ -159,11 +182,13 @@ logs why and runs without the export. ## Permissions in Dependency-Track -The team that owns the API key needs `BOM_UPLOAD`, `PROJECT_CREATION_UPLOAD` -and `VIEW_PORTFOLIO` to upload. To create root and parent projects, set their +The team that owns the API key needs `VIEW_PORTFOLIO` to look projects up, +`BOM_UPLOAD` to upload, and `PORTFOLIO_MANAGEMENT` to create projects, set the collection logic, mark the running version latest, record the image digest and -move projects, it also needs `PORTFOLIO_MANAGEMENT`; on Dependency-Track 5, -`PORTFOLIO_MANAGEMENT_CREATE` and `PORTFOLIO_MANAGEMENT_UPDATE` replace it. With +move projects; on Dependency-Track 5, `PORTFOLIO_MANAGEMENT_CREATE` and +`PORTFOLIO_MANAGEMENT_UPDATE` replace it. The operator creates each image's +project before it uploads the BOM, so `PROJECT_CREATION_UPLOAD` is not needed, +also when the layout only names existing root and parent projects. With portfolio access control on, the team also needs access to the existing projects the layout names. A refused call is logged with the reason Dependency-Track gave. diff --git a/sbomexport/dtrack.go b/sbomexport/dtrack.go index b7834495..a0760aa2 100644 --- a/sbomexport/dtrack.go +++ b/sbomexport/dtrack.go @@ -35,6 +35,13 @@ type project struct { CollectionLogic string `json:"collectionLogic"` LastBOMImport epochMillis `json:"lastBomImport"` Tags []tag `json:"tags"` + // Parent is set in a GET by UUID; a lookup by name leaves it out. + Parent *projectParent `json:"parent,omitempty"` +} + +// projectParent is a project's parent, by UUID. +type projectParent struct { + UUID string `json:"uuid"` } type tag struct { @@ -55,6 +62,7 @@ func (p *project) managed() bool { // milliseconds or from an RFC 3339 string. type epochMillis int64 +// UnmarshalJSON decodes a number of milliseconds, an RFC 3339 string, or null. func (m *epochMillis) UnmarshalJSON(b []byte) error { if string(b) == "null" { *m = 0 @@ -126,14 +134,16 @@ func (c *dtClient) do(ctx context.Context, method, path string, body any, out an req.Header.Set("Content-Type", "application/json") } + // errors name the path without its query, which can hold a project name + errPath, _, _ := strings.Cut(path, "?") resp, err := c.http.Do(req) if err != nil { - return 0, fmt.Errorf("%s %s: %w", method, path, err) + return 0, fmt.Errorf("%s %s: %w", method, errPath, err) } defer func() { _ = resp.Body.Close() }() data, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20)) if err != nil { - return resp.StatusCode, fmt.Errorf("%s %s: read response: %w", method, path, err) + return resp.StatusCode, fmt.Errorf("%s %s: read response: %w", method, errPath, err) } for _, s := range ok { @@ -142,7 +152,7 @@ func (c *dtClient) do(ctx context.Context, method, path string, body any, out an } if out != nil && len(data) > 0 && resp.StatusCode < 300 { // a 404 body is plain text if err := json.Unmarshal(data, out); err != nil { - return resp.StatusCode, fmt.Errorf("%s %s: decode response: %w", method, path, err) + return resp.StatusCode, fmt.Errorf("%s %s: decode response: %w", method, errPath, err) } } return resp.StatusCode, nil @@ -151,7 +161,7 @@ func (c *dtClient) do(ctx context.Context, method, path string, body any, out an if len(msg) > errBody { msg = msg[:errBody] } - return resp.StatusCode, &httpError{method: method, path: path, status: resp.StatusCode, body: msg} + return resp.StatusCode, &httpError{method: method, path: errPath, status: resp.StatusCode, body: msg} } // httpError is a response with an unexpected status. @@ -161,6 +171,7 @@ type httpError struct { body string } +// Error names the request and quotes the start of the response body. func (e *httpError) Error() string { return fmt.Sprintf("%s %s: HTTP %d: %s", e.method, e.path, e.status, e.body) } @@ -230,21 +241,31 @@ func (c *dtClient) patch(ctx context.Context, uuid string, fields map[string]any return err } -// uploadBOM uploads bom as the latest version of name:version, creating the -// project under parentUUID, tagged as created by the exporter, when it does -// not exist. Dependency-Track processes the BOM asynchronously after answering. -func (c *dtClient) uploadBOM(ctx context.Context, name, version, parentUUID string, bom []byte) error { +// createProject creates the project name:version under parentUUID, tagged as +// created by the exporter. +func (c *dtClient) createProject(ctx context.Context, name, version, parentUUID string) (*project, error) { + body := map[string]any{"name": name, "version": version, "tags": []tag{{Name: managedTag}}} + if parentUUID != "" { + body["parent"] = map[string]string{"uuid": parentUUID} + } + var p project + if _, err := c.do(ctx, http.MethodPut, "/api/v1/project", body, &p, http.StatusCreated, http.StatusOK); err != nil { + return nil, err + } + return &p, nil +} + +// uploadBOM uploads bom as the latest version of the existing project +// name:version. Dependency-Track processes the BOM asynchronously after +// answering. +func (c *dtClient) uploadBOM(ctx context.Context, name, version string, bom []byte) error { body := map[string]any{ "projectName": name, "projectVersion": version, - "autoCreate": true, + "autoCreate": false, "isLatest": true, - "projectTags": []tag{{Name: managedTag}}, "bom": base64.StdEncoding.EncodeToString(bom), } - if parentUUID != "" { - body["parentUUID"] = parentUUID - } _, err := c.do(ctx, http.MethodPut, "/api/v1/bom", body, nil, http.StatusOK) return err } diff --git a/sbomexport/dtrack_test.go b/sbomexport/dtrack_test.go index c131357c..be2f40df 100644 --- a/sbomexport/dtrack_test.go +++ b/sbomexport/dtrack_test.go @@ -39,7 +39,7 @@ func TestDTClientLooksUpCreatesAndPatchesProjects(t *testing.T) { assert.Equal(t, ns.UUID, p.UUID) } -func TestDTClientUploadsTheBOMUnderItsParent(t *testing.T) { +func TestDTClientCreatesAProjectUnderItsParentAndUploadsItsBOM(t *testing.T) { ctx := context.Background() now := time.Date(2026, 11, 10, 12, 0, 0, 0, time.UTC) f, srv, keyFile := newFakeDT(t, func() time.Time { return now }) @@ -47,7 +47,9 @@ func TestDTClientUploadsTheBOMUnderItsParent(t *testing.T) { ns, err := c.createCollection(ctx, "cluster/apps", "current", "", parentLogic) require.NoError(t, err) - require.NoError(t, c.uploadBOM(ctx, "cluster/apps/redis", "8", ns.UUID, curlCycloneDX)) + _, err = c.createProject(ctx, "cluster/apps/redis", "8", ns.UUID) + require.NoError(t, err) + require.NoError(t, c.uploadBOM(ctx, "cluster/apps/redis", "8", curlCycloneDX)) got, parent, ok := f.project("cluster/apps/redis", "8") require.True(t, ok) @@ -84,7 +86,7 @@ func TestDTClientErrorCarriesTheStatusAndTheStartOfTheBody(t *testing.T) { require.NoError(t, os.WriteFile(keyFile, []byte(testAPIKey), 0600)) c := &dtClient{baseURL: srv.URL, apiKeyFile: keyFile, http: srv.Client()} - err := c.uploadBOM(context.Background(), "cluster/apps/redis", "8", "", curlCycloneDX) + err := c.uploadBOM(context.Background(), "cluster/apps/redis", "8", curlCycloneDX) require.Error(t, err) assert.Contains(t, err.Error(), "PUT /api/v1/bom: HTTP 500: "+strings.Repeat("x", errBody)) diff --git a/sbomexport/exporter.go b/sbomexport/exporter.go index a0b5db85..ccd57257 100644 --- a/sbomexport/exporter.go +++ b/sbomexport/exporter.go @@ -85,6 +85,7 @@ type Options struct { SkipNamespace func(namespace string) bool } +// validate checks the options that need no Dependency-Track call. func (o Options) validate() error { u, err := url.Parse(o.URL) if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" { @@ -117,10 +118,11 @@ type Exporter struct { namespaceLister corelisters.NamespaceLister mu sync.Mutex - sbomByImage map[string]sbomMeta // digest → its SBOMSyft - projects map[string]string // collection reference → uuid, emptied on every resync - pendingMoves map[string]namespaceInfo // namespace → what it was before its layout changed - logged map[string]bool // problems logged since the last resync + sbomByImage map[string]sbomMeta // digest → its SBOMSyft + projects map[projectRef]string // collection → uuid, emptied on every resync + pendingMoves map[string]*pendingMove // namespace → where its move starts + moveSeq uint64 // counts namespace layout changes + logged map[string]bool // problems logged since the last resync stats stats } @@ -131,6 +133,8 @@ func New(opts Options, pods kubernetes.Interface, storage kssc.Interface) (*Expo return newExporter(opts, pods, storageSBOMs{client: storage}, &http.Client{Timeout: requestTimeout}, time.Now) } +// newExporter is New with the SBOM store, the HTTP client and the clock +// replaceable, for tests. func newExporter(opts Options, pods kubernetes.Interface, sboms sbomStore, httpClient *http.Client, now func() time.Time) (*Exporter, error) { if err := opts.validate(); err != nil { return nil, err @@ -157,8 +161,8 @@ func newExporter(opts Options, pods kubernetes.Interface, sboms sbomStore, httpC namespaceInformer: informer, namespaceLister: corelisters.NewNamespaceLister(informer.GetIndexer()), sbomByImage: map[string]sbomMeta{}, - projects: map[string]string{}, - pendingMoves: map[string]namespaceInfo{}, + projects: map[projectRef]string{}, + pendingMoves: map[string]*pendingMove{}, logged: map[string]bool{}, } if _, err := informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ @@ -217,21 +221,33 @@ func (e *Exporter) ObserveSBOM(eventType watch.EventType, obj *spdxv1beta1.SBOMS } } +// pendingMove is a namespace whose layout changed: its projects still sit +// where from put them. seq tells whether another change came in since. +type pendingMove struct { + from namespaceInfo + seq uint64 +} + // namespaceChanged queues a move of the namespace's projects when a change of // its labels or of its kubescape.io/dtrack-* annotations can change where its -// images go. The first state before the move runs is the one moved from. +// images go. The state before the first pending change is the one moved from. func (e *Exporter) namespaceChanged(oldNS, newNS *corev1.Namespace) { if maps.Equal(oldNS.Labels, newNS.Labels) && maps.Equal(layoutAnnotations(oldNS), layoutAnnotations(newNS)) { return } e.mu.Lock() - if _, ok := e.pendingMoves[newNS.Name]; !ok { - e.pendingMoves[newNS.Name] = infoOf(oldNS) + pm, ok := e.pendingMoves[newNS.Name] + if !ok { + pm = &pendingMove{from: infoOf(oldNS)} + e.pendingMoves[newNS.Name] = pm } + e.moveSeq++ + pm.seq = e.moveSeq e.mu.Unlock() e.queue.Add(moveKeyPrefix + newNS.Name) } +// layoutAnnotations returns the kubescape.io/dtrack-* annotations of ns. func layoutAnnotations(ns *corev1.Namespace) map[string]string { out := map[string]string{} for k, v := range ns.Annotations { @@ -242,6 +258,7 @@ func layoutAnnotations(ns *corev1.Namespace) map[string]string { return out } +// infoOf copies what the layout needs from ns. func infoOf(ns *corev1.Namespace) namespaceInfo { return namespaceInfo{name: ns.Name, labels: maps.Clone(ns.Labels), annotations: maps.Clone(ns.Annotations)} } @@ -286,6 +303,8 @@ func (e *Exporter) Start(ctx context.Context) { } } +// work drains the queue: image digests are exported, namespace keys moved. +// A failed item is retried with backoff, then left to the next resync. func (e *Exporter) work(ctx context.Context) { for { key, shutdown := e.queue.Get() @@ -349,7 +368,7 @@ func (e *Exporter) resync(ctx context.Context) error { } e.mu.Lock() e.sbomByImage = byImage - e.projects = map[string]string{} + e.projects = map[projectRef]string{} e.mu.Unlock() for _, d := range e.running.digests() { @@ -358,6 +377,7 @@ func (e *Exporter) resync(ctx context.Context) error { return nil } +// sbom returns the metadata of digest's SBOMSyft, if one is known. func (e *Exporter) sbom(digest string) (sbomMeta, bool) { e.mu.Lock() defer e.mu.Unlock() @@ -431,21 +451,32 @@ func (e *Exporter) process(ctx context.Context, digest string) error { // exportTo makes p's project hold m's SBOM as its latest version. An image // whose project imported a BOM no older than its SBOMSyft is only marked // latest. It never uploads to a project it did not create, nor to one that -// holds the BOM of another image that still runs. +// holds the BOM of another image that still runs. Before it uploads, it +// creates a missing project and records m's digest on it, so a second image +// can never take a project whose upload went through but whose owner was not +// recorded. func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placement, bom *[]byte) (result, error) { - existing, err := e.dt.lookup(ctx, p.name, p.version) + target, err := e.dt.lookup(ctx, p.name, p.version) if err != nil { return 0, err } - parentUUID := "" - if existing != nil { - if !existing.managed() { + owner, parentUUID := "", "" + if target != nil { + if !target.managed() { e.logOnce(ctx, "conflict/"+p.name+":"+p.version, "a Dependency-Track project with this name and version exists and was not created by Kubescape, skipping the image", helpers.String("project", p.name), helpers.String("version", p.version), helpers.String("image", m.imageTag), helpers.String("namespace", ns)) return resultConflict, nil } - owner, err := e.dt.digestOwner(ctx, existing.UUID) - if err != nil { + // Resolve the parent of an existing project too, so a collection the + // exporter created follows a changed root. + if _, err := e.parent(ctx, p); err != nil { + var missing *missingProjectError + if !errors.As(err, &missing) { + return 0, err + } + e.logMissing(ctx, missing, ns) + } + if owner, err = e.dt.digestOwner(ctx, target.UUID); err != nil { return 0, err } if owner != "" && owner != m.digest && len(e.running.namespaces(owner)) > 0 { @@ -454,9 +485,9 @@ func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placem helpers.String("ownerDigest", owner), helpers.String("digest", m.digest)) return resultConflict, nil } - if owner == m.digest && !existing.LastBOMImport.Time().Before(m.created) { - if !existing.IsLatest { - if err := e.dt.patch(ctx, existing.UUID, map[string]any{"isLatest": true}); err != nil { + if owner == m.digest && !target.LastBOMImport.Time().Before(m.created) { + if !target.IsLatest { + if err := e.dt.patch(ctx, target.UUID, map[string]any{"isLatest": true}); err != nil { return 0, err } } @@ -466,8 +497,7 @@ func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placem parentUUID, err = e.parent(ctx, p) var missing *missingProjectError if errors.As(err, &missing) { - e.logOnce(ctx, "missing/"+missing.ref.String(), "a Dependency-Track project the layout names does not exist and may not be created, skipping the namespace's images", - helpers.String("project", missing.ref.String()), helpers.String("reason", missing.reason), helpers.String("namespace", ns)) + e.logMissing(ctx, missing, ns) return resultSkipped, nil } if err != nil { @@ -488,25 +518,24 @@ func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placem return 0, fmt.Errorf("convert SBOMSyft %s/%s: %w", m.namespace, m.name, err) } } - if err := e.dt.uploadBOM(ctx, p.name, p.version, parentUUID, *bom); err != nil { - return 0, err - } - - // Record which image the project holds. The upload created the project - // when it was missing; tag it in case Dependency-Track ignored projectTags. - uploaded, err := e.dt.lookup(ctx, p.name, p.version) - if err != nil { - return 0, err - } - if uploaded == nil { - return 0, fmt.Errorf("project %s:%s is missing after the upload", p.name, p.version) + if target == nil { + if target, err = e.dt.createProject(ctx, p.name, p.version, parentUUID); err != nil { + return 0, err + } } - if !uploaded.managed() { - if err := e.dt.patch(ctx, uploaded.UUID, map[string]any{"tags": append(uploaded.Tags, tag{Name: managedTag})}); err != nil { + if owner != m.digest { + if err := e.dt.setDigestOwner(ctx, target.UUID, m.digest); err != nil { return 0, err } } - return resultSent, e.dt.setDigestOwner(ctx, uploaded.UUID, m.digest) + return resultSent, e.dt.uploadBOM(ctx, p.name, p.version, *bom) +} + +// logMissing logs, once per resync, a project the layout names that is +// missing and may not be created. +func (e *Exporter) logMissing(ctx context.Context, missing *missingProjectError, ns string) { + e.logOnce(ctx, "missing/"+missing.ref.String(), "a Dependency-Track project the layout names does not exist and may not be created, skipping the namespace's images", + helpers.String("project", missing.ref.String()), helpers.String("reason", missing.reason), helpers.String("namespace", ns)) } // missingProjectError is a project the layout names that does not exist and @@ -516,6 +545,7 @@ type missingProjectError struct { reason string } +// Error describes the missing project. func (e *missingProjectError) Error() string { return fmt.Sprintf("project %s %s", e.ref, e.reason) } @@ -530,11 +560,13 @@ func (e *Exporter) parent(ctx context.Context, p placement) (string, error) { // collection returns the uuid of the collection project ref names. A missing // one is created, under the project parentOf returns, only when create allows -// it. The collection logic is set only on projects the exporter created. +// it. A collection the exporter created keeps its collection logic and, when +// parentOf is given, its place under the project parentOf returns, so a +// changed root takes the collection along. Other collections are never +// changed. func (e *Exporter) collection(ctx context.Context, ref projectRef, create bool, logic string, parentOf func() (string, error)) (string, error) { - key := ref.String() e.mu.Lock() - uuid, ok := e.projects[key] + uuid, ok := e.projects[ref] e.mu.Unlock() if ok { return uuid, nil @@ -545,13 +577,11 @@ func (e *Exporter) collection(ctx context.Context, ref projectRef, create bool, return "", err } switch { - case p != nil: - if p.managed() && p.CollectionLogic != logic { - if err := e.dt.patch(ctx, p.UUID, map[string]any{"collectionLogic": logic}); err != nil { - return "", err - } - logger.L().Info("set collection logic of Dependency-Track project", helpers.String("project", key), helpers.String("collectionLogic", logic)) + case p != nil && p.managed(): + if err := e.reconcileCollection(ctx, p, logic, parentOf); err != nil { + return "", err } + case p != nil: case ref.uuid != "": return "", &missingProjectError{ref: ref, reason: "does not exist; a project named by UUID is never created"} case !create: @@ -566,15 +596,53 @@ func (e *Exporter) collection(ctx context.Context, ref projectRef, create bool, if p, err = e.dt.createCollection(ctx, ref.name, ref.version, parentUUID, logic); err != nil { return "", err } - logger.L().Info("created Dependency-Track collection project", helpers.String("project", key), helpers.String("collectionLogic", logic)) + logger.L().Info("created Dependency-Track collection project", helpers.String("project", ref.String()), helpers.String("collectionLogic", logic)) } e.mu.Lock() - e.projects[key] = p.UUID + e.projects[ref] = p.UUID e.mu.Unlock() return p.UUID, nil } +// reconcileCollection sets the collection logic of a collection the exporter +// created and, when parentOf is given, moves it under the project parentOf +// returns. When that project is missing and may not be created, the +// collection stays where it is. +func (e *Exporter) reconcileCollection(ctx context.Context, p *project, logic string, parentOf func() (string, error)) error { + fields := map[string]any{} + if p.CollectionLogic != logic { + fields["collectionLogic"] = logic + } + if parentOf != nil { + want, err := parentOf() + var missing *missingProjectError + switch { + case errors.As(err, &missing): + e.logMissing(ctx, missing, "") + case err != nil: + return err + default: + // a lookup by name leaves the parent out; a GET by UUID has it + full, err := e.dt.getByUUID(ctx, p.UUID) + if err != nil { + return err + } + if full != nil && (full.Parent == nil || full.Parent.UUID != want) { + fields["parent"] = map[string]string{"uuid": want} + } + } + } + if len(fields) == 0 { + return nil + } + if err := e.dt.patch(ctx, p.UUID, fields); err != nil { + return err + } + logger.L().Info("updated Dependency-Track collection project", helpers.String("project", p.Name+":"+p.Version), helpers.Interface("changes", fields)) + return nil +} + // move moves the projects of the images running in ns from where the // namespace's previous labels and annotations put them to where the current // ones do, so their history in Dependency-Track moves with them. It moves only @@ -583,7 +651,12 @@ func (e *Exporter) collection(ctx context.Context, ref projectRef, create bool, // running is not seen, so those images start new projects. func (e *Exporter) move(ctx context.Context, ns string) error { e.mu.Lock() - before, ok := e.pendingMoves[ns] + pm, ok := e.pendingMoves[ns] + var before namespaceInfo + var seq uint64 + if ok { + before, seq = pm.from, pm.seq + } e.mu.Unlock() if !ok { return nil @@ -609,8 +682,18 @@ func (e *Exporter) move(ctx context.Context, ns string) error { if err := errors.Join(errs...); err != nil { return err } + // A change that came in meanwhile moves on from where this one ended. The + // collection cache is dropped, so the exports below find, and reconcile, + // the collections of the new layout. e.mu.Lock() - delete(e.pendingMoves, ns) + e.projects = map[projectRef]string{} + if cur, ok := e.pendingMoves[ns]; ok { + if cur.seq == seq { + delete(e.pendingMoves, ns) + } else { + cur.from = now + } + } e.mu.Unlock() for _, d := range digests { e.queue.Add(d) @@ -618,20 +701,23 @@ func (e *Exporter) move(ctx context.Context, ns string) error { return nil } +// moveProject moves the exporter's project at from to to: its name, version +// and parent. When only the parent changes, the project found at to is the +// same one, and only its parent changes. A different project at to is left +// alone. func (e *Exporter) moveProject(ctx context.Context, ns string, from, to placement) error { p, err := e.dt.lookup(ctx, from.name, from.version) if err != nil || p == nil || !p.managed() { return err } target, err := e.dt.lookup(ctx, to.name, to.version) - if err != nil || target != nil { + if err != nil || (target != nil && target.UUID != p.UUID) { return err } parentUUID, err := e.parent(ctx, to) var missing *missingProjectError if errors.As(err, &missing) { - e.logOnce(ctx, "missing/"+missing.ref.String(), "a Dependency-Track project the layout names does not exist and may not be created, skipping the namespace's images", - helpers.String("project", missing.ref.String()), helpers.String("reason", missing.reason), helpers.String("namespace", ns)) + e.logMissing(ctx, missing, ns) return nil } if err != nil { @@ -646,6 +732,7 @@ func (e *Exporter) moveProject(ctx context.Context, ns string, from, to placemen return nil } +// logTooLarge logs, once per resync, that the SBOM of m is over MaxSBOMSize. func (e *Exporter) logTooLarge(ctx context.Context, m sbomMeta, size int64) { e.logOnce(ctx, "too-large/"+m.digest, "SBOM is larger than sbomExport.maxSBOMSize, skipping it", helpers.String("image", m.imageTag), helpers.Int("size", int(size)), helpers.Int("maxSBOMSize", int(e.opts.MaxSBOMSize))) @@ -671,7 +758,7 @@ func (e *Exporter) logFailure(ctx context.Context, err error, image, name, versi if e.stats.authLogged.Swap(true) { return } - fields = append(fields, helpers.String("hint", "the API key's team needs BOM_UPLOAD, PROJECT_CREATION_UPLOAD and VIEW_PORTFOLIO, plus PORTFOLIO_MANAGEMENT (on 5.x: PORTFOLIO_MANAGEMENT_CREATE and PORTFOLIO_MANAGEMENT_UPDATE) to create and update projects; with portfolio access control on, access to the projects the layout names")) + fields = append(fields, helpers.String("hint", "the API key's team needs VIEW_PORTFOLIO, BOM_UPLOAD and PORTFOLIO_MANAGEMENT (on 5.x: PORTFOLIO_MANAGEMENT_CREATE and PORTFOLIO_MANAGEMENT_UPDATE); with portfolio access control on, access to the projects the layout names")) } logger.L().Ctx(ctx).Error("failed to export SBOM to Dependency-Track", fields...) } diff --git a/sbomexport/exporter_layout_test.go b/sbomexport/exporter_layout_test.go index 354486fe..501c9a50 100644 --- a/sbomexport/exporter_layout_test.go +++ b/sbomexport/exporter_layout_test.go @@ -192,30 +192,113 @@ func TestExporterMovesItsProjectsWhenANamespaceLayoutChanges(t *testing.T) { assert.Equal(t, int64(1), te.stats.moved.Load()) } -func TestExporterTagsItsProjectWhenDependencyTrackIgnoresProjectTags(t *testing.T) { +// The namespace a pod event names is resolved when the image is exported, so +// an image seen before its Namespace object still lands in the right place. +func TestExporterUsesTheNamespaceAnnotationsWhenItExports(t *testing.T) { + te := newTestExporter(t, namespace("payments", map[string]string{AnnotationProjectName: "{{ .Repository }}"})) + te.ObserveSBOM(watch.Added, sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + te.ObservePod(watch.Added, runningPod("payments", "api", redisImageID)) + + require.Empty(t, te.drain()) + + assert.Equal(t, []string{redisProject + ":8"}, te.dt.takeUploads()) +} + +// The image's digest is recorded on its project before the upload, so a +// project never holds a BOM without an owner another image could take. +func TestExporterUploadsOnlyAfterRecordingTheImage(t *testing.T) { te := newTestExporter(t, runningPod("apps", "web", redisImageID)) - te.dt.ignoreUpTags = true te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + te.dt.failProperties = 1 require.NoError(t, te.resync(context.Background())) + assert.Len(t, te.drain(), 1) + assert.Empty(t, te.dt.takeUploads(), "no upload without a recorded owner") + + te.queue.Add(digestOf(redisImageID)) require.Empty(t, te.drain()) + assert.Equal(t, []string{"cluster/apps/" + redisProject + ":8"}, te.dt.takeUploads()) + p, _, _ := te.dt.project("cluster/apps/"+redisProject, "8") + owner, err := te.dt.digestOwnerForTest(p.UUID) + require.NoError(t, err) + assert.Equal(t, digestOf(redisImageID), owner) +} - image, _, _ := te.dt.project("cluster/apps/"+redisProject, "8") - assert.True(t, image.managed()) - te.clock.t = te.clock.t.Add(time.Hour) +func TestCollectionKeepsReferencesThatOnlyDifferInWhereTheColonIs(t *testing.T) { + te := newTestExporter(t) + a := te.dt.addProject("team:ops", "current", "") + b := te.dt.addProject("team", "ops:current", "") + + gotA, err := te.collection(context.Background(), projectRef{name: "team:ops", version: "current"}, false, parentLogic, nil) + require.NoError(t, err) + gotB, err := te.collection(context.Background(), projectRef{name: "team", version: "ops:current"}, false, parentLogic, nil) + require.NoError(t, err) + + assert.Equal(t, a, gotA) + assert.Equal(t, b, gotB) +} + +func TestExporterMovesAProjectWhenOnlyItsParentChanges(t *testing.T) { + annotations := map[string]string{AnnotationParent: "Team A:current", AnnotationProjectName: "{{ .Repository }}"} + te := newTestExporter(t, namespace("payments", annotations), runningPod("payments", "api", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) require.NoError(t, te.resync(context.Background())) require.Empty(t, te.drain()) - assert.Equal(t, []string{"cluster/apps/" + redisProject + ":8"}, te.dt.takeUploads(), "the second resync finds it its own and unchanged") + before, parent, _ := te.dt.project(redisProject, "8") + require.Equal(t, "Team A", parent) + te.dt.takeUploads() + + annotations[AnnotationParent] = "Team B:current" + te.updateNamespace(t, namespace("payments", annotations)) + require.Empty(t, te.drain()) + + after, parent, _ := te.dt.project(redisProject, "8") + assert.Equal(t, before.UUID, after.UUID) + assert.Equal(t, "Team B", parent) + assert.Empty(t, te.dt.takeUploads()) } -// The namespace a pod event names is resolved when the image is exported, so -// an image seen before its Namespace object still lands in the right place. -func TestExporterUsesTheNamespaceAnnotationsWhenItExports(t *testing.T) { - te := newTestExporter(t, namespace("payments", map[string]string{AnnotationProjectName: "{{ .Repository }}"})) - te.ObserveSBOM(watch.Added, sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) - te.ObservePod(watch.Added, runningPod("payments", "api", redisImageID)) +// A parent the exporter created follows its namespace's root. +func TestExporterMovesItsParentWhenOnlyTheRootChanges(t *testing.T) { + te := newTestExporter(t, namespace("payments", nil), runningPod("payments", "api", redisImageID)) + te.dt.addProject("Finance", "current", "") + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + _, root, _ := te.dt.project("cluster/payments", "current") + require.Equal(t, "cluster", root) + te.updateNamespace(t, namespace("payments", map[string]string{AnnotationRootProject: "Finance:current", AnnotationRootProjectCreate: "false"})) require.Empty(t, te.drain()) - assert.Equal(t, []string{redisProject + ":8"}, te.dt.takeUploads()) + _, root, _ = te.dt.project("cluster/payments", "current") + assert.Equal(t, "Finance", root) + finance, _, _ := te.dt.project("Finance", "current") + assert.Empty(t, finance.Tags, "the root itself is not changed") +} + +// A layout change that comes in while the previous one is being moved is not +// lost: the projects end up where the latest layout puts them. +func TestExporterKeepsALayoutChangeThatComesInDuringAMove(t *testing.T) { + te := newTestExporter(t, namespace("payments", nil), runningPod("payments", "api", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + before, _, _ := te.dt.project("cluster/payments/"+redisProject, "8") + + toC := namespace("payments", map[string]string{AnnotationParent: "Team C:current", AnnotationProjectName: "{{ .Repository }}"}) + te.dt.onPatch = func() { + // while the move to B patches the project, the namespace changes to C + seq := te.moveSeqForTest() + _, err := te.client.CoreV1().Namespaces().Update(context.Background(), toC, metav1.UpdateOptions{}) + assert.NoError(t, err) + assert.Eventually(t, func() bool { return te.moveSeqForTest() > seq }, 5*time.Second, 10*time.Millisecond) + } + te.updateNamespace(t, namespace("payments", map[string]string{AnnotationParent: "Team B:current", AnnotationProjectName: "{{ .Repository }}"})) + require.Empty(t, te.drain()) + + after, parent, ok := te.dt.project(redisProject, "8") + require.True(t, ok) + assert.Equal(t, before.UUID, after.UUID) + assert.Equal(t, "Team C", parent) } diff --git a/sbomexport/exporter_test.go b/sbomexport/exporter_test.go index 23205faa..58a422e7 100644 --- a/sbomexport/exporter_test.go +++ b/sbomexport/exporter_test.go @@ -133,6 +133,22 @@ func (te *testExporter) drain() []error { return errs } +// updateNamespace replaces ns and waits until the exporter has queued its move. +func (te *testExporter) updateNamespace(t *testing.T, ns *corev1.Namespace) { + t.Helper() + seq := te.moveSeqForTest() + _, err := te.client.CoreV1().Namespaces().Update(context.Background(), ns, metav1.UpdateOptions{}) + require.NoError(t, err) + require.Eventually(t, func() bool { return te.moveSeqForTest() > seq }, 5*time.Second, 10*time.Millisecond) +} + +// moveSeqForTest returns how many namespace layout changes the exporter saw. +func (te *testExporter) moveSeqForTest() uint64 { + te.mu.Lock() + defer te.mu.Unlock() + return te.moveSeq +} + func namespace(name string, annotations map[string]string) *corev1.Namespace { return &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name, Annotations: annotations}} } @@ -179,7 +195,6 @@ func TestNewRejectsAnInvalidConfiguration(t *testing.T) { {"no URL", func(o *Options) { o.URL = "" }}, {"a URL without a scheme", func(o *Options) { o.URL = "dtrack.example.com" }}, {"no API key file", func(o *Options) { o.APIKeyFile = "" }}, - {"no cluster name for the default root project", func(o *Options) { o.Cluster = "" }}, {"a template that does not parse", func(o *Options) { o.Layout.Name = "{{ .Namespace" }}, {"a template with an unknown field", func(o *Options) { o.Layout.Version = "{{ .Digest }}" }}, {"no size limit", func(o *Options) { o.MaxSBOMSize = 0 }}, diff --git a/sbomexport/fakedtrack_test.go b/sbomexport/fakedtrack_test.go index 3a769ab4..bafef4bc 100644 --- a/sbomexport/fakedtrack_test.go +++ b/sbomexport/fakedtrack_test.go @@ -20,16 +20,17 @@ const testAPIKey = "test-api-key" // makes: project lookup, get by UUID, create, patch (including a move), BOM // upload with autoCreate and projectTags, and project properties. type fakeDT struct { - mu sync.Mutex - now func() time.Time - projects map[string]*project // uuid → project - parents map[string]string // project uuid → parent uuid - properties map[string]map[string]string // project uuid → "group/name" → value - uploads []string // "name:version" of every BOM upload - nextID int - fail int // answer the next fail requests with 503 - ignoreUpTags bool // drop projectTags on upload, as a Dependency-Track that does not support them - keyFile string // holds testAPIKey + mu sync.Mutex + now func() time.Time + projects map[string]*project // uuid → project + parents map[string]string // project uuid → parent uuid + properties map[string]map[string]string // project uuid → "group/name" → value + uploads []string // "name:version" of every BOM upload + nextID int + fail int // answer the next fail requests with 503 + failProperties int // answer the next property writes with 503 + onPatch func() // called before a PATCH is applied, without the lock + keyFile string // holds testAPIKey } // newFakeDT starts a fakeDT and returns it with an API key file holding @@ -47,6 +48,15 @@ func newFakeDT(t *testing.T, now func() time.Time) (*fakeDT, *httptest.Server, s } func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPatch { + f.mu.Lock() + hook := f.onPatch + f.onPatch = nil + f.mu.Unlock() + if hook != nil { + hook() + } + } f.mu.Lock() defer f.mu.Unlock() if r.Header.Get("X-Api-Key") != testAPIKey { @@ -119,6 +129,7 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { } if req.Parent != nil { f.parents[uuid] = req.Parent.UUID + p.Parent = &projectParent{UUID: req.Parent.UUID} } if req.Tags != nil { p.Tags = *req.Tags @@ -126,23 +137,19 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, p) case r.Method == http.MethodPut && path == "/api/v1/bom": var req struct { - ProjectName, ProjectVersion, ParentUUID, BOM string - AutoCreate, IsLatest bool - ProjectTags []tag + ProjectName, ProjectVersion, BOM string + AutoCreate, IsLatest bool } _ = json.NewDecoder(r.Body).Decode(&req) bom, err := base64.StdEncoding.DecodeString(req.BOM) - if err != nil || !json.Valid(bom) || !req.AutoCreate { + if err != nil || !json.Valid(bom) { http.Error(w, "bad BOM upload", http.StatusBadRequest) return } p := f.byNameLocked(req.ProjectName, req.ProjectVersion) if p == nil { - tags := req.ProjectTags - if f.ignoreUpTags { - tags = nil - } - p = f.addLocked(req.ProjectName, req.ProjectVersion, req.ParentUUID, tags) + http.Error(w, "The project could not be found.", http.StatusNotFound) + return } if req.IsLatest { for _, other := range f.projects { @@ -174,6 +181,11 @@ func (f *fakeDT) serveProperty(w http.ResponseWriter, r *http.Request, uuid stri writeJSON(w, http.StatusOK, out) return } + if f.failProperties > 0 { + f.failProperties-- + http.Error(w, "unavailable", http.StatusServiceUnavailable) + return + } var p projectProperty _ = json.NewDecoder(r.Body).Decode(&p) key := p.GroupName + "/" + p.PropertyName @@ -207,6 +219,9 @@ func (f *fakeDT) byNameLocked(name, version string) *project { func (f *fakeDT) addLocked(name, version, parentUUID string, tags []tag) *project { f.nextID++ p := &project{UUID: fmt.Sprintf("00000000-0000-0000-0000-%012d", f.nextID), Name: name, Version: version, Tags: tags} + if parentUUID != "" { + p.Parent = &projectParent{UUID: parentUUID} + } f.projects[p.UUID] = p f.parents[p.UUID] = parentUUID return p diff --git a/sbomexport/index.go b/sbomexport/index.go index e7ff6bbb..c6560a19 100644 --- a/sbomexport/index.go +++ b/sbomexport/index.go @@ -28,6 +28,7 @@ type index struct { ended map[string]map[string]time.Time // digest → namespace → when its last pod ended } +// newIndex returns an empty index that keeps ended namespaces for grace. func newIndex(grace time.Duration, now func() time.Time) *index { return &index{ grace: grace, @@ -44,18 +45,21 @@ func newIndex(grace time.Duration, now func() time.Time) *index { func (x *index) setPod(key, namespace string, digests []string) []string { x.mu.Lock() defer x.mu.Unlock() - x.removeLocked(key) if len(digests) == 0 { + x.removeLocked(key) return nil } digests = slices.Compact(slices.Sorted(slices.Values(digests))) - x.pods[key] = podEntry{namespace: namespace, digests: digests} + // Count the new digests before dropping the old entry, so a digest the pod + // keeps never falls to zero and is not reported as started again. var started []string for _, d := range digests { if x.incLocked(d, namespace) { started = append(started, d) } } + x.removeLocked(key) + x.pods[key] = podEntry{namespace: namespace, digests: digests} return started } @@ -141,6 +145,7 @@ func (x *index) digests() []string { return out } +// removeLocked forgets the pod with key and uncounts its digests. func (x *index) removeLocked(key string) { p, ok := x.pods[key] if !ok { @@ -168,6 +173,8 @@ func (x *index) incLocked(digest, ns string) bool { return x.refs[digest][ns] == 1 } +// decLocked counts one less pod running digest in ns, and starts the grace +// period of ns when it was the last. func (x *index) decLocked(digest, ns string) { r := x.refs[digest] if r == nil || r[ns] == 0 { @@ -187,6 +194,7 @@ func (x *index) decLocked(digest, ns string) { x.ended[digest][ns] = x.now() } +// pruneLocked drops the namespaces whose grace period is over. func (x *index) pruneLocked() { now := x.now() for d, e := range x.ended { diff --git a/sbomexport/index_test.go b/sbomexport/index_test.go index ae24eb90..faacbba0 100644 --- a/sbomexport/index_test.go +++ b/sbomexport/index_test.go @@ -29,6 +29,16 @@ func TestIndexListsEveryNamespaceAnImageRunsIn(t *testing.T) { assert.Equal(t, []string{"sha256:a", "sha256:b"}, x.digests()) } +// A Modified event for a pod that keeps its images must not queue them again. +func TestIndexDoesNotReportAnUnchangedPodAsStarted(t *testing.T) { + x, _ := newTestIndex() + assert.Equal(t, []string{"sha256:a"}, x.setPod("apps/web", "apps", []string{"sha256:a"})) + + assert.Empty(t, x.setPod("apps/web", "apps", []string{"sha256:a"})) + assert.Equal(t, []string{"sha256:b"}, x.setPod("apps/web", "apps", []string{"sha256:a", "sha256:b"})) + assert.Equal(t, []string{"apps"}, x.namespaces("sha256:a")) +} + func TestIndexKeepsANamespaceForOneGracePeriodAfterItsLastPod(t *testing.T) { x, clock := newTestIndex() x.setPod("apps/web", "apps", []string{"sha256:a"}) diff --git a/sbomexport/layout.go b/sbomexport/layout.go index e736046f..8401d5ac 100644 --- a/sbomexport/layout.go +++ b/sbomexport/layout.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "io" "regexp" "strconv" "strings" @@ -61,6 +62,7 @@ type projectRef struct { name, version string } +// String returns the UUID, or name:version. func (r projectRef) String() string { if r.uuid != "" { return r.uuid @@ -112,6 +114,9 @@ type layout struct { nameByText map[string]*template.Template // parsed AnnotationProjectName values } +// parseTemplate parses text as the template for field. A missing map key is an +// error, so {{ .Labels.team }} fails for a namespace without that label instead +// of rendering "". func parseTemplate(field, text string) (*template.Template, error) { t, err := template.New(field).Option("missingkey=error").Parse(text) if err != nil { @@ -146,10 +151,17 @@ func newLayout(cluster string, l Layout) (*layout, error) { if err := errors.Join(errs...); err != nil { return nil, err } + // Catch unknown fields now. A missing label or an empty result is not an + // error here: a template like {{ .Labels.team }} renders only for + // namespaces that have the label, and resolve rejects it for the others. sample := TemplateData{Cluster: cluster, Namespace: "default", Repository: "docker.io/library/nginx", Tag: "1.27", Labels: map[string]string{}} for _, t := range []*template.Template{x.root[0], x.root[1], x.parent[0], x.parent[1], x.project[0], x.project[1]} { - if _, err := render(t, sample); err != nil { - errs = append(errs, err) + c, err := t.Clone() + if err == nil { + err = c.Option("missingkey=zero").Execute(io.Discard, sample) + } + if err != nil { + errs = append(errs, fmt.Errorf("render %s: %w", t.Name(), err)) } } return x, errors.Join(errs...) @@ -168,6 +180,7 @@ func render(t *template.Template, data TemplateData) (string, error) { return out, nil } +// projectNameTemplate parses an AnnotationProjectName value once and reuses it. func (x *layout) projectNameTemplate(text string) (*template.Template, error) { x.mu.Lock() defer x.mu.Unlock() diff --git a/sbomexport/layout_test.go b/sbomexport/layout_test.go index b8654c10..fd4e3739 100644 --- a/sbomexport/layout_test.go +++ b/sbomexport/layout_test.go @@ -50,6 +50,34 @@ func TestLayoutDefaultsBuildTheClusterNamespaceTree(t *testing.T) { }, p) } +// A template that needs a label is valid; a namespace without the label cannot +// be placed, and only that namespace is skipped. +func TestLayoutAcceptsATemplateThatNeedsANamespaceLabel(t *testing.T) { + for _, text := range []string{`{{ index .Labels "team" }}`, `{{ .Labels.team }}`} { + t.Run(text, func(t *testing.T) { + byTeam := defaultLayout + byTeam.Parent.Name = text + l, err := newLayout("prod", byTeam) + require.NoError(t, err) + + _, err = l.resolve(namespaceInfo{name: "apps"}, "docker.io/library/redis", "8") + assert.Error(t, err, "no team label") + p, err := l.resolve(namespaceInfo{name: "apps", labels: map[string]string{"team": "payments"}}, "docker.io/library/redis", "8") + require.NoError(t, err) + assert.Equal(t, projectRef{name: "payments", version: "current"}, p.parent) + }) + } +} + +func TestNewLayoutRejectsATemplateWithAnUnknownField(t *testing.T) { + bad := defaultLayout + bad.Parent.Name = "{{ .Cluster }}/{{ .Team }}" + + _, err := newLayout("prod", bad) + + assert.ErrorContains(t, err, "project.parent.name") +} + func TestLayoutTemplatesSeeTheNamespaceLabels(t *testing.T) { byTeam := defaultLayout byTeam.Parent.Name = `{{ .Cluster }}/{{ index .Labels "team" }}` diff --git a/sbomexport/store.go b/sbomexport/store.go index 58108e62..5c048e76 100644 --- a/sbomexport/store.go +++ b/sbomexport/store.go @@ -69,6 +69,7 @@ type storageSBOMs struct { client kssc.Interface } +// listSBOMs pages through every SBOMSyft and keeps the ones metaOf accepts. func (s storageSBOMs) listSBOMs(ctx context.Context) ([]sbomMeta, error) { var out []sbomMeta err := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { From 1712d6a63153ee07c319a3b6c110042b73917a22 Mon Sep 17 00:00:00 2001 From: Cristian Glavan Date: Wed, 11 Nov 2026 13:55:04 +0200 Subject: [PATCH 5/6] fix(sbomexport): keep a collection's logic when moving it Dependency-Track 4.14 sets a project's collection logic to NONE on a PATCH that leaves it out. Moving a parent under a new root sent only the parent, so the collection stopped aggregating until the next pass restored it. The PATCH now carries the collection logic too, and the fake Dependency-Track resets it the same way. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristian Glavan --- sbomexport/exporter.go | 6 +++++- sbomexport/exporter_layout_test.go | 7 +++++-- sbomexport/fakedtrack_test.go | 16 ++++++++++++++-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/sbomexport/exporter.go b/sbomexport/exporter.go index ccd57257..caae6364 100644 --- a/sbomexport/exporter.go +++ b/sbomexport/exporter.go @@ -636,7 +636,11 @@ func (e *Exporter) reconcileCollection(ctx context.Context, p *project, logic st if len(fields) == 0 { return nil } - if err := e.dt.patch(ctx, p.UUID, fields); err != nil { + // Dependency-Track sets the collection logic to NONE on a PATCH that + // leaves it out, so moving a collection sends its logic too. + body := maps.Clone(fields) + body["collectionLogic"] = logic + if err := e.dt.patch(ctx, p.UUID, body); err != nil { return err } logger.L().Info("updated Dependency-Track collection project", helpers.String("project", p.Name+":"+p.Version), helpers.Interface("changes", fields)) diff --git a/sbomexport/exporter_layout_test.go b/sbomexport/exporter_layout_test.go index 501c9a50..453cbdcb 100644 --- a/sbomexport/exporter_layout_test.go +++ b/sbomexport/exporter_layout_test.go @@ -265,14 +265,17 @@ func TestExporterMovesItsParentWhenOnlyTheRootChanges(t *testing.T) { te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) require.NoError(t, te.resync(context.Background())) require.Empty(t, te.drain()) - _, root, _ := te.dt.project("cluster/payments", "current") + parent, root, _ := te.dt.project("cluster/payments", "current") require.Equal(t, "cluster", root) + patches := te.dt.patchCount(parent.UUID) te.updateNamespace(t, namespace("payments", map[string]string{AnnotationRootProject: "Finance:current", AnnotationRootProjectCreate: "false"})) require.Empty(t, te.drain()) - _, root, _ = te.dt.project("cluster/payments", "current") + parent, root, _ = te.dt.project("cluster/payments", "current") assert.Equal(t, "Finance", root) + assert.Equal(t, parentLogic, parent.CollectionLogic) + assert.Equal(t, 1, te.dt.patchCount(parent.UUID)-patches, "one PATCH moves it and keeps its collection logic") finance, _, _ := te.dt.project("Finance", "current") assert.Empty(t, finance.Tags, "the root itself is not changed") } diff --git a/sbomexport/fakedtrack_test.go b/sbomexport/fakedtrack_test.go index bafef4bc..c883e6bc 100644 --- a/sbomexport/fakedtrack_test.go +++ b/sbomexport/fakedtrack_test.go @@ -18,7 +18,7 @@ const testAPIKey = "test-api-key" // fakeDT is an in-memory Dependency-Track that serves the calls dtClient // makes: project lookup, get by UUID, create, patch (including a move), BOM -// upload with autoCreate and projectTags, and project properties. +// upload to an existing project, and project properties. type fakeDT struct { mu sync.Mutex now func() time.Time @@ -26,6 +26,7 @@ type fakeDT struct { parents map[string]string // project uuid → parent uuid properties map[string]map[string]string // project uuid → "group/name" → value uploads []string // "name:version" of every BOM upload + patches map[string]int // project uuid → PATCH requests nextID int fail int // answer the next fail requests with 503 failProperties int // answer the next property writes with 503 @@ -41,7 +42,7 @@ func newFakeDT(t *testing.T, now func() time.Time) (*fakeDT, *httptest.Server, s if err := os.WriteFile(keyFile, []byte(testAPIKey+"\n"), 0600); err != nil { t.Fatal(err) } - f := &fakeDT{now: now, projects: map[string]*project{}, parents: map[string]string{}, properties: map[string]map[string]string{}, keyFile: keyFile} + f := &fakeDT{now: now, projects: map[string]*project{}, parents: map[string]string{}, properties: map[string]map[string]string{}, patches: map[string]int{}, keyFile: keyFile} srv := httptest.NewServer(f) t.Cleanup(srv.Close) return f, srv, keyFile @@ -115,9 +116,13 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { Tags *[]tag } _ = json.NewDecoder(r.Body).Decode(&req) + f.patches[uuid]++ if req.IsLatest != nil { p.IsLatest = *req.IsLatest } + // Like Dependency-Track 4.14, a PATCH without collectionLogic sets it + // to NONE. + p.CollectionLogic = "NONE" if req.CollectionLogic != nil { p.CollectionLogic = *req.CollectionLogic } @@ -235,6 +240,13 @@ func (f *fakeDT) addProject(name, version, parentUUID string) string { return f.addLocked(name, version, parentUUID, nil).UUID } +// patchCount returns how many PATCH requests the project with uuid got. +func (f *fakeDT) patchCount(uuid string) int { + f.mu.Lock() + defer f.mu.Unlock() + return f.patches[uuid] +} + // project returns a copy of name:version, and the name of its parent. func (f *fakeDT) project(name, version string) (project, string, bool) { f.mu.Lock() From 1c2717fafc63228273c4a6f8a820822d0bc61c63 Mon Sep 17 00:00:00 2001 From: Cristian Glavan Date: Wed, 11 Nov 2026 14:55:33 +0200 Subject: [PATCH 6/6] fix(sbomexport): re-send rebuilt SBOMs, retry waiting moves, bound the SBOM read - node-agent rebuilds the SBOM of a running image in place when its version changes, so the creation time stays. The exporter records the node-agent version of each upload in the project property kubescape/sbom-tool-version, after the upload, and sends the SBOM again when it differs. - A move whose destination is missing and may not be created stays pending and is tried again at every resync, before the images are exported. - A root-only change moves the namespace's parent right away, also when none of its images can be exported (too large, not ready). - The SBOMSyft object is read up to sbomExport.maxSBOMSize plus 1 MiB, so an object without a size annotation cannot make the operator buffer more. - Pod and SBOM events that arrive while a resync lists are kept instead of being replaced by the older lists. - Doc comments on the functions this change touches. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Cristian Glavan --- config/config.go | 1 + config/config_test.go | 2 + docs/features/sbom-export.md | 21 ++- mainhandler/handlerequests.go | 3 + mainhandler/sbomexport_test.go | 2 + sbomexport/dtrack.go | 37 +++--- sbomexport/dtrack_test.go | 2 + sbomexport/exporter.go | 197 +++++++++++++++++++++++------ sbomexport/exporter_layout_test.go | 70 ++++++++++ sbomexport/exporter_test.go | 118 ++++++++++++++++- sbomexport/fakedtrack_test.go | 19 ++- sbomexport/image_test.go | 2 + sbomexport/index.go | 12 +- sbomexport/index_test.go | 2 +- sbomexport/layout_test.go | 3 + sbomexport/store.go | 51 ++++++-- sbomexport/store_test.go | 21 ++- watcher/sbomwatcher.go | 2 + 18 files changed, 479 insertions(+), 86 deletions(-) diff --git a/config/config.go b/config/config.go index 4adfb423..a23a9db2 100644 --- a/config/config.go +++ b/config/config.go @@ -449,6 +449,7 @@ func (c *OperatorConfig) NodeAgentAutoscalerConfig() NodeAgentAutoscalerConfig { return c.serviceConfig.NodeAgentAutoscaler } +// LoadConfig reads config.json from path over the defaults set here. func LoadConfig(path string) (Config, error) { viper.AddConfigPath(path) viper.SetConfigName("config") diff --git a/config/config_test.go b/config/config_test.go index 5cbf9fae..5f73654b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -120,6 +120,7 @@ func TestLoadCapabilities(t *testing.T) { } } +// TestLoadConfig loads ../configuration/config.json and compares the result. func TestLoadConfig(t *testing.T) { type args struct { path string @@ -633,6 +634,7 @@ func TestNewOperatorConfig_RegexValidation(t *testing.T) { }) } +// TestSBOMExportEnabled checks that only "enable" turns the export on. func TestSBOMExportEnabled(t *testing.T) { tests := []struct { value string diff --git a/docs/features/sbom-export.md b/docs/features/sbom-export.md index 6497b6d7..1bacb2a0 100644 --- a/docs/features/sbom-export.md +++ b/docs/features/sbom-export.md @@ -19,11 +19,17 @@ default), which compares every running image with Dependency-Track. The resync catches up after Dependency-Track was unreachable or the operator restarted. For each namespace the image runs in, the exporter looks up its project. If the -project's last BOM import is not older than the `SBOMSyft`, the image is only -marked as the project's latest version. Otherwise the exporter reads the -`SBOMSyft`, converts its syft document to CycloneDX JSON (1.6) with syft, the -way `syft convert` does, and uploads it. An image that runs in several -namespaces is converted once. +project's last BOM import is not older than the `SBOMSyft`, and node-agent has +not rebuilt the SBOM since, the image is only marked as the project's latest +version. Otherwise the exporter reads the `SBOMSyft`, converts its syft +document to CycloneDX JSON (1.6) with syft, the way `syft convert` does, and +uploads it. An image that runs in several namespaces is converted once. + +node-agent rebuilds the SBOM of a running image when its own version changes, +and replaces the document in the same `SBOMSyft`. The exporter records the +node-agent version of each upload (`kubescape.io/tool-version`) in the project +property `kubescape/sbom-tool-version`, and sends the SBOM again when it +differs. An image stays counted as running in a namespace for one resync interval after its last pod there ended, so the image of a short Job is still exported once @@ -99,7 +105,7 @@ Rules: | A project exists that the operator did not create | Uses it, but never changes it: no collection logic, no BOM, no move. The operator tags what it creates with `managed-by:kubescape`; to let it take over projects created another way, for example by an earlier exporter, add that tag to them. | | The image's project exists and was not created by the operator | Skips the image and logs a conflict, instead of replacing someone's BOM. | | Two running images resolve to the same project and version | Uploads the first and skips the second, logging a conflict: one project version holds one BOM. The operator records each project's image digest in the project property `kubescape/image-digest`. When the first image stops running, the second takes the project over. | -| A namespace's labels or `kubescape.io/dtrack-*` annotations change | Moves the projects it created for that namespace's running images to their new place with a PATCH, so their history in Dependency-Track goes with them. A change made while the operator is not running is not seen; those images start new projects. | +| A namespace's labels or `kubescape.io/dtrack-*` annotations change | Moves the projects it created for that namespace's running images to their new place with a PATCH, so their history in Dependency-Track goes with them. When only the root changes, it moves the namespace's parent, also when none of its images can be exported. When the new place names a project that is missing and may not be created, the move waits and is tried again at every resync. A change made while the operator is not running is not seen; those images start new projects. | | A template or annotation cannot be used (a typo, an unknown field, an empty result, a create flag that is not `true` or `false`) | Logs it once per resync and skips that namespace's images. Invalid default templates in `config.json` turn the export off at startup, with the reason in the log. | ## SBOM size @@ -112,7 +118,8 @@ a `GOMEMLIMIT`. SBOMs larger than `sbomExport.maxSBOMSize` (10 MiB by default) are skipped and logged once per resync. The operator checks the `kubescape.io/resource-size` -annotation before it reads the SBOM, and the document's length after. +annotation before it reads the SBOM, stops reading an `SBOMSyft` that is more +than 1 MiB over the limit, and checks the document's length after. node-agent has its own `maxSBOMSize`, 20 MiB by default: it marks a larger SBOM `too-large` and stores it without its content, so there is nothing to export. diff --git a/mainhandler/handlerequests.go b/mainhandler/handlerequests.go index 996fd942..b5b3d70a 100644 --- a/mainhandler/handlerequests.go +++ b/mainhandler/handlerequests.go @@ -141,6 +141,9 @@ func (mainHandler *MainHandler) SetupContinuousScanning(ctx context.Context) err return nil } +// HandleWatchers starts the watchers the enabled components need: commands, +// SBOMs or pods for image scans and the SBOM export, container profiles and +// security exceptions. func (mainHandler *MainHandler) HandleWatchers(ctx context.Context) { defer func() { if err := recover(); err != nil { diff --git a/mainhandler/sbomexport_test.go b/mainhandler/sbomexport_test.go index 4de5e239..46f61c3e 100644 --- a/mainhandler/sbomexport_test.go +++ b/mainhandler/sbomexport_test.go @@ -27,6 +27,8 @@ func sbomExportConfig(t *testing.T, capabilities config.Capabilities, export con return cfg } +// TestNewSBOMExporter checks when the operator starts the export and when it +// runs without it. func TestNewSBOMExporter(t *testing.T) { valid := config.SBOMExportConfig{ ResyncInterval: time.Hour, diff --git a/sbomexport/dtrack.go b/sbomexport/dtrack.go index a0760aa2..7c78a328 100644 --- a/sbomexport/dtrack.go +++ b/sbomexport/dtrack.go @@ -20,10 +20,15 @@ const ( // project: no collection logic, no BOM, no move. managedTag = "managed-by:kubescape" - // The project property that records which image digest a project's BOM - // came from, so two images cannot overwrite each other's project. - digestPropertyGroup = "kubescape" - digestPropertyName = "image-digest" + // propertyGroup holds the project properties the exporter keeps on the + // image projects it creates. + propertyGroup = "kubescape" + // digestProperty records which image digest a project's BOM came from, so + // two images cannot overwrite each other's project. + digestProperty = "image-digest" + // toolVersionProperty records the node-agent version that built the SBOM + // last uploaded, so an SBOM node-agent rebuilt in place is sent again. + toolVersionProperty = "sbom-tool-version" ) // project is the part of a Dependency-Track project the exporter reads. @@ -278,25 +283,27 @@ type projectProperty struct { PropertyType string `json:"propertyType"` } -// digestOwner returns the image digest recorded on the project with uuid, or -// "" when none is. -func (c *dtClient) digestOwner(ctx context.Context, uuid string) (string, error) { +// properties returns the exporter's properties of the project with uuid, by +// name. +func (c *dtClient) properties(ctx context.Context, uuid string) (map[string]string, error) { var props []projectProperty if _, err := c.do(ctx, http.MethodGet, "/api/v1/project/"+url.PathEscape(uuid)+"/property", nil, &props, http.StatusOK); err != nil { - return "", err + return nil, err } + out := map[string]string{} for _, p := range props { - if p.GroupName == digestPropertyGroup && p.PropertyName == digestPropertyName { - return p.PropertyValue, nil + if p.GroupName == propertyGroup { + out[p.PropertyName] = p.PropertyValue } } - return "", nil + return out, nil } -// setDigestOwner records digest on the project with uuid: PUT creates the -// property, and answers 409 when it exists, which POST then updates. -func (c *dtClient) setDigestOwner(ctx context.Context, uuid, digest string) error { - body := projectProperty{GroupName: digestPropertyGroup, PropertyName: digestPropertyName, PropertyValue: digest, PropertyType: "STRING"} +// setProperty records the exporter's property name on the project with uuid: +// PUT creates the property, and answers 409 when it exists, which POST then +// updates. +func (c *dtClient) setProperty(ctx context.Context, uuid, name, value string) error { + body := projectProperty{GroupName: propertyGroup, PropertyName: name, PropertyValue: value, PropertyType: "STRING"} path := "/api/v1/project/" + url.PathEscape(uuid) + "/property" status, err := c.do(ctx, http.MethodPut, path, body, nil, http.StatusCreated, http.StatusOK, http.StatusConflict) if err != nil || status != http.StatusConflict { diff --git a/sbomexport/dtrack_test.go b/sbomexport/dtrack_test.go index be2f40df..b2834d30 100644 --- a/sbomexport/dtrack_test.go +++ b/sbomexport/dtrack_test.go @@ -94,6 +94,8 @@ func TestDTClientErrorCarriesTheStatusAndTheStartOfTheBody(t *testing.T) { assert.False(t, isAuthError(err)) } +// A Dependency-Track timestamp decodes from epoch milliseconds, RFC 3339, null +// or nothing. func TestEpochMillisDecodesNumbersAndRFC3339(t *testing.T) { tt := []struct { name string diff --git a/sbomexport/exporter.go b/sbomexport/exporter.go index caae6364..9a4013ee 100644 --- a/sbomexport/exporter.go +++ b/sbomexport/exporter.go @@ -27,6 +27,7 @@ import ( "maps" "net/http" "net/url" + "slices" "strings" "sync" "sync/atomic" @@ -59,6 +60,10 @@ const ( requestTimeout = 2 * time.Minute + // objectOverhead is room, on top of MaxSBOMSize, for the rest of an + // SBOMSyft object around its syft document when the object is read. + objectOverhead = 1 << 20 + // moveKeyPrefix marks queue items that move a namespace's projects after // its layout changed; every other item is an image digest. moveKeyPrefix = "namespace/" @@ -123,6 +128,10 @@ type Exporter struct { pendingMoves map[string]*pendingMove // namespace → where its move starts moveSeq uint64 // counts namespace layout changes logged map[string]bool // problems logged since the last resync + // While resync lists, these hold the pods and SBOM digests that events + // changed meanwhile. Their state from the events is newer than the + // lists', so resync keeps it. nil when no resync is listing. + touchedPods, touchedSBOMs map[string]bool stats stats } @@ -186,16 +195,25 @@ func (e *Exporter) ObservePod(eventType watch.EventType, pod *corev1.Pod) { return } key := pod.Namespace + "/" + pod.Name + var ready []string + e.mu.Lock() + if e.touchedPods != nil { + e.touchedPods[key] = true + } switch eventType { case watch.Added, watch.Modified: for _, d := range e.running.setPod(key, pod.Namespace, runningDigests(pod)) { - if m, ok := e.sbom(d); ok && m.ready() { - e.queue.Add(d) + if m, ok := e.sbomByImage[d]; ok && m.ready() { + ready = append(ready, d) } } case watch.Deleted: e.running.removePod(key) } + e.mu.Unlock() + for _, d := range ready { + e.queue.Add(d) + } } // ObserveSBOM records an SBOMSyft event from SBOMWatch. @@ -204,20 +222,21 @@ func (e *Exporter) ObserveSBOM(eventType watch.EventType, obj *spdxv1beta1.SBOMS if !ok { return } + e.mu.Lock() + if e.touchedSBOMs != nil { + e.touchedSBOMs[m.digest] = true + } switch eventType { case watch.Added, watch.Modified: - e.mu.Lock() e.sbomByImage[m.digest] = m - e.mu.Unlock() - if m.ready() { - e.queue.Add(m.digest) - } case watch.Deleted: - e.mu.Lock() if cur, ok := e.sbomByImage[m.digest]; ok && cur.name == m.name && cur.namespace == m.namespace { delete(e.sbomByImage, m.digest) } - e.mu.Unlock() + } + e.mu.Unlock() + if eventType != watch.Deleted && m.ready() { + e.queue.Add(m.digest) } } @@ -332,13 +351,20 @@ func (e *Exporter) work(ctx context.Context) { } // resync rebuilds the running index from one paged list of running pods and -// the SBOM metadata from one list of SBOMSyft objects, then queues every -// running image. If either list fails, nothing changes. +// the SBOM metadata from one list of SBOMSyft objects, keeping what events +// changed while it listed, then queues the pending moves and every running +// image. If either list fails, nothing changes. func (e *Exporter) resync(ctx context.Context) error { logger.L().Info("SBOM export since the last resync", e.stats.takeFields()...) e.mu.Lock() e.logged = map[string]bool{} + e.touchedPods, e.touchedSBOMs = map[string]bool{}, map[string]bool{} e.mu.Unlock() + defer func() { + e.mu.Lock() + e.touchedPods, e.touchedSBOMs = nil, nil + e.mu.Unlock() + }() pods := map[string]podEntry{} err := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) { @@ -361,16 +387,29 @@ func (e *Exporter) resync(ctx context.Context) error { return fmt.Errorf("list SBOMSyft objects: %w", err) } - e.running.replace(pods) byImage := make(map[string]sbomMeta, len(sboms)) for _, m := range sboms { byImage[m.digest] = m } e.mu.Lock() + e.running.replace(pods, e.touchedPods) + for d := range e.touchedSBOMs { + if m, ok := e.sbomByImage[d]; ok { + byImage[d] = m + } else { + delete(byImage, d) + } + } + e.touchedPods, e.touchedSBOMs = nil, nil e.sbomByImage = byImage e.projects = map[projectRef]string{} + moves := slices.Sorted(maps.Keys(e.pendingMoves)) e.mu.Unlock() + // Moves go first, so an image is moved before it is exported again. + for _, ns := range moves { + e.queue.Add(moveKeyPrefix + ns) + } for _, d := range e.running.digests() { e.queue.Add(d) } @@ -449,18 +488,18 @@ func (e *Exporter) process(ctx context.Context, digest string) error { } // exportTo makes p's project hold m's SBOM as its latest version. An image -// whose project imported a BOM no older than its SBOMSyft is only marked -// latest. It never uploads to a project it did not create, nor to one that -// holds the BOM of another image that still runs. Before it uploads, it -// creates a missing project and records m's digest on it, so a second image -// can never take a project whose upload went through but whose owner was not -// recorded. +// whose project imported a BOM no older than its SBOMSyft, built by the same +// node-agent version, is only marked latest. It never uploads to a project it +// did not create, nor to one that holds the BOM of another image that still +// runs. Before it uploads, it creates a missing project and records m's digest +// on it, so a second image can never take a project whose upload went through +// but whose owner was not recorded. func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placement, bom *[]byte) (result, error) { target, err := e.dt.lookup(ctx, p.name, p.version) if err != nil { return 0, err } - owner, parentUUID := "", "" + owner, sentVersion, parentUUID := "", "", "" if target != nil { if !target.managed() { e.logOnce(ctx, "conflict/"+p.name+":"+p.version, "a Dependency-Track project with this name and version exists and was not created by Kubescape, skipping the image", @@ -476,16 +515,21 @@ func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placem } e.logMissing(ctx, missing, ns) } - if owner, err = e.dt.digestOwner(ctx, target.UUID); err != nil { + props, err := e.dt.properties(ctx, target.UUID) + if err != nil { return 0, err } + owner, sentVersion = props[digestProperty], props[toolVersionProperty] if owner != "" && owner != m.digest && len(e.running.namespaces(owner)) > 0 { e.logOnce(ctx, "conflict/"+p.name+":"+p.version, "two running images map to the same Dependency-Track project and version, skipping the second; include the repository in the project name template", helpers.String("project", p.name), helpers.String("version", p.version), helpers.String("image", m.imageTag), helpers.String("namespace", ns), helpers.String("ownerDigest", owner), helpers.String("digest", m.digest)) return resultConflict, nil } - if owner == m.digest && !target.LastBOMImport.Time().Before(m.created) { + // node-agent rebuilds an SBOM in place when its version changes, so + // the creation time alone does not show a new document. + rebuilt := m.toolVersion != "" && m.toolVersion != sentVersion + if owner == m.digest && !rebuilt && !target.LastBOMImport.Time().Before(m.created) { if !target.IsLatest { if err := e.dt.patch(ctx, target.UUID, map[string]any{"isLatest": true}); err != nil { return 0, err @@ -506,7 +550,12 @@ func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placem } if *bom == nil { - doc, err := e.sboms.syftDocument(ctx, m.namespace, m.name) + doc, err := e.sboms.syftDocument(ctx, m.namespace, m.name, e.opts.MaxSBOMSize+objectOverhead) + var tooLarge *tooLargeError + if errors.As(err, &tooLarge) { + e.logTooLarge(ctx, m, tooLarge.read) + return resultTooLarge, nil + } if err != nil { return 0, err } @@ -524,11 +573,21 @@ func (e *Exporter) exportTo(ctx context.Context, m sbomMeta, ns string, p placem } } if owner != m.digest { - if err := e.dt.setDigestOwner(ctx, target.UUID, m.digest); err != nil { + if err := e.dt.setProperty(ctx, target.UUID, digestProperty, m.digest); err != nil { return 0, err } } - return resultSent, e.dt.uploadBOM(ctx, p.name, p.version, *bom) + if err := e.dt.uploadBOM(ctx, p.name, p.version, *bom); err != nil { + return 0, err + } + // Recorded after the upload: when this fails, the next pass sends the SBOM + // again rather than skip a document Dependency-Track never got. + if m.toolVersion != "" && m.toolVersion != sentVersion { + if err := e.dt.setProperty(ctx, target.UUID, toolVersionProperty, m.toolVersion); err != nil { + return 0, err + } + } + return resultSent, nil } // logMissing logs, once per resync, a project the layout names that is @@ -651,7 +710,10 @@ func (e *Exporter) reconcileCollection(ctx context.Context, p *project, logic st // namespace's previous labels and annotations put them to where the current // ones do, so their history in Dependency-Track moves with them. It moves only // projects the exporter created, and never onto a project that exists; the -// images are then exported as usual. A change made while the operator was not +// images are then exported as usual. When only the root changes, it moves the +// namespace's parent collection, also when none of its images can be exported. +// A move whose destination is missing and may not be created stays pending and +// is tried again at every resync. A change made while the operator was not // running is not seen, so those images start new projects. func (e *Exporter) move(ctx context.Context, ns string) error { e.mu.Lock() @@ -661,12 +723,17 @@ func (e *Exporter) move(ctx context.Context, ns string) error { if ok { before, seq = pm.from, pm.seq } + // The collection cache is dropped, so the move and the exports after it + // find, and reconcile, the collections of the new layout. + e.projects = map[projectRef]string{} e.mu.Unlock() if !ok { return nil } now := e.namespaceInfo(ns) digests := e.running.digestsIn(ns) + parents := map[projectRef]bool{} // parents whose root was handled + waiting := false var errs []error for _, d := range digests { m, ok := e.sbom(d) @@ -676,22 +743,34 @@ func (e *Exporter) move(ctx context.Context, ns string) error { repository, tag := splitRef(m.imageTag) from, err1 := e.layout.resolve(before, repository, tag) to, err2 := e.layout.resolve(now, repository, tag) - if err1 != nil || err2 != nil || (from.name == to.name && from.version == to.version && from.parent == to.parent) { + if err1 != nil || err2 != nil { continue } - if err := e.moveProject(ctx, ns, from, to); err != nil { + var done bool + var err error + if from.name == to.name && from.version == to.version && from.parent == to.parent { + if parents[to.parent] { + continue + } + parents[to.parent] = true + done, err = e.moveParent(ctx, ns, from, to) + } else { + done, err = e.moveProject(ctx, ns, from, to) + } + switch { + case err != nil: errs = append(errs, err) + case !done: + waiting = true } } if err := errors.Join(errs...); err != nil { return err } - // A change that came in meanwhile moves on from where this one ended. The - // collection cache is dropped, so the exports below find, and reconcile, - // the collections of the new layout. + // A waiting move keeps the state it started from. Otherwise a change that + // came in meanwhile moves on from where this one ended. e.mu.Lock() - e.projects = map[projectRef]string{} - if cur, ok := e.pendingMoves[ns]; ok { + if cur, ok := e.pendingMoves[ns]; ok && !waiting { if cur.seq == seq { delete(e.pendingMoves, ns) } else { @@ -708,32 +787,70 @@ func (e *Exporter) move(ctx context.Context, ns string) error { // moveProject moves the exporter's project at from to to: its name, version // and parent. When only the parent changes, the project found at to is the // same one, and only its parent changes. A different project at to is left -// alone. -func (e *Exporter) moveProject(ctx context.Context, ns string, from, to placement) error { +// alone. It reports false when to's parent, or its root, is missing and may +// not be created, so the move waits. +func (e *Exporter) moveProject(ctx context.Context, ns string, from, to placement) (bool, error) { p, err := e.dt.lookup(ctx, from.name, from.version) if err != nil || p == nil || !p.managed() { - return err + return true, err } target, err := e.dt.lookup(ctx, to.name, to.version) if err != nil || (target != nil && target.UUID != p.UUID) { - return err + return true, err } parentUUID, err := e.parent(ctx, to) var missing *missingProjectError if errors.As(err, &missing) { e.logMissing(ctx, missing, ns) - return nil + return false, nil } if err != nil { - return err + return false, err + } + if target != nil { + // Only the parent changes. An earlier try of a waiting move may have + // moved this project already; a lookup by name leaves the parent out. + full, err := e.dt.getByUUID(ctx, p.UUID) + if err != nil { + return false, err + } + if full != nil && full.Parent != nil && full.Parent.UUID == parentUUID { + return true, nil + } } if err := e.dt.patch(ctx, p.UUID, map[string]any{"name": to.name, "version": to.version, "parent": map[string]string{"uuid": parentUUID}}); err != nil { - return err + return false, err } e.stats.moved.Add(1) logger.L().Info("moved Dependency-Track project after the namespace's layout changed", helpers.String("namespace", ns), helpers.String("from", from.name+":"+from.version), helpers.String("to", to.name+":"+to.version)) - return nil + return true, nil +} + +// moveParent moves to's parent collection under to's root when only the root +// changed. Exports move it too, but only for images they can export, so a +// namespace whose SBOMs are all too large or not ready would keep its parent +// under the old root. A parent the exporter did not create, or none yet, is +// left alone. It reports false when the root is missing and may not be +// created, so the move waits. +func (e *Exporter) moveParent(ctx context.Context, ns string, from, to placement) (bool, error) { + if from.root == to.root && from.rootCreate == to.rootCreate { + return true, nil + } + parent, err := e.dt.find(ctx, to.parent) + if err != nil || parent == nil || !parent.managed() { + return true, err + } + rootUUID, err := e.collection(ctx, to.root, to.rootCreate, rootLogic, nil) + var missing *missingProjectError + if errors.As(err, &missing) { + e.logMissing(ctx, missing, ns) + return false, nil + } + if err != nil { + return false, err + } + return true, e.reconcileCollection(ctx, parent, parentLogic, func() (string, error) { return rootUUID, nil }) } // logTooLarge logs, once per resync, that the SBOM of m is over MaxSBOMSize. diff --git a/sbomexport/exporter_layout_test.go b/sbomexport/exporter_layout_test.go index 453cbdcb..07094414 100644 --- a/sbomexport/exporter_layout_test.go +++ b/sbomexport/exporter_layout_test.go @@ -68,6 +68,8 @@ func TestExporterCreatesTheParentUnderAnExistingRoot(t *testing.T) { assert.Equal(t, "Payments Team", imageParent) } +// A namespace whose root or parent is missing and may not be created is +// skipped, and nothing is created for it. func TestExporterSkipsANamespaceWhoseProjectIsMissingAndMayNotBeCreated(t *testing.T) { tt := []struct { name string @@ -155,6 +157,18 @@ func TestExporterSkipsAnSBOMLargerThanTheLimit(t *testing.T) { assert.Zero(t, te.store.documentReads()) assert.Equal(t, int64(1), te.stats.tooLarge.Load()) }) + t.Run("by the object it reads, when the annotation is missing", func(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + te.store.tooLarge = true + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.Empty(t, te.dt.takeUploads()) + assert.Equal(t, int64(1), te.stats.tooLarge.Load()) + assert.Equal(t, int64(10<<20+objectOverhead), te.store.lastLimit()) + }) t.Run("by its document, when the annotation is missing", func(t *testing.T) { te := newTestExporterWith(t, func(o *Options) { o.MaxSBOMSize = 1000 }, runningPod("apps", "web", redisImageID)) te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) @@ -305,3 +319,59 @@ func TestExporterKeepsALayoutChangeThatComesInDuringAMove(t *testing.T) { assert.Equal(t, before.UUID, after.UUID) assert.Equal(t, "Team C", parent) } + +// A move whose parent is missing and may not be created waits, and is done at +// a resync once the parent exists. +func TestExporterKeepsAMoveWhoseParentIsMissingUntilItExists(t *testing.T) { + te := newTestExporter(t, namespace("payments", nil), runningPod("payments", "api", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + before, _, _ := te.dt.project("cluster/payments/"+redisProject, "8") + + te.updateNamespace(t, namespace("payments", map[string]string{AnnotationParent: "Team B:current", AnnotationParentCreate: "false"})) + require.Empty(t, te.drain()) + _, parent, _ := te.dt.project("cluster/payments/"+redisProject, "8") + require.Equal(t, "cluster/payments", parent, "Team B does not exist yet") + + te.dt.addProject("Team B", "current", "") + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + after, parent, _ := te.dt.project("cluster/payments/"+redisProject, "8") + assert.Equal(t, before.UUID, after.UUID) + assert.Equal(t, "Team B", parent) + assert.Equal(t, int64(1), te.stats.moved.Load()) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Zero(t, te.stats.moved.Load(), "the move is done, not tried again") +} + +// A root-only change moves the parent also when no image of the namespace +// can be exported, and waits while the new root is missing. +func TestExporterMovesTheParentWhenNoImageCanBeExported(t *testing.T) { + te := newTestExporter(t, namespace("payments", nil), runningPod("payments", "api", redisImageID)) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + big := sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t) + big.Annotations[helpersv1.ResourceSizeMetadataKey] = strconv.Itoa(11 << 20) + te.store.set(big) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + te.updateNamespace(t, namespace("payments", map[string]string{AnnotationRootProject: "Finance:current", AnnotationRootProjectCreate: "false"})) + require.Empty(t, te.drain()) + _, root, _ := te.dt.project("cluster/payments", "current") + require.Equal(t, "cluster", root, "Finance does not exist yet") + + te.dt.addProject("Finance", "current", "") + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + parent, root, _ := te.dt.project("cluster/payments", "current") + assert.Equal(t, "Finance", root) + assert.Equal(t, parentLogic, parent.CollectionLogic) + assert.Empty(t, te.dt.takeUploads()[1:], "the SBOM is still too large") +} diff --git a/sbomexport/exporter_test.go b/sbomexport/exporter_test.go index 58a422e7..c488bdc7 100644 --- a/sbomexport/exporter_test.go +++ b/sbomexport/exporter_test.go @@ -32,27 +32,41 @@ const ( // fakeStore serves SBOMSyft objects from memory; every document is the curl // fixture. type fakeStore struct { - mu sync.Mutex - sboms []*spdxv1beta1.SBOMSyft - gets int + mu sync.Mutex + sboms []*spdxv1beta1.SBOMSyft + gets int + tooLarge bool // answer every document read with a *tooLargeError + limit int64 // the limit of the last document read + onList func() // called after listSBOMs took its snapshot, without the lock } +// listSBOMs returns the metadata of the objects set, then calls onList. func (s *fakeStore) listSBOMs(context.Context) ([]sbomMeta, error) { s.mu.Lock() - defer s.mu.Unlock() var out []sbomMeta for _, obj := range s.sboms { if m, ok := metaOf(obj); ok { out = append(out, m) } } + hook := s.onList + s.mu.Unlock() + if hook != nil { + hook() + } return out, nil } -func (s *fakeStore) syftDocument(context.Context, string, string) ([]byte, error) { +// syftDocument counts the read and returns the curl fixture, or a +// *tooLargeError when tooLarge is set. +func (s *fakeStore) syftDocument(_ context.Context, _, _ string, limit int64) ([]byte, error) { s.mu.Lock() defer s.mu.Unlock() s.gets++ + s.limit = limit + if s.tooLarge { + return nil, &tooLargeError{read: limit + 1} + } return curlSyftJSON, nil } @@ -62,6 +76,13 @@ func (s *fakeStore) set(sboms ...*spdxv1beta1.SBOMSyft) { s.sboms = sboms } +// lastLimit returns the limit of the last document read. +func (s *fakeStore) lastLimit() int64 { + s.mu.Lock() + defer s.mu.Unlock() + return s.limit +} + func (s *fakeStore) documentReads() int { s.mu.Lock() defer s.mu.Unlock() @@ -186,6 +207,7 @@ var defaultLayout = Layout{ Version: "{{ .Tag }}", } +// New refuses options the exporter cannot run with. func TestNewRejectsAnInvalidConfiguration(t *testing.T) { valid := Options{URL: "https://dtrack.example.com", APIKeyFile: "/etc/sbom-export/api-key", Cluster: "cluster", Layout: defaultLayout, MaxSBOMSize: 10 << 20, ResyncInterval: time.Hour} tt := []struct { @@ -331,6 +353,7 @@ func TestExporterResyncKeepsWhatItKnewWhenThePodListFails(t *testing.T) { assert.Equal(t, []string{"apps"}, te.running.namespaces(digestOf(redisImageID))) } +// Start exports the running images and returns once its context ends. func TestStartExportsUntilTheContextEnds(t *testing.T) { te := newTestExporter(t, runningPod("apps", "web", redisImageID)) te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour))) @@ -373,3 +396,88 @@ func TestExporterCountsARefusedAPIKeyAsAFailureAndLogsItOncePerResync(t *testing te.stats.takeFields() assert.False(t, te.stats.authLogged.Load(), "the next resync logs it again") } + +// node-agent rebuilds the SBOM of a running image in place when its own +// version changes: the creation time stays and the tool version changes. +func TestExporterSendsAnSBOMAgainWhenNodeAgentRebuiltIt(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + sbom := sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour)) + sbom.Annotations[helpersv1.ToolVersionMetadataKey] = "v0.3.38" + te.store.set(sbom) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + require.Len(t, te.dt.takeUploads(), 1) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + require.Empty(t, te.dt.takeUploads(), "same node-agent version: unchanged") + + rebuilt := sbom.DeepCopy() + rebuilt.Annotations[helpersv1.ToolVersionMetadataKey] = "v0.3.39" + te.store.set(rebuilt) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Len(t, te.dt.takeUploads(), 1, "rebuilt by another node-agent version") + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Empty(t, te.dt.takeUploads()) + p, _, _ := te.dt.project("cluster/apps/"+redisProject, "8") + assert.Equal(t, "v0.3.39", te.dt.propertyForTest(p.UUID, toolVersionProperty)) +} + +// The tool version is recorded only after the upload, so a failed upload of a +// rebuilt SBOM is tried again instead of being taken as sent. +func TestExporterSendsARebuiltSBOMAgainAfterAFailedUpload(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + sbom := sbomObject(redisImageID, redisTag, helpersv1.Learning, te.clock.t.Add(-time.Hour)) + sbom.Annotations[helpersv1.ToolVersionMetadataKey] = "v0.3.38" + te.store.set(sbom) + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + require.Len(t, te.dt.takeUploads(), 1) + + rebuilt := sbom.DeepCopy() + rebuilt.Annotations[helpersv1.ToolVersionMetadataKey] = "v0.3.39" + te.store.set(rebuilt) + te.dt.mu.Lock() + te.dt.failUploads = 1 + te.dt.mu.Unlock() + require.NoError(t, te.resync(context.Background())) + require.Len(t, te.drain(), 1) + require.Empty(t, te.dt.takeUploads()) + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + assert.Len(t, te.dt.takeUploads(), 1) +} + +// An SBOM that becomes ready while resync lists the SBOMSyft objects is not +// replaced by the older state the list returned. +func TestResyncKeepsAnSBOMThatBecameReadyWhileItListed(t *testing.T) { + te := newTestExporter(t, runningPod("apps", "web", redisImageID)) + created := te.clock.t.Add(-time.Hour) + te.store.set(sbomObject(redisImageID, redisTag, helpersv1.Initializing, created)) + te.store.onList = func() { + te.ObserveSBOM(watch.Modified, sbomObject(redisImageID, redisTag, helpersv1.Learning, created)) + } + + require.NoError(t, te.resync(context.Background())) + require.Empty(t, te.drain()) + + assert.Len(t, te.dt.takeUploads(), 1) +} + +// A pod that starts while resync lists the pods stays running instead of +// starting its grace period because the list missed it. +func TestResyncKeepsAPodThatStartedWhileItListed(t *testing.T) { + te := newTestExporter(t) + te.client.PrependReactor("list", "pods", func(ktesting.Action) (bool, runtime.Object, error) { + te.ObservePod(watch.Added, runningPod("apps", "web", redisImageID)) + return false, nil, nil + }) + + require.NoError(t, te.resync(context.Background())) + + te.clock.t = te.clock.t.Add(2 * time.Hour) + assert.Equal(t, []string{"apps"}, te.running.namespaces(digestOf(redisImageID))) +} diff --git a/sbomexport/fakedtrack_test.go b/sbomexport/fakedtrack_test.go index c883e6bc..b3e73ecd 100644 --- a/sbomexport/fakedtrack_test.go +++ b/sbomexport/fakedtrack_test.go @@ -30,6 +30,7 @@ type fakeDT struct { nextID int fail int // answer the next fail requests with 503 failProperties int // answer the next property writes with 503 + failUploads int // answer the next BOM uploads with 503 onPatch func() // called before a PATCH is applied, without the lock keyFile string // holds testAPIKey } @@ -48,6 +49,7 @@ func newFakeDT(t *testing.T, now func() time.Time) (*fakeDT, *httptest.Server, s return f, srv, keyFile } +// ServeHTTP answers the Dependency-Track API calls dtClient makes. func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPatch { f.mu.Lock() @@ -140,6 +142,9 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.Tags = *req.Tags } writeJSON(w, http.StatusOK, p) + case r.Method == http.MethodPut && path == "/api/v1/bom" && f.failUploads > 0: + f.failUploads-- + http.Error(w, "unavailable", http.StatusServiceUnavailable) case r.Method == http.MethodPut && path == "/api/v1/bom": var req struct { ProjectName, ProjectVersion, BOM string @@ -172,6 +177,8 @@ func (f *fakeDT) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } +// serveProperty lists, creates (PUT) and updates (POST) the properties of +// the project with uuid, like Dependency-Track does. func (f *fakeDT) serveProperty(w http.ResponseWriter, r *http.Request, uuid string) { if f.projects[uuid] == nil { http.Error(w, "The project could not be found.", http.StatusNotFound) @@ -212,6 +219,7 @@ func (f *fakeDT) serveProperty(w http.ResponseWriter, r *http.Request, uuid stri } } +// byNameLocked returns the project name:version, or nil. func (f *fakeDT) byNameLocked(name, version string) *project { for _, p := range f.projects { if p.Name == name && p.Version == version { @@ -221,6 +229,8 @@ func (f *fakeDT) byNameLocked(name, version string) *project { return nil } +// addLocked creates a project under parentUUID, or at the top when it is +// empty, and returns it. func (f *fakeDT) addLocked(name, version, parentUUID string, tags []tag) *project { f.nextID++ p := &project{UUID: fmt.Sprintf("00000000-0000-0000-0000-%012d", f.nextID), Name: name, Version: version, Tags: tags} @@ -262,6 +272,13 @@ func (f *fakeDT) project(name, version string) (project, string, bool) { return *p, parent, true } +// propertyForTest returns the exporter's property name on the project with uuid. +func (f *fakeDT) propertyForTest(uuid, name string) string { + f.mu.Lock() + defer f.mu.Unlock() + return f.properties[uuid][propertyGroup+"/"+name] +} + // digestOwnerForTest returns the image digest recorded on the project with uuid. func (f *fakeDT) digestOwnerForTest(uuid string) (string, error) { f.mu.Lock() @@ -269,7 +286,7 @@ func (f *fakeDT) digestOwnerForTest(uuid string) (string, error) { if f.projects[uuid] == nil { return "", fmt.Errorf("no project %s", uuid) } - return f.properties[uuid][digestPropertyGroup+"/"+digestPropertyName], nil + return f.properties[uuid][propertyGroup+"/"+digestProperty], nil } // count returns how many projects exist. diff --git a/sbomexport/image_test.go b/sbomexport/image_test.go index ca996b4c..7d61216c 100644 --- a/sbomexport/image_test.go +++ b/sbomexport/image_test.go @@ -7,6 +7,7 @@ import ( corev1 "k8s.io/api/core/v1" ) +// TestSplitRef checks how image references split into repository and version. func TestSplitRef(t *testing.T) { tt := []struct { ref string @@ -35,6 +36,7 @@ func TestDigestOf(t *testing.T) { assert.Equal(t, "sha256:abc", digestOf("sha256:abc")) } +// TestRunningDigests checks which containers of a pod count as running an image. func TestRunningDigests(t *testing.T) { running := corev1.ContainerState{Running: &corev1.ContainerStateRunning{}} terminated := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{}} diff --git a/sbomexport/index.go b/sbomexport/index.go index c6560a19..88829638 100644 --- a/sbomexport/index.go +++ b/sbomexport/index.go @@ -70,17 +70,21 @@ func (x *index) removePod(key string) { x.removeLocked(key) } -// replace rebuilds the index from a full list of running pods. An image that -// no longer runs in a namespace starts its grace period there. -func (x *index) replace(pods map[string]podEntry) { +// replace rebuilds the index from a full list of running pods, except for the +// pods in keep, whose recorded state stays. An image that no longer runs in a +// namespace starts its grace period there. +func (x *index) replace(pods map[string]podEntry, keep map[string]bool) { x.mu.Lock() defer x.mu.Unlock() for key := range x.pods { - if _, ok := pods[key]; !ok { + if _, ok := pods[key]; !ok && !keep[key] { x.removeLocked(key) } } for key, p := range pods { + if keep[key] { + continue + } x.removeLocked(key) x.pods[key] = p for _, d := range p.digests { diff --git a/sbomexport/index_test.go b/sbomexport/index_test.go index faacbba0..cd2be303 100644 --- a/sbomexport/index_test.go +++ b/sbomexport/index_test.go @@ -85,7 +85,7 @@ func TestIndexReplaceStartsTheGracePeriodOfWhatStopped(t *testing.T) { x.replace(map[string]podEntry{ "apps/web": {namespace: "apps", digests: []string{"sha256:a"}}, "apps/api": {namespace: "apps", digests: []string{"sha256:c"}}, - }) + }, nil) assert.Equal(t, []string{"sha256:a", "sha256:b", "sha256:c"}, x.digests()) clock.t = clock.t.Add(time.Hour) diff --git a/sbomexport/layout_test.go b/sbomexport/layout_test.go index fd4e3739..c85eb1a9 100644 --- a/sbomexport/layout_test.go +++ b/sbomexport/layout_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" ) +// TestParseRef checks the project references namespace annotations accept. func TestParseRef(t *testing.T) { tt := []struct { in string @@ -90,6 +91,7 @@ func TestLayoutTemplatesSeeTheNamespaceLabels(t *testing.T) { assert.Equal(t, projectRef{name: "prod/payments", version: "current"}, p.parent) } +// Each layout annotation replaces only the setting it names. func TestLayoutAnnotationsOverrideOneSettingEach(t *testing.T) { l, err := newLayout("prod", defaultLayout) require.NoError(t, err) @@ -135,6 +137,7 @@ func TestLayoutAnnotationsOverrideOneSettingEach(t *testing.T) { } } +// An annotation that cannot be used fails resolve, so the namespace is skipped. func TestLayoutRejectsAnnotationsItCannotUse(t *testing.T) { l, err := newLayout("prod", defaultLayout) require.NoError(t, err) diff --git a/sbomexport/store.go b/sbomexport/store.go index 5c048e76..df1eeeb0 100644 --- a/sbomexport/store.go +++ b/sbomexport/store.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "strconv" "time" @@ -24,6 +25,10 @@ type sbomMeta struct { status string created time.Time size int64 // bytes, from the kubescape.io/resource-size annotation; 0 when unknown + // toolVersion is the node-agent version that built the SBOM. node-agent + // rebuilds the SBOM of a running image when its version changes and + // replaces the document in place, so the creation time stays the same. + toolVersion string } // ready reports whether the SBOM is complete: the statuses the operator's @@ -46,13 +51,14 @@ func metaOf(obj *spdxv1beta1.SBOMSyft) (sbomMeta, bool) { } size, _ := strconv.ParseInt(ann[helpersv1.ResourceSizeMetadataKey], 10, 64) return sbomMeta{ - size: size, - namespace: obj.GetNamespace(), - name: obj.GetName(), - digest: digestOf(imageID), - imageTag: imageTag, - status: ann[helpersv1.StatusMetadataKey], - created: obj.GetCreationTimestamp().Time, + size: size, + namespace: obj.GetNamespace(), + name: obj.GetName(), + digest: digestOf(imageID), + imageTag: imageTag, + status: ann[helpersv1.StatusMetadataKey], + created: obj.GetCreationTimestamp().Time, + toolVersion: ann[helpersv1.ToolVersionMetadataKey], }, true } @@ -60,8 +66,19 @@ func metaOf(obj *spdxv1beta1.SBOMSyft) (sbomMeta, bool) { type sbomStore interface { // listSBOMs returns the metadata of every SBOMSyft. listSBOMs(ctx context.Context) ([]sbomMeta, error) - // syftDocument returns the `.spec.syft` of one SBOMSyft as JSON. - syftDocument(ctx context.Context, namespace, name string) ([]byte, error) + // syftDocument returns the `.spec.syft` of one SBOMSyft as JSON. It reads + // at most limit bytes of the object and returns a *tooLargeError past that. + syftDocument(ctx context.Context, namespace, name string, limit int64) ([]byte, error) +} + +// tooLargeError is an SBOMSyft object larger than the limit it was read with. +type tooLargeError struct { + read int64 // bytes read before the limit stopped the read +} + +// Error says how much of the object was read. +func (e *tooLargeError) Error() string { + return fmt.Sprintf("SBOMSyft object is larger than the %d bytes read", e.read) } // storageSBOMs reads SBOMSyft objects from Kubescape's storage. @@ -87,14 +104,24 @@ func (s storageSBOMs) listSBOMs(ctx context.Context) ([]sbomMeta, error) { // client: the document goes to syft's own decoder, and the storage types this // module builds against may lack fields that a newer node-agent writes. The // operator's storage client asks for protobuf, so this request asks for JSON. -func (s storageSBOMs) syftDocument(ctx context.Context, namespace, name string) ([]byte, error) { - raw, err := s.client.SpdxV1beta1().RESTClient().Get(). +// The response is read up to limit, so an object without a size annotation +// cannot make the operator buffer more than that. +func (s storageSBOMs) syftDocument(ctx context.Context, namespace, name string, limit int64) ([]byte, error) { + body, err := s.client.SpdxV1beta1().RESTClient().Get(). Namespace(namespace).Resource("sbomsyfts").Name(name). SetHeader("Accept", "application/json"). - Do(ctx).Raw() + Stream(ctx) if err != nil { return nil, fmt.Errorf("get SBOMSyft %s/%s: %w", namespace, name, err) } + defer func() { _ = body.Close() }() + raw, err := io.ReadAll(io.LimitReader(body, limit+1)) + if err != nil { + return nil, fmt.Errorf("read SBOMSyft %s/%s: %w", namespace, name, err) + } + if int64(len(raw)) > limit { + return nil, &tooLargeError{read: int64(len(raw))} + } var obj struct { Spec struct { Syft json.RawMessage `json:"syft"` diff --git a/sbomexport/store_test.go b/sbomexport/store_test.go index 91256cd3..86602125 100644 --- a/sbomexport/store_test.go +++ b/sbomexport/store_test.go @@ -39,10 +39,29 @@ func TestStorageSBOMsReadsTheSyftDocumentAsJSONFromAProtobufClient(t *testing.T) }) require.NoError(t, err) - doc, err := storageSBOMs{client: client}.syftDocument(context.Background(), "kubescape", "nginx-sbom") + doc, err := storageSBOMs{client: client}.syftDocument(context.Background(), "kubescape", "nginx-sbom", 1<<20) require.NoError(t, err) assert.JSONEq(t, string(curlSyftJSON), string(doc)) _, err = toCycloneDX(doc) assert.NoError(t, err) } + +// An object without a size annotation is read only up to the limit. +func TestStorageSBOMsStopsReadingAnObjectLargerThanTheLimit(t *testing.T) { + big := `{"spec":{"syft":{"artifacts":[` + strings.Repeat(`{"name":"pkg"},`, 200_000) + `{}]}}}` + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(big)) + })) + defer srv.Close() + client, err := kssc.NewForConfig(&rest.Config{Host: srv.URL}) + require.NoError(t, err) + + _, err = storageSBOMs{client: client}.syftDocument(context.Background(), "kubescape", "big-sbom", 1<<20) + + var tooLarge *tooLargeError + require.ErrorAs(t, err, &tooLarge) + assert.Equal(t, int64(1<<20+1), tooLarge.read) + assert.Greater(t, len(big), 1<<20) +} diff --git a/watcher/sbomwatcher.go b/watcher/sbomwatcher.go index f0fc9cea..f91540a7 100644 --- a/watcher/sbomwatcher.go +++ b/watcher/sbomwatcher.go @@ -145,6 +145,8 @@ func (wh *WatchHandler) SBOMWatch(ctx context.Context, workerPool *ants.PoolWith } +// HandleSBOMEvents passes SBOMSyft events to the SBOM observer, if any, and, +// unless image scans are disabled, turns them into vulnerability scan commands. func (wh *WatchHandler) HandleSBOMEvents(eventQueue *CooldownQueue, producedCommands chan<- *apis.Command, errorCh chan<- error) { defer close(errorCh)