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(¶meterReference, "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
