From ff13d40273c0b934b7c17f8f2b86e2a2e7c36519 Mon Sep 17 00:00:00 2001 From: freedisch Date: Fri, 26 Jul 2024 15:14:55 +0200 Subject: [PATCH 01/14] feat: setup cascading command Signed-off-by: freedisch --- scbctl/cmd/cascading.go | 124 ++++++++++++++++++++++++++++++++++++++++ scbctl/go.mod | 9 +-- scbctl/go.sum | 10 ++++ 3 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 scbctl/cmd/cascading.go diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go new file mode 100644 index 0000000000..3bdb3c4f4c --- /dev/null +++ b/scbctl/cmd/cascading.go @@ -0,0 +1,124 @@ +package cmd + +import ( + "context" + "fmt" + "io" + + v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type cascadeOptions struct { + configFlags *genericclioptions.ConfigFlags + genericclioptions.IOStreams + + namespace string +} + +func NewCascadeCommand(streams genericclioptions.IOStreams) *cobra.Command { + o := &cascadeOptions{ + configFlags: genericclioptions.NewConfigFlags(true), + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "cascade", + Short: "Visualize cascading rules and scan flow", + Long: `Display a tree-like structure of scans and their cascading rules in a specified namespace`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := o.Complete(cmd); err != nil { + return err + } + if err := o.Validate(); err != nil { + return err + } + if err := o.Run(); err != nil { + return err + } + return nil + }, + } + + cmd.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace to visualize cascading rules for") + o.configFlags.AddFlags(cmd.Flags()) + + return cmd +} + +func (o *cascadeOptions) fetchScansAndRules(c client.Client) ([]v1.Scan, []v1.CascadeSpec, error) { + var scanList v1.ScanList + var ruleList []v1.CascadeSpec + + err := c.List(context.Background(), &scanList, client.InNamespace(o.namespace)) + if err != nil { + return nil, nil, err + } + + err = c.List(context.Background(), &ruleList, client.InNamespace(o.namespace)) + if err != nil { + return nil, nil, err + } + + return scanList.Items, ruleList.Items, nil +} + +type treeNode struct { + scan *v1.Scan + children []*treeNode +} + +func (o *cascadeOptions) buildCascadeTree(scans []v1.Scan, rules []v1.CascadeSpec) *treeNode { + scanMap := make(map[string]*treeNode) + for i := range scans { + scanMap[scans[i].Name] = &treeNode{scan: &scans[i]} + } + + root := &treeNode{} + + for _, rule := range rules { + for _, scan := range scans { + if o.scanMatchesRule(&scan, &rule) { + childScan := scanMap[rule.] + if childScan != nil { + scanMap[scan.Name].children = append(scanMap[scan.Name].children, childScan) + } + } + } + } + + for _, node := range scanMap { + if node.scan.Spec.InitialScan { + root.children = append(root.children, node) + } + } + + return root +} + +func (o *cascadeOptions) renderTree(root *treeNode, out io.Writer) { + w := printers.GetNewTabWriter(out) + defer w.Flush() + + fmt.Fprintln(w, "SCAN TYPE\tNAME\tSTATUS") + o.renderNode(root, "", w) +} + +func (o *cascadeOptions) renderNode(node *treeNode, prefix string, w io.Writer) { + if node.scan != nil { + fmt.Fprintf(w, "%s%s\t%s\t%s\n", prefix, node.scan.Spec.ScanType, node.scan.Name, string(node.scan.Status.State)) + } + + childPrefix := prefix + "└── " + for i, child := range node.children { + if i == len(node.children)-1 { + childPrefix = prefix + "└── " + } else { + childPrefix = prefix + "├── " + } + o.renderNode(child, childPrefix, w) + } +} diff --git a/scbctl/go.mod b/scbctl/go.mod index c77109b85c..63548b9287 100644 --- a/scbctl/go.mod +++ b/scbctl/go.mod @@ -10,9 +10,9 @@ toolchain go1.22.3 require ( github.com/secureCodeBox/secureCodeBox/operator v0.0.0-20240709091631-4e5c4b973bf2 github.com/spf13/cobra v1.7.0 - k8s.io/api v0.30.2 - k8s.io/apimachinery v0.30.2 - k8s.io/cli-runtime v0.30.1 + k8s.io/api v0.30.3 + k8s.io/apimachinery v0.30.3 + k8s.io/cli-runtime v0.30.3 sigs.k8s.io/controller-runtime v0.18.4 ) @@ -67,9 +67,10 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.30.2 // indirect + k8s.io/client-go v0.30.3 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/kubectl v0.30.3 k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect diff --git a/scbctl/go.sum b/scbctl/go.sum index 8174680682..96536ccb10 100644 --- a/scbctl/go.sum +++ b/scbctl/go.sum @@ -258,22 +258,32 @@ k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= +k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= +k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/cli-runtime v0.30.1 h1:kSBBpfrJGS6lllc24KeniI9JN7ckOOJKnmFYH1RpTOw= k8s.io/cli-runtime v0.30.1/go.mod h1:zhHgbqI4J00pxb6gM3gJPVf2ysDjhQmQtnTxnMScab8= +k8s.io/cli-runtime v0.30.3 h1:aG69oRzJuP2Q4o8dm+f5WJIX4ZBEwrvdID0+MXyUY6k= +k8s.io/cli-runtime v0.30.3/go.mod h1:hwrrRdd9P84CXSKzhHxrOivAR9BRnkMt0OeP5mj7X30= k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= +k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kubectl v0.30.3 h1:YIBBvMdTW0xcDpmrOBzcpUVsn+zOgjMYIu7kAq+yqiI= +k8s.io/kubectl v0.30.3/go.mod h1:IcR0I9RN2+zzTRUa1BzZCm4oM0NLOawE6RzlDvd1Fpo= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= From 54592c8bfc9834dfd7935fd59c3ffd3fcf8c0242 Mon Sep 17 00:00:00 2001 From: freedisch Date: Fri, 2 Aug 2024 01:18:03 +0200 Subject: [PATCH 02/14] feat: add `cascade` command to display scan tree Signed-off-by: freedisch --- scbctl/cmd/cascading.go | 298 ++++++++++++++++++++++++++++++---------- scbctl/cmd/root.go | 1 + scbctl/go.mod | 10 +- scbctl/go.sum | 16 +++ 4 files changed, 254 insertions(+), 71 deletions(-) diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go index 3bdb3c4f4c..851662c8ee 100644 --- a/scbctl/cmd/cascading.go +++ b/scbctl/cmd/cascading.go @@ -1,14 +1,20 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 package cmd import ( "context" "fmt" - "io" + "log" + "os" + "github.com/ddddddO/gtree" + cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/printers" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -19,106 +25,258 @@ type cascadeOptions struct { namespace string } -func NewCascadeCommand(streams genericclioptions.IOStreams) *cobra.Command { - o := &cascadeOptions{ - configFlags: genericclioptions.NewConfigFlags(true), - IOStreams: streams, - } - - cmd := &cobra.Command{ - Use: "cascade", - Short: "Visualize cascading rules and scan flow", - Long: `Display a tree-like structure of scans and their cascading rules in a specified namespace`, +func NewCascadeCommand() *cobra.Command { + cascadeCmd := &cobra.Command{ + Use: "cascade", + Short: "Display cascade of scans", + Long: `Display a tree-like structure showing the flow from initial scans to subsquent scans within a specified kubernetes namespace`, RunE: func(cmd *cobra.Command, args []string) error { - if err := o.Complete(cmd); err != nil { - return err - } - if err := o.Validate(); err != nil { - return err - } - if err := o.Run(); err != nil { - return err - } - return nil + return runCascade(cmd) }, } - cmd.Flags().StringVarP(&o.namespace, "namespace", "n", "", "Namespace to visualize cascading rules for") - o.configFlags.AddFlags(cmd.Flags()) - - return cmd + cascadeCmd.Flags().String("namespace", "", "Namespace to scans from") + //cascadeCmd.MarkFlagRequired("namespace") + return cascadeCmd } -func (o *cascadeOptions) fetchScansAndRules(c client.Client) ([]v1.Scan, []v1.CascadeSpec, error) { - var scanList v1.ScanList - var ruleList []v1.CascadeSpec - err := c.List(context.Background(), &scanList, client.InNamespace(o.namespace)) +func runCascade(cmd *cobra.Command) error { + namespace, _ := cmd.Flags().GetString("namespace") + + kubeclient, _, err := clientProvider.GetClient(kubeconfigArgs) + if err != nil { + return fmt.Errorf("error initializing kubernetes client: %w", err) + } + + scans, err := fetchScans(cmd.Context(), kubeclient, namespace) if err != nil { - return nil, nil, err + return fmt.Errorf("error fetching scans: %w", err) } - err = c.List(context.Background(), &ruleList, client.InNamespace(o.namespace)) + cascadingRules, err := fetchCascadingRules(cmd.Context(), kubeclient, namespace) if err != nil { - return nil, nil, err + return fmt.Errorf("error fetching cascading rules: %w", err) + } + + tree := buildTree(scans, cascadingRules) + + if err := gtree.OutputProgrammably(os.Stdout, tree); err != nil { + return fmt.Errorf("error outputting tree: %w", err) } - return scanList.Items, ruleList.Items, nil + return nil } -type treeNode struct { - scan *v1.Scan - children []*treeNode +func fetchScans(ctx context.Context, client client.Client, namespace string) ([]v1.Scan, error) { + var scanList v1.ScanList + if err := client.List(ctx, &scanList); err != nil { + return nil, fmt.Errorf("error listing scans: %w", err) + } + return scanList.Items, nil } -func (o *cascadeOptions) buildCascadeTree(scans []v1.Scan, rules []v1.CascadeSpec) *treeNode { - scanMap := make(map[string]*treeNode) +func fetchCascadingRules(ctx context.Context, client client.Client, namespace string) ([]cascadingv1.CascadingRule, error) { + var ruleList cascadingv1.CascadingRuleList + if err := client.List(ctx, &ruleList); err != nil { + return nil, fmt.Errorf("error listing cascading rules: %w", err) + } + return ruleList.Items, nil +} + + + +func buildTree(scans []v1.Scan, rules []cascadingv1.CascadingRule) *gtree.Node { + root := gtree.NewRoot("Scans") + + scanMap := make(map[string]*v1.Scan) for i := range scans { - scanMap[scans[i].Name] = &treeNode{scan: &scans[i]} + scanMap[scans[i].Name] = &scans[i] } - root := &treeNode{} + for _, scan := range scans { + if isInitialScan(&scan) { + scanNode := root.Add(scan.Name) + buildScanSubtree(scanNode, &scan, scanMap, rules) + } + } - for _, rule := range rules { - for _, scan := range scans { - if o.scanMatchesRule(&scan, &rule) { - childScan := scanMap[rule.] - if childScan != nil { - scanMap[scan.Name].children = append(scanMap[scan.Name].children, childScan) - } + return root +} + +func buildScanSubtree(node *gtree.Node, scan *v1.Scan, scanMap map[string]*v1.Scan, rules []cascadingv1.CascadingRule) { + for _, childScan := range getChildScans(scan, scanMap, rules) { + childNode := node.Add(childScan.Name) + buildScanSubtree(childNode, childScan, scanMap, rules) + } +} + + +func getChildScans(parentScan *v1.Scan, scanMap map[string]*v1.Scan, rules []cascadingv1.CascadingRule) []*v1.Scan { + var childScans []*v1.Scan + + for _, scan := range scanMap { + if isCascadedFrom(scan, parentScan, rules) { + childScans = append(childScans, scan) } - } } - for _, node := range scanMap { - if node.scan.Spec.InitialScan { - root.children = append(root.children, node) - } + return childScans +} + +func isCascadedFrom(childScan, parentScan *v1.Scan, rules []cascadingv1.CascadingRule) bool { + // Check if the child was created after the parent + if !childScan.CreationTimestamp.After(parentScan.CreationTimestamp.Time) { + return false } - return root + // Check for a specific annotation indicating the parent scan + if parentScanName, exists := childScan.Annotations["parentScan"]; exists && parentScanName == parentScan.Name { + return true + } + + // Check if any CascadingRule matches + for _, rule := range rules { + if matchesRule(parentScan, rule) && scanMatchesSpec(childScan, rule.Spec.ScanSpec) { + return true + } + } + + return false +} + +func matchesRule(scan *v1.Scan, rule cascadingv1.CascadingRule) bool { + for _, matchRule := range rule.Spec.Matches.AnyOf { + if matchesFinding(scan, matchRule) { + return true + } + } + return false } -func (o *cascadeOptions) renderTree(root *treeNode, out io.Writer) { - w := printers.GetNewTabWriter(out) - defer w.Flush() +func matchesFinding(scan *v1.Scan, matchRule cascadingv1.MatchesRule) bool { + // This is a simplified matching logic. You may need to adjust it based on how + // your findings are stored and how you want to match them. + + // For this example, we'll assume that findings are stored as annotations on the Scan + // with keys like "finding.category", "finding.severity", etc. + + if matchRule.Category != "" && scan.Annotations["finding.category"] != matchRule.Category { + return false + } + if matchRule.Severity != "" && scan.Annotations["finding.severity"] != matchRule.Severity { + return false + } + if matchRule.OsiLayer != "" && scan.Annotations["finding.osi_layer"] != matchRule.OsiLayer { + return false + } + + // Check attributes + for key, value := range matchRule.Attributes { + scanValue, exists := scan.Annotations["finding.attribute."+key] + if !exists { + return false + } + if value.Type == intstr.String && scanValue != value.StrVal { + return false + } + // For Int type, you'd need to parse the scanValue to int and compare + // This is left as an exercise as it depends on how you store int values in annotations + } + + return true +} - fmt.Fprintln(w, "SCAN TYPE\tNAME\tSTATUS") - o.renderNode(root, "", w) +func scanMatchesSpec(scan *v1.Scan, spec v1.ScanSpec) bool { + // Check if the scan type matches + if scan.Spec.ScanType != spec.ScanType { + return false + } + + if len(scan.Spec.Parameters) != len(spec.Parameters) { + return false + } + for i, param := range scan.Spec.Parameters { + if param != spec.Parameters[i] { + return false + } + } + + + return true } +// func isCascadedFrom(childScan, parentScan *v1.Scan) bool { +// // Check if the parent scan has a CascadeSpec +// if parentScan.Spec.Cascades == nil { +// return false +// } + +// // Check if the child was created after the parent +// if !childScan.CreationTimestamp.After(parentScan.CreationTimestamp.Time) { +// return false +// } + +// // Check for a specific annotation indicating the parent scan +// if parentScanName, exists := childScan.Annotations["parentScan"]; exists && parentScanName == parentScan.Name { +// return true +// } + +// // Create a label selector from the parent's CascadeSpec +// selector := labels.SelectorFromSet(parentScan.Spec.Cascades.MatchLabels) + +// // Add the MatchExpressions to the selector +// for _, expr := range parentScan.Spec.Cascades.MatchExpressions { +// var op selection.Operator +// switch expr.Operator { +// case metav1.LabelSelectorOpIn: +// op = selection.In +// case metav1.LabelSelectorOpNotIn: +// op = selection.NotIn +// case metav1.LabelSelectorOpExists: +// op = selection.Exists +// case metav1.LabelSelectorOpDoesNotExist: +// op = selection.DoesNotExist +// default: +// // Skip invalid operators +// continue +// } +// r, err := labels.NewRequirement(expr.Key, op, expr.Values) +// if err == nil { +// selector = selector.Add(*r) +// } +// } + +// // Check if the child scan's labels match the selector +// return selector.Matches(labels.Set(childScan.Labels)) +// } -func (o *cascadeOptions) renderNode(node *treeNode, prefix string, w io.Writer) { - if node.scan != nil { - fmt.Fprintf(w, "%s%s\t%s\t%s\n", prefix, node.scan.Spec.ScanType, node.scan.Name, string(node.scan.Status.State)) +func isInitialScan(scan *v1.Scan) bool { + log.Printf("Checking if scan %s is an initial scan", scan.Name) + + if value, exists := scan.Labels["initialScan"]; exists && value == "true" { + log.Printf("Scan %s is an initial scan (initialScan label)", scan.Name) + return true + } + if value, exists := scan.Annotations["initialScan"]; exists && value == "true" { + log.Printf("Scan %s is an initial scan (initialScan annotation)", scan.Name) + return true + } + + if _, exists := scan.Annotations["parentScan"]; exists { + log.Printf("Scan %s is not an initial scan (has parentScan annotation)", scan.Name) + return false } - childPrefix := prefix + "└── " - for i, child := range node.children { - if i == len(node.children)-1 { - childPrefix = prefix + "└── " - } else { - childPrefix = prefix + "├── " - } - o.renderNode(child, childPrefix, w) + if scan.Spec.Cascades != nil { + log.Printf("Scan %s is not an initial scan (has Cascade spec)", scan.Name) + return false } + + if len(scan.OwnerReferences) == 0 { + log.Printf("Scan %s is an initial scan (no owner references)", scan.Name) + return true + } + + log.Printf("Scan %s is assumed to be an initial scan", scan.Name) + return true } diff --git a/scbctl/cmd/root.go b/scbctl/cmd/root.go index 78a576bda3..10aad2e5c4 100644 --- a/scbctl/cmd/root.go +++ b/scbctl/cmd/root.go @@ -25,6 +25,7 @@ func NewRootCommand() *cobra.Command { rootCmd.AddCommand(NewScanCommand()) rootCmd.AddCommand(NewTriggerCommand()) + rootCmd.AddCommand(NewCascadeCommand()) return rootCmd } diff --git a/scbctl/go.mod b/scbctl/go.mod index 63548b9287..b119dd6b9f 100644 --- a/scbctl/go.mod +++ b/scbctl/go.mod @@ -16,11 +16,19 @@ require ( sigs.k8s.io/controller-runtime v0.18.4 ) -require github.com/stretchr/testify v1.8.4 +require github.com/stretchr/testify v1.9.0 + +require ( + github.com/fatih/color v1.16.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pelletier/go-toml/v2 v2.2.0 // indirect +) require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/ddddddO/gtree v1.10.10 github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v5.6.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect diff --git a/scbctl/go.sum b/scbctl/go.sum index 96536ccb10..d9f73dc78b 100644 --- a/scbctl/go.sum +++ b/scbctl/go.sum @@ -14,6 +14,8 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/ddddddO/gtree v1.10.10 h1:RcPJUTsNOq8+kVKa9OK8vkUW4R6dJw38Vcf1nD+Jpms= +github.com/ddddddO/gtree v1.10.10/go.mod h1:FVCTJTSyT7F9mVNNOyAk9qyeSbXGHIFBT0Lp+EDlEK0= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -22,6 +24,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= @@ -96,6 +100,11 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= @@ -114,6 +123,8 @@ github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8 github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= +github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -138,6 +149,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -145,6 +157,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -194,6 +208,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From c734372c9126642fc45240319813b814b22266be Mon Sep 17 00:00:00 2001 From: freedisch Date: Fri, 2 Aug 2024 13:55:47 +0200 Subject: [PATCH 03/14] fix register cascadingRule in schema Signed-off-by: freedisch --- scbctl/cmd/cascading.go | 83 +++++++++++++---------------------------- scbctl/pkg/client.go | 4 ++ 2 files changed, 29 insertions(+), 58 deletions(-) diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go index 851662c8ee..db4eac88e4 100644 --- a/scbctl/cmd/cascading.go +++ b/scbctl/cmd/cascading.go @@ -13,7 +13,6 @@ import ( cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -25,6 +24,12 @@ type cascadeOptions struct { namespace string } +// var scheme = runtime.NewScheme() + +// func init() { +// utilruntime.Must(cascadingv1.AddToScheme(scheme)) +// utilruntime.Must(v1.AddToScheme(scheme)) +// } func NewCascadeCommand() *cobra.Command { cascadeCmd := &cobra.Command{ Use: "cascade", @@ -84,8 +89,6 @@ func fetchCascadingRules(ctx context.Context, client client.Client, namespace st return ruleList.Items, nil } - - func buildTree(scans []v1.Scan, rules []cascadingv1.CascadingRule) *gtree.Node { root := gtree.NewRoot("Scans") @@ -111,6 +114,9 @@ func buildScanSubtree(node *gtree.Node, scan *v1.Scan, scanMap map[string]*v1.Sc } } +// func isInitialScan(scan *v1.Scan) bool { +// return len(scan.OwnerReferences) == 0 +// } func getChildScans(parentScan *v1.Scan, scanMap map[string]*v1.Scan, rules []cascadingv1.CascadingRule) []*v1.Scan { var childScans []*v1.Scan @@ -125,17 +131,14 @@ func getChildScans(parentScan *v1.Scan, scanMap map[string]*v1.Scan, rules []cas } func isCascadedFrom(childScan, parentScan *v1.Scan, rules []cascadingv1.CascadingRule) bool { - // Check if the child was created after the parent if !childScan.CreationTimestamp.After(parentScan.CreationTimestamp.Time) { return false } - // Check for a specific annotation indicating the parent scan if parentScanName, exists := childScan.Annotations["parentScan"]; exists && parentScanName == parentScan.Name { return true } - // Check if any CascadingRule matches for _, rule := range rules { if matchesRule(parentScan, rule) && scanMatchesSpec(childScan, rule.Spec.ScanSpec) { return true @@ -146,64 +149,28 @@ func isCascadedFrom(childScan, parentScan *v1.Scan, rules []cascadingv1.Cascadin } func matchesRule(scan *v1.Scan, rule cascadingv1.CascadingRule) bool { - for _, matchRule := range rule.Spec.Matches.AnyOf { - if matchesFinding(scan, matchRule) { - return true - } - } - return false + for _, matchRule := range rule.Spec.Matches.AnyOf { + if matchesFinding(scan, matchRule) { + return true + } + } + return false } func matchesFinding(scan *v1.Scan, matchRule cascadingv1.MatchesRule) bool { - // This is a simplified matching logic. You may need to adjust it based on how - // your findings are stored and how you want to match them. - - // For this example, we'll assume that findings are stored as annotations on the Scan - // with keys like "finding.category", "finding.severity", etc. - - if matchRule.Category != "" && scan.Annotations["finding.category"] != matchRule.Category { - return false - } - if matchRule.Severity != "" && scan.Annotations["finding.severity"] != matchRule.Severity { - return false - } - if matchRule.OsiLayer != "" && scan.Annotations["finding.osi_layer"] != matchRule.OsiLayer { - return false - } - - // Check attributes - for key, value := range matchRule.Attributes { - scanValue, exists := scan.Annotations["finding.attribute."+key] - if !exists { - return false - } - if value.Type == intstr.String && scanValue != value.StrVal { - return false - } - // For Int type, you'd need to parse the scanValue to int and compare - // This is left as an exercise as it depends on how you store int values in annotations - } - - return true + // This is a simplified matching logic. Adjust based on how your findings are stored. + if matchRule.Category != "" && scan.Annotations["finding.category"] != matchRule.Category { + return false + } + if matchRule.Severity != "" && scan.Annotations["finding.severity"] != matchRule.Severity { + return false + } + // Add more checks for other fields as necessary + return true } func scanMatchesSpec(scan *v1.Scan, spec v1.ScanSpec) bool { - // Check if the scan type matches - if scan.Spec.ScanType != spec.ScanType { - return false - } - - if len(scan.Spec.Parameters) != len(spec.Parameters) { - return false - } - for i, param := range scan.Spec.Parameters { - if param != spec.Parameters[i] { - return false - } - } - - - return true + return scan.Spec.ScanType == spec.ScanType } // func isCascadedFrom(childScan, parentScan *v1.Scan) bool { // // Check if the parent scan has a CascadeSpec diff --git a/scbctl/pkg/client.go b/scbctl/pkg/client.go index bb0b05e195..5d63edd23a 100644 --- a/scbctl/pkg/client.go +++ b/scbctl/pkg/client.go @@ -6,6 +6,8 @@ package client import ( "fmt" + cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" + excv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -29,6 +31,8 @@ func (d *DefaultClientProvider) GetClient(flags *genericclioptions.ConfigFlags) func init() { utilruntime.Must(v1.AddToScheme(scheme)) + utilruntime.Must(cascadingv1.AddToScheme(scheme)) + utilruntime.Must(excv1.AddToScheme(scheme)) } func GetClient(flags *genericclioptions.ConfigFlags) (client.Client, string, error) { From 0806db66c7d3d527baef82c0a594a889251982ee Mon Sep 17 00:00:00 2001 From: freedisch Date: Tue, 6 Aug 2024 22:12:31 +0200 Subject: [PATCH 04/14] bug: fix scans rendering issue Signed-off-by: freedisch --- scbctl/cmd/cascading.go | 152 +++++++++++++--------------------------- 1 file changed, 47 insertions(+), 105 deletions(-) diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go index db4eac88e4..ba2a4df451 100644 --- a/scbctl/cmd/cascading.go +++ b/scbctl/cmd/cascading.go @@ -6,13 +6,15 @@ package cmd import ( "context" "fmt" - "log" "os" "github.com/ddddddO/gtree" cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -107,27 +109,17 @@ func buildTree(scans []v1.Scan, rules []cascadingv1.CascadingRule) *gtree.Node { return root } -func buildScanSubtree(node *gtree.Node, scan *v1.Scan, scanMap map[string]*v1.Scan, rules []cascadingv1.CascadingRule) { - for _, childScan := range getChildScans(scan, scanMap, rules) { - childNode := node.Add(childScan.Name) - buildScanSubtree(childNode, childScan, scanMap, rules) - } +func isInitialScan(scan *v1.Scan) bool { + return scan.Spec.Cascades != nil } -// func isInitialScan(scan *v1.Scan) bool { -// return len(scan.OwnerReferences) == 0 -// } - -func getChildScans(parentScan *v1.Scan, scanMap map[string]*v1.Scan, rules []cascadingv1.CascadingRule) []*v1.Scan { - var childScans []*v1.Scan - - for _, scan := range scanMap { - if isCascadedFrom(scan, parentScan, rules) { - childScans = append(childScans, scan) +func buildScanSubtree(node *gtree.Node, scan *v1.Scan, scanMap map[string]*v1.Scan, rules []cascadingv1.CascadingRule) { + for _, childScan := range scanMap { + if isCascadedFrom(childScan, scan, rules) { + childNode := node.Add(childScan.Name) + buildScanSubtree(childNode, childScan, scanMap, rules) } } - - return childScans } func isCascadedFrom(childScan, parentScan *v1.Scan, rules []cascadingv1.CascadingRule) bool { @@ -135,12 +127,14 @@ func isCascadedFrom(childScan, parentScan *v1.Scan, rules []cascadingv1.Cascadin return false } - if parentScanName, exists := childScan.Annotations["parentScan"]; exists && parentScanName == parentScan.Name { - return true + if parentScan.Spec.Cascades == nil { + return false } + selector := createSelectorFromCascadeSpec(parentScan.Spec.Cascades) + for _, rule := range rules { - if matchesRule(parentScan, rule) && scanMatchesSpec(childScan, rule.Spec.ScanSpec) { + if selector.Matches(labels.Set(rule.Labels)) && ruleMatchesScan(rule, parentScan) && childMatchesRule(childScan, rule) { return true } } @@ -148,7 +142,35 @@ func isCascadedFrom(childScan, parentScan *v1.Scan, rules []cascadingv1.Cascadin return false } -func matchesRule(scan *v1.Scan, rule cascadingv1.CascadingRule) bool { +func createSelectorFromCascadeSpec(cascadeSpec *v1.CascadeSpec) labels.Selector { + selector := labels.NewSelector() + + for key, value := range cascadeSpec.MatchLabels { + req, _ := labels.NewRequirement(key, selection.Equals, []string{value}) + selector = selector.Add(*req) + } + + for _, expr := range cascadeSpec.MatchExpressions { + var op selection.Operator + switch expr.Operator { + case metav1.LabelSelectorOpIn: + op = selection.In + case metav1.LabelSelectorOpNotIn: + op = selection.NotIn + case metav1.LabelSelectorOpExists: + op = selection.Exists + case metav1.LabelSelectorOpDoesNotExist: + op = selection.DoesNotExist + default: + continue + } + req, _ := labels.NewRequirement(expr.Key, op, expr.Values) + selector = selector.Add(*req) + } + + return selector +} +func ruleMatchesScan(rule cascadingv1.CascadingRule, scan *v1.Scan) bool { for _, matchRule := range rule.Spec.Matches.AnyOf { if matchesFinding(scan, matchRule) { return true @@ -158,92 +180,12 @@ func matchesRule(scan *v1.Scan, rule cascadingv1.CascadingRule) bool { } func matchesFinding(scan *v1.Scan, matchRule cascadingv1.MatchesRule) bool { - // This is a simplified matching logic. Adjust based on how your findings are stored. - if matchRule.Category != "" && scan.Annotations["finding.category"] != matchRule.Category { - return false - } - if matchRule.Severity != "" && scan.Annotations["finding.severity"] != matchRule.Severity { + if matchRule.Category != "" && scan.Status.State != matchRule.Category { return false } - // Add more checks for other fields as necessary return true } -func scanMatchesSpec(scan *v1.Scan, spec v1.ScanSpec) bool { - return scan.Spec.ScanType == spec.ScanType -} -// func isCascadedFrom(childScan, parentScan *v1.Scan) bool { -// // Check if the parent scan has a CascadeSpec -// if parentScan.Spec.Cascades == nil { -// return false -// } - -// // Check if the child was created after the parent -// if !childScan.CreationTimestamp.After(parentScan.CreationTimestamp.Time) { -// return false -// } - -// // Check for a specific annotation indicating the parent scan -// if parentScanName, exists := childScan.Annotations["parentScan"]; exists && parentScanName == parentScan.Name { -// return true -// } - -// // Create a label selector from the parent's CascadeSpec -// selector := labels.SelectorFromSet(parentScan.Spec.Cascades.MatchLabels) - -// // Add the MatchExpressions to the selector -// for _, expr := range parentScan.Spec.Cascades.MatchExpressions { -// var op selection.Operator -// switch expr.Operator { -// case metav1.LabelSelectorOpIn: -// op = selection.In -// case metav1.LabelSelectorOpNotIn: -// op = selection.NotIn -// case metav1.LabelSelectorOpExists: -// op = selection.Exists -// case metav1.LabelSelectorOpDoesNotExist: -// op = selection.DoesNotExist -// default: -// // Skip invalid operators -// continue -// } -// r, err := labels.NewRequirement(expr.Key, op, expr.Values) -// if err == nil { -// selector = selector.Add(*r) -// } -// } - -// // Check if the child scan's labels match the selector -// return selector.Matches(labels.Set(childScan.Labels)) -// } - -func isInitialScan(scan *v1.Scan) bool { - log.Printf("Checking if scan %s is an initial scan", scan.Name) - - if value, exists := scan.Labels["initialScan"]; exists && value == "true" { - log.Printf("Scan %s is an initial scan (initialScan label)", scan.Name) - return true - } - if value, exists := scan.Annotations["initialScan"]; exists && value == "true" { - log.Printf("Scan %s is an initial scan (initialScan annotation)", scan.Name) - return true - } - - if _, exists := scan.Annotations["parentScan"]; exists { - log.Printf("Scan %s is not an initial scan (has parentScan annotation)", scan.Name) - return false - } - - if scan.Spec.Cascades != nil { - log.Printf("Scan %s is not an initial scan (has Cascade spec)", scan.Name) - return false - } - - if len(scan.OwnerReferences) == 0 { - log.Printf("Scan %s is an initial scan (no owner references)", scan.Name) - return true - } - - log.Printf("Scan %s is assumed to be an initial scan", scan.Name) - return true +func childMatchesRule(childScan *v1.Scan, rule cascadingv1.CascadingRule) bool { + return childScan.Spec.ScanType == rule.Spec.ScanSpec.ScanType } From ab1c1ba5480a9cea62f70a6815410d87401d2c40 Mon Sep 17 00:00:00 2001 From: freedisch Date: Fri, 9 Aug 2024 08:51:01 +0200 Subject: [PATCH 05/14] bug: fix tree to render cascading rules based on findings Signed-off-by: freedisch --- scbctl/cmd/cascading.go | 160 ++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 106 deletions(-) diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go index ba2a4df451..1f544ad3d8 100644 --- a/scbctl/cmd/cascading.go +++ b/scbctl/cmd/cascading.go @@ -4,7 +4,6 @@ package cmd import ( - "context" "fmt" "os" @@ -12,9 +11,6 @@ import ( cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -32,63 +28,43 @@ type cascadeOptions struct { // utilruntime.Must(cascadingv1.AddToScheme(scheme)) // utilruntime.Must(v1.AddToScheme(scheme)) // } + func NewCascadeCommand() *cobra.Command { cascadeCmd := &cobra.Command{ - Use: "cascade", - Short: "Display cascade of scans", - Long: `Display a tree-like structure showing the flow from initial scans to subsquent scans within a specified kubernetes namespace`, - RunE: func(cmd *cobra.Command, args []string) error { - return runCascade(cmd) - }, - } - - cascadeCmd.Flags().String("namespace", "", "Namespace to scans from") - //cascadeCmd.MarkFlagRequired("namespace") - return cascadeCmd -} - - -func runCascade(cmd *cobra.Command) error { - namespace, _ := cmd.Flags().GetString("namespace") - - kubeclient, _, err := clientProvider.GetClient(kubeconfigArgs) - if err != nil { - return fmt.Errorf("error initializing kubernetes client: %w", err) - } + Use: "cascade", + Short: "Visualize cascading rules and scans", + Long: `Visualize the relationships between scans, their findings, and the cascading rules they trigger`, + RunE: func(cmd *cobra.Command, args []string) error { + kubeclient, namespace, err := clientProvider.GetClient(kubeconfigArgs) + if err != nil { + return fmt.Errorf("error initializing kubernetes client: %w", err) + } - scans, err := fetchScans(cmd.Context(), kubeclient, namespace) - if err != nil { - return fmt.Errorf("error fetching scans: %w", err) - } + if namespaceFlag, err := cmd.Flags().GetString("namespace"); err == nil && namespaceFlag != "" { + namespace = namespaceFlag + } - cascadingRules, err := fetchCascadingRules(cmd.Context(), kubeclient, namespace) - if err != nil { - return fmt.Errorf("error fetching cascading rules: %w", err) - } + var rules cascadingv1.CascadingRuleList + if err := kubeclient.List(cmd.Context(), &rules, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("error listing CascadingRules: %w", err) + } - tree := buildTree(scans, cascadingRules) + var scans v1.ScanList + if err := kubeclient.List(cmd.Context(), &scans, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("error listing Scans: %w", err) + } - if err := gtree.OutputProgrammably(os.Stdout, tree); err != nil { - return fmt.Errorf("error outputting tree: %w", err) - } + root := buildTree(scans.Items, rules.Items) - return nil -} + if err := gtree.OutputProgrammably(os.Stdout, root); err != nil { + return fmt.Errorf("error outputting tree: %w", err) + } -func fetchScans(ctx context.Context, client client.Client, namespace string) ([]v1.Scan, error) { - var scanList v1.ScanList - if err := client.List(ctx, &scanList); err != nil { - return nil, fmt.Errorf("error listing scans: %w", err) + return nil + }, } - return scanList.Items, nil -} -func fetchCascadingRules(ctx context.Context, client client.Client, namespace string) ([]cascadingv1.CascadingRule, error) { - var ruleList cascadingv1.CascadingRuleList - if err := client.List(ctx, &ruleList); err != nil { - return nil, fmt.Errorf("error listing cascading rules: %w", err) - } - return ruleList.Items, nil + return cascadeCmd } func buildTree(scans []v1.Scan, rules []cascadingv1.CascadingRule) *gtree.Node { @@ -122,70 +98,42 @@ func buildScanSubtree(node *gtree.Node, scan *v1.Scan, scanMap map[string]*v1.Sc } } -func isCascadedFrom(childScan, parentScan *v1.Scan, rules []cascadingv1.CascadingRule) bool { - if !childScan.CreationTimestamp.After(parentScan.CreationTimestamp.Time) { - return false - } - - if parentScan.Spec.Cascades == nil { - return false - } - - selector := createSelectorFromCascadeSpec(parentScan.Spec.Cascades) - +func isCascadedFrom(childScan *v1.Scan, parentScan *v1.Scan, rules []cascadingv1.CascadingRule) bool { for _, rule := range rules { - if selector.Matches(labels.Set(rule.Labels)) && ruleMatchesScan(rule, parentScan) && childMatchesRule(childScan, rule) { + if matchScanFindings(parentScan, &rule) && rule.Spec.ScanSpec.ScanType == childScan.Spec.ScanType { return true } } - return false } -func createSelectorFromCascadeSpec(cascadeSpec *v1.CascadeSpec) labels.Selector { - selector := labels.NewSelector() - - for key, value := range cascadeSpec.MatchLabels { - req, _ := labels.NewRequirement(key, selection.Equals, []string{value}) - selector = selector.Add(*req) - } - - for _, expr := range cascadeSpec.MatchExpressions { - var op selection.Operator - switch expr.Operator { - case metav1.LabelSelectorOpIn: - op = selection.In - case metav1.LabelSelectorOpNotIn: - op = selection.NotIn - case metav1.LabelSelectorOpExists: - op = selection.Exists - case metav1.LabelSelectorOpDoesNotExist: - op = selection.DoesNotExist - default: - continue - } - req, _ := labels.NewRequirement(expr.Key, op, expr.Values) - selector = selector.Add(*req) - } - - return selector -} -func ruleMatchesScan(rule cascadingv1.CascadingRule, scan *v1.Scan) bool { +func matchScanFindings(scan *v1.Scan, rule *cascadingv1.CascadingRule) bool { for _, matchRule := range rule.Spec.Matches.AnyOf { - if matchesFinding(scan, matchRule) { - return true + if matchRule.Category != "" { + if _, exists := scan.Status.Findings.FindingCategories[matchRule.Category]; exists { + return true + } + } + if matchRule.Severity != "" { + switch matchRule.Severity { + case "High": + if scan.Status.Findings.FindingSeverities.High > 0 { + return true + } + case "Medium": + if scan.Status.Findings.FindingSeverities.Medium > 0 { + return true + } + case "Low": + if scan.Status.Findings.FindingSeverities.Low > 0 { + return true + } + case "Informational": + if scan.Status.Findings.FindingSeverities.Informational > 0 { + return true + } + } } } return false } - -func matchesFinding(scan *v1.Scan, matchRule cascadingv1.MatchesRule) bool { - if matchRule.Category != "" && scan.Status.State != matchRule.Category { - return false - } - return true -} - -func childMatchesRule(childScan *v1.Scan, rule cascadingv1.CascadingRule) bool { - return childScan.Spec.ScanType == rule.Spec.ScanSpec.ScanType -} From 530370dc1fe5de02e25c248f54999374a773909c Mon Sep 17 00:00:00 2001 From: freedisch Date: Fri, 9 Aug 2024 10:01:57 +0200 Subject: [PATCH 06/14] feat: add test cases Signed-off-by: freedisch --- scbctl/cmd/cascading_test.go | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 scbctl/cmd/cascading_test.go diff --git a/scbctl/cmd/cascading_test.go b/scbctl/cmd/cascading_test.go new file mode 100644 index 0000000000..6f27dffd8c --- /dev/null +++ b/scbctl/cmd/cascading_test.go @@ -0,0 +1,145 @@ +package cmd + +import ( + "bytes" + "testing" + + cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" + v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +type MockCascadeClientProvider struct { + client.Client + namespace string + err error +} + +func (m *MockCascadeClientProvider) GetClient(_ *genericclioptions.ConfigFlags) (client.Client, string, error) { + return m.Client, m.namespace, m.err +} + +type testcases struct { + name string + args []string + expectedError error + expectedOutput string + initialScans []v1.Scan + initialRules []cascadingv1.CascadingRule +} + +func TestCascadeCommand(t *testing.T) { + testcases := []testcases{ + { + name: "Should display simple scan tree", + args: []string{"cascade"}, + expectedError: nil, + expectedOutput: `Scans +└── nmap-scan +`, + initialScans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{Name: "nmap-scan"}, + Spec: v1.ScanSpec{ScanType: "nmap"}, + }, + }, + initialRules: []cascadingv1.CascadingRule{}, + }, + { + name: "Should display scan tree with cascading rule", + args: []string{"cascade"}, + expectedError: nil, + expectedOutput: `Scans +└── nmap-scan + └── nuclei-scan +`, + initialScans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{Name: "nmap-scan"}, + Spec: v1.ScanSpec{ScanType: "nmap"}, + Status: v1.ScanStatus{ + Findings: v1.FindingStats{ + FindingSeverities: v1.FindingSeverities{High: 1}, + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "nuclei-scan"}, + Spec: v1.ScanSpec{ScanType: "nuclei"}, + }, + }, + initialRules: []cascadingv1.CascadingRule{ + { + Spec: cascadingv1.CascadingRuleSpec{ + Matches: cascadingv1.Matches{ + AnyOf: []cascadingv1.MatchesRule{ + {Severity: "High"}, + }, + }, + ScanSpec: v1.ScanSpec{ScanType: "nuclei"}, + }, + }, + }, + }, + { + name: "Should respect namespace flag", + args: []string{"cascade", "--namespace", "test-namespace"}, + expectedError: nil, + expectedOutput: `Scans +└── nmap-scan +`, + initialScans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{Name: "nmap-scan", Namespace: "test-namespace"}, + Spec: v1.ScanSpec{ScanType: "nmap"}, + }, + { + ObjectMeta: metav1.ObjectMeta{Name: "other-scan", Namespace: "default"}, + Spec: v1.ScanSpec{ScanType: "nmap"}, + }, + }, + initialRules: []cascadingv1.CascadingRule{}, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + scheme := runtime.NewScheme() + utilruntime.Must(v1.AddToScheme(scheme)) + utilruntime.Must(cascadingv1.AddToScheme(scheme)) + + clientBuilder := fake.NewClientBuilder().WithScheme(scheme) + for _, scan := range tc.initialScans { + clientBuilder = clientBuilder.WithObjects(&scan) + } + for _, rule := range tc.initialRules { + clientBuilder = clientBuilder.WithObjects(&rule) + } + client := clientBuilder.Build() + + clientProvider = &MockClientProvider{ + Client: client, + namespace: "default", + err: nil, + } + + rootCmd := NewRootCommand() + rootCmd.SetArgs(tc.args) + rootCmd.SilenceUsage = true + + output := &bytes.Buffer{} + rootCmd.SetOut(output) + + err := rootCmd.Execute() + + assert.Equal(t, tc.expectedError, err, "error returned by cascade should match") + assert.Equal(t, tc.expectedOutput, output.String(), "output should match expected") + }) + } +} From 9758fc4dcca136d5180b13e17bf78fbdab036679 Mon Sep 17 00:00:00 2001 From: freedisch Date: Thu, 15 Aug 2024 02:18:45 +0200 Subject: [PATCH 07/14] bug: fix cascade command to target scans annotations Signed-off-by: freedisch --- scbctl/cmd/cascading.go | 132 +++++++++++++--------------------------- 1 file changed, 41 insertions(+), 91 deletions(-) diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go index 1f544ad3d8..61ccd6b149 100644 --- a/scbctl/cmd/cascading.go +++ b/scbctl/cmd/cascading.go @@ -5,10 +5,8 @@ package cmd import ( "fmt" - "os" "github.com/ddddddO/gtree" - cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -22,118 +20,70 @@ type cascadeOptions struct { namespace string } -// var scheme = runtime.NewScheme() - -// func init() { -// utilruntime.Must(cascadingv1.AddToScheme(scheme)) -// utilruntime.Must(v1.AddToScheme(scheme)) -// } - func NewCascadeCommand() *cobra.Command { cascadeCmd := &cobra.Command{ - Use: "cascade", - Short: "Visualize cascading rules and scans", - Long: `Visualize the relationships between scans, their findings, and the cascading rules they trigger`, - RunE: func(cmd *cobra.Command, args []string) error { - kubeclient, namespace, err := clientProvider.GetClient(kubeconfigArgs) - if err != nil { - return fmt.Errorf("error initializing kubernetes client: %w", err) - } - - if namespaceFlag, err := cmd.Flags().GetString("namespace"); err == nil && namespaceFlag != "" { - namespace = namespaceFlag - } - - var rules cascadingv1.CascadingRuleList - if err := kubeclient.List(cmd.Context(), &rules, client.InNamespace(namespace)); err != nil { - return fmt.Errorf("error listing CascadingRules: %w", err) - } - - var scans v1.ScanList - if err := kubeclient.List(cmd.Context(), &scans, client.InNamespace(namespace)); err != nil { - return fmt.Errorf("error listing Scans: %w", err) - } - - root := buildTree(scans.Items, rules.Items) - - if err := gtree.OutputProgrammably(os.Stdout, root); err != nil { - return fmt.Errorf("error outputting tree: %w", err) - } - - return nil - }, + Use: "cascade", + Short: "Visualize cascading scans", + Long: `Visualize the relationships between scans based on their cascading relationships`, + RunE: func(cmd *cobra.Command, args []string) error { + kubeclient, namespace, err := clientProvider.GetClient(kubeconfigArgs) + if err != nil { + return fmt.Errorf("error initializing kubernetes client: %w", err) + } + + if namespaceFlag, err := cmd.Flags().GetString("namespace"); err == nil && namespaceFlag != "" { + namespace = namespaceFlag + } + + var scans v1.ScanList + if err := kubeclient.List(cmd.Context(), &scans, client.InNamespace(namespace)); err != nil { + return fmt.Errorf("error listing Scans: %w", err) + } + + root := buildTree(scans.Items) + + return gtree.OutputProgrammably(cmd.OutOrStdout(), root) + }, } return cascadeCmd } -func buildTree(scans []v1.Scan, rules []cascadingv1.CascadingRule) *gtree.Node { +const ( + ParentScanAnnotation = "cascading.securecodebox.io/parent-scan" +) + +func buildTree(scans []v1.Scan) *gtree.Node { root := gtree.NewRoot("Scans") - + scanMap := make(map[string]*v1.Scan) for i := range scans { - scanMap[scans[i].Name] = &scans[i] + scanMap[scans[i].Name] = &scans[i] } for _, scan := range scans { - if isInitialScan(&scan) { - scanNode := root.Add(scan.Name) - buildScanSubtree(scanNode, &scan, scanMap, rules) - } + if isInitialScan(&scan) { + scanNode := root.Add(scan.Name) + buildScanSubtree(scanNode, &scan, scanMap) + } } return root } func isInitialScan(scan *v1.Scan) bool { - return scan.Spec.Cascades != nil + return scan.Annotations[ParentScanAnnotation] == "" } -func buildScanSubtree(node *gtree.Node, scan *v1.Scan, scanMap map[string]*v1.Scan, rules []cascadingv1.CascadingRule) { +func buildScanSubtree(node *gtree.Node, scan *v1.Scan, scanMap map[string]*v1.Scan) { for _, childScan := range scanMap { - if isCascadedFrom(childScan, scan, rules) { - childNode := node.Add(childScan.Name) - buildScanSubtree(childNode, childScan, scanMap, rules) - } - } -} - -func isCascadedFrom(childScan *v1.Scan, parentScan *v1.Scan, rules []cascadingv1.CascadingRule) bool { - for _, rule := range rules { - if matchScanFindings(parentScan, &rule) && rule.Spec.ScanSpec.ScanType == childScan.Spec.ScanType { - return true - } + if isCascadedFrom(childScan, scan) { + childNode := node.Add(childScan.Name) + buildScanSubtree(childNode, childScan, scanMap) + } } - return false } -func matchScanFindings(scan *v1.Scan, rule *cascadingv1.CascadingRule) bool { - for _, matchRule := range rule.Spec.Matches.AnyOf { - if matchRule.Category != "" { - if _, exists := scan.Status.Findings.FindingCategories[matchRule.Category]; exists { - return true - } - } - if matchRule.Severity != "" { - switch matchRule.Severity { - case "High": - if scan.Status.Findings.FindingSeverities.High > 0 { - return true - } - case "Medium": - if scan.Status.Findings.FindingSeverities.Medium > 0 { - return true - } - case "Low": - if scan.Status.Findings.FindingSeverities.Low > 0 { - return true - } - case "Informational": - if scan.Status.Findings.FindingSeverities.Informational > 0 { - return true - } - } - } - } - return false +func isCascadedFrom(childScan *v1.Scan, parentScan *v1.Scan) bool { + return childScan.Annotations[ParentScanAnnotation] == parentScan.Name } From a69ac4f6b54574446aec06f11b5fb99b67513778 Mon Sep 17 00:00:00 2001 From: freedisch Date: Thu, 15 Aug 2024 02:30:36 +0200 Subject: [PATCH 08/14] update test cases Signed-off-by: freedisch --- scbctl/cmd/cascading_test.go | 228 ++++++++++++++++++++++------------- 1 file changed, 144 insertions(+), 84 deletions(-) diff --git a/scbctl/cmd/cascading_test.go b/scbctl/cmd/cascading_test.go index 6f27dffd8c..ea9eb98ab5 100644 --- a/scbctl/cmd/cascading_test.go +++ b/scbctl/cmd/cascading_test.go @@ -2,6 +2,7 @@ package cmd import ( "bytes" + "strings" "testing" cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" @@ -36,110 +37,169 @@ type testcases struct { func TestCascadeCommand(t *testing.T) { testcases := []testcases{ - { - name: "Should display simple scan tree", - args: []string{"cascade"}, - expectedError: nil, - expectedOutput: `Scans + { + name: "Should display simple scan tree", + args: []string{"cascade"}, + expectedError: nil, + expectedOutput: `Scans └── nmap-scan `, - initialScans: []v1.Scan{ - { - ObjectMeta: metav1.ObjectMeta{Name: "nmap-scan"}, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, + initialScans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "nmap-scan", + Annotations: map[string]string{}, + }, + Spec: v1.ScanSpec{ScanType: "nmap"}, + }, + }, + initialRules: []cascadingv1.CascadingRule{}, }, - initialRules: []cascadingv1.CascadingRule{}, - }, - { - name: "Should display scan tree with cascading rule", - args: []string{"cascade"}, - expectedError: nil, - expectedOutput: `Scans + { + name: "Should display scan tree with parent-child relationship", + args: []string{"cascade"}, + expectedError: nil, + expectedOutput: `Scans └── nmap-scan - └── nuclei-scan + └── nuclei-scan `, - initialScans: []v1.Scan{ - { - ObjectMeta: metav1.ObjectMeta{Name: "nmap-scan"}, - Spec: v1.ScanSpec{ScanType: "nmap"}, - Status: v1.ScanStatus{ - Findings: v1.FindingStats{ - FindingSeverities: v1.FindingSeverities{High: 1}, - }, + initialScans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "nmap-scan", + Annotations: map[string]string{}, + }, + Spec: v1.ScanSpec{ScanType: "nmap"}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "nuclei-scan", + Annotations: map[string]string{ + ParentScanAnnotation: "nmap-scan", + }, + }, + Spec: v1.ScanSpec{ScanType: "nuclei"}, + }, }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "nuclei-scan"}, - Spec: v1.ScanSpec{ScanType: "nuclei"}, - }, + initialRules: []cascadingv1.CascadingRule{}, }, - initialRules: []cascadingv1.CascadingRule{ - { - Spec: cascadingv1.CascadingRuleSpec{ - Matches: cascadingv1.Matches{ - AnyOf: []cascadingv1.MatchesRule{ - {Severity: "High"}, + { + name: "Should respect namespace flag", + args: []string{"cascade", "--namespace", "test-namespace"}, + expectedError: nil, + expectedOutput: `Scans +└── nmap-scan +`, + initialScans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "nmap-scan", + Namespace: "test-namespace", + Annotations: map[string]string{}, + }, + Spec: v1.ScanSpec{ScanType: "nmap"}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "other-scan", + Namespace: "default", + Annotations: map[string]string{}, + }, + Spec: v1.ScanSpec{ScanType: "nmap"}, }, - }, - ScanSpec: v1.ScanSpec{ScanType: "nuclei"}, }, - }, + initialRules: []cascadingv1.CascadingRule{}, }, - }, - { - name: "Should respect namespace flag", - args: []string{"cascade", "--namespace", "test-namespace"}, - expectedError: nil, - expectedOutput: `Scans + { + name: "Should display complex scan tree", + args: []string{"cascade"}, + expectedError: nil, + expectedOutput: `Scans └── nmap-scan + ├── nuclei-scan-1 + │ └── zap-scan + └── nuclei-scan-2 `, - initialScans: []v1.Scan{ - { - ObjectMeta: metav1.ObjectMeta{Name: "nmap-scan", Namespace: "test-namespace"}, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "other-scan", Namespace: "default"}, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, + initialScans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "nmap-scan", + Annotations: map[string]string{}, + }, + Spec: v1.ScanSpec{ScanType: "nmap"}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "nuclei-scan-1", + Annotations: map[string]string{ + ParentScanAnnotation: "nmap-scan", + }, + }, + Spec: v1.ScanSpec{ScanType: "nuclei"}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "nuclei-scan-2", + Annotations: map[string]string{ + ParentScanAnnotation: "nmap-scan", + }, + }, + Spec: v1.ScanSpec{ScanType: "nuclei"}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "zap-scan", + Annotations: map[string]string{ + ParentScanAnnotation: "nuclei-scan-1", + }, + }, + Spec: v1.ScanSpec{ScanType: "zap"}, + }, + }, + initialRules: []cascadingv1.CascadingRule{}, }, - initialRules: []cascadingv1.CascadingRule{}, - }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { - scheme := runtime.NewScheme() - utilruntime.Must(v1.AddToScheme(scheme)) - utilruntime.Must(cascadingv1.AddToScheme(scheme)) - - clientBuilder := fake.NewClientBuilder().WithScheme(scheme) - for _, scan := range tc.initialScans { - clientBuilder = clientBuilder.WithObjects(&scan) - } - for _, rule := range tc.initialRules { - clientBuilder = clientBuilder.WithObjects(&rule) - } - client := clientBuilder.Build() - - clientProvider = &MockClientProvider{ - Client: client, - namespace: "default", - err: nil, - } + scheme := runtime.NewScheme() + utilruntime.Must(v1.AddToScheme(scheme)) + utilruntime.Must(cascadingv1.AddToScheme(scheme)) + + scanList := &v1.ScanList{ + Items: tc.initialScans, + } + client := fake.NewClientBuilder(). + WithScheme(scheme). + WithLists(scanList). + Build() - rootCmd := NewRootCommand() - rootCmd.SetArgs(tc.args) - rootCmd.SilenceUsage = true + clientProvider = &MockClientProvider{ + Client: client, + namespace: "default", + err: nil, + } - output := &bytes.Buffer{} - rootCmd.SetOut(output) + rootCmd := NewRootCommand() + rootCmd.SetArgs(tc.args) + rootCmd.SilenceUsage = true - err := rootCmd.Execute() + output := &bytes.Buffer{} + rootCmd.SetOut(output) - assert.Equal(t, tc.expectedError, err, "error returned by cascade should match") - assert.Equal(t, tc.expectedOutput, output.String(), "output should match expected") + err := rootCmd.Execute() + + assert.Equal(t, tc.expectedError, err, "error returned by cascade should match") + + actualOutput := strings.TrimRight(output.String(), "\n") + expectedOutput := strings.TrimRight(tc.expectedOutput, "\n") + + assert.Equal(t, expectedOutput, actualOutput, "output should match expected") + + if expectedOutput != actualOutput { + t.Logf("Expected:\n%s", expectedOutput) + t.Logf("Actual:\n%s", actualOutput) + } }) - } +} } From f33318467709195f5710721d764b05b9bb3ec02e Mon Sep 17 00:00:00 2001 From: freedisch Date: Thu, 15 Aug 2024 22:13:17 +0200 Subject: [PATCH 09/14] bug: fix failing test cases and update format Signed-off-by: freedisch --- scbctl/cmd/cascading_test.go | 326 ++++++++++++++++++----------------- 1 file changed, 170 insertions(+), 156 deletions(-) diff --git a/scbctl/cmd/cascading_test.go b/scbctl/cmd/cascading_test.go index ea9eb98ab5..82987387d2 100644 --- a/scbctl/cmd/cascading_test.go +++ b/scbctl/cmd/cascading_test.go @@ -2,18 +2,15 @@ package cmd import ( "bytes" - "strings" "testing" + "github.com/ddddddO/gtree" cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" ) type MockCascadeClientProvider struct { @@ -35,171 +32,188 @@ type testcases struct { initialRules []cascadingv1.CascadingRule } -func TestCascadeCommand(t *testing.T) { - testcases := []testcases{ - { - name: "Should display simple scan tree", - args: []string{"cascade"}, - expectedError: nil, - expectedOutput: `Scans -└── nmap-scan -`, - initialScans: []v1.Scan{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nmap-scan", - Annotations: map[string]string{}, - }, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, +func TestBuildTree(t *testing.T) { + tests := []struct { + name string + scans []v1.Scan + expected string + }{ + { + name: "Single scan", + scans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "scan1", }, - initialRules: []cascadingv1.CascadingRule{}, + }, }, - { - name: "Should display scan tree with parent-child relationship", - args: []string{"cascade"}, - expectedError: nil, - expectedOutput: `Scans -└── nmap-scan - └── nuclei-scan -`, - initialScans: []v1.Scan{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nmap-scan", - Annotations: map[string]string{}, - }, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nuclei-scan", - Annotations: map[string]string{ - ParentScanAnnotation: "nmap-scan", - }, - }, - Spec: v1.ScanSpec{ScanType: "nuclei"}, - }, + expected: "Scans\n└── scan1\n", + }, + { + name: "Two unrelated scans", + scans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "scan1", }, - initialRules: []cascadingv1.CascadingRule{}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "scan2", + }, + }, }, - { - name: "Should respect namespace flag", - args: []string{"cascade", "--namespace", "test-namespace"}, - expectedError: nil, - expectedOutput: `Scans -└── nmap-scan -`, - initialScans: []v1.Scan{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nmap-scan", - Namespace: "test-namespace", - Annotations: map[string]string{}, - }, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "other-scan", - Namespace: "default", - Annotations: map[string]string{}, - }, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, + expected: "Scans\n├── scan1\n└── scan2\n", + }, + { + name: "One parent, one child", + scans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "parent", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "child", + Annotations: map[string]string{ + ParentScanAnnotation: "parent", + }, }, - initialRules: []cascadingv1.CascadingRule{}, + }, }, - { - name: "Should display complex scan tree", - args: []string{"cascade"}, - expectedError: nil, - expectedOutput: `Scans -└── nmap-scan - ├── nuclei-scan-1 - │ └── zap-scan - └── nuclei-scan-2 -`, - initialScans: []v1.Scan{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nmap-scan", - Annotations: map[string]string{}, - }, - Spec: v1.ScanSpec{ScanType: "nmap"}, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nuclei-scan-1", - Annotations: map[string]string{ - ParentScanAnnotation: "nmap-scan", - }, - }, - Spec: v1.ScanSpec{ScanType: "nuclei"}, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "nuclei-scan-2", - Annotations: map[string]string{ - ParentScanAnnotation: "nmap-scan", - }, - }, - Spec: v1.ScanSpec{ScanType: "nuclei"}, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "zap-scan", - Annotations: map[string]string{ - ParentScanAnnotation: "nuclei-scan-1", - }, - }, - Spec: v1.ScanSpec{ScanType: "zap"}, - }, + expected: "Scans\n└── parent\n └── child\n", + }, + { + name: "Complex cascade", + scans: []v1.Scan{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "root", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "child1", + Annotations: map[string]string{ + ParentScanAnnotation: "root", + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "child2", + Annotations: map[string]string{ + ParentScanAnnotation: "root", + }, }, - initialRules: []cascadingv1.CascadingRule{}, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "grandchild", + Annotations: map[string]string{ + ParentScanAnnotation: "child1", + }, + }, + }, }, + expected: "Scans\n└── root\n ├── child1\n │ └── grandchild\n └── child2\n", + }, } - for _, tc := range testcases { - t.Run(tc.name, func(t *testing.T) { - scheme := runtime.NewScheme() - utilruntime.Must(v1.AddToScheme(scheme)) - utilruntime.Must(cascadingv1.AddToScheme(scheme)) - - scanList := &v1.ScanList{ - Items: tc.initialScans, - } - client := fake.NewClientBuilder(). - WithScheme(scheme). - WithLists(scanList). - Build() - - clientProvider = &MockClientProvider{ - Client: client, - namespace: "default", - err: nil, - } - - rootCmd := NewRootCommand() - rootCmd.SetArgs(tc.args) - rootCmd.SilenceUsage = true + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := buildTree(tt.scans) + var buf bytes.Buffer + err := gtree.OutputProgrammably(&buf, root) + assert.NoError(t, err) + result := buf.String() + assert.Equal(t, tt.expected, result) + }) + } +} - output := &bytes.Buffer{} - rootCmd.SetOut(output) +func TestIsInitialScan(t *testing.T) { + tests := []struct { + name string + scan v1.Scan + expected bool + }{ + { + name: "Initial scan", + scan: v1.Scan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "initial", + }, + }, + expected: true, + }, + { + name: "Child scan", + scan: v1.Scan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "child", + Annotations: map[string]string{ + ParentScanAnnotation: "parent", + }, + }, + }, + expected: false, + }, + } - err := rootCmd.Execute() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isInitialScan(&tt.scan) + assert.Equal(t, tt.expected, result) + }) + } +} - assert.Equal(t, tc.expectedError, err, "error returned by cascade should match") - - actualOutput := strings.TrimRight(output.String(), "\n") - expectedOutput := strings.TrimRight(tc.expectedOutput, "\n") - - assert.Equal(t, expectedOutput, actualOutput, "output should match expected") +func TestIsCascadedFrom(t *testing.T) { + tests := []struct { + name string + childScan v1.Scan + parentScan v1.Scan + expected bool + }{ + { + name: "Direct child", + childScan: v1.Scan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "child", + Annotations: map[string]string{ + ParentScanAnnotation: "parent", + }, + }, + }, + parentScan: v1.Scan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "parent", + }, + }, + expected: true, + }, + { + name: "Unrelated scans", + childScan: v1.Scan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scan1", + }, + }, + parentScan: v1.Scan{ + ObjectMeta: metav1.ObjectMeta{ + Name: "scan2", + }, + }, + expected: false, + }, + } - if expectedOutput != actualOutput { - t.Logf("Expected:\n%s", expectedOutput) - t.Logf("Actual:\n%s", actualOutput) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isCascadedFrom(&tt.childScan, &tt.parentScan) + assert.Equal(t, tt.expected, result) }) -} + } } From 17ea8b1241052f02ecd533c0c43ab930ca0e0235 Mon Sep 17 00:00:00 2001 From: freedisch Date: Thu, 15 Aug 2024 22:16:36 +0200 Subject: [PATCH 10/14] fix failing license CI Signed-off-by: freedisch --- scbctl/cmd/cascading_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scbctl/cmd/cascading_test.go b/scbctl/cmd/cascading_test.go index 82987387d2..99c49f2e8c 100644 --- a/scbctl/cmd/cascading_test.go +++ b/scbctl/cmd/cascading_test.go @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: the secureCodeBox authors +// +// SPDX-License-Identifier: Apache-2.0 package cmd import ( From ed2471fc5e244169456f71eb453439cf1297b667 Mon Sep 17 00:00:00 2001 From: Freedisch <82499435+Freedisch@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:10:19 +0200 Subject: [PATCH 11/14] Update scbctl/cmd/cascading.go Co-authored-by: Jannik Hollenbach Signed-off-by: Freedisch <82499435+Freedisch@users.noreply.github.com> --- scbctl/cmd/cascading.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go index 61ccd6b149..1d81875a3b 100644 --- a/scbctl/cmd/cascading.go +++ b/scbctl/cmd/cascading.go @@ -59,6 +59,8 @@ func buildTree(scans []v1.Scan) *gtree.Node { scanMap := make(map[string]*v1.Scan) for i := range scans { scanMap[scans[i].Name] = &scans[i] + for _, scan := range scans { + scanMap[scan.Name] = &scan } for _, scan := range scans { From 5db8dd491f736b258850e5c5833e74e1c20b2764 Mon Sep 17 00:00:00 2001 From: Freedisch <82499435+Freedisch@users.noreply.github.com> Date: Fri, 16 Aug 2024 14:14:01 +0200 Subject: [PATCH 12/14] Update scbctl/cmd/cascading_test.go Co-authored-by: Jannik Hollenbach Signed-off-by: Freedisch <82499435+Freedisch@users.noreply.github.com> --- scbctl/cmd/cascading_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scbctl/cmd/cascading_test.go b/scbctl/cmd/cascading_test.go index 99c49f2e8c..73d86d57f3 100644 --- a/scbctl/cmd/cascading_test.go +++ b/scbctl/cmd/cascading_test.go @@ -67,7 +67,10 @@ func TestBuildTree(t *testing.T) { }, }, expected: "Scans\n├── scan1\n└── scan2\n", - }, + expected: `Scans +├── scan1 +└── scan2 +`, { name: "One parent, one child", scans: []v1.Scan{ From cd9639d6b43579e0f3c34a238b11e2e9fc3acd1a Mon Sep 17 00:00:00 2001 From: freedisch Date: Fri, 16 Aug 2024 15:26:12 +0200 Subject: [PATCH 13/14] update test cases expected response Signed-off-by: freedisch --- scbctl/cmd/cascading.go | 1 + scbctl/cmd/cascading_test.go | 30 ++++++++++++++---------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/scbctl/cmd/cascading.go b/scbctl/cmd/cascading.go index 1d81875a3b..844192e70a 100644 --- a/scbctl/cmd/cascading.go +++ b/scbctl/cmd/cascading.go @@ -59,6 +59,7 @@ func buildTree(scans []v1.Scan) *gtree.Node { scanMap := make(map[string]*v1.Scan) for i := range scans { scanMap[scans[i].Name] = &scans[i] + } for _, scan := range scans { scanMap[scan.Name] = &scan } diff --git a/scbctl/cmd/cascading_test.go b/scbctl/cmd/cascading_test.go index 73d86d57f3..df57e8b840 100644 --- a/scbctl/cmd/cascading_test.go +++ b/scbctl/cmd/cascading_test.go @@ -12,20 +12,8 @@ import ( v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/cli-runtime/pkg/genericclioptions" - "sigs.k8s.io/controller-runtime/pkg/client" ) -type MockCascadeClientProvider struct { - client.Client - namespace string - err error -} - -func (m *MockCascadeClientProvider) GetClient(_ *genericclioptions.ConfigFlags) (client.Client, string, error) { - return m.Client, m.namespace, m.err -} - type testcases struct { name string args []string @@ -50,7 +38,9 @@ func TestBuildTree(t *testing.T) { }, }, }, - expected: "Scans\n└── scan1\n", + expected: `Scans +└── scan1 +`, }, { name: "Two unrelated scans", @@ -66,11 +56,11 @@ func TestBuildTree(t *testing.T) { }, }, }, - expected: "Scans\n├── scan1\n└── scan2\n", expected: `Scans ├── scan1 └── scan2 `, + }, { name: "One parent, one child", scans: []v1.Scan{ @@ -88,7 +78,10 @@ func TestBuildTree(t *testing.T) { }, }, }, - expected: "Scans\n└── parent\n └── child\n", + expected: `Scans +└── parent + └── child +`, }, { name: "Complex cascade", @@ -123,7 +116,12 @@ func TestBuildTree(t *testing.T) { }, }, }, - expected: "Scans\n└── root\n ├── child1\n │ └── grandchild\n └── child2\n", + expected: `Scans +└── root + ├── child1 + │ └── grandchild + └── child2 +`, }, } From 284dbe38fcb73f3437124a6b1fe6cd768469e944 Mon Sep 17 00:00:00 2001 From: freedisch Date: Sat, 17 Aug 2024 08:47:02 +0200 Subject: [PATCH 14/14] remove unused testcases Signed-off-by: freedisch --- scbctl/cmd/cascading_test.go | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/scbctl/cmd/cascading_test.go b/scbctl/cmd/cascading_test.go index df57e8b840..d85420c9f9 100644 --- a/scbctl/cmd/cascading_test.go +++ b/scbctl/cmd/cascading_test.go @@ -8,21 +8,11 @@ import ( "testing" "github.com/ddddddO/gtree" - cascadingv1 "github.com/secureCodeBox/secureCodeBox/operator/apis/cascading/v1" v1 "github.com/secureCodeBox/secureCodeBox/operator/apis/execution/v1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type testcases struct { - name string - args []string - expectedError error - expectedOutput string - initialScans []v1.Scan - initialRules []cascadingv1.CascadingRule -} - func TestBuildTree(t *testing.T) { tests := []struct { name string