Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package kubescape for openSUSE:Factory 
checked in at 2026-05-11 16:56:50
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/kubescape (Old)
 and      /work/SRC/openSUSE:Factory/.kubescape.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "kubescape"

Mon May 11 16:56:50 2026 rev:42 rq:1352296 version:4.0.8

Changes:
--------
--- /work/SRC/openSUSE:Factory/kubescape/kubescape.changes      2026-05-09 
12:59:35.004519906 +0200
+++ /work/SRC/openSUSE:Factory/.kubescape.new.1966/kubescape.changes    
2026-05-11 17:07:04.388813886 +0200
@@ -1,0 +2,25 @@
+Sun May 10 15:31:33 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 4.0.8:
+  * Fix: back-propagate connector URLs to configObj in
+    initializeCloudAPI
+  * Initial plan
+  * Coderabbit findings
+  * suppress spurious interrupt signal log on graceful exit
+  * docs: fix TOC nesting and heading capitalization
+  * docs: add PDF output format and fix heading inconsistencies in
+    getting-started.md
+  * fix(vap): create parent directories in writeOutput
+  * fix(vap): use K8s upstream validation helpers for names and
+    namespaces
+  * fix(vap): use DNS label validation for namespace names
+  * fix(vap): build MatchLabels from parsed requirements, not raw
+    split
+  * fix(vap): reject DoubleEquals, downstream split on = would
+    break
+  * fix(vap): restrict label validation to equality selectors only
+  * fix(vap): use k8s labels.Parse for label selector validation
+  * fix(vap): fix K8s name and label selector validation
+  * feat(vap): add --timeout flag to deploy-library command
+
+-------------------------------------------------------------------

Old:
----
  kubescape-4.0.7.obscpio

New:
----
  kubescape-4.0.8.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ kubescape.spec ++++++
--- /var/tmp/diff_new_pack.KCIZPc/_old  2026-05-11 17:07:13.309180989 +0200
+++ /var/tmp/diff_new_pack.KCIZPc/_new  2026-05-11 17:07:13.313181153 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           kubescape
-Version:        4.0.7
+Version:        4.0.8
 Release:        0
 Summary:        Tool providing a multi-cloud K8s single pane of glass
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.KCIZPc/_old  2026-05-11 17:07:13.393184446 +0200
+++ /var/tmp/diff_new_pack.KCIZPc/_new  2026-05-11 17:07:13.409185104 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/armosec/kubescape</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v4.0.7</param>
+    <param name="revision">v4.0.8</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.KCIZPc/_old  2026-05-11 17:07:13.453186914 +0200
+++ /var/tmp/diff_new_pack.KCIZPc/_new  2026-05-11 17:07:13.465187409 +0200
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param name="url">https://github.com/armosec/kubescape</param>
-              <param 
name="changesrevision">3b9a22164e5165768d472b1761b061b3aa77f787</param></service></servicedata>
+              <param 
name="changesrevision">d7539c2264560a8685f59e89a731d6de833258a6</param></service></servicedata>
 (No newline at EOF)
 

++++++ kubescape-4.0.7.obscpio -> kubescape-4.0.8.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kubescape-4.0.7/cmd/vap/vap.go 
new/kubescape-4.0.8/cmd/vap/vap.go
--- old/kubescape-4.0.7/cmd/vap/vap.go  2026-05-08 08:35:14.000000000 +0200
+++ new/kubescape-4.0.8/cmd/vap/vap.go  2026-05-08 18:47:00.000000000 +0200
@@ -1,18 +1,22 @@
 package vap
 
 import (
-       "errors"
        "fmt"
        "io"
        "net/http"
-       "regexp"
+       "os"
+       "path/filepath"
        "strings"
+       "time"
 
        "github.com/kubescape/go-logger"
        "github.com/kubescape/kubescape/v3/core/cautils"
        "github.com/spf13/cobra"
        admissionv1 "k8s.io/api/admissionregistration/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+       "k8s.io/apimachinery/pkg/labels"
+       "k8s.io/apimachinery/pkg/selection"
+       "k8s.io/apimachinery/pkg/util/validation"
        "sigs.k8s.io/yaml"
 )
 
