diff --git a/Makefile b/Makefile
index 940914eb9..4992bcb8f 100644
--- a/Makefile
+++ b/Makefile
@@ -77,7 +77,7 @@ storage-image:
-t "$(REGISTRY)/$(REPO)/storage:$(TAG)" .
@echo "Built $(REGISTRY)/$(REPO)/storage:$(TAG)"
-WORKER_SRC_DIRS := cmd/worker api internal/messaging internal/handlers
+WORKER_SRC_DIRS := cmd/worker api internal/messaging internal/handlers internal/skippatterns
WORKER_GO_SRCS := $(shell find $(WORKER_SRC_DIRS) -type f -name '*.go')
WORKER_SRCS := $(GO_MOD_SRCS) $(WORKER_GO_SRCS)
.PHONY: worker
diff --git a/api/labels.go b/api/labels.go
index 02e718bb3..9c0c4b481 100644
--- a/api/labels.go
+++ b/api/labels.go
@@ -9,4 +9,6 @@ const (
LabelPartOfValue = "sbomscanner"
LabelWorkloadScanKey = "sbomscanner.kubewarden.io/workloadscan"
LabelWorkloadScanValue = "true"
+ LabelNodeScanKey = "sbomscanner.kubewarden.io/nodescan"
+ LabelNodeScanValue = "true"
)
diff --git a/api/storage/register.go b/api/storage/register.go
index d0588e282..f8f4e330e 100644
--- a/api/storage/register.go
+++ b/api/storage/register.go
@@ -40,6 +40,12 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&v1alpha1.VulnerabilityReport{},
&v1alpha1.VulnerabilityReportList{},
+
+ &v1alpha1.NodeSBOM{},
+ &v1alpha1.NodeSBOMList{},
+
+ &v1alpha1.NodeVulnerabilityReport{},
+ &v1alpha1.NodeVulnerabilityReportList{},
)
return nil
}
diff --git a/api/storage/v1alpha1/node_metadata.go b/api/storage/v1alpha1/node_metadata.go
new file mode 100644
index 000000000..9ca715ab6
--- /dev/null
+++ b/api/storage/v1alpha1/node_metadata.go
@@ -0,0 +1,18 @@
+package v1alpha1
+
+// IndexNodeMetadataName is the field index for the digest of a node.
+const (
+ IndexNodeMetadataName = "nodeMetadata.name"
+)
+
+// NodeMetadata contains the metadata details of a node.
+type NodeMetadata struct {
+ // Name specifies the name of the node.
+ Name string `json:"name" protobuf:"bytes,1,req,name=name"`
+ // Platform specifies the platform of the image. Example "linux/amd64".
+ Platform string `json:"platform" protobuf:"bytes,2,req,name=platform"`
+}
+
+type NodeMetadataAccessor interface {
+ GetNodeMetadata() NodeMetadata
+}
diff --git a/api/storage/v1alpha1/nodesbom_types.go b/api/storage/v1alpha1/nodesbom_types.go
new file mode 100644
index 000000000..9c0479285
--- /dev/null
+++ b/api/storage/v1alpha1/nodesbom_types.go
@@ -0,0 +1,37 @@
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// NodeSBOMList contains a list of Software Bill of Materials for nodes
+type NodeSBOMList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+
+ Items []NodeSBOM `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+// +genclient
+// +genclient:nonNamespaced
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:resource:scope=Cluster
+// +kubebuilder:selectablefield:JSONPath=`.nodeMetadata.name`
+// +kubebuilder:selectablefield:JSONPath=`.nodeMetadata.platform`
+
+// NodeSBOM represents a Software Bill of Materials of a node
+type NodeSBOM struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+
+ NodeMetadata NodeMetadata `json:"nodeMetadata" protobuf:"bytes,2,req,name=nodeMetadata"`
+ // SPDX contains the SPDX document of the SBOM in JSON format
+ SPDX runtime.RawExtension `json:"spdx" protobuf:"bytes,3,req,name=spdx"`
+}
+
+func (s *NodeSBOM) GetNodeMetadata() NodeMetadata {
+ return s.NodeMetadata
+}
diff --git a/api/storage/v1alpha1/nodevulnerabilityreport_types.go b/api/storage/v1alpha1/nodevulnerabilityreport_types.go
new file mode 100644
index 000000000..ac348de3e
--- /dev/null
+++ b/api/storage/v1alpha1/nodevulnerabilityreport_types.go
@@ -0,0 +1,38 @@
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// NodeVulnerabilityReportList contains a list of NodeVulnerabilityReport
+type NodeVulnerabilityReportList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+
+ Items []NodeVulnerabilityReport `json:"items" protobuf:"bytes,2,rep,name=items"`
+}
+
+// +genclient
+// +genclient:nonNamespaced
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+// +kubebuilder:resource:scope=Cluster
+// +kubebuilder:selectablefield:JSONPath=`.nodeMetadata.name`
+// +kubebuilder:selectablefield:JSONPath=`.nodeMetadata.platform`
+
+// NodeVulnerabilityReport is the Schema for the scanresults API
+type NodeVulnerabilityReport struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
+
+ // NodeMetadata contains info about the scanned node
+ NodeMetadata NodeMetadata `json:"nodeMetadata" protobuf:"bytes,2,req,name=nodeMetadata"`
+
+ // Report is the actual vulnerability scan report
+ Report Report `json:"report" protobuf:"bytes,3,req,name=report"`
+}
+
+func (v *NodeVulnerabilityReport) GetNodeMetadata() NodeMetadata {
+ return v.NodeMetadata
+}
diff --git a/api/storage/v1alpha1/register.go b/api/storage/v1alpha1/register.go
index 1fc8348a7..f16b0124f 100644
--- a/api/storage/v1alpha1/register.go
+++ b/api/storage/v1alpha1/register.go
@@ -42,6 +42,10 @@ func AddKnownTypes(scheme *runtime.Scheme) error {
&VulnerabilityReportList{},
&WorkloadScanReport{},
&WorkloadScanReportList{},
+ &NodeSBOM{},
+ &NodeSBOMList{},
+ &NodeVulnerabilityReport{},
+ &NodeVulnerabilityReportList{},
&metav1.GetOptions{},
&metav1.CreateOptions{},
&metav1.UpdateOptions{},
@@ -58,7 +62,10 @@ func AddKnownTypes(scheme *runtime.Scheme) error {
return fmt.Errorf("unable to add field selector conversion function to Image: %w", err)
}
- err = scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("SBOM"), imageMetadataFieldSelectorConversion)
+ err = scheme.AddFieldLabelConversionFunc(
+ SchemeGroupVersion.WithKind("SBOM"),
+ imageMetadataFieldSelectorConversion,
+ )
if err != nil {
return fmt.Errorf("unable to add field selector conversion function to SBOM: %w", err)
}
@@ -71,9 +78,26 @@ func AddKnownTypes(scheme *runtime.Scheme) error {
return fmt.Errorf("unable to add field selector conversion function to VulnerabilityReport: %w", err)
}
+ err = scheme.AddFieldLabelConversionFunc(
+ SchemeGroupVersion.WithKind("NodeSBOM"),
+ nodeMetadataFieldSelectorConversion,
+ )
+ if err != nil {
+ return fmt.Errorf("unable to add field selector conversion function to NodeSBOM: %w", err)
+ }
+
+ err = scheme.AddFieldLabelConversionFunc(
+ SchemeGroupVersion.WithKind("NodeVulnerabilityReport"),
+ nodeMetadataFieldSelectorConversion,
+ )
+ if err != nil {
+ return fmt.Errorf("unable to add field selector conversion function to NodeVulnerabilityReport: %w", err)
+ }
+
return nil
}
+// imageMetadataFieldSelectorConversion allows field selection on the image metadata fields.
func imageMetadataFieldSelectorConversion(label, value string) (string, string, error) {
switch label {
case "metadata.name":
@@ -104,3 +128,27 @@ func imageMetadataFieldSelectorConversion(label, value string) (string, string,
)
}
}
+
+// nodeMetadataFieldSelectorConversion allows field selection on the node metadata fields.
+// This is needed to allow listing NodeSBOMs and NodeVulnerabilityReports by node metadata,
+// since the node name and platform are part of the node metadata and not the top-level resource metadata.
+func nodeMetadataFieldSelectorConversion(label, value string) (string, string, error) {
+ switch label {
+ case "metadata.name":
+ return label, value, nil
+ case "metadata.namespace":
+ return label, value, nil
+ case "nodeMetadata.name":
+ return label, value, nil
+ case "nodeMetadata.platform":
+ return label, value, nil
+ default:
+ return "", "", fmt.Errorf(
+ "%q is not a known field selector: only %q, %q, %q",
+ label,
+ "metadata.name",
+ "metadata.namespace",
+ "nodeMetadata.*",
+ )
+ }
+}
diff --git a/api/storage/v1alpha1/zz_generated.deepcopy.go b/api/storage/v1alpha1/zz_generated.deepcopy.go
index c8d9d6bc9..f7adb133f 100644
--- a/api/storage/v1alpha1/zz_generated.deepcopy.go
+++ b/api/storage/v1alpha1/zz_generated.deepcopy.go
@@ -232,6 +232,144 @@ func (in *ImageWorkloadScanReports) DeepCopy() *ImageWorkloadScanReports {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeMetadata) DeepCopyInto(out *NodeMetadata) {
+ *out = *in
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeMetadata.
+func (in *NodeMetadata) DeepCopy() *NodeMetadata {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeMetadata)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeSBOM) DeepCopyInto(out *NodeSBOM) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ out.NodeMetadata = in.NodeMetadata
+ in.SPDX.DeepCopyInto(&out.SPDX)
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeSBOM.
+func (in *NodeSBOM) DeepCopy() *NodeSBOM {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeSBOM)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeSBOM) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeSBOMList) DeepCopyInto(out *NodeSBOMList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]NodeSBOM, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeSBOMList.
+func (in *NodeSBOMList) DeepCopy() *NodeSBOMList {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeSBOMList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeSBOMList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeVulnerabilityReport) DeepCopyInto(out *NodeVulnerabilityReport) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ out.NodeMetadata = in.NodeMetadata
+ in.Report.DeepCopyInto(&out.Report)
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeVulnerabilityReport.
+func (in *NodeVulnerabilityReport) DeepCopy() *NodeVulnerabilityReport {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeVulnerabilityReport)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeVulnerabilityReport) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeVulnerabilityReportList) DeepCopyInto(out *NodeVulnerabilityReportList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]NodeVulnerabilityReport, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeVulnerabilityReportList.
+func (in *NodeVulnerabilityReportList) DeepCopy() *NodeVulnerabilityReportList {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeVulnerabilityReportList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeVulnerabilityReportList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Report) DeepCopyInto(out *Report) {
*out = *in
diff --git a/api/storage/v1alpha1/zz_generated.model_name.go b/api/storage/v1alpha1/zz_generated.model_name.go
index 9771bf3d5..193bc574a 100644
--- a/api/storage/v1alpha1/zz_generated.model_name.go
+++ b/api/storage/v1alpha1/zz_generated.model_name.go
@@ -60,6 +60,31 @@ func (in ImageWorkloadScanReports) OpenAPIModelName() string {
return "storage.sbomscanner.kubewarden.io.v1alpha1.ImageWorkloadScanReports"
}
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in NodeMetadata) OpenAPIModelName() string {
+ return "storage.sbomscanner.kubewarden.io.v1alpha1.NodeMetadata"
+}
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in NodeSBOM) OpenAPIModelName() string {
+ return "storage.sbomscanner.kubewarden.io.v1alpha1.NodeSBOM"
+}
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in NodeSBOMList) OpenAPIModelName() string {
+ return "storage.sbomscanner.kubewarden.io.v1alpha1.NodeSBOMList"
+}
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in NodeVulnerabilityReport) OpenAPIModelName() string {
+ return "storage.sbomscanner.kubewarden.io.v1alpha1.NodeVulnerabilityReport"
+}
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in NodeVulnerabilityReportList) OpenAPIModelName() string {
+ return "storage.sbomscanner.kubewarden.io.v1alpha1.NodeVulnerabilityReportList"
+}
+
// OpenAPIModelName returns the OpenAPI model name for this type.
func (in Report) OpenAPIModelName() string {
return "storage.sbomscanner.kubewarden.io.v1alpha1.Report"
diff --git a/api/v1alpha1/nodescanconfiguration_types.go b/api/v1alpha1/nodescanconfiguration_types.go
new file mode 100644
index 000000000..1d38026b0
--- /dev/null
+++ b/api/v1alpha1/nodescanconfiguration_types.go
@@ -0,0 +1,66 @@
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+ // NodeScanConfigurationName is the only allowed name for the singleton NodeScanConfiguration resource.
+ NodeScanConfigurationName = "default"
+
+ // AnnotationForceNodeScanKey triggers an immediate rescan of all matching nodes,
+ // bypassing the scan interval timer. The annotation is removed after the scan jobs are created.
+ AnnotationForceNodeScanKey = "force-node-scan"
+)
+
+// NodeScanConfigurationSpec defines the desired configuration for node scanning.
+type NodeScanConfigurationSpec struct {
+ // NodeSelector filters which nodes are scanned.
+ // If not specified, all the nodes are scanned.
+ // +optional
+ NodeSelector *metav1.LabelSelector `json:"nodeSelector,omitempty"`
+
+ // ScanInterval is the interval at which nodes are scanned.
+ // +optional
+ ScanInterval *metav1.Duration `json:"scanInterval,omitempty"`
+
+ // SkipPatterns specifies gitignore-style patterns for directories and files to skip during node scanning.
+ // Patterns ending with "/" are treated as directories.
+ // All other patterns are treated as files.
+ // Glob patterns like "**/vendor/" or "*.min.js" are supported.
+ // +optional
+ SkipPatterns []string `json:"skipPatterns,omitempty"`
+
+ // Platforms allows to specify the list of platforms to scan.
+ // If not set, all nodes are scanned regardless of their platform.
+ // +optional
+ Platforms []Platform `json:"platforms,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:scope=Cluster
+// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default'",message="NodeScanConfiguration name must be 'default'"
+
+// NodeScanConfiguration is the Schema for the nodescanconfigurations API.
+// This is a singleton resource - only one instance named "default" is allowed.
+type NodeScanConfiguration struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec NodeScanConfigurationSpec `json:"spec,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// NodeScanConfigurationList contains a list of NodeScanConfiguration.
+type NodeScanConfigurationList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+
+ Items []NodeScanConfiguration `json:"items"`
+}
+
+func init() {
+ register(&NodeScanConfiguration{}, &NodeScanConfigurationList{})
+}
diff --git a/api/v1alpha1/nodescanjob_types.go b/api/v1alpha1/nodescanjob_types.go
new file mode 100644
index 000000000..94a1056da
--- /dev/null
+++ b/api/v1alpha1/nodescanjob_types.go
@@ -0,0 +1,319 @@
+package v1alpha1
+
+import (
+ "time"
+
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const LabelNodeScanJobUIDKey = "sbomscanner.kubewarden.io/nodescanjob-uid"
+
+const (
+ // IndexNodeScanJobSpecNodeName is the field index for the node name of a NodeScanJob.
+ IndexNodeScanJobSpecNodeName = "spec.nodeName"
+)
+
+// RegistryAnnotation stores a snapshot of the Registry targeted by the NodeScanJob.
+const (
+ // AnnotationNodeScanJobCreationTimestampKey is used to store the creation timestamp of the NodeScanJob.
+ AnnotationNodeScanJobCreationTimestampKey = "sbomscanner.kubewarden.io/creation-timestamp"
+ // AnnotationNodeScanJobTriggerKey is used to identify the source of the NodeScanJob trigger.
+ AnnotationNodeScanJobTriggerKey = "sbomscanner.kubewarden.io/trigger"
+)
+
+const (
+ ConditionNodeScanJobTypeScheduled = "Scheduled"
+ ConditionNodeScanJobTypeInProgress = "InProgress"
+ ConditionNodeScanJobTypeComplete = "Complete"
+ ConditionNodeScanJobTypeFailed = "Failed"
+)
+
+const (
+ ReasonNodeScanJobInProgress = "NodeScanInProgress"
+ ReasonNodeScanJobScanned = "NodeScanned"
+ ReasonNodeScanJobConfigurationMissing = "NodeScanConfigurationMissing"
+ ReasonNodeScanJobNotMatching = "NodeNotMatching"
+ ReasonNodeScanJobPending = "NodeScanPending"
+ ReasonNodeScanJobScheduled = "NodeScanScheduled"
+ ReasonNodeScanJobComplete = "NodeScanComplete"
+ ReasonNodeScanJobFailed = "NodeScanFailed"
+)
+
+// NodeScanJobSpec defines the desired state of NodeScanJob.
+type NodeScanJobSpec struct {
+ // NodeName specifies the name of the node to be scanned.
+ NodeName string `json:"nodeName"`
+}
+
+// NodeScanJobStatus defines the observed state of NodeScanJob.
+type NodeScanJobStatus struct {
+ // Conditions represent the latest available observations of ScanJob state
+ // +optional
+ Conditions []metav1.Condition `json:"conditions,omitempty"`
+
+ // StartTime is when the job started processing.
+ // +optional
+ StartTime *metav1.Time `json:"startTime,omitempty"`
+
+ // CompletionTime is when the job completed or failed.
+ // +optional
+ CompletionTime *metav1.Time `json:"completionTime,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:resource:scope=Cluster
+// +kubebuilder:selectablefield:JSONPath=`.spec.nodeName`
+// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.status=='True')].type",description="Current status"
+// +kubebuilder:printcolumn:name="Reason",type="string",JSONPath=".status.conditions[?(@.status=='True')].reason",description="Status reason"
+// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
+
+// NodeScanJob is the Schema for the nodescanjobs API.
+type NodeScanJob struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ObjectMeta `json:"metadata,omitempty"`
+
+ Spec NodeScanJobSpec `json:"spec,omitempty"`
+ Status NodeScanJobStatus `json:"status,omitempty"`
+}
+
+// GetCreationTimestampFromAnnotation returns the creation timestamp of the NodeScanJob.
+// It first attempts to parse the timestamp from the CreationTimestampAnnotation.
+// If the annotation is missing or malformed, it falls back to the Kubernetes object's
+// standard metadata.CreationTimestamp.
+func (s *NodeScanJob) GetCreationTimestampFromAnnotation() time.Time {
+ if timestampStr, ok := s.Annotations[AnnotationNodeScanJobCreationTimestampKey]; ok {
+ if timestamp, err := time.Parse(time.RFC3339Nano, timestampStr); err == nil {
+ return timestamp
+ }
+ }
+
+ return s.CreationTimestamp.Time
+}
+
+// InitializeConditions initializes status fields and conditions.
+func (s *NodeScanJob) InitializeConditions() {
+ s.Status.Conditions = []metav1.Condition{}
+
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeScheduled,
+ Status: metav1.ConditionUnknown,
+ Reason: ReasonNodeScanJobPending,
+ Message: messagePending,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeInProgress,
+ Status: metav1.ConditionUnknown,
+ Reason: ReasonNodeScanJobPending,
+ Message: messagePending,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeComplete,
+ Status: metav1.ConditionUnknown,
+ Reason: ReasonNodeScanJobPending,
+ Message: messagePending,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeFailed,
+ Status: metav1.ConditionUnknown,
+ Reason: ReasonNodeScanJobPending,
+ Message: messagePending,
+ ObservedGeneration: s.Generation,
+ })
+}
+
+// MarkScheduled marks the job as scheduled.
+func (s *NodeScanJob) MarkScheduled(reason, message string) {
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeScheduled,
+ Status: metav1.ConditionTrue,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeInProgress,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobScheduled,
+ Message: messageScheduled,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeComplete,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobScheduled,
+ Message: messageScheduled,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeFailed,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobScheduled,
+ Message: messageScheduled,
+ ObservedGeneration: s.Generation,
+ })
+}
+
+// MarkInProgress marks the job as in progress.
+func (s *NodeScanJob) MarkInProgress(reason, message string) {
+ now := metav1.Now()
+ s.Status.StartTime = &now
+
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeScheduled,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobInProgress,
+ Message: messageInProgress,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeInProgress,
+ Status: metav1.ConditionTrue,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeComplete,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobInProgress,
+ Message: messageInProgress,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeFailed,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobInProgress,
+ Message: messageInProgress,
+ ObservedGeneration: s.Generation,
+ })
+}
+
+// MarkComplete marks the job as complete.
+func (s *NodeScanJob) MarkComplete(reason, message string) {
+ now := metav1.Now()
+ s.Status.CompletionTime = &now
+
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeScheduled,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobComplete,
+ Message: messageCompleted,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeInProgress,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobComplete,
+ Message: messageCompleted,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeComplete,
+ Status: metav1.ConditionTrue,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeFailed,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobComplete,
+ Message: messageCompleted,
+ ObservedGeneration: s.Generation,
+ })
+}
+
+// MarkFailed marks the job as failed.
+func (s *NodeScanJob) MarkFailed(reason, message string) {
+ now := metav1.Now()
+ s.Status.CompletionTime = &now
+
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeScheduled,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobFailed,
+ Message: messageFailed,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeInProgress,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobFailed,
+ Message: messageFailed,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeComplete,
+ Status: metav1.ConditionFalse,
+ Reason: ReasonNodeScanJobFailed,
+ Message: messageFailed,
+ ObservedGeneration: s.Generation,
+ })
+ meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
+ Type: ConditionNodeScanJobTypeFailed,
+ Status: metav1.ConditionTrue,
+ Reason: reason,
+ Message: message,
+ ObservedGeneration: s.Generation,
+ })
+}
+
+// IsPending returns true if the job is not in any other state.
+func (s *NodeScanJob) IsPending() bool {
+ return !s.IsScheduled() && !s.IsInProgress() && !s.IsComplete() && !s.IsFailed()
+}
+
+// IsScheduled returns true if the job is scheduled.
+func (s *NodeScanJob) IsScheduled() bool {
+ scheduledCond := meta.FindStatusCondition(s.Status.Conditions, ConditionNodeScanJobTypeScheduled)
+ if scheduledCond == nil {
+ return false
+ }
+ return scheduledCond.Status == metav1.ConditionTrue
+}
+
+// IsInProgress returns true if the job is currently in progress.
+func (s *NodeScanJob) IsInProgress() bool {
+ inProgressCond := meta.FindStatusCondition(s.Status.Conditions, ConditionNodeScanJobTypeInProgress)
+ if inProgressCond == nil {
+ return false
+ }
+ return inProgressCond.Status == metav1.ConditionTrue
+}
+
+// IsComplete returns true if the job has completed successfully.
+func (s *NodeScanJob) IsComplete() bool {
+ completeCond := meta.FindStatusCondition(s.Status.Conditions, ConditionNodeScanJobTypeComplete)
+ if completeCond == nil {
+ return false
+ }
+ return completeCond.Status == metav1.ConditionTrue
+}
+
+// IsFailed returns true if the job has failed.
+func (s *NodeScanJob) IsFailed() bool {
+ failedCond := meta.FindStatusCondition(s.Status.Conditions, ConditionNodeScanJobTypeFailed)
+ if failedCond == nil {
+ return false
+ }
+ return failedCond.Status == metav1.ConditionTrue
+}
+
+// +kubebuilder:object:root=true
+
+// NodeScanJobList contains a list of NodeScanJob.
+type NodeScanJobList struct {
+ metav1.TypeMeta `json:",inline"`
+ metav1.ListMeta `json:"metadata,omitempty"`
+
+ Items []NodeScanJob `json:"items"`
+}
+
+func init() {
+ register(&NodeScanJob{}, &NodeScanJobList{})
+}
diff --git a/api/v1alpha1/scanjob_types.go b/api/v1alpha1/scanjob_types.go
index ce140a16b..77a7314cb 100644
--- a/api/v1alpha1/scanjob_types.go
+++ b/api/v1alpha1/scanjob_types.go
@@ -50,27 +50,27 @@ type ScanJobRepository struct {
}
const (
- ConditionTypeScheduled = "Scheduled"
- ConditionTypeInProgress = "InProgress"
- ConditionTypeComplete = "Complete"
- ConditionTypeFailed = "Failed"
+ ConditionScanJobTypeScheduled = "Scheduled"
+ ConditionScanJobTypeInProgress = "InProgress"
+ ConditionScanJobTypeComplete = "Complete"
+ ConditionScanJobTypeFailed = "Failed"
)
const (
- ReasonPending = "Pending"
- ReasonScheduled = "Scheduled"
- ReasonInProgress = "InProgress"
- ReasonCatalogCreationInProgress = "CatalogCreationInProgress"
- ReasonSBOMGenerationInProgress = "SBOMGenerationInProgress"
- ReasonImageScanInProgress = "ImageScanInProgress"
- ReasonComplete = "Complete"
- ReasonFailed = "Failed"
- ReasonNoImagesToScan = "NoImagesToScan"
- ReasonAllImagesScanned = "AllImagesScanned"
- ReasonRegistryNotFound = "RegistryNotFound"
- ReasonRepositoryNotFound = "RepositoryNotFound"
- ReasonMatchConditionNotFound = "MatchConditionNotFound"
- ReasonInternalError = "InternalError"
+ ReasonScanJobPending = "Pending"
+ ReasonScanJobScheduled = "Scheduled"
+ ReasonScanJobInProgress = "InProgress"
+ ReasonScanJobCatalogCreationInProgress = "CatalogCreationInProgress"
+ ReasonScanJobSBOMGenerationInProgress = "SBOMGenerationInProgress"
+ ReasonScanJobImageScanInProgress = "ImageScanInProgress"
+ ReasonScanJobComplete = "Complete"
+ ReasonScanJobFailed = "Failed"
+ ReasonScanJobNoImagesToScan = "NoImagesToScan"
+ ReasonScanJobAllImagesScanned = "AllImagesScanned"
+ ReasonScanJobRegistryNotFound = "RegistryNotFound"
+ ReasonScanJobRepositoryNotFound = "RepositoryNotFound"
+ ReasonScanJobMatchConditionNotFound = "MatchConditionNotFound"
+ ReasonScanJobInternalError = "InternalError"
)
const (
@@ -140,30 +140,30 @@ func (s *ScanJob) InitializeConditions() {
s.Status.Conditions = []metav1.Condition{}
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeScheduled,
+ Type: ConditionScanJobTypeScheduled,
Status: metav1.ConditionUnknown,
- Reason: ReasonPending,
+ Reason: ReasonScanJobPending,
Message: messagePending,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeInProgress,
+ Type: ConditionScanJobTypeInProgress,
Status: metav1.ConditionUnknown,
- Reason: ReasonPending,
+ Reason: ReasonScanJobPending,
Message: messagePending,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeComplete,
+ Type: ConditionScanJobTypeComplete,
Status: metav1.ConditionUnknown,
- Reason: ReasonPending,
+ Reason: ReasonScanJobPending,
Message: messagePending,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeFailed,
+ Type: ConditionScanJobTypeFailed,
Status: metav1.ConditionUnknown,
- Reason: ReasonPending,
+ Reason: ReasonScanJobPending,
Message: messagePending,
ObservedGeneration: s.Generation,
})
@@ -172,30 +172,30 @@ func (s *ScanJob) InitializeConditions() {
// MarkScheduled marks the job as scheduled.
func (s *ScanJob) MarkScheduled(reason, message string) {
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeScheduled,
+ Type: ConditionScanJobTypeScheduled,
Status: metav1.ConditionTrue,
Reason: reason,
Message: message,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeInProgress,
+ Type: ConditionScanJobTypeInProgress,
Status: metav1.ConditionFalse,
- Reason: ReasonScheduled,
+ Reason: ReasonScanJobScheduled,
Message: messageScheduled,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeComplete,
+ Type: ConditionScanJobTypeComplete,
Status: metav1.ConditionFalse,
- Reason: ReasonScheduled,
+ Reason: ReasonScanJobScheduled,
Message: messageScheduled,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeFailed,
+ Type: ConditionScanJobTypeFailed,
Status: metav1.ConditionFalse,
- Reason: ReasonScheduled,
+ Reason: ReasonScanJobScheduled,
Message: messageScheduled,
ObservedGeneration: s.Generation,
})
@@ -207,30 +207,30 @@ func (s *ScanJob) MarkInProgress(reason, message string) {
s.Status.StartTime = &now
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeScheduled,
+ Type: ConditionScanJobTypeScheduled,
Status: metav1.ConditionFalse,
- Reason: ReasonInProgress,
+ Reason: ReasonScanJobInProgress,
Message: messageInProgress,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeInProgress,
+ Type: ConditionScanJobTypeInProgress,
Status: metav1.ConditionTrue,
Reason: reason,
Message: message,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeComplete,
+ Type: ConditionScanJobTypeComplete,
Status: metav1.ConditionFalse,
- Reason: ReasonInProgress,
+ Reason: ReasonScanJobInProgress,
Message: messageInProgress,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeFailed,
+ Type: ConditionScanJobTypeFailed,
Status: metav1.ConditionFalse,
- Reason: ReasonInProgress,
+ Reason: ReasonScanJobInProgress,
Message: messageInProgress,
ObservedGeneration: s.Generation,
})
@@ -242,30 +242,30 @@ func (s *ScanJob) MarkComplete(reason, message string) {
s.Status.CompletionTime = &now
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeScheduled,
+ Type: ConditionScanJobTypeScheduled,
Status: metav1.ConditionFalse,
- Reason: ReasonComplete,
+ Reason: ReasonScanJobComplete,
Message: messageCompleted,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeInProgress,
+ Type: ConditionScanJobTypeInProgress,
Status: metav1.ConditionFalse,
- Reason: ReasonComplete,
+ Reason: ReasonScanJobComplete,
Message: messageCompleted,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeComplete,
+ Type: ConditionScanJobTypeComplete,
Status: metav1.ConditionTrue,
Reason: reason,
Message: message,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeFailed,
+ Type: ConditionScanJobTypeFailed,
Status: metav1.ConditionFalse,
- Reason: ReasonComplete,
+ Reason: ReasonScanJobComplete,
Message: messageCompleted,
ObservedGeneration: s.Generation,
})
@@ -277,28 +277,28 @@ func (s *ScanJob) MarkFailed(reason, message string) {
s.Status.CompletionTime = &now
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeScheduled,
+ Type: ConditionScanJobTypeScheduled,
Status: metav1.ConditionFalse,
- Reason: ReasonFailed,
+ Reason: ReasonScanJobFailed,
Message: messageFailed,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeInProgress,
+ Type: ConditionScanJobTypeInProgress,
Status: metav1.ConditionFalse,
- Reason: ReasonFailed,
+ Reason: ReasonScanJobFailed,
Message: messageFailed,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeComplete,
+ Type: ConditionScanJobTypeComplete,
Status: metav1.ConditionFalse,
- Reason: ReasonFailed,
+ Reason: ReasonScanJobFailed,
Message: messageFailed,
ObservedGeneration: s.Generation,
})
meta.SetStatusCondition(&s.Status.Conditions, metav1.Condition{
- Type: ConditionTypeFailed,
+ Type: ConditionScanJobTypeFailed,
Status: metav1.ConditionTrue,
Reason: reason,
Message: message,
@@ -313,7 +313,7 @@ func (s *ScanJob) IsPending() bool {
// IsScheduled returns true if the job is scheduled.
func (s *ScanJob) IsScheduled() bool {
- scheduledCond := meta.FindStatusCondition(s.Status.Conditions, ConditionTypeScheduled)
+ scheduledCond := meta.FindStatusCondition(s.Status.Conditions, ConditionScanJobTypeScheduled)
if scheduledCond == nil {
return false
}
@@ -322,7 +322,7 @@ func (s *ScanJob) IsScheduled() bool {
// IsInProgress returns true if the job is currently in progress.
func (s *ScanJob) IsInProgress() bool {
- inProgressCond := meta.FindStatusCondition(s.Status.Conditions, ConditionTypeInProgress)
+ inProgressCond := meta.FindStatusCondition(s.Status.Conditions, ConditionScanJobTypeInProgress)
if inProgressCond == nil {
return false
}
@@ -331,7 +331,7 @@ func (s *ScanJob) IsInProgress() bool {
// IsComplete returns true if the job has completed successfully.
func (s *ScanJob) IsComplete() bool {
- completeCond := meta.FindStatusCondition(s.Status.Conditions, ConditionTypeComplete)
+ completeCond := meta.FindStatusCondition(s.Status.Conditions, ConditionScanJobTypeComplete)
if completeCond == nil {
return false
}
@@ -340,7 +340,7 @@ func (s *ScanJob) IsComplete() bool {
// IsFailed returns true if the job has failed.
func (s *ScanJob) IsFailed() bool {
- failedCond := meta.FindStatusCondition(s.Status.Conditions, ConditionTypeFailed)
+ failedCond := meta.FindStatusCondition(s.Status.Conditions, ConditionScanJobTypeFailed)
if failedCond == nil {
return false
}
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 8cf57eb96..09725c2cc 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -31,6 +31,203 @@ func (in *MatchCondition) DeepCopy() *MatchCondition {
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeScanConfiguration) DeepCopyInto(out *NodeScanConfiguration) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ in.Spec.DeepCopyInto(&out.Spec)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeScanConfiguration.
+func (in *NodeScanConfiguration) DeepCopy() *NodeScanConfiguration {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeScanConfiguration)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeScanConfiguration) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeScanConfigurationList) DeepCopyInto(out *NodeScanConfigurationList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]NodeScanConfiguration, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeScanConfigurationList.
+func (in *NodeScanConfigurationList) DeepCopy() *NodeScanConfigurationList {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeScanConfigurationList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeScanConfigurationList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeScanConfigurationSpec) DeepCopyInto(out *NodeScanConfigurationSpec) {
+ *out = *in
+ if in.NodeSelector != nil {
+ in, out := &in.NodeSelector, &out.NodeSelector
+ *out = new(v1.LabelSelector)
+ (*in).DeepCopyInto(*out)
+ }
+ if in.ScanInterval != nil {
+ in, out := &in.ScanInterval, &out.ScanInterval
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.SkipPatterns != nil {
+ in, out := &in.SkipPatterns, &out.SkipPatterns
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Platforms != nil {
+ in, out := &in.Platforms, &out.Platforms
+ *out = make([]Platform, len(*in))
+ copy(*out, *in)
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeScanConfigurationSpec.
+func (in *NodeScanConfigurationSpec) DeepCopy() *NodeScanConfigurationSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeScanConfigurationSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeScanJob) DeepCopyInto(out *NodeScanJob) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+ out.Spec = in.Spec
+ in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeScanJob.
+func (in *NodeScanJob) DeepCopy() *NodeScanJob {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeScanJob)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeScanJob) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeScanJobList) DeepCopyInto(out *NodeScanJobList) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ in.ListMeta.DeepCopyInto(&out.ListMeta)
+ if in.Items != nil {
+ in, out := &in.Items, &out.Items
+ *out = make([]NodeScanJob, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeScanJobList.
+func (in *NodeScanJobList) DeepCopy() *NodeScanJobList {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeScanJobList)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *NodeScanJobList) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeScanJobSpec) DeepCopyInto(out *NodeScanJobSpec) {
+ *out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeScanJobSpec.
+func (in *NodeScanJobSpec) DeepCopy() *NodeScanJobSpec {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeScanJobSpec)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *NodeScanJobStatus) DeepCopyInto(out *NodeScanJobStatus) {
+ *out = *in
+ if in.Conditions != nil {
+ in, out := &in.Conditions, &out.Conditions
+ *out = make([]v1.Condition, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ if in.StartTime != nil {
+ in, out := &in.StartTime, &out.StartTime
+ *out = (*in).DeepCopy()
+ }
+ if in.CompletionTime != nil {
+ in, out := &in.CompletionTime, &out.CompletionTime
+ *out = (*in).DeepCopy()
+ }
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeScanJobStatus.
+func (in *NodeScanJobStatus) DeepCopy() *NodeScanJobStatus {
+ if in == nil {
+ return nil
+ }
+ out := new(NodeScanJobStatus)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Platform) DeepCopyInto(out *Platform) {
*out = *in
diff --git a/charts/sbomscanner/templates/controller/role.yaml b/charts/sbomscanner/templates/controller/role.yaml
index a23583f49..7d6d16d34 100644
--- a/charts/sbomscanner/templates/controller/role.yaml
+++ b/charts/sbomscanner/templates/controller/role.yaml
@@ -11,6 +11,7 @@ rules:
- ""
resources:
- namespaces
+ - nodes
- pods
verbs:
- get
@@ -35,24 +36,36 @@ rules:
- apiGroups:
- sbomscanner.kubewarden.io
resources:
- - registries
- - scanjobs
+ - nodescanconfigurations
verbs:
- - create
- - delete
- get
- list
- - patch
- update
- watch
- apiGroups:
- sbomscanner.kubewarden.io
resources:
+ - nodescanconfigurations/status
+ - nodescanjobs/status
- scanjobs/status
verbs:
- get
- patch
- update
+- apiGroups:
+ - sbomscanner.kubewarden.io
+ resources:
+ - nodescanjobs
+ - registries
+ - scanjobs
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
- apiGroups:
- sbomscanner.kubewarden.io
resources:
@@ -70,6 +83,14 @@ rules:
- list
- patch
- watch
+- apiGroups:
+ - storage.sbomscanner.kubewarden.io
+ resources:
+ - nodesboms
+ verbs:
+ - delete
+ - list
+ - watch
- apiGroups:
- storage.sbomscanner.kubewarden.io
resources:
diff --git a/charts/sbomscanner/templates/controller/webhooks.yaml b/charts/sbomscanner/templates/controller/webhooks.yaml
index f9edcd33b..bb4a8dbda 100644
--- a/charts/sbomscanner/templates/controller/webhooks.yaml
+++ b/charts/sbomscanner/templates/controller/webhooks.yaml
@@ -126,3 +126,46 @@ webhooks:
resources:
- workloadscanconfigurations
sideEffects: None
+ - admissionReviewVersions:
+ - v1
+ - v1beta1
+ clientConfig:
+ service:
+ name: {{ include "sbomscanner.fullname" . }}-controller-webhook
+ namespace: {{ .Release.Namespace }}
+ path: /validate-sbomscanner-kubewarden-io-v1alpha1-nodescanconfiguration
+ failurePolicy: Fail
+ name: vnodescanconfiguration.sbomscanner.kubewarden.io
+ rules:
+ - apiGroups:
+ - sbomscanner.kubewarden.io
+ apiVersions:
+ - v1alpha1
+ operations:
+ - CREATE
+ - UPDATE
+ - DELETE
+ resources:
+ - nodescanconfigurations
+ sideEffects: None
+ - admissionReviewVersions:
+ - v1
+ - v1beta1
+ clientConfig:
+ service:
+ name: {{ include "sbomscanner.fullname" . }}-controller-webhook
+ namespace: {{ .Release.Namespace }}
+ path: /validate-sbomscanner-kubewarden-io-v1alpha1-nodescanjob
+ failurePolicy: Fail
+ name: vnodescanjob.sbomscanner.kubewarden.io
+ rules:
+ - apiGroups:
+ - sbomscanner.kubewarden.io
+ apiVersions:
+ - v1alpha1
+ operations:
+ - CREATE
+ - UPDATE
+ resources:
+ - nodescanjobs
+ sideEffects: None
diff --git a/charts/sbomscanner/templates/crd/sbomscanner.kubewarden.io_nodescanconfigurations.yaml b/charts/sbomscanner/templates/crd/sbomscanner.kubewarden.io_nodescanconfigurations.yaml
new file mode 100644
index 000000000..712c6ccb3
--- /dev/null
+++ b/charts/sbomscanner/templates/crd/sbomscanner.kubewarden.io_nodescanconfigurations.yaml
@@ -0,0 +1,141 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ helm.sh/resource-policy: keep
+ controller-gen.kubebuilder.io/version: v0.16.5
+ name: nodescanconfigurations.sbomscanner.kubewarden.io
+spec:
+ group: sbomscanner.kubewarden.io
+ names:
+ kind: NodeScanConfiguration
+ listKind: NodeScanConfigurationList
+ plural: nodescanconfigurations
+ singular: nodescanconfiguration
+ scope: Cluster
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: |-
+ NodeScanConfiguration is the Schema for the nodescanconfigurations API.
+ This is a singleton resource - only one instance named "default" is allowed.
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: NodeScanConfigurationSpec defines the desired configuration
+ for node scanning.
+ properties:
+ nodeSelector:
+ description: |-
+ NodeSelector filters which nodes are scanned.
+ If not specified, all the nodes are scanned.
+ properties:
+ matchExpressions:
+ description: matchExpressions is a list of label selector requirements.
+ The requirements are ANDed.
+ items:
+ description: |-
+ A label selector requirement is a selector that contains values, a key, and an operator that
+ relates the key and values.
+ properties:
+ key:
+ description: key is the label key that the selector applies
+ to.
+ type: string
+ operator:
+ description: |-
+ operator represents a key's relationship to a set of values.
+ Valid operators are In, NotIn, Exists and DoesNotExist.
+ type: string
+ values:
+ description: |-
+ values is an array of string values. If the operator is In or NotIn,
+ the values array must be non-empty. If the operator is Exists or DoesNotExist,
+ the values array must be empty. This array is replaced during a strategic
+ merge patch.
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ description: |-
+ matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
+ map is equivalent to an element of matchExpressions, whose key field is "key", the
+ operator is "In", and the values array contains only "value". The requirements are ANDed.
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ platforms:
+ description: |-
+ Platforms allows to specify the list of platforms to scan.
+ If not set, all nodes are scanned regardless of their platform.
+ items:
+ description: Platform describes the platform which the image in
+ the manifest runs on.
+ properties:
+ arch:
+ description: |-
+ Architecture field specifies the CPU architecture, for example
+ `amd64` or `ppc64le`.
+ type: string
+ os:
+ description: OS specifies the operating system, for example
+ `linux` or `windows`.
+ type: string
+ variant:
+ description: |-
+ Variant is an optional field specifying a variant of the CPU, for
+ example `v7` to specify ARMv7 when architecture is `arm`.
+ type: string
+ required:
+ - arch
+ - os
+ type: object
+ type: array
+ scanInterval:
+ description: ScanInterval is the interval at which nodes are scanned.
+ type: string
+ skipPatterns:
+ description: |-
+ SkipPatterns specifies gitignore-style patterns for directories and files to skip during node scanning.
+ Patterns ending with "/" are treated as directories.
+ All other patterns are treated as files.
+ Glob patterns like "**/vendor/" or "*.min.js" are supported.
+ items:
+ type: string
+ type: array
+ type: object
+ type: object
+ x-kubernetes-validations:
+ - message: NodeScanConfiguration name must be 'default'
+ rule: self.metadata.name == 'default'
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/charts/sbomscanner/templates/crd/sbomscanner.kubewarden.io_nodescanjobs.yaml b/charts/sbomscanner/templates/crd/sbomscanner.kubewarden.io_nodescanjobs.yaml
new file mode 100644
index 000000000..cd137e96d
--- /dev/null
+++ b/charts/sbomscanner/templates/crd/sbomscanner.kubewarden.io_nodescanjobs.yaml
@@ -0,0 +1,137 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ helm.sh/resource-policy: keep
+ controller-gen.kubebuilder.io/version: v0.16.5
+ name: nodescanjobs.sbomscanner.kubewarden.io
+spec:
+ group: sbomscanner.kubewarden.io
+ names:
+ kind: NodeScanJob
+ listKind: NodeScanJobList
+ plural: nodescanjobs
+ singular: nodescanjob
+ scope: Cluster
+ versions:
+ - additionalPrinterColumns:
+ - description: Current status
+ jsonPath: .status.conditions[?(@.status=='True')].type
+ name: Status
+ type: string
+ - description: Status reason
+ jsonPath: .status.conditions[?(@.status=='True')].reason
+ name: Reason
+ type: string
+ - jsonPath: .metadata.creationTimestamp
+ name: Age
+ type: date
+ name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: NodeScanJob is the Schema for the nodescanjobs API.
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ spec:
+ description: NodeScanJobSpec defines the desired state of NodeScanJob.
+ properties:
+ nodeName:
+ description: NodeName specifies the name of the node to be scanned.
+ type: string
+ required:
+ - nodeName
+ type: object
+ status:
+ description: NodeScanJobStatus defines the observed state of NodeScanJob.
+ properties:
+ completionTime:
+ description: CompletionTime is when the job completed or failed.
+ format: date-time
+ type: string
+ conditions:
+ description: Conditions represent the latest available observations
+ of ScanJob state
+ items:
+ description: Condition contains details for one aspect of the current
+ state of this API Resource.
+ properties:
+ lastTransitionTime:
+ description: |-
+ lastTransitionTime is the last time the condition transitioned from one status to another.
+ This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
+ format: date-time
+ type: string
+ message:
+ description: |-
+ message is a human readable message indicating details about the transition.
+ This may be an empty string.
+ maxLength: 32768
+ type: string
+ observedGeneration:
+ description: |-
+ observedGeneration represents the .metadata.generation that the condition was set based upon.
+ For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
+ with respect to the current state of the instance.
+ format: int64
+ minimum: 0
+ type: integer
+ reason:
+ description: |-
+ reason contains a programmatic identifier indicating the reason for the condition's last transition.
+ Producers of specific condition types may define expected values and meanings for this field,
+ and whether the values are considered a guaranteed API.
+ The value should be a CamelCase string.
+ This field may not be empty.
+ maxLength: 1024
+ minLength: 1
+ pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
+ type: string
+ status:
+ description: status of the condition, one of True, False, Unknown.
+ enum:
+ - "True"
+ - "False"
+ - Unknown
+ type: string
+ type:
+ description: type of condition in CamelCase or in foo.example.com/CamelCase.
+ maxLength: 316
+ pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
+ type: string
+ required:
+ - lastTransitionTime
+ - message
+ - reason
+ - status
+ - type
+ type: object
+ type: array
+ startTime:
+ description: StartTime is when the job started processing.
+ format: date-time
+ type: string
+ type: object
+ type: object
+ selectableFields:
+ - jsonPath: .spec.nodeName
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/charts/sbomscanner/templates/worker/daemonset.yaml b/charts/sbomscanner/templates/worker/daemonset.yaml
new file mode 100644
index 000000000..38a24eecb
--- /dev/null
+++ b/charts/sbomscanner/templates/worker/daemonset.yaml
@@ -0,0 +1,109 @@
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+ name: {{ include "sbomscanner.fullname" . }}-worker-node
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{ include "sbomscanner.labels" .| nindent 4 }}
+ app.kubernetes.io/component: worker-node
+spec:
+ selector:
+ matchLabels:
+ {{ include "sbomscanner.selectorLabels" .| nindent 6 }}
+ app.kubernetes.io/component: worker-node
+ template:
+ metadata:
+ labels:
+ {{ include "sbomscanner.labels" .| nindent 8 }}
+ app.kubernetes.io/component: worker-node
+ spec:
+ serviceAccountName: {{ include "sbomscanner.fullname" . }}-worker-node
+ initContainers:
+ - name: init
+ image: '{{ template "system_default_registry" . }}{{ .Values.worker.image.repository }}:{{ .Values.worker.image.tag }}'
+ imagePullPolicy: {{ .Values.worker.image.pullPolicy }}
+ securityContext:
+ {{ include "sbomscanner.securityContext" . | nindent 12 }}
+ args:
+ - --init
+ - -nats-url
+ - {{ .Release.Name }}-nats.{{ .Release.Namespace }}.svc.cluster.local:4222
+ {{- if .Values.worker.logLevel }}
+ - -log-level={{ .Values.worker.logLevel }}
+ {{- end }}
+ volumeMounts:
+ - mountPath: "/nats/tls"
+ name: nats-tls
+ readOnly: true
+ containers:
+ - name: worker
+ image: '{{ template "system_default_registry" . }}{{ .Values.worker.image.repository }}:{{ .Values.worker.image.tag }}'
+ env:
+ - name: NODE_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: spec.nodeName
+ imagePullPolicy: {{ .Values.worker.image.pullPolicy }}
+ securityContext:
+ readOnlyRootFilesystem: true
+ runAsNonRoot: false
+ runAsUser: 0
+ seccompProfile:
+ type: RuntimeDefault
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - "ALL"
+ add:
+ - "DAC_READ_SEARCH"
+ args:
+ - -nats-url
+ - {{ .Release.Name }}-nats.{{ .Release.Namespace }}.svc.cluster.local:4222
+ {{- if .Values.worker.trivyDBRepository }}
+ - -trivy-db-repository={{ .Values.worker.trivyDBRepository | quote }}
+ {{- end }}
+ {{- if .Values.worker.trivyJavaDBRepository }}
+ - -trivy-java-db-repository={{ .Values.worker.trivyJavaDBRepository | quote }}
+ {{- end }}
+ - -installation-namespace={{ .Release.Namespace }}
+ {{- if .Values.worker.logLevel }}
+ - -log-level={{ .Values.worker.logLevel }}
+ {{- end }}
+ - -mode=node
+ - -node-name=$(NODE_NAME)
+ {{- if and .Values.worker .Values.worker.resources }}
+ resources:
+{{ toYaml .Values.worker.resources | indent 12 }}
+ {{- end }}
+ livenessProbe:
+ httpGet:
+ path: /livez
+ port: 8081
+ readinessProbe:
+ httpGet:
+ path: /readyz
+ port: 8081
+ volumeMounts:
+ - name: host-root
+ mountPath: /host
+ readOnly: true
+ mountPropagation: None
+ - mountPath: /var/run/worker
+ name: run-volume
+ - mountPath: /tmp
+ name: tmp-dir
+ - mountPath: "/nats/tls"
+ name: nats-tls
+ readOnly: true
+ volumes:
+ - name: host-root
+ hostPath:
+ path: /
+ type: Directory
+ - name: run-volume
+ emptyDir: {}
+ - name: tmp-dir
+ emptyDir: {}
+ - name: nats-tls
+ secret:
+ secretName: {{ include "sbomscanner.fullname" . }}-nats-worker-client-tls
diff --git a/charts/sbomscanner/templates/worker/deployment.yaml b/charts/sbomscanner/templates/worker/deployment.yaml
index 09359f1b2..f4ce4942e 100644
--- a/charts/sbomscanner/templates/worker/deployment.yaml
+++ b/charts/sbomscanner/templates/worker/deployment.yaml
@@ -55,6 +55,7 @@ spec:
{{- if .Values.worker.logLevel }}
- -log-level={{ .Values.worker.logLevel }}
{{- end }}
+ - -mode=registry
{{- if and .Values.worker .Values.worker.resources }}
resources:
{{ toYaml .Values.worker.resources | indent 12 }}
diff --git a/charts/sbomscanner/templates/worker/node-role.yaml b/charts/sbomscanner/templates/worker/node-role.yaml
new file mode 100644
index 000000000..0bf9f21bc
--- /dev/null
+++ b/charts/sbomscanner/templates/worker/node-role.yaml
@@ -0,0 +1,47 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: {{ include "sbomscanner.fullname" . }}-worker-node
+ labels:
+ {{ include "sbomscanner.labels" .| nindent 4 }}
+ app.kubernetes.io/component: worker-node
+rules:
+ - apiGroups:
+ - sbomscanner.kubewarden.io
+ resources:
+ - vexhubs
+ verbs:
+ - get
+ - list
+ - watch
+ - apiGroups:
+ - sbomscanner.kubewarden.io
+ resources:
+ - nodescanjobs
+ - nodescanjobs/status
+ - nodescanconfigurations
+ verbs:
+ - get
+ - list
+ - watch
+ - patch
+ - update
+ - apiGroups:
+ - storage.sbomscanner.kubewarden.io
+ resources:
+ - nodesboms
+ - nodevulnerabilityreports
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - patch
+ - update
+ - watch
+ - apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - get
diff --git a/charts/sbomscanner/templates/worker/node-rolebinding.yaml b/charts/sbomscanner/templates/worker/node-rolebinding.yaml
new file mode 100644
index 000000000..c550ecf1f
--- /dev/null
+++ b/charts/sbomscanner/templates/worker/node-rolebinding.yaml
@@ -0,0 +1,15 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: {{ include "sbomscanner.fullname" . }}-worker-node
+ labels:
+ {{ include "sbomscanner.labels" .| nindent 4 }}
+ app.kubernetes.io/component: worker-node
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: {{ include "sbomscanner.fullname" . }}-worker-node
+subjects:
+ - kind: ServiceAccount
+ name: {{ include "sbomscanner.fullname" . }}-worker-node
+ namespace: {{ .Release.Namespace }}
diff --git a/charts/sbomscanner/templates/worker/node-serviceaccount.yaml b/charts/sbomscanner/templates/worker/node-serviceaccount.yaml
new file mode 100644
index 000000000..c54bd5edb
--- /dev/null
+++ b/charts/sbomscanner/templates/worker/node-serviceaccount.yaml
@@ -0,0 +1,8 @@
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+ name: {{ include "sbomscanner.fullname" . }}-worker-node
+ namespace: {{ .Release.Namespace }}
+ labels:
+ {{ include "sbomscanner.labels" .| nindent 4 }}
+ app.kubernetes.io/component: worker-node
diff --git a/charts/sbomscanner/templates/worker/role.yaml b/charts/sbomscanner/templates/worker/registry-role.yaml
similarity index 100%
rename from charts/sbomscanner/templates/worker/role.yaml
rename to charts/sbomscanner/templates/worker/registry-role.yaml
diff --git a/charts/sbomscanner/templates/worker/rolebinding.yaml b/charts/sbomscanner/templates/worker/registry-rolebinding.yaml
similarity index 100%
rename from charts/sbomscanner/templates/worker/rolebinding.yaml
rename to charts/sbomscanner/templates/worker/registry-rolebinding.yaml
diff --git a/charts/sbomscanner/templates/worker/serviceaccount.yaml b/charts/sbomscanner/templates/worker/registry-serviceaccount.yaml
similarity index 100%
rename from charts/sbomscanner/templates/worker/serviceaccount.yaml
rename to charts/sbomscanner/templates/worker/registry-serviceaccount.yaml
diff --git a/cmd/controller/main.go b/cmd/controller/main.go
index b8fdd221e..565804136 100644
--- a/cmd/controller/main.go
+++ b/cmd/controller/main.go
@@ -74,6 +74,9 @@ type Config struct {
Init bool
LogLevel string
WorkloadScan bool
+ NodeScan bool
+ NodeScanImage string
+ NodeScanNamespace string
}
func parseFlags() Config {
@@ -99,6 +102,7 @@ func parseFlags() Config {
flag.BoolVar(&cfg.Init, "init", false, "Run initialization tasks and exit.")
flag.StringVar(&cfg.LogLevel, "log-level", slog.LevelInfo.String(), "Log level")
flag.BoolVar(&cfg.WorkloadScan, "workloadscan", true, "Enable workload scan controllers.")
+ flag.BoolVar(&cfg.NodeScan, "nodescan", true, "Enable node scan controllers.")
flag.Parse()
return cfg
@@ -346,6 +350,42 @@ func main() {
}
}
+ if cfg.NodeScan {
+ if err = (&controller.NodeScanRunner{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create runner", "runner", "NodeScanRunner")
+ os.Exit(1)
+ }
+
+ if err = (&controller.NodeScanReconciler{
+ Client: mgr.GetClient(),
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "NodeScan")
+ os.Exit(1)
+ }
+
+ if err = (&controller.NodeScanJobReconciler{
+ Client: mgr.GetClient(),
+ Scheme: mgr.GetScheme(),
+ Publisher: publisher,
+ }).SetupWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create controller", "controller", "NodeScanJob")
+ os.Exit(1)
+ }
+
+ if err = webhookv1alpha1.SetupNodeScanConfigurationWebhookWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create webhook", "webhook", "NodeScanConfiguration")
+ os.Exit(1)
+ }
+
+ if err = webhookv1alpha1.SetupNodeScanJobWebhookWithManager(mgr); err != nil {
+ setupLog.Error(err, "unable to create webhook", "webhook", "NodeScanJob")
+ os.Exit(1)
+ }
+ }
+
if err = webhookv1alpha1.SetupRegistryWebhookWithManager(mgr, cfg.ServiceAccountNamespace, cfg.ServiceAccountName); err != nil {
setupLog.Error(err, "unable to create webhook", "webhook", "Registry")
os.Exit(1)
diff --git a/cmd/worker/main.go b/cmd/worker/main.go
index 270d5aa81..d66fdcc66 100644
--- a/cmd/worker/main.go
+++ b/cmd/worker/main.go
@@ -25,6 +25,12 @@ import (
k8sscheme "k8s.io/client-go/kubernetes/scheme"
)
+const (
+ targetDir = "/host"
+ nodeMode = "node"
+ registryMode = "registry"
+)
+
func main() {
var natsURL string
var natsCertFile string
@@ -36,6 +42,8 @@ func main() {
var installationNamespace string
var init bool
var logLevel string
+ var mode string
+ var nodeName string
flag.StringVar(&natsURL, "nats-url", "localhost:4222", "The URL of the NATS server.")
flag.StringVar(&natsCertFile, "nats-cert-file", "/nats/tls/tls.crt", "The path to the NATS client certificate.")
@@ -47,6 +55,8 @@ func main() {
flag.StringVar(&installationNamespace, "installation-namespace", "sbomscanner", "The namespace where sbomscanner is installed.")
flag.BoolVar(&init, "init", false, "Run initialization tasks and exit.")
flag.StringVar(&logLevel, "log-level", slog.LevelInfo.String(), "Log level.")
+ flag.StringVar(&mode, "mode", "registry", "Mode of operation ('registry' or 'node').")
+ flag.StringVar(&nodeName, "node-name", "", "The name of the node (required if mode is 'node').")
flag.Parse()
slogLevel, err := cmdutil.ParseLogLevel(logLevel)
@@ -97,6 +107,11 @@ func main() {
os.Exit(0)
}
+ if mode == nodeMode && nodeName == "" {
+ logger.Error("Node name must be provided when mode is 'node'")
+ os.Exit(1)
+ }
+
nc, err := nats.Connect(natsURL,
natsOpts...,
)
@@ -133,19 +148,40 @@ func main() {
return registry.NewClient(transport, logger)
}
- registry := messaging.HandlerRegistry{
- handlers.CreateCatalogSubject: handlers.NewCreateCatalogHandler(registryClientFactory, k8sClient, scheme, publisher, installationNamespace, logger),
- handlers.GenerateSBOMSubject: handlers.NewGenerateSBOMHandler(k8sClient, scheme, runDir, trivyJavaDBRepository, publisher, installationNamespace, logger),
- handlers.ScanSBOMSubject: handlers.NewScanSBOMHandler(k8sClient, scheme, runDir, trivyDBRepository, trivyJavaDBRepository, logger),
+ var scanMode messaging.HandlerScan
+ durableName := "worker"
+ switch mode {
+ case registryMode:
+ scanMode = messaging.HandlerScan{
+ handlers.CreateCatalogSubject: handlers.NewCreateCatalogHandler(registryClientFactory, k8sClient, scheme, publisher, installationNamespace, logger),
+ handlers.GenerateSBOMSubject: handlers.NewGenerateSBOMHandler(k8sClient, scheme, runDir, trivyJavaDBRepository, publisher, installationNamespace, logger),
+ handlers.ScanSBOMSubject: handlers.NewScanSBOMHandler(k8sClient, scheme, runDir, trivyDBRepository, trivyJavaDBRepository, logger),
+ }
+ case nodeMode:
+ scanMode = messaging.HandlerScan{
+ handlers.GenerateNodeSBOMSubject + "." + nodeName: handlers.NewGenerateNodeSBOMHandler(k8sClient, scheme, runDir, targetDir, trivyJavaDBRepository, publisher, installationNamespace, logger),
+ handlers.ScanNodeSBOMSubject + "." + nodeName: handlers.NewScanNodeSBOMHandler(k8sClient, scheme, runDir, trivyDBRepository, trivyJavaDBRepository, logger),
+ }
+ durableName = "worker-node-" + nodeName
+ default:
+ logger.Error("Invalid scanning mode", "mode", mode)
+ os.Exit(1)
+ }
+
+ var failureHandler messaging.FailureHandler
+ switch mode {
+ case nodeMode:
+ failureHandler = handlers.NewNodeScanJobFailureHandler(k8sClient, logger)
+ default:
+ failureHandler = handlers.NewScanJobFailureHandler(k8sClient, logger)
}
- failureHandler := handlers.NewScanJobFailureHandler(k8sClient, logger)
retryConfig := &messaging.RetryConfig{
BaseDelay: 5 * time.Second,
Jitter: 0.2,
MaxAttempts: 5,
}
- subscriber, err := messaging.NewNatsSubscriber(ctx, nc, "worker", registry, failureHandler, retryConfig, logger)
+ subscriber, err := messaging.NewNatsSubscriber(ctx, nc, durableName, scanMode, failureHandler, retryConfig, logger)
if err != nil {
logger.Error("Error creating NATS subscriber", "error", err)
os.Exit(1)
diff --git a/docs/crds/CRD-docs-for-docs-repo.adoc b/docs/crds/CRD-docs-for-docs-repo.adoc
index ea1afd775..e6a6b917c 100644
--- a/docs/crds/CRD-docs-for-docs-repo.adoc
+++ b/docs/crds/CRD-docs-for-docs-repo.adoc
@@ -15,6 +15,10 @@
Package v1alpha1 contains API Schema definitions for the SBOMscanner v1alpha1 API group.
.Resource Types
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfiguration[$$NodeScanConfiguration$$]
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfigurationlist[$$NodeScanConfigurationList$$]
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjob[$$NodeScanJob$$]
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjoblist[$$NodeScanJobList$$]
- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-registry[$$Registry$$]
- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-registrylist[$$RegistryList$$]
- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-scanjob[$$ScanJob$$]
@@ -69,6 +73,182 @@ MatchOperator defines how multiple match conditions are combined.
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfiguration"]
+==== NodeScanConfiguration
+
+
+
+NodeScanConfiguration is the Schema for the nodescanconfigurations API.
+This is a singleton resource - only one instance named "default" is allowed.
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfigurationlist[$$NodeScanConfigurationList$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`apiVersion`* __string__ | `sbomscanner.kubewarden.io/v1alpha1` | |
+| *`kind`* __string__ | `NodeScanConfiguration` | |
+| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
+ | |
+| *`spec`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfigurationspec[$$NodeScanConfigurationSpec$$]__ | | |
+|===
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfigurationlist"]
+==== NodeScanConfigurationList
+
+
+
+NodeScanConfigurationList contains a list of NodeScanConfiguration.
+
+
+
+
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`apiVersion`* __string__ | `sbomscanner.kubewarden.io/v1alpha1` | |
+| *`kind`* __string__ | `NodeScanConfigurationList` | |
+| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#listmeta-v1-meta[$$ListMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
+ | |
+| *`items`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfiguration[$$NodeScanConfiguration$$] array__ | | |
+|===
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfigurationspec"]
+==== NodeScanConfigurationSpec
+
+
+
+NodeScanConfigurationSpec defines the desired configuration for node scanning.
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfiguration[$$NodeScanConfiguration$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`nodeSelector`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#labelselector-v1-meta[$$LabelSelector$$]__ | NodeSelector filters which nodes are scanned. +
+If not specified, all the nodes are scanned. + | | Optional: \{} +
+
+| *`scanInterval`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#duration-v1-meta[$$Duration$$]__ | ScanInterval is the interval at which nodes are scanned. + | | Optional: \{} +
+
+| *`skipPatterns`* __string array__ | SkipPatterns specifies gitignore-style patterns for directories and files to skip during node scanning. +
+Patterns ending with "/" are treated as directories. +
+All other patterns are treated as files. +
+Glob patterns like "**/vendor/" or "*.min.js" are supported. + | | Optional: \{} +
+
+| *`platforms`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-platform[$$Platform$$] array__ | Platforms allows to specify the list of platforms to scan. +
+If not set, all nodes are scanned regardless of their platform. + | | Optional: \{} +
+
+|===
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjob"]
+==== NodeScanJob
+
+
+
+NodeScanJob is the Schema for the nodescanjobs API.
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjoblist[$$NodeScanJobList$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`apiVersion`* __string__ | `sbomscanner.kubewarden.io/v1alpha1` | |
+| *`kind`* __string__ | `NodeScanJob` | |
+| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
+ | |
+| *`spec`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjobspec[$$NodeScanJobSpec$$]__ | | |
+| *`status`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjobstatus[$$NodeScanJobStatus$$]__ | | |
+|===
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjoblist"]
+==== NodeScanJobList
+
+
+
+NodeScanJobList contains a list of NodeScanJob.
+
+
+
+
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`apiVersion`* __string__ | `sbomscanner.kubewarden.io/v1alpha1` | |
+| *`kind`* __string__ | `NodeScanJobList` | |
+| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#listmeta-v1-meta[$$ListMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
+ | |
+| *`items`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjob[$$NodeScanJob$$] array__ | | |
+|===
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjobspec"]
+==== NodeScanJobSpec
+
+
+
+NodeScanJobSpec defines the desired state of NodeScanJob.
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjob[$$NodeScanJob$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`nodeName`* __string__ | NodeName specifies the name of the node to be scanned. + | |
+|===
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjobstatus"]
+==== NodeScanJobStatus
+
+
+
+NodeScanJobStatus defines the observed state of NodeScanJob.
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanjob[$$NodeScanJob$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#condition-v1-meta[$$Condition$$] array__ | Conditions represent the latest available observations of ScanJob state + | | Optional: \{} +
+
+| *`startTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#time-v1-meta[$$Time$$]__ | StartTime is when the job started processing. + | | Optional: \{} +
+
+| *`completionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#time-v1-meta[$$Time$$]__ | CompletionTime is when the job completed or failed. + | | Optional: \{} +
+
+|===
+
+
[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-platform"]
==== Platform
@@ -80,6 +260,7 @@ Platform describes the platform which the image in the manifest runs on.
.Appears In:
****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-nodescanconfigurationspec[$$NodeScanConfigurationSpec$$]
- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-registryspec[$$RegistrySpec$$]
- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-v1alpha1-workloadscanconfigurationspec[$$WorkloadScanConfigurationSpec$$]
****
@@ -798,6 +979,83 @@ ImageWorkloadScanReports identifies a workload that references this image
|===
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodemetadata"]
+==== NodeMetadata
+
+
+
+NodeMetadata contains the metadata details of a node.
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodesbom[$$NodeSBOM$$]
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodevulnerabilityreport[$$NodeVulnerabilityReport$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`name`* __string__ | Name specifies the name of the node. + | |
+| *`platform`* __string__ | Platform specifies the platform of the image. Example "linux/amd64". + | |
+|===
+
+
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodesbom"]
+==== NodeSBOM
+
+
+
+NodeSBOM represents a Software Bill of Materials of a node
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodesbomlist[$$NodeSBOMList$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
+ | |
+| *`nodeMetadata`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodemetadata[$$NodeMetadata$$]__ | | |
+| *`spdx`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#rawextension-runtime-pkg[$$RawExtension$$]__ | SPDX contains the SPDX document of the SBOM in JSON format + | |
+|===
+
+
+
+
+[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodevulnerabilityreport"]
+==== NodeVulnerabilityReport
+
+
+
+NodeVulnerabilityReport is the Schema for the scanresults API
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodevulnerabilityreportlist[$$NodeVulnerabilityReportList$$]
+****
+
+[cols="20a,50a,15a,15a", options="header"]
+|===
+| Field | Description | Default | Validation
+| *`metadata`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta[$$ObjectMeta$$]__ | Refer to Kubernetes API documentation for fields of `metadata`.
+ | |
+| *`nodeMetadata`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodemetadata[$$NodeMetadata$$]__ | NodeMetadata contains info about the scanned node + | |
+| *`report`* __xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-report[$$Report$$]__ | Report is the actual vulnerability scan report + | |
+|===
+
+
+
+
[id="{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-report"]
==== Report
@@ -809,6 +1067,7 @@ Report contains metadata about the scanned image and a list of vulnerability res
.Appears In:
****
+- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-nodevulnerabilityreport[$$NodeVulnerabilityReport$$]
- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-vulnerabilityreport[$$VulnerabilityReport$$]
- xref:{anchor_prefix}-github-com-kubewarden-sbomscanner-api-storage-v1alpha1-workloadscanvulnerabilityreport[$$WorkloadScanVulnerabilityReport$$]
****
diff --git a/docs/crds/CRD-docs-for-docs-repo.md b/docs/crds/CRD-docs-for-docs-repo.md
index dc68eaf01..989eb2e69 100644
--- a/docs/crds/CRD-docs-for-docs-repo.md
+++ b/docs/crds/CRD-docs-for-docs-repo.md
@@ -10,6 +10,10 @@
Package v1alpha1 contains API Schema definitions for the SBOMscanner v1alpha1 API group.
### Resource Types
+- [NodeScanConfiguration](#nodescanconfiguration)
+- [NodeScanConfigurationList](#nodescanconfigurationlist)
+- [NodeScanJob](#nodescanjob)
+- [NodeScanJobList](#nodescanjoblist)
- [Registry](#registry)
- [RegistryList](#registrylist)
- [ScanJob](#scanjob)
@@ -57,6 +61,135 @@ _Appears in:_
| `Or` | MatchOperatorOr requires at least one condition to pass.
|
+#### NodeScanConfiguration
+
+
+
+NodeScanConfiguration is the Schema for the nodescanconfigurations API.
+This is a singleton resource - only one instance named "default" is allowed.
+
+
+
+_Appears in:_
+- [NodeScanConfigurationList](#nodescanconfigurationlist)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `sbomscanner.kubewarden.io/v1alpha1` | | |
+| `kind` _string_ | `NodeScanConfiguration` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[NodeScanConfigurationSpec](#nodescanconfigurationspec)_ | | | |
+
+
+#### NodeScanConfigurationList
+
+
+
+NodeScanConfigurationList contains a list of NodeScanConfiguration.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `sbomscanner.kubewarden.io/v1alpha1` | | |
+| `kind` _string_ | `NodeScanConfigurationList` | | |
+| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `items` _[NodeScanConfiguration](#nodescanconfiguration) array_ | | | |
+
+
+#### NodeScanConfigurationSpec
+
+
+
+NodeScanConfigurationSpec defines the desired configuration for node scanning.
+
+
+
+_Appears in:_
+- [NodeScanConfiguration](#nodescanconfiguration)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `nodeSelector` _[LabelSelector](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#labelselector-v1-meta)_ | NodeSelector filters which nodes are scanned.
If not specified, all the nodes are scanned. | | Optional: \{\}
|
+| `scanInterval` _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#duration-v1-meta)_ | ScanInterval is the interval at which nodes are scanned. | | Optional: \{\}
|
+| `skipPatterns` _string array_ | SkipPatterns specifies gitignore-style patterns for directories and files to skip during node scanning.
Patterns ending with "/" are treated as directories.
All other patterns are treated as files.
Glob patterns like "**/vendor/" or "*.min.js" are supported. | | Optional: \{\}
|
+| `platforms` _[Platform](#platform) array_ | Platforms allows to specify the list of platforms to scan.
If not set, all nodes are scanned regardless of their platform. | | Optional: \{\}
|
+
+
+#### NodeScanJob
+
+
+
+NodeScanJob is the Schema for the nodescanjobs API.
+
+
+
+_Appears in:_
+- [NodeScanJobList](#nodescanjoblist)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `sbomscanner.kubewarden.io/v1alpha1` | | |
+| `kind` _string_ | `NodeScanJob` | | |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `spec` _[NodeScanJobSpec](#nodescanjobspec)_ | | | |
+| `status` _[NodeScanJobStatus](#nodescanjobstatus)_ | | | |
+
+
+#### NodeScanJobList
+
+
+
+NodeScanJobList contains a list of NodeScanJob.
+
+
+
+
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `apiVersion` _string_ | `sbomscanner.kubewarden.io/v1alpha1` | | |
+| `kind` _string_ | `NodeScanJobList` | | |
+| `metadata` _[ListMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#listmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `items` _[NodeScanJob](#nodescanjob) array_ | | | |
+
+
+#### NodeScanJobSpec
+
+
+
+NodeScanJobSpec defines the desired state of NodeScanJob.
+
+
+
+_Appears in:_
+- [NodeScanJob](#nodescanjob)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `nodeName` _string_ | NodeName specifies the name of the node to be scanned. | | |
+
+
+#### NodeScanJobStatus
+
+
+
+NodeScanJobStatus defines the observed state of NodeScanJob.
+
+
+
+_Appears in:_
+- [NodeScanJob](#nodescanjob)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `conditions` _[Condition](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#condition-v1-meta) array_ | Conditions represent the latest available observations of ScanJob state | | Optional: \{\}
|
+| `startTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#time-v1-meta)_ | StartTime is when the job started processing. | | Optional: \{\}
|
+| `completionTime` _[Time](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#time-v1-meta)_ | CompletionTime is when the job completed or failed. | | Optional: \{\}
|
+
+
#### Platform
@@ -66,6 +199,7 @@ Platform describes the platform which the image in the manifest runs on.
_Appears in:_
+- [NodeScanConfigurationSpec](#nodescanconfigurationspec)
- [RegistrySpec](#registryspec)
- [WorkloadScanConfigurationSpec](#workloadscanconfigurationspec)
@@ -602,6 +736,66 @@ _Appears in:_
| `namespace` _string_ | Namespace of the WorkloadScanReport | | |
+#### NodeMetadata
+
+
+
+NodeMetadata contains the metadata details of a node.
+
+
+
+_Appears in:_
+- [NodeSBOM](#nodesbom)
+- [NodeVulnerabilityReport](#nodevulnerabilityreport)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `name` _string_ | Name specifies the name of the node. | | |
+| `platform` _string_ | Platform specifies the platform of the image. Example "linux/amd64". | | |
+
+
+
+
+#### NodeSBOM
+
+
+
+NodeSBOM represents a Software Bill of Materials of a node
+
+
+
+_Appears in:_
+- [NodeSBOMList](#nodesbomlist)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `nodeMetadata` _[NodeMetadata](#nodemetadata)_ | | | |
+| `spdx` _[RawExtension](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#rawextension-runtime-pkg)_ | SPDX contains the SPDX document of the SBOM in JSON format | | |
+
+
+
+
+#### NodeVulnerabilityReport
+
+
+
+NodeVulnerabilityReport is the Schema for the scanresults API
+
+
+
+_Appears in:_
+- [NodeVulnerabilityReportList](#nodevulnerabilityreportlist)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.36/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | |
+| `nodeMetadata` _[NodeMetadata](#nodemetadata)_ | NodeMetadata contains info about the scanned node | | |
+| `report` _[Report](#report)_ | Report is the actual vulnerability scan report | | |
+
+
+
+
#### Report
@@ -611,6 +805,7 @@ Report contains metadata about the scanned image and a list of vulnerability res
_Appears in:_
+- [NodeVulnerabilityReport](#nodevulnerabilityreport)
- [VulnerabilityReport](#vulnerabilityreport)
- [WorkloadScanVulnerabilityReport](#workloadscanvulnerabilityreport)
diff --git a/examples/nodesbom.yaml b/examples/nodesbom.yaml
new file mode 100644
index 000000000..c0b0bd8d3
--- /dev/null
+++ b/examples/nodesbom.yaml
@@ -0,0 +1,134 @@
+apiVersion: storage.sbomscanner.kubewarden.io/v1alpha1
+kind: NodeSBOM
+metadata:
+ name: worker-node-1
+nodeMetadata:
+ name: worker-node-1
+ platform: linux/amd64
+spdx:
+ SPDXID: SPDXRef-DOCUMENT
+ creationInfo:
+ created: "2026-05-19T10:15:00Z"
+ creators:
+ - "Organization: aquasecurity"
+ - "Tool: trivy-dev"
+ dataLicense: CC0-1.0
+ documentNamespace: http://aquasecurity.github.io/trivy/filesystem/worker-node-1-3b2d8e10-0c4b-4a7f-9d2a-1f6b8e0d4c5a
+ name: worker-node-1
+ packages:
+ - SPDXID: SPDXRef-Package-2a1c4e7b9d0f3a52
+ annotations:
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgID: libc6@2.39-0ubuntu8.3"
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgType: ubuntu"
+ checksums:
+ - algorithm: SHA256
+ checksumValue: c1f4e1b2d3a4c5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0
+ downloadLocation: NONE
+ externalRefs:
+ - referenceCategory: PACKAGE-MANAGER
+ referenceLocator: pkg:deb/ubuntu/libc6@2.39-0ubuntu8.3?arch=amd64&distro=ubuntu-24.04
+ referenceType: purl
+ filesAnalyzed: false
+ licenseConcluded: GPL-2.0-only AND LGPL-2.1-only
+ licenseDeclared: GPL-2.0-only AND LGPL-2.1-only
+ name: libc6
+ primaryPackagePurpose: LIBRARY
+ sourceInfo: "built package from: glibc 2.39-0ubuntu8.3"
+ supplier: "Organization: Ubuntu Developers "
+ versionInfo: 2.39-0ubuntu8.3
+ - SPDXID: SPDXRef-Package-6f3b8d2a4c1e9b07
+ annotations:
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgID: systemd@255.4-1ubuntu8.4"
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgType: ubuntu"
+ checksums:
+ - algorithm: SHA256
+ checksumValue: a9b8c7d6e5f4039281a7b6c5d4e3f201928374a5b6c7d8e9f0a1b2c3d4e5f607
+ downloadLocation: NONE
+ externalRefs:
+ - referenceCategory: PACKAGE-MANAGER
+ referenceLocator: pkg:deb/ubuntu/systemd@255.4-1ubuntu8.4?arch=amd64&distro=ubuntu-24.04
+ referenceType: purl
+ filesAnalyzed: false
+ licenseConcluded: LGPL-2.1-or-later AND GPL-2.0-or-later
+ licenseDeclared: LGPL-2.1-or-later AND GPL-2.0-or-later
+ name: systemd
+ primaryPackagePurpose: APPLICATION
+ sourceInfo: "built package from: systemd 255.4-1ubuntu8.4"
+ supplier: "Organization: Ubuntu Developers "
+ versionInfo: 255.4-1ubuntu8.4
+ - SPDXID: SPDXRef-Package-8e2a1d5c7b9f4036
+ annotations:
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgID: containerd@1.7.24-0ubuntu1~24.04.1"
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgType: ubuntu"
+ checksums:
+ - algorithm: SHA256
+ checksumValue: 5f8e7d6c5b4a3928170615243342516273849500a1b2c3d4e5f6a7b8c9d0e1f2
+ downloadLocation: NONE
+ externalRefs:
+ - referenceCategory: PACKAGE-MANAGER
+ referenceLocator: pkg:deb/ubuntu/containerd@1.7.24-0ubuntu1~24.04.1?arch=amd64&distro=ubuntu-24.04
+ referenceType: purl
+ filesAnalyzed: false
+ licenseConcluded: Apache-2.0
+ licenseDeclared: Apache-2.0
+ name: containerd
+ primaryPackagePurpose: APPLICATION
+ sourceInfo: "built package from: containerd 1.7.24-0ubuntu1~24.04.1"
+ supplier: "Organization: Ubuntu Developers "
+ versionInfo: 1.7.24-0ubuntu1~24.04.1
+ - SPDXID: SPDXRef-Package-3c7b2e5a8d1f9046
+ annotations:
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgID: kubelet@1.31.2-1.1"
+ - annotationDate: "2026-05-19T10:15:00Z"
+ annotationType: OTHER
+ annotator: "Tool: trivy-dev"
+ comment: "PkgType: ubuntu"
+ checksums:
+ - algorithm: SHA256
+ checksumValue: 0e1d2c3b4a5968778695a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3
+ downloadLocation: NONE
+ externalRefs:
+ - referenceCategory: PACKAGE-MANAGER
+ referenceLocator: pkg:deb/ubuntu/kubelet@1.31.2-1.1?arch=amd64&distro=ubuntu-24.04
+ referenceType: purl
+ filesAnalyzed: false
+ licenseConcluded: Apache-2.0
+ licenseDeclared: Apache-2.0
+ name: kubelet
+ primaryPackagePurpose: APPLICATION
+ versionInfo: 1.31.2-1.1
+ relationships:
+ - relatedSpdxElement: SPDXRef-Package-2a1c4e7b9d0f3a52
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+ - relatedSpdxElement: SPDXRef-Package-6f3b8d2a4c1e9b07
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+ - relatedSpdxElement: SPDXRef-Package-8e2a1d5c7b9f4036
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+ - relatedSpdxElement: SPDXRef-Package-3c7b2e5a8d1f9046
+ relationshipType: DESCRIBES
+ spdxElementId: SPDXRef-DOCUMENT
+ spdxVersion: SPDX-2.3
diff --git a/examples/nodescanconfiguration.yaml b/examples/nodescanconfiguration.yaml
new file mode 100644
index 000000000..9b36cc221
--- /dev/null
+++ b/examples/nodescanconfiguration.yaml
@@ -0,0 +1,20 @@
+apiVersion: sbomscanner.kubewarden.io/v1alpha1
+kind: NodeScanConfiguration
+metadata:
+ name: default
+ annotations:
+ "force-node-scan": "true"
+spec:
+ skipPatterns:
+ - "/run/k3s/containerd/"
+ scanInterval: 5m
+ nodeSelector:
+ matchExpressions:
+ - key: kubernetes.io/hostname
+ operator: In
+ values:
+ - "node1"
+ - "node2"
+ platforms:
+ - arch: "amd64"
+ os: "linux"
diff --git a/examples/nodescanjob.yaml b/examples/nodescanjob.yaml
new file mode 100644
index 000000000..f84417c85
--- /dev/null
+++ b/examples/nodescanjob.yaml
@@ -0,0 +1,5 @@
+apiVersion: sbomscanner.kubewarden.io/v1alpha1
+kind: NodeScanJob
+metadata:
+ name: node-scan-1
+spec:
diff --git a/internal/apiserver/storage.go b/internal/apiserver/storage.go
index 53bfdaccc..4708436e2 100644
--- a/internal/apiserver/storage.go
+++ b/internal/apiserver/storage.go
@@ -187,11 +187,35 @@ func NewStorageAPIServer(db *pgxpool.Pool, nc *nats.Conn, logger *slog.Logger, c
return nil, fmt.Errorf("error creating WorkloadScanReport store: %w", err)
}
+ nodeSBOMStore, nodeSBOMWatchers, err := storage.NewNodeSBOMStore(
+ Scheme,
+ serverConfig.RESTOptionsGetter,
+ db,
+ nc,
+ logger,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("error creating NodeSBOM store: %w", err)
+ }
+
+ nodeVulnerabilityReportStore, nodeVulnerabilityReportWatchers, err := storage.NewNodeVulnerabilityReportStore(
+ Scheme,
+ serverConfig.RESTOptionsGetter,
+ db,
+ nc,
+ logger,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("error creating NodeVulnerabilityReport store: %w", err)
+ }
+
v1alpha1storage := map[string]rest.Storage{
- "images": imageStore,
- "sboms": sbomStore,
- "vulnerabilityreports": vulnerabilityReportStore,
- "workloadscanreports": workloadScanReportStore,
+ "images": imageStore,
+ "sboms": sbomStore,
+ "vulnerabilityreports": vulnerabilityReportStore,
+ "workloadscanreports": workloadScanReportStore,
+ "nodesboms": nodeSBOMStore,
+ "nodevulnerabilityreports": nodeVulnerabilityReportStore,
}
apiGroupInfo.VersionedResourcesStorageMap["v1alpha1"] = v1alpha1storage
@@ -201,7 +225,7 @@ func NewStorageAPIServer(db *pgxpool.Pool, nc *nats.Conn, logger *slog.Logger, c
return &StorageAPIServer{
db: db,
- watchers: slices.Concat(imageWatchers, sbomWatchers, vulnerabilityReportWatchers, workloadScanReportWatchers),
+ watchers: slices.Concat(imageWatchers, sbomWatchers, vulnerabilityReportWatchers, workloadScanReportWatchers, nodeSBOMWatchers, nodeVulnerabilityReportWatchers),
logger: logger,
server: genericServer,
dynamicCertKeyPairContent: dynamicCertKeyPairContent,
diff --git a/internal/controller/indexer.go b/internal/controller/indexer.go
index 09c60ad7a..219df5480 100644
--- a/internal/controller/indexer.go
+++ b/internal/controller/indexer.go
@@ -33,6 +33,16 @@ func SetupIndexer(ctx context.Context, mgr ctrl.Manager) error {
return fmt.Errorf("unable to create field indexer: %w", err)
}
+ if err := mgr.GetFieldIndexer().IndexField(ctx, &v1alpha1.NodeScanJob{}, v1alpha1.IndexNodeScanJobSpecNodeName, func(rawObj client.Object) []string {
+ nodeScanJob, ok := rawObj.(*v1alpha1.NodeScanJob)
+ if !ok {
+ panic(fmt.Sprintf("Expected NodeScanJob, got %T", rawObj))
+ }
+ return []string{nodeScanJob.Spec.NodeName}
+ }); err != nil {
+ return fmt.Errorf("failed to setup field indexer for spec.nodeName: %w", err)
+ }
+
if err := mgr.GetFieldIndexer().IndexField(ctx, &storagev1alpha1.Image{}, storagev1alpha1.IndexImageMetadataComposite, indexImageByMetadata); err != nil {
return fmt.Errorf("failed to setup field indexer for %s: %w", storagev1alpha1.IndexImageMetadataComposite, err)
}
diff --git a/internal/controller/nodescan_controller.go b/internal/controller/nodescan_controller.go
new file mode 100644
index 000000000..85dac5e82
--- /dev/null
+++ b/internal/controller/nodescan_controller.go
@@ -0,0 +1,120 @@
+package controller
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/kubewarden/sbomscanner/api"
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/builder"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/event"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+ "sigs.k8s.io/controller-runtime/pkg/predicate"
+
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ v1alpha1 "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+// NodeScanReconciler watches Nodes and cleans up NodeScanJobs and NodeSBOMs
+// when a Node is deleted.
+type NodeScanReconciler struct {
+ client.Client
+}
+
+// +kubebuilder:rbac:groups=sbomscanner.kubewarden.io,resources=nodescanconfigurations,verbs=get;list;watch;update
+// +kubebuilder:rbac:groups=sbomscanner.kubewarden.io,resources=nodescanconfigurations/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=storage.sbomscanner.kubewarden.io,resources=nodesboms,verbs=list;watch;delete
+// +kubebuilder:rbac:groups=sbomscanner.kubewarden.io,resources=nodescanjobs,verbs=list;delete
+// +kubebuilder:rbac:groups=storage.sbomscanner.kubewarden.io,resources=nodesboms,verbs=list;delete
+// +kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;watch
+
+func (r *NodeScanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ logger := log.FromContext(ctx)
+
+ var node corev1.Node
+ if err := r.Get(ctx, req.NamespacedName, &node); err != nil {
+ if apierrors.IsNotFound(err) {
+ logger.Info("Node deleted, cleaning up related resources", "node", req.Name)
+ if err := r.cleanupNodeResources(ctx, req.Name); err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to cleanup resources for deleted node %s: %w", req.Name, err)
+ }
+ return ctrl.Result{}, nil
+ }
+ return ctrl.Result{}, fmt.Errorf("failed to get Node: %w", err)
+ }
+
+ return ctrl.Result{}, nil
+}
+
+func (r *NodeScanReconciler) cleanupNodeResources(ctx context.Context, nodeName string) error {
+ logger := log.FromContext(ctx)
+
+ var nodeScanJobs v1alpha1.NodeScanJobList
+ if err := r.List(ctx, &nodeScanJobs,
+ client.MatchingLabels{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelNodeScanKey: api.LabelNodeScanValue,
+ },
+ ); err != nil {
+ return fmt.Errorf("failed to list managed NodeScanJobs: %w", err)
+ }
+
+ for i := range nodeScanJobs.Items {
+ job := &nodeScanJobs.Items[i]
+ if job.Spec.NodeName == nodeName {
+ logger.Info("Deleting NodeScanJob for deleted node", "nodeScanJob", job.Name, "nodeName", nodeName)
+ if err := r.Delete(ctx, job); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to delete NodeScanJob %s: %w", job.Name, err)
+ }
+ }
+ }
+
+ var nodesboms storagev1alpha1.NodeSBOMList
+ if err := r.List(ctx, &nodesboms,
+ client.MatchingLabels{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ },
+ ); err != nil {
+ return fmt.Errorf("failed to list managed NodeSBOMs: %w", err)
+ }
+
+ for i := range nodesboms.Items {
+ nodesbom := &nodesboms.Items[i]
+ if nodesbom.NodeMetadata.Name == nodeName {
+ logger.Info("Deleting NodeSBOM for deleted node", "nodesbom", nodesbom.Name, "nodeName", nodeName)
+ if err := r.Delete(ctx, nodesbom); err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to delete NodeSBOM %s: %w", nodesbom.Name, err)
+ }
+ }
+ }
+
+ return nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *NodeScanReconciler) SetupWithManager(manager ctrl.Manager) error {
+ err := ctrl.NewControllerManagedBy(manager).
+ Named("nodescan-controller").
+ Watches(&corev1.Node{},
+ handler.EnqueueRequestsFromMapFunc(func(_ context.Context, obj client.Object) []ctrl.Request {
+ return []ctrl.Request{{NamespacedName: types.NamespacedName{Name: obj.GetName()}}}
+ }),
+ builder.WithPredicates(predicate.Funcs{
+ // Only trigger reconciliation on Node deletions, ignore creates and updates.
+ DeleteFunc: func(e event.DeleteEvent) bool { return true },
+ CreateFunc: func(e event.CreateEvent) bool { return false },
+ UpdateFunc: func(e event.UpdateEvent) bool { return false },
+ GenericFunc: func(e event.GenericEvent) bool { return false },
+ }),
+ ).
+ Complete(r)
+ if err != nil {
+ return fmt.Errorf("failed to create nodescan controller: %w", err)
+ }
+ return nil
+}
diff --git a/internal/controller/nodescan_controller_test.go b/internal/controller/nodescan_controller_test.go
new file mode 100644
index 000000000..115772afd
--- /dev/null
+++ b/internal/controller/nodescan_controller_test.go
@@ -0,0 +1,158 @@
+package controller
+
+import (
+ "context"
+ "fmt"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/google/uuid"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ "github.com/kubewarden/sbomscanner/api"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+var _ = Describe("NodeScan Controller", func() {
+ When("a Node still exists", func() {
+ var reconciler NodeScanReconciler
+ var node corev1.Node
+
+ BeforeEach(func(ctx context.Context) {
+ reconciler = NodeScanReconciler{
+ Client: k8sClient,
+ }
+
+ By("Creating a Node")
+ node = corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, &node)).To(Succeed())
+ })
+
+ It("should be a no-op", func(ctx context.Context) {
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{Name: node.Name},
+ })
+ Expect(err).NotTo(HaveOccurred())
+ })
+ })
+
+ When("a Node is deleted", func() {
+ var reconciler NodeScanReconciler
+ var nodeName string
+
+ BeforeEach(func(ctx context.Context) {
+ reconciler = NodeScanReconciler{
+ Client: k8sClient,
+ }
+
+ nodeName = fmt.Sprintf("node-%s", uuid.New().String())
+
+ By("Creating and then deleting a Node")
+ node := corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nodeName,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &node)).To(Succeed())
+ Expect(k8sClient.Delete(ctx, &node)).To(Succeed())
+ })
+
+ It("should cleanup NodeScanJobs for the deleted node", func(ctx context.Context) {
+ By("Creating a NodeScanJob tied to the deleted node")
+ nodeScanJob := v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("nodescanjob-%s", nodeName),
+ Labels: map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelNodeScanKey: api.LabelNodeScanValue,
+ },
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: nodeName,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &nodeScanJob)).To(Succeed())
+
+ By("Reconciling the deleted node")
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{Name: nodeName},
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the NodeScanJob was deleted")
+ deletedJob := &v1alpha1.NodeScanJob{}
+ err = k8sClient.Get(ctx, types.NamespacedName{Name: nodeScanJob.Name}, deletedJob)
+ Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred())
+ Expect(deletedJob.Name).To(BeEmpty())
+ })
+
+ It("should cleanup NodeSBOMs for the deleted node", func(ctx context.Context) {
+ By("Creating a NodeSBOM tied to the deleted node")
+ nodesbom := storagev1alpha1.NodeSBOM{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nodeName,
+ Labels: map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ },
+ },
+ NodeMetadata: storagev1alpha1.NodeMetadata{
+ Name: nodeName,
+ Platform: "linux/amd64",
+ },
+ SPDX: runtime.RawExtension{Raw: []byte("{}")},
+ }
+ Expect(k8sClient.Create(ctx, &nodesbom)).To(Succeed())
+
+ By("Reconciling the deleted node")
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{Name: nodeName},
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the NodeSBOM was deleted")
+ deletedSBOM := &storagev1alpha1.NodeSBOM{}
+ err = k8sClient.Get(ctx, types.NamespacedName{Name: nodesbom.Name}, deletedSBOM)
+ Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred())
+ Expect(deletedSBOM.Name).To(BeEmpty())
+ })
+
+ It("should not affect resources for other nodes", func(ctx context.Context) {
+ By("Creating a NodeScanJob for a different node")
+ otherNodeName := fmt.Sprintf("other-node-%s", uuid.New().String())
+ otherJob := v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("nodescanjob-%s", otherNodeName),
+ Labels: map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelNodeScanKey: api.LabelNodeScanValue,
+ },
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: otherNodeName,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &otherJob)).To(Succeed())
+
+ By("Reconciling the deleted node")
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{Name: nodeName},
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the other node's NodeScanJob still exists")
+ remainingJob := &v1alpha1.NodeScanJob{}
+ Expect(k8sClient.Get(ctx, types.NamespacedName{Name: otherJob.Name}, remainingJob)).To(Succeed())
+ })
+ })
+})
diff --git a/internal/controller/nodescan_runner.go b/internal/controller/nodescan_runner.go
new file mode 100644
index 000000000..0d9f84971
--- /dev/null
+++ b/internal/controller/nodescan_runner.go
@@ -0,0 +1,280 @@
+package controller
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "time"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/client-go/util/retry"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+ "sigs.k8s.io/controller-runtime/pkg/log"
+
+ "github.com/kubewarden/sbomscanner/api"
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/filters"
+)
+
+const nodeScanRunnerPeriod = 10 * time.Second
+
+// NodeScanRunner handles periodic scanning of nodes based on the NodeScanConfiguration.
+type NodeScanRunner struct {
+ client.Client
+ Scheme *runtime.Scheme
+}
+
+func (r *NodeScanRunner) Start(ctx context.Context) error {
+ log := log.FromContext(ctx)
+ log.Info("Starting node scan runner")
+
+ ticker := time.NewTicker(nodeScanRunnerPeriod)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ log.Info("Stopping node scan runner")
+
+ return nil
+ case <-ticker.C:
+ if err := r.scanNodes(ctx); err != nil {
+ log.Error(err, "Failed to scan nodes")
+ }
+ }
+ }
+}
+
+func (r *NodeScanRunner) scanNodes(ctx context.Context) error {
+ log := log.FromContext(ctx)
+
+ log.Info("Starting node scan cycle")
+ var config v1alpha1.NodeScanConfiguration
+ if err := r.Get(ctx, types.NamespacedName{Name: v1alpha1.NodeScanConfigurationName}, &config); err != nil {
+ if apierrors.IsNotFound(err) {
+ log.V(1).Info("NodeScanConfiguration not found, skipping")
+ return nil
+ }
+
+ return fmt.Errorf("failed to get NodeScanConfiguration: %w", err)
+ }
+
+ forceRescan := hasForceNodeScanAnnotation(&config)
+
+ nodes, err := r.getMatchingNodes(ctx, &config)
+ if err != nil {
+ return fmt.Errorf("failed to list matching nodes: %w", err)
+ }
+
+ log.V(1).Info("Checking nodes for scanning", "count", len(nodes))
+
+ for i := range nodes {
+ if err := r.checkNodeForScan(ctx, &config, &nodes[i], forceRescan); err != nil {
+ log.Error(err, "Failed to check node for scan", "node", nodes[i].Name)
+ continue
+ }
+ }
+
+ log.V(1).Info("Finished checking nodes for scanning", "force scan", forceRescan)
+ if forceRescan {
+ log.Info("Removing force rescan annotation from NodeScanConfiguration")
+ if err := r.removeForceNodeScanAnnotation(ctx, &config); err != nil {
+ log.Error(err, "Failed to remove force-node-scan annotation: %w", "error", err)
+ return fmt.Errorf("failed to remove force-node-scan annotation: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func (r *NodeScanRunner) getMatchingNodes(ctx context.Context, config *v1alpha1.NodeScanConfiguration) ([]corev1.Node, error) {
+ var nodeList corev1.NodeList
+
+ listOpts := []client.ListOption{}
+
+ if config.Spec.NodeSelector != nil {
+ selector, err := metav1.LabelSelectorAsSelector(config.Spec.NodeSelector)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse node selector: %w", err)
+ }
+
+ listOpts = append(listOpts, client.MatchingLabelsSelector{Selector: selector})
+ }
+
+ if err := r.List(ctx, &nodeList, listOpts...); err != nil {
+ return nil, fmt.Errorf("failed to list nodes: %w", err)
+ }
+
+ return nodeList.Items, nil
+}
+
+func (r *NodeScanRunner) checkNodeForScan(ctx context.Context, config *v1alpha1.NodeScanConfiguration, node *corev1.Node, forceRescan bool) error {
+ log := log.FromContext(ctx)
+
+ if !filters.IsPlatformAllowed(
+ node.Status.NodeInfo.OperatingSystem,
+ node.Status.NodeInfo.Architecture,
+ "",
+ config.Spec.Platforms,
+ ) {
+ log.V(1).Info("Skipping node with disallowed platform",
+ "node", node.Name,
+ "platform", fmt.Sprintf("%s/%s", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.Architecture),
+ )
+ return nil
+ }
+
+ lastScanJob, err := r.getLastNodeScanJob(ctx, node.Name)
+ if err != nil && !apierrors.IsNotFound(err) {
+ return fmt.Errorf("failed to get last node scan job for node %s: %w", node.Name, err)
+ }
+
+ if lastScanJob != nil && !lastScanJob.IsComplete() && !lastScanJob.IsFailed() {
+ log.V(1).Info("Node has a running NodeScanJob, skipping", "node", node.Name, "nodeScanJob", lastScanJob.Name)
+
+ return nil
+ }
+
+ if !r.shouldCreateNodeScanJob(ctx, config, node.Name, lastScanJob, forceRescan) {
+ return nil
+ }
+
+ if err := r.createNodeScanJob(ctx, config, node.Name); err != nil {
+ return fmt.Errorf("failed to create node scan job for node %s: %w", node.Name, err)
+ }
+
+ log.Info("Created node scan job for node", "node", node.Name)
+
+ return nil
+}
+
+func (r *NodeScanRunner) shouldCreateNodeScanJob(ctx context.Context, config *v1alpha1.NodeScanConfiguration, nodeName string, lastScanJob *v1alpha1.NodeScanJob, forceRescan bool) bool {
+ log := log.FromContext(ctx)
+
+ if forceRescan {
+ log.Info("Force rescan requested for node", "node", nodeName)
+ return true
+ }
+
+ if config.Spec.ScanInterval == nil || config.Spec.ScanInterval.Duration == 0 {
+ if lastScanJob != nil {
+ log.V(1).Info("Skipping node with disabled scan interval", "node", nodeName)
+ }
+
+ return false
+ }
+
+ if lastScanJob == nil {
+ return true
+ }
+
+ if lastScanJob.Status.CompletionTime != nil {
+ timeSinceLastScan := time.Since(lastScanJob.Status.CompletionTime.Time)
+ if timeSinceLastScan < config.Spec.ScanInterval.Duration {
+ log.V(1).Info("Node doesn't need scanning yet", "node", nodeName, "timeSinceLastScan", timeSinceLastScan)
+
+ return false
+ }
+ }
+
+ return true
+}
+
+func hasForceNodeScanAnnotation(config *v1alpha1.NodeScanConfiguration) bool {
+ if config.Annotations == nil {
+ return false
+ }
+
+ return config.Annotations[v1alpha1.AnnotationForceNodeScanKey] == "true"
+}
+
+func (r *NodeScanRunner) removeForceNodeScanAnnotation(ctx context.Context, config *v1alpha1.NodeScanConfiguration) error {
+ return retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ var current v1alpha1.NodeScanConfiguration
+ if err := r.Get(ctx, types.NamespacedName{Name: config.Name}, ¤t); err != nil {
+ return fmt.Errorf("failed to get current NodeScanConfiguration: %w", err)
+ }
+
+ if _, ok := current.Annotations[v1alpha1.AnnotationForceNodeScanKey]; !ok {
+ return nil
+ }
+
+ delete(current.Annotations, v1alpha1.AnnotationForceNodeScanKey)
+
+ if err := r.Update(ctx, ¤t); err != nil {
+ return fmt.Errorf("failed to update NodeScanConfiguration: %w", err)
+ }
+
+ return nil
+ })
+}
+
+func (r *NodeScanRunner) getLastNodeScanJob(ctx context.Context, nodeName string) (*v1alpha1.NodeScanJob, error) {
+ var nodeScanJobs v1alpha1.NodeScanJobList
+
+ listOpts := []client.ListOption{
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: nodeName},
+ }
+ if err := r.List(ctx, &nodeScanJobs, listOpts...); err != nil {
+ return nil, fmt.Errorf("failed to list node scan jobs: %w", err)
+ }
+
+ if len(nodeScanJobs.Items) == 0 {
+ return nil, apierrors.NewNotFound(
+ v1alpha1.GroupVersion.WithResource("nodescanjobs").GroupResource(),
+ fmt.Sprintf("for node %s", nodeName),
+ )
+ }
+
+ sort.Slice(nodeScanJobs.Items, func(i, j int) bool {
+ return nodeScanJobs.Items[i].CreationTimestamp.After(nodeScanJobs.Items[j].CreationTimestamp.Time)
+ })
+
+ return &nodeScanJobs.Items[0], nil
+}
+
+func (r *NodeScanRunner) createNodeScanJob(ctx context.Context, config *v1alpha1.NodeScanConfiguration, nodeName string) error {
+ nodeScanJob := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ GenerateName: fmt.Sprintf("node-%s-", nodeName),
+ Annotations: map[string]string{
+ v1alpha1.AnnotationNodeScanJobTriggerKey: "runner",
+ },
+ Labels: map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelNodeScanKey: api.LabelNodeScanValue,
+ },
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: nodeName,
+ },
+ }
+
+ if err := controllerutil.SetControllerReference(config, nodeScanJob, r.Scheme); err != nil {
+ return fmt.Errorf("failed to set owner reference on NodeScanJob: %w", err)
+ }
+
+ if err := r.Create(ctx, nodeScanJob); err != nil {
+ return fmt.Errorf("failed to create NodeScanJob: %w", err)
+ }
+
+ return nil
+}
+
+func (r *NodeScanRunner) NeedLeaderElection() bool {
+ return true
+}
+
+func (r *NodeScanRunner) SetupWithManager(mgr ctrl.Manager) error {
+ if err := mgr.Add(r); err != nil {
+ return fmt.Errorf("failed to create NodeScanRunner: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/controller/nodescan_runner_test.go b/internal/controller/nodescan_runner_test.go
new file mode 100644
index 000000000..33a762905
--- /dev/null
+++ b/internal/controller/nodescan_runner_test.go
@@ -0,0 +1,302 @@
+package controller
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+
+ "github.com/google/uuid"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+var _ = Describe("NodeScanRunner", func() {
+ Describe("scanNodes", func() {
+ var (
+ runner *NodeScanRunner
+ config *v1alpha1.NodeScanConfiguration
+ node *corev1.Node
+ )
+
+ BeforeEach(func() {
+ runner = &NodeScanRunner{
+ Client: k8sClient,
+ }
+ })
+
+ When("A node needs scanning", func() {
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a Node")
+ node = &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, node)).To(Succeed())
+
+ By("Creating a NodeScanConfiguration with a scan interval of 1 hour")
+ config = &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{Duration: 1 * time.Hour},
+ },
+ }
+ Expect(k8sClient.Create(ctx, config)).To(Succeed())
+ })
+
+ AfterEach(func(ctx context.Context) {
+ Expect(k8sClient.Delete(ctx, config)).To(Succeed())
+ })
+
+ It("Should create a NodeScanJob for the node", func(ctx context.Context) {
+ By("Running the node scanner")
+ err := runner.scanNodes(ctx)
+ Expect(err).To(Succeed())
+
+ By("Verifying a NodeScanJob was created for the node")
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ Expect(k8sClient.List(ctx, nodeScanJobs,
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: node.Name},
+ )).To(Succeed())
+ Expect(nodeScanJobs.Items).To(HaveLen(1))
+
+ By("Checking the NodeScanJob has correct node name and trigger annotation")
+ Expect(nodeScanJobs.Items[0].Spec.NodeName).To(Equal(node.Name))
+ Expect(nodeScanJobs.Items[0].Annotations).To(HaveKeyWithValue(v1alpha1.AnnotationNodeScanJobTriggerKey, "runner"))
+ })
+ })
+
+ When("Node platform is not allowed by the configuration", func() {
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a Node with linux/arm64 platform")
+ node = &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, node)).To(Succeed())
+ node.Status = corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "arm64",
+ },
+ }
+ Expect(k8sClient.Status().Update(ctx, node)).To(Succeed())
+
+ By("Creating a NodeScanConfiguration that only allows linux/amd64")
+ config = &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{Duration: 1 * time.Hour},
+ Platforms: []v1alpha1.Platform{
+ {OS: "linux", Architecture: "amd64"},
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, config)).To(Succeed())
+ })
+
+ AfterEach(func(ctx context.Context) {
+ Expect(k8sClient.Delete(ctx, config)).To(Succeed())
+ })
+
+ It("Should not create a NodeScanJob for the node", func(ctx context.Context) {
+ By("Running the node scanner")
+ err := runner.scanNodes(ctx)
+ Expect(err).To(Succeed())
+
+ By("Verifying no NodeScanJobs were created for the node")
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ Expect(k8sClient.List(ctx, nodeScanJobs,
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: node.Name},
+ )).To(Succeed())
+ Expect(nodeScanJobs.Items).To(BeEmpty())
+ })
+ })
+
+ When("NodeScanConfiguration has the force-node-scan annotation", func() {
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a Node")
+ node = &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, node)).To(Succeed())
+
+ By("Creating a NodeScanConfiguration with the force-node-scan annotation")
+ config = &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ Annotations: map[string]string{
+ v1alpha1.AnnotationForceNodeScanKey: "true",
+ },
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{Duration: 1 * time.Hour},
+ },
+ }
+ Expect(k8sClient.Create(ctx, config)).To(Succeed())
+ })
+
+ AfterEach(func(ctx context.Context) {
+ Expect(k8sClient.Delete(ctx, config)).To(Succeed())
+ })
+
+ It("Should create a NodeScanJob even if the timer has not expired and remove the annotation", func(ctx context.Context) {
+ By("Creating a recently completed NodeScanJob within the scan interval")
+ recentJob := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("recent-node-job-%s", uuid.New().String()),
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: node.Name,
+ },
+ }
+ Expect(k8sClient.Create(ctx, recentJob)).To(Succeed())
+ recentJob.MarkComplete(v1alpha1.ReasonScanJobComplete, "Done")
+ recentJob.Status.CompletionTime = &metav1.Time{Time: time.Now()}
+ Expect(k8sClient.Status().Update(ctx, recentJob)).To(Succeed())
+
+ By("Running the node scanner")
+ err := runner.scanNodes(ctx)
+ Expect(err).To(Succeed())
+
+ By("Verifying a new NodeScanJob was created despite the timer not being expired")
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ Expect(k8sClient.List(ctx, nodeScanJobs,
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: node.Name},
+ )).To(Succeed())
+ Expect(nodeScanJobs.Items).To(HaveLen(2))
+
+ By("Verifying the force-node-scan annotation was removed")
+ var updatedConfig v1alpha1.NodeScanConfiguration
+ Expect(k8sClient.Get(ctx, types.NamespacedName{Name: v1alpha1.NodeScanConfigurationName}, &updatedConfig)).To(Succeed())
+ Expect(updatedConfig.Annotations).NotTo(HaveKey(v1alpha1.AnnotationForceNodeScanKey))
+ })
+
+ It("Should not create a NodeScanJob when one is already running", func(ctx context.Context) {
+ By("Creating an existing running NodeScanJob for the node")
+ existingJob := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("running-node-job-%s", uuid.New().String()),
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: node.Name,
+ },
+ }
+ Expect(k8sClient.Create(ctx, existingJob)).To(Succeed())
+
+ By("Running the node scanner")
+ err := runner.scanNodes(ctx)
+ Expect(err).To(Succeed())
+
+ By("Verifying no additional NodeScanJob was created")
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ Expect(k8sClient.List(ctx, nodeScanJobs,
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: node.Name},
+ )).To(Succeed())
+ Expect(nodeScanJobs.Items).To(HaveLen(1))
+ })
+ })
+
+ When("NodeScanConfiguration has the force-node-scan annotation but no scan interval", func() {
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a Node")
+ node = &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, node)).To(Succeed())
+
+ By("Creating a NodeScanConfiguration with force annotation and disabled interval")
+ config = &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ Annotations: map[string]string{
+ v1alpha1.AnnotationForceNodeScanKey: "true",
+ },
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{Duration: 0},
+ },
+ }
+ Expect(k8sClient.Create(ctx, config)).To(Succeed())
+ })
+
+ AfterEach(func(ctx context.Context) {
+ Expect(k8sClient.Delete(ctx, config)).To(Succeed())
+ })
+
+ It("Should create a NodeScanJob even with disabled interval", func(ctx context.Context) {
+ By("Running the node scanner")
+ err := runner.scanNodes(ctx)
+ Expect(err).To(Succeed())
+
+ By("Verifying a NodeScanJob was created due to force annotation")
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ Expect(k8sClient.List(ctx, nodeScanJobs,
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: node.Name},
+ )).To(Succeed())
+ Expect(nodeScanJobs.Items).To(HaveLen(1))
+
+ By("Verifying the force-node-scan annotation was removed")
+ var updatedConfig v1alpha1.NodeScanConfiguration
+ Expect(k8sClient.Get(ctx, types.NamespacedName{Name: v1alpha1.NodeScanConfigurationName}, &updatedConfig)).To(Succeed())
+ Expect(updatedConfig.Annotations).NotTo(HaveKey(v1alpha1.AnnotationForceNodeScanKey))
+ })
+ })
+
+ When("NodeScanConfiguration has no scan interval", func() {
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a Node")
+ node = &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, node)).To(Succeed())
+
+ By("Creating a NodeScanConfiguration with scan interval disabled (0 duration)")
+ config = &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{Duration: 0},
+ },
+ }
+ Expect(k8sClient.Create(ctx, config)).To(Succeed())
+ })
+
+ AfterEach(func(ctx context.Context) {
+ Expect(k8sClient.Delete(ctx, config)).To(Succeed())
+ })
+
+ It("Should not create any NodeScanJob", func(ctx context.Context) {
+ By("Running the node scanner")
+ err := runner.scanNodes(ctx)
+ Expect(err).To(Succeed())
+
+ By("Verifying no NodeScanJobs were created for the node")
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ Expect(k8sClient.List(ctx, nodeScanJobs,
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: node.Name},
+ )).To(Succeed())
+ Expect(nodeScanJobs.Items).To(BeEmpty())
+ })
+ })
+ })
+})
diff --git a/internal/controller/nodescanjob_controller.go b/internal/controller/nodescanjob_controller.go
new file mode 100644
index 000000000..41bef9604
--- /dev/null
+++ b/internal/controller/nodescanjob_controller.go
@@ -0,0 +1,253 @@
+package controller
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "sort"
+
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller"
+ "sigs.k8s.io/controller-runtime/pkg/handler"
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/filters"
+ "github.com/kubewarden/sbomscanner/internal/handlers"
+ "github.com/kubewarden/sbomscanner/internal/messaging"
+)
+
+// NodeScanJobReconciler reconciles a NodeScanJob object
+type NodeScanJobReconciler struct {
+ client.Client
+
+ Scheme *runtime.Scheme
+ Publisher messaging.Publisher
+}
+
+// +kubebuilder:rbac:groups=sbomscanner.kubewarden.io,resources=nodescanjobs,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=sbomscanner.kubewarden.io,resources=nodescanjobs/status,verbs=get;update;patch
+
+// Reconcile reconciles a NodeScanJob object.
+func (r *NodeScanJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+ log := logf.FromContext(ctx)
+ log.Info("Reconciling NodeScanJob")
+
+ nodeScanJob := &v1alpha1.NodeScanJob{}
+ if err := r.Get(ctx, req.NamespacedName, nodeScanJob); err != nil {
+ if errors.IsNotFound(err) {
+ log.V(1).Info("NodeScanJob not found, skipping reconciliation", "nodeScanJob", req.NamespacedName)
+ return ctrl.Result{}, nil
+ }
+ return ctrl.Result{}, fmt.Errorf("unable to get NodeScanJob: %w", err)
+ }
+
+ if !nodeScanJob.DeletionTimestamp.IsZero() {
+ log.V(1).Info("NodeScanJob is being deleted, skipping reconciliation", "nodeScanJob", req.NamespacedName)
+ return ctrl.Result{}, nil
+ }
+
+ var node corev1.Node
+ if err := r.Get(ctx, types.NamespacedName{Name: nodeScanJob.Spec.NodeName}, &node); err != nil {
+ if errors.IsNotFound(err) {
+ log.Info("Node no longer exists, deleting NodeScanJob", "nodeScanJob", req.NamespacedName, "nodeName", nodeScanJob.Spec.NodeName)
+ if delErr := r.Delete(ctx, nodeScanJob); delErr != nil && !errors.IsNotFound(delErr) {
+ return ctrl.Result{}, fmt.Errorf("failed to delete NodeScanJob for missing node: %w", delErr)
+ }
+ return ctrl.Result{}, nil
+ }
+ return ctrl.Result{}, fmt.Errorf("failed to check if node %s exists: %w", nodeScanJob.Spec.NodeName, err)
+ }
+
+ if !nodeScanJob.IsPending() {
+ log.V(1).Info("NodeScanJob is not in pending state, skipping reconciliation", "nodeScanJob", req.NamespacedName)
+ return ctrl.Result{}, nil
+ }
+
+ nodeScanJob.InitializeConditions()
+
+ if failed, err := r.validateNodeAgainstConfig(ctx, nodeScanJob, &node); err != nil {
+ return ctrl.Result{}, err
+ } else if failed {
+ if err := r.Status().Update(ctx, nodeScanJob); err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to update NodeScanJob status: %w", err)
+ }
+ return ctrl.Result{}, nil
+ }
+
+ reconcileResult, reconcileErr := r.reconcileNodeScanJob(ctx, nodeScanJob)
+
+ if err := r.Status().Update(ctx, nodeScanJob); err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to update NodeScanJob status: %w", err)
+ }
+
+ log.V(1).Info("Successfully reconciled NodeScanJob", "nodeScanJob", req.NamespacedName)
+ return reconcileResult, reconcileErr
+}
+
+// validateNodeAgainstConfig checks the NodeScanConfiguration exists and
+// the node matches it. Returns (true, nil) when the job was marked Failed.
+func (r *NodeScanJobReconciler) validateNodeAgainstConfig(ctx context.Context, job *v1alpha1.NodeScanJob, node *corev1.Node) (bool, error) {
+ log := logf.FromContext(ctx)
+
+ var config v1alpha1.NodeScanConfiguration
+ if err := r.Get(ctx, types.NamespacedName{Name: v1alpha1.NodeScanConfigurationName}, &config); err != nil {
+ if errors.IsNotFound(err) {
+ log.Info("NodeScanConfiguration not found, marking NodeScanJob as failed", "nodeScanJob", job.Name)
+ job.MarkFailed(v1alpha1.ReasonNodeScanJobConfigurationMissing, "NodeScanConfiguration not found: node scanning is not configured")
+ return true, nil
+ }
+ return false, fmt.Errorf("failed to get NodeScanConfiguration: %w", err)
+ }
+
+ if config.Spec.NodeSelector != nil {
+ selector, err := metav1.LabelSelectorAsSelector(config.Spec.NodeSelector)
+ if err != nil {
+ return false, fmt.Errorf("failed to parse NodeSelector: %w", err)
+ }
+ if !selector.Matches(labels.Set(node.Labels)) {
+ log.Info("Node does not match NodeScanConfiguration nodeSelector, marking NodeScanJob as failed",
+ "nodeScanJob", job.Name, "node", node.Name)
+ job.MarkFailed(v1alpha1.ReasonNodeScanJobNotMatching,
+ fmt.Sprintf("node %q does not match the NodeScanConfiguration nodeSelector", node.Name))
+ return true, nil
+ }
+ }
+
+ if !filters.IsPlatformAllowed(
+ node.Status.NodeInfo.OperatingSystem,
+ node.Status.NodeInfo.Architecture,
+ "",
+ config.Spec.Platforms,
+ ) {
+ log.Info("Node platform not allowed by NodeScanConfiguration, marking NodeScanJob as failed",
+ "nodeScanJob", job.Name, "node", node.Name,
+ "platform", fmt.Sprintf("%s/%s", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.Architecture))
+ job.MarkFailed(v1alpha1.ReasonNodeScanJobNotMatching,
+ fmt.Sprintf("node %q platform %s/%s is not allowed by the NodeScanConfiguration",
+ node.Name, node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.Architecture))
+ return true, nil
+ }
+
+ return false, nil
+}
+
+// reconcileScanJob implements the actual reconciliation logic.
+func (r *NodeScanJobReconciler) reconcileNodeScanJob(ctx context.Context, nodeScanJob *v1alpha1.NodeScanJob) (ctrl.Result, error) {
+ log := logf.FromContext(ctx)
+
+ if err := r.cleanupOldNodeScanJobs(ctx); err != nil {
+ return ctrl.Result{}, fmt.Errorf("failed to cleanup old NodeScanJobs: %w", err)
+ }
+
+ log.V(1).Info("Publishing GenerateNodeSBOM message for NodeScanJob", "nodescanJob", nodeScanJob.Name)
+ messageID := fmt.Sprintf("generateNodeSBOM/%s", nodeScanJob.GetUID())
+ message, err := json.Marshal(&handlers.GenerateNodeSBOMMessage{
+ NodeBaseMessage: handlers.NodeBaseMessage{
+ NodeScanJob: handlers.ObjectRef{
+ Name: nodeScanJob.Name,
+ Namespace: nodeScanJob.Namespace,
+ UID: string(nodeScanJob.GetUID()),
+ },
+ },
+ Node: handlers.ObjectRef{
+ Name: nodeScanJob.Spec.NodeName,
+ },
+ })
+ if err != nil {
+ return ctrl.Result{}, fmt.Errorf("unable to marshal GenerateNodeSBOM message: %w", err)
+ }
+
+ if err := r.Publisher.Publish(ctx, handlers.GenerateNodeSBOMSubject+"."+nodeScanJob.Spec.NodeName, messageID, message); err != nil {
+ return ctrl.Result{}, fmt.Errorf("unable to publish GenerateNodeSBOM message: %w", err)
+ }
+
+ nodeScanJob.MarkScheduled(v1alpha1.ReasonScanJobScheduled, "NodeScanJob has been scheduled for processing by the controller")
+
+ return ctrl.Result{}, nil
+}
+
+// cleanupOldNodeScanJobs ensures we don't have more than scanJobsHistoryLimit
+func (r *NodeScanJobReconciler) cleanupOldNodeScanJobs(ctx context.Context) error {
+ log := logf.FromContext(ctx)
+
+ scanJobList := &v1alpha1.NodeScanJobList{}
+
+ if err := r.List(ctx, scanJobList); err != nil {
+ return fmt.Errorf("failed to list NodeScanJobs: %w", err)
+ }
+
+ if len(scanJobList.Items) <= scanJobsHistoryLimit {
+ return nil
+ }
+
+ sort.Slice(scanJobList.Items, func(i, j int) bool {
+ ti := scanJobList.Items[i].GetCreationTimestampFromAnnotation()
+ tj := scanJobList.Items[j].GetCreationTimestampFromAnnotation()
+
+ return ti.Before(tj)
+ })
+
+ log.V(1).Info("Sorting NodeScanJobs by creation timestamp for cleanup",
+ "scanjobs", scanJobList.Items)
+
+ scanJobsToDelete := len(scanJobList.Items) - scanJobsHistoryLimit
+ for _, scanJob := range scanJobList.Items[:scanJobsToDelete] {
+ if err := r.Delete(ctx, &scanJob); err != nil {
+ return fmt.Errorf("failed to delete old NodeScanJob %s: %w", scanJob.Name, err)
+ }
+ log.Info("cleaned up old NodeScanJob",
+ "name", scanJob.Name,
+ "creationTimestamp", scanJob.CreationTimestamp)
+ }
+
+ return nil
+}
+
+func (r *NodeScanJobReconciler) mapNodeToNodeScanJobs(ctx context.Context, obj client.Object) []ctrl.Request {
+ log := logf.FromContext(ctx)
+
+ var nodeScanJobs v1alpha1.NodeScanJobList
+ if err := r.List(ctx, &nodeScanJobs,
+ client.MatchingFields{v1alpha1.IndexNodeScanJobSpecNodeName: obj.GetName()},
+ ); err != nil {
+ log.Error(err, "Failed to list NodeScanJobs for node", "node", obj.GetName())
+ return nil
+ }
+
+ requests := make([]ctrl.Request, 0, len(nodeScanJobs.Items))
+ for i := range nodeScanJobs.Items {
+ requests = append(requests, ctrl.Request{
+ NamespacedName: types.NamespacedName{
+ Name: nodeScanJobs.Items[i].Name,
+ },
+ })
+ }
+
+ return requests
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *NodeScanJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
+ err := ctrl.NewControllerManagedBy(mgr).
+ For(&v1alpha1.NodeScanJob{}).
+ Watches(&corev1.Node{},
+ handler.EnqueueRequestsFromMapFunc(r.mapNodeToNodeScanJobs),
+ ).
+ WithOptions(controller.Options{
+ MaxConcurrentReconciles: maxConcurrentReconciles,
+ }).
+ Complete(r)
+ if err != nil {
+ return fmt.Errorf("failed to create NodeScanJob controller: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/controller/nodescanjob_controller_test.go b/internal/controller/nodescanjob_controller_test.go
new file mode 100644
index 000000000..0e02c6e2d
--- /dev/null
+++ b/internal/controller/nodescanjob_controller_test.go
@@ -0,0 +1,344 @@
+package controller
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "github.com/stretchr/testify/mock"
+
+ "github.com/google/uuid"
+ corev1 "k8s.io/api/core/v1"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/handlers"
+ messagingMocks "github.com/kubewarden/sbomscanner/internal/messaging/mocks"
+)
+
+var _ = Describe("NodeScanJob Controller", func() {
+ When("A NodeScanJob is created for a valid Node", func() {
+ var reconciler NodeScanJobReconciler
+ var nodeScanJob v1alpha1.NodeScanJob
+ var node corev1.Node
+ var mockPublisher *messagingMocks.MockPublisher
+
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a new NodeScanJobReconciler")
+ mockPublisher = messagingMocks.NewMockPublisher(GinkgoT())
+ reconciler = NodeScanJobReconciler{
+ Client: k8sClient,
+ Publisher: mockPublisher,
+ Scheme: k8sClient.Scheme(),
+ }
+
+ By("Creating a Node")
+ node = corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, &node)).To(Succeed())
+
+ By("Creating a NodeScanJob for the node")
+ nodeScanJob = v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("nodescanjob-%s", uuid.New().String()),
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: node.Name,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &nodeScanJob)).To(Succeed())
+ })
+
+ It("should successfully reconcile and publish GenerateNodeSBOM message", func(ctx context.Context) {
+ By("Setting up the expected message publication")
+ message, err := json.Marshal(&handlers.GenerateNodeSBOMMessage{
+ NodeBaseMessage: handlers.NodeBaseMessage{
+ NodeScanJob: handlers.ObjectRef{
+ Name: nodeScanJob.Name,
+ Namespace: nodeScanJob.Namespace,
+ UID: string(nodeScanJob.GetUID()),
+ },
+ },
+ Node: handlers.ObjectRef{
+ Name: node.Name,
+ },
+ })
+ Expect(err).NotTo(HaveOccurred())
+ mockPublisher.On("Publish", mock.Anything, handlers.GenerateNodeSBOMSubject+"."+node.Name, fmt.Sprintf("generateNodeSBOM/%s", nodeScanJob.GetUID()), message).Return(nil)
+
+ By("Reconciling the NodeScanJob")
+ _, err = reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Name: nodeScanJob.Name,
+ },
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the NodeScanJob is marked as scheduled")
+ updatedJob := &v1alpha1.NodeScanJob{}
+ err = k8sClient.Get(ctx, types.NamespacedName{
+ Name: nodeScanJob.Name,
+ }, updatedJob)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(updatedJob.IsScheduled()).To(BeTrue())
+ })
+ })
+
+ When("A NodeScanJob is created but NodeScanConfiguration is missing", func() {
+ var reconciler NodeScanJobReconciler
+ var nodeScanJob v1alpha1.NodeScanJob
+ var node corev1.Node
+ var mockPublisher *messagingMocks.MockPublisher
+
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a new NodeScanJobReconciler")
+ mockPublisher = messagingMocks.NewMockPublisher(GinkgoT())
+ reconciler = NodeScanJobReconciler{
+ Client: k8sClient,
+ Publisher: mockPublisher,
+ Scheme: k8sClient.Scheme(),
+ }
+
+ By("Creating a Node")
+ node = corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ }
+ Expect(k8sClient.Create(ctx, &node)).To(Succeed())
+
+ By("Creating a NodeScanJob for the node")
+ nodeScanJob = v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("nodescanjob-%s", uuid.New().String()),
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: node.Name,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &nodeScanJob)).To(Succeed())
+ })
+
+ It("should mark the NodeScanJob as failed", func(ctx context.Context) {
+ By("Reconciling the NodeScanJob")
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Name: nodeScanJob.Name,
+ },
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the NodeScanJob is marked as failed")
+ updatedJob := &v1alpha1.NodeScanJob{}
+ err = k8sClient.Get(ctx, types.NamespacedName{
+ Name: nodeScanJob.Name,
+ }, updatedJob)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(updatedJob.IsFailed()).To(BeTrue())
+ })
+ })
+
+ When("A NodeScanJob is created but node does not match NodeScanConfiguration nodeSelector", func() {
+ var reconciler NodeScanJobReconciler
+ var nodeScanJob v1alpha1.NodeScanJob
+ var node corev1.Node
+ var config v1alpha1.NodeScanConfiguration
+ var mockPublisher *messagingMocks.MockPublisher
+
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a new NodeScanJobReconciler")
+ mockPublisher = messagingMocks.NewMockPublisher(GinkgoT())
+ reconciler = NodeScanJobReconciler{
+ Client: k8sClient,
+ Publisher: mockPublisher,
+ Scheme: k8sClient.Scheme(),
+ }
+
+ By("Creating a NodeScanConfiguration with a nodeSelector")
+ config = v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{"env": "production"},
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, &config)).To(Succeed())
+
+ By("Creating a Node without matching labels")
+ node = corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ Labels: map[string]string{"env": "staging"},
+ },
+ }
+ Expect(k8sClient.Create(ctx, &node)).To(Succeed())
+
+ By("Creating a NodeScanJob for the node")
+ nodeScanJob = v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("nodescanjob-%s", uuid.New().String()),
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: node.Name,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &nodeScanJob)).To(Succeed())
+ })
+
+ AfterEach(func(ctx context.Context) {
+ Expect(k8sClient.Delete(ctx, &config)).To(Succeed())
+ })
+
+ It("should mark the NodeScanJob as failed", func(ctx context.Context) {
+ By("Reconciling the NodeScanJob")
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Name: nodeScanJob.Name,
+ },
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the NodeScanJob is marked as failed")
+ updatedJob := &v1alpha1.NodeScanJob{}
+ err = k8sClient.Get(ctx, types.NamespacedName{
+ Name: nodeScanJob.Name,
+ }, updatedJob)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(updatedJob.IsFailed()).To(BeTrue())
+ })
+ })
+
+ When("A NodeScanJob is created but node platform is not allowed", func() {
+ var reconciler NodeScanJobReconciler
+ var nodeScanJob v1alpha1.NodeScanJob
+ var node corev1.Node
+ var config v1alpha1.NodeScanConfiguration
+ var mockPublisher *messagingMocks.MockPublisher
+
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a new NodeScanJobReconciler")
+ mockPublisher = messagingMocks.NewMockPublisher(GinkgoT())
+ reconciler = NodeScanJobReconciler{
+ Client: k8sClient,
+ Publisher: mockPublisher,
+ Scheme: k8sClient.Scheme(),
+ }
+
+ By("Creating a NodeScanConfiguration with platform filter")
+ config = v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ Platforms: []v1alpha1.Platform{
+ {Architecture: "amd64", OS: "linux"},
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, &config)).To(Succeed())
+
+ By("Creating a Node with a different platform")
+ node = corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("node-%s", uuid.New().String()),
+ },
+ Status: corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "arm64",
+ },
+ },
+ }
+ Expect(k8sClient.Create(ctx, &node)).To(Succeed())
+
+ By("Creating a NodeScanJob for the node")
+ nodeScanJob = v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("nodescanjob-%s", uuid.New().String()),
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: node.Name,
+ },
+ }
+ Expect(k8sClient.Create(ctx, &nodeScanJob)).To(Succeed())
+ })
+
+ AfterEach(func(ctx context.Context) {
+ Expect(k8sClient.Delete(ctx, &config)).To(Succeed())
+ })
+
+ It("should mark the NodeScanJob as failed", func(ctx context.Context) {
+ By("Reconciling the NodeScanJob")
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Name: nodeScanJob.Name,
+ },
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the NodeScanJob is marked as failed")
+ updatedJob := &v1alpha1.NodeScanJob{}
+ err = k8sClient.Get(ctx, types.NamespacedName{
+ Name: nodeScanJob.Name,
+ }, updatedJob)
+ Expect(err).NotTo(HaveOccurred())
+ Expect(updatedJob.IsFailed()).To(BeTrue())
+ })
+ })
+
+ When("A NodeScanJob is created for an invalid Node", func() {
+ var reconciler NodeScanJobReconciler
+ var nodeScanJob v1alpha1.NodeScanJob
+ var mockPublisher *messagingMocks.MockPublisher
+
+ BeforeEach(func(ctx context.Context) {
+ By("Creating a new NodeScanJobReconciler")
+ mockPublisher = messagingMocks.NewMockPublisher(GinkgoT())
+ reconciler = NodeScanJobReconciler{
+ Client: k8sClient,
+ Publisher: mockPublisher,
+ Scheme: k8sClient.Scheme(),
+ }
+
+ By("Creating a NodeScanJob referencing a non-existent node")
+ nodeScanJob = v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: fmt.Sprintf("nodescanjob-%s", uuid.New().String()),
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: "non-existent-node",
+ },
+ }
+ Expect(k8sClient.Create(ctx, &nodeScanJob)).To(Succeed())
+ })
+
+ It("should delete the NodeScanJob when the node no longer exists", func(ctx context.Context) {
+ By("Reconciling the NodeScanJob")
+ _, err := reconciler.Reconcile(ctx, reconcile.Request{
+ NamespacedName: types.NamespacedName{
+ Name: nodeScanJob.Name,
+ },
+ })
+ Expect(err).NotTo(HaveOccurred())
+
+ By("Verifying the NodeScanJob was deleted")
+ deletedJob := &v1alpha1.NodeScanJob{}
+ err = k8sClient.Get(ctx, types.NamespacedName{
+ Name: nodeScanJob.Name,
+ }, deletedJob)
+ Expect(client.IgnoreNotFound(err)).NotTo(HaveOccurred())
+ Expect(deletedJob.Name).To(BeEmpty())
+ })
+ })
+})
diff --git a/internal/controller/registry_scan_runner_test.go b/internal/controller/registry_scan_runner_test.go
index 4437948d0..c5dc6ab69 100644
--- a/internal/controller/registry_scan_runner_test.go
+++ b/internal/controller/registry_scan_runner_test.go
@@ -110,7 +110,7 @@ var _ = Describe("RegistryScanRunner", func() {
},
}
Expect(k8sClient.Create(ctx, completedJob)).To(Succeed())
- completedJob.MarkComplete(v1alpha1.ReasonComplete, "Done")
+ completedJob.MarkComplete(v1alpha1.ReasonScanJobComplete, "Done")
completedJob.Status.CompletionTime = &metav1.Time{Time: time.Now().Add(-2 * time.Hour)}
Expect(k8sClient.Status().Update(ctx, completedJob)).To(Succeed())
@@ -140,7 +140,7 @@ var _ = Describe("RegistryScanRunner", func() {
},
}
Expect(k8sClient.Create(ctx, recentJob)).To(Succeed())
- recentJob.MarkComplete(v1alpha1.ReasonComplete, "Done")
+ recentJob.MarkComplete(v1alpha1.ReasonScanJobComplete, "Done")
recentJob.Status.CompletionTime = &metav1.Time{Time: time.Now()}
Expect(k8sClient.Status().Update(ctx, recentJob)).To(Succeed())
@@ -277,7 +277,7 @@ var _ = Describe("RegistryScanRunner", func() {
},
}
Expect(k8sClient.Create(ctx, completedJob)).To(Succeed())
- completedJob.MarkComplete(v1alpha1.ReasonComplete, "Done")
+ completedJob.MarkComplete(v1alpha1.ReasonScanJobComplete, "Done")
completedJob.Status.CompletionTime = &metav1.Time{Time: time.Now()}
Expect(k8sClient.Status().Update(ctx, completedJob)).To(Succeed())
@@ -314,7 +314,7 @@ var _ = Describe("RegistryScanRunner", func() {
},
}
Expect(k8sClient.Create(ctx, completedJob)).To(Succeed())
- completedJob.MarkComplete(v1alpha1.ReasonComplete, "Done")
+ completedJob.MarkComplete(v1alpha1.ReasonScanJobComplete, "Done")
completedJob.Status.CompletionTime = &metav1.Time{Time: time.Now()}
Expect(k8sClient.Status().Update(ctx, completedJob)).To(Succeed())
diff --git a/internal/controller/scanjob_controller.go b/internal/controller/scanjob_controller.go
index 88c90f72e..770dd7134 100644
--- a/internal/controller/scanjob_controller.go
+++ b/internal/controller/scanjob_controller.go
@@ -87,7 +87,7 @@ func (r *ScanJobReconciler) reconcileScanJob(ctx context.Context, scanJob *v1alp
}, registry); err != nil {
if errors.IsNotFound(err) {
log.Error(err, "Registry not found", "registry", scanJob.Spec.Registry)
- scanJob.MarkFailed(v1alpha1.ReasonRegistryNotFound, fmt.Sprintf("Registry %s not found", scanJob.Spec.Registry))
+ scanJob.MarkFailed(v1alpha1.ReasonScanJobRegistryNotFound, fmt.Sprintf("Registry %s not found", scanJob.Spec.Registry))
return ctrl.Result{}, nil
}
@@ -149,7 +149,7 @@ func (r *ScanJobReconciler) reconcileScanJob(ctx context.Context, scanJob *v1alp
return ctrl.Result{}, fmt.Errorf("unable to publish CreateSBOM message: %w", err)
}
- scanJob.MarkScheduled(v1alpha1.ReasonScheduled, "ScanJob has been scheduled for processing by the controller")
+ scanJob.MarkScheduled(v1alpha1.ReasonScanJobScheduled, "ScanJob has been scheduled for processing by the controller")
return ctrl.Result{}, nil
}
@@ -203,14 +203,14 @@ func validateScanJobTargets(scanJob *v1alpha1.ScanJob, registry *v1alpha1.Regist
for _, target := range scanJob.Spec.Repositories {
repository := registry.GetRepository(target.Name)
if repository == nil {
- return v1alpha1.ReasonRepositoryNotFound,
+ return v1alpha1.ReasonScanJobRepositoryNotFound,
fmt.Errorf("repository %q is not declared on registry %q", target.Name, registry.Name)
}
for _, conditionName := range target.MatchConditions {
if !slices.ContainsFunc(repository.MatchConditions, func(mc v1alpha1.MatchCondition) bool {
return mc.Name == conditionName
}) {
- return v1alpha1.ReasonMatchConditionNotFound,
+ return v1alpha1.ReasonScanJobMatchConditionNotFound,
fmt.Errorf("match condition %q is not declared on repository %q of registry %q", conditionName, target.Name, registry.Name)
}
}
diff --git a/internal/controller/scanjob_controller_test.go b/internal/controller/scanjob_controller_test.go
index 90764bb1e..487563efb 100644
--- a/internal/controller/scanjob_controller_test.go
+++ b/internal/controller/scanjob_controller_test.go
@@ -246,17 +246,17 @@ var _ = Describe("ScanJob Controller", func() {
Namespace: scanJob.Namespace,
}, updated)).To(Succeed())
Expect(updated.IsFailed()).To(BeTrue())
- cond := meta.FindStatusCondition(updated.Status.Conditions, v1alpha1.ConditionTypeFailed)
+ cond := meta.FindStatusCondition(updated.Status.Conditions, v1alpha1.ConditionScanJobTypeFailed)
Expect(cond).NotTo(BeNil())
Expect(cond.Reason).To(Equal(expectedReason))
},
Entry("unknown repository => RepositoryNotFound",
[]v1alpha1.ScanJobRepository{{Name: "missing/repo"}},
- v1alpha1.ReasonRepositoryNotFound,
+ v1alpha1.ReasonScanJobRepositoryNotFound,
),
Entry("unknown matchCondition => MatchConditionNotFound",
[]v1alpha1.ScanJobRepository{{Name: "foo/bar", MatchConditions: []string{"tag-missing"}}},
- v1alpha1.ReasonMatchConditionNotFound,
+ v1alpha1.ReasonScanJobMatchConditionNotFound,
),
)
})
@@ -288,7 +288,7 @@ var _ = Describe("ScanJob Controller", func() {
Expect(k8sClient.Create(ctx, &scanJob)).To(Succeed())
By("Marking the ScanJob as completed")
- scanJob.MarkComplete(v1alpha1.ReasonAllImagesScanned, "Scan completed successfully")
+ scanJob.MarkComplete(v1alpha1.ReasonScanJobAllImagesScanned, "Scan completed successfully")
Expect(k8sClient.Status().Update(ctx, &scanJob)).To(Succeed())
})
diff --git a/internal/controller/vulnerabilityreport_controller.go b/internal/controller/vulnerabilityreport_controller.go
index 844623075..deffcbab9 100644
--- a/internal/controller/vulnerabilityreport_controller.go
+++ b/internal/controller/vulnerabilityreport_controller.go
@@ -92,9 +92,9 @@ func (r *VulnerabilityReportReconciler) Reconcile(ctx context.Context, req ctrl.
if scanJob.Status.ScannedImagesCount == scanJob.Status.ImagesCount {
now := metav1.Now()
scanJob.Status.CompletionTime = &now
- scanJob.MarkComplete(v1alpha1.ReasonAllImagesScanned, "All images scanned successfully")
+ scanJob.MarkComplete(v1alpha1.ReasonScanJobAllImagesScanned, "All images scanned successfully")
} else {
- scanJob.MarkInProgress(v1alpha1.ReasonImageScanInProgress, "Image scan in progress")
+ scanJob.MarkInProgress(v1alpha1.ReasonScanJobImageScanInProgress, "Image scan in progress")
}
}
if err := r.Status().Update(ctx, scanJob); err != nil {
diff --git a/internal/controller/vulnerabilityreport_controller_test.go b/internal/controller/vulnerabilityreport_controller_test.go
index 5ed0f7b3c..ce5d80705 100644
--- a/internal/controller/vulnerabilityreport_controller_test.go
+++ b/internal/controller/vulnerabilityreport_controller_test.go
@@ -164,7 +164,7 @@ var _ = Describe("VulnerabilityReport Controller", func() {
),
Entry("should update count but preserve failed status when ScanJob is already failed",
func(scanJob *v1alpha1.ScanJob) {
- scanJob.MarkFailed(v1alpha1.ReasonInternalError, "kaboom")
+ scanJob.MarkFailed(v1alpha1.ReasonScanJobInternalError, "kaboom")
},
func(scanJob *v1alpha1.ScanJob) {
Expect(scanJob.IsFailed()).To(BeTrue())
@@ -193,7 +193,7 @@ var _ = Describe("VulnerabilityReport Controller", func() {
scanJob.Status.ImagesCount = 2
scanJob.Status.ScannedImagesCount = 2
scanJob.Status.CompletionTime = &originalCompletionTime
- scanJob.MarkComplete(v1alpha1.ReasonAllImagesScanned, "All images scanned successfully")
+ scanJob.MarkComplete(v1alpha1.ReasonScanJobAllImagesScanned, "All images scanned successfully")
// MarkComplete overwrites CompletionTime, so set it back to our known value
scanJob.Status.CompletionTime = &originalCompletionTime
Expect(k8sClient.Status().Update(ctx, scanJob)).To(Succeed())
diff --git a/internal/handlers/create_catalog.go b/internal/handlers/create_catalog.go
index f5e09a649..0b4f72726 100644
--- a/internal/handlers/create_catalog.go
+++ b/internal/handlers/create_catalog.go
@@ -98,7 +98,7 @@ func (h *CreateCatalogHandler) Handle(ctx context.Context, message messaging.Mes
)
}
- scanJob.MarkInProgress(v1alpha1.ReasonCatalogCreationInProgress, "Catalog creation in progress")
+ scanJob.MarkInProgress(v1alpha1.ReasonScanJobCatalogCreationInProgress, "Catalog creation in progress")
return h.k8sClient.Status().Update(ctx, scanJob)
})
if err != nil {
@@ -289,10 +289,10 @@ func (h *CreateCatalogHandler) Handle(ctx context.Context, message messaging.Mes
if len(discoveredImages) == 0 {
h.logger.InfoContext(ctx, "No images to process", "scanjob", scanJob.Name, "namespace", scanJob.Namespace)
- scanJob.MarkComplete(v1alpha1.ReasonNoImagesToScan, "No images to process")
+ scanJob.MarkComplete(v1alpha1.ReasonScanJobNoImagesToScan, "No images to process")
} else {
h.logger.InfoContext(ctx, "Images to process", "count", len(discoveredImages))
- scanJob.MarkInProgress(v1alpha1.ReasonSBOMGenerationInProgress, "SBOM generation in progress")
+ scanJob.MarkInProgress(v1alpha1.ReasonScanJobSBOMGenerationInProgress, "SBOM generation in progress")
scanJob.Status.ImagesCount = len(discoveredImages)
scanJob.Status.ScannedImagesCount = 0
}
@@ -311,7 +311,7 @@ func (h *CreateCatalogHandler) Handle(ctx context.Context, message messaging.Mes
for _, image := range discoveredImages {
h.logger.DebugContext(ctx, "Sending generate SBOM message", "image", image.Name, "namespace", image.Namespace)
- messageID := fmt.Sprintf("generateSBOM/%s/%s", scanJob.UID, image.Name)
+ messageID := fmt.Sprintf("generateSBOM/%s/%s", scanJob.GetUID(), image.Name)
message, err := json.Marshal(&GenerateSBOMMessage{
BaseMessage: BaseMessage{
ScanJob: createCatalogMessage.ScanJob,
diff --git a/internal/handlers/create_catalog_test.go b/internal/handlers/create_catalog_test.go
index 6e12297a8..807b8b8fa 100644
--- a/internal/handlers/create_catalog_test.go
+++ b/internal/handlers/create_catalog_test.go
@@ -531,7 +531,7 @@ func TestCreateCatalogHandler_Handle(t *testing.T) {
mockPublisher := messagingMocks.NewMockPublisher(t)
for _, expectedImage := range test.expectedImages {
- messageID := fmt.Sprintf("generateSBOM/%s/%s", scanJob.UID, expectedImage.Name)
+ messageID := fmt.Sprintf("generateSBOM/%s/%s", scanJob.GetUID(), expectedImage.Name)
expectedMessage, err := json.Marshal(&GenerateSBOMMessage{
BaseMessage: BaseMessage{
ScanJob: ObjectRef{
diff --git a/internal/handlers/generate_node_sbom.go b/internal/handlers/generate_node_sbom.go
new file mode 100644
index 000000000..02c89ff36
--- /dev/null
+++ b/internal/handlers/generate_node_sbom.go
@@ -0,0 +1,307 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "path"
+
+ _ "modernc.org/sqlite" // sqlite driver for RPM DB and Java DB
+
+ trivyCommands "github.com/aquasecurity/trivy/pkg/commands"
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/util/retry"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
+ "github.com/kubewarden/sbomscanner/api"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/messaging"
+ "github.com/kubewarden/sbomscanner/internal/skippatterns"
+)
+
+// GenerateNodeSBOMHandler is responsible for handling SBOM generation requests.
+type GenerateNodeSBOMHandler struct {
+ k8sClient client.Client
+ scheme *runtime.Scheme
+ workDir string
+ targetDir string
+ trivyJavaDBRepository string
+ publisher messaging.Publisher
+ installationNamespace string
+ logger *slog.Logger
+}
+
+// NewGenerateNodeSBOMHandler creates a new instance of GenerateNodeSBOMHandler.
+func NewGenerateNodeSBOMHandler(
+ k8sClient client.Client,
+ scheme *runtime.Scheme,
+ workDir string,
+ targetDir string,
+ trivyJavaDBRepository string,
+ publisher messaging.Publisher,
+ installationNamespace string,
+ logger *slog.Logger,
+) *GenerateNodeSBOMHandler {
+ return &GenerateNodeSBOMHandler{
+ k8sClient: k8sClient,
+ scheme: scheme,
+ workDir: workDir,
+ targetDir: targetDir,
+ trivyJavaDBRepository: trivyJavaDBRepository,
+ publisher: publisher,
+ installationNamespace: installationNamespace,
+ logger: logger.With("handler", "generate_node_sbom_handler"),
+ }
+}
+
+// Handle processes the GenerateNodeSBOMMessage and generates a SBOM resource from the specified image.
+//
+//nolint:gocognit
+func (h *GenerateNodeSBOMHandler) Handle(ctx context.Context, message messaging.Message) error {
+ generateNodeSBOMMessage := &GenerateNodeSBOMMessage{}
+ if err := json.Unmarshal(message.Data(), generateNodeSBOMMessage); err != nil {
+ return fmt.Errorf("failed to unmarshal GenerateNodeSBOM message: %w", err)
+ }
+
+ h.logger.InfoContext(ctx, "Node SBOM generation requested",
+ "node", generateNodeSBOMMessage.Node.Name,
+ )
+
+ nodeScanJob := &v1alpha1.NodeScanJob{}
+ err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: generateNodeSBOMMessage.NodeScanJob.Name,
+ }, nodeScanJob)
+ if err != nil {
+ // Stop processing if the scanjob is not found, since it might have been deleted.
+ if apierrors.IsNotFound(err) {
+ h.logger.InfoContext(ctx, "NodeScanJob not found, stopping NodeSBOM generation", "nodescanjob", generateNodeSBOMMessage.NodeScanJob.Name)
+ return nil
+ }
+
+ return fmt.Errorf("cannot get NodeScanJob %s: %w", generateNodeSBOMMessage.NodeScanJob.Name, err)
+ }
+ if nodeScanJob.Name != generateNodeSBOMMessage.NodeScanJob.Name {
+ h.logger.InfoContext(ctx, "NodeScanJob not found, stopping NodeSBOM generation", "nodescanjob", generateNodeSBOMMessage.NodeScanJob.Name,
+ "uid", generateNodeSBOMMessage.NodeScanJob.UID)
+ return nil
+ }
+
+ node := &corev1.Node{}
+ err = h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: generateNodeSBOMMessage.Node.Name,
+ }, node)
+ if err != nil {
+ // Stop processing if the node is not found, since it might have been deleted.
+ if apierrors.IsNotFound(err) {
+ h.logger.InfoContext(ctx, "Node not found, stopping NodeSBOM generation", "node", generateNodeSBOMMessage.Node.Name)
+ return nil
+ }
+
+ return fmt.Errorf("cannot get node %s: %w", generateNodeSBOMMessage.Node.Name, err)
+ }
+ h.logger.DebugContext(ctx, "Node found", "node", node.Name)
+
+ if nodeScanJob.IsFailed() {
+ h.logger.InfoContext(ctx, "NodeScanJob is in failed state, stopping NodeSBOM generation", "nodescanjob", nodeScanJob.Name)
+ return nil
+ }
+
+ err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: generateNodeSBOMMessage.NodeScanJob.Name,
+ }, nodeScanJob); err != nil {
+ return fmt.Errorf("cannot get NodeScanJob %s: %w", generateNodeSBOMMessage.NodeScanJob.Name, err)
+ }
+
+ nodeScanJob.MarkInProgress(v1alpha1.ReasonNodeScanJobInProgress, "Scanning node filesystem and collecting data")
+ return h.k8sClient.Status().Update(ctx, nodeScanJob)
+ })
+ if err != nil {
+ return fmt.Errorf("failed to update NodeScanJob status to in progress: %w", err)
+ }
+
+ // Get the NodeScanConfiguration to determine where to create the NodeSBOM.
+ nodescanconfig := &v1alpha1.NodeScanConfiguration{}
+ err = h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: v1alpha1.NodeScanConfigurationName,
+ }, nodescanconfig)
+ if err != nil {
+ // Stop processing if the scanjob is not found, since it might have been deleted.
+ if apierrors.IsNotFound(err) {
+ h.logger.InfoContext(ctx, "NodeScanConfiguration not found, stopping NodeSBOM generation", "node name", node.Name)
+ return fmt.Errorf("NodeScanConfiguration %s not found: %w", v1alpha1.NodeScanConfigurationName, err)
+ }
+ return fmt.Errorf("cannot get NodeScanConfiguration: %w", err)
+ }
+
+ nodeSbom, err := h.getOrGenerateNodeSBOM(ctx, node, generateNodeSBOMMessage, nodescanconfig)
+ if err != nil {
+ return fmt.Errorf("failed to get or generate NodeSBOM: %w", err)
+ }
+
+ if err = message.InProgress(); err != nil {
+ return fmt.Errorf("failed to ack message as in progress: %w", err)
+ }
+
+ if err = h.k8sClient.Create(ctx, nodeSbom); err != nil {
+ if apierrors.IsAlreadyExists(err) {
+ h.logger.InfoContext(ctx, "NodeSBOM already exists, skipping creation", "nodesbom", generateNodeSBOMMessage.Node.Name)
+ } else {
+ return fmt.Errorf("failed to create NodeSBOM: %w", err)
+ }
+ }
+
+ scanNodeSBOMMessageID := fmt.Sprintf("nodeScanSBOM/%s/%s", nodeScanJob.GetUID(), generateNodeSBOMMessage.Node.Name)
+ scanNodeSBOMMessage, err := json.Marshal(&ScanNodeSBOMMessage{
+ NodeBaseMessage: NodeBaseMessage{
+ NodeScanJob: generateNodeSBOMMessage.NodeScanJob,
+ },
+ NodeSBOM: ObjectRef{
+ Name: generateNodeSBOMMessage.Node.Name,
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("cannot marshal scan NodeSBOM message: %w", err)
+ }
+
+ if err = h.publisher.Publish(ctx, ScanNodeSBOMSubject+"."+nodeScanJob.Spec.NodeName, scanNodeSBOMMessageID, scanNodeSBOMMessage); err != nil {
+ return fmt.Errorf("failed to publish scan NodeSBOM message: %w", err)
+ }
+
+ return nil
+}
+
+// getOrGenerateNodeSBOM checks if an SBOM with the same node name exists and reuses it, or generates a new one.
+func (h *GenerateNodeSBOMHandler) getOrGenerateNodeSBOM(ctx context.Context, node *corev1.Node, message *GenerateNodeSBOMMessage, config *v1alpha1.NodeScanConfiguration) (*storagev1alpha1.NodeSBOM, error) {
+ // Check if an SBOM with the same machine ID already exists
+ existingSBOM, err := h.findSBOMByNodeName(ctx, node.Name)
+ if err != nil && !apierrors.IsNotFound(err) {
+ return nil, fmt.Errorf("failed to check for existing NodeSBOM: %w", err)
+ }
+
+ var spdxBytes []byte
+ if existingSBOM != nil {
+ h.logger.InfoContext(ctx, "Found existing NodeSBOM with matching node name, reusing content",
+ "sbom", existingSBOM.Name,
+ "nodeName", node.Name,
+ )
+ spdxBytes = existingSBOM.SPDX.Raw
+ } else {
+ h.logger.InfoContext(ctx, "No existing NodeSBOM found, generating new one", "node name", node.Name)
+ spdxBytes, err = h.generateSPDX(ctx, config.Spec.SkipPatterns)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ sbomLabels := map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelPartOfKey: api.LabelPartOfValue,
+ }
+
+ nodePlatform := fmt.Sprintf("%s/%s", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.Architecture)
+ nodeSbom := &storagev1alpha1.NodeSBOM{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: message.Node.Name,
+ Labels: sbomLabels,
+ },
+ NodeMetadata: storagev1alpha1.NodeMetadata{
+ Name: node.Name,
+ Platform: nodePlatform,
+ },
+ SPDX: runtime.RawExtension{Raw: spdxBytes},
+ }
+
+ if err := controllerutil.SetControllerReference(config, nodeSbom, h.scheme); err != nil {
+ return nil, fmt.Errorf("failed to set owner reference: %w", err)
+ }
+
+ return nodeSbom, nil
+}
+
+// findSBOMByMachineID searches for an existing SBOM with the given machine ID.
+func (h *GenerateNodeSBOMHandler) findSBOMByNodeName(ctx context.Context, nodeName string) (*storagev1alpha1.NodeSBOM, error) {
+ sbomList := &storagev1alpha1.NodeSBOMList{}
+ err := h.k8sClient.List(ctx, sbomList,
+ client.MatchingFields{storagev1alpha1.IndexNodeMetadataName: nodeName},
+ client.Limit(1),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("failed to find NodeSBOM by node name: %w", err)
+ }
+
+ if len(sbomList.Items) == 0 {
+ return nil, apierrors.NewNotFound(storagev1alpha1.Resource("nodesbom"), nodeName)
+ }
+
+ return &sbomList.Items[0], nil
+}
+
+// generateSPDX generates SPDX JSON content for an image using Trivy.
+func (h *GenerateNodeSBOMHandler) generateSPDX(ctx context.Context, skipPats []string) ([]byte, error) {
+ sbomFile, err := os.CreateTemp(h.workDir, "trivy.sbom.*.json")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create temporary SBOM file: %w", err)
+ }
+ defer func() {
+ if err = sbomFile.Close(); err != nil {
+ h.logger.ErrorContext(ctx, "failed to close temporary SBOM file", "error", err)
+ }
+ if err = os.Remove(sbomFile.Name()); err != nil {
+ h.logger.ErrorContext(ctx, "failed to remove temporary SBOM file", "error", err)
+ }
+ }()
+
+ args := []string{
+ "filesystem",
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
+ "--skip-version-check",
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
+ "--disable-telemetry",
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
+ "--cache-dir", h.workDir,
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
+ "--format", "spdx-json",
+ "--skip-db-update",
+ // The Java DB is needed to generate SBOMs for images containing Java components
+ // See: https://github.com/aquasecurity/trivy/discussions/9666
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
+ "--java-db-repository", h.trivyJavaDBRepository,
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
+ "--output", sbomFile.Name(),
+ }
+
+ parsed := skippatterns.Parse(skipPats)
+ for _, dir := range parsed.SkipDirs {
+ args = append(args, "--skip-dirs", path.Join(h.targetDir, dir))
+ }
+ for _, file := range parsed.SkipFiles {
+ args = append(args, "--skip-files", path.Join(h.targetDir, file))
+ }
+
+ args = append(args, h.targetDir)
+
+ app := trivyCommands.NewApp()
+ app.SetArgs(args)
+
+ h.logger.DebugContext(ctx, "Executing Trivy to generate SPDX SBOM", "args", args)
+
+ if err = app.ExecuteContext(ctx); err != nil {
+ return nil, fmt.Errorf("failed to execute trivy: %w", err)
+ }
+
+ spdxBytes, err := io.ReadAll(sbomFile)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read NodeSBOM output: %w", err)
+ }
+
+ return spdxBytes, nil
+}
diff --git a/internal/handlers/generate_node_sbom_test.go b/internal/handlers/generate_node_sbom_test.go
new file mode 100644
index 000000000..5713b58e1
--- /dev/null
+++ b/internal/handlers/generate_node_sbom_test.go
@@ -0,0 +1,351 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/types"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ "github.com/kubewarden/sbomscanner/api"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ messagingMocks "github.com/kubewarden/sbomscanner/internal/messaging/mocks"
+ "github.com/kubewarden/sbomscanner/pkg/generated/clientset/versioned/scheme"
+)
+
+func TestGenerateNodeSBOMHandler_Handle(t *testing.T) {
+ targetDir := filepath.Join("..", "..", "test", "fixtures", "node")
+ nodeName := "test-node"
+
+ node := &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nodeName,
+ UID: "test-node-uid",
+ },
+ Status: corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "amd64",
+ },
+ },
+ }
+
+ nodeScanJob := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-nodescanjob",
+ UID: "test-nodescanjob-uid",
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: nodeName,
+ },
+ }
+
+ config := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ }
+
+ scheme := scheme.Scheme
+ require.NoError(t, storagev1alpha1.AddToScheme(scheme))
+ require.NoError(t, v1alpha1.AddToScheme(scheme))
+ require.NoError(t, corev1.AddToScheme(scheme))
+
+ k8sClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithRuntimeObjects(node, nodeScanJob, config).
+ WithStatusSubresource(&v1alpha1.NodeScanJob{}).
+ WithIndex(&storagev1alpha1.NodeSBOM{}, storagev1alpha1.IndexNodeMetadataName, func(obj client.Object) []string {
+ sbom, ok := obj.(*storagev1alpha1.NodeSBOM)
+ if !ok {
+ return nil
+ }
+ return []string{sbom.NodeMetadata.Name}
+ }).
+ Build()
+
+ publisher := messagingMocks.NewMockPublisher(t)
+
+ expectedScanMessage, err := json.Marshal(&ScanNodeSBOMMessage{
+ NodeBaseMessage: NodeBaseMessage{
+ NodeScanJob: ObjectRef{
+ Name: nodeScanJob.Name,
+ UID: string(nodeScanJob.UID),
+ },
+ },
+ NodeSBOM: ObjectRef{
+ Name: nodeName,
+ },
+ })
+ require.NoError(t, err)
+
+ publisher.On("Publish",
+ mock.Anything,
+ ScanNodeSBOMSubject+"."+nodeName,
+ fmt.Sprintf("nodeScanSBOM/%s/%s", nodeScanJob.UID, nodeName),
+ expectedScanMessage,
+ ).Return(nil).Once()
+
+ handler := NewGenerateNodeSBOMHandler(k8sClient, scheme, t.TempDir(), targetDir, testTrivyJavaDBRepository, publisher, "sbomscanner", slog.Default())
+
+ message, err := json.Marshal(&GenerateNodeSBOMMessage{
+ NodeBaseMessage: NodeBaseMessage{
+ NodeScanJob: ObjectRef{
+ Name: nodeScanJob.Name,
+ UID: string(nodeScanJob.UID),
+ },
+ },
+ Node: ObjectRef{
+ Name: nodeName,
+ },
+ })
+ require.NoError(t, err)
+
+ err = handler.Handle(t.Context(), &testMessage{data: message})
+ require.NoError(t, err)
+
+ sbom := &storagev1alpha1.NodeSBOM{}
+ err = k8sClient.Get(t.Context(), types.NamespacedName{Name: nodeName}, sbom)
+ require.NoError(t, err)
+
+ assert.Equal(t, nodeName, sbom.NodeMetadata.Name)
+ assert.Equal(t, "linux/amd64", sbom.NodeMetadata.Platform)
+ assert.Equal(t, api.LabelManagedByValue, sbom.Labels[api.LabelManagedByKey])
+ assert.Equal(t, api.LabelPartOfValue, sbom.Labels[api.LabelPartOfKey])
+ assert.Equal(t, node.UID, sbom.GetOwnerReferences()[0].UID)
+ assert.NotEmpty(t, sbom.SPDX.Raw)
+}
+
+func TestGenerateNodeSBOMHandler_Handle_StopProcessing(t *testing.T) {
+ nodeName := "test-node"
+
+ node := &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nodeName,
+ UID: "test-node-uid",
+ },
+ Status: corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "amd64",
+ },
+ },
+ }
+
+ nodeScanJob := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-nodescanjob",
+ UID: "test-nodescanjob-uid",
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: nodeName,
+ },
+ }
+
+ failedNodeScanJob := nodeScanJob.DeepCopy()
+ failedNodeScanJob.MarkFailed(v1alpha1.ReasonScanJobInternalError, "kaboom")
+
+ config := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ }
+
+ tests := []struct {
+ name string
+ nodeScanJob *v1alpha1.NodeScanJob
+ existingObjects []runtime.Object
+ }{
+ {
+ name: "nodescanjob not found",
+ nodeScanJob: nodeScanJob,
+ existingObjects: []runtime.Object{node, config},
+ },
+ {
+ name: "nodescanjob is failed",
+ nodeScanJob: failedNodeScanJob,
+ existingObjects: []runtime.Object{failedNodeScanJob, node, config},
+ },
+ {
+ name: "node not found",
+ nodeScanJob: nodeScanJob,
+ existingObjects: []runtime.Object{nodeScanJob, config},
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ scheme := scheme.Scheme
+ require.NoError(t, storagev1alpha1.AddToScheme(scheme))
+ require.NoError(t, v1alpha1.AddToScheme(scheme))
+ require.NoError(t, corev1.AddToScheme(scheme))
+
+ k8sClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithRuntimeObjects(test.existingObjects...).
+ WithIndex(&storagev1alpha1.NodeSBOM{}, storagev1alpha1.IndexNodeMetadataName, func(obj client.Object) []string {
+ sbom, ok := obj.(*storagev1alpha1.NodeSBOM)
+ if !ok {
+ return nil
+ }
+ return []string{sbom.NodeMetadata.Name}
+ }).
+ Build()
+
+ publisher := messagingMocks.NewMockPublisher(t)
+
+ handler := NewGenerateNodeSBOMHandler(k8sClient, scheme, t.TempDir(), "/tmp", testTrivyJavaDBRepository, publisher, "sbomscanner", slog.Default())
+
+ message, err := json.Marshal(&GenerateNodeSBOMMessage{
+ NodeBaseMessage: NodeBaseMessage{
+ NodeScanJob: ObjectRef{
+ Name: test.nodeScanJob.Name,
+ UID: string(test.nodeScanJob.UID),
+ },
+ },
+ Node: ObjectRef{
+ Name: nodeName,
+ },
+ })
+ require.NoError(t, err)
+
+ err = handler.Handle(context.Background(), &testMessage{data: message})
+ require.NoError(t, err)
+
+ sbom := &storagev1alpha1.NodeSBOM{}
+ err = k8sClient.Get(context.Background(), types.NamespacedName{Name: nodeName}, sbom)
+ assert.True(t, apierrors.IsNotFound(err), "NodeSBOM should not exist")
+ })
+ }
+}
+
+func TestGenerateNodeSBOMHandler_Handle_ReuseSBOMWithSameNodeName(t *testing.T) {
+ targetDir := filepath.Join("..", "..", "test", "fixtures", "node")
+ nodeName := "test-node"
+
+ expectedSPDXContent := []byte(`{"spdxVersion":"SPDX-2.3","dataLicense":"CC0-1.0"}`)
+
+ existingNodeSBOM := &storagev1alpha1.NodeSBOM{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "existing-node-sbom",
+ UID: "existing-sbom-uid",
+ },
+ NodeMetadata: storagev1alpha1.NodeMetadata{
+ Name: nodeName,
+ Platform: "linux/amd64",
+ },
+ SPDX: runtime.RawExtension{Raw: expectedSPDXContent},
+ }
+
+ node := &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nodeName,
+ UID: "test-node-uid",
+ },
+ Status: corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "amd64",
+ },
+ },
+ }
+
+ nodeScanJob := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "test-nodescanjob",
+ UID: "test-nodescanjob-uid",
+ },
+ Spec: v1alpha1.NodeScanJobSpec{
+ NodeName: nodeName,
+ },
+ }
+
+ config := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ }
+
+ scheme := scheme.Scheme
+ require.NoError(t, storagev1alpha1.AddToScheme(scheme))
+ require.NoError(t, v1alpha1.AddToScheme(scheme))
+ require.NoError(t, corev1.AddToScheme(scheme))
+
+ k8sClient := fake.NewClientBuilder().
+ WithScheme(scheme).
+ WithRuntimeObjects(existingNodeSBOM, node, nodeScanJob, config).
+ WithStatusSubresource(&v1alpha1.NodeScanJob{}).
+ WithIndex(&storagev1alpha1.NodeSBOM{}, storagev1alpha1.IndexNodeMetadataName, func(obj client.Object) []string {
+ sbom, ok := obj.(*storagev1alpha1.NodeSBOM)
+ if !ok {
+ return nil
+ }
+ return []string{sbom.NodeMetadata.Name}
+ }).
+ Build()
+
+ publisher := messagingMocks.NewMockPublisher(t)
+
+ expectedScanMessage, err := json.Marshal(&ScanNodeSBOMMessage{
+ NodeBaseMessage: NodeBaseMessage{
+ NodeScanJob: ObjectRef{
+ Name: nodeScanJob.Name,
+ UID: string(nodeScanJob.UID),
+ },
+ },
+ NodeSBOM: ObjectRef{
+ Name: nodeName,
+ },
+ })
+ require.NoError(t, err)
+
+ publisher.On("Publish",
+ mock.Anything,
+ ScanNodeSBOMSubject+"."+nodeScanJob.Spec.NodeName,
+ fmt.Sprintf("nodeScanSBOM/%s/%s", nodeScanJob.UID, nodeName),
+ expectedScanMessage,
+ ).Return(nil).Once()
+
+ handler := NewGenerateNodeSBOMHandler(k8sClient, scheme, t.TempDir(), targetDir, testTrivyJavaDBRepository, publisher, "sbomscanner", slog.Default())
+
+ message, err := json.Marshal(&GenerateNodeSBOMMessage{
+ NodeBaseMessage: NodeBaseMessage{
+ NodeScanJob: ObjectRef{
+ Name: nodeScanJob.Name,
+ UID: string(nodeScanJob.UID),
+ },
+ },
+ Node: ObjectRef{
+ Name: nodeName,
+ },
+ })
+ require.NoError(t, err)
+
+ err = handler.Handle(t.Context(), &testMessage{data: message})
+ require.NoError(t, err)
+
+ newSBOM := &storagev1alpha1.NodeSBOM{}
+ err = k8sClient.Get(t.Context(), types.NamespacedName{Name: nodeName}, newSBOM)
+ require.NoError(t, err)
+
+ assert.Equal(t, expectedSPDXContent, newSBOM.SPDX.Raw, "SPDX content should be reused from existing NodeSBOM")
+ assert.Equal(t, nodeName, newSBOM.NodeMetadata.Name)
+ assert.Equal(t, "linux/amd64", newSBOM.NodeMetadata.Platform)
+ assert.Equal(t, node.UID, newSBOM.GetOwnerReferences()[0].UID)
+ assert.Equal(t, api.LabelManagedByValue, newSBOM.Labels[api.LabelManagedByKey])
+ assert.Equal(t, api.LabelPartOfValue, newSBOM.Labels[api.LabelPartOfKey])
+}
diff --git a/internal/handlers/generate_sbom.go b/internal/handlers/generate_sbom.go
index 819d0fffd..207c0f7c1 100644
--- a/internal/handlers/generate_sbom.go
+++ b/internal/handlers/generate_sbom.go
@@ -266,14 +266,20 @@ func (h *GenerateSBOMHandler) generateSPDX(ctx context.Context, image *storagev1
args := []string{
"image",
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
"--skip-version-check",
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
"--disable-telemetry",
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
"--cache-dir", h.workDir,
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
"--format", "spdx-json",
"--skip-db-update",
// The Java DB is needed to generate SBOMs for images containing Java components
// See: https://github.com/aquasecurity/trivy/discussions/9666
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
"--java-db-repository", h.trivyJavaDBRepository,
+ //nolint:goconst // These are specific Trivy command arguments, not constant values used elsewhere
"--output", sbomFile.Name(),
}
diff --git a/internal/handlers/generate_sbom_test.go b/internal/handlers/generate_sbom_test.go
index a5b42d9d1..89aae2b6c 100644
--- a/internal/handlers/generate_sbom_test.go
+++ b/internal/handlers/generate_sbom_test.go
@@ -409,7 +409,7 @@ func TestGenerateSBOMHandler_Handle_StopProcessing(t *testing.T) {
differentUIDScanJob.UID = "test-scanjob-different-uid"
failedScanJob := scanJob.DeepCopy()
- failedScanJob.MarkFailed(v1alpha1.ReasonInternalError, "kaboom")
+ failedScanJob.MarkFailed(v1alpha1.ReasonScanJobInternalError, "kaboom")
tests := []struct {
name string
diff --git a/internal/handlers/image_scan_sbom.go b/internal/handlers/image_scan_sbom.go
new file mode 100644
index 000000000..fd2596c94
--- /dev/null
+++ b/internal/handlers/image_scan_sbom.go
@@ -0,0 +1,156 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
+ "github.com/kubewarden/sbomscanner/api"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/messaging"
+)
+
+// ImageScanSBOMHandler handles SBOM scan requests for container images.
+type ImageScanSBOMHandler struct {
+ scanSBOMBase
+}
+
+// NewScanSBOMHandler creates a new instance of ImageScanSBOMHandler for container images.
+func NewScanSBOMHandler(
+ k8sClient client.Client,
+ scheme *runtime.Scheme,
+ workDir string,
+ trivyDBRepository string,
+ trivyJavaDBRepository string,
+ logger *slog.Logger,
+) *ImageScanSBOMHandler {
+ return &ImageScanSBOMHandler{
+ scanSBOMBase: scanSBOMBase{
+ k8sClient: k8sClient,
+ scheme: scheme,
+ workDir: workDir,
+ trivyDBRepository: trivyDBRepository,
+ trivyJavaDBRepository: trivyJavaDBRepository,
+ logger: logger.With("handler", "scan_sbom_handler"),
+ },
+ }
+}
+
+//nolint:funlen
+func (h *ImageScanSBOMHandler) Handle(ctx context.Context, message messaging.Message) error {
+ scanSBOMMessage := &ScanSBOMMessage{}
+ if err := json.Unmarshal(message.Data(), scanSBOMMessage); err != nil {
+ return fmt.Errorf("failed to unmarshal scan job message: %w", err)
+ }
+
+ scanJobName := scanSBOMMessage.ScanJob.Name
+ scanJobNamespace := scanSBOMMessage.ScanJob.Namespace
+ scanJobUID := scanSBOMMessage.ScanJob.UID
+ sbomName := scanSBOMMessage.SBOM.Name
+ sbomNamespace := scanSBOMMessage.SBOM.Namespace
+
+ h.logger.InfoContext(ctx, "SBOM scan requested",
+ "sbom", sbomName,
+ "namespace", sbomNamespace,
+ )
+
+ scanJob := &v1alpha1.ScanJob{}
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: scanJobName,
+ Namespace: scanJobNamespace,
+ }, scanJob); err != nil {
+ if apierrors.IsNotFound(err) {
+ h.logger.ErrorContext(ctx, "ScanJob not found, stopping SBOM scan", "scanJob", scanJobName, "namespace", scanJobNamespace)
+ return nil
+ }
+
+ return fmt.Errorf("failed to get ScanJob: %w", err)
+ }
+
+ if string(scanJob.GetUID()) != scanJobUID {
+ h.logger.InfoContext(ctx, "ScanJob not found, stopping SBOM generation (UID changed)", "scanjob", scanJobName, "namespace", scanJobNamespace,
+ "uid", scanJobUID)
+ return nil
+ }
+
+ if scanJob.IsFailed() {
+ h.logger.InfoContext(ctx, "ScanJob is in failed state, stopping SBOM scan", "scanjob", scanJobName, "namespace", scanJobNamespace)
+ return nil
+ }
+
+ registryData, ok := scanJob.Annotations[v1alpha1.AnnotationScanJobRegistryKey]
+ if !ok {
+ return fmt.Errorf("scan job %s/%s does not have a registry annotation", scanJobNamespace, scanJobName)
+ }
+
+ registry := &v1alpha1.Registry{}
+ if err := json.Unmarshal([]byte(registryData), registry); err != nil {
+ return fmt.Errorf("cannot unmarshal registry data from scan job %s/%s: %w", scanJobNamespace, scanJobName, err)
+ }
+
+ sbom := &storagev1alpha1.SBOM{}
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: sbomName,
+ Namespace: sbomNamespace,
+ }, sbom); err != nil {
+ if apierrors.IsNotFound(err) {
+ h.logger.ErrorContext(ctx, "SBOM not found, stopping SBOM scan", "sbom", sbomName, "namespace", sbomNamespace)
+ return nil
+ }
+
+ return fmt.Errorf("failed to get SBOM: %w", err)
+ }
+
+ results, summary, err := h.runTrivyScan(ctx, sbom.SPDX.Raw, message)
+ if err != nil {
+ return err
+ }
+
+ h.logger.InfoContext(ctx, "SBOM scanned",
+ "sbom", sbomName,
+ "namespace", sbomNamespace,
+ )
+
+ vulnerabilityReport := &storagev1alpha1.VulnerabilityReport{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: sbomName,
+ Namespace: sbomNamespace,
+ },
+ }
+ if err = controllerutil.SetControllerReference(sbom, vulnerabilityReport, h.scheme); err != nil {
+ return fmt.Errorf("failed to set owner reference: %w", err)
+ }
+
+ _, err = controllerutil.CreateOrUpdate(ctx, h.k8sClient, vulnerabilityReport, func() error {
+ vulnerabilityReport.Labels = map[string]string{
+ v1alpha1.LabelScanJobUIDKey: scanJobUID,
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelPartOfKey: api.LabelPartOfValue,
+ }
+ if registry.Labels[api.LabelWorkloadScanKey] == api.LabelWorkloadScanValue {
+ vulnerabilityReport.Labels[api.LabelWorkloadScanKey] = api.LabelWorkloadScanValue
+ }
+
+ vulnerabilityReport.ImageMetadata = sbom.GetImageMetadata()
+ vulnerabilityReport.Report = storagev1alpha1.Report{
+ Summary: summary,
+ Results: results,
+ }
+
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create or update vulnerability report: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/handlers/messages.go b/internal/handlers/messages.go
index 48b137d43..096abf299 100644
--- a/internal/handlers/messages.go
+++ b/internal/handlers/messages.go
@@ -1,9 +1,11 @@
package handlers
const (
- GenerateSBOMSubject = "sbomscanner.sbom.generate"
- ScanSBOMSubject = "sbomscanner.sbom.scan"
- CreateCatalogSubject = "sbomscanner.catalog.create"
+ GenerateSBOMSubject = "sbomscanner.sbom.generate"
+ ScanSBOMSubject = "sbomscanner.sbom.scan"
+ CreateCatalogSubject = "sbomscanner.catalog.create"
+ GenerateNodeSBOMSubject = "sbomscanner.nodesbom.generate"
+ ScanNodeSBOMSubject = "sbomscanner.nodesbom.scan"
)
// ObjectRef is a reference to a Kubernetes object, used in messages to identify resources.
@@ -38,3 +40,22 @@ type ScanSBOMMessage struct {
SBOM ObjectRef `json:"sbom"`
}
+
+// NodeBaseMessage is the base structure for node messages.
+type NodeBaseMessage struct {
+ NodeScanJob ObjectRef `json:"nodescanjob"`
+}
+
+// GenerateNodeSBOMMessage represents the request message for generating a node SBOM.
+type GenerateNodeSBOMMessage struct {
+ NodeBaseMessage
+
+ Node ObjectRef `json:"node"`
+}
+
+// ScanNodeSBOMMessage represents the request message for scanning a node SBOM.
+type ScanNodeSBOMMessage struct {
+ NodeBaseMessage
+
+ NodeSBOM ObjectRef `json:"nodesbom"`
+}
diff --git a/internal/handlers/node_scan_sbom.go b/internal/handlers/node_scan_sbom.go
new file mode 100644
index 000000000..1a1fa4ee8
--- /dev/null
+++ b/internal/handlers/node_scan_sbom.go
@@ -0,0 +1,182 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/client-go/util/retry"
+
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
+ "github.com/kubewarden/sbomscanner/api"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/messaging"
+)
+
+// NodeScanSBOMHandler handles SBOM scan requests for nodes.
+type NodeScanSBOMHandler struct {
+ scanSBOMBase
+}
+
+// NewScanNodeSBOMHandler creates a new instance of NodeScanSBOMHandler for nodes.
+func NewScanNodeSBOMHandler(
+ k8sClient client.Client,
+ scheme *runtime.Scheme,
+ workDir string,
+ trivyDBRepository string,
+ trivyJavaDBRepository string,
+ logger *slog.Logger,
+) *NodeScanSBOMHandler {
+ return &NodeScanSBOMHandler{
+ scanSBOMBase: scanSBOMBase{
+ k8sClient: k8sClient,
+ scheme: scheme,
+ workDir: workDir,
+ trivyDBRepository: trivyDBRepository,
+ trivyJavaDBRepository: trivyJavaDBRepository,
+ logger: logger.With("handler", "scan_node_sbom_handler"),
+ },
+ }
+}
+
+//nolint:funlen
+func (h *NodeScanSBOMHandler) Handle(ctx context.Context, message messaging.Message) error {
+ scanNodeSBOMMessage := &ScanNodeSBOMMessage{}
+ if err := json.Unmarshal(message.Data(), scanNodeSBOMMessage); err != nil {
+ return fmt.Errorf("failed to unmarshal scan job message: %w", err)
+ }
+
+ nodeScanJobName := scanNodeSBOMMessage.NodeScanJob.Name
+ nodeScanJobNamespace := scanNodeSBOMMessage.NodeScanJob.Namespace
+ nodeScanJobUID := scanNodeSBOMMessage.NodeScanJob.UID
+ nodeSBOMName := scanNodeSBOMMessage.NodeSBOM.Name
+ nodeSBOMNamespace := scanNodeSBOMMessage.NodeSBOM.Namespace
+
+ h.logger.InfoContext(ctx, "SBOM scan requested",
+ "sbom", nodeSBOMName,
+ "namespace", nodeSBOMNamespace,
+ )
+
+ scanJob := &v1alpha1.NodeScanJob{}
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: nodeScanJobName,
+ Namespace: nodeScanJobNamespace,
+ }, scanJob); err != nil {
+ if apierrors.IsNotFound(err) {
+ h.logger.ErrorContext(ctx, "ScanJob not found, stopping SBOM scan", "scanJob", nodeScanJobName, "namespace", nodeScanJobNamespace)
+ return nil
+ }
+
+ return fmt.Errorf("failed to get ScanJob: %w", err)
+ }
+
+ if string(scanJob.GetUID()) != nodeScanJobUID {
+ h.logger.InfoContext(ctx, "ScanJob not found, stopping SBOM generation (UID changed)", "scanjob", nodeScanJobName, "namespace", nodeScanJobNamespace,
+ "uid", nodeScanJobUID)
+ return nil
+ }
+
+ if scanJob.IsFailed() {
+ h.logger.InfoContext(ctx, "ScanJob is in failed state, stopping SBOM scan", "scanjob", nodeScanJobName, "namespace", nodeScanJobNamespace)
+ return nil
+ }
+
+ err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: nodeScanJobName,
+ Namespace: nodeScanJobNamespace,
+ }, scanJob); err != nil {
+ return fmt.Errorf("failed to get ScanJob: %w", err)
+ }
+
+ scanJob.MarkInProgress(v1alpha1.ReasonScanJobSBOMGenerationInProgress, "SBOM vulnerability scan in progress")
+ return h.k8sClient.Status().Update(ctx, scanJob)
+ })
+ if err != nil {
+ return fmt.Errorf("failed to update NodeScanJob status to SBOM generation in progress: %w", err)
+ }
+
+ sbom := &storagev1alpha1.NodeSBOM{}
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: nodeSBOMName,
+ Namespace: nodeSBOMNamespace,
+ }, sbom); err != nil {
+ if apierrors.IsNotFound(err) {
+ h.logger.ErrorContext(ctx, "SBOM not found, stopping SBOM scan", "sbom", nodeSBOMName, "namespace", nodeSBOMNamespace)
+ return nil
+ }
+
+ return fmt.Errorf("failed to get SBOM: %w", err)
+ }
+
+ results, summary, err := h.runTrivyScan(ctx, sbom.SPDX.Raw, message)
+ if err != nil {
+ return err
+ }
+
+ h.logger.InfoContext(ctx, "SBOM scanned",
+ "sbom", nodeSBOMName,
+ "namespace", nodeSBOMNamespace,
+ )
+
+ nodeVulnerabilityReport := &storagev1alpha1.NodeVulnerabilityReport{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: nodeSBOMName,
+ },
+ }
+
+ _, err = controllerutil.CreateOrUpdate(ctx, h.k8sClient, nodeVulnerabilityReport, func() error {
+ if err = controllerutil.SetControllerReference(sbom, nodeVulnerabilityReport, h.scheme); err != nil {
+ return fmt.Errorf("failed to set owner reference: %w", err)
+ }
+
+ nodeVulnerabilityReport.Labels = map[string]string{
+ v1alpha1.LabelNodeScanJobUIDKey: nodeScanJobUID,
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelPartOfKey: api.LabelPartOfValue,
+ }
+
+ nodeVulnerabilityReport.NodeMetadata = sbom.GetNodeMetadata()
+ nodeVulnerabilityReport.Report = storagev1alpha1.Report{
+ Summary: summary,
+ Results: results,
+ }
+
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("failed to create or update nodevulnerability report: %w", err)
+ }
+ h.logger.InfoContext(ctx, "Vulnerability report created or updated",
+ "sbom", nodeSBOMName,
+ "namespace", nodeSBOMNamespace,
+ )
+
+ err = retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: nodeScanJobName,
+ Namespace: nodeScanJobNamespace,
+ }, scanJob); err != nil {
+ return fmt.Errorf("failed to get ScanJob: %w", err)
+ }
+
+ scanJob.MarkComplete(v1alpha1.ReasonNodeScanJobScanned, "Node SBOM scanned successfully")
+ return h.k8sClient.Status().Update(ctx, scanJob)
+ })
+ if err != nil {
+ return fmt.Errorf("failed to update NodeScanJob status: %w", err)
+ }
+ h.logger.InfoContext(ctx, "SBOM scanned",
+ "sbom", nodeSBOMName,
+ "namespace", nodeSBOMNamespace,
+ )
+
+ return nil
+}
diff --git a/internal/handlers/nodescanjob_failure.go b/internal/handlers/nodescanjob_failure.go
new file mode 100644
index 000000000..5454e2a32
--- /dev/null
+++ b/internal/handlers/nodescanjob_failure.go
@@ -0,0 +1,70 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/client-go/util/retry"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ v1alpha1 "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/messaging"
+)
+
+// NodeScanJobFailureHandler handles failures for messages related to node scan jobs.
+type NodeScanJobFailureHandler struct {
+ k8sClient client.Client
+ logger *slog.Logger
+}
+
+// NewNodeScanJobFailureHandler creates a new instance of NodeScanJobFailureHandler.
+func NewNodeScanJobFailureHandler(
+ k8sClient client.Client,
+ logger *slog.Logger,
+) *NodeScanJobFailureHandler {
+ return &NodeScanJobFailureHandler{
+ k8sClient: k8sClient,
+ logger: logger.With("handler", "nodescanjob_failure_handler"),
+ }
+}
+
+// HandleFailure processes message failures and updates the associated NodeScanJob status.
+func (h *NodeScanJobFailureHandler) HandleFailure(ctx context.Context, message messaging.Message, errorMessage string) error {
+ nodeBaseMessage := &NodeBaseMessage{}
+ if err := json.Unmarshal(message.Data(), nodeBaseMessage); err != nil {
+ return fmt.Errorf("failed to unmarshal node base message: %w", err)
+ }
+ h.logger.DebugContext(ctx, "Handling NodeScanJob failure",
+ "nodescanjob", nodeBaseMessage.NodeScanJob.Name,
+ "error", errorMessage,
+ )
+
+ nodeScanJob := &v1alpha1.NodeScanJob{}
+
+ err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
+ if err := h.k8sClient.Get(ctx, client.ObjectKey{
+ Name: nodeBaseMessage.NodeScanJob.Name,
+ }, nodeScanJob); err != nil {
+ return fmt.Errorf("cannot get NodeScanJob %s: %w", nodeBaseMessage.NodeScanJob.Name, err)
+ }
+
+ nodeScanJob.MarkFailed(v1alpha1.ReasonScanJobInternalError, errorMessage)
+ return h.k8sClient.Status().Update(ctx, nodeScanJob)
+ })
+ if err != nil {
+ if apierrors.IsNotFound(err) {
+ h.logger.InfoContext(ctx, "NodeScanJob not found, skipping updating NodeScanJob status to failed", "nodescanjob", nodeBaseMessage.NodeScanJob.Name)
+ return nil
+ }
+ return fmt.Errorf("failed to update NodeScanJob %s status to failed: %w", nodeScanJob.Name, err)
+ }
+
+ h.logger.DebugContext(ctx, "NodeScanJob marked as failed",
+ "nodescanjob", nodeScanJob.Name,
+ "error_message", errorMessage,
+ )
+ return nil
+}
diff --git a/internal/handlers/scan_sbom.go b/internal/handlers/scan_sbom.go
deleted file mode 100644
index f4307dc95..000000000
--- a/internal/handlers/scan_sbom.go
+++ /dev/null
@@ -1,316 +0,0 @@
-package handlers
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "log/slog"
- "os"
- "path"
-
- "go.yaml.in/yaml/v3"
- _ "modernc.org/sqlite" // sqlite driver for RPM DB and Java DB
-
- apierrors "k8s.io/apimachinery/pkg/api/errors"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
- "k8s.io/apimachinery/pkg/runtime"
-
- vexrepo "github.com/aquasecurity/trivy/pkg/vex/repo"
- "sigs.k8s.io/controller-runtime/pkg/client"
- "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-
- trivyCommands "github.com/aquasecurity/trivy/pkg/commands"
- trivyTypes "github.com/aquasecurity/trivy/pkg/types"
- "github.com/kubewarden/sbomscanner/api"
- storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
- "github.com/kubewarden/sbomscanner/api/v1alpha1"
- trivyreport "github.com/kubewarden/sbomscanner/internal/handlers/trivyreport"
- "github.com/kubewarden/sbomscanner/internal/messaging"
-)
-
-const (
- // trivyVEXSubPath is the directory used by trivy to hold VEX repositories.
- trivyVEXSubPath = ".trivy/vex"
- // trivyVEXRepoFile is the file used by trivy to hold VEX repositories.
- trivyVEXRepoFile = "repository.yaml"
-)
-
-// ScanSBOMHandler is responsible for handling SBOM scan requests.
-type ScanSBOMHandler struct {
- k8sClient client.Client
- scheme *runtime.Scheme
- workDir string
- trivyDBRepository string
- trivyJavaDBRepository string
- logger *slog.Logger
-}
-
-// NewScanSBOMHandler creates a new instance of ScanSBOMHandler.
-func NewScanSBOMHandler(
- k8sClient client.Client,
- scheme *runtime.Scheme,
- workDir string,
- trivyDBRepository string,
- trivyJavaDBRepository string,
- logger *slog.Logger,
-) *ScanSBOMHandler {
- return &ScanSBOMHandler{
- k8sClient: k8sClient,
- scheme: scheme,
- workDir: workDir,
- trivyDBRepository: trivyDBRepository,
- trivyJavaDBRepository: trivyJavaDBRepository,
- logger: logger.With("handler", "scan_sbom_handler"),
- }
-}
-
-// Handle processes the ScanSBOMMessage and scans the specified SBOM resource for vulnerabilities.
-func (h *ScanSBOMHandler) Handle(ctx context.Context, message messaging.Message) error { //nolint:funlen,gocognit,gocyclo,cyclop // TODO: refactor this function in smaller ones
- scanSBOMMessage := &ScanSBOMMessage{}
- if err := json.Unmarshal(message.Data(), scanSBOMMessage); err != nil {
- return fmt.Errorf("failed to unmarshal scan job message: %w", err)
- }
-
- h.logger.InfoContext(ctx, "SBOM scan requested",
- "sbom", scanSBOMMessage.SBOM.Name,
- "namespace", scanSBOMMessage.SBOM.Namespace,
- )
-
- scanJob := &v1alpha1.ScanJob{}
- err := h.k8sClient.Get(ctx, client.ObjectKey{
- Name: scanSBOMMessage.ScanJob.Name,
- Namespace: scanSBOMMessage.ScanJob.Namespace,
- }, scanJob)
- if err != nil {
- if apierrors.IsNotFound(err) {
- // Stop processing if the scanjob is not found, since it might have been deleted.
- h.logger.ErrorContext(ctx, "ScanJob not found, stopping SBOM scan", "scanJob", scanSBOMMessage.ScanJob.Name, "namespace", scanSBOMMessage.ScanJob.Namespace)
- return nil
- }
- return fmt.Errorf("failed to get ScanJob: %w", err)
- }
- if string(scanJob.GetUID()) != scanSBOMMessage.ScanJob.UID {
- h.logger.InfoContext(ctx, "ScanJob not found, stopping SBOM generation (UID changed)", "scanjob", scanSBOMMessage.ScanJob.Name, "namespace", scanSBOMMessage.ScanJob.Namespace,
- "uid", scanSBOMMessage.ScanJob.UID)
- return nil
- }
-
- h.logger.DebugContext(ctx, "ScanJob found", "scanjob", scanJob)
-
- if scanJob.IsFailed() {
- h.logger.InfoContext(ctx, "ScanJob is in failed state, stopping SBOM scan", "scanjob", scanJob.Name, "namespace", scanJob.Namespace)
- return nil
- }
-
- // Retrieve the registry from the scan job annotations.
- registryData, ok := scanJob.Annotations[v1alpha1.AnnotationScanJobRegistryKey]
- if !ok {
- return fmt.Errorf("scan job %s/%s does not have a registry annotation", scanJob.Namespace, scanJob.Name)
- }
- registry := &v1alpha1.Registry{}
- if err = json.Unmarshal([]byte(registryData), registry); err != nil {
- return fmt.Errorf("cannot unmarshal registry data from scan job %s/%s: %w", scanJob.Namespace, scanJob.Name, err)
- }
-
- sbom := &storagev1alpha1.SBOM{}
- err = h.k8sClient.Get(ctx, client.ObjectKey{
- Name: scanSBOMMessage.SBOM.Name,
- Namespace: scanSBOMMessage.SBOM.Namespace,
- }, sbom)
- if err != nil {
- // Stop processing if the SBOM is not found, since it might have been deleted.
- if apierrors.IsNotFound(err) {
- h.logger.ErrorContext(ctx, "SBOM not found, stopping SBOM scan", "sbom", scanSBOMMessage.SBOM.Name, "namespace", scanSBOMMessage.SBOM.Namespace)
- return nil
- }
- return fmt.Errorf("failed to get SBOM: %w", err)
- }
-
- vexHubList := &v1alpha1.VEXHubList{}
- err = h.k8sClient.List(ctx, vexHubList, &client.ListOptions{})
- if err != nil {
- return fmt.Errorf("failed to list VEXHub: %w", err)
- }
-
- sbomFile, err := os.CreateTemp(h.workDir, "trivy.sbom.*.json")
- if err != nil {
- return fmt.Errorf("failed to create temporary SBOM file: %w", err)
- }
- defer func() {
- if err = sbomFile.Close(); err != nil {
- h.logger.ErrorContext(ctx, "failed to close temporary SBOM file", "error", err)
- }
-
- if err = os.Remove(sbomFile.Name()); err != nil {
- h.logger.ErrorContext(ctx, "failed to remove temporary SBOM file", "error", err)
- }
- }()
-
- _, err = sbomFile.Write(sbom.SPDX.Raw)
- if err != nil {
- return fmt.Errorf("failed to write SBOM file: %w", err)
- }
- reportFile, err := os.CreateTemp(h.workDir, "trivy.report.*.json")
- if err != nil {
- return fmt.Errorf("failed to create temporary report file: %w", err)
- }
- defer func() {
- if err = reportFile.Close(); err != nil {
- h.logger.ErrorContext(ctx, "failed to close temporary report file", "error", err)
- }
-
- if err = os.Remove(reportFile.Name()); err != nil {
- h.logger.ErrorContext(ctx, "failed to remove temporary report file", "error", err)
- }
- }()
-
- trivyArgs := []string{
- "sbom",
- "--skip-version-check",
- "--disable-telemetry",
- "--cache-dir", h.workDir,
- "--format", "json",
- "--db-repository", h.trivyDBRepository,
- "--java-db-repository", h.trivyJavaDBRepository,
- "--output", reportFile.Name(),
- }
- // Set XDG_DATA_HOME environment variable to /tmp because trivy expects
- // the repository file in that location and there is no way to change it
- // through input flags:
- // https://trivy.dev/v0.64/docs/supply-chain/vex/repo/#default-configuration
- // TODO(alegrey91): fix upstream
- trivyHome, err := os.MkdirTemp("/tmp", "trivy-")
- if err != nil {
- return fmt.Errorf("failed to create temporary trivy home: %w", err)
- }
- err = os.Setenv("XDG_DATA_HOME", trivyHome)
- if err != nil {
- return fmt.Errorf("failed to set XDG_DATA_HOME to %s: %w", trivyHome, err)
- }
-
- if len(vexHubList.Items) > 0 {
- trivyVEXPath := path.Join(trivyHome, trivyVEXSubPath)
- vexRepoPath := path.Join(trivyVEXPath, trivyVEXRepoFile)
- if err = h.setupVEXHubRepositories(vexHubList, trivyVEXPath, vexRepoPath); err != nil {
- return fmt.Errorf("failed to setup VEX Hub repositories: %w", err)
- }
- // Clean up the trivy home directory after each handler execution to
- // ensure VEX repositories are refreshed on every run.
- defer func() {
- h.logger.DebugContext(ctx, "Removing trivy home")
- if err = os.RemoveAll(trivyHome); err != nil {
- h.logger.ErrorContext(ctx, "failed to remove temporary trivy home", "error", err)
- }
- }()
-
- // We explicitly set the `--vex` option only when needed
- // (VEXHub resources are found). This is because trivy automatically
- // fills the repository file with aquasecurity VEX files, when
- // `--vex` is specificed.
- trivyArgs = append(trivyArgs, "--vex", "repo", "--show-suppressed")
- }
-
- app := trivyCommands.NewApp()
- // add SBOM file name at the end.
- trivyArgs = append(trivyArgs, sbomFile.Name())
- app.SetArgs(trivyArgs)
-
- if err = app.ExecuteContext(ctx); err != nil {
- return fmt.Errorf("failed to execute trivy: %w", err)
- }
-
- h.logger.InfoContext(ctx, "SBOM scanned",
- "sbom", scanSBOMMessage.SBOM.Name,
- "namespace", scanSBOMMessage.SBOM.Namespace,
- )
-
- if err = message.InProgress(); err != nil {
- return fmt.Errorf("failed to ack message as in progress: %w", err)
- }
-
- reportBytes, err := io.ReadAll(reportFile)
- if err != nil {
- return fmt.Errorf("failed to read SBOM output: %w", err)
- }
-
- reportOrig := trivyTypes.Report{}
- err = json.Unmarshal(reportBytes, &reportOrig)
- if err != nil {
- return fmt.Errorf("failed to unmarshal report: %w", err)
- }
-
- results, err := trivyreport.NewResultsFromTrivyReport(reportOrig)
- if err != nil {
- return fmt.Errorf("failed to convert from trivy results: %w", err)
- }
- summary := storagev1alpha1.NewSummaryFromResults(results)
-
- vulnerabilityReport := &storagev1alpha1.VulnerabilityReport{
- ObjectMeta: metav1.ObjectMeta{
- Name: sbom.Name,
- Namespace: sbom.Namespace,
- },
- }
- if err = controllerutil.SetControllerReference(sbom, vulnerabilityReport, h.scheme); err != nil {
- return fmt.Errorf("failed to set owner reference: %w", err)
- }
-
- _, err = controllerutil.CreateOrUpdate(ctx, h.k8sClient, vulnerabilityReport, func() error {
- vulnerabilityReport.Labels = map[string]string{
- v1alpha1.LabelScanJobUIDKey: string(scanJob.UID),
- api.LabelManagedByKey: api.LabelManagedByValue,
- api.LabelPartOfKey: api.LabelPartOfValue,
- }
- if registry.Labels[api.LabelWorkloadScanKey] == api.LabelWorkloadScanValue {
- vulnerabilityReport.Labels[api.LabelWorkloadScanKey] = api.LabelWorkloadScanValue
- }
-
- vulnerabilityReport.ImageMetadata = sbom.GetImageMetadata()
- vulnerabilityReport.Report = storagev1alpha1.Report{
- Summary: summary,
- Results: results,
- }
- return nil
- })
- if err != nil {
- return fmt.Errorf("failed to create or update vulnerability report: %w", err)
- }
-
- return nil
-}
-
-// setupVEXHubRepositories creates all the necessary files and directories
-// to use VEX Hub repositories.
-func (h *ScanSBOMHandler) setupVEXHubRepositories(vexHubList *v1alpha1.VEXHubList, trivyVEXPath, vexRepoPath string) error {
- config := vexrepo.Config{}
- var err error
- for _, repo := range vexHubList.Items {
- repo := vexrepo.Repository{
- Name: repo.Name,
- URL: repo.Spec.URL,
- Enabled: repo.Spec.Enabled,
- }
- config.Repositories = append(config.Repositories, repo)
- }
-
- var repositories []byte
- repositories, err = yaml.Marshal(config)
- if err != nil {
- return fmt.Errorf("failed to marshal struct: %w", err)
- }
-
- h.logger.Debug("Creating VEX repository directory", "vexhub", trivyVEXPath)
- err = os.MkdirAll(trivyVEXPath, 0o750)
- if err != nil {
- return fmt.Errorf("failed to create VEX configuration directory: %w", err)
- }
-
- h.logger.Debug("Creating VEX repository file", "vexhub", vexRepoPath)
- err = os.WriteFile(vexRepoPath, repositories, 0o600)
- if err != nil {
- return fmt.Errorf("failed to create VEX repository file: %w", err)
- }
-
- return nil
-}
diff --git a/internal/handlers/scan_sbom_helpers.go b/internal/handlers/scan_sbom_helpers.go
new file mode 100644
index 000000000..f58ba2dd0
--- /dev/null
+++ b/internal/handlers/scan_sbom_helpers.go
@@ -0,0 +1,181 @@
+package handlers
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log/slog"
+ "os"
+ "path"
+
+ "go.yaml.in/yaml/v3"
+ _ "modernc.org/sqlite" // sqlite driver for RPM DB and Java DB
+
+ "k8s.io/apimachinery/pkg/runtime"
+
+ vexrepo "github.com/aquasecurity/trivy/pkg/vex/repo"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ trivyCommands "github.com/aquasecurity/trivy/pkg/commands"
+ trivyTypes "github.com/aquasecurity/trivy/pkg/types"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ trivyreport "github.com/kubewarden/sbomscanner/internal/handlers/trivyreport"
+ "github.com/kubewarden/sbomscanner/internal/messaging"
+)
+
+const (
+ trivyVEXSubPath = ".trivy/vex"
+ trivyVEXRepoFile = "repository.yaml"
+)
+
+type scanSBOMBase struct {
+ k8sClient client.Client
+ scheme *runtime.Scheme
+ workDir string
+ trivyDBRepository string
+ trivyJavaDBRepository string
+ logger *slog.Logger
+}
+
+// runTrivyScan executes a trivy scan on the given SPDX data and returns the parsed results and summary.
+func (b *scanSBOMBase) runTrivyScan(ctx context.Context, rawSPDX []byte, message messaging.Message) ([]storagev1alpha1.Result, storagev1alpha1.Summary, error) { //nolint:funlen,gocognit // trivy setup requires sequential steps
+ vexHubList := &v1alpha1.VEXHubList{}
+ if err := b.k8sClient.List(ctx, vexHubList, &client.ListOptions{}); err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to list VEXHub: %w", err)
+ }
+
+ sbomFile, err := os.CreateTemp(b.workDir, "trivy.sbom.*.json")
+ if err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to create temporary SBOM file: %w", err)
+ }
+ defer func() {
+ if err = sbomFile.Close(); err != nil {
+ b.logger.ErrorContext(ctx, "failed to close temporary SBOM file", "error", err)
+ }
+
+ if err = os.Remove(sbomFile.Name()); err != nil {
+ b.logger.ErrorContext(ctx, "failed to remove temporary SBOM file", "error", err)
+ }
+ }()
+
+ if _, err = sbomFile.Write(rawSPDX); err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to write SBOM file: %w", err)
+ }
+
+ reportFile, err := os.CreateTemp(b.workDir, "trivy.report.*.json")
+ if err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to create temporary report file: %w", err)
+ }
+ defer func() {
+ if err = reportFile.Close(); err != nil {
+ b.logger.ErrorContext(ctx, "failed to close temporary report file", "error", err)
+ }
+
+ if err = os.Remove(reportFile.Name()); err != nil {
+ b.logger.ErrorContext(ctx, "failed to remove temporary report file", "error", err)
+ }
+ }()
+
+ trivyArgs := []string{
+ "sbom",
+ //nolint:goconst // These are specific trivy command arguments, not constant values used elsewhere
+ "--skip-version-check",
+ //nolint:goconst // These are specific trivy command arguments, not constant values used elsewhere
+ "--disable-telemetry",
+ //nolint:goconst // These are specific trivy command arguments, not constant values used elsewhere
+ "--cache-dir", b.workDir,
+ //nolint:goconst // These are specific trivy command arguments, not constant values used elsewhere
+ "--format", "json",
+ "--db-repository", b.trivyDBRepository,
+ //nolint:goconst // These are specific trivy command arguments, not constant values used elsewhere
+ "--java-db-repository", b.trivyJavaDBRepository,
+ //nolint:goconst // These are specific trivy command arguments, not constant values used elsewhere
+ "--output", reportFile.Name(),
+ }
+
+ trivyHome, err := os.MkdirTemp("/tmp", "trivy-")
+ if err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to create temporary trivy home: %w", err)
+ }
+ if err = os.Setenv("XDG_DATA_HOME", trivyHome); err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to set XDG_DATA_HOME to %s: %w", trivyHome, err)
+ }
+
+ if len(vexHubList.Items) > 0 {
+ trivyVEXPath := path.Join(trivyHome, trivyVEXSubPath)
+ vexRepoPath := path.Join(trivyVEXPath, trivyVEXRepoFile)
+ if err = b.setupVEXHubRepositories(vexHubList, trivyVEXPath, vexRepoPath); err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to setup VEX Hub repositories: %w", err)
+ }
+ defer func() {
+ b.logger.DebugContext(ctx, "Removing trivy home")
+ if err = os.RemoveAll(trivyHome); err != nil {
+ b.logger.ErrorContext(ctx, "failed to remove temporary trivy home", "error", err)
+ }
+ }()
+
+ trivyArgs = append(trivyArgs, "--vex", "repo", "--show-suppressed")
+ }
+
+ app := trivyCommands.NewApp()
+ trivyArgs = append(trivyArgs, sbomFile.Name())
+ app.SetArgs(trivyArgs)
+
+ if err = app.ExecuteContext(ctx); err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to execute trivy: %w", err)
+ }
+
+ if err = message.InProgress(); err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to ack message as in progress: %w", err)
+ }
+
+ reportBytes, err := io.ReadAll(reportFile)
+ if err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to read SBOM output: %w", err)
+ }
+
+ reportOrig := trivyTypes.Report{}
+ if err = json.Unmarshal(reportBytes, &reportOrig); err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to unmarshal report: %w", err)
+ }
+
+ results, err := trivyreport.NewResultsFromTrivyReport(reportOrig)
+ if err != nil {
+ return nil, storagev1alpha1.Summary{}, fmt.Errorf("failed to convert from trivy results: %w", err)
+ }
+
+ summary := storagev1alpha1.NewSummaryFromResults(results)
+
+ return results, summary, nil
+}
+
+func (b *scanSBOMBase) setupVEXHubRepositories(vexHubList *v1alpha1.VEXHubList, trivyVEXPath, vexRepoPath string) error {
+ config := vexrepo.Config{}
+ for _, repo := range vexHubList.Items {
+ repo := vexrepo.Repository{
+ Name: repo.Name,
+ URL: repo.Spec.URL,
+ Enabled: repo.Spec.Enabled,
+ }
+ config.Repositories = append(config.Repositories, repo)
+ }
+
+ repositories, err := yaml.Marshal(config)
+ if err != nil {
+ return fmt.Errorf("failed to marshal struct: %w", err)
+ }
+
+ b.logger.Debug("Creating VEX repository directory", "vexhub", trivyVEXPath)
+ if err = os.MkdirAll(trivyVEXPath, 0o750); err != nil {
+ return fmt.Errorf("failed to create VEX configuration directory: %w", err)
+ }
+
+ b.logger.Debug("Creating VEX repository file", "vexhub", vexRepoPath)
+ if err = os.WriteFile(vexRepoPath, repositories, 0o600); err != nil {
+ return fmt.Errorf("failed to create VEX repository file: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/handlers/scan_sbom_test.go b/internal/handlers/scan_sbom_test.go
index 060b193c2..119fffe76 100644
--- a/internal/handlers/scan_sbom_test.go
+++ b/internal/handlers/scan_sbom_test.go
@@ -277,7 +277,7 @@ func TestScanSBOMHandler_Handle_StopProcessing(t *testing.T) {
differentUIDScanJob.UID = "test-scanjob-different-uid"
failedScanJob := scanJob.DeepCopy()
- failedScanJob.MarkFailed(v1alpha1.ReasonInternalError, "kaboom")
+ failedScanJob.MarkFailed(v1alpha1.ReasonScanJobInternalError, "kaboom")
tests := []struct {
name string
diff --git a/internal/handlers/scanjob_failure.go b/internal/handlers/scanjob_failure.go
index 681e99c2a..eb1160195 100644
--- a/internal/handlers/scanjob_failure.go
+++ b/internal/handlers/scanjob_failure.go
@@ -55,7 +55,7 @@ func (h *ScanJobFailureHandler) HandleFailure(ctx context.Context, message messa
return fmt.Errorf("cannot get scanjob %s/%s: %w", baseMessage.ScanJob.Namespace, baseMessage.ScanJob.Name, err)
}
- scanJob.MarkFailed(sbombasticv1alpha1.ReasonInternalError, errorMessage)
+ scanJob.MarkFailed(sbombasticv1alpha1.ReasonScanJobInternalError, errorMessage)
return h.k8sClient.Status().Update(ctx, scanJob)
})
if err != nil {
diff --git a/internal/handlers/scanjob_failure_test.go b/internal/handlers/scanjob_failure_test.go
index 3615cc71a..66c1e739b 100644
--- a/internal/handlers/scanjob_failure_test.go
+++ b/internal/handlers/scanjob_failure_test.go
@@ -66,8 +66,8 @@ func TestScanJobFailureHandler_HandleFailure(t *testing.T) {
require.NoError(t, err)
assert.True(t, updatedScanJob.IsFailed())
- failedCondition := meta.FindStatusCondition(updatedScanJob.Status.Conditions, string(sbombasticv1alpha1.ConditionTypeFailed))
+ failedCondition := meta.FindStatusCondition(updatedScanJob.Status.Conditions, string(sbombasticv1alpha1.ConditionScanJobTypeFailed))
require.NotNil(t, failedCondition)
- assert.Equal(t, sbombasticv1alpha1.ReasonInternalError, failedCondition.Reason)
+ assert.Equal(t, sbombasticv1alpha1.ReasonScanJobInternalError, failedCondition.Reason)
assert.Equal(t, errorMessage, failedCondition.Message)
}
diff --git a/internal/messaging/subscriber.go b/internal/messaging/subscriber.go
index c77530d1c..d87f24c93 100644
--- a/internal/messaging/subscriber.go
+++ b/internal/messaging/subscriber.go
@@ -26,13 +26,13 @@ type RetryConfig struct {
MaxAttempts int
}
-// HandlerRegistry is a map that associates subjects with their respective handlers.
-type HandlerRegistry map[string]Handler
+// HandlerScan is a map that associates subjects with their respective handlers.
+type HandlerScan map[string]Handler
// NatsSubscriber is an implementation of a message subscriber that uses NATS JetStream to receive messages.
type NatsSubscriber struct {
cons jetstream.Consumer
- handlers HandlerRegistry
+ handlers HandlerScan
failureHandler FailureHandler
retryConfig *RetryConfig
logger *slog.Logger
@@ -42,7 +42,7 @@ type NatsSubscriber struct {
func NewNatsSubscriber(ctx context.Context,
nc *nats.Conn,
durable string,
- handlers HandlerRegistry,
+ handlers HandlerScan,
failureHandler FailureHandler,
retryConfig *RetryConfig,
logger *slog.Logger,
diff --git a/internal/messaging/subscriber_test.go b/internal/messaging/subscriber_test.go
index 8e7740de2..cc01563ae 100644
--- a/internal/messaging/subscriber_test.go
+++ b/internal/messaging/subscriber_test.go
@@ -68,7 +68,7 @@ func TestSubscriber_Run(t *testing.T) {
}
testHandler := &testHandler{handleFunc: handleFunc}
- handlers := HandlerRegistry{
+ handlers := HandlerScan{
testSubscriberSubject: testHandler,
}
subscriber, err := NewNatsSubscriber(t.Context(), nc, "test-durable", handlers, nil, nil, slog.Default())
@@ -129,7 +129,7 @@ func TestSubscriber_Run_WithRetry(t *testing.T) {
}
testHandler := &testHandler{handleFunc: handleFunc}
- handlers := HandlerRegistry{
+ handlers := HandlerScan{
testSubscriberSubject: testHandler,
}
retryConfig := &RetryConfig{
@@ -202,7 +202,7 @@ func TestSubscriber_Run_WithMaxRetriesExceeded(t *testing.T) {
testHandler := &testHandler{handleFunc: handleFunc}
testFailureHandler := &testFailureHandler{handleFailureFunc: failureHandleFunc}
- handlers := HandlerRegistry{
+ handlers := HandlerScan{
testSubscriberSubject: testHandler,
}
retryConfig := &RetryConfig{
@@ -279,7 +279,7 @@ func TestSubscriber_handleMessage(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- handlers := HandlerRegistry{}
+ handlers := HandlerScan{}
handlers[testSubscriberSubject] = &testHandler{
handleFunc: test.handleFunc,
}
diff --git a/internal/skippatterns/doc.go b/internal/skippatterns/doc.go
new file mode 100644
index 000000000..f25853356
--- /dev/null
+++ b/internal/skippatterns/doc.go
@@ -0,0 +1,8 @@
+// Package skippatterns classifies gitignore-style skip patterns into
+// directory and file categories for use with trivy's --skip-dirs and
+// --skip-files flags.
+//
+// Patterns ending with "/" are treated as directories; all others are
+// treated as files. Glob syntax (e.g. "**/vendor/", "*.min.js") is
+// passed through as-is since trivy handles expansion natively.
+package skippatterns
diff --git a/internal/skippatterns/skippatterns.go b/internal/skippatterns/skippatterns.go
new file mode 100644
index 000000000..6afad0f40
--- /dev/null
+++ b/internal/skippatterns/skippatterns.go
@@ -0,0 +1,32 @@
+package skippatterns
+
+import "strings"
+
+// ParseResult holds the classified skip patterns for trivy flags.
+type ParseResult struct {
+ SkipDirs []string
+ SkipFiles []string
+}
+
+// Parse classifies gitignore-style patterns into directory and file patterns.
+// Patterns ending with "/" are directory patterns (trivy --skip-dirs).
+// All other patterns are file patterns (trivy --skip-files).
+// The trailing "/" is stripped from directory patterns.
+func Parse(patterns []string) ParseResult {
+ var result ParseResult
+
+ for _, p := range patterns {
+ p = strings.TrimSpace(p)
+ if p == "" {
+ continue
+ }
+
+ if dir, ok := strings.CutSuffix(p, "/"); ok {
+ result.SkipDirs = append(result.SkipDirs, dir)
+ } else {
+ result.SkipFiles = append(result.SkipFiles, p)
+ }
+ }
+
+ return result
+}
diff --git a/internal/skippatterns/skippatterns_test.go b/internal/skippatterns/skippatterns_test.go
new file mode 100644
index 000000000..3bf0dc81a
--- /dev/null
+++ b/internal/skippatterns/skippatterns_test.go
@@ -0,0 +1,89 @@
+package skippatterns
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParse(t *testing.T) {
+ tests := []struct {
+ name string
+ patterns []string
+ wantDirs []string
+ wantFiles []string
+ }{
+ {
+ name: "nil input",
+ patterns: nil,
+ },
+ {
+ name: "empty slice",
+ patterns: []string{},
+ },
+ {
+ name: "directory pattern",
+ patterns: []string{"node_modules/"},
+ wantDirs: []string{"node_modules"},
+ },
+ {
+ name: "file pattern",
+ patterns: []string{"package-lock.json"},
+ wantFiles: []string{"package-lock.json"},
+ },
+ {
+ name: "mixed patterns",
+ patterns: []string{"node_modules/", "*.min.js", ".git/", "package-lock.json"},
+ wantDirs: []string{"node_modules", ".git"},
+ wantFiles: []string{"*.min.js", "package-lock.json"},
+ },
+ {
+ name: "glob directory pattern",
+ patterns: []string{"**/vendor/"},
+ wantDirs: []string{"**/vendor"},
+ },
+ {
+ name: "glob file pattern",
+ patterns: []string{"*.min.js"},
+ wantFiles: []string{"*.min.js"},
+ },
+ {
+ name: "whitespace-only patterns are skipped",
+ patterns: []string{" ", "", "\t"},
+ },
+ {
+ name: "nested directory path",
+ patterns: []string{"foo/bar/"},
+ wantDirs: []string{"foo/bar"},
+ },
+ {
+ name: "absolute path without trailing slash is file",
+ patterns: []string{"/tmp"},
+ wantFiles: []string{"/tmp"},
+ },
+ {
+ name: "absolute path with trailing slash is dir",
+ patterns: []string{"/tmp/"},
+ wantDirs: []string{"/tmp"},
+ },
+ {
+ name: "pattern with leading double star",
+ patterns: []string{"**/node_modules/"},
+ wantDirs: []string{"**/node_modules"},
+ },
+ {
+ name: "whitespace around pattern is trimmed",
+ patterns: []string{" node_modules/ ", " *.log "},
+ wantDirs: []string{"node_modules"},
+ wantFiles: []string{"*.log"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := Parse(tt.patterns)
+ assert.Equal(t, tt.wantDirs, result.SkipDirs)
+ assert.Equal(t, tt.wantFiles, result.SkipFiles)
+ })
+ }
+}
diff --git a/internal/storage/matcher.go b/internal/storage/matcher.go
index 9536865b0..5721f22cc 100644
--- a/internal/storage/matcher.go
+++ b/internal/storage/matcher.go
@@ -35,6 +35,7 @@ func getAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
}
selectableMetadata := fields.Set{
+ //nolint:goconst // These are user-friendly field names, not constant values used elsewhere
"metadata.name": objMeta.GetName(),
"metadata.namespace": objMeta.GetNamespace(),
}
diff --git a/internal/storage/migrations.go b/internal/storage/migrations.go
index 1b96d7dbe..eae698086 100644
--- a/internal/storage/migrations.go
+++ b/internal/storage/migrations.go
@@ -32,6 +32,12 @@ func RunMigrations(ctx context.Context, db *pgxpool.Pool) error {
if _, err := db.Exec(ctx, createWorkloadScanReportTableSQL); err != nil {
return fmt.Errorf("creating workload scan report table: %w", err)
}
+ if _, err := db.Exec(ctx, createNodeSBOMTableSQL); err != nil {
+ return fmt.Errorf("creating node sbom table: %w", err)
+ }
+ if _, err := db.Exec(ctx, createNodeVulnerabilityReportTableSQL); err != nil {
+ return fmt.Errorf("creating node vulnerability report table: %w", err)
+ }
return nil
}
diff --git a/internal/storage/node_sbom_store.go b/internal/storage/node_sbom_store.go
new file mode 100644
index 000000000..5c1ddfb29
--- /dev/null
+++ b/internal/storage/node_sbom_store.go
@@ -0,0 +1,169 @@
+package storage
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/nats-io/nats.go"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/fields"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/watch"
+ "k8s.io/apiserver/pkg/registry/generic"
+ "k8s.io/apiserver/pkg/registry/generic/registry"
+ apistorage "k8s.io/apiserver/pkg/storage"
+
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/storage/repository"
+)
+
+const (
+ nodeSBOMResourceSingularName = "nodesbom"
+ nodeSBOMResourcePluralName = "nodesboms"
+)
+
+const createNodeSBOMTableSQL = `
+CREATE TABLE IF NOT EXISTS nodesboms (
+ name VARCHAR(253) NOT NULL,
+ object JSONB NOT NULL,
+ PRIMARY KEY (name)
+);
+
+ALTER TABLE nodesboms ADD COLUMN IF NOT EXISTS id BIGSERIAL;
+CREATE INDEX IF NOT EXISTS idx_nodesboms_id ON nodesboms(id);
+`
+
+// NewNodeSBOMStore returns a store registry that will work against API services.
+func NewNodeSBOMStore(
+ scheme *runtime.Scheme,
+ optsGetter generic.RESTOptionsGetter,
+ db *pgxpool.Pool,
+ nc *nats.Conn,
+ logger *slog.Logger,
+) (*registry.Store, []Watcher, error) {
+ strategy := newNodeSBOMStrategy(scheme)
+ newFunc := func() runtime.Object { return &storagev1alpha1.NodeSBOM{} }
+ newListFunc := func() runtime.Object { return &storagev1alpha1.NodeSBOMList{} }
+
+ watchBroadcaster := watch.NewBroadcaster(1000, watch.WaitIfChannelFull)
+ natsBroadcaster := newNatsBroadcaster(nc, nodeSBOMResourcePluralName, watchBroadcaster, TransformStripNodeSBOM, logger)
+
+ repo := repository.NewClusterScopedObjectRepository(nodeSBOMResourcePluralName, newFunc)
+
+ store := &store{
+ db: db,
+ repository: repo,
+ broadcaster: natsBroadcaster,
+ newFunc: newFunc,
+ newListFunc: newListFunc,
+ logger: logger.With("store", nodeSBOMResourceSingularName),
+ clusterScoped: true,
+ }
+
+ natsWatcher := newNatsWatcher(nc, nodeSBOMResourcePluralName, watchBroadcaster, store, logger)
+
+ registryStore := ®istry.Store{
+ NewFunc: newFunc,
+ NewListFunc: newListFunc,
+ PredicateFunc: nodeSBOMMatcher,
+ DefaultQualifiedResource: storagev1alpha1.Resource(nodeSBOMResourcePluralName),
+ SingularQualifiedResource: storagev1alpha1.Resource(nodeSBOMResourceSingularName),
+ Storage: registry.DryRunnableStorage{
+ Storage: store,
+ },
+ CreateStrategy: strategy,
+ UpdateStrategy: strategy,
+ DeleteStrategy: strategy,
+ TableConvertor: &nodeSBOMTableConvertor{},
+ }
+
+ options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: getNodeSBOMAttrs}
+ if err := registryStore.CompleteWithOptions(options); err != nil {
+ return nil, nil, fmt.Errorf("unable to complete store with options: %w", err)
+ }
+
+ return registryStore, []Watcher{natsWatcher}, nil
+}
+
+// nodeSBOMMatcher returns a storage.SelectionPredicate that matches the given label and field selectors.
+func nodeSBOMMatcher(label labels.Selector, field fields.Selector) apistorage.SelectionPredicate {
+ return apistorage.SelectionPredicate{
+ Label: label,
+ Field: field,
+ GetAttrs: getNodeSBOMAttrs,
+ }
+}
+
+// getNodeSBOMAttrs returns labels and fields that can be used in a selection.
+func getNodeSBOMAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
+ objMeta, err := meta.Accessor(obj)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get metadata: %w", err)
+ }
+
+ nodeMetadataAccessor, ok := obj.(storagev1alpha1.NodeMetadataAccessor)
+ if !ok {
+ return nil, nil, errors.New("object does not implement NodeMetadataAccessor")
+ }
+
+ selectableMetadata := fields.Set{
+ //nolint:goconst // These are user-friendly field names, not constant values used elsewhere
+ "metadata.name": objMeta.GetName(),
+ }
+
+ selectableFields := fields.Set{
+ "nodeMetadata.platform": nodeMetadataAccessor.GetNodeMetadata().Platform,
+ }
+
+ return labels.Set(objMeta.GetLabels()), generic.MergeFieldsSets(selectableMetadata, selectableFields), nil
+}
+
+type nodeSBOMTableConvertor struct{}
+
+func (c *nodeSBOMTableConvertor) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) {
+ table := &metav1.Table{
+ ColumnDefinitions: nodeMetadataTableColumns(),
+ Rows: []metav1.TableRow{},
+ }
+
+ var nodeSBOMs []storagev1alpha1.NodeSBOM
+ switch t := obj.(type) {
+ case *storagev1alpha1.NodeSBOMList:
+ nodeSBOMs = t.Items
+ case *storagev1alpha1.NodeSBOM:
+ nodeSBOMs = []storagev1alpha1.NodeSBOM{*t}
+ default:
+ return nil, fmt.Errorf("unexpected type %T", obj)
+ }
+
+ for _, nodeSBOM := range nodeSBOMs {
+ row := metav1.TableRow{
+ Object: runtime.RawExtension{Object: &nodeSBOM},
+ Cells: nodeMetadataTableRowCells(nodeSBOM.Name, &nodeSBOM),
+ }
+ table.Rows = append(table.Rows, row)
+ }
+
+ return table, nil
+}
+
+func nodeMetadataTableColumns() []metav1.TableColumnDefinition {
+ return []metav1.TableColumnDefinition{
+ //nolint:goconst // These are user-friendly column names, not constant values used elsewhere
+ {Name: "Name", Type: "string", Description: "Name"},
+ {Name: "Platform", Type: "string", Description: "Node platform"},
+ }
+}
+
+func nodeMetadataTableRowCells(name string, obj storagev1alpha1.NodeMetadataAccessor) []any {
+ nodeMeta := obj.GetNodeMetadata()
+ return []any{
+ name,
+ nodeMeta.Platform,
+ }
+}
diff --git a/internal/storage/node_sbom_strategy.go b/internal/storage/node_sbom_strategy.go
new file mode 100644
index 000000000..db9f77428
--- /dev/null
+++ b/internal/storage/node_sbom_strategy.go
@@ -0,0 +1,58 @@
+package storage
+
+import (
+ "context"
+
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/util/validation/field"
+ "k8s.io/apiserver/pkg/storage/names"
+)
+
+// newNodeSBOMStrategy creates and returns a nodeSBOMStrategy instance
+func newNodeSBOMStrategy(typer runtime.ObjectTyper) nodeSBOMStrategy {
+ return nodeSBOMStrategy{typer, names.SimpleNameGenerator}
+}
+
+type nodeSBOMStrategy struct {
+ runtime.ObjectTyper
+ names.NameGenerator
+}
+
+func (nodeSBOMStrategy) NamespaceScoped() bool {
+ return false
+}
+
+func (nodeSBOMStrategy) PrepareForCreate(_ context.Context, _ runtime.Object) {
+}
+
+func (nodeSBOMStrategy) PrepareForUpdate(_ context.Context, _, _ runtime.Object) {
+}
+
+func (nodeSBOMStrategy) Validate(_ context.Context, _ runtime.Object) field.ErrorList {
+ return field.ErrorList{}
+}
+
+// WarningsOnCreate returns warnings for the creation of the given object.
+func (nodeSBOMStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string {
+ return nil
+}
+
+func (nodeSBOMStrategy) AllowCreateOnUpdate() bool {
+ return false
+}
+
+func (nodeSBOMStrategy) AllowUnconditionalUpdate() bool {
+ return false
+}
+
+func (nodeSBOMStrategy) Canonicalize(_ runtime.Object) {
+}
+
+func (nodeSBOMStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList {
+ return field.ErrorList{}
+}
+
+// WarningsOnUpdate returns warnings for the given update.
+func (nodeSBOMStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string {
+ return nil
+}
diff --git a/internal/storage/nodevulnerabilityreport_store.go b/internal/storage/nodevulnerabilityreport_store.go
new file mode 100644
index 000000000..1fc8521aa
--- /dev/null
+++ b/internal/storage/nodevulnerabilityreport_store.go
@@ -0,0 +1,163 @@
+package storage
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/jackc/pgx/v5/pgxpool"
+ "github.com/nats-io/nats.go"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/fields"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/watch"
+ "k8s.io/apiserver/pkg/registry/generic"
+ "k8s.io/apiserver/pkg/registry/generic/registry"
+ apistorage "k8s.io/apiserver/pkg/storage"
+
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/storage/repository"
+)
+
+const (
+ nodeVulnerabilityReportResourceSingularName = "nodevulnerabilityreport"
+ nodeVulnerabilityReportResourcePluralName = "nodevulnerabilityreports"
+)
+
+const createNodeVulnerabilityReportTableSQL = `
+CREATE TABLE IF NOT EXISTS nodevulnerabilityreports (
+ name VARCHAR(253) NOT NULL,
+ object JSONB NOT NULL,
+ PRIMARY KEY (name)
+);
+
+ALTER TABLE nodevulnerabilityreports ADD COLUMN IF NOT EXISTS id BIGSERIAL;
+CREATE INDEX IF NOT EXISTS idx_nodevulnerabilityreports_id ON nodevulnerabilityreports(id);
+`
+
+// NewNodeVulnerabilityReportStore returns a store registry that will work against API services.
+func NewNodeVulnerabilityReportStore(
+ scheme *runtime.Scheme,
+ optsGetter generic.RESTOptionsGetter,
+ db *pgxpool.Pool,
+ nc *nats.Conn,
+ logger *slog.Logger,
+) (*registry.Store, []Watcher, error) {
+ strategy := newNodeVulnerabilityReportStrategy(scheme)
+ newFunc := func() runtime.Object { return &storagev1alpha1.NodeVulnerabilityReport{} }
+ newListFunc := func() runtime.Object { return &storagev1alpha1.NodeVulnerabilityReportList{} }
+
+ watchBroadcaster := watch.NewBroadcaster(1000, watch.WaitIfChannelFull)
+ natsBroadcaster := newNatsBroadcaster(nc, nodeVulnerabilityReportResourcePluralName, watchBroadcaster, TransformStripNodeVulnerabilityReport, logger)
+
+ repo := repository.NewClusterScopedObjectRepository(nodeVulnerabilityReportResourcePluralName, newFunc)
+
+ store := &store{
+ db: db,
+ repository: repo,
+ broadcaster: natsBroadcaster,
+ newFunc: newFunc,
+ newListFunc: newListFunc,
+ logger: logger.With("store", nodeVulnerabilityReportResourceSingularName),
+ clusterScoped: true,
+ }
+
+ natsWatcher := newNatsWatcher(nc, nodeVulnerabilityReportResourcePluralName, watchBroadcaster, store, logger)
+
+ registryStore := ®istry.Store{
+ NewFunc: newFunc,
+ NewListFunc: newListFunc,
+ PredicateFunc: nodeVulnerabilityReportMatcher,
+ DefaultQualifiedResource: storagev1alpha1.Resource(nodeVulnerabilityReportResourcePluralName),
+ SingularQualifiedResource: storagev1alpha1.Resource(nodeVulnerabilityReportResourceSingularName),
+ Storage: registry.DryRunnableStorage{
+ Storage: store,
+ },
+ CreateStrategy: strategy,
+ UpdateStrategy: strategy,
+ DeleteStrategy: strategy,
+ TableConvertor: &nodeVulnerabilityReportTableConvertor{},
+ }
+
+ options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: getNodeVulnerabilityReportAttrs}
+ if err := registryStore.CompleteWithOptions(options); err != nil {
+ return nil, nil, fmt.Errorf("unable to complete store with options: %w", err)
+ }
+
+ return registryStore, []Watcher{natsWatcher}, nil
+}
+
+// nodeVulnerabilityReportMatcher returns a storage.SelectionPredicate that matches the given label and field selectors.
+func nodeVulnerabilityReportMatcher(label labels.Selector, field fields.Selector) apistorage.SelectionPredicate {
+ return apistorage.SelectionPredicate{
+ Label: label,
+ Field: field,
+ GetAttrs: getNodeVulnerabilityReportAttrs,
+ }
+}
+
+// getNodeVulnerabilityReportAttrs returns labels and fields that can be used in a selection.
+func getNodeVulnerabilityReportAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
+ objMeta, err := meta.Accessor(obj)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to get metadata: %w", err)
+ }
+
+ nodeMetadataAccessor, ok := obj.(storagev1alpha1.NodeMetadataAccessor)
+ if !ok {
+ return nil, nil, errors.New("object does not implement NodeMetadataAccessor")
+ }
+
+ selectableMetadata := fields.Set{
+ //nolint:goconst // These are user-friendly field names, not constant values used elsewhere
+ "metadata.name": objMeta.GetName(),
+ }
+
+ selectableFields := fields.Set{
+ "nodeMetadata.platform": nodeMetadataAccessor.GetNodeMetadata().Platform,
+ }
+
+ return labels.Set(objMeta.GetLabels()), generic.MergeFieldsSets(selectableMetadata, selectableFields), nil
+}
+
+type nodeVulnerabilityReportTableConvertor struct{}
+
+func (c *nodeVulnerabilityReportTableConvertor) ConvertToTable(_ context.Context, obj runtime.Object, _ runtime.Object) (*metav1.Table, error) {
+ columns := append(
+ nodeMetadataTableColumns(),
+ //nolint:goconst // These are user-friendly column names, not constant values used elsewhere
+ metav1.TableColumnDefinition{Name: "Vulnerabilities", Type: "string", Description: "Vulnerabilities"},
+ )
+
+ table := &metav1.Table{
+ ColumnDefinitions: columns,
+ Rows: []metav1.TableRow{},
+ }
+
+ var nodeVulnerabilityReports []storagev1alpha1.NodeVulnerabilityReport
+ switch t := obj.(type) {
+ case *storagev1alpha1.NodeVulnerabilityReportList:
+ nodeVulnerabilityReports = t.Items
+ case *storagev1alpha1.NodeVulnerabilityReport:
+ nodeVulnerabilityReports = []storagev1alpha1.NodeVulnerabilityReport{*t}
+ default:
+ return nil, fmt.Errorf("unexpected type %T", obj)
+ }
+
+ for _, nodeVulnerabilityReport := range nodeVulnerabilityReports {
+ cells := append(
+ nodeMetadataTableRowCells(nodeVulnerabilityReport.Name, &nodeVulnerabilityReport),
+ computeVulnerabilities(nodeVulnerabilityReport.Report.Summary),
+ )
+ row := metav1.TableRow{
+ Object: runtime.RawExtension{Object: &nodeVulnerabilityReport},
+ Cells: cells,
+ }
+ table.Rows = append(table.Rows, row)
+ }
+
+ return table, nil
+}
diff --git a/internal/storage/nodevulnerabilityreport_strategy.go b/internal/storage/nodevulnerabilityreport_strategy.go
new file mode 100644
index 000000000..bf7ab97fa
--- /dev/null
+++ b/internal/storage/nodevulnerabilityreport_strategy.go
@@ -0,0 +1,58 @@
+package storage
+
+import (
+ "context"
+
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/util/validation/field"
+ "k8s.io/apiserver/pkg/storage/names"
+)
+
+// newNodeVulnerabilityReportStrategy creates and returns a nodeVulnerabilityReportStrategy instance
+func newNodeVulnerabilityReportStrategy(typer runtime.ObjectTyper) nodeVulnerabilityReportStrategy {
+ return nodeVulnerabilityReportStrategy{typer, names.SimpleNameGenerator}
+}
+
+type nodeVulnerabilityReportStrategy struct {
+ runtime.ObjectTyper
+ names.NameGenerator
+}
+
+func (nodeVulnerabilityReportStrategy) NamespaceScoped() bool {
+ return false
+}
+
+func (nodeVulnerabilityReportStrategy) PrepareForCreate(_ context.Context, _ runtime.Object) {
+}
+
+func (nodeVulnerabilityReportStrategy) PrepareForUpdate(_ context.Context, _, _ runtime.Object) {
+}
+
+func (nodeVulnerabilityReportStrategy) Validate(_ context.Context, _ runtime.Object) field.ErrorList {
+ return field.ErrorList{}
+}
+
+// WarningsOnCreate returns warnings for the creation of the given object.
+func (nodeVulnerabilityReportStrategy) WarningsOnCreate(_ context.Context, _ runtime.Object) []string {
+ return nil
+}
+
+func (nodeVulnerabilityReportStrategy) AllowCreateOnUpdate() bool {
+ return false
+}
+
+func (nodeVulnerabilityReportStrategy) AllowUnconditionalUpdate() bool {
+ return false
+}
+
+func (nodeVulnerabilityReportStrategy) Canonicalize(_ runtime.Object) {
+}
+
+func (nodeVulnerabilityReportStrategy) ValidateUpdate(_ context.Context, _, _ runtime.Object) field.ErrorList {
+ return field.ErrorList{}
+}
+
+// WarningsOnUpdate returns warnings for the given update.
+func (nodeVulnerabilityReportStrategy) WarningsOnUpdate(_ context.Context, _, _ runtime.Object) []string {
+ return nil
+}
diff --git a/internal/storage/repository/cluster_scoped_object_repository.go b/internal/storage/repository/cluster_scoped_object_repository.go
new file mode 100644
index 000000000..38f125e10
--- /dev/null
+++ b/internal/storage/repository/cluster_scoped_object_repository.go
@@ -0,0 +1,184 @@
+package repository
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+
+ "github.com/jackc/pgx/v5"
+ "github.com/stephenafamo/bob/dialect/psql"
+ "github.com/stephenafamo/bob/dialect/psql/dm"
+ "github.com/stephenafamo/bob/dialect/psql/im"
+ "github.com/stephenafamo/bob/dialect/psql/sm"
+ "github.com/stephenafamo/bob/dialect/psql/um"
+ "k8s.io/apimachinery/pkg/api/meta"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apiserver/pkg/storage"
+)
+
+// ClusterScopedObjectRepository is a repository for cluster-scoped Kubernetes objects.
+// Unlike GenericObjectRepository, it does not use a namespace column.
+//
+// Expected table schema:
+//
+// CREATE TABLE (
+// id BIGSERIAL,
+// name TEXT NOT NULL,
+// object JSONB NOT NULL,
+// PRIMARY KEY (name)
+// );
+type ClusterScopedObjectRepository struct {
+ table string
+ newFunc func() runtime.Object
+}
+
+var _ Repository = &ClusterScopedObjectRepository{}
+
+func NewClusterScopedObjectRepository(table string, newFunc func() runtime.Object) *ClusterScopedObjectRepository {
+ return &ClusterScopedObjectRepository{
+ table,
+ newFunc,
+ }
+}
+
+func (r *ClusterScopedObjectRepository) Create(ctx context.Context, tx pgx.Tx, obj runtime.Object) error {
+ meta, err := meta.Accessor(obj)
+ if err != nil {
+ return fmt.Errorf("failed to get object metadata: %w", err)
+ }
+
+ bytes, err := json.Marshal(obj)
+ if err != nil {
+ return fmt.Errorf("failed to marshal object: %w", err)
+ }
+
+ query, args, err := psql.Insert(
+ im.Into(psql.Quote(r.table), "name", "object"),
+ im.Values(psql.Arg(meta.GetName()), psql.Arg(bytes)),
+ im.OnConflict().DoNothing(),
+ ).Build(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to build insert query: %w", err)
+ }
+
+ result, err := tx.Exec(ctx, query, args...)
+ if err != nil {
+ return fmt.Errorf("failed to execute insert: %w", err)
+ }
+
+ if result.RowsAffected() == 0 {
+ return ErrAlreadyExists
+ }
+
+ return nil
+}
+
+func (r *ClusterScopedObjectRepository) Delete(ctx context.Context, tx pgx.Tx, name, _ string) (runtime.Object, error) {
+ query, args, err := psql.Delete(
+ dm.From(psql.Quote(r.table)),
+ dm.Where(psql.Quote("name").EQ(psql.Arg(name))),
+ dm.Returning("object"),
+ ).Build(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build delete query: %w", err)
+ }
+
+ var bytes []byte
+ err = tx.QueryRow(ctx, query, args...).Scan(&bytes)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, ErrNotFound
+ }
+ return nil, fmt.Errorf("failed to execute delete: %w", err)
+ }
+
+ obj := r.newFunc()
+ if err := json.Unmarshal(bytes, obj); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal object: %w", err)
+ }
+
+ return obj, nil
+}
+
+func (r *ClusterScopedObjectRepository) Get(ctx context.Context, db Querier, name, _ string) (runtime.Object, error) {
+ query, args, err := psql.Select(
+ sm.Columns("object"),
+ sm.From(psql.Quote(r.table)),
+ sm.Where(psql.Quote("name").EQ(psql.Arg(name))),
+ ).Build(ctx)
+ if err != nil {
+ return nil, fmt.Errorf("failed to build select query: %w", err)
+ }
+
+ var bytes []byte
+ err = db.QueryRow(ctx, query, args...).Scan(&bytes)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil, ErrNotFound
+ }
+ return nil, fmt.Errorf("failed to execute select: %w", err)
+ }
+
+ obj := r.newFunc()
+ if err := json.Unmarshal(bytes, obj); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal object: %w", err)
+ }
+
+ return obj, nil
+}
+
+func (r *ClusterScopedObjectRepository) List(ctx context.Context, db Querier, _ string, opts storage.ListOptions) ([]runtime.Object, string, error) {
+ qb := psql.Select(
+ sm.From(psql.Quote(r.table)),
+ sm.Columns("id", "object"),
+ sm.OrderBy(psql.Quote("id")),
+ )
+
+ // Pass empty namespace so the list helper does not filter by namespace.
+ return list(ctx, db, qb, "", opts, r.newFunc)
+}
+
+func (r *ClusterScopedObjectRepository) Update(ctx context.Context, tx pgx.Tx, name, _ string, obj runtime.Object) error {
+ bytes, err := json.Marshal(obj)
+ if err != nil {
+ return fmt.Errorf("failed to marshal object: %w", err)
+ }
+
+ query, args, err := psql.Update(
+ um.Table(psql.Quote(r.table)),
+ um.SetCol("object").To(psql.Arg(bytes)),
+ um.Where(psql.Quote("name").EQ(psql.Arg(name))),
+ ).Build(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to build update query: %w", err)
+ }
+
+ result, err := tx.Exec(ctx, query, args...)
+ if err != nil {
+ return fmt.Errorf("failed to execute update: %w", err)
+ }
+
+ if result.RowsAffected() == 0 {
+ return ErrNotFound
+ }
+
+ return nil
+}
+
+func (r *ClusterScopedObjectRepository) Count(ctx context.Context, db Querier, _ string) (int64, error) {
+ query, args, err := psql.Select(
+ sm.Columns("COUNT(*)"),
+ sm.From(psql.Quote(r.table)),
+ ).Build(ctx)
+ if err != nil {
+ return 0, fmt.Errorf("failed to build count query: %w", err)
+ }
+
+ var count int64
+ if err := db.QueryRow(ctx, query, args...).Scan(&count); err != nil {
+ return 0, fmt.Errorf("failed to execute count query: %w", err)
+ }
+
+ return count, nil
+}
diff --git a/internal/storage/store.go b/internal/storage/store.go
index 7c69ddd08..d33d15d38 100644
--- a/internal/storage/store.go
+++ b/internal/storage/store.go
@@ -28,12 +28,13 @@ import (
var _ storage.Interface = &store{}
type store struct {
- db *pgxpool.Pool
- repository repository.Repository
- broadcaster *natsBroadcaster
- newFunc func() runtime.Object
- newListFunc func() runtime.Object
- logger *slog.Logger
+ db *pgxpool.Pool
+ repository repository.Repository
+ broadcaster *natsBroadcaster
+ newFunc func() runtime.Object
+ newListFunc func() runtime.Object
+ logger *slog.Logger
+ clusterScoped bool
}
// Versioner returns API object versioner associated with this interface.
@@ -138,8 +139,8 @@ func (s *store) Delete(
) error {
s.logger.DebugContext(ctx, "Deleting object", "key", key)
- name, namespace := extractNameAndNamespace(key)
- if name == "" || namespace == "" {
+ name, namespace := s.extractKeyNameAndNamespace(key)
+ if name == "" {
return storage.NewInternalError(fmt.Errorf("invalid key: %s", key))
}
@@ -342,8 +343,8 @@ func (s *store) Get(ctx context.Context, key string, opts storage.GetOptions, ob
opts.ResourceVersion,
)
- name, namespace := extractNameAndNamespace(key)
- if name == "" || namespace == "" {
+ name, namespace := s.extractKeyNameAndNamespace(key)
+ if name == "" {
return storage.NewInternalError(fmt.Errorf("invalid key: %s", key))
}
@@ -391,7 +392,7 @@ func (s *store) GetList(ctx context.Context, key string, opts storage.ListOption
return apierrors.NewBadRequest(fmt.Sprintf("invalid resource version: %v", err))
}
- namespace := extractNamespace(key)
+ namespace := s.extractKeyNamespace(key)
items, continueToken, err := s.repository.List(ctx, s.db, namespace, opts)
if err != nil {
return storage.NewInternalError(err)
@@ -473,8 +474,8 @@ func (s *store) GuaranteedUpdate(
) error {
s.logger.DebugContext(ctx, "Guaranteed update", "key", key)
- name, namespace := extractNameAndNamespace(key)
- if name == "" || namespace == "" {
+ name, namespace := s.extractKeyNameAndNamespace(key)
+ if name == "" {
return storage.NewInternalError(fmt.Errorf("invalid key: %s", key))
}
@@ -557,7 +558,7 @@ func (s *store) GuaranteedUpdate(
func (s *store) Count(key string) (int64, error) {
s.logger.Debug("Counting objects", "key", key)
- namespace := extractNamespace(key)
+ namespace := s.extractKeyNamespace(key)
count, err := s.repository.Count(context.Background(), s.db, namespace)
if err != nil {
@@ -619,23 +620,38 @@ func (s *store) EnableResourceSizeEstimation(_ storage.KeysFunc) error {
return nil
}
-// extractNameAndNamespace extracts the name and namespace from the key.
-// Used for single object operations.
-// Key format: /storage.sbomscanner.kubewarden.io///
-func extractNameAndNamespace(key string) (string, string) {
+// extractKeyNameAndNamespace extracts the name and namespace from the key,
+// handling both namespaced and cluster-scoped key formats.
+//
+// Namespaced key format: /storage.sbomscanner.kubewarden.io///
+// Cluster-scoped key format: /storage.sbomscanner.kubewarden.io//
+func (s *store) extractKeyNameAndNamespace(key string) (string, string) {
key = strings.TrimPrefix(key, "/")
parts := strings.Split(key, "/")
+
+ if s.clusterScoped {
+ if len(parts) == 3 {
+ return parts[2], ""
+ }
+ return "", ""
+ }
+
if len(parts) == 4 {
return parts[3], parts[2]
}
-
return "", ""
}
-// extractNamespace extracts the namespace from the key.
-// Used for list operations.
-// Key format: /storage.sbomscanner.kubewarden.io//
-func extractNamespace(key string) string {
+// extractKeyNamespace extracts the namespace from the key.
+// For cluster-scoped resources, always returns empty string.
+//
+// Namespaced key format: /storage.sbomscanner.kubewarden.io//
+// Cluster-scoped key format: /storage.sbomscanner.kubewarden.io/
+func (s *store) extractKeyNamespace(key string) string {
+ if s.clusterScoped {
+ return ""
+ }
+
key = strings.TrimPrefix(key, "/")
parts := strings.Split(key, "/")
if len(parts) == 3 {
diff --git a/internal/storage/transform.go b/internal/storage/transform.go
index c0cef3a71..f66e3ab03 100644
--- a/internal/storage/transform.go
+++ b/internal/storage/transform.go
@@ -34,6 +34,19 @@ func TransformStripSBOM(obj any) (any, error) {
return cache.TransformStripManagedFields()(sbom)
}
+// TransformStripNodeSBOM strips the NodeSBOM object of its SPDX field and managed fields.
+// This is useful for caching scenarios where the SPDX data is not needed, reducing memory usage.
+func TransformStripNodeSBOM(obj any) (any, error) {
+ nodeSBOM, ok := obj.(*storagev1alpha1.NodeSBOM)
+ if !ok {
+ return nil, fmt.Errorf("expected NodeSBOM object, got %T", obj)
+ }
+
+ nodeSBOM.SPDX = runtime.RawExtension{}
+
+ return cache.TransformStripManagedFields()(nodeSBOM)
+}
+
// TransformStripVulnerabilityReport strips the VulnerabilityReport object of its Results field and managed fields.
// This is useful for caching scenarios where the Results data is not needed, reducing memory usage.
func TransformStripVulnerabilityReport(obj any) (any, error) {
@@ -47,6 +60,19 @@ func TransformStripVulnerabilityReport(obj any) (any, error) {
return cache.TransformStripManagedFields()(vulnerabilityReport)
}
+// TransformStripNodeVulnerabilityReport strips the NodeVulnerabilityReport object of its Results field and managed fields.
+// This is useful for caching scenarios where the Results data is not needed, reducing memory usage.
+func TransformStripNodeVulnerabilityReport(obj any) (any, error) {
+ nodeVulnerabilityReport, ok := obj.(*storagev1alpha1.NodeVulnerabilityReport)
+ if !ok {
+ return obj, fmt.Errorf("expected NodeVulnerabilityReport object, got %T", obj)
+ }
+
+ nodeVulnerabilityReport.Report.Results = nil
+
+ return cache.TransformStripManagedFields()(nodeVulnerabilityReport)
+}
+
// TransformStripWorkloadScanReport strips the WorkloadScanReport object of its status.
// This is useful for caching scenarios where the Reports data is not needed, reducing memory usage.
func TransformStripWorkloadScanReport(object any) (any, error) {
diff --git a/internal/storage/watcher.go b/internal/storage/watcher.go
index bb00a316a..05e695395 100644
--- a/internal/storage/watcher.go
+++ b/internal/storage/watcher.go
@@ -150,7 +150,12 @@ func (w *natsWatcher) rehydrate(ctx context.Context, payloadObj runtime.Object)
if err != nil {
return nil, fmt.Errorf("failed to get meta accessor: %w", err)
}
- key := fmt.Sprintf("%s/%s/%s/%s", storagev1alpha1.GroupName, w.resource, payloadAccessor.GetNamespace(), payloadAccessor.GetName())
+ var key string
+ if w.store.clusterScoped {
+ key = fmt.Sprintf("%s/%s/%s", storagev1alpha1.GroupName, w.resource, payloadAccessor.GetName())
+ } else {
+ key = fmt.Sprintf("%s/%s/%s/%s", storagev1alpha1.GroupName, w.resource, payloadAccessor.GetNamespace(), payloadAccessor.GetName())
+ }
fetched := w.store.newFunc()
if err := w.store.Get(ctx, key, storage.GetOptions{}, fetched); err != nil {
diff --git a/internal/webhook/v1alpha1/nodescanconfiguration_webhook.go b/internal/webhook/v1alpha1/nodescanconfiguration_webhook.go
new file mode 100644
index 000000000..bf28263b2
--- /dev/null
+++ b/internal/webhook/v1alpha1/nodescanconfiguration_webhook.go
@@ -0,0 +1,101 @@
+package v1alpha1
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/go-logr/logr"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
+ "k8s.io/apimachinery/pkg/util/validation/field"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+// SetupNodeScanConfigurationWebhookWithManager registers the webhook for NodeScanConfiguration in the manager.
+func SetupNodeScanConfigurationWebhookWithManager(mgr ctrl.Manager) error {
+ err := ctrl.NewWebhookManagedBy(mgr, &v1alpha1.NodeScanConfiguration{}).
+ WithValidator(&NodeScanConfigurationCustomValidator{
+ logger: mgr.GetLogger().WithName("NodeScanConfiguration_validator"),
+ }).
+ Complete()
+ if err != nil {
+ return fmt.Errorf("failed to setup NodeScanConfiguration webhook: %w", err)
+ }
+ return nil
+}
+
+// +kubebuilder:webhook:path=/validate-sbomscanner-kubewarden-io-v1alpha1-nodescanconfiguration,mutating=false,failurePolicy=fail,sideEffects=None,groups=sbomscanner.kubewarden.io,resources=nodescanconfigurations,verbs=create;update;delete,versions=v1alpha1,name=vnodescanconfiguration.sbomscanner.kubewarden.io,admissionReviewVersions=v1
+
+type NodeScanConfigurationCustomValidator struct {
+ logger logr.Logger
+}
+
+var _ admission.Validator[*v1alpha1.NodeScanConfiguration] = &NodeScanConfigurationCustomValidator{}
+
+func (v *NodeScanConfigurationCustomValidator) ValidateCreate(_ context.Context, config *v1alpha1.NodeScanConfiguration) (admission.Warnings, error) {
+ v.logger.Info("Validation for NodeScanConfiguration upon creation", "name", config.GetName())
+
+ allErrs := validateNodeScanConfiguration(config)
+
+ if len(allErrs) > 0 {
+ return nil, apierrors.NewInvalid(
+ v1alpha1.GroupVersion.WithKind("NodeScanConfiguration").GroupKind(),
+ config.Name,
+ allErrs,
+ )
+ }
+
+ return nil, nil
+}
+
+func (v *NodeScanConfigurationCustomValidator) ValidateUpdate(_ context.Context, _, config *v1alpha1.NodeScanConfiguration) (admission.Warnings, error) {
+ v.logger.Info("Validation for NodeScanConfiguration upon update", "name", config.GetName())
+
+ allErrs := validateNodeScanConfiguration(config)
+
+ if len(allErrs) > 0 {
+ return nil, apierrors.NewInvalid(
+ v1alpha1.GroupVersion.WithKind("NodeScanConfiguration").GroupKind(),
+ config.Name,
+ allErrs,
+ )
+ }
+
+ return nil, nil
+}
+
+func (v *NodeScanConfigurationCustomValidator) ValidateDelete(_ context.Context, config *v1alpha1.NodeScanConfiguration) (admission.Warnings, error) {
+ v.logger.Info("Validation for NodeScanConfiguration upon deletion", "name", config.GetName())
+
+ return admission.Warnings{
+ "NodeScanConfiguration deleted. Node scan feature is now disabled",
+ }, nil
+}
+
+func validateNodeScanConfiguration(config *v1alpha1.NodeScanConfiguration) field.ErrorList {
+ var allErrs field.ErrorList
+
+ if err := validateScanInterval(config.Spec.ScanInterval); err != nil {
+ allErrs = append(allErrs, err)
+ }
+ allErrs = append(allErrs, validatePlatforms(config.Spec.Platforms)...)
+ allErrs = append(allErrs, validateNodeSelector(config.Spec.NodeSelector)...)
+
+ return allErrs
+}
+
+func validateNodeSelector(selector *metav1.LabelSelector) field.ErrorList {
+ if selector == nil {
+ return nil
+ }
+
+ fieldPath := field.NewPath("spec").Child("nodeSelector")
+ opts := metav1validation.LabelSelectorValidationOptions{}
+
+ return metav1validation.ValidateLabelSelector(selector, opts, fieldPath)
+}
diff --git a/internal/webhook/v1alpha1/nodescanconfiguration_webhook_test.go b/internal/webhook/v1alpha1/nodescanconfiguration_webhook_test.go
new file mode 100644
index 000000000..f34930090
--- /dev/null
+++ b/internal/webhook/v1alpha1/nodescanconfiguration_webhook_test.go
@@ -0,0 +1,239 @@
+package v1alpha1
+
+import (
+ "testing"
+ "time"
+
+ "github.com/go-logr/logr"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+type nodeScanConfigurationTestCase struct {
+ name string
+ configuration *v1alpha1.NodeScanConfiguration
+ expectedError string
+ expectedField string
+}
+
+var nodeScanConfigurationTestCases = []nodeScanConfigurationTestCase{
+ {
+ name: "should allow when all fields are empty",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{},
+ },
+ },
+ // ScanInterval test cases
+ {
+ name: "should allow when scanInterval is nil",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: nil,
+ },
+ },
+ },
+ {
+ name: "should admit when scanInterval is exactly 1 minute",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{
+ Duration: time.Minute,
+ },
+ },
+ },
+ },
+ {
+ name: "should admit when scanInterval is greater than 1 minute",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{
+ Duration: 1 * time.Hour,
+ },
+ },
+ },
+ },
+ {
+ name: "should deny when scanInterval is less than 1 minute",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ ScanInterval: &metav1.Duration{
+ Duration: 30 * time.Second,
+ },
+ },
+ },
+ expectedField: "spec.scanInterval",
+ expectedError: "scanInterval must be at least 1 minute",
+ },
+ // Platform test cases
+ {
+ name: "should allow when platforms are valid",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ Platforms: []v1alpha1.Platform{
+ {
+ Architecture: "amd64",
+ OS: "linux",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "should deny when platforms are not valid",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ Platforms: []v1alpha1.Platform{
+ {
+ Architecture: "xxx",
+ OS: "yyy",
+ },
+ },
+ },
+ },
+ expectedField: "spec.platforms[0]",
+ expectedError: "unsupported OS: yyy",
+ },
+ // NodeSelector test cases
+ {
+ name: "should allow when nodeSelector is nil",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: nil,
+ },
+ },
+ },
+ {
+ name: "should allow when nodeSelector is valid",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "env": "production",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "should deny when nodeSelector is invalid",
+ configuration: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "invalid key": "value",
+ },
+ },
+ },
+ },
+ expectedField: "spec.nodeSelector.matchLabels",
+ expectedError: "Invalid value",
+ },
+}
+
+func TestNodeScanConfigurationCustomValidator_ValidateCreate(t *testing.T) {
+ for _, test := range nodeScanConfigurationTestCases {
+ t.Run(test.name, func(t *testing.T) {
+ validator := &NodeScanConfigurationCustomValidator{
+ logger: logr.Discard(),
+ }
+ warnings, err := validator.ValidateCreate(t.Context(), test.configuration)
+
+ if test.expectedError != "" {
+ require.Error(t, err)
+ statusErr, ok := err.(interface{ Status() metav1.Status })
+ require.True(t, ok)
+ details := statusErr.Status().Details
+ require.NotNil(t, details)
+ require.Len(t, details.Causes, 1)
+ assert.Equal(t, test.expectedField, details.Causes[0].Field)
+ assert.Contains(t, details.Causes[0].Message, test.expectedError)
+ } else {
+ require.NoError(t, err)
+ }
+
+ assert.Empty(t, warnings)
+ })
+ }
+}
+
+func TestNodeScanConfigurationCustomValidator_ValidateUpdate(t *testing.T) {
+ for _, test := range nodeScanConfigurationTestCases {
+ t.Run(test.name, func(t *testing.T) {
+ validator := &NodeScanConfigurationCustomValidator{
+ logger: logr.Discard(),
+ }
+
+ old := &v1alpha1.NodeScanConfiguration{}
+ warnings, err := validator.ValidateUpdate(t.Context(), old, test.configuration)
+
+ if test.expectedError != "" {
+ require.Error(t, err)
+ statusErr, ok := err.(interface{ Status() metav1.Status })
+ require.True(t, ok)
+ details := statusErr.Status().Details
+ require.NotNil(t, details)
+ require.Len(t, details.Causes, 1)
+ assert.Equal(t, test.expectedField, details.Causes[0].Field)
+ assert.Contains(t, details.Causes[0].Message, test.expectedError)
+ } else {
+ require.NoError(t, err)
+ }
+
+ assert.Empty(t, warnings)
+ })
+ }
+}
+
+func TestNodeScanConfigurationCustomValidator_ValidateDelete(t *testing.T) {
+ t.Run("should return warning on delete", func(t *testing.T) {
+ validator := &NodeScanConfigurationCustomValidator{
+ logger: logr.Discard(),
+ }
+
+ config := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "default",
+ },
+ }
+
+ warnings, err := validator.ValidateDelete(t.Context(), config)
+
+ require.NoError(t, err)
+ require.Len(t, warnings, 1)
+ assert.Equal(t, "NodeScanConfiguration deleted. Node scan feature is now disabled", warnings[0])
+ })
+}
diff --git a/internal/webhook/v1alpha1/nodescanjob_validators.go b/internal/webhook/v1alpha1/nodescanjob_validators.go
new file mode 100644
index 000000000..07c234464
--- /dev/null
+++ b/internal/webhook/v1alpha1/nodescanjob_validators.go
@@ -0,0 +1,75 @@
+package v1alpha1
+
+import (
+ "context"
+ "fmt"
+
+ corev1 "k8s.io/api/core/v1"
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/validation/field"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+ "github.com/kubewarden/sbomscanner/internal/filters"
+)
+
+// ValidateNodeScanJobAgainstConfig checks that the node targeted by a
+// NodeScanJob exists and matches the singleton NodeScanConfiguration.
+// It returns field-level errors suitable for both webhook rejection and
+// controller failure reporting.
+func ValidateNodeScanJobAgainstConfig(ctx context.Context, c client.Reader, nodeName string) field.ErrorList {
+ var allErrs field.ErrorList
+ nodeNameField := field.NewPath("spec").Child("nodeName")
+
+ var config v1alpha1.NodeScanConfiguration
+ if err := c.Get(ctx, types.NamespacedName{Name: v1alpha1.NodeScanConfigurationName}, &config); err != nil {
+ if apierrors.IsNotFound(err) {
+ allErrs = append(allErrs, field.Forbidden(nodeNameField,
+ "NodeScanConfiguration not found: node scanning is not configured"))
+ return allErrs
+ }
+ allErrs = append(allErrs, field.InternalError(nodeNameField,
+ fmt.Errorf("fetching NodeScanConfiguration: %w", err)))
+ return allErrs
+ }
+
+ var node corev1.Node
+ if err := c.Get(ctx, types.NamespacedName{Name: nodeName}, &node); err != nil {
+ if apierrors.IsNotFound(err) {
+ allErrs = append(allErrs, field.NotFound(nodeNameField, nodeName))
+ return allErrs
+ }
+ allErrs = append(allErrs, field.InternalError(nodeNameField,
+ fmt.Errorf("fetching Node %q: %w", nodeName, err)))
+ return allErrs
+ }
+
+ if config.Spec.NodeSelector != nil {
+ selector, err := metav1.LabelSelectorAsSelector(config.Spec.NodeSelector)
+ if err != nil {
+ allErrs = append(allErrs, field.InternalError(nodeNameField,
+ fmt.Errorf("parsing NodeSelector: %w", err)))
+ return allErrs
+ }
+ if !selector.Matches(labels.Set(node.Labels)) {
+ allErrs = append(allErrs, field.Forbidden(nodeNameField,
+ fmt.Sprintf("node %q does not match the NodeScanConfiguration nodeSelector", nodeName)))
+ }
+ }
+
+ if !filters.IsPlatformAllowed(
+ node.Status.NodeInfo.OperatingSystem,
+ node.Status.NodeInfo.Architecture,
+ "",
+ config.Spec.Platforms,
+ ) {
+ allErrs = append(allErrs, field.Forbidden(nodeNameField,
+ fmt.Sprintf("node %q platform %s/%s is not allowed by the NodeScanConfiguration",
+ nodeName, node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.Architecture)))
+ }
+
+ return allErrs
+}
diff --git a/internal/webhook/v1alpha1/nodescanjob_webhook.go b/internal/webhook/v1alpha1/nodescanjob_webhook.go
new file mode 100644
index 000000000..a7e0f6230
--- /dev/null
+++ b/internal/webhook/v1alpha1/nodescanjob_webhook.go
@@ -0,0 +1,80 @@
+package v1alpha1
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/go-logr/logr"
+
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/util/validation/field"
+ ctrl "sigs.k8s.io/controller-runtime"
+ "sigs.k8s.io/controller-runtime/pkg/client"
+ "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+// SetupNodeScanJobWebhookWithManager registers the webhook for NodeScanJob in the manager.
+func SetupNodeScanJobWebhookWithManager(mgr ctrl.Manager) error {
+ err := ctrl.NewWebhookManagedBy(mgr, &v1alpha1.NodeScanJob{}).
+ WithValidator(&NodeScanJobCustomValidator{
+ client: mgr.GetClient(),
+ logger: mgr.GetLogger().WithName("NodeScanJob_validator"),
+ }).
+ Complete()
+ if err != nil {
+ return fmt.Errorf("failed to setup NodeScanJob webhook: %w", err)
+ }
+ return nil
+}
+
+// +kubebuilder:webhook:path=/validate-sbomscanner-kubewarden-io-v1alpha1-nodescanjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=sbomscanner.kubewarden.io,resources=nodescanjobs,verbs=create;update,versions=v1alpha1,name=vnodescanjob.sbomscanner.kubewarden.io,admissionReviewVersions=v1
+
+type NodeScanJobCustomValidator struct {
+ client client.Client
+ logger logr.Logger
+}
+
+var _ admission.Validator[*v1alpha1.NodeScanJob] = &NodeScanJobCustomValidator{}
+
+func (v *NodeScanJobCustomValidator) ValidateCreate(ctx context.Context, job *v1alpha1.NodeScanJob) (admission.Warnings, error) {
+ v.logger.Info("Validation for NodeScanJob upon creation", "name", job.GetName())
+
+ allErrs := ValidateNodeScanJobAgainstConfig(ctx, v.client, job.Spec.NodeName)
+
+ if len(allErrs) > 0 {
+ return nil, apierrors.NewInvalid(
+ v1alpha1.GroupVersion.WithKind("NodeScanJob").GroupKind(),
+ job.Name,
+ allErrs,
+ )
+ }
+
+ return nil, nil
+}
+
+func (v *NodeScanJobCustomValidator) ValidateUpdate(_ context.Context, oldJob, newJob *v1alpha1.NodeScanJob) (admission.Warnings, error) {
+ v.logger.Info("Validation for NodeScanJob upon update", "name", newJob.GetName())
+
+ var allErrs field.ErrorList
+ if oldJob.Spec.NodeName != newJob.Spec.NodeName {
+ fieldPath := field.NewPath("spec").Child("nodeName")
+ allErrs = append(allErrs, field.Invalid(fieldPath, newJob.Spec.NodeName, "field is immutable"))
+ }
+
+ if len(allErrs) > 0 {
+ return nil, apierrors.NewInvalid(
+ v1alpha1.GroupVersion.WithKind("NodeScanJob").GroupKind(),
+ newJob.Name,
+ allErrs,
+ )
+ }
+
+ return nil, nil
+}
+
+func (v *NodeScanJobCustomValidator) ValidateDelete(_ context.Context, job *v1alpha1.NodeScanJob) (admission.Warnings, error) {
+ v.logger.Info("Validation for NodeScanJob upon deletion", "name", job.GetName())
+ return nil, nil
+}
diff --git a/internal/webhook/v1alpha1/nodescanjob_webhook_test.go b/internal/webhook/v1alpha1/nodescanjob_webhook_test.go
new file mode 100644
index 000000000..2aa423da3
--- /dev/null
+++ b/internal/webhook/v1alpha1/nodescanjob_webhook_test.go
@@ -0,0 +1,309 @@
+package v1alpha1
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/go-logr/logr"
+ "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"
+ "sigs.k8s.io/controller-runtime/pkg/client/fake"
+
+ "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+func TestNodeScanJobCustomValidator_ValidateCreate(t *testing.T) {
+ tests := []struct {
+ name string
+ config *v1alpha1.NodeScanConfiguration
+ node *corev1.Node
+ job *v1alpha1.NodeScanJob
+ expectedError string
+ expectedField string
+ }{
+ {
+ name: "should admit when config exists and node matches",
+ config: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{Name: "default"},
+ Spec: v1alpha1.NodeScanConfigurationSpec{},
+ },
+ node: &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{Name: "worker-1"},
+ },
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "worker-1"},
+ },
+ },
+ {
+ name: "should deny when NodeScanConfiguration is missing",
+ config: nil,
+ node: &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{Name: "worker-1"},
+ },
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "worker-1"},
+ },
+ expectedField: "spec.nodeName",
+ expectedError: "NodeScanConfiguration not found",
+ },
+ {
+ name: "should deny when node does not exist",
+ config: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{Name: "default"},
+ Spec: v1alpha1.NodeScanConfigurationSpec{},
+ },
+ node: nil,
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "non-existent"},
+ },
+ expectedField: "spec.nodeName",
+ expectedError: "not found",
+ },
+ {
+ name: "should deny when node does not match nodeSelector",
+ config: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{Name: "default"},
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{"env": "production"},
+ },
+ },
+ },
+ node: &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "worker-1",
+ Labels: map[string]string{"env": "staging"},
+ },
+ },
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "worker-1"},
+ },
+ expectedField: "spec.nodeName",
+ expectedError: "does not match the NodeScanConfiguration nodeSelector",
+ },
+ {
+ name: "should admit when node matches nodeSelector",
+ config: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{Name: "default"},
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{"env": "production"},
+ },
+ },
+ },
+ node: &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "worker-1",
+ Labels: map[string]string{"env": "production"},
+ },
+ },
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "worker-1"},
+ },
+ },
+ {
+ name: "should deny when node platform is not allowed",
+ config: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{Name: "default"},
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ Platforms: []v1alpha1.Platform{
+ {Architecture: "amd64", OS: "linux"},
+ },
+ },
+ },
+ node: &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{Name: "worker-1"},
+ Status: corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "arm64",
+ },
+ },
+ },
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "worker-1"},
+ },
+ expectedField: "spec.nodeName",
+ expectedError: "platform linux/arm64 is not allowed",
+ },
+ {
+ name: "should admit when node platform matches",
+ config: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{Name: "default"},
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ Platforms: []v1alpha1.Platform{
+ {Architecture: "amd64", OS: "linux"},
+ },
+ },
+ },
+ node: &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{Name: "worker-1"},
+ Status: corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "amd64",
+ },
+ },
+ },
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "worker-1"},
+ },
+ },
+ {
+ name: "should deny when nodeSelector and platform both fail",
+ config: &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{Name: "default"},
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{"env": "production"},
+ },
+ Platforms: []v1alpha1.Platform{
+ {Architecture: "amd64", OS: "linux"},
+ },
+ },
+ },
+ node: &corev1.Node{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "worker-1",
+ Labels: map[string]string{"env": "staging"},
+ },
+ Status: corev1.NodeStatus{
+ NodeInfo: corev1.NodeSystemInfo{
+ OperatingSystem: "linux",
+ Architecture: "arm64",
+ },
+ },
+ },
+ job: &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: "worker-1"},
+ },
+ expectedField: "spec.nodeName",
+ expectedError: "does not match the NodeScanConfiguration nodeSelector",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ scheme := runtime.NewScheme()
+ require.NoError(t, v1alpha1.AddToScheme(scheme))
+ require.NoError(t, corev1.AddToScheme(scheme))
+
+ builder := fake.NewClientBuilder().WithScheme(scheme)
+ if test.config != nil {
+ builder = builder.WithObjects(test.config)
+ }
+ if test.node != nil {
+ builder = builder.WithObjects(test.node)
+ }
+ fakeClient := builder.Build()
+
+ validator := &NodeScanJobCustomValidator{
+ client: fakeClient,
+ logger: logr.Discard(),
+ }
+
+ warnings, err := validator.ValidateCreate(t.Context(), test.job)
+
+ if test.expectedError != "" {
+ require.Error(t, err)
+ statusErr, ok := err.(interface{ Status() metav1.Status })
+ require.True(t, ok)
+ details := statusErr.Status().Details
+ require.NotNil(t, details)
+ require.NotEmpty(t, details.Causes)
+ assert.Equal(t, test.expectedField, details.Causes[0].Field)
+ assert.Contains(t, details.Causes[0].Message, test.expectedError)
+ } else {
+ require.NoError(t, err)
+ }
+
+ assert.Empty(t, warnings)
+ })
+ }
+}
+
+func TestNodeScanJobCustomValidator_ValidateUpdate(t *testing.T) {
+ tests := []struct {
+ name string
+ oldNodeName string
+ newNodeName string
+ expectedFields []string
+ }{
+ {
+ name: "nodeName changed is rejected",
+ oldNodeName: "worker-1",
+ newNodeName: "worker-2",
+ expectedFields: []string{"spec.nodeName"},
+ },
+ {
+ name: "no changes is admitted",
+ oldNodeName: "worker-1",
+ newNodeName: "worker-1",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ validator := &NodeScanJobCustomValidator{
+ logger: logr.Discard(),
+ }
+
+ oldObj := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: test.oldNodeName},
+ }
+ newObj := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: "test-job"},
+ Spec: v1alpha1.NodeScanJobSpec{NodeName: test.newNodeName},
+ }
+
+ warnings, err := validator.ValidateUpdate(t.Context(), oldObj, newObj)
+ assert.Empty(t, warnings)
+
+ if len(test.expectedFields) == 0 {
+ require.NoError(t, err)
+ return
+ }
+
+ require.Error(t, err)
+ statusErr, ok := err.(interface{ Status() metav1.Status })
+ require.True(t, ok)
+ details := statusErr.Status().Details
+ require.NotNil(t, details)
+ require.Len(t, details.Causes, len(test.expectedFields))
+ for i, f := range test.expectedFields {
+ assert.Equal(t, f, details.Causes[i].Field)
+ assert.Contains(t, details.Causes[i].Message, "immutable")
+ }
+ })
+ }
+}
+
+func TestNodeScanJobCustomValidator_ValidateDelete(t *testing.T) {
+ t.Run("should allow deletion", func(t *testing.T) {
+ validator := &NodeScanJobCustomValidator{
+ logger: logr.Discard(),
+ }
+
+ job := &v1alpha1.NodeScanJob{
+ ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("test-job")},
+ }
+
+ warnings, err := validator.ValidateDelete(t.Context(), job)
+
+ require.NoError(t, err)
+ assert.Empty(t, warnings)
+ })
+}
diff --git a/internal/webhook/v1alpha1/scanjob_webhook_test.go b/internal/webhook/v1alpha1/scanjob_webhook_test.go
index 1cc3af053..3d7f036cb 100644
--- a/internal/webhook/v1alpha1/scanjob_webhook_test.go
+++ b/internal/webhook/v1alpha1/scanjob_webhook_test.go
@@ -100,7 +100,7 @@ func TestScanJobCustomValidator_ValidateCreate(t *testing.T) {
},
}
job.InitializeConditions()
- job.MarkInProgress(v1alpha1.ReasonImageScanInProgress, "Image scan in progress")
+ job.MarkInProgress(v1alpha1.ReasonScanJobImageScanInProgress, "Image scan in progress")
return job
}(),
scanJob: &v1alpha1.ScanJob{
@@ -128,7 +128,7 @@ func TestScanJobCustomValidator_ValidateCreate(t *testing.T) {
},
}
job.InitializeConditions()
- job.MarkComplete(v1alpha1.ReasonAllImagesScanned, "Done")
+ job.MarkComplete(v1alpha1.ReasonScanJobAllImagesScanned, "Done")
return job
}(),
scanJob: &v1alpha1.ScanJob{
@@ -154,7 +154,7 @@ func TestScanJobCustomValidator_ValidateCreate(t *testing.T) {
},
}
job.InitializeConditions()
- job.MarkFailed(v1alpha1.ReasonInternalError, "Failed")
+ job.MarkFailed(v1alpha1.ReasonScanJobInternalError, "Failed")
return job
}(),
scanJob: &v1alpha1.ScanJob{
diff --git a/pkg/generated/applyconfiguration/storage/v1alpha1/nodemetadata.go b/pkg/generated/applyconfiguration/storage/v1alpha1/nodemetadata.go
new file mode 100644
index 000000000..2be2103da
--- /dev/null
+++ b/pkg/generated/applyconfiguration/storage/v1alpha1/nodemetadata.go
@@ -0,0 +1,36 @@
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+// NodeMetadataApplyConfiguration represents a declarative configuration of the NodeMetadata type for use
+// with apply.
+//
+// NodeMetadata contains the metadata details of a node.
+type NodeMetadataApplyConfiguration struct {
+ // Name specifies the name of the node.
+ Name *string `json:"name,omitempty"`
+ // Platform specifies the platform of the image. Example "linux/amd64".
+ Platform *string `json:"platform,omitempty"`
+}
+
+// NodeMetadataApplyConfiguration constructs a declarative configuration of the NodeMetadata type for use with
+// apply.
+func NodeMetadata() *NodeMetadataApplyConfiguration {
+ return &NodeMetadataApplyConfiguration{}
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *NodeMetadataApplyConfiguration) WithName(value string) *NodeMetadataApplyConfiguration {
+ b.Name = &value
+ return b
+}
+
+// WithPlatform sets the Platform field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Platform field is set to the value of the last call.
+func (b *NodeMetadataApplyConfiguration) WithPlatform(value string) *NodeMetadataApplyConfiguration {
+ b.Platform = &value
+ return b
+}
diff --git a/pkg/generated/applyconfiguration/storage/v1alpha1/nodesbom.go b/pkg/generated/applyconfiguration/storage/v1alpha1/nodesbom.go
new file mode 100644
index 000000000..2ce16fc71
--- /dev/null
+++ b/pkg/generated/applyconfiguration/storage/v1alpha1/nodesbom.go
@@ -0,0 +1,230 @@
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+ types "k8s.io/apimachinery/pkg/types"
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// NodeSBOMApplyConfiguration represents a declarative configuration of the NodeSBOM type for use
+// with apply.
+//
+// NodeSBOM represents a Software Bill of Materials of a node
+type NodeSBOMApplyConfiguration struct {
+ v1.TypeMetaApplyConfiguration `json:",inline"`
+ *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
+ NodeMetadata *NodeMetadataApplyConfiguration `json:"nodeMetadata,omitempty"`
+ // SPDX contains the SPDX document of the SBOM in JSON format
+ SPDX *runtime.RawExtension `json:"spdx,omitempty"`
+}
+
+// NodeSBOM constructs a declarative configuration of the NodeSBOM type for use with
+// apply.
+func NodeSBOM(name string) *NodeSBOMApplyConfiguration {
+ b := &NodeSBOMApplyConfiguration{}
+ b.WithName(name)
+ b.WithKind("NodeSBOM")
+ b.WithAPIVersion("storage.sbomscanner.kubewarden.io/v1alpha1")
+ return b
+}
+
+func (b NodeSBOMApplyConfiguration) IsApplyConfiguration() {}
+
+// WithKind sets the Kind field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Kind field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithKind(value string) *NodeSBOMApplyConfiguration {
+ b.TypeMetaApplyConfiguration.Kind = &value
+ return b
+}
+
+// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the APIVersion field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithAPIVersion(value string) *NodeSBOMApplyConfiguration {
+ b.TypeMetaApplyConfiguration.APIVersion = &value
+ return b
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithName(value string) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Name = &value
+ return b
+}
+
+// WithGenerateName sets the GenerateName field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GenerateName field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithGenerateName(value string) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.GenerateName = &value
+ return b
+}
+
+// WithNamespace sets the Namespace field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Namespace field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithNamespace(value string) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Namespace = &value
+ return b
+}
+
+// WithUID sets the UID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UID field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithUID(value types.UID) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.UID = &value
+ return b
+}
+
+// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ResourceVersion field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithResourceVersion(value string) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.ResourceVersion = &value
+ return b
+}
+
+// WithGeneration sets the Generation field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Generation field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithGeneration(value int64) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Generation = &value
+ return b
+}
+
+// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the CreationTimestamp field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithCreationTimestamp(value metav1.Time) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.CreationTimestamp = &value
+ return b
+}
+
+// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionTimestamp field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value
+ return b
+}
+
+// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value
+ return b
+}
+
+// WithLabels puts the entries into the Labels field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Labels field,
+// overwriting an existing map entries in Labels field with the same key.
+func (b *NodeSBOMApplyConfiguration) WithLabels(entries map[string]string) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Labels[k] = v
+ }
+ return b
+}
+
+// WithAnnotations puts the entries into the Annotations field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Annotations field,
+// overwriting an existing map entries in Annotations field with the same key.
+func (b *NodeSBOMApplyConfiguration) WithAnnotations(entries map[string]string) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Annotations[k] = v
+ }
+ return b
+}
+
+// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the OwnerReferences field.
+func (b *NodeSBOMApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithOwnerReferences")
+ }
+ b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i])
+ }
+ return b
+}
+
+// WithFinalizers adds the given value to the Finalizers field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Finalizers field.
+func (b *NodeSBOMApplyConfiguration) WithFinalizers(values ...string) *NodeSBOMApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i])
+ }
+ return b
+}
+
+func (b *NodeSBOMApplyConfiguration) ensureObjectMetaApplyConfigurationExists() {
+ if b.ObjectMetaApplyConfiguration == nil {
+ b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{}
+ }
+}
+
+// WithNodeMetadata sets the NodeMetadata field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the NodeMetadata field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithNodeMetadata(value *NodeMetadataApplyConfiguration) *NodeSBOMApplyConfiguration {
+ b.NodeMetadata = value
+ return b
+}
+
+// WithSPDX sets the SPDX field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the SPDX field is set to the value of the last call.
+func (b *NodeSBOMApplyConfiguration) WithSPDX(value runtime.RawExtension) *NodeSBOMApplyConfiguration {
+ b.SPDX = &value
+ return b
+}
+
+// GetKind retrieves the value of the Kind field in the declarative configuration.
+func (b *NodeSBOMApplyConfiguration) GetKind() *string {
+ return b.TypeMetaApplyConfiguration.Kind
+}
+
+// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration.
+func (b *NodeSBOMApplyConfiguration) GetAPIVersion() *string {
+ return b.TypeMetaApplyConfiguration.APIVersion
+}
+
+// GetName retrieves the value of the Name field in the declarative configuration.
+func (b *NodeSBOMApplyConfiguration) GetName() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Name
+}
+
+// GetNamespace retrieves the value of the Namespace field in the declarative configuration.
+func (b *NodeSBOMApplyConfiguration) GetNamespace() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Namespace
+}
diff --git a/pkg/generated/applyconfiguration/storage/v1alpha1/nodevulnerabilityreport.go b/pkg/generated/applyconfiguration/storage/v1alpha1/nodevulnerabilityreport.go
new file mode 100644
index 000000000..e3d967189
--- /dev/null
+++ b/pkg/generated/applyconfiguration/storage/v1alpha1/nodevulnerabilityreport.go
@@ -0,0 +1,230 @@
+// Code generated by applyconfiguration-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ v1 "k8s.io/client-go/applyconfigurations/meta/v1"
+)
+
+// NodeVulnerabilityReportApplyConfiguration represents a declarative configuration of the NodeVulnerabilityReport type for use
+// with apply.
+//
+// NodeVulnerabilityReport is the Schema for the scanresults API
+type NodeVulnerabilityReportApplyConfiguration struct {
+ v1.TypeMetaApplyConfiguration `json:",inline"`
+ *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"`
+ // NodeMetadata contains info about the scanned node
+ NodeMetadata *NodeMetadataApplyConfiguration `json:"nodeMetadata,omitempty"`
+ // Report is the actual vulnerability scan report
+ Report *ReportApplyConfiguration `json:"report,omitempty"`
+}
+
+// NodeVulnerabilityReport constructs a declarative configuration of the NodeVulnerabilityReport type for use with
+// apply.
+func NodeVulnerabilityReport(name string) *NodeVulnerabilityReportApplyConfiguration {
+ b := &NodeVulnerabilityReportApplyConfiguration{}
+ b.WithName(name)
+ b.WithKind("NodeVulnerabilityReport")
+ b.WithAPIVersion("storage.sbomscanner.kubewarden.io/v1alpha1")
+ return b
+}
+
+func (b NodeVulnerabilityReportApplyConfiguration) IsApplyConfiguration() {}
+
+// WithKind sets the Kind field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Kind field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithKind(value string) *NodeVulnerabilityReportApplyConfiguration {
+ b.TypeMetaApplyConfiguration.Kind = &value
+ return b
+}
+
+// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the APIVersion field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithAPIVersion(value string) *NodeVulnerabilityReportApplyConfiguration {
+ b.TypeMetaApplyConfiguration.APIVersion = &value
+ return b
+}
+
+// WithName sets the Name field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Name field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithName(value string) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Name = &value
+ return b
+}
+
+// WithGenerateName sets the GenerateName field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the GenerateName field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithGenerateName(value string) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.GenerateName = &value
+ return b
+}
+
+// WithNamespace sets the Namespace field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Namespace field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithNamespace(value string) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Namespace = &value
+ return b
+}
+
+// WithUID sets the UID field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the UID field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithUID(value types.UID) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.UID = &value
+ return b
+}
+
+// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the ResourceVersion field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithResourceVersion(value string) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.ResourceVersion = &value
+ return b
+}
+
+// WithGeneration sets the Generation field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Generation field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithGeneration(value int64) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.Generation = &value
+ return b
+}
+
+// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the CreationTimestamp field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithCreationTimestamp(value metav1.Time) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.CreationTimestamp = &value
+ return b
+}
+
+// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionTimestamp field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value
+ return b
+}
+
+// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value
+ return b
+}
+
+// WithLabels puts the entries into the Labels field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Labels field,
+// overwriting an existing map entries in Labels field with the same key.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithLabels(entries map[string]string) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Labels[k] = v
+ }
+ return b
+}
+
+// WithAnnotations puts the entries into the Annotations field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, the entries provided by each call will be put on the Annotations field,
+// overwriting an existing map entries in Annotations field with the same key.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithAnnotations(entries map[string]string) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 {
+ b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries))
+ }
+ for k, v := range entries {
+ b.ObjectMetaApplyConfiguration.Annotations[k] = v
+ }
+ return b
+}
+
+// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the OwnerReferences field.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ if values[i] == nil {
+ panic("nil value passed to WithOwnerReferences")
+ }
+ b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i])
+ }
+ return b
+}
+
+// WithFinalizers adds the given value to the Finalizers field in the declarative configuration
+// and returns the receiver, so that objects can be build by chaining "With" function invocations.
+// If called multiple times, values provided by each call will be appended to the Finalizers field.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithFinalizers(values ...string) *NodeVulnerabilityReportApplyConfiguration {
+ b.ensureObjectMetaApplyConfigurationExists()
+ for i := range values {
+ b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i])
+ }
+ return b
+}
+
+func (b *NodeVulnerabilityReportApplyConfiguration) ensureObjectMetaApplyConfigurationExists() {
+ if b.ObjectMetaApplyConfiguration == nil {
+ b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{}
+ }
+}
+
+// WithNodeMetadata sets the NodeMetadata field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the NodeMetadata field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithNodeMetadata(value *NodeMetadataApplyConfiguration) *NodeVulnerabilityReportApplyConfiguration {
+ b.NodeMetadata = value
+ return b
+}
+
+// WithReport sets the Report field in the declarative configuration to the given value
+// and returns the receiver, so that objects can be built by chaining "With" function invocations.
+// If called multiple times, the Report field is set to the value of the last call.
+func (b *NodeVulnerabilityReportApplyConfiguration) WithReport(value *ReportApplyConfiguration) *NodeVulnerabilityReportApplyConfiguration {
+ b.Report = value
+ return b
+}
+
+// GetKind retrieves the value of the Kind field in the declarative configuration.
+func (b *NodeVulnerabilityReportApplyConfiguration) GetKind() *string {
+ return b.TypeMetaApplyConfiguration.Kind
+}
+
+// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration.
+func (b *NodeVulnerabilityReportApplyConfiguration) GetAPIVersion() *string {
+ return b.TypeMetaApplyConfiguration.APIVersion
+}
+
+// GetName retrieves the value of the Name field in the declarative configuration.
+func (b *NodeVulnerabilityReportApplyConfiguration) GetName() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Name
+}
+
+// GetNamespace retrieves the value of the Namespace field in the declarative configuration.
+func (b *NodeVulnerabilityReportApplyConfiguration) GetNamespace() *string {
+ b.ensureObjectMetaApplyConfigurationExists()
+ return b.ObjectMetaApplyConfiguration.Namespace
+}
diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go
index 25686616c..65fcc9e37 100644
--- a/pkg/generated/applyconfiguration/utils.go
+++ b/pkg/generated/applyconfiguration/utils.go
@@ -36,6 +36,12 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &storagev1alpha1.ImageStatusApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("ImageWorkloadScanReports"):
return &storagev1alpha1.ImageWorkloadScanReportsApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NodeMetadata"):
+ return &storagev1alpha1.NodeMetadataApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NodeSBOM"):
+ return &storagev1alpha1.NodeSBOMApplyConfiguration{}
+ case v1alpha1.SchemeGroupVersion.WithKind("NodeVulnerabilityReport"):
+ return &storagev1alpha1.NodeVulnerabilityReportApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("Report"):
return &storagev1alpha1.ReportApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("Result"):
diff --git a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_nodesbom.go b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_nodesbom.go
new file mode 100644
index 000000000..d75a64c3c
--- /dev/null
+++ b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_nodesbom.go
@@ -0,0 +1,35 @@
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+ v1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/applyconfiguration/storage/v1alpha1"
+ typedstoragev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/clientset/versioned/typed/storage/v1alpha1"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// fakeNodeSBOMs implements NodeSBOMInterface
+type fakeNodeSBOMs struct {
+ *gentype.FakeClientWithListAndApply[*v1alpha1.NodeSBOM, *v1alpha1.NodeSBOMList, *storagev1alpha1.NodeSBOMApplyConfiguration]
+ Fake *FakeStorageV1alpha1
+}
+
+func newFakeNodeSBOMs(fake *FakeStorageV1alpha1) typedstoragev1alpha1.NodeSBOMInterface {
+ return &fakeNodeSBOMs{
+ gentype.NewFakeClientWithListAndApply[*v1alpha1.NodeSBOM, *v1alpha1.NodeSBOMList, *storagev1alpha1.NodeSBOMApplyConfiguration](
+ fake.Fake,
+ "",
+ v1alpha1.SchemeGroupVersion.WithResource("nodesboms"),
+ v1alpha1.SchemeGroupVersion.WithKind("NodeSBOM"),
+ func() *v1alpha1.NodeSBOM { return &v1alpha1.NodeSBOM{} },
+ func() *v1alpha1.NodeSBOMList { return &v1alpha1.NodeSBOMList{} },
+ func(dst, src *v1alpha1.NodeSBOMList) { dst.ListMeta = src.ListMeta },
+ func(list *v1alpha1.NodeSBOMList) []*v1alpha1.NodeSBOM { return gentype.ToPointerSlice(list.Items) },
+ func(list *v1alpha1.NodeSBOMList, items []*v1alpha1.NodeSBOM) {
+ list.Items = gentype.FromPointerSlice(items)
+ },
+ ),
+ fake,
+ }
+}
diff --git a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_nodevulnerabilityreport.go b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_nodevulnerabilityreport.go
new file mode 100644
index 000000000..a1a82fa21
--- /dev/null
+++ b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_nodevulnerabilityreport.go
@@ -0,0 +1,37 @@
+// Code generated by client-gen. DO NOT EDIT.
+
+package fake
+
+import (
+ v1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/applyconfiguration/storage/v1alpha1"
+ typedstoragev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/clientset/versioned/typed/storage/v1alpha1"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// fakeNodeVulnerabilityReports implements NodeVulnerabilityReportInterface
+type fakeNodeVulnerabilityReports struct {
+ *gentype.FakeClientWithListAndApply[*v1alpha1.NodeVulnerabilityReport, *v1alpha1.NodeVulnerabilityReportList, *storagev1alpha1.NodeVulnerabilityReportApplyConfiguration]
+ Fake *FakeStorageV1alpha1
+}
+
+func newFakeNodeVulnerabilityReports(fake *FakeStorageV1alpha1) typedstoragev1alpha1.NodeVulnerabilityReportInterface {
+ return &fakeNodeVulnerabilityReports{
+ gentype.NewFakeClientWithListAndApply[*v1alpha1.NodeVulnerabilityReport, *v1alpha1.NodeVulnerabilityReportList, *storagev1alpha1.NodeVulnerabilityReportApplyConfiguration](
+ fake.Fake,
+ "",
+ v1alpha1.SchemeGroupVersion.WithResource("nodevulnerabilityreports"),
+ v1alpha1.SchemeGroupVersion.WithKind("NodeVulnerabilityReport"),
+ func() *v1alpha1.NodeVulnerabilityReport { return &v1alpha1.NodeVulnerabilityReport{} },
+ func() *v1alpha1.NodeVulnerabilityReportList { return &v1alpha1.NodeVulnerabilityReportList{} },
+ func(dst, src *v1alpha1.NodeVulnerabilityReportList) { dst.ListMeta = src.ListMeta },
+ func(list *v1alpha1.NodeVulnerabilityReportList) []*v1alpha1.NodeVulnerabilityReport {
+ return gentype.ToPointerSlice(list.Items)
+ },
+ func(list *v1alpha1.NodeVulnerabilityReportList, items []*v1alpha1.NodeVulnerabilityReport) {
+ list.Items = gentype.FromPointerSlice(items)
+ },
+ ),
+ fake,
+ }
+}
diff --git a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_storage_client.go b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_storage_client.go
index 75de77209..7eaaeb33e 100644
--- a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_storage_client.go
+++ b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/fake/fake_storage_client.go
@@ -16,6 +16,14 @@ func (c *FakeStorageV1alpha1) Images(namespace string) v1alpha1.ImageInterface {
return newFakeImages(c, namespace)
}
+func (c *FakeStorageV1alpha1) NodeSBOMs() v1alpha1.NodeSBOMInterface {
+ return newFakeNodeSBOMs(c)
+}
+
+func (c *FakeStorageV1alpha1) NodeVulnerabilityReports() v1alpha1.NodeVulnerabilityReportInterface {
+ return newFakeNodeVulnerabilityReports(c)
+}
+
func (c *FakeStorageV1alpha1) SBOMs(namespace string) v1alpha1.SBOMInterface {
return newFakeSBOMs(c, namespace)
}
diff --git a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/generated_expansion.go b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/generated_expansion.go
index c59cd7502..8f660d0e0 100644
--- a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/generated_expansion.go
+++ b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/generated_expansion.go
@@ -4,6 +4,10 @@ package v1alpha1
type ImageExpansion interface{}
+type NodeSBOMExpansion interface{}
+
+type NodeVulnerabilityReportExpansion interface{}
+
type SBOMExpansion interface{}
type VulnerabilityReportExpansion interface{}
diff --git a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/nodesbom.go b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/nodesbom.go
new file mode 100644
index 000000000..d6bed2132
--- /dev/null
+++ b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/nodesbom.go
@@ -0,0 +1,54 @@
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ applyconfigurationstoragev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/applyconfiguration/storage/v1alpha1"
+ scheme "github.com/kubewarden/sbomscanner/pkg/generated/clientset/versioned/scheme"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ watch "k8s.io/apimachinery/pkg/watch"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// NodeSBOMsGetter has a method to return a NodeSBOMInterface.
+// A group's client should implement this interface.
+type NodeSBOMsGetter interface {
+ NodeSBOMs() NodeSBOMInterface
+}
+
+// NodeSBOMInterface has methods to work with NodeSBOM resources.
+type NodeSBOMInterface interface {
+ Create(ctx context.Context, nodeSBOM *storagev1alpha1.NodeSBOM, opts v1.CreateOptions) (*storagev1alpha1.NodeSBOM, error)
+ Update(ctx context.Context, nodeSBOM *storagev1alpha1.NodeSBOM, opts v1.UpdateOptions) (*storagev1alpha1.NodeSBOM, error)
+ Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+ DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+ Get(ctx context.Context, name string, opts v1.GetOptions) (*storagev1alpha1.NodeSBOM, error)
+ List(ctx context.Context, opts v1.ListOptions) (*storagev1alpha1.NodeSBOMList, error)
+ Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+ Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *storagev1alpha1.NodeSBOM, err error)
+ Apply(ctx context.Context, nodeSBOM *applyconfigurationstoragev1alpha1.NodeSBOMApplyConfiguration, opts v1.ApplyOptions) (result *storagev1alpha1.NodeSBOM, err error)
+ NodeSBOMExpansion
+}
+
+// nodeSBOMs implements NodeSBOMInterface
+type nodeSBOMs struct {
+ *gentype.ClientWithListAndApply[*storagev1alpha1.NodeSBOM, *storagev1alpha1.NodeSBOMList, *applyconfigurationstoragev1alpha1.NodeSBOMApplyConfiguration]
+}
+
+// newNodeSBOMs returns a NodeSBOMs
+func newNodeSBOMs(c *StorageV1alpha1Client) *nodeSBOMs {
+ return &nodeSBOMs{
+ gentype.NewClientWithListAndApply[*storagev1alpha1.NodeSBOM, *storagev1alpha1.NodeSBOMList, *applyconfigurationstoragev1alpha1.NodeSBOMApplyConfiguration](
+ "nodesboms",
+ c.RESTClient(),
+ scheme.ParameterCodec,
+ "",
+ func() *storagev1alpha1.NodeSBOM { return &storagev1alpha1.NodeSBOM{} },
+ func() *storagev1alpha1.NodeSBOMList { return &storagev1alpha1.NodeSBOMList{} },
+ ),
+ }
+}
diff --git a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/nodevulnerabilityreport.go b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/nodevulnerabilityreport.go
new file mode 100644
index 000000000..736a1682d
--- /dev/null
+++ b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/nodevulnerabilityreport.go
@@ -0,0 +1,56 @@
+// Code generated by client-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ applyconfigurationstoragev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/applyconfiguration/storage/v1alpha1"
+ scheme "github.com/kubewarden/sbomscanner/pkg/generated/clientset/versioned/scheme"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ types "k8s.io/apimachinery/pkg/types"
+ watch "k8s.io/apimachinery/pkg/watch"
+ gentype "k8s.io/client-go/gentype"
+)
+
+// NodeVulnerabilityReportsGetter has a method to return a NodeVulnerabilityReportInterface.
+// A group's client should implement this interface.
+type NodeVulnerabilityReportsGetter interface {
+ NodeVulnerabilityReports() NodeVulnerabilityReportInterface
+}
+
+// NodeVulnerabilityReportInterface has methods to work with NodeVulnerabilityReport resources.
+type NodeVulnerabilityReportInterface interface {
+ Create(ctx context.Context, nodeVulnerabilityReport *storagev1alpha1.NodeVulnerabilityReport, opts v1.CreateOptions) (*storagev1alpha1.NodeVulnerabilityReport, error)
+ Update(ctx context.Context, nodeVulnerabilityReport *storagev1alpha1.NodeVulnerabilityReport, opts v1.UpdateOptions) (*storagev1alpha1.NodeVulnerabilityReport, error)
+ Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
+ DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
+ Get(ctx context.Context, name string, opts v1.GetOptions) (*storagev1alpha1.NodeVulnerabilityReport, error)
+ List(ctx context.Context, opts v1.ListOptions) (*storagev1alpha1.NodeVulnerabilityReportList, error)
+ Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
+ Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *storagev1alpha1.NodeVulnerabilityReport, err error)
+ Apply(ctx context.Context, nodeVulnerabilityReport *applyconfigurationstoragev1alpha1.NodeVulnerabilityReportApplyConfiguration, opts v1.ApplyOptions) (result *storagev1alpha1.NodeVulnerabilityReport, err error)
+ NodeVulnerabilityReportExpansion
+}
+
+// nodeVulnerabilityReports implements NodeVulnerabilityReportInterface
+type nodeVulnerabilityReports struct {
+ *gentype.ClientWithListAndApply[*storagev1alpha1.NodeVulnerabilityReport, *storagev1alpha1.NodeVulnerabilityReportList, *applyconfigurationstoragev1alpha1.NodeVulnerabilityReportApplyConfiguration]
+}
+
+// newNodeVulnerabilityReports returns a NodeVulnerabilityReports
+func newNodeVulnerabilityReports(c *StorageV1alpha1Client) *nodeVulnerabilityReports {
+ return &nodeVulnerabilityReports{
+ gentype.NewClientWithListAndApply[*storagev1alpha1.NodeVulnerabilityReport, *storagev1alpha1.NodeVulnerabilityReportList, *applyconfigurationstoragev1alpha1.NodeVulnerabilityReportApplyConfiguration](
+ "nodevulnerabilityreports",
+ c.RESTClient(),
+ scheme.ParameterCodec,
+ "",
+ func() *storagev1alpha1.NodeVulnerabilityReport { return &storagev1alpha1.NodeVulnerabilityReport{} },
+ func() *storagev1alpha1.NodeVulnerabilityReportList {
+ return &storagev1alpha1.NodeVulnerabilityReportList{}
+ },
+ ),
+ }
+}
diff --git a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/storage_client.go b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/storage_client.go
index ea6eca370..ed9e0ce7f 100644
--- a/pkg/generated/clientset/versioned/typed/storage/v1alpha1/storage_client.go
+++ b/pkg/generated/clientset/versioned/typed/storage/v1alpha1/storage_client.go
@@ -13,6 +13,8 @@ import (
type StorageV1alpha1Interface interface {
RESTClient() rest.Interface
ImagesGetter
+ NodeSBOMsGetter
+ NodeVulnerabilityReportsGetter
SBOMsGetter
VulnerabilityReportsGetter
WorkloadScanReportsGetter
@@ -27,6 +29,14 @@ func (c *StorageV1alpha1Client) Images(namespace string) ImageInterface {
return newImages(c, namespace)
}
+func (c *StorageV1alpha1Client) NodeSBOMs() NodeSBOMInterface {
+ return newNodeSBOMs(c)
+}
+
+func (c *StorageV1alpha1Client) NodeVulnerabilityReports() NodeVulnerabilityReportInterface {
+ return newNodeVulnerabilityReports(c)
+}
+
func (c *StorageV1alpha1Client) SBOMs(namespace string) SBOMInterface {
return newSBOMs(c, namespace)
}
diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go
index 3f196bfaf..c5ba9d591 100644
--- a/pkg/generated/informers/externalversions/generic.go
+++ b/pkg/generated/informers/externalversions/generic.go
@@ -39,6 +39,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource
// Group=storage.sbomscanner.kubewarden.io, Version=v1alpha1
case v1alpha1.SchemeGroupVersion.WithResource("images"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Storage().V1alpha1().Images().Informer()}, nil
+ case v1alpha1.SchemeGroupVersion.WithResource("nodesboms"):
+ return &genericInformer{resource: resource.GroupResource(), informer: f.Storage().V1alpha1().NodeSBOMs().Informer()}, nil
+ case v1alpha1.SchemeGroupVersion.WithResource("nodevulnerabilityreports"):
+ return &genericInformer{resource: resource.GroupResource(), informer: f.Storage().V1alpha1().NodeVulnerabilityReports().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("sboms"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Storage().V1alpha1().SBOMs().Informer()}, nil
case v1alpha1.SchemeGroupVersion.WithResource("vulnerabilityreports"):
diff --git a/pkg/generated/informers/externalversions/storage/v1alpha1/interface.go b/pkg/generated/informers/externalversions/storage/v1alpha1/interface.go
index ed58b463d..719d59b14 100644
--- a/pkg/generated/informers/externalversions/storage/v1alpha1/interface.go
+++ b/pkg/generated/informers/externalversions/storage/v1alpha1/interface.go
@@ -10,6 +10,10 @@ import (
type Interface interface {
// Images returns a ImageInformer.
Images() ImageInformer
+ // NodeSBOMs returns a NodeSBOMInformer.
+ NodeSBOMs() NodeSBOMInformer
+ // NodeVulnerabilityReports returns a NodeVulnerabilityReportInformer.
+ NodeVulnerabilityReports() NodeVulnerabilityReportInformer
// SBOMs returns a SBOMInformer.
SBOMs() SBOMInformer
// VulnerabilityReports returns a VulnerabilityReportInformer.
@@ -34,6 +38,16 @@ func (v *version) Images() ImageInformer {
return &imageInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}
+// NodeSBOMs returns a NodeSBOMInformer.
+func (v *version) NodeSBOMs() NodeSBOMInformer {
+ return &nodeSBOMInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
+}
+
+// NodeVulnerabilityReports returns a NodeVulnerabilityReportInformer.
+func (v *version) NodeVulnerabilityReports() NodeVulnerabilityReportInformer {
+ return &nodeVulnerabilityReportInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
+}
+
// SBOMs returns a SBOMInformer.
func (v *version) SBOMs() SBOMInformer {
return &sBOMInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
diff --git a/pkg/generated/informers/externalversions/storage/v1alpha1/nodesbom.go b/pkg/generated/informers/externalversions/storage/v1alpha1/nodesbom.go
new file mode 100644
index 000000000..24bb76292
--- /dev/null
+++ b/pkg/generated/informers/externalversions/storage/v1alpha1/nodesbom.go
@@ -0,0 +1,99 @@
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+ time "time"
+
+ apistoragev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ versioned "github.com/kubewarden/sbomscanner/pkg/generated/clientset/versioned"
+ internalinterfaces "github.com/kubewarden/sbomscanner/pkg/generated/informers/externalversions/internalinterfaces"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/listers/storage/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+ schema "k8s.io/apimachinery/pkg/runtime/schema"
+ watch "k8s.io/apimachinery/pkg/watch"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// NodeSBOMInformer provides access to a shared informer and lister for
+// NodeSBOMs.
+type NodeSBOMInformer interface {
+ Informer() cache.SharedIndexInformer
+ Lister() storagev1alpha1.NodeSBOMLister
+}
+
+type nodeSBOMInformer struct {
+ factory internalinterfaces.SharedInformerFactory
+ tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// NewNodeSBOMInformer constructs a new informer for NodeSBOM type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewNodeSBOMInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+ return NewNodeSBOMInformerWithOptions(client, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
+}
+
+// NewFilteredNodeSBOMInformer constructs a new informer for NodeSBOM type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredNodeSBOMInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+ return NewNodeSBOMInformerWithOptions(client, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
+}
+
+// NewNodeSBOMInformerWithOptions constructs a new informer for NodeSBOM type with additional options.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewNodeSBOMInformerWithOptions(client versioned.Interface, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
+ gvr := schema.GroupVersionResource{Group: "storage.sbomscanner.kubewarden.io", Version: "v1alpha1", Resource: "nodesboms"}
+ identifier := options.InformerName.WithResource(gvr)
+ tweakListOptions := options.TweakListOptions
+ return cache.NewSharedIndexInformerWithOptions(
+ cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
+ ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeSBOMs().List(context.Background(), opts)
+ },
+ WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeSBOMs().Watch(context.Background(), opts)
+ },
+ ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeSBOMs().List(ctx, opts)
+ },
+ WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeSBOMs().Watch(ctx, opts)
+ },
+ }, client),
+ &apistoragev1alpha1.NodeSBOM{},
+ cache.SharedIndexInformerOptions{
+ ResyncPeriod: options.ResyncPeriod,
+ Indexers: options.Indexers,
+ Identifier: identifier,
+ },
+ )
+}
+
+func (f *nodeSBOMInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+ return NewNodeSBOMInformerWithOptions(client, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
+}
+
+func (f *nodeSBOMInformer) Informer() cache.SharedIndexInformer {
+ return f.factory.InformerFor(&apistoragev1alpha1.NodeSBOM{}, f.defaultInformer)
+}
+
+func (f *nodeSBOMInformer) Lister() storagev1alpha1.NodeSBOMLister {
+ return storagev1alpha1.NewNodeSBOMLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/generated/informers/externalversions/storage/v1alpha1/nodevulnerabilityreport.go b/pkg/generated/informers/externalversions/storage/v1alpha1/nodevulnerabilityreport.go
new file mode 100644
index 000000000..ae862816f
--- /dev/null
+++ b/pkg/generated/informers/externalversions/storage/v1alpha1/nodevulnerabilityreport.go
@@ -0,0 +1,99 @@
+// Code generated by informer-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ context "context"
+ time "time"
+
+ apistoragev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ versioned "github.com/kubewarden/sbomscanner/pkg/generated/clientset/versioned"
+ internalinterfaces "github.com/kubewarden/sbomscanner/pkg/generated/informers/externalversions/internalinterfaces"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/pkg/generated/listers/storage/v1alpha1"
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+ schema "k8s.io/apimachinery/pkg/runtime/schema"
+ watch "k8s.io/apimachinery/pkg/watch"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// NodeVulnerabilityReportInformer provides access to a shared informer and lister for
+// NodeVulnerabilityReports.
+type NodeVulnerabilityReportInformer interface {
+ Informer() cache.SharedIndexInformer
+ Lister() storagev1alpha1.NodeVulnerabilityReportLister
+}
+
+type nodeVulnerabilityReportInformer struct {
+ factory internalinterfaces.SharedInformerFactory
+ tweakListOptions internalinterfaces.TweakListOptionsFunc
+}
+
+// NewNodeVulnerabilityReportInformer constructs a new informer for NodeVulnerabilityReport type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewNodeVulnerabilityReportInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
+ return NewNodeVulnerabilityReportInformerWithOptions(client, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers})
+}
+
+// NewFilteredNodeVulnerabilityReportInformer constructs a new informer for NodeVulnerabilityReport type.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewFilteredNodeVulnerabilityReportInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
+ return NewNodeVulnerabilityReportInformerWithOptions(client, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: indexers, TweakListOptions: tweakListOptions})
+}
+
+// NewNodeVulnerabilityReportInformerWithOptions constructs a new informer for NodeVulnerabilityReport type with additional options.
+// Always prefer using an informer factory to get a shared informer instead of getting an independent
+// one. This reduces memory footprint and number of connections to the server.
+func NewNodeVulnerabilityReportInformerWithOptions(client versioned.Interface, options internalinterfaces.InformerOptions) cache.SharedIndexInformer {
+ gvr := schema.GroupVersionResource{Group: "storage.sbomscanner.kubewarden.io", Version: "v1alpha1", Resource: "nodevulnerabilityreports"}
+ identifier := options.InformerName.WithResource(gvr)
+ tweakListOptions := options.TweakListOptions
+ return cache.NewSharedIndexInformerWithOptions(
+ cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{
+ ListFunc: func(opts v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeVulnerabilityReports().List(context.Background(), opts)
+ },
+ WatchFunc: func(opts v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeVulnerabilityReports().Watch(context.Background(), opts)
+ },
+ ListWithContextFunc: func(ctx context.Context, opts v1.ListOptions) (runtime.Object, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeVulnerabilityReports().List(ctx, opts)
+ },
+ WatchFuncWithContext: func(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) {
+ if tweakListOptions != nil {
+ tweakListOptions(&opts)
+ }
+ return client.StorageV1alpha1().NodeVulnerabilityReports().Watch(ctx, opts)
+ },
+ }, client),
+ &apistoragev1alpha1.NodeVulnerabilityReport{},
+ cache.SharedIndexInformerOptions{
+ ResyncPeriod: options.ResyncPeriod,
+ Indexers: options.Indexers,
+ Identifier: identifier,
+ },
+ )
+}
+
+func (f *nodeVulnerabilityReportInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
+ return NewNodeVulnerabilityReportInformerWithOptions(client, internalinterfaces.InformerOptions{ResyncPeriod: resyncPeriod, Indexers: cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, InformerName: f.factory.InformerName(), TweakListOptions: f.tweakListOptions})
+}
+
+func (f *nodeVulnerabilityReportInformer) Informer() cache.SharedIndexInformer {
+ return f.factory.InformerFor(&apistoragev1alpha1.NodeVulnerabilityReport{}, f.defaultInformer)
+}
+
+func (f *nodeVulnerabilityReportInformer) Lister() storagev1alpha1.NodeVulnerabilityReportLister {
+ return storagev1alpha1.NewNodeVulnerabilityReportLister(f.Informer().GetIndexer())
+}
diff --git a/pkg/generated/listers/storage/v1alpha1/expansion_generated.go b/pkg/generated/listers/storage/v1alpha1/expansion_generated.go
index 7ea30ef6b..8b114429a 100644
--- a/pkg/generated/listers/storage/v1alpha1/expansion_generated.go
+++ b/pkg/generated/listers/storage/v1alpha1/expansion_generated.go
@@ -10,6 +10,14 @@ type ImageListerExpansion interface{}
// ImageNamespaceLister.
type ImageNamespaceListerExpansion interface{}
+// NodeSBOMListerExpansion allows custom methods to be added to
+// NodeSBOMLister.
+type NodeSBOMListerExpansion interface{}
+
+// NodeVulnerabilityReportListerExpansion allows custom methods to be added to
+// NodeVulnerabilityReportLister.
+type NodeVulnerabilityReportListerExpansion interface{}
+
// SBOMListerExpansion allows custom methods to be added to
// SBOMLister.
type SBOMListerExpansion interface{}
diff --git a/pkg/generated/listers/storage/v1alpha1/nodesbom.go b/pkg/generated/listers/storage/v1alpha1/nodesbom.go
new file mode 100644
index 000000000..4c3196102
--- /dev/null
+++ b/pkg/generated/listers/storage/v1alpha1/nodesbom.go
@@ -0,0 +1,32 @@
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ listers "k8s.io/client-go/listers"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// NodeSBOMLister helps list NodeSBOMs.
+// All objects returned here must be treated as read-only.
+type NodeSBOMLister interface {
+ // List lists all NodeSBOMs in the indexer.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*storagev1alpha1.NodeSBOM, err error)
+ // Get retrieves the NodeSBOM from the index for a given name.
+ // Objects returned here must be treated as read-only.
+ Get(name string) (*storagev1alpha1.NodeSBOM, error)
+ NodeSBOMListerExpansion
+}
+
+// nodeSBOMLister implements the NodeSBOMLister interface.
+type nodeSBOMLister struct {
+ listers.ResourceIndexer[*storagev1alpha1.NodeSBOM]
+}
+
+// NewNodeSBOMLister returns a new NodeSBOMLister.
+func NewNodeSBOMLister(indexer cache.Indexer) NodeSBOMLister {
+ return &nodeSBOMLister{listers.New[*storagev1alpha1.NodeSBOM](indexer, storagev1alpha1.Resource("nodesbom"))}
+}
diff --git a/pkg/generated/listers/storage/v1alpha1/nodevulnerabilityreport.go b/pkg/generated/listers/storage/v1alpha1/nodevulnerabilityreport.go
new file mode 100644
index 000000000..67b7faf91
--- /dev/null
+++ b/pkg/generated/listers/storage/v1alpha1/nodevulnerabilityreport.go
@@ -0,0 +1,32 @@
+// Code generated by lister-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ labels "k8s.io/apimachinery/pkg/labels"
+ listers "k8s.io/client-go/listers"
+ cache "k8s.io/client-go/tools/cache"
+)
+
+// NodeVulnerabilityReportLister helps list NodeVulnerabilityReports.
+// All objects returned here must be treated as read-only.
+type NodeVulnerabilityReportLister interface {
+ // List lists all NodeVulnerabilityReports in the indexer.
+ // Objects returned here must be treated as read-only.
+ List(selector labels.Selector) (ret []*storagev1alpha1.NodeVulnerabilityReport, err error)
+ // Get retrieves the NodeVulnerabilityReport from the index for a given name.
+ // Objects returned here must be treated as read-only.
+ Get(name string) (*storagev1alpha1.NodeVulnerabilityReport, error)
+ NodeVulnerabilityReportListerExpansion
+}
+
+// nodeVulnerabilityReportLister implements the NodeVulnerabilityReportLister interface.
+type nodeVulnerabilityReportLister struct {
+ listers.ResourceIndexer[*storagev1alpha1.NodeVulnerabilityReport]
+}
+
+// NewNodeVulnerabilityReportLister returns a new NodeVulnerabilityReportLister.
+func NewNodeVulnerabilityReportLister(indexer cache.Indexer) NodeVulnerabilityReportLister {
+ return &nodeVulnerabilityReportLister{listers.New[*storagev1alpha1.NodeVulnerabilityReport](indexer, storagev1alpha1.Resource("nodevulnerabilityreport"))}
+}
diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go
index 5aed7cafb..3be37d70b 100644
--- a/pkg/generated/openapi/zz_generated.openapi.go
+++ b/pkg/generated/openapi/zz_generated.openapi.go
@@ -28,6 +28,11 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
v1alpha1.ImageRef{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_ImageRef(ref),
v1alpha1.ImageStatus{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_ImageStatus(ref),
v1alpha1.ImageWorkloadScanReports{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_ImageWorkloadScanReports(ref),
+ v1alpha1.NodeMetadata{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_NodeMetadata(ref),
+ v1alpha1.NodeSBOM{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_NodeSBOM(ref),
+ v1alpha1.NodeSBOMList{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_NodeSBOMList(ref),
+ v1alpha1.NodeVulnerabilityReport{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_NodeVulnerabilityReport(ref),
+ v1alpha1.NodeVulnerabilityReportList{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_NodeVulnerabilityReportList(ref),
v1alpha1.Report{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_Report(ref),
v1alpha1.Result{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_Result(ref),
v1alpha1.SBOM{}.OpenAPIModelName(): schema_sbomscanner_api_storage_v1alpha1_SBOM(ref),
@@ -554,6 +559,232 @@ func schema_sbomscanner_api_storage_v1alpha1_ImageWorkloadScanReports(ref common
}
}
+func schema_sbomscanner_api_storage_v1alpha1_NodeMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "NodeMetadata contains the metadata details of a node.",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "name": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Name specifies the name of the node.",
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "platform": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Platform specifies the platform of the image. Example \"linux/amd64\".",
+ Default: "",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ },
+ Required: []string{"name", "platform"},
+ },
+ },
+ }
+}
+
+func schema_sbomscanner_api_storage_v1alpha1_NodeSBOM(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "NodeSBOM represents a Software Bill of Materials of a node",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()),
+ },
+ },
+ "nodeMetadata": {
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref(v1alpha1.NodeMetadata{}.OpenAPIModelName()),
+ },
+ },
+ "spdx": {
+ SchemaProps: spec.SchemaProps{
+ Description: "SPDX contains the SPDX document of the SBOM in JSON format",
+ Ref: ref(runtime.RawExtension{}.OpenAPIModelName()),
+ },
+ },
+ },
+ Required: []string{"nodeMetadata", "spdx"},
+ },
+ },
+ Dependencies: []string{
+ v1alpha1.NodeMetadata{}.OpenAPIModelName(), v1.ObjectMeta{}.OpenAPIModelName(), runtime.RawExtension{}.OpenAPIModelName()},
+ }
+}
+
+func schema_sbomscanner_api_storage_v1alpha1_NodeSBOMList(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "NodeSBOMList contains a list of Software Bill of Materials for nodes",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref(v1.ListMeta{}.OpenAPIModelName()),
+ },
+ },
+ "items": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref(v1alpha1.NodeSBOM{}.OpenAPIModelName()),
+ },
+ },
+ },
+ },
+ },
+ },
+ Required: []string{"items"},
+ },
+ },
+ Dependencies: []string{
+ v1alpha1.NodeSBOM{}.OpenAPIModelName(), v1.ListMeta{}.OpenAPIModelName()},
+ }
+}
+
+func schema_sbomscanner_api_storage_v1alpha1_NodeVulnerabilityReport(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "NodeVulnerabilityReport is the Schema for the scanresults API",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()),
+ },
+ },
+ "nodeMetadata": {
+ SchemaProps: spec.SchemaProps{
+ Description: "NodeMetadata contains info about the scanned node",
+ Default: map[string]interface{}{},
+ Ref: ref(v1alpha1.NodeMetadata{}.OpenAPIModelName()),
+ },
+ },
+ "report": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Report is the actual vulnerability scan report",
+ Default: map[string]interface{}{},
+ Ref: ref(v1alpha1.Report{}.OpenAPIModelName()),
+ },
+ },
+ },
+ Required: []string{"nodeMetadata", "report"},
+ },
+ },
+ Dependencies: []string{
+ v1alpha1.NodeMetadata{}.OpenAPIModelName(), v1alpha1.Report{}.OpenAPIModelName(), v1.ObjectMeta{}.OpenAPIModelName()},
+ }
+}
+
+func schema_sbomscanner_api_storage_v1alpha1_NodeVulnerabilityReportList(ref common.ReferenceCallback) common.OpenAPIDefinition {
+ return common.OpenAPIDefinition{
+ Schema: spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Description: "NodeVulnerabilityReportList contains a list of NodeVulnerabilityReport",
+ Type: []string{"object"},
+ Properties: map[string]spec.Schema{
+ "kind": {
+ SchemaProps: spec.SchemaProps{
+ Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "apiVersion": {
+ SchemaProps: spec.SchemaProps{
+ Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources",
+ Type: []string{"string"},
+ Format: "",
+ },
+ },
+ "metadata": {
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref(v1.ListMeta{}.OpenAPIModelName()),
+ },
+ },
+ "items": {
+ SchemaProps: spec.SchemaProps{
+ Type: []string{"array"},
+ Items: &spec.SchemaOrArray{
+ Schema: &spec.Schema{
+ SchemaProps: spec.SchemaProps{
+ Default: map[string]interface{}{},
+ Ref: ref(v1alpha1.NodeVulnerabilityReport{}.OpenAPIModelName()),
+ },
+ },
+ },
+ },
+ },
+ },
+ Required: []string{"items"},
+ },
+ },
+ Dependencies: []string{
+ v1alpha1.NodeVulnerabilityReport{}.OpenAPIModelName(), v1.ListMeta{}.OpenAPIModelName()},
+ }
+}
+
func schema_sbomscanner_api_storage_v1alpha1_Report(ref common.ReferenceCallback) common.OpenAPIDefinition {
return common.OpenAPIDefinition{
Schema: spec.Schema{
diff --git a/test/crd/storage.sbomscanner.kubewarden.io_nodesboms.yaml b/test/crd/storage.sbomscanner.kubewarden.io_nodesboms.yaml
new file mode 100644
index 000000000..ee0eee0d5
--- /dev/null
+++ b/test/crd/storage.sbomscanner.kubewarden.io_nodesboms.yaml
@@ -0,0 +1,65 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.5
+ name: nodesboms.storage.sbomscanner.kubewarden.io
+spec:
+ group: storage.sbomscanner.kubewarden.io
+ names:
+ kind: NodeSBOM
+ listKind: NodeSBOMList
+ plural: nodesboms
+ singular: nodesbom
+ scope: Cluster
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: NodeSBOM represents a Software Bill of Materials of a node
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ nodeMetadata:
+ description: NodeMetadata contains the metadata details of a node.
+ properties:
+ name:
+ description: Name specifies the name of the node.
+ type: string
+ platform:
+ description: Platform specifies the platform of the image. Example
+ "linux/amd64".
+ type: string
+ required:
+ - name
+ - platform
+ type: object
+ spdx:
+ description: SPDX contains the SPDX document of the SBOM in JSON format
+ type: object
+ x-kubernetes-preserve-unknown-fields: true
+ required:
+ - nodeMetadata
+ - spdx
+ type: object
+ selectableFields:
+ - jsonPath: .nodeMetadata.name
+ - jsonPath: .nodeMetadata.platform
+ served: true
+ storage: true
diff --git a/test/crd/storage.sbomscanner.kubewarden.io_nodevulnerabilityreports.yaml b/test/crd/storage.sbomscanner.kubewarden.io_nodevulnerabilityreports.yaml
new file mode 100644
index 000000000..34b6d2e7c
--- /dev/null
+++ b/test/crd/storage.sbomscanner.kubewarden.io_nodevulnerabilityreports.yaml
@@ -0,0 +1,227 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.5
+ name: nodevulnerabilityreports.storage.sbomscanner.kubewarden.io
+spec:
+ group: storage.sbomscanner.kubewarden.io
+ names:
+ kind: NodeVulnerabilityReport
+ listKind: NodeVulnerabilityReportList
+ plural: nodevulnerabilityreports
+ singular: nodevulnerabilityreport
+ scope: Cluster
+ versions:
+ - name: v1alpha1
+ schema:
+ openAPIV3Schema:
+ description: NodeVulnerabilityReport is the Schema for the scanresults API
+ properties:
+ apiVersion:
+ description: |-
+ APIVersion defines the versioned schema of this representation of an object.
+ Servers should convert recognized schemas to the latest internal value, and
+ may reject unrecognized values.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+ type: string
+ kind:
+ description: |-
+ Kind is a string value representing the REST resource this object represents.
+ Servers may infer this from the endpoint the client submits requests to.
+ Cannot be updated.
+ In CamelCase.
+ More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+ type: string
+ metadata:
+ type: object
+ nodeMetadata:
+ description: NodeMetadata contains info about the scanned node
+ properties:
+ name:
+ description: Name specifies the name of the node.
+ type: string
+ platform:
+ description: Platform specifies the platform of the image. Example
+ "linux/amd64".
+ type: string
+ required:
+ - name
+ - platform
+ type: object
+ report:
+ description: Report is the actual vulnerability scan report
+ properties:
+ results:
+ description: Results per target (e.g., layer, package type)
+ items:
+ description: Result represents scan findings for a specific target
+ and class of packages
+ properties:
+ class:
+ description: Class is the classification of the target
+ type: string
+ target:
+ description: Target is the specific target scanned
+ type: string
+ type:
+ description: Type is the language type
+ type: string
+ vulnerabilities:
+ description: Vulnerabilities found in this target
+ items:
+ description: |-
+ Vulnerability contains detailed information about a single vulnerability
+ found in a package
+ properties:
+ cve:
+ description: CVE identifier
+ type: string
+ cvss:
+ additionalProperties:
+ description: CVSS holds Common Vulnerability Scoring
+ System data for a vulnerability.
+ properties:
+ v3score:
+ description: V3Score numerical score
+ type: string
+ v3vector:
+ description: V3Vector string (e.g., "AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H")
+ type: string
+ required:
+ - v3score
+ - v3vector
+ type: object
+ description: CVSS scoring details
+ type: object
+ cwes:
+ description: CWEs with which the CVE is classified
+ items:
+ type: string
+ type: array
+ description:
+ description: Description of the vulnerability
+ type: string
+ diffID:
+ description: DiffID of the image layer where the vulnerability
+ was introduced
+ type: string
+ fixedVersions:
+ description: FixedVersions is the list of versions where
+ the vulnerability is fixed
+ items:
+ type: string
+ type: array
+ installedVersion:
+ description: InstalledVersion of the package that was
+ found
+ type: string
+ packageName:
+ description: |-
+ PackageName is the name of the vulnerable package
+ (empty when Class is "binary")
+ type: string
+ packagePath:
+ description: |-
+ PackagePath is the path where the package was found
+ (equal to Target when Class is "binary").
+ trivy removes the "/" at the beginning of the path
+ so we have to restore it.
+ type: string
+ purl:
+ description: PURL (Package URL) identify the package uniquely
+ type: string
+ references:
+ description: References contains URLs for more information
+ items:
+ type: string
+ type: array
+ severity:
+ description: Severity rating (e.g., "HIGH", "MEDIUM")
+ type: string
+ suppressed:
+ description: |-
+ Suppressed identify when vulnerability has
+ been suppressed by VEX documents
+ type: boolean
+ title:
+ description: Title is the title of the vulnerability
+ type: string
+ vexStatus:
+ description: VEXStatus information
+ properties:
+ repository:
+ description: Repository providing the VEX document
+ type: string
+ statement:
+ description: Statement optionally explain statement
+ from the VEX document
+ type: string
+ status:
+ description: VEX status (e.g., "not_affected", "fixed",
+ "under_investigation")
+ type: string
+ required:
+ - repository
+ - statement
+ - status
+ type: object
+ required:
+ - cve
+ - diffID
+ - installedVersion
+ - purl
+ - severity
+ - suppressed
+ type: object
+ type: array
+ required:
+ - class
+ - target
+ - type
+ - vulnerabilities
+ type: object
+ type: array
+ summary:
+ description: Summary of vulnerabilities found
+ properties:
+ critical:
+ description: Critical vulnerabilities count
+ type: integer
+ high:
+ description: High vulnerabilities count
+ type: integer
+ low:
+ description: Low vulnerabilities count
+ type: integer
+ medium:
+ description: Medium vulnerabilities count
+ type: integer
+ suppressed:
+ description: Suppressed vulnerabilities count
+ type: integer
+ unknown:
+ description: Unknown vulnerabilities count
+ type: integer
+ required:
+ - critical
+ - high
+ - low
+ - medium
+ - suppressed
+ - unknown
+ type: object
+ required:
+ - results
+ - summary
+ type: object
+ required:
+ - nodeMetadata
+ - report
+ type: object
+ selectableFields:
+ - jsonPath: .nodeMetadata.name
+ - jsonPath: .nodeMetadata.platform
+ served: true
+ storage: true
diff --git a/test/e2e/main_test.go b/test/e2e/main_test.go
index ce6c17295..50df2b028 100644
--- a/test/e2e/main_test.go
+++ b/test/e2e/main_test.go
@@ -38,7 +38,9 @@ func TestMain(m *testing.M) {
testenv.Setup(
envfuncs.CreateCluster(kind.NewProvider(), kindClusterName),
envfuncs.CreateNamespace(namespace, envfuncs.WithLabels(map[string]string{
- "pod-security.kubernetes.io/enforce": "restricted",
+ "pod-security.kubernetes.io/enforce": "privileged",
+ "pod-security.kubernetes.io/warn": "restricted",
+ "pod-security.kubernetes.io/audit": "restricted",
"pod-security.kubernetes.io/enforce-version": "latest",
})),
envfuncs.LoadImageToCluster(kindClusterName, workerImage, "--verbose", "--mode", "direct"),
diff --git a/test/e2e/nodescan_test.go b/test/e2e/nodescan_test.go
new file mode 100644
index 000000000..77c2c0e05
--- /dev/null
+++ b/test/e2e/nodescan_test.go
@@ -0,0 +1,212 @@
+package e2e
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/labels"
+ "sigs.k8s.io/e2e-framework/klient/k8s"
+ "sigs.k8s.io/e2e-framework/klient/k8s/resources"
+ "sigs.k8s.io/e2e-framework/klient/wait"
+ "sigs.k8s.io/e2e-framework/klient/wait/conditions"
+ "sigs.k8s.io/e2e-framework/pkg/envconf"
+ "sigs.k8s.io/e2e-framework/pkg/features"
+
+ "github.com/kubewarden/sbomscanner/api"
+ storagev1alpha1 "github.com/kubewarden/sbomscanner/api/storage/v1alpha1"
+ v1alpha1 "github.com/kubewarden/sbomscanner/api/v1alpha1"
+)
+
+func TestNodeScan(t *testing.T) {
+ nodeScanLabelSelector := labels.FormatLabels(map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ api.LabelNodeScanKey: api.LabelNodeScanValue,
+ })
+
+ f := features.New("Node Scan").
+ Assess("Create NodeScanConfiguration", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
+ nodeScanConfig := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ SkipPatterns: []string{
+ // Exclude containerd runtime files since they
+ // can cause issues with file access and are not
+ // relevant for node SBOM generation.
+ "/run/containerd/",
+ },
+ Platforms: []v1alpha1.Platform{
+ {Architecture: "amd64", OS: "linux"},
+ {Architecture: "arm64", OS: "linux"},
+ },
+ },
+ }
+ err := cfg.Client().Resources().Create(ctx, nodeScanConfig)
+ require.NoError(t, err, "failed to create NodeScanConfiguration")
+
+ return ctx
+ }).
+ Assess("Wait for NodeScanJob to be created", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ err := wait.For(
+ conditions.New(cfg.Client().Resources()).ResourceListN(
+ nodeScanJobs, 1,
+ resources.WithLabelSelector(nodeScanLabelSelector),
+ ),
+ wait.WithTimeout(scanTimeout),
+ )
+ require.NoError(t, err, "expected at least 1 NodeScanJob to be created")
+
+ return ctx
+ }).
+ Assess("Wait for NodeScanJob to complete", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
+ nodeScanJobs := &v1alpha1.NodeScanJobList{}
+ err := wait.For(func(ctx context.Context) (bool, error) {
+ if err := cfg.Client().Resources().List(ctx, nodeScanJobs,
+ resources.WithLabelSelector(nodeScanLabelSelector),
+ ); err != nil {
+ return false, err
+ }
+
+ if len(nodeScanJobs.Items) == 0 {
+ return false, nil
+ }
+
+ for i := range nodeScanJobs.Items {
+ if !nodeScanJobs.Items[i].IsComplete() {
+ return false, nil
+ }
+ }
+ return true, nil
+ }, wait.WithTimeout(scanTimeout))
+ require.NoError(t, err, "timeout waiting for NodeScanJobs to complete")
+
+ for _, job := range nodeScanJobs.Items {
+ assert.NotNil(t, job.Status.CompletionTime, "NodeScanJob %s should have a CompletionTime", job.Name)
+ }
+
+ return ctx
+ }).
+ Assess("Verify NodeSBOM is created", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
+ nodeSBOMs := &storagev1alpha1.NodeSBOMList{}
+ err := wait.For(
+ conditions.New(cfg.Client().Resources()).ResourceListN(
+ nodeSBOMs, 1,
+ resources.WithLabelSelector(labels.FormatLabels(map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ })),
+ ),
+ wait.WithTimeout(scanTimeout),
+ )
+ require.NoError(t, err, "expected at least 1 NodeSBOM to be created")
+
+ for _, sbom := range nodeSBOMs.Items {
+ assert.NotEmpty(t, sbom.NodeMetadata.Name, "NodeSBOM should have a node name")
+ assert.NotEmpty(t, sbom.NodeMetadata.Platform, "NodeSBOM should have a platform")
+ assert.NotEmpty(t, sbom.SPDX.Raw, "NodeSBOM should have SPDX data")
+ }
+
+ return ctx
+ }).
+ Assess("Verify NodeVulnerabilityReport is created", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
+ nodeVulnReports := &storagev1alpha1.NodeVulnerabilityReportList{}
+ err := wait.For(
+ conditions.New(cfg.Client().Resources()).ResourceListN(
+ nodeVulnReports, 1,
+ resources.WithLabelSelector(labels.FormatLabels(map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ })),
+ ),
+ wait.WithTimeout(scanTimeout),
+ )
+ require.NoError(t, err, "expected at least 1 NodeVulnerabilityReport to be created")
+
+ for _, report := range nodeVulnReports.Items {
+ assert.NotEmpty(t, report.NodeMetadata.Name, "NodeVulnerabilityReport should have a node name")
+ assert.NotEmpty(t, report.NodeMetadata.Platform, "NodeVulnerabilityReport should have a platform")
+ }
+
+ return ctx
+ }).
+ Assess("Delete NodeScanConfiguration and verify cleanup", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
+ nodeScanConfig := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ }
+ err := cfg.Client().Resources().Delete(ctx, nodeScanConfig)
+ require.NoError(t, err, "failed to delete NodeScanConfiguration")
+
+ err = wait.For(func(ctx context.Context) (bool, error) {
+ jobs := &v1alpha1.NodeScanJobList{}
+ if err := cfg.Client().Resources().List(ctx, jobs,
+ resources.WithLabelSelector(nodeScanLabelSelector),
+ ); err != nil {
+ return false, err
+ }
+ return len(jobs.Items) == 0, nil
+ }, wait.WithTimeout(scanTimeout))
+ require.NoError(t, err, "NodeScanJobs should be cleaned up after deleting NodeScanConfiguration")
+
+ err = wait.For(func(ctx context.Context) (bool, error) {
+ sboms := &storagev1alpha1.NodeSBOMList{}
+ if err := cfg.Client().Resources().List(ctx, sboms,
+ resources.WithLabelSelector(labels.FormatLabels(map[string]string{
+ api.LabelManagedByKey: api.LabelManagedByValue,
+ }))); err != nil {
+ return false, err
+ }
+ return len(sboms.Items) == 0, nil
+ }, wait.WithTimeout(scanTimeout))
+ require.NoError(t, err, "NodeSBOMs should be cleaned up after deleting NodeScanConfiguration")
+
+ return ctx
+ }).
+ Assess("Recreate NodeScanConfiguration with NodeSelector and verify filtering", func(ctx context.Context, t *testing.T, cfg *envconf.Config) context.Context {
+ nodeScanConfig := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ Spec: v1alpha1.NodeScanConfigurationSpec{
+ NodeSelector: &metav1.LabelSelector{
+ MatchLabels: map[string]string{
+ "non-existent-label": "no-match",
+ },
+ },
+ },
+ }
+ err := cfg.Client().Resources().Create(ctx, nodeScanConfig)
+ require.NoError(t, err, "failed to create NodeScanConfiguration with NodeSelector")
+
+ err = wait.For(conditions.New(cfg.Client().Resources()).ResourceMatch(nodeScanConfig, func(object k8s.Object) bool {
+ return object.(*v1alpha1.NodeScanConfiguration) != nil
+ }), wait.WithTimeout(scanTimeout))
+ require.NoError(t, err)
+
+ // No NodeScanJobs should be created since no nodes match the selector
+ jobs := &v1alpha1.NodeScanJobList{}
+ err = cfg.Client().Resources().List(ctx, jobs,
+ resources.WithLabelSelector(nodeScanLabelSelector),
+ )
+ require.NoError(t, err)
+ assert.Empty(t, jobs.Items, "no NodeScanJobs should exist when NodeSelector matches no nodes")
+
+ return ctx
+ }).
+ Assess("Final cleanup", func(ctx context.Context, _ *testing.T, cfg *envconf.Config) context.Context {
+ nodeScanConfig := &v1alpha1.NodeScanConfiguration{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: v1alpha1.NodeScanConfigurationName,
+ },
+ }
+ _ = cfg.Client().Resources().Delete(ctx, nodeScanConfig)
+
+ return ctx
+ })
+
+ testenv.Test(t, f.Feature())
+}
diff --git a/test/fixtures/node/go.mod b/test/fixtures/node/go.mod
new file mode 100644
index 000000000..e7153d55f
--- /dev/null
+++ b/test/fixtures/node/go.mod
@@ -0,0 +1,494 @@
+module github.com/kubewarden/sbomscanner
+
+go 1.26.3
+
+require (
+ github.com/aquasecurity/trivy v0.70.0
+ github.com/aquasecurity/trivy-db v0.0.0-20260429080452-bd11dd425d21
+ github.com/avast/retry-go/v4 v4.7.0
+ github.com/aws/smithy-go v1.25.1
+ github.com/docker/cli v29.4.3+incompatible
+ github.com/docker/go-units v0.5.0
+ github.com/go-logr/logr v1.4.3
+ github.com/google/cel-go v0.28.1
+ github.com/google/go-cmp v0.7.0
+ github.com/google/go-containerregistry v0.21.5
+ github.com/google/uuid v1.6.0
+ github.com/jackc/pgx/v5 v5.9.2
+ github.com/modelcontextprotocol/go-sdk v1.6.0
+ github.com/nats-io/nats-server/v2 v2.14.0
+ github.com/nats-io/nats.go v1.52.0
+ github.com/onsi/ginkgo/v2 v2.28.3
+ github.com/onsi/gomega v1.40.0
+ github.com/spdx/tools-golang v0.5.7
+ github.com/stephenafamo/bob v0.43.0
+ github.com/stretchr/testify v1.11.1
+ github.com/testcontainers/testcontainers-go v0.42.0
+ github.com/testcontainers/testcontainers-go/modules/postgres v0.42.0
+ github.com/testcontainers/testcontainers-go/modules/registry v0.42.0
+ go.yaml.in/yaml/v3 v3.0.4
+ golang.org/x/sync v0.20.0
+ golang.org/x/time v0.15.0
+ k8s.io/api v0.35.4
+ k8s.io/apimachinery v0.35.4
+ k8s.io/apiserver v0.35.4
+ k8s.io/client-go v0.35.4
+ k8s.io/code-generator v0.35.4
+ k8s.io/component-base v0.35.4
+ k8s.io/klog/v2 v2.140.0
+ k8s.io/kube-openapi v0.0.0-20260509192518-b540ad9def2b
+ k8s.io/utils v0.0.0-20260507154919-ff6756f316d2
+ modernc.org/sqlite v1.50.1
+ sigs.k8s.io/controller-runtime v0.23.3
+ sigs.k8s.io/e2e-framework v0.7.0
+ sigs.k8s.io/structured-merge-diff/v6 v6.4.0
+)
+
+require (
+ al.essio.dev/pkg/shellescape v1.6.0 // indirect
+ cel.dev/expr v0.25.1 // indirect
+ cloud.google.com/go v0.123.0 // indirect
+ cloud.google.com/go/auth v0.18.2 // 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/iam v1.5.3 // indirect
+ cloud.google.com/go/monitoring v1.24.3 // indirect
+ cloud.google.com/go/storage v1.61.3 // indirect
+ cyphar.com/go-pathrs v0.2.1 // indirect
+ dario.cat/mergo v1.0.2 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
+ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
+ github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
+ github.com/BurntSushi/toml v1.6.0 // indirect
+ github.com/CycloneDX/cyclonedx-go v0.10.0 // indirect
+ github.com/DataDog/zstd v1.5.7 // indirect
+ github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.32 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
+ github.com/Intevation/gval v1.3.0 // indirect
+ github.com/Intevation/jsonpath v0.2.1 // indirect
+ github.com/MakeNowJust/heredoc v1.0.0 // indirect
+ github.com/Masterminds/goutils v1.1.1 // indirect
+ github.com/Masterminds/semver/v3 v3.4.0 // indirect
+ github.com/Masterminds/sprig/v3 v3.3.0 // indirect
+ github.com/Masterminds/squirrel v1.5.4 // indirect
+ github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect
+ github.com/NYTimes/gziphandler v1.1.1 // indirect
+ github.com/ProtonMail/go-crypto v1.3.0 // indirect
+ github.com/VividCortex/ewma v1.2.0 // indirect
+ github.com/aarondl/opt v0.0.0-20250607033636-982744e1bd65 // indirect
+ github.com/agext/levenshtein v1.2.3 // indirect
+ github.com/agnivade/levenshtein v1.2.1 // indirect
+ github.com/alecthomas/chroma v0.10.0 // indirect
+ github.com/anchore/go-struct-converter v0.1.0 // indirect
+ github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect
+ github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+ github.com/apparentlymart/go-cidr v1.1.0 // indirect
+ github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
+ github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce // indirect
+ github.com/aquasecurity/go-npm-version v0.0.2 // indirect
+ github.com/aquasecurity/go-pep440-version v0.0.1 // indirect
+ github.com/aquasecurity/go-version v0.0.1 // indirect
+ github.com/aquasecurity/iamgo v0.0.10 // indirect
+ github.com/aquasecurity/jfather v0.0.8 // indirect
+ github.com/aquasecurity/table v1.11.0 // indirect
+ github.com/aquasecurity/tml v0.6.1 // indirect
+ github.com/aquasecurity/trivy-checks v1.12.2-0.20251219190323-79d27547baf5 // indirect
+ github.com/aquasecurity/trivy-java-db v0.0.0-20250912094916-94bd09842933 // indirect
+ github.com/aquasecurity/trivy-kubernetes v0.9.1 // indirect
+ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
+ github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
+ github.com/aws/aws-sdk-go-v2/config v1.32.12 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ebs v1.33.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ec2 v1.290.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ecr v1.55.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
+ github.com/bitnami/go-version v0.0.0-20250916072751-cb23e8405957 // 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/briandowns/spinner v1.23.2 // indirect
+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+ github.com/cenkalti/backoff/v5 v5.0.3 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/chai2010/gettext-go v1.0.3 // indirect
+ github.com/cheggaaa/pb/v3 v3.1.7 // indirect
+ github.com/clipperhouse/stringish v0.1.1 // indirect
+ github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
+ github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
+ github.com/containerd/cgroups/v3 v3.1.3 // indirect
+ github.com/containerd/containerd v1.7.30 // indirect
+ github.com/containerd/containerd/api v1.10.0 // indirect
+ github.com/containerd/containerd/v2 v2.2.2 // indirect
+ github.com/containerd/continuity v0.4.5 // 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.4 // indirect
+ github.com/containerd/plugin v1.0.0 // indirect
+ github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
+ github.com/containerd/ttrpc v1.2.8 // indirect
+ github.com/containerd/typeurl/v2 v2.2.3 // indirect
+ github.com/coreos/go-oidc/v3 v3.17.0 // indirect
+ github.com/coreos/go-semver v0.3.1 // indirect
+ github.com/coreos/go-systemd/v22 v22.7.0 // indirect
+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
+ github.com/cyphar/filepath-securejoin v0.6.1 // indirect
+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
+ github.com/digitorus/pkcs7 v0.0.0-20250730155240-ffadbf3f398c // indirect
+ github.com/digitorus/timestamp v0.0.0-20250524132541-c45532741eea // indirect
+ github.com/distribution/reference v0.6.0 // indirect
+ github.com/dlclark/regexp2 v1.11.5 // indirect
+ github.com/docker/docker-credential-helpers v0.9.5 // indirect
+ github.com/docker/go-connections v0.6.0 // indirect
+ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/ebitengine/purego v0.10.0 // indirect
+ github.com/emicklei/go-restful/v3 v3.13.0 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
+ github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
+ github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
+ github.com/evanphx/json-patch v5.9.11+incompatible // indirect
+ github.com/evanphx/json-patch/v5 v5.9.11 // indirect
+ github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
+ github.com/fatih/color v1.19.0 // indirect
+ github.com/felixge/httpsnoop v1.0.4 // indirect
+ github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/fvbommel/sortorder v1.1.0 // indirect
+ github.com/fxamacker/cbor/v2 v2.9.0 // indirect
+ github.com/go-chi/chi/v5 v5.2.5 // indirect
+ github.com/go-errors/errors v1.5.1 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.9.0 // indirect
+ github.com/go-git/go-git/v5 v5.19.0 // indirect
+ github.com/go-gorp/gorp/v3 v3.1.0 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.4 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-logr/zapr v1.3.0 // indirect
+ github.com/go-ole/go-ole v1.3.0 // indirect
+ github.com/go-openapi/analysis v0.24.3 // indirect
+ github.com/go-openapi/errors v0.22.7 // indirect
+ github.com/go-openapi/jsonpointer v0.22.5 // indirect
+ github.com/go-openapi/jsonreference v0.21.5 // indirect
+ github.com/go-openapi/loads v0.23.3 // indirect
+ github.com/go-openapi/runtime v0.29.3 // indirect
+ github.com/go-openapi/spec v0.22.4 // indirect
+ github.com/go-openapi/strfmt v0.26.1 // indirect
+ github.com/go-openapi/swag v0.25.5 // indirect
+ github.com/go-openapi/swag/cmdutils v0.25.5 // indirect
+ github.com/go-openapi/swag/conv v0.25.5 // indirect
+ github.com/go-openapi/swag/fileutils v0.25.5 // indirect
+ github.com/go-openapi/swag/jsonname v0.25.5 // indirect
+ github.com/go-openapi/swag/jsonutils v0.25.5 // indirect
+ github.com/go-openapi/swag/loading v0.25.5 // indirect
+ github.com/go-openapi/swag/mangling v0.25.5 // indirect
+ github.com/go-openapi/swag/netutils v0.25.5 // indirect
+ github.com/go-openapi/swag/stringutils v0.25.5 // indirect
+ github.com/go-openapi/swag/typeutils v0.25.5 // indirect
+ github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
+ github.com/go-openapi/validate v0.25.2 // indirect
+ github.com/go-redis/redis/v8 v8.11.5 // indirect
+ github.com/go-task/slim-sprig/v3 v3.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.5 // indirect
+ github.com/gocsaf/csaf/v3 v3.5.1 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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/certificate-transparency-go v1.3.2 // indirect
+ github.com/google/gnostic-models v0.7.0 // indirect
+ github.com/google/go-github/v62 v62.0.0 // indirect
+ github.com/google/go-querystring v1.1.0 // indirect
+ github.com/google/go-tpm v0.9.8 // indirect
+ github.com/google/jsonschema-go v0.4.3 // indirect
+ github.com/google/licenseclassifier/v2 v2.0.0 // indirect
+ github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
+ github.com/google/s2a-go v0.1.9 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
+ github.com/googleapis/gax-go/v2 v2.19.0 // indirect
+ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
+ github.com/gosuri/uitable v0.0.4 // 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.27.7 // indirect
+ github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.72 // 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.6 // indirect
+ github.com/hashicorp/go-multierror v1.1.1 // indirect
+ github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
+ github.com/hashicorp/go-uuid v1.0.3 // indirect
+ github.com/hashicorp/go-version v1.9.0 // indirect
+ github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
+ github.com/hashicorp/hcl/v2 v2.24.0 // indirect
+ github.com/huandu/xstrings v1.5.0 // indirect
+ github.com/in-toto/attestation v1.1.2 // indirect
+ github.com/in-toto/in-toto-golang v0.11.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 // indirect
+ github.com/jmoiron/sqlx v1.4.0 // indirect
+ github.com/josephburnett/jd/v2 v2.3.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/kevinburke/ssh_config v1.4.0 // indirect
+ github.com/klauspost/compress v1.18.5 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f // indirect
+ github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 // indirect
+ github.com/knqyf263/go-rpm-version v0.0.0-20240918084003-2afd7dc6a38f // indirect
+ github.com/knqyf263/go-rpmdb v0.1.1 // indirect
+ github.com/knqyf263/nested v0.0.1 // indirect
+ github.com/kylelemons/godebug v1.1.0 // indirect
+ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
+ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
+ github.com/lestrrat-go/blackmagic v1.0.4 // indirect
+ github.com/lestrrat-go/dsig v1.0.0 // 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.2 // indirect
+ github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect
+ github.com/lestrrat-go/option/v2 v2.0.0 // indirect
+ github.com/letsencrypt/boulder v0.20260223.0 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
+ github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect
+ github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543 // indirect
+ github.com/magiconair/properties v1.8.10 // indirect
+ github.com/masahiro331/go-disk v0.0.0-20260423015231-f7a470ebd472 // indirect
+ github.com/masahiro331/go-ebs-file v0.0.0-20260422020928-9d24e29aac27 // indirect
+ github.com/masahiro331/go-ext4-filesystem v0.0.0-20260423010602-fe51f5b5e52b // indirect
+ github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a // indirect
+ github.com/masahiro331/go-vmdk-parser v0.0.0-20260423020818-08305fa668d2 // indirect
+ github.com/masahiro331/go-xfs-filesystem v0.0.0-20260422061116-d21e5e4481bb // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-runewidth v0.0.19 // indirect
+ github.com/mattn/go-shellwords v1.0.12 // indirect
+ github.com/minio/highwayhash v1.0.4 // indirect
+ github.com/mitchellh/copystructure v1.2.0 // indirect
+ github.com/mitchellh/go-homedir v1.1.0 // indirect
+ github.com/mitchellh/go-wordwrap v1.0.1 // indirect
+ github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
+ github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect
+ github.com/mitchellh/reflectwalk v1.0.2 // indirect
+ github.com/moby/buildkit v0.29.0 // indirect
+ github.com/moby/docker-image-spec v1.3.1 // indirect
+ github.com/moby/go-archive v0.2.0 // indirect
+ github.com/moby/locker v1.0.1 // indirect
+ github.com/moby/moby/api v1.54.1 // indirect
+ github.com/moby/moby/client v0.4.0 // indirect
+ github.com/moby/patternmatcher v0.6.1 // indirect
+ github.com/moby/spdystream v0.5.1 // indirect
+ github.com/moby/sys/atomicwriter v0.1.0 // indirect
+ github.com/moby/sys/mountinfo v0.7.2 // indirect
+ github.com/moby/sys/sequential v0.6.0 // indirect
+ github.com/moby/sys/signal v0.7.1 // indirect
+ github.com/moby/sys/user v0.4.0 // indirect
+ github.com/moby/sys/userns v0.1.0 // 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.1.0 // indirect
+ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
+ github.com/nats-io/jwt/v2 v2.8.1 // indirect
+ github.com/nats-io/nkeys v0.4.15 // indirect
+ github.com/nats-io/nuid v1.0.1 // indirect
+ github.com/ncruces/go-strftime v1.0.0 // indirect
+ github.com/nikolalohinski/gonja/v2 v2.7.0 // indirect
+ github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
+ github.com/oklog/ulid/v2 v2.1.1 // indirect
+ github.com/open-policy-agent/opa v1.15.2 // 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/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553 // indirect
+ github.com/openvex/go-vex v0.2.7 // indirect
+ github.com/owenrumney/go-sarif/v2 v2.3.3 // indirect
+ github.com/owenrumney/squealer v1.2.12 // indirect
+ github.com/package-url/packageurl-go v0.1.5 // indirect
+ github.com/pandatix/go-cvss v0.6.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
+ github.com/pjbgf/sha1cd v0.6.0 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+ github.com/pkg/errors v0.9.1 // 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-20240221224432-82ca36839d55 // indirect
+ github.com/prometheus/client_golang v1.23.2 // 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/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
+ github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/rubenv/sql-migrate v1.8.1 // indirect
+ github.com/russross/blackfriday/v2 v2.1.0 // indirect
+ github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect
+ github.com/sagikazarmark/locafero v0.12.0 // indirect
+ github.com/samber/lo v1.53.0 // indirect
+ github.com/samber/oops v1.19.3 // indirect
+ github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
+ github.com/sassoftware/go-rpmutils v0.4.0 // indirect
+ github.com/sassoftware/relic v7.2.1+incompatible // indirect
+ github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
+ github.com/segmentio/asm v1.2.1 // indirect
+ github.com/segmentio/encoding v0.5.4 // indirect
+ github.com/sergi/go-diff v1.4.0 // indirect
+ github.com/shibumi/go-pathspec v1.3.0 // indirect
+ github.com/shirou/gopsutil/v4 v4.26.3 // indirect
+ github.com/shopspring/decimal v1.4.0 // indirect
+ github.com/sigstore/cosign/v2 v2.6.3 // indirect
+ github.com/sigstore/protobuf-specs v0.5.0 // indirect
+ github.com/sigstore/rekor v1.5.1 // indirect
+ github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect
+ github.com/sigstore/sigstore v1.10.5 // indirect
+ github.com/sigstore/sigstore-go v1.1.4 // indirect
+ github.com/sigstore/timestamp-authority/v2 v2.0.6 // indirect
+ github.com/sirupsen/logrus v1.9.4 // indirect
+ github.com/skeema/knownhosts v1.3.2 // 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/spf13/viper v1.21.0 // indirect
+ github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
+ github.com/stephenafamo/scan v0.7.0 // indirect
+ github.com/stretchr/objx v0.5.3 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
+ github.com/tchap/go-patricia/v2 v2.3.3 // indirect
+ github.com/tetratelabs/wazero v1.11.0 // indirect
+ github.com/theupdateframework/go-tuf v0.7.0 // indirect
+ github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect
+ github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
+ github.com/tklauser/go-sysconf v0.3.16 // indirect
+ github.com/tklauser/numcpus v0.11.0 // indirect
+ github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect
+ github.com/toqueteos/webbrowser v1.2.0 // indirect
+ github.com/transparency-dev/formats v0.0.0-20251103090025-99ec6f4410eb // indirect
+ github.com/transparency-dev/merkle v0.0.2 // indirect
+ github.com/twitchtv/twirp v8.1.3+incompatible // indirect
+ github.com/ulikunitz/xz v0.5.15 // indirect
+ github.com/valyala/fastjson v1.6.7 // indirect
+ github.com/vbatts/tar-split v0.12.2 // indirect
+ github.com/vektah/gqlparser/v2 v2.5.32 // indirect
+ github.com/vladimirvivien/gexe v0.5.0 // indirect
+ github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ github.com/x448/float16 v0.8.4 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
+ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
+ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
+ github.com/xlab/treeprint v1.2.0 // indirect
+ github.com/yashtewari/glob-intersection v0.2.0 // indirect
+ github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ github.com/zclconf/go-cty v1.18.0 // indirect
+ github.com/zclconf/go-cty-yaml v1.2.0 // indirect
+ go.etcd.io/bbolt v1.4.3 // indirect
+ go.etcd.io/etcd/api/v3 v3.6.5 // indirect
+ go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect
+ go.etcd.io/etcd/client/v3 v3.6.5 // indirect
+ go.opencensus.io v0.24.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+ go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
+ go.opentelemetry.io/otel v1.43.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
+ go.opentelemetry.io/otel/metric v1.43.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.43.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
+ go.opentelemetry.io/otel/trace v1.43.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.9.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.27.1 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
+ go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
+ golang.org/x/crypto v0.50.0 // indirect
+ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
+ golang.org/x/mod v0.35.0 // indirect
+ golang.org/x/net v0.53.0 // indirect
+ golang.org/x/oauth2 v0.36.0 // indirect
+ golang.org/x/sys v0.43.0 // indirect
+ golang.org/x/term v0.42.0 // indirect
+ golang.org/x/text v0.36.0 // indirect
+ golang.org/x/tools v0.44.0 // indirect
+ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
+ gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
+ google.golang.org/api v0.272.0 // indirect
+ google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect
+ google.golang.org/grpc v1.79.3 // indirect
+ google.golang.org/protobuf v1.36.11 // indirect
+ gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
+ gopkg.in/inf.v0 v0.9.1 // indirect
+ gopkg.in/ini.v1 v1.67.1 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ helm.sh/helm/v3 v3.20.2 // indirect
+ k8s.io/apiextensions-apiserver v0.35.1 // indirect
+ k8s.io/cli-runtime v0.35.1 // indirect
+ k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect
+ k8s.io/kms v0.35.4 // indirect
+ k8s.io/kubectl v0.35.1 // indirect
+ modernc.org/libc v1.72.3 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
+ mvdan.cc/sh/v3 v3.12.0 // indirect
+ oras.land/oras-go/v2 v2.6.0 // indirect
+ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.34.0 // indirect
+ sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+ sigs.k8s.io/kustomize/api v0.20.1 // indirect
+ sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect
+ sigs.k8s.io/randfill v1.0.0 // indirect
+ sigs.k8s.io/yaml v1.6.0 // indirect
+)
+
+// TODO: remove https://github.com/containerd/containerd/issues/12493 is resolved.
+replace github.com/opencontainers/runtime-spec => github.com/opencontainers/runtime-spec v1.3.0
+
+replace github.com/aquasecurity/trivy => github.com/alegrey91/trivy v0.0.0-20260506084153-920fad253709