@@ -45,19 +49,23 @@
 }
 
 func getDeployLibraryCmd() *cobra.Command {
+       var outputFile string
+       var timeout time.Duration
+
        cmd := &cobra.Command{
                Use:   "deploy-library",
                Short: "Install Kubescape CEL admission policy library",
                Long:  ``,
                RunE: func(cmd *cobra.Command, args []string) error {
-                       content, err := deployLibrary()
+                       content, err := deployLibrary(timeout)
                        if err != nil {
                                return err
                        }
-                       fmt.Print(content)
-                       return nil
+                       return writeOutput(content, outputFile)
                },
        }
+       cmd.Flags().StringVarP(&outputFile, "output", "o", "", "Write output to 
file instead of stdout")
+       cmd.Flags().DurationVar(&timeout, "timeout", 0, "HTTP request timeout 
per download (e.g. 30s, 1m)")
 
        return cmd
 }
@@ -69,6 +77,7 @@
        var labelArr []string
        var action string
        var parameterReference string
+       var outputFile string
 
        createPolicyBindingCmd := &cobra.Command{
                Use:   "create-policy-binding",
@@ -83,15 +92,21 @@
                                return fmt.Errorf("invalid policy name %s: %w", 
policyName, err)
                        }
                        for _, namespace := range namespaceArr {
-                               if err := isValidK8sObjectName(namespace); err 
!= nil {
+                               if err := isValidNamespace(namespace); err != 
nil {
                                        return fmt.Errorf("invalid namespace 
%s: %w", namespace, err)
                                }
                        }
                        for _, label := range labelArr {
-                               // Label selector must be in the format 
key=value
-                               if 
!regexp.MustCompile(`^[a-zA-Z0-9]+=[a-zA-Z0-9]+$`).MatchString(label) {
+                               parsed, err := labels.Parse(label)
+                               if err != nil {
                                        return fmt.Errorf("invalid label 
selector: %s", label)
                                }
+                               requirements, _ := parsed.Requirements()
+                               for _, r := range requirements {
+                                       if r.Operator() != selection.Equals {
+                                               return fmt.Errorf("only '=' 
equality label selectors are supported: %s", label)
+                                       }
+                               }
                        }
                        if action != "Deny" && action != "Audit" && action != 
"Warn" {
                                return fmt.Errorf("invalid action: %s", action)
@@ -106,8 +121,7 @@
                        if err != nil {
                                return err
                        }
-                       fmt.Print(content)
-                       return nil
+                       return writeOutput(content, outputFile)
                },
        }
        // Must specify the name of the policy binding
@@ -119,31 +133,32 @@
        createPolicyBindingCmd.Flags().StringSliceVar(&labelArr, "label", 
[]string{}, "Resource label selector")
        createPolicyBindingCmd.Flags().StringVarP(&action, "action", "a", 
"Deny", "Action to take when policy fails")
        createPolicyBindingCmd.Flags().StringVarP(&parameterReference, 
"parameter-reference", "r", "", "Parameter reference object name")
+       createPolicyBindingCmd.Flags().StringVarP(&outputFile, "output", "o", 
"", "Write output to file instead of stdout")
 
        return createPolicyBindingCmd
 }
 
 // Implementation of the VAP helper commands
 // deploy-library
-func deployLibrary() (string, error) {
+func deployLibrary(timeout time.Duration) (string, error) {
        logger.L().Info("Downloading the Kubescape CEL admission policy 
library")
        // Download the policy-configuration-definition.yaml from the latest 
release URL
        policyConfigurationDefinitionURL := 
"https://github.com/kubescape/cel-admission-library/releases/latest/download/policy-configuration-definition.yaml";
-       policyConfigurationDefinition, err := 
downloadFileToString(policyConfigurationDefinitionURL)
+       policyConfigurationDefinition, err := 
downloadFileToString(policyConfigurationDefinitionURL, timeout)
        if err != nil {
                return "", err
        }
 
        // Download the basic-control-configuration.yaml from the latest 
release URL
        basicControlConfigurationURL := 
"https://github.com/kubescape/cel-admission-library/releases/latest/download/basic-control-configuration.yaml";
-       basicControlConfiguration, err := 
downloadFileToString(basicControlConfigurationURL)
+       basicControlConfiguration, err := 
downloadFileToString(basicControlConfigurationURL, timeout)
        if err != nil {
                return "", err
        }
 
        // Download the kubescape-validating-admission-policies.yaml from the 
latest release URL
        kubescapeValidatingAdmissionPoliciesURL := 
"https://github.com/kubescape/cel-admission-library/releases/latest/download/kubescape-validating-admission-policies.yaml";
-       kubescapeValidatingAdmissionPolicies, err := 
downloadFileToString(kubescapeValidatingAdmissionPoliciesURL)
+       kubescapeValidatingAdmissionPolicies, err := 
downloadFileToString(kubescapeValidatingAdmissionPoliciesURL, timeout)
        if err != nil {
                return "", err
        }
@@ -162,9 +177,11 @@
        return result.String(), nil
 }
 
-func downloadFileToString(url string) (string, error) {
-       // Send an HTTP GET request to the URL
-       response, err := http.Get(url) //nolint:gosec
+func downloadFileToString(url string, timeout time.Duration) (string, error) {
+       client := &http.Client{
+               Timeout: timeout,
+       }
+       response, err := client.Get(url) //nolint:gosec
        if err != nil {
                return "", err // Return an empty string and the error if the 
request fails
        }
@@ -186,19 +203,28 @@
        return bodyString, nil
 }
 
-func isValidK8sObjectName(name string) error {
-       // Kubernetes object names must consist of lower case alphanumeric 
characters, '-' or '.',
-       // and must start and end with an alphanumeric character (e.g., 
'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')
-       // Max length of 63 characters.
-       if len(name) > 63 {
-               return errors.New("name should be less than 63 characters")
+func writeOutput(content string, outputFile string) error {
+       if outputFile != "" {
+               if err := os.MkdirAll(filepath.Dir(outputFile), 0755); err != 
nil {
+                       return err
+               }
+               return os.WriteFile(outputFile, []byte(content), 0644)
        }
+       fmt.Print(content)
+       return nil
+}
 
-       regex := regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
-       if !regex.MatchString(name) {
-               return errors.New("name should consist of lower case 
alphanumeric characters, '-' or '.', and must start and end with an 
alphanumeric character")
+func isValidK8sObjectName(name string) error {
+       if errs := validation.IsDNS1123Subdomain(name); len(errs) > 0 {
+               return fmt.Errorf("invalid name: %s", strings.Join(errs, "; "))
        }
+       return nil
+}
 
+func isValidNamespace(name string) error {
+       if errs := validation.IsDNS1123Label(name); len(errs) > 0 {
+               return fmt.Errorf("invalid namespace: %s", strings.Join(errs, 
"; "))
+       }
        return nil
 }
 
@@ -227,8 +253,16 @@
                policyBinding.Spec.MatchResources.ObjectSelector = 
&metav1.LabelSelector{}
                policyBinding.Spec.MatchResources.ObjectSelector.MatchLabels = 
make(map[string]string)
                for _, label := range labelMatch {
-                       labelParts := regexp.MustCompile(`=`).Split(label, 2)
-                       
policyBinding.Spec.MatchResources.ObjectSelector.MatchLabels[labelParts[0]] = 
labelParts[1]
+                       parsed, err := labels.Parse(label)
+                       if err != nil {
+                               continue
+                       }
+                       requirements, _ := parsed.Requirements()
+                       for _, r := range requirements {
+                               if len(r.Values().List()) > 0 {
+                                       
policyBinding.Spec.MatchResources.ObjectSelector.MatchLabels[r.Key()] = 
r.Values().List()[0]
+                               }
+                       }
                }
        }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kubescape-4.0.7/cmd/vap/vap_test.go 
new/kubescape-4.0.8/cmd/vap/vap_test.go
--- old/kubescape-4.0.7/cmd/vap/vap_test.go     2026-05-08 08:35:14.000000000 
+0200
+++ new/kubescape-4.0.8/cmd/vap/vap_test.go     2026-05-08 18:47:00.000000000 
+0200
@@ -2,10 +2,13 @@
 
 import (
        "fmt"
+       "io"
        "net/http"
        "net/http/httptest"
+       "os"
        "strings"
        "testing"
+       "time"
 
        "github.com/spf13/cobra"
        "github.com/stretchr/testify/assert"
@@ -28,31 +31,32 @@
                {name: "starts with digit", input: "123", wantErr: false},
                {name: "contains multiple hyphens", input: "abc-def-ghi", 
wantErr: false},
                {name: "hyphen in middle", input: "abc-def123", wantErr: false},
-               {name: "exactly 63 chars", input: strings.Repeat("a", 63), 
wantErr: false},
+               {name: "dots in middle", input: "abc.def", wantErr: false},
+               {name: "dots and hyphens mixed", input: "team.prod-v2", 
wantErr: false},
+               {name: "exactly 253 chars", input: strings.Repeat("a", 253), 
wantErr: false},
                {name: "1 char", input: "x", wantErr: false},
 
                // invalid - length
-               {name: "empty string", input: "", wantErr: true, errMsg: 
"should consist of lower case alphanumeric characters"},
-               {name: "exceeds 63 chars", input: strings.Repeat("a", 64), 
wantErr: true, errMsg: "less than 63 characters"},
+               {name: "empty string", input: "", wantErr: true, errMsg: "RFC 
1123 subdomain"},
+               {name: "exceeds 253 chars", input: strings.Repeat("a", 254), 
wantErr: true, errMsg: "no more than 253"},
 
-               // invalid - starts with hyphen
-               {name: "starts with hyphen", input: "-abc", wantErr: true, 
errMsg: "should consist of lower case alphanumeric characters"},
-
-               // invalid - ends with hyphen
-               {name: "ends with hyphen", input: "abc-", wantErr: true, 
errMsg: "should consist of lower case alphanumeric characters"},
+               // invalid - starts/ends with dot or hyphen
+               {name: "starts with hyphen", input: "-abc", wantErr: true, 
errMsg: "RFC 1123 subdomain"},
+               {name: "ends with hyphen", input: "abc-", wantErr: true, 
errMsg: "RFC 1123 subdomain"},
+               {name: "starts with dot", input: ".abc", wantErr: true, errMsg: 
"RFC 1123 subdomain"},
+               {name: "ends with dot", input: "abc.", wantErr: true, errMsg: 
"RFC 1123 subdomain"},
 
                // invalid - uppercase
-               {name: "contains uppercase", input: "Abc", wantErr: true, 
errMsg: "should consist of lower case alphanumeric characters"},
-               {name: "all uppercase", input: "ABC", wantErr: true, errMsg: 
"should consist of lower case alphanumeric characters"},
+               {name: "contains uppercase", input: "Abc", wantErr: true, 
errMsg: "RFC 1123 subdomain"},
+               {name: "all uppercase", input: "ABC", wantErr: true, errMsg: 
"RFC 1123 subdomain"},
 
                // invalid - special characters
-               {name: "contains underscore", input: "abc_def", wantErr: true, 
errMsg: "should consist of lower case alphanumeric characters"},
-               {name: "contains space", input: "abc def", wantErr: true, 
errMsg: "should consist of lower case alphanumeric characters"},
-               {name: "contains dot in middle (not allowed by regex)", input: 
"abc.def", wantErr: true, errMsg: "should consist of lower case alphanumeric 
characters"},
-               {name: "contains at sign", input: "a@b", wantErr: true, errMsg: 
"should consist of lower case alphanumeric characters"},
+               {name: "contains underscore", input: "abc_def", wantErr: true, 
errMsg: "RFC 1123 subdomain"},
+               {name: "contains space", input: "abc def", wantErr: true, 
errMsg: "RFC 1123 subdomain"},
+               {name: "contains at sign", input: "a@b", wantErr: true, errMsg: 
"RFC 1123 subdomain"},
 
                // invalid - starts/ends with digit
-               {name: "starts with hyphen and digit", input: "-123abc", 
wantErr: true, errMsg: "should consist of lower case alphanumeric characters"},
+               {name: "starts with hyphen and digit", input: "-123abc", 
wantErr: true, errMsg: "RFC 1123 subdomain"},
        }
 
        for _, tt := range tests {
@@ -68,6 +72,37 @@
        }
 }
 
+func TestIsValidNamespace(t *testing.T) {
+       tests := []struct {
+               name    string
+               input   string
+               wantErr bool
+               errMsg  string
+       }{
+               {name: "valid simple", input: "default", wantErr: false},
+               {name: "valid with hyphen", input: "kube-system", wantErr: 
false},
+               {name: "valid starts with digit", input: "0default", wantErr: 
false},
+               {name: "empty", input: "", wantErr: true, errMsg: "RFC 1123 
label"},
+               {name: "exceeds 63 chars", input: strings.Repeat("a", 64), 
wantErr: true, errMsg: "no more than 63"},
+               {name: "contains dot", input: "team.prod", wantErr: true, 
errMsg: "must not contain dots"},
+               {name: "contains uppercase", input: "Default", wantErr: true, 
errMsg: "RFC 1123 label"},
+               {name: "starts with hyphen", input: "-default", wantErr: true, 
errMsg: "RFC 1123 label"},
+               {name: "ends with hyphen", input: "default-", wantErr: true, 
errMsg: "RFC 1123 label"},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       err := isValidNamespace(tt.input)
+                       if tt.wantErr {
+                               require.Error(t, err)
+                               assert.Contains(t, err.Error(), tt.errMsg)
+                       } else {
+                               assert.NoError(t, err)
+                       }
+               })
+       }
+}
+
 func TestDownloadFileToString(t *testing.T) {
        t.Run("successful download", func(t *testing.T) {
                server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
@@ -76,7 +111,7 @@
                }))
                defer server.Close()
 
-               result, err := downloadFileToString(server.URL)
+               result, err := downloadFileToString(server.URL, 0)
                require.NoError(t, err)
                assert.Equal(t, "hello world", result)
        })
@@ -87,7 +122,7 @@
                }))
                defer server.Close()
 
-               _, err := downloadFileToString(server.URL)
+               _, err := downloadFileToString(server.URL, 0)
                require.Error(t, err)
                assert.Contains(t, err.Error(), "failed to download file")
        })
@@ -98,15 +133,14 @@
                }))
                defer server.Close()
 
-               _, err := downloadFileToString(server.URL)
+               _, err := downloadFileToString(server.URL, 0)
                require.Error(t, err)
                assert.Contains(t, err.Error(), "failed to download file")
                assert.Contains(t, err.Error(), "500")
        })
 
        t.Run("connection refused", func(t *testing.T) {
-               // Use an invalid URL to simulate connection refused
-               _, err := downloadFileToString("http://127.0.0.1:1/nonexistent";)
+               _, err := 
downloadFileToString("http://127.0.0.1:1/nonexistent";, 0)
                require.Error(t, err)
        })
 
@@ -116,7 +150,7 @@
                }))
                defer server.Close()
 
-               result, err := downloadFileToString(server.URL)
+               result, err := downloadFileToString(server.URL, 0)
                require.NoError(t, err)
                assert.Empty(t, result)
        })
@@ -166,13 +200,14 @@
                }
                defer func() { http.DefaultTransport = origTransport }()
 
-               content, err := deployLibrary()
+               // Capture stdout
+               content, err := deployLibrary(0)
                require.NoError(t, err)
 
                parts := strings.Split(content, "\n---\n")
                require.Len(t, parts, 3)
-               assert.Contains(t, parts[0], "policy-config-content")
-               assert.Contains(t, parts[1], "basic-control-content")
+               assert.Equal(t, "policy-config-content", 
strings.TrimSpace(parts[0]))
+               assert.Equal(t, "basic-control-content", 
strings.TrimSpace(parts[1]))
                assert.Contains(t, parts[2], "kubescape-policies-content")
        })
 
@@ -193,7 +228,7 @@
                }
                defer func() { http.DefaultTransport = origTransport }()
 
-               _, err := deployLibrary()
+               _, err := deployLibrary(0)
                require.Error(t, err)
                assert.Contains(t, err.Error(), "failed to download file")
        })
@@ -215,7 +250,7 @@
                }
                defer func() { http.DefaultTransport = origTransport }()
 
-               _, err := deployLibrary()
+               _, err := deployLibrary(0)
                require.Error(t, err)
                assert.Contains(t, err.Error(), "failed to download file")
        })
@@ -237,7 +272,7 @@
                }
                defer func() { http.DefaultTransport = origTransport }()
 
-               _, err := deployLibrary()
+               _, err := deployLibrary(0)
                require.Error(t, err)
                assert.Contains(t, err.Error(), "failed to download file")
        })
@@ -287,6 +322,17 @@
                assert.Equal(t, "Warn", 
string(binding.Spec.ValidationActions[0]))
        })
 
+       t.Run("labels with whitespace are trimmed", func(t *testing.T) {
+               out, err := createPolicyBinding("my-binding", "c-0016", "Deny", 
"", nil, []string{"app = nginx"})
+               require.NoError(t, err)
+
+               var binding admissionv1.ValidatingAdmissionPolicyBinding
+               err = yaml.Unmarshal([]byte(out), &binding)
+               require.NoError(t, err)
+               require.NotNil(t, binding.Spec.MatchResources.ObjectSelector)
+               assert.Equal(t, map[string]string{"app": "nginx"}, 
binding.Spec.MatchResources.ObjectSelector.MatchLabels)
+       })
+
        t.Run("with parameter reference", func(t *testing.T) {
                out, err := createPolicyBinding("my-binding", "c-0016", "Deny", 
"my-params", nil, nil)
                require.NoError(t, err)
@@ -397,6 +443,15 @@
        assert.Equal(t, "deploy-library", cmd.Use)
        assert.Equal(t, "Install Kubescape CEL admission policy library", 
cmd.Short)
        assert.NotNil(t, cmd.RunE)
+
+       // Check flags
+       outputFlag := cmd.Flags().Lookup("output")
+       require.NotNil(t, outputFlag)
+       assert.Equal(t, "o", outputFlag.Shorthand)
+
+       timeoutFlag := cmd.Flags().Lookup("timeout")
+       require.NotNil(t, timeoutFlag)
+       assert.Equal(t, "0s", timeoutFlag.DefValue)
 }
 
 func TestGetCreatePolicyBindingCmd(t *testing.T) {
@@ -442,8 +497,8 @@
 }
 
 func TestLabelSelectorRegexEdgeCases(t *testing.T) {
-       validLabels := []string{"app=nginx", "env1=prod2", "App=Value", 
"appName=NginxValue"}
-       invalidLabels := []string{"key value", "key=", "=value", 
"key=val=extra", "app-name=nginx", "app.name=nginx", "app_name=nginx", 
"app@=nginx", "app=nginx@"}
+       validLabels := []string{"app=nginx", "env1=prod2", "App=Value", 
"appName=NginxValue", "app-name=nginx", "app.name=nginx", "app_name=nginx", 
"app.kubernetes.io/name=nginx", "key=", "app = nginx"}
+       invalidLabels := []string{"key value", "=value", "key=val=extra", 
"app@=nginx", "app=nginx@", "app!=nginx", "app", "app==nginx"}
 
        for _, label := range validLabels {
                t.Run("valid label "+label, func(t *testing.T) {
@@ -460,7 +515,7 @@
                        cmd.SetArgs([]string{"--name", "my-binding", 
"--policy", "c-0016", "--label", label})
                        err := cmd.Execute()
                        require.Error(t, err)
-                       assert.Contains(t, err.Error(), "invalid label 
selector")
+                       assert.True(t, strings.Contains(err.Error(), "invalid 
label selector") || strings.Contains(err.Error(), "only '=' equality"), 
"unexpected error: %v", err)
                })
        }
 }
@@ -506,3 +561,96 @@
        _, isRequired = annotations[cobra.BashCompOneRequiredFlag]
        assert.True(t, isRequired, "policy flag should be marked as required")
 }
+
+func TestDeployLibraryCmdTimeoutFlag(t *testing.T) {
+       cmd := getDeployLibraryCmd()
+
+       t.Run("timeout flag is registered with default 0s", func(t *testing.T) {
+               timeoutFlag := cmd.Flags().Lookup("timeout")
+               require.NotNil(t, timeoutFlag)
+               assert.Equal(t, "0s", timeoutFlag.DefValue)
+       })
+
+       t.Run("timeout flag can be set via args", func(t *testing.T) {
+               cmd := getDeployLibraryCmd()
+               err := cmd.ParseFlags([]string{"--timeout", "30s"})
+               require.NoError(t, err)
+               got, err := cmd.Flags().GetDuration("timeout")
+               require.NoError(t, err)
+               assert.Equal(t, 30*time.Second, got)
+       })
+
+       t.Run("timeout flag accepts 0s shorthand", func(t *testing.T) {
+               cmd := getDeployLibraryCmd()
+               err := cmd.ParseFlags([]string{"--timeout", "0s"})
+               require.NoError(t, err)
+               got, err := cmd.Flags().GetDuration("timeout")
+               require.NoError(t, err)
+               assert.Equal(t, time.Duration(0), got)
+       })
+}
+
+func TestDownloadFileToStringTimeout(t *testing.T) {
+       t.Run("timeout 0 means no timeout", func(t *testing.T) {
+               server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+                       w.WriteHeader(http.StatusOK)
+                       fmt.Fprint(w, "ok")
+               }))
+               defer server.Close()
+
+               result, err := downloadFileToString(server.URL, 0)
+               require.NoError(t, err)
+               assert.Equal(t, "ok", result)
+       })
+
+       t.Run("short timeout triggers on slow server", func(t *testing.T) {
+               server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+                       select {
+                       case <-time.After(2 * time.Second):
+                       case <-r.Context().Done():
+                               return
+                       }
+                       fmt.Fprint(w, "too late")
+               }))
+               defer server.Close()
+
+               _, err := downloadFileToString(server.URL, 10*time.Millisecond)
+               require.Error(t, err)
+       })
+
+       t.Run("non-zero timeout works for fast server", func(t *testing.T) {
+               server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+                       w.WriteHeader(http.StatusOK)
+                       fmt.Fprint(w, "fast")
+               }))
+               defer server.Close()
+
+               result, err := downloadFileToString(server.URL, 5*time.Second)
+               require.NoError(t, err)
+               assert.Equal(t, "fast", result)
+       })
+}
+
+// captureStdout captures stdout output from a function and returns it as a 
string.
+func captureStdout(t *testing.T, fn func()) string {
+       t.Helper()
+
+       oldStdout := os.Stdout
+       r, w, err := os.Pipe()
+       require.NoError(t, err)
+       os.Stdout = w
+
+       outC := make(chan string)
+       go func() {
+               var buf strings.Builder
+               _, _ = io.Copy(&buf, r)
+               outC <- buf.String()
+       }()
+
+       fn()
+
+       w.Close()
+       os.Stdout = oldStdout
+
+       return <-outC
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kubescape-4.0.7/core/cautils/customerloader.go 
new/kubescape-4.0.8/core/cautils/customerloader.go
--- old/kubescape-4.0.7/core/cautils/customerloader.go  2026-05-08 
08:35:14.000000000 +0200
+++ new/kubescape-4.0.8/core/cautils/customerloader.go  2026-05-08 
18:47:00.000000000 +0200
@@ -498,6 +498,22 @@
                        logger.L().Debug("updating Access Key from config", 
helpers.Int("old (len)", len(ksCloud.GetAccessKey())), helpers.Int("new (len)", 
len(val)))
                        ksCloud.SetAccessKey(val)
                }
+
+               // Back-propagate URLs from connector to configObj when 
configObj has no URL but connector does.
+               // This handles the API_URL-based live service discovery path 
where initializeSaaSEnv sets URLs
+               // on the global connector before any scan runs, but 
configObj.CloudReportURL is never populated
+               // (e.g. when running without services.json and without 
KS_CLOUD_REPORT_URL env var).
+               if co := c.GetConfigObj(); co != nil {
+                       if co.CloudReportURL == "" && 
ksCloud.GetCloudReportURL() != "" {
+                               logger.L().Debug("updating Cloud Report URL in 
config from connector", helpers.String("cloudReportURL", 
ksCloud.GetCloudReportURL()))
+                               co.CloudReportURL = ksCloud.GetCloudReportURL()
+                       }
+                       if co.CloudAPIURL == "" && ksCloud.GetCloudAPIURL() != 
"" {
+                               logger.L().Debug("updating Cloud API URL in 
config from connector", helpers.String("cloudAPIURL", ksCloud.GetCloudAPIURL()))
+                               co.CloudAPIURL = ksCloud.GetCloudAPIURL()
+                       }
+               }
+
                getter.SetKSCloudAPIConnector(ksCloud)
        } else {
                logger.L().Debug("initializing KS Cloud API from config", 
helpers.String("accountID", c.GetAccountID()), helpers.String("cloudAPIURL", 
c.GetCloudAPIURL()), helpers.String("cloudReportURL", c.GetCloudReportURL()))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kubescape-4.0.7/core/cautils/customerloader_test.go 
new/kubescape-4.0.8/core/cautils/customerloader_test.go
--- old/kubescape-4.0.7/core/cautils/customerloader_test.go     2026-05-08 
08:35:14.000000000 +0200
+++ new/kubescape-4.0.8/core/cautils/customerloader_test.go     2026-05-08 
18:47:00.000000000 +0200
@@ -5,8 +5,10 @@
        "os"
        "testing"
 
+       v1 "github.com/kubescape/backend/pkg/client/v1"
        "github.com/kubescape/kubescape/v3/core/cautils/getter"
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
        corev1 "k8s.io/api/core/v1"
 )
 
@@ -236,6 +238,33 @@
        }
 }
 
+// Test_initializeCloudAPI_backPropagatesURLsFromConnector verifies that when 
the global
+// connector already has URLs (e.g. set by initializeSaaSEnv via live service 
discovery)
+// but configObj has no URLs (e.g. no services.json and no KS_CLOUD_REPORT_URL 
env var),
+// initializeCloudAPI back-propagates the connector URLs into configObj so that
+// setSubmitBehavior can see a non-empty CloudReportURL and allow result 
submission.
+func Test_initializeCloudAPI_backPropagatesURLsFromConnector(t *testing.T) {
+       // Pre-populate the global connector with URLs (simulating 
initializeSaaSEnv via API_URL discovery)
+       presetCloud, err := v1.NewKSCloudAPI("https://api.example.com";, 
"https://report.example.com";, "test-account", "test-key")
+       require.NoError(t, err)
+       getter.SetKSCloudAPIConnector(presetCloud)
+
+       // Config with empty URLs (simulating no services.json and no 
KS_CLOUD_REPORT_URL env var)
+       cfg := &ClusterConfig{
+               configObj: &ConfigObj{
+                       AccountID:      "test-account",
+                       CloudReportURL: "",
+                       CloudAPIURL:    "",
+               },
+       }
+
+       initializeCloudAPI(cfg)
+
+       // configObj should now have URLs from the connector
+       assert.Equal(t, "https://report.example.com";, 
cfg.configObj.CloudReportURL)
+       assert.Equal(t, "https://api.example.com";, cfg.configObj.CloudAPIURL)
+}
+
 func TestGetConfigMapNamespace(t *testing.T) {
        tests := []struct {
                name string
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kubescape-4.0.7/docs/getting-started.md 
new/kubescape-4.0.8/docs/getting-started.md
--- old/kubescape-4.0.7/docs/getting-started.md 2026-05-08 08:35:14.000000000 
+0200
+++ new/kubescape-4.0.8/docs/getting-started.md 2026-05-08 18:47:00.000000000 
+0200
@@ -10,6 +10,8 @@
 - [Run your first scan](#run-your-first-scan)
 - [Usage](#usage)
   - [Misconfigurations Scanning](#misconfigurations-scanning)
+    - [Output Formats](#output-formats)
+    - [Compliance Score](#compliance-score)
   - [Image Scanning](#image-scanning)
   - [Auto-Fix Misconfigurations](#auto-fix-misconfigurations)
   - [Image Patching](#image-patching)
@@ -259,20 +261,20 @@
     kubescape scan framework <FRAMEWORK_NAME> --compliance-threshold 
<SCORE_VALUE[float32]>
     ```
 
-### Output formats
+### Output Formats
 
-#### JSON:
+#### JSON
 
 ```bash
 kubescape scan --format json --output results.json
 ```
 
-#### junit XML: 
+#### JUnit XML
 
 ```bash
 kubescape scan --format junit --output results.xml
 ```
-#### SARIF: 
+#### SARIF
 
 SARIF is a standard format for the output of static analysis tools. It is 
supported by many tools, including GitHub Code Scanning and Azure DevOps. [Read 
more about 
SARIF](https://docs.github.com/en/code-security/secure-coding/sarif-support-for-code-scanning/about-sarif-support-for-code-scanning).
 
@@ -288,6 +290,12 @@
 kubescape scan --format html --output results.html
 ```
 
+#### PDF
+
+```bash
+kubescape scan --format pdf --output report.pdf
+```
+
 ## Offline/air-gapped environment support
 
 It is possible to run Kubescape offline!  Check out our [video 
tutorial](https://youtu.be/IGXL9s37smM).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/kubescape-4.0.7/main.go new/kubescape-4.0.8/main.go
--- old/kubescape-4.0.7/main.go 2026-05-08 08:35:14.000000000 +0200
+++ new/kubescape-4.0.8/main.go 2026-05-08 18:47:00.000000000 +0200
@@ -22,21 +22,28 @@
        // Set the global build number for version checking
        versioncheck.BuildNumber = version
 
-       // Capture interrupt signal
-       ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, 
syscall.SIGTERM)
-       defer stop()
+       // Capture interrupt signal on a dedicated channel so the watcher can
+       // distinguish a real signal from a normal cancel() on graceful exit.
+       sigCh := make(chan os.Signal, 1)
+       signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
+       defer signal.Stop(sigCh)
+       ctx, cancel := context.WithCancel(context.Background())
+       defer cancel()
 
-       // Handle interrupt signal
        go func() {
-               <-ctx.Done()
-               // Perform cleanup or graceful shutdown here
-               logger.L().StopError("Received interrupt signal, exiting...")
-               // Clear the signal handler so that a second interrupt signal 
shuts down immediately
-               stop()
+               select {
+               case <-sigCh:
+                       logger.L().StopError("Received interrupt signal, 
exiting...")
+                       // Clear the signal handler so a second signal 
terminates immediately.
+                       signal.Stop(sigCh)
+                       cancel()
+               case <-ctx.Done():
+                       // Normal shutdown — no log line.
+               }
        }()
 
        if err := cmd.Execute(ctx, version, commit, date); err != nil {
-               stop()
+               cancel()
                logger.L().Fatal(err.Error())
        }
 }
\ No newline at end of file

++++++ kubescape.obsinfo ++++++
--- /var/tmp/diff_new_pack.KCIZPc/_old  2026-05-11 17:07:15.205259018 +0200
+++ /var/tmp/diff_new_pack.KCIZPc/_new  2026-05-11 17:07:15.213259347 +0200
@@ -1,5 +1,5 @@
 name: kubescape
-version: 4.0.7
-mtime: 1778222114
-commit: 3b9a22164e5165768d472b1761b061b3aa77f787
+version: 4.0.8
+mtime: 1778258820
+commit: d7539c2264560a8685f59e89a731d6de833258a6
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/kubescape/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.kubescape.new.1966/vendor.tar.gz differ: char 112, 
line 1

Reply via email to