From bf4f22d50b5945a9954cc4a0ede349e9d2c3dbaa Mon Sep 17 00:00:00 2001 From: achrefbensaad Date: Tue, 8 Aug 2023 10:27:52 +0000 Subject: [PATCH 1/3] refractoring recommend cli Signed-off-by: Prateek Nandle --- cmd/recommend.go | 17 +- go.mod | 6 +- hacks/common.go | 18 + recommend/admissionControllerPolicy.go | 212 ------- recommend/common/policy.go | 43 ++ recommend/engines/engine.go | 13 + .../generic_policies/generic_policies.go | 123 ++++ .../generic_policies/policy-templates.go} | 178 ++++-- .../generic_policies}/yaml/rules.yaml | 0 recommend/html/section.html | 27 - recommend/image/image.go | 322 ++++++++++ recommend/{ => image}/yaml/distro.yaml | 0 recommend/imageHandler.go | 581 ------------------ recommend/policy.go | 266 -------- recommend/policyRules.go | 81 --- recommend/recommend.go | 242 ++++---- recommend/registry/registry.go | 300 +++++++++ recommend/report.go | 112 ---- recommend/{ => report}/html/css/main.css | 0 recommend/{ => report}/html/footer.html | 0 recommend/{ => report}/html/header.html | 0 .../{ => report}/html/images/v38_6837.png | Bin .../{ => report}/html/images/v38_7029.png | Bin recommend/{ => report}/html/record.html | 0 recommend/{ => report}/html/sectend.html | 0 recommend/report/html/section.html | 23 + recommend/report/report.go | 84 +++ recommend/{ => report}/report_html.go | 86 +-- recommend/{ => report}/report_text.go | 42 +- recommend/runtimePolicy.go | 126 ---- tests/recommend/recommend_test.go | 18 +- 31 files changed, 1214 insertions(+), 1706 deletions(-) create mode 100644 hacks/common.go delete mode 100644 recommend/admissionControllerPolicy.go create mode 100644 recommend/common/policy.go create mode 100644 recommend/engines/engine.go create mode 100644 recommend/engines/generic_policies/generic_policies.go rename recommend/{policyTemplates.go => engines/generic_policies/policy-templates.go} (62%) rename recommend/{ => engines/generic_policies}/yaml/rules.yaml (100%) delete mode 100644 recommend/html/section.html create mode 100644 recommend/image/image.go rename recommend/{ => image}/yaml/distro.yaml (100%) delete mode 100644 recommend/imageHandler.go delete mode 100644 recommend/policy.go delete mode 100644 recommend/policyRules.go create mode 100644 recommend/registry/registry.go delete mode 100644 recommend/report.go rename recommend/{ => report}/html/css/main.css (100%) rename recommend/{ => report}/html/footer.html (100%) rename recommend/{ => report}/html/header.html (100%) rename recommend/{ => report}/html/images/v38_6837.png (100%) rename recommend/{ => report}/html/images/v38_7029.png (100%) rename recommend/{ => report}/html/record.html (100%) rename recommend/{ => report}/html/sectend.html (100%) create mode 100644 recommend/report/html/section.html create mode 100644 recommend/report/report.go rename recommend/{ => report}/report_html.go (67%) rename recommend/{ => report}/report_text.go (63%) delete mode 100644 recommend/runtimePolicy.go diff --git a/cmd/recommend.go b/cmd/recommend.go index 19a40218..41bd7463 100644 --- a/cmd/recommend.go +++ b/cmd/recommend.go @@ -5,11 +5,13 @@ package cmd import ( "github.com/kubearmor/kubearmor-client/recommend" + "github.com/kubearmor/kubearmor-client/recommend/common" + genericpolicies "github.com/kubearmor/kubearmor-client/recommend/engines/generic_policies" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) -var recommendOptions recommend.Options +var recommendOptions common.Options // recommendCmd represents the recommend command var recommendCmd = &cobra.Command{ @@ -17,10 +19,8 @@ var recommendCmd = &cobra.Command{ Short: "Recommend Policies", Long: `Recommend policies based on container image, k8s manifest or the actual runtime env`, RunE: func(cmd *cobra.Command, args []string) error { - if err := recommend.Recommend(client, recommendOptions); err != nil { - return err - } - return nil + err := recommend.Recommend(client, recommendOptions, genericpolicies.GenericPolicy{}) + return err }, } var updateCmd = &cobra.Command{ @@ -29,11 +29,11 @@ var updateCmd = &cobra.Command{ Long: "Updates the local cache of policy-templates ($HOME/.cache/karmor)", RunE: func(cmd *cobra.Command, args []string) error { - if _, err := recommend.DownloadAndUnzipRelease(); err != nil { + if _, err := genericpolicies.DownloadAndUnzipRelease(); err != nil { return err } log.WithFields(log.Fields{ - "Current Version": recommend.CurrentVersion, + "Current Version": genericpolicies.CurrentVersion, }).Info("policy-templates updated") return nil }, @@ -45,10 +45,9 @@ func init() { recommendCmd.Flags().StringSliceVarP(&recommendOptions.Images, "image", "i", []string{}, "Container image list (comma separated)") recommendCmd.Flags().StringSliceVarP(&recommendOptions.Labels, "labels", "l", []string{}, "User defined labels for policy (comma separated)") - recommendCmd.Flags().StringSliceVarP(&recommendOptions.Policy, "policy", "p", recommend.DefaultPoliciesToBeRecommended, "Types of policy that can be recommended: KubeArmorPolicy|KyvernoPolicy (comma separated)") recommendCmd.Flags().StringVarP(&recommendOptions.Namespace, "namespace", "n", "", "User defined namespace value for policies") recommendCmd.Flags().StringVarP(&recommendOptions.OutDir, "outdir", "o", "out", "output folder to write policies") recommendCmd.Flags().StringVarP(&recommendOptions.ReportFile, "report", "r", "report.txt", "report file") recommendCmd.Flags().StringSliceVarP(&recommendOptions.Tags, "tag", "t", []string{}, "tags (comma-separated) to apply. Eg. PCI-DSS, MITRE") - recommendCmd.Flags().StringVarP(&recommendOptions.Config, "config", "c", recommend.UserHome()+"/.docker/config.json", "absolute path to image registry configuration file") + recommendCmd.Flags().StringVarP(&recommendOptions.Config, "config", "c", genericpolicies.UserHome()+"/.docker/config.json", "absolute path to image registry configuration file") } diff --git a/go.mod b/go.mod index 118961a3..83e3ebcb 100644 --- a/go.mod +++ b/go.mod @@ -57,17 +57,14 @@ require ( github.com/kubearmor/KubeArmor/KubeArmor v0.0.0-20230704182508-0dd8f8bb9507 github.com/kubearmor/KubeArmor/deployments v0.0.0-20230626102117-7ce0284b7dc1 github.com/kubearmor/KubeArmor/pkg/KubeArmorController v0.0.0-20230626060245-4f5b8ac4f298 - github.com/kyverno/kyverno v1.9.2 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/onsi/ginkgo/v2 v2.9.5 github.com/onsi/gomega v1.27.7 - golang.org/x/text v0.10.0 k8s.io/api v0.27.3 k8s.io/apiextensions-apiserver v0.27.3 k8s.io/apimachinery v0.27.3 k8s.io/cli-runtime v0.27.1 k8s.io/client-go v0.27.2 - k8s.io/utils v0.0.0-20230505201702-9f6742963106 ) require ( @@ -209,6 +206,7 @@ require ( github.com/klauspost/pgzip v1.2.5 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/kyverno/kyverno v1.9.2 // indirect github.com/leodido/go-urn v1.2.3 // indirect github.com/letsencrypt/boulder v0.0.0-20230426205424-1c7e0fd1d876 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect @@ -319,6 +317,7 @@ require ( golang.org/x/net v0.11.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/term v0.9.0 // indirect + golang.org/x/text v0.10.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.1 // indirect google.golang.org/api v0.122.0 // indirect @@ -334,6 +333,7 @@ require ( k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect k8s.io/kubectl v0.27.1 // indirect k8s.io/pod-security-admission v0.27.1 // indirect + k8s.io/utils v0.0.0-20230505201702-9f6742963106 // indirect sigs.k8s.io/controller-runtime v0.15.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.2 // indirect diff --git a/hacks/common.go b/hacks/common.go new file mode 100644 index 00000000..f90ad7e8 --- /dev/null +++ b/hacks/common.go @@ -0,0 +1,18 @@ +// Package hacks close the file +package hacks + +import ( + "os" + + log "github.com/sirupsen/logrus" +) + +// CloseCheckErr close file +func CloseCheckErr(f *os.File, fname string) { + err := f.Close() + if err != nil { + log.WithFields(log.Fields{ + "file": fname, + }).Error("close file failed") + } +} diff --git a/recommend/admissionControllerPolicy.go b/recommend/admissionControllerPolicy.go deleted file mode 100644 index b1b05c28..00000000 --- a/recommend/admissionControllerPolicy.go +++ /dev/null @@ -1,212 +0,0 @@ -package recommend - -import ( - "context" - "errors" - "fmt" - "os" - "strconv" - "strings" - - "github.com/accuknox/auto-policy-discovery/src/libs" - "github.com/accuknox/auto-policy-discovery/src/protobuf/v1/worker" - "github.com/accuknox/auto-policy-discovery/src/types" - "github.com/clarketm/json" - "github.com/fatih/color" - "github.com/kubearmor/kubearmor-client/k8s" - "github.com/kubearmor/kubearmor-client/utils" - kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" - log "github.com/sirupsen/logrus" - "golang.org/x/exp/slices" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "sigs.k8s.io/yaml" -) - -var connection *grpc.ClientConn - -func initClientConnection(c *k8s.Client) error { - if connection != nil { - return nil - } - var err error - connection, err = getClientConnection(c) - if err != nil { - return err - } - log.Info("Connected to discovery engine") - return nil -} - -func closeConnectionToDiscoveryEngine() { - if connection != nil { - err := connection.Close() - if err != nil { - log.Println("Error while closing connection") - } else { - log.Info("Connection to discovery engine closed successfully!") - } - } -} - -func getClientConnection(c *k8s.Client) (*grpc.ClientConn, error) { - gRPC := "" - targetSvc := "discovery-engine" - var port int64 = 9089 - mtchLabels := map[string]string{"app": "discovery-engine"} - if val, ok := os.LookupEnv("DISCOVERY_SERVICE"); ok { - gRPC = val - } else { - pf, err := utils.InitiatePortForward(c, port, port, mtchLabels, targetSvc) - if err != nil { - return nil, err - } - gRPC = "localhost:" + strconv.FormatInt(pf.LocalPort, 10) - } - // create a client - conn, err := grpc.Dial(gRPC, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - return nil, errors.New("could not connect to the server. Possible troubleshooting:\n- Check if discovery engine is running\n- Create a portforward to discovery engine service using\n\t\033[1mkubectl port-forward -n explorer service/knoxautopolicy --address 0.0.0.0 --address :: 9089:9089\033[0m\n[0m") - } - return conn, nil -} - -func recommendAdmissionControllerPolicies(img ImageInfo) error { - client := worker.NewWorkerClient(connection) - labels := libs.LabelMapToString(img.Labels) - resp, err := client.Convert(context.Background(), &worker.WorkerRequest{ - Labels: labels, - Namespace: img.Namespace, - Policytype: types.PolicyTypeAdmissionController, - }) - if err != nil { - color.Red(err.Error()) - return err - } - if resp.AdmissionControllerPolicy != nil { - for _, policy := range resp.AdmissionControllerPolicy { - var kyvernoPolicyInterface kyvernov1.PolicyInterface - kyvernoPolicyInterface, err = getKyvernoPolicy(policy.Data) - if err != nil { - return err - } - if namespaceMatches(kyvernoPolicyInterface.GetNamespace()) && matchAdmissionControllerPolicyTags(kyvernoPolicyInterface.GetAnnotations()) { - img.writeAdmissionControllerPolicy(kyvernoPolicyInterface) - } - } - } - return nil -} - -func recommendGenericAdmissionControllerPolicies() error { - client := worker.NewWorkerClient(connection) - resp, err := client.Convert(context.Background(), &worker.WorkerRequest{ - Policytype: types.PolicyTypeAdmissionControllerGeneric, - }) - if err != nil { - color.Red(err.Error()) - return err - } - if resp.AdmissionControllerPolicy != nil { - reportStarted := false - for _, policy := range resp.AdmissionControllerPolicy { - var kyvernoPolicyInterface kyvernov1.PolicyInterface - kyvernoPolicyInterface, err = getKyvernoPolicy(policy.Data) - if err != nil { - if reportStarted { - err := ReportSectEnd() - if err != nil { - return err - } - } - return err - } - if matchAdmissionControllerPolicyTags(kyvernoPolicyInterface.GetAnnotations()) { - if !reportStarted { - err := ReportStartGenericAdmissionControllerPolicies() - if err != nil { - return err - } - reportStarted = true - } - writeGenericAdmissionControllerPolicy(kyvernoPolicyInterface) - } - } - if reportStarted { - err := ReportSectEnd() - if err != nil { - return err - } - } - } - return nil -} - -func matchAdmissionControllerPolicyTags(policyAnnotations map[string]string) bool { - policyTags := strings.Split(policyAnnotations[types.RecommendedPolicyTagsAnnotation], ",") - if len(options.Tags) <= 0 { - return true - } - for _, t := range options.Tags { - if slices.Contains(policyTags, t) { - return true - } - } - return false -} - -func namespaceMatches(policyNamespace string) bool { - return options.Namespace == "" || options.Namespace == policyNamespace -} - -func getKyvernoPolicy(policyYaml []byte) (kyvernov1.PolicyInterface, error) { - var policy map[string]interface{} - err := yaml.Unmarshal(policyYaml, &policy) - if err != nil { - return nil, err - } - policyKind := policy["kind"].(string) - - var kyvernoPolicyInterface kyvernov1.PolicyInterface - switch policyKind { - case "Policy": - var kyvernoPolicy kyvernov1.Policy - err = yaml.Unmarshal(policyYaml, &kyvernoPolicy) - if err != nil { - return nil, err - } - kyvernoPolicyInterface = &kyvernoPolicy - case "ClusterPolicy": - var kyvernoClusterPolicy kyvernov1.ClusterPolicy - err = yaml.Unmarshal(policyYaml, &kyvernoClusterPolicy) - if err != nil { - return nil, err - } - kyvernoPolicyInterface = &kyvernoClusterPolicy - default: - return nil, fmt.Errorf("unexpected policy kind: %s", policyKind) - } - return kyvernoPolicyInterface, nil -} - -func convertKyvernoPolicyInterfaceToJSON(policyInterface kyvernov1.PolicyInterface) ([]byte, error) { - var jsonBytes []byte - var err error - switch policyInterface.(type) { - case *kyvernov1.ClusterPolicy: - kyvernoClusterPolicy := policyInterface.(*kyvernov1.ClusterPolicy) - jsonBytes, err = json.Marshal(*kyvernoClusterPolicy) - if err != nil { - log.WithError(err).Error("json marshal failed") - return nil, err - } - case *kyvernov1.Policy: - kyvernoPolicy := policyInterface.(*kyvernov1.Policy) - jsonBytes, err = json.Marshal(*kyvernoPolicy) - if err != nil { - log.WithError(err).Error("json marshal failed") - return nil, err - } - } - return jsonBytes, nil -} diff --git a/recommend/common/policy.go b/recommend/common/policy.go new file mode 100644 index 00000000..9865ee17 --- /dev/null +++ b/recommend/common/policy.go @@ -0,0 +1,43 @@ +// Package common contains object types used by multiple packages +package common + +import ( + pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" +) + +// Handler interface +var Handler interface{} + +// MatchSpec spec to match for defining policy +type MatchSpec struct { + Name string `json:"name" yaml:"name"` + Precondition []string `json:"precondition" yaml:"precondition"` + Description Description `json:"description" yaml:"description"` + Yaml string `json:"yaml" yaml:"yaml"` + Spec pol.KubeArmorPolicySpec `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +// Ref for the policy rules +type Ref struct { + Name string `json:"name" yaml:"name"` + URL []string `json:"url" yaml:"url"` +} + +// Description detailed description for the policy rule +type Description struct { + Refs []Ref `json:"refs" yaml:"refs"` + Tldr string `json:"tldr" yaml:"tldr"` + Detailed string `json:"detailed" yaml:"detailed"` +} + +// Options for karmor recommend +type Options struct { + Images []string + Labels []string + Tags []string + Policy []string + Namespace string + OutDir string + ReportFile string + Config string +} diff --git a/recommend/engines/engine.go b/recommend/engines/engine.go new file mode 100644 index 00000000..69427db0 --- /dev/null +++ b/recommend/engines/engine.go @@ -0,0 +1,13 @@ +// Package engines provides interfaces and implementations for policy generation +package engines + +import ( + "github.com/kubearmor/kubearmor-client/recommend/common" + "github.com/kubearmor/kubearmor-client/recommend/image" +) + +// Engine interface used by policy generators to generate policies +type Engine interface { + Init() error + Scan(img *image.Info, options common.Options) (map[string][]byte, map[string]interface{}, error) +} diff --git a/recommend/engines/generic_policies/generic_policies.go b/recommend/engines/generic_policies/generic_policies.go new file mode 100644 index 00000000..6053ffeb --- /dev/null +++ b/recommend/engines/generic_policies/generic_policies.go @@ -0,0 +1,123 @@ +// Package genericpolicies is responsible for creating and managing policies based on policy generator +package genericpolicies + +import ( + _ "embed" // need for embedding + "fmt" + "path/filepath" + + "regexp" + "strings" + + "github.com/fatih/color" + "github.com/kubearmor/kubearmor-client/recommend/common" + "github.com/kubearmor/kubearmor-client/recommend/image" + "github.com/kubearmor/kubearmor-client/recommend/report" + log "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" +) + +const ( + org = "kubearmor" + repo = "policy-templates" + url = "https://github.com/kubearmor/policy-templates/archive/refs/tags/" + cache = ".cache/karmor/" +) + +// GenericPolicy defines Policy Generators +type GenericPolicy struct { +} + +// Init initializing Policy Generator +func (P GenericPolicy) Init() error { + if _, err := DownloadAndUnzipRelease(); err != nil { + return err + } + return nil +} + +// Scan image and generates policies +func (P GenericPolicy) Scan(img *image.Info, options common.Options) (map[string][]byte, map[string]interface{}, error) { + var policyMap map[string][]byte + var msMap map[string]interface{} + var err error + if policyMap, msMap, err = getPolicyFromImageInfo(img, options); err != nil { + log.WithError(err).Error("policy generation from image info failed") + } + return policyMap, msMap, nil +} + +func checkForSpec(spec string, fl []string) []string { + var matches []string + if !strings.HasSuffix(spec, "*") { + spec = fmt.Sprintf("%s$", spec) + } + + re := regexp.MustCompile(spec) + for _, name := range fl { + if re.Match([]byte(name)) { + matches = append(matches, name) + } + } + return matches +} + +func matchTags(ms *common.MatchSpec, tags []string) bool { + if len(tags) <= 0 { + return true + } + for _, t := range tags { + if slices.Contains(ms.Spec.Tags, t) { + return true + } + } + return false +} + +func checkPreconditions(img *image.Info, ms *common.MatchSpec) bool { + var matches []string + for _, preCondition := range ms.Precondition { + matches = append(matches, checkForSpec(filepath.Join(preCondition), img.FileList)...) + if strings.Contains(preCondition, "OPTSCAN") { + return true + } + } + return len(matches) >= len(ms.Precondition) +} + +func getPolicyFromImageInfo(img *image.Info, options common.Options) (map[string][]byte, map[string]interface{}, error) { + var policy []byte + var outFile string + policyMap := map[string][]byte{} + msMap := make(map[string]interface{}) + + if img.OS != "linux" { + color.Red("non-linux platforms are not supported, yet.") + return nil, nil, nil + } + + idx := 0 + + if err := report.Start(img, options, CurrentVersion); err != nil { + log.WithError(err).Error("report start failed") + return nil, nil, err + } + var ms common.MatchSpec + var err error + + ms, err = getNextRule(&idx) + for ; err == nil; ms, err = getNextRule(&idx) { + + if !matchTags(&ms, options.Tags) { + continue + } + + if !checkPreconditions(img, &ms) { + continue + } + policy, outFile = img.GetPolicy(ms, options) + policyMap[outFile] = policy + msMap[outFile] = ms + } + return policyMap, msMap, nil +} diff --git a/recommend/policyTemplates.go b/recommend/engines/generic_policies/policy-templates.go similarity index 62% rename from recommend/policyTemplates.go rename to recommend/engines/generic_policies/policy-templates.go index b67e7f38..36ae9b99 100644 --- a/recommend/policyTemplates.go +++ b/recommend/engines/generic_policies/policy-templates.go @@ -1,11 +1,11 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor - -package recommend +package genericpolicies import ( "archive/zip" "context" + _ "embed" // need for embedding + "encoding/json" + "errors" "fmt" "os" "path" @@ -14,42 +14,27 @@ import ( "strings" "github.com/cavaliergopher/grab/v3" + "github.com/fatih/color" "github.com/google/go-github/github" kg "github.com/kubearmor/KubeArmor/KubeArmor/log" pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" + "github.com/kubearmor/kubearmor-client/recommend/common" log "github.com/sirupsen/logrus" "sigs.k8s.io/yaml" ) -const ( - org = "kubearmor" - repo = "policy-templates" - url = "https://github.com/kubearmor/policy-templates/archive/refs/tags/" - cache = ".cache/karmor/" -) - // CurrentVersion stores the current version of policy-template var CurrentVersion string -// LatestVersion stores the latest version of policy-template -var LatestVersion string - -func getCachePath() string { - cache := fmt.Sprintf("%s/%s", UserHome(), cache) - return cache - -} - -// UserHome function returns users home directory -func UserHome() string { - if runtime.GOOS == "windows" { - home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - if home == "" { - home = os.Getenv("USERPROFILE") - } - return home +func isLatest() bool { + LatestVersion := latestRelease() + CurrentVersion = CurrentRelease() + if LatestVersion == "" { + // error while fetching latest release tag + // assume the current release is the latest one + return true } - return os.Getenv("HOME") + return (CurrentVersion == LatestVersion) } func latestRelease() string { @@ -63,7 +48,7 @@ func latestRelease() string { // CurrentRelease gets the current release of policy-templates func CurrentRelease() string { - + CurrentVersion := "" path, err := os.ReadFile(fmt.Sprintf("%s%s", getCachePath(), "rules.yaml")) if err != nil { CurrentVersion = strings.Trim(updateRulesYAML([]byte{}), "\"") @@ -71,19 +56,55 @@ func CurrentRelease() string { CurrentVersion = strings.Trim(updateRulesYAML(path), "\"") } - return CurrentVersion } -func isLatest() bool { - LatestVersion = latestRelease() +func getCachePath() string { + cache := fmt.Sprintf("%s/%s", UserHome(), cache) + return cache - if LatestVersion == "" { - // error while fetching latest release tag - // assume the current release is the latest one - return true +} + +// UserHome function returns users home directory +func UserHome() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home } - return (CurrentVersion == LatestVersion) + return os.Getenv("HOME") +} + +//go:embed yaml/rules.yaml + +var policyRulesYAML []byte + +var policyRules []common.MatchSpec + +func updateRulesYAML(yamlFile []byte) string { + policyRules = []common.MatchSpec{} + if len(yamlFile) < 30 { + yamlFile = policyRulesYAML + } + policyRulesJSON, err := yaml.YAMLToJSON(yamlFile) + if err != nil { + color.Red("failed to convert policy rules yaml to json") + log.WithError(err).Fatal("failed to convert policy rules yaml to json") + } + var jsonRaw map[string]json.RawMessage + err = json.Unmarshal(policyRulesJSON, &jsonRaw) + if err != nil { + color.Red("failed to unmarshal policy rules json") + log.WithError(err).Fatal("failed to unmarshal policy rules json") + } + err = json.Unmarshal(jsonRaw["policyRules"], &policyRules) + if err != nil { + color.Red("failed to unmarshal policy rules") + log.WithError(err).Fatal("failed to unmarshal policy rules") + } + return string(jsonRaw["version"]) } func removeData(file string) error { @@ -91,24 +112,35 @@ func removeData(file string) error { return err } -func init() { - CurrentVersion = CurrentRelease() -} - // DownloadAndUnzipRelease downloads the latest version of policy-templates func DownloadAndUnzipRelease() (string, error) { + latestVersion := latestRelease() + currentVersion := CurrentRelease() + + if isLatest() { + return latestVersion, nil + } - LatestVersion = latestRelease() + log.WithFields(log.Fields{ + "Current Version": currentVersion, + }).Info("Found outdated version of policy-templates") + log.Info("Downloading latest version [", latestVersion, "]") - _ = removeData(getCachePath()) - err := os.MkdirAll(filepath.Dir(getCachePath()), 0750) + err := removeData(getCachePath()) + if err != nil { + log.WithError(err).Error("failed to remove cache files") + } + err = os.MkdirAll(filepath.Dir(getCachePath()), 0750) if err != nil { return "", err } - downloadURL := fmt.Sprintf("%s%s.zip", url, LatestVersion) + downloadURL := fmt.Sprintf("%s%s.zip", url, latestVersion) resp, err := grab.Get(getCachePath(), downloadURL) if err != nil { - _ = removeData(getCachePath()) + err = removeData(getCachePath()) + if err != nil { + log.WithError(err).Error("failed to remove cache files") + } return "", err } err = unZip(resp.Filename, getCachePath()) @@ -117,11 +149,26 @@ func DownloadAndUnzipRelease() (string, error) { } err = removeData(resp.Filename) if err != nil { - return "", err + log.WithError(err).Error("failed to remove cache files") } - _ = updatePolicyRules(strings.TrimSuffix(resp.Filename, ".zip")) - CurrentVersion = CurrentRelease() - return LatestVersion, nil + err = updatePolicyRules(strings.TrimSuffix(resp.Filename, ".zip")) + if err != nil { + log.WithError(err).Error("failed to update policy rules") + } + log.WithFields(log.Fields{ + "Updated Version": latestVersion, + }).Info("policy-templates updated") + return latestVersion, nil +} + +// Sanitize archive file pathing from "G305: Zip Slip vulnerability" +func sanitizeArchivePath(d, t string) (v string, err error) { + v = filepath.Join(d, t) + if strings.HasPrefix(v, filepath.Clean(d)) { + return v, nil + } + + return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) } func unZip(source, dest string) error { @@ -142,7 +189,10 @@ func unZip(source, dest string) error { if err != nil { return err } - _ = os.MkdirAll(path.Dir(name), 0750) + err = os.MkdirAll(path.Dir(name), 0750) + if err != nil { + log.WithError(err).Error("failed to create directory") + } create, err := os.Create(filepath.Clean(name)) if err != nil { return err @@ -163,6 +213,18 @@ func unZip(source, dest string) error { return nil } +func getNextRule(idx *int) (common.MatchSpec, error) { + if *idx < 0 { + (*idx)++ + } + if *idx >= len(policyRules) { + return common.MatchSpec{}, errors.New("no rule at idx") + } + r := policyRules[*idx] + (*idx)++ + return r, nil +} + func updatePolicyRules(filePath string) error { var files []string err := filepath.Walk(filePath, func(path string, info os.FileInfo, err error) error { @@ -184,7 +246,7 @@ func updatePolicyRules(filePath string) error { } var yamlFile []byte - var completePolicy []MatchSpec + var completePolicy []common.MatchSpec var version string for _, file := range files { @@ -207,23 +269,22 @@ func updatePolicyRules(filePath string) error { return err } apiVersion := policy["apiVersion"].(string) - if strings.Contains(apiVersion, "kyverno") { - // No need to add Kyverno policies to 'rules.yaml' - // Kyverno policies are fetched from discovery engine - continue - } else if strings.Contains(apiVersion, "kubearmor") { + if strings.Contains(apiVersion, "kubearmor") { var kubeArmorPolicy pol.KubeArmorPolicy err = yaml.Unmarshal(newYaml, &kubeArmorPolicy) if err != nil { return err } ms.Spec = kubeArmorPolicy.Spec + } else { + continue } ms.Yaml = "" } completePolicy = append(completePolicy, ms) } } + policyRules = completePolicy yamlFile, err = yaml.Marshal(completePolicy) if err != nil { return err @@ -239,5 +300,6 @@ func updatePolicyRules(filePath string) error { if err := f.Close(); err != nil { log.WithError(err).Error("file close failed") } + return nil } diff --git a/recommend/yaml/rules.yaml b/recommend/engines/generic_policies/yaml/rules.yaml similarity index 100% rename from recommend/yaml/rules.yaml rename to recommend/engines/generic_policies/yaml/rules.yaml diff --git a/recommend/html/section.html b/recommend/html/section.html deleted file mode 100644 index ea347d79..00000000 --- a/recommend/html/section.html +++ /dev/null @@ -1,27 +0,0 @@ -
-
-
-
- {{if .GenericAdmissionControllerPolicy}} -

Generic Kyverno Policies

- {{else}} - - {{range .ImgInfo}} - - - - - {{end}} -
{{.Key}}: {{.Val}}
- {{end}} -
-
-
-
- - - {{range .HdrCols}} - - {{end}} - - diff --git a/recommend/image/image.go b/recommend/image/image.go new file mode 100644 index 00000000..e075f750 --- /dev/null +++ b/recommend/image/image.go @@ -0,0 +1,322 @@ +// Package image scan and provide image info +package image + +import ( + _ "embed" // need for embedding + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/fatih/color" + pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" + "github.com/kubearmor/kubearmor-client/hacks" + "github.com/kubearmor/kubearmor-client/recommend/common" + log "github.com/sirupsen/logrus" + "sigs.k8s.io/yaml" +) + +type distroRule struct { + Name string `json:"name" yaml:"name"` + Match []struct { + Path string `json:"path" yaml:"path"` + } `json:"match" yaml:"match"` +} + +//go:embed yaml/distro.yaml +var distroYAML []byte + +var distroRules []distroRule + +// Info contains image information +type Info struct { + Name string + Namespace string + Labels LabelMap + Deployment string + Image string + + RepoTags []string + Arch string + Distro string + OS string + FileList []string + DirList []string + + TempDir string +} + +// LabelMap is an alias for map[string]string +type LabelMap = map[string]string + +func init() { + distroJSON, err := yaml.YAMLToJSON(distroYAML) + if err != nil { + color.Red("failed to convert distro rules yaml to json") + log.WithError(err).Fatal("failed to convert distro rules yaml to json") + } + + var jsonRaw map[string]json.RawMessage + err = json.Unmarshal(distroJSON, &jsonRaw) + if err != nil { + color.Red("failed to unmarshal distro rules json") + log.WithError(err).Fatal("failed to unmarshal distro rules json") + } + + err = json.Unmarshal(jsonRaw["distroRules"], &distroRules) + if err != nil { + color.Red("failed to unmarshal distro rules") + log.WithError(err).Fatal("failed to unmarshal distro rules") + } +} + +// GetImageInfo fetches information about the image and reads its manifest +func (img *Info) GetImageInfo() { + matches := checkForSpec(filepath.Join(img.TempDir, "manifest.json"), img.FileList) + if len(matches) != 1 { + log.WithFields(log.Fields{ + "len": len(matches), + "matches": matches, + }).Fatal("expecting one manifest.json!") + } + img.readManifest(matches[0]) + + img.GetDistro() +} + +// GetDistro identifies the distribution of the image +func (img *Info) GetDistro() { + for _, d := range distroRules { + match := true + for _, m := range d.Match { + matches := checkForSpec(filepath.Clean(img.TempDir+m.Path), img.FileList) + if len(matches) == 0 { + match = false + break + } + } + if len(d.Match) > 0 && match { + color.Green("Distribution %s", d.Name) + img.Distro = d.Name + return + } + } +} + +func checkForSpec(spec string, fl []string) []string { + var matches []string + if !strings.HasSuffix(spec, "*") { + spec = fmt.Sprintf("%s$", spec) + } + + re := regexp.MustCompile(spec) + for _, name := range fl { + if re.Match([]byte(name)) { + matches = append(matches, name) + } + } + return matches +} + +func (img *Info) readManifest(manifest string) { + // read manifest file + barr, err := getFileBytes(manifest) + if err != nil { + log.WithError(err).Fatal("manifest read failed") + } + var manres []map[string]interface{} + err = json.Unmarshal(barr, &manres) + if err != nil { + log.WithError(err).Fatal("manifest json unmarshal failed") + } + if len(manres) < 1 { + log.WithFields(log.Fields{ + "len": len(manres), + "results": manres, + }).Fatal("expecting atleast one config in manifest!") + } + + var man map[string]interface{} + for _, man = range manres { + if man["RepoTags"] != nil { + break + } + } + + // read config file + config := filepath.Join(img.TempDir, man["Config"].(string)) + barr, err = getFileBytes(config) + if err != nil { + log.WithFields(log.Fields{ + "config": config, + }).Fatal("config read failed") + } + var cfgres map[string]interface{} + err = json.Unmarshal(barr, &cfgres) + if err != nil { + log.WithError(err).Fatal("config json unmarshal failed") + } + img.Arch = cfgres["architecture"].(string) + img.OS = cfgres["os"].(string) + + if man["RepoTags"] == nil { + // If the image name contains sha256 digest, + // then manifest["RepoTags"] will be `nil`. + img.RepoTags = append(img.RepoTags, shortenImageNameWithSha256(img.Name)) + } else { + for _, tag := range man["RepoTags"].([]interface{}) { + img.RepoTags = append(img.RepoTags, tag.(string)) + } + } +} + +// shortenImageNameWithSha256 truncates the sha256 digest in image name +func shortenImageNameWithSha256(name string) string { + if strings.Contains(name, "@sha256:") { + // shorten sha256 to first 8 chars + return name[:len(name)-56] + } + return name +} + +func getFileBytes(fname string) ([]byte, error) { + f, err := os.Open(filepath.Clean(fname)) + if err != nil { + log.WithFields(log.Fields{ + "file": fname, + }).Fatal("open file failed") + } + defer hacks.CloseCheckErr(f, fname) + return io.ReadAll(f) +} + +func mkPathFromTag(tag string) string { + r := strings.NewReplacer( + "/", "-", + ":", "-", + "\\", "-", + ".", "-", + "@", "-", + ) + return r.Replace(tag) +} + +func (img *Info) getPolicyName(spec string) string { + var policyName string + + if img.Deployment == "" { + // policy recommendation for container images + policyName = fmt.Sprintf("%s-%s", mkPathFromTag(img.RepoTags[0]), spec) + } else { + // policy recommendation based on k8s manifest + policyName = fmt.Sprintf("%s-%s-%s", img.Deployment, mkPathFromTag(img.RepoTags[0]), spec) + } + return policyName +} + +// GetPolicyDir generates a policy directory path based on the image information +func (img *Info) GetPolicyDir(outDir string) string { + var policyDir string + + if img.Deployment == "" { + // policy recommendation for container images + if img.Namespace == "" { + policyDir = mkPathFromTag(img.RepoTags[0]) + } else { + policyDir = fmt.Sprintf("%s-%s", img.Namespace, mkPathFromTag(img.RepoTags[0])) + } + } else { + // policy recommendation based on k8s manifest + policyDir = fmt.Sprintf("%s-%s", img.Namespace, img.Deployment) + } + return filepath.Join(outDir, policyDir) +} + +func (img *Info) getPolicyFile(spec string, outDir string) string { + var policyFile string + + if img.Deployment != "" { + // policy recommendation based on k8s manifest + policyFile = fmt.Sprintf("%s-%s.yaml", mkPathFromTag(img.RepoTags[0]), spec) + } else { + policyFile = fmt.Sprintf("%s.yaml", spec) + } + + return filepath.Join(img.GetPolicyDir(outDir), policyFile) +} + +func addPolicyRule(policy *pol.KubeArmorPolicy, r pol.KubeArmorPolicySpec) { + + if len(r.File.MatchDirectories) != 0 || len(r.File.MatchPaths) != 0 { + policy.Spec.File = r.File + } + if len(r.Process.MatchDirectories) != 0 || len(r.Process.MatchPaths) != 0 { + policy.Spec.Process = r.Process + } + if len(r.Network.MatchProtocols) != 0 { + policy.Spec.Network = r.Network + } +} + +func (img *Info) createPolicy(ms common.MatchSpec) (pol.KubeArmorPolicy, error) { + policy := pol.KubeArmorPolicy{ + Spec: pol.KubeArmorPolicySpec{ + Severity: 1, // by default + Selector: pol.SelectorType{ + MatchLabels: map[string]string{}}, + }, + } + policy.APIVersion = "security.kubearmor.com/v1" + policy.Kind = "KubeArmorPolicy" + + policy.ObjectMeta.Name = img.getPolicyName(ms.Name) + + if img.Namespace != "" { + policy.ObjectMeta.Namespace = img.Namespace + } + + policy.Spec.Action = ms.Spec.Action + policy.Spec.Severity = ms.Spec.Severity + if ms.Spec.Message != "" { + policy.Spec.Message = ms.Spec.Message + } + if len(ms.Spec.Tags) > 0 { + policy.Spec.Tags = ms.Spec.Tags + } + + if len(img.Labels) > 0 { + policy.Spec.Selector.MatchLabels = img.Labels + } else { + repotag := strings.Split(img.RepoTags[0], ":") + policy.Spec.Selector.MatchLabels["kubearmor.io/container.name"] = repotag[0] + } + + addPolicyRule(&policy, ms.Spec) + return policy, nil +} + +// GetPolicy - creates policy and return back +func (img *Info) GetPolicy(ms common.MatchSpec, options common.Options) ([]byte, string) { + policy, err := img.createPolicy(ms) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "image": img, "spec": ms, + }).Error("create policy failed, skipping") + } + + arr, _ := json.Marshal(policy) + outFile := img.getPolicyFile(ms.Name, options.OutDir) + err = os.MkdirAll(filepath.Dir(outFile), 0750) + if err != nil { + log.WithError(err).Error("failed to create directory") + } + _, err = os.Create(filepath.Clean(outFile)) + if err != nil { + log.WithError(err).Error(fmt.Sprintf("create file %s failed", outFile)) + } + + return arr, outFile +} diff --git a/recommend/yaml/distro.yaml b/recommend/image/yaml/distro.yaml similarity index 100% rename from recommend/yaml/distro.yaml rename to recommend/image/yaml/distro.yaml diff --git a/recommend/imageHandler.go b/recommend/imageHandler.go deleted file mode 100644 index 8f285a56..00000000 --- a/recommend/imageHandler.go +++ /dev/null @@ -1,581 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor - -package recommend - -import ( - "archive/tar" - "bufio" - "context" - _ "embed" // need for embedding - "encoding/base64" - "fmt" - "io" - "math/rand" - "os" - "path/filepath" - "regexp" - "strings" - "time" - - "github.com/clarketm/json" - "sigs.k8s.io/yaml" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "github.com/docker/docker/pkg/jsonmessage" - "github.com/fatih/color" - kg "github.com/kubearmor/KubeArmor/KubeArmor/log" - "github.com/kubearmor/kubearmor-client/k8s" - "github.com/moby/term" - log "github.com/sirupsen/logrus" -) - -var cli *client.Client // docker client -var tempDir string // temporary directory used by karmor to save image etc -var dockerConfigPath string // stores path of docker config.json - -// ImageInfo contains image information -type ImageInfo struct { - Name string - RepoTags []string - Arch string - Distro string - OS string - FileList []string - DirList []string - Namespace string - Deployment string - Labels LabelMap -} - -// AuthConfigurations contains the configuration information's -type AuthConfigurations struct { - Configs map[string]types.AuthConfig `json:"configs"` -} - -// getConf reads the docker config.json file and returns a map -func getConf() map[string]types.AuthConfig { - var confs map[string]types.AuthConfig - if dockerConfigPath != "" { - data, err := os.ReadFile(filepath.Clean(dockerConfigPath)) - if err != nil { - return confs - } - confs, err = parseDockerConfig(data) - if err != nil { - return confs - } - } - return confs -} - -func getAuthStr(u, p string) string { - if u == "" || p == "" { - return "" - } - - encodedJSON, err := json.Marshal(types.AuthConfig{ - Username: u, - Password: p, - }) - if err != nil { - log.WithError(err).Fatal("failed to marshal credentials") - } - - return base64.URLEncoding.EncodeToString(encodedJSON) -} - -func init() { - var err error - - rand.Seed(time.Now().UnixNano()) // random seed init for random string generator - - cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - log.WithError(err).Fatal("could not create new docker client") - } -} - -// parseDockerConfig parses the docker config.json to generate a map -func parseDockerConfig(byteData []byte) (map[string]types.AuthConfig, error) { - - confsWrapper := struct { - Auths map[string]types.AuthConfig `json:"auths"` - }{} - if err := json.Unmarshal(byteData, &confsWrapper); err == nil { - if len(confsWrapper.Auths) > 0 { - return confsWrapper.Auths, nil - } - } - - var confs map[string]types.AuthConfig - if err := json.Unmarshal(byteData, &confs); err != nil { - return nil, err - } - return confs, nil -} - -func pullImage(imageName string) error { - var out io.ReadCloser - var err error - var confData []string - confData = append(confData, fmt.Sprintf("%s:%s", os.Getenv("DOCKER_USERNAME"), os.Getenv("DOCKER_PASSWORD"))) - if dockerConfigPath != "" { - confs := getConf() - if len(confs) > 0 { - for _, conf := range confs { - data, _ := base64.StdEncoding.DecodeString(conf.Auth) - confData = append(confData, string(data)) - } - } - } - for _, data := range confData { - userpass := strings.SplitN(string(data), ":", 2) - out, err = cli.ImagePull(context.Background(), imageName, types.ImagePullOptions{RegistryAuth: getAuthStr(userpass[0], userpass[1])}) - if err == nil { - break - } - } - if err != nil { - return err - } - defer func() { - if err := out.Close(); err != nil { - kg.Warnf("Error closing io stream %s\n", err) - } - }() - termFd, isTerm := term.GetFdInfo(os.Stderr) - err = jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil) - if err != nil { - log.WithError(err).Error("could not display json") - } - - return nil -} - -// The randomizer used in this function is not used for any cryptographic -// operation and hence safe to use. -func randString(n int) string { - var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] // #nosec - } - return string(b) -} - -func closeCheckErr(f *os.File, fname string) { - err := f.Close() - if err != nil { - log.WithFields(log.Fields{ - "file": fname, - }).Error("close file failed") - } -} - -// Sanitize archive file pathing from "G305: Zip Slip vulnerability" -func sanitizeArchivePath(d, t string) (v string, err error) { - v = filepath.Join(d, t) - if strings.HasPrefix(v, filepath.Clean(d)) { - return v, nil - } - - return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) -} - -func extractTar(tarname string) ([]string, []string) { - var fl []string - var dl []string - - f, err := os.Open(filepath.Clean(tarname)) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "tar": tarname, - }).Fatal("os create failed") - } - defer closeCheckErr(f, tarname) - - tr := tar.NewReader(bufio.NewReader(f)) - for { - hdr, err := tr.Next() - if err == io.EOF { - break // End of archive - } - if err != nil { - log.WithError(err).Fatal("tar next failed") - } - - tgt, err := sanitizeArchivePath(tempDir, hdr.Name) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "file": hdr.Name, - }).Error("ignoring file since it could not be sanitized") - continue - } - - switch hdr.Typeflag { - case tar.TypeDir: - if _, err := os.Stat(tgt); err != nil { - if err := os.MkdirAll(tgt, 0750); err != nil { - log.WithError(err).WithFields(log.Fields{ - "target": tgt, - }).Fatal("tar mkdirall") - } - } - dl = append(dl, tgt) - case tar.TypeReg: - f, err := os.OpenFile(filepath.Clean(tgt), os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode)) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "target": tgt, - }).Error("tar open file") - } else { - - // copy over contents - if _, err := io.CopyN(f, tr, 2e+9 /*2GB*/); err != io.EOF { - log.WithError(err).WithFields(log.Fields{ - "target": tgt, - }).Fatal("tar io.Copy()") - } - } - closeCheckErr(f, tgt) - if strings.HasSuffix(tgt, "layer.tar") { // deflate container image layer - ifl, idl := extractTar(tgt) - fl = append(fl, ifl...) - dl = append(dl, idl...) - } else { - fl = append(fl, tgt) - } - } - } - return fl, dl -} - -func saveImageToTar(imageName string) string { - imgdata, err := cli.ImageSave(context.Background(), []string{imageName}) - if err != nil { - log.WithError(err).Fatal("could not save image") - } - defer func() { - if err := imgdata.Close(); err != nil { - kg.Warnf("Error closing io stream %s\n", err) - } - }() - - tarname := filepath.Join(tempDir, randString(8)+".tar") - - f, err := os.Create(filepath.Clean(tarname)) - if err != nil { - log.WithError(err).Fatal("os create failed") - } - - if _, err := io.CopyN(bufio.NewWriter(f), imgdata, 5e+9 /*5GB*/); err != io.EOF { - log.WithError(err).WithFields(log.Fields{ - "tar": tarname, - }).Fatal("io.CopyN() failed") - } - closeCheckErr(f, tarname) - log.WithFields(log.Fields{ - "tar": tarname, - }).Info("dumped image to tar") - return tarname -} - -func checkForSpec(spec string, fl []string) []string { - var matches []string - if !strings.HasSuffix(spec, "*") { - spec = fmt.Sprintf("%s$", spec) - } - - re := regexp.MustCompile(spec) - for _, name := range fl { - if re.Match([]byte(name)) { - matches = append(matches, name) - } - } - return matches -} - -func getFileBytes(fname string) ([]byte, error) { - f, err := os.Open(filepath.Clean(fname)) - if err != nil { - log.WithFields(log.Fields{ - "file": fname, - }).Fatal("open file failed") - } - defer closeCheckErr(f, fname) - return io.ReadAll(f) -} - -func (img *ImageInfo) readManifest(manifest string) { - // read manifest file - barr, err := getFileBytes(manifest) - if err != nil { - log.WithError(err).Fatal("manifest read failed") - } - var manres []map[string]interface{} - err = json.Unmarshal(barr, &manres) - if err != nil { - log.WithError(err).Fatal("manifest json unmarshal failed") - } - if len(manres) < 1 { - log.WithFields(log.Fields{ - "len": len(manres), - "results": manres, - }).Fatal("expecting atleast one config in manifest!") - } - - var man map[string]interface{} - for _, man = range manres { - if man["RepoTags"] != nil { - break - } - } - - // read config file - config := filepath.Join(tempDir, man["Config"].(string)) - barr, err = getFileBytes(config) - if err != nil { - log.WithFields(log.Fields{ - "config": config, - }).Fatal("config read failed") - } - var cfgres map[string]interface{} - err = json.Unmarshal(barr, &cfgres) - if err != nil { - log.WithError(err).Fatal("config json unmarshal failed") - } - img.Arch = cfgres["architecture"].(string) - img.OS = cfgres["os"].(string) - - if man["RepoTags"] == nil { - // If the image name contains sha256 digest, - // then manifest["RepoTags"] will be `nil`. - img.RepoTags = append(img.RepoTags, shortenImageNameWithSha256(img.Name)) - } else { - for _, tag := range man["RepoTags"].([]interface{}) { - img.RepoTags = append(img.RepoTags, tag.(string)) - } - } -} - -func (img *ImageInfo) getPolicyDir() string { - var policyDir string - - if img.Deployment == "" { - // policy recommendation for container images - if img.Namespace == "" { - policyDir = mkPathFromTag(img.RepoTags[0]) - } else { - policyDir = fmt.Sprintf("%s-%s", img.Namespace, mkPathFromTag(img.RepoTags[0])) - } - } else { - // policy recommendation based on k8s manifest - policyDir = fmt.Sprintf("%s-%s", img.Namespace, img.Deployment) - } - return filepath.Join(options.OutDir, policyDir) -} - -func (img *ImageInfo) getPolicyFile(spec string) string { - var policyFile string - - if img.Deployment != "" { - // policy recommendation based on k8s manifest - policyFile = fmt.Sprintf("%s-%s.yaml", mkPathFromTag(img.RepoTags[0]), spec) - } else { - policyFile = fmt.Sprintf("%s.yaml", spec) - } - - return filepath.Join(img.getPolicyDir(), policyFile) -} - -func (img *ImageInfo) getPolicyName(spec string) string { - var policyName string - - if img.Deployment == "" { - // policy recommendation for container images - policyName = fmt.Sprintf("%s-%s", mkPathFromTag(img.RepoTags[0]), spec) - } else { - // policy recommendation based on k8s manifest - policyName = fmt.Sprintf("%s-%s-%s", img.Deployment, mkPathFromTag(img.RepoTags[0]), spec) - } - return policyName -} - -type distroRule struct { - Name string `json:"name" yaml:"name"` - Match []struct { - Path string `json:"path" yaml:"path"` - } `json:"match" yaml:"match"` -} - -//go:embed yaml/distro.yaml -var distroYAML []byte - -var distroRules []distroRule - -func init() { - distroJSON, err := yaml.YAMLToJSON(distroYAML) - if err != nil { - color.Red("failed to convert distro rules yaml to json") - log.WithError(err).Fatal("failed to convert distro rules yaml to json") - } - - var jsonRaw map[string]json.RawMessage - err = json.Unmarshal(distroJSON, &jsonRaw) - if err != nil { - color.Red("failed to unmarshal distro rules json") - log.WithError(err).Fatal("failed to unmarshal distro rules json") - } - - err = json.Unmarshal(jsonRaw["distroRules"], &distroRules) - if err != nil { - color.Red("failed to unmarshal distro rules") - log.WithError(err).Fatal("failed to unmarshal distro rules") - } -} - -func (img *ImageInfo) getDistro() { - for _, d := range distroRules { - match := true - for _, m := range d.Match { - matches := checkForSpec(filepath.Clean(tempDir+m.Path), img.FileList) - if len(matches) == 0 { - match = false - break - } - } - if len(d.Match) > 0 && match { - color.Green("Distribution %s", d.Name) - img.Distro = d.Name - return - } - } -} - -func (img *ImageInfo) getImageInfo() { - matches := checkForSpec(filepath.Join(tempDir, "manifest.json"), img.FileList) - if len(matches) != 1 { - log.WithFields(log.Fields{ - "len": len(matches), - "matches": matches, - }).Fatal("expecting one manifest.json!") - } - img.readManifest(matches[0]) - - img.getDistro() -} - -func getImageDetails(img ImageInfo) error { - - // step 1: save the image to a tar file - tarname := saveImageToTar(img.Name) - - // step 2: retrieve information from tar - img.FileList, img.DirList = extractTar(tarname) - - // step 3: getImageInfo - img.getImageInfo() - - if len(img.RepoTags) == 0 { - img.RepoTags = append(img.RepoTags, img.Name) - } - // step 4: get policy from image info - img.getPolicyFromImageInfo() - - return nil -} - -func imageHandler(namespace, deployment string, labels LabelMap, imageName string, c *k8s.Client) error { - dockerConfigPath = options.Config - img := ImageInfo{ - Name: imageName, - Namespace: namespace, - Deployment: deployment, - Labels: labels, - } - - if len(options.Policy) == 0 { - return fmt.Errorf("no policy specified, specify at least one policy to be recommended") - } - - policiesToBeRecommendedSet := make(map[string]bool) - for _, policy := range options.Policy { - policiesToBeRecommendedSet[policy] = true - } - - _, containsKubeArmorPolicy := policiesToBeRecommendedSet[KubeArmorPolicy] - if containsKubeArmorPolicy { - err := recommendKubeArmorPolicies(imageName, img) - if err != nil { - log.WithError(err).Error("failed to recommend kubearmor policies.") - return err - } - } - - _, containsKyvernoPolicy := policiesToBeRecommendedSet[KyvernoPolicy] - - // Admission Controller Policies are not recommended based on an image - if len(options.Images) == 0 && containsKyvernoPolicy { - if len(img.RepoTags) == 0 { - img.RepoTags = append(img.RepoTags, img.Name) - } - if !containsKubeArmorPolicy { - if err := ReportStart(&img); err != nil { - log.WithError(err).Error("report start failed") - return err - } - } - err := initClientConnection(c) - if err != nil { - log.WithError(err).Info("failed to initialize DE client connection. Won't recommend admission controller policies.") - //return err - } else { - err = recommendAdmissionControllerPolicies(img) - if err != nil { - log.WithError(err).Error("failed to recommend admission controller policies.") - return err - } - } - } - - if !containsKyvernoPolicy && !containsKubeArmorPolicy { - return fmt.Errorf("policy type not supported: %v", options.Policy) - } - err := ReportSectEnd() - if err != nil { - log.WithError(err).Error("report section end failed") - return err - } - - return nil -} - -func recommendKubeArmorPolicies(imageName string, img ImageInfo) error { - log.WithFields(log.Fields{ - "image": imageName, - }).Info("pulling image") - err := pullImage(imageName) - if err != nil { - log.Warn("Failed to pull image. Dumping generic policies.") - img.OS = "linux" - img.RepoTags = append(img.RepoTags, img.Name) - img.getPolicyFromImageInfo() - } else { - err = getImageDetails(img) - if err != nil { - return err - } - } - return nil -} - -// shortenImageNameWithSha256 truncates the sha256 digest in image name -func shortenImageNameWithSha256(name string) string { - if strings.Contains(name, "@sha256:") { - // shorten sha256 to first 8 chars - return name[:len(name)-56] - } - return name -} diff --git a/recommend/policy.go b/recommend/policy.go deleted file mode 100644 index 84f0e09b..00000000 --- a/recommend/policy.go +++ /dev/null @@ -1,266 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor - -package recommend - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/clarketm/json" - "github.com/fatih/color" - pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" - kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" - log "github.com/sirupsen/logrus" - "k8s.io/utils/strings/slices" - "sigs.k8s.io/yaml" -) - -func addPolicyRule(policy *pol.KubeArmorPolicy, r pol.KubeArmorPolicySpec) { - - if len(r.File.MatchDirectories) != 0 || len(r.File.MatchPaths) != 0 { - policy.Spec.File = r.File - } - if len(r.Process.MatchDirectories) != 0 || len(r.Process.MatchPaths) != 0 { - policy.Spec.Process = r.Process - } - if len(r.Network.MatchProtocols) != 0 { - policy.Spec.Network = r.Network - } -} - -func mkPathFromTag(tag string) string { - r := strings.NewReplacer( - "/", "-", - ":", "-", - "\\", "-", - ".", "-", - "@", "-", - ) - return r.Replace(tag) -} - -func (img *ImageInfo) createPolicy(ms MatchSpec) (pol.KubeArmorPolicy, error) { - policy := pol.KubeArmorPolicy{ - Spec: pol.KubeArmorPolicySpec{ - Severity: 1, // by default - Selector: pol.SelectorType{ - MatchLabels: map[string]string{}}, - }, - } - policy.APIVersion = "security.kubearmor.com/v1" - policy.Kind = "KubeArmorPolicy" - - policy.ObjectMeta.Name = img.getPolicyName(ms.Name) - - if img.Namespace != "" { - policy.ObjectMeta.Namespace = img.Namespace - } - - policy.Spec.Action = ms.Spec.Action - policy.Spec.Severity = ms.Spec.Severity - if ms.Spec.Message != "" { - policy.Spec.Message = ms.Spec.Message - } - if len(ms.Spec.Tags) > 0 { - policy.Spec.Tags = ms.Spec.Tags - } - - if len(img.Labels) > 0 { - policy.Spec.Selector.MatchLabels = img.Labels - } else { - repotag := strings.Split(img.RepoTags[0], ":") - policy.Spec.Selector.MatchLabels["kubearmor.io/container.name"] = repotag[0] - } - - addPolicyRule(&policy, ms.Spec) - return policy, nil -} - -func (img *ImageInfo) checkPreconditions(ms MatchSpec) bool { - var matches []string - for _, preCondition := range ms.Precondition { - matches = append(matches, checkForSpec(filepath.Join(preCondition), img.FileList)...) - if strings.Contains(preCondition, "OPTSCAN") { - return true - } - } - return len(matches) >= len(ms.Precondition) -} - -func matchTags(ms MatchSpec) bool { - if len(options.Tags) <= 0 { - return true - } - for _, t := range options.Tags { - if slices.Contains(ms.Spec.Tags, t) { - return true - } - } - return false -} - -func (img *ImageInfo) writePolicyFile(ms MatchSpec) { - policy, err := img.createPolicy(ms) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "image": img, "spec": ms, - }).Error("create policy failed, skipping") - - } - - outFile := img.getPolicyFile(ms.Name) - _ = os.MkdirAll(filepath.Dir(outFile), 0750) - - f, err := os.Create(filepath.Clean(outFile)) - if err != nil { - log.WithError(err).Error(fmt.Sprintf("create file %s failed", outFile)) - - } - - arr, _ := json.Marshal(policy) - yamlArr, _ := yaml.JSONToYAML(arr) - if _, err := f.WriteString(string(yamlArr)); err != nil { - log.WithError(err).Error("WriteString failed") - } - if err := f.Sync(); err != nil { - log.WithError(err).Error("file sync failed") - } - if err := f.Close(); err != nil { - log.WithError(err).Error("file close failed") - } - _ = ReportRecord(ms, outFile) - color.Green("created policy %s ...", outFile) - -} - -func (img *ImageInfo) writeAdmissionControllerPolicy(policyInterface kyvernov1.PolicyInterface) { - policyName := strings.ReplaceAll(policyInterface.GetName(), img.Name+"-", "") - outFile := img.getPolicyFile(policyName) - err := os.MkdirAll(filepath.Dir(outFile), 0750) - if err != nil { - log.WithError(err).Error("create dir failed") - return - } - - f, err := os.Create(filepath.Clean(outFile)) - if err != nil { - log.WithError(err).Error(fmt.Sprintf("create file %s failed", outFile)) - return - } - var jsonBytes []byte - var yamlBytes []byte - jsonBytes, err = convertKyvernoPolicyInterfaceToJSON(policyInterface) - if err != nil { - log.WithError(err).Error("json marshal failed") - return - } - yamlBytes, err = yaml.JSONToYAML(jsonBytes) - if err != nil { - log.WithError(err).Error("yaml marshal failed") - return - } - if _, err := f.WriteString(string(yamlBytes)); err != nil { - log.WithError(err).Error("WriteString failed") - return - } - if err := f.Sync(); err != nil { - log.WithError(err).Error("file sync failed") - return - } - if err := f.Close(); err != nil { - log.WithError(err).Error("file close failed") - return - } - err = ReportAdmissionControllerRecord(outFile, string(policyInterface.GetSpec().ValidationFailureAction), policyInterface.GetAnnotations()) - if err != nil { - log.WithError(err).Error("report admission controller record failed") - } else { - color.Green("created policy %s ...", outFile) - } -} - -func writeGenericAdmissionControllerPolicy(policyInterface kyvernov1.PolicyInterface) { - policyName := policyInterface.GetName() - outFile := filepath.Join(options.OutDir, "genericKyvernoPolicies", policyName+".yaml") - err := os.MkdirAll(filepath.Dir(outFile), 0750) - if err != nil { - log.WithError(err).Error("create dir failed") - return - } - - f, err := os.Create(filepath.Clean(outFile)) - if err != nil { - log.WithError(err).Error(fmt.Sprintf("create file %s failed", outFile)) - return - } - var jsonBytes []byte - var yamlBytes []byte - jsonBytes, err = convertKyvernoPolicyInterfaceToJSON(policyInterface) - if err != nil { - log.WithError(err).Error("json marshal failed") - return - } - yamlBytes, err = yaml.JSONToYAML(jsonBytes) - if err != nil { - log.WithError(err).Error("yaml marshal failed") - return - } - if _, err := f.WriteString(string(yamlBytes)); err != nil { - log.WithError(err).Error("WriteString failed") - return - } - if err := f.Sync(); err != nil { - log.WithError(err).Error("file sync failed") - return - } - if err := f.Close(); err != nil { - log.WithError(err).Error("file close failed") - return - } - err = ReportAdmissionControllerRecord(outFile, string(policyInterface.GetSpec().ValidationFailureAction), policyInterface.GetAnnotations()) - if err != nil { - log.WithError(err).Error("report admission controller record failed") - } else { - color.Green("created policy %s ...", outFile) - } -} - -func (img *ImageInfo) getPolicyFromImageInfo() { - if img.OS != "linux" { - color.Red("non-linux platforms are not supported, yet.") - return - } - idx := 0 - if err := ReportStart(img); err != nil { - log.WithError(err).Error("report start failed") - return - } - var ms MatchSpec - var err error - - err = createRuntimePolicy(img) - if err != nil { - log.Infof("No runtime policy generated for %s/%s/%s", img.Namespace, img.Deployment, img.Name) - } - - ms, err = getNextRule(&idx) - for ; err == nil; ms, err = getNextRule(&idx) { - - // Kyverno policies are fetched from Discovery-Engine - if ms.KyvernoPolicySpec != nil { - continue - } - - if !matchTags(ms) { - continue - } - - if !img.checkPreconditions(ms) { - continue - } - img.writePolicyFile(ms) - } -} diff --git a/recommend/policyRules.go b/recommend/policyRules.go deleted file mode 100644 index 8d47ea2d..00000000 --- a/recommend/policyRules.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor - -package recommend - -import ( - _ "embed" // need for embedding - "errors" - "github.com/clarketm/json" - "sigs.k8s.io/yaml" - - "github.com/fatih/color" - pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" - kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" - log "github.com/sirupsen/logrus" -) - -// MatchSpec spec to match for defining policy -type MatchSpec struct { - Name string `json:"name" yaml:"name"` - Precondition []string `json:"precondition" yaml:"precondition"` - Description Description `json:"description" yaml:"description"` - Yaml string `json:"yaml" yaml:"yaml"` - Spec pol.KubeArmorPolicySpec `json:"spec,omitempty" yaml:"spec,omitempty"` - KyvernoPolicySpec *kyvernov1.Spec `json:"kyvernoPolicySpec,omitempty" yaml:"kyvernoPolicySpec,omitempty"` - KyvernoPolicyTags []string `json:"kyvernoPolicyTags,omitempty" yaml:"kyvernoPolicyTags,omitempty"` -} - -// Ref for the policy rules -type Ref struct { - Name string `json:"name" yaml:"name"` - URL []string `json:"url" yaml:"url"` -} - -// Description detailed description for the policy rule -type Description struct { - Refs []Ref `json:"refs" yaml:"refs"` - Tldr string `json:"tldr" yaml:"tldr"` - Detailed string `json:"detailed" yaml:"detailed"` -} - -var policyRules []MatchSpec - -//go:embed yaml/rules.yaml -var policyRulesYAML []byte - -func updateRulesYAML(yamlFile []byte) string { - policyRules = []MatchSpec{} - if len(yamlFile) < 30 { - yamlFile = policyRulesYAML - } - policyRulesJSON, err := yaml.YAMLToJSON(yamlFile) - if err != nil { - color.Red("failed to convert policy rules yaml to json") - log.WithError(err).Fatal("failed to convert policy rules yaml to json") - } - var jsonRaw map[string]json.RawMessage - err = json.Unmarshal(policyRulesJSON, &jsonRaw) - if err != nil { - color.Red("failed to unmarshal policy rules json") - log.WithError(err).Fatal("failed to unmarshal policy rules json") - } - err = json.Unmarshal(jsonRaw["policyRules"], &policyRules) - if err != nil { - color.Red("failed to unmarshal policy rules") - log.WithError(err).Fatal("failed to unmarshal policy rules") - } - return string(jsonRaw["version"]) -} - -func getNextRule(idx *int) (MatchSpec, error) { - if *idx < 0 { - (*idx)++ - } - if *idx >= len(policyRules) { - return MatchSpec{}, errors.New("no rule at idx") - } - r := policyRules[*idx] - (*idx)++ - return r, nil -} diff --git a/recommend/recommend.go b/recommend/recommend.go index 0a9eb14f..e7a6b05b 100644 --- a/recommend/recommend.go +++ b/recommend/recommend.go @@ -1,6 +1,4 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor - +// Package recommend provides policies by policy generators package recommend import ( @@ -13,34 +11,18 @@ import ( "github.com/fatih/color" "github.com/kubearmor/kubearmor-client/k8s" + "github.com/kubearmor/kubearmor-client/recommend/common" + "github.com/kubearmor/kubearmor-client/recommend/engines" + "github.com/kubearmor/kubearmor-client/recommend/image" + "github.com/kubearmor/kubearmor-client/recommend/registry" + "github.com/kubearmor/kubearmor-client/recommend/report" + "sigs.k8s.io/yaml" + log "github.com/sirupsen/logrus" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// DefaultPoliciesToBeRecommended are the default policies to be recommended -var DefaultPoliciesToBeRecommended = []string{ /*KyvernoPolicy, */ KubeArmorPolicy} - -// KyvernoPolicy is alias for kyverno policy. The actual kind of Kyverno policy is 'Policy' but we use 'KyvernoPolicy' -// to explicitly differentiate it from other policy types. -var KyvernoPolicy = "KyvernoPolicy" - -// KubeArmorPolicy is alias for kubearmor policy -var KubeArmorPolicy = "KubeArmorPolicy" - -// Options for karmor recommend -type Options struct { - Images []string - Labels []string - Tags []string - Policy []string - Namespace string - OutDir string - ReportFile string - Config string -} - -// LabelMap is an alias for map[string]string -type LabelMap = map[string]string +var options common.Options // Deployment contains brief information about a k8s deployment type Deployment struct { @@ -50,7 +32,35 @@ type Deployment struct { Images []string } -var options Options +// LabelMap is an alias for map[string]string +type LabelMap = map[string]string + +func labelSplitter(r rune) bool { + return r == ':' || r == '=' +} + +func labelArrayToLabelMap(labels []string) LabelMap { + labelMap := LabelMap{} + for _, label := range labels { + kvPair := strings.FieldsFunc(label, labelSplitter) + if len(kvPair) != 2 { + continue + } + labelMap[kvPair[0]] = kvPair[1] + } + return labelMap +} + +func matchLabels(filter, selector LabelMap) bool { + match := true + for k, v := range filter { + if selector[k] != v { + match = false + break + } + } + return match +} func unique(s []string) []string { inResult := make(map[string]bool) @@ -83,7 +93,9 @@ func createOutDir(dir string) error { func finalReport() { repFile := filepath.Clean(filepath.Join(options.OutDir, options.ReportFile)) - _ = ReportRender(repFile) + if err := report.Render(repFile); err != nil { + log.WithError(err).Error("report render failed") + } color.Green("output report in %s ...", repFile) if strings.Contains(repFile, ".html") { return @@ -96,33 +108,39 @@ func finalReport() { fmt.Println(string(data)) } -// Recommend handler for karmor cli tool -func Recommend(c *k8s.Client, o Options) error { - deployments := []Deployment{} - var err error - if !isLatest() { - log.WithFields(log.Fields{ - "Current Version": CurrentVersion, - }).Info("Found outdated version of policy-templates") - log.Info("Downloading latest version [", LatestVersion, "]") - if _, err := DownloadAndUnzipRelease(); err != nil { - return err +func writePolicyFile(policMap map[string][]byte, msMap map[string]interface{}) { + for outFile, policy := range policMap { + f, err := os.OpenFile(filepath.Clean(outFile), os.O_RDWR, 0) + if err != nil { + log.WithError(err).Error(fmt.Sprintf("create file %s failed", outFile)) } - log.WithFields(log.Fields{ - "Updated Version": LatestVersion, - }).Info("policy-templates updated") - } - if err = createOutDir(o.OutDir); err != nil { - return err - } + yamlPolicy, _ := yaml.JSONToYAML(policy) + if _, err = f.WriteString(string(yamlPolicy)); err != nil { + log.WithError(err).Error("WriteString failed") + } + if err = f.Sync(); err != nil { + log.WithError(err).Error("file sync failed") + } + if err = f.Close(); err != nil { + log.WithError(err).Error("file close failed") + } + if err = report.Record(msMap[outFile], outFile); err != nil { + log.WithError(err).Error("report record failed") + } - if o.ReportFile != "" { - ReportInit(o.ReportFile) + color.Green("created policy %s ...", outFile) } +} - labelMap := labelArrayToLabelMap(o.Labels) +// Recommend handler for karmor cli tool +func Recommend(c *k8s.Client, o common.Options, policyGenerators ...engines.Engine) error { + var policyMap map[string][]byte + var msMap map[string]interface{} + var err error + deployments := []Deployment{} + labelMap := labelArrayToLabelMap(o.Labels) if len(o.Images) == 0 { // recommendation based on k8s manifest dps, err := c.K8sClientset.AppsV1().Deployments(o.Namespace).List(context.TODO(), v1.ListOptions{}) @@ -130,26 +148,20 @@ func Recommend(c *k8s.Client, o Options) error { return err } for _, dp := range dps.Items { - if !matchLabels(labelMap, dp.Spec.Template.Labels) { - continue - } - images := []string{} - for _, container := range dp.Spec.Template.Spec.Containers { - images = append(images, container.Image) - } - deployments = append(deployments, Deployment{ - Name: dp.Name, - Namespace: dp.Namespace, - Labels: dp.Spec.Template.Labels, - Images: images, - }) - } - if len(deployments) == 0 { - log.WithFields(log.Fields{ - "namespace": o.Namespace, - }).Error("no k8s deployments found, hence nothing to recommend!") - return nil + if matchLabels(labelMap, dp.Spec.Template.Labels) { + images := []string{} + for _, container := range dp.Spec.Template.Spec.Containers { + images = append(images, container.Image) + } + + deployments = append(deployments, Deployment{ + Name: dp.Name, + Namespace: dp.Namespace, + Labels: dp.Spec.Template.Labels, + Images: images, + }) + } } } else { deployments = append(deployments, Deployment{ @@ -159,79 +171,43 @@ func Recommend(c *k8s.Client, o Options) error { }) } - // o.Images = unique(o.Images) o.Tags = unique(o.Tags) options = o + reg := registry.New(o.Config) - defer closeConnectionToDiscoveryEngine() - for _, dp := range deployments { - err := handleDeployment(dp, c) - if err != nil { - log.Error(err) - } - } - - recommendKyvernoPolicies := false - for _, policy := range options.Policy { - if policy == KyvernoPolicy { - recommendKyvernoPolicies = true - } + if err = createOutDir(o.OutDir); err != nil { + return err } - if recommendKyvernoPolicies { - err = recommendGenericAdmissionControllerPolicies() - if err != nil { - log.Error(err) + for _, gen := range policyGenerators { + if o.ReportFile != "" { + report.Init(o.ReportFile) } - } - - finalReport() - return nil -} - -func handleDeployment(dp Deployment, c *k8s.Client) error { - - var err error - for _, img := range dp.Images { - tempDir, err = os.MkdirTemp("", "karmor") - if err != nil { - log.WithError(err).Error("could not create temp dir") + if err := gen.Init(); err != nil { + log.WithError(err).Error("policy generator init failed") } - err = imageHandler(dp.Namespace, dp.Name, dp.Labels, img, c) - if err != nil { - log.WithError(err).WithFields(log.Fields{ - "image": img, - }).Error("could not handle container image") + for _, deployment := range deployments { + for _, i := range deployment.Images { + img := image.Info{ + Name: i, + Namespace: deployment.Namespace, + Labels: deployment.Labels, + Image: i, + Deployment: deployment.Name, + } + reg.Analyze(&img) + if policyMap, msMap, err = gen.Scan(&img, o); err != nil { + log.WithError(err).Error("policy generator scan failed") + } + writePolicyFile(policyMap, msMap) + if err := report.SectEnd(); err != nil { + log.WithError(err).Error("report section end failed") + return err + } + } } - _ = os.RemoveAll(tempDir) + finalReport() } return nil } - -func matchLabels(filter, selector LabelMap) bool { - match := true - for k, v := range filter { - if selector[k] != v { - match = false - break - } - } - return match -} - -func labelArrayToLabelMap(labels []string) LabelMap { - labelMap := LabelMap{} - for _, label := range labels { - kvPair := strings.FieldsFunc(label, labelSplitter) - if len(kvPair) != 2 { - continue - } - labelMap[kvPair[0]] = kvPair[1] - } - return labelMap -} - -func labelSplitter(r rune) bool { - return r == ':' || r == '=' -} diff --git a/recommend/registry/registry.go b/recommend/registry/registry.go new file mode 100644 index 00000000..0dbdd5df --- /dev/null +++ b/recommend/registry/registry.go @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Authors of KubeArmor + +// Package registry contains scanner for image info +package registry + +import ( + "archive/tar" + "bufio" + "context" + _ "embed" // need for embedding + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math/rand" + "os" + "path/filepath" + "strings" + + "github.com/docker/docker/client" + "github.com/docker/docker/pkg/jsonmessage" + image "github.com/kubearmor/kubearmor-client/recommend/image" + "github.com/moby/term" + + dockerTypes "github.com/docker/docker/api/types" + kg "github.com/kubearmor/KubeArmor/KubeArmor/log" + "github.com/kubearmor/kubearmor-client/hacks" + log "github.com/sirupsen/logrus" +) + +const karmorTempDirPattern = "karmor" + +// Scanner represents a utility for scanning Docker registries +type Scanner struct { + authConfiguration authConfigurations + cli *client.Client // docker client + cache map[string]image.Info +} + +// authConfigurations contains the configuration information's +type authConfigurations struct { + configPath string // stores path of docker config.json + authCreds []string +} + +func getAuthStr(u, p string) string { + if u == "" || p == "" { + return "" + } + + encodedJSON, err := json.Marshal(dockerTypes.AuthConfig{ + Username: u, + Password: p, + }) + if err != nil { + log.WithError(err).Fatal("failed to marshal credentials") + } + + return base64.URLEncoding.EncodeToString(encodedJSON) +} + +func (r *Scanner) loadDockerAuthConfigs() { + r.authConfiguration.authCreds = append(r.authConfiguration.authCreds, fmt.Sprintf("%s:%s", os.Getenv("DOCKER_USERNAME"), os.Getenv("DOCKER_PASSWORD"))) + if r.authConfiguration.configPath != "" { + data, err := os.ReadFile(filepath.Clean(r.authConfiguration.configPath)) + if err != nil { + return + } + + confsWrapper := struct { + Auths map[string]dockerTypes.AuthConfig `json:"auths"` + }{} + err = json.Unmarshal(data, &confsWrapper) + if err != nil { + return + } + + for _, conf := range confsWrapper.Auths { + data, _ := base64.StdEncoding.DecodeString(conf.Auth) + userPass := strings.SplitN(string(data), ":", 2) + r.authConfiguration.authCreds = append(r.authConfiguration.authCreds, getAuthStr(userPass[0], userPass[1])) + } + } +} + +// New creates and initializes a new instance of the Scanner +func New(dockerConfigPath string) *Scanner { + var err error + scanner := Scanner{ + authConfiguration: authConfigurations{ + configPath: dockerConfigPath, + }, + cache: make(map[string]image.Info), + } + + if err != nil { + log.WithError(err).Error("could not create temp dir") + } + + scanner.cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + log.WithError(err).Fatal("could not create new docker client") + } + scanner.loadDockerAuthConfigs() + + return &scanner +} + +// Analyze performs analysis and caching of image information using the Scanner +func (r *Scanner) Analyze(img *image.Info) { + if val, ok := r.cache[img.Name]; ok { + log.WithFields(log.Fields{ + "image": img.Name, + }).Infof("Image already scanned in this session, using cached informations for image") + img.Arch = val.Arch + img.DirList = val.DirList + img.FileList = val.FileList + img.Distro = val.Distro + img.Labels = val.Labels + img.OS = val.OS + img.RepoTags = val.RepoTags + return + } + tmpDir, err := os.MkdirTemp("", karmorTempDirPattern) + if err != nil { + log.WithError(err).Error("could not create temp dir") + } + defer func() { + err = os.RemoveAll(tmpDir) + if err != nil { + log.WithError(err).Error("failed to remove cache files") + } + }() + img.TempDir = tmpDir + err = r.pullImage(img.Name) + if err != nil { + log.Warn("Failed to pull image. Dumping generic policies.") + img.OS = "linux" + img.RepoTags = append(img.RepoTags, img.Name) + } else { + tarname := saveImageToTar(img.Name, r.cli, tmpDir) + img.FileList, img.DirList = extractTar(tarname, tmpDir) + img.GetImageInfo() + } + + r.cache[img.Name] = *img +} + +// The randomizer used in this function is not used for any cryptographic +// operation and hence safe to use. +func randString(n int) string { + var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] // #nosec + } + return string(b) +} + +func (r *Scanner) pullImage(imageName string) (err error) { + log.WithFields(log.Fields{ + "image": imageName, + }).Info("pulling image") + + var out io.ReadCloser + + for _, cred := range r.authConfiguration.authCreds { + out, err = r.cli.ImagePull(context.Background(), imageName, + dockerTypes.ImagePullOptions{ + RegistryAuth: cred, + }) + if err == nil { + break + } + } + if err != nil { + return err + } + defer func() { + if err := out.Close(); err != nil { + kg.Warnf("Error closing io stream %s\n", err) + } + }() + termFd, isTerm := term.GetFdInfo(os.Stderr) + err = jsonmessage.DisplayJSONMessagesStream(out, os.Stderr, termFd, isTerm, nil) + if err != nil { + log.WithError(err).Error("could not display json") + } + + return +} + +// Sanitize archive file pathing from "G305: Zip Slip vulnerability" +func sanitizeArchivePath(d, t string) (v string, err error) { + v = filepath.Join(d, t) + if strings.HasPrefix(v, filepath.Clean(d)) { + return v, nil + } + + return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) +} + +func extractTar(tarname string, tempDir string) ([]string, []string) { + var fl []string + var dl []string + + f, err := os.Open(filepath.Clean(tarname)) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "tar": tarname, + }).Fatal("os create failed") + } + defer hacks.CloseCheckErr(f, tarname) + + tr := tar.NewReader(bufio.NewReader(f)) + for { + hdr, err := tr.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + log.WithError(err).Fatal("tar next failed") + } + + tgt, err := sanitizeArchivePath(tempDir, hdr.Name) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "file": hdr.Name, + }).Error("ignoring file since it could not be sanitized") + continue + } + + switch hdr.Typeflag { + case tar.TypeDir: + if _, err := os.Stat(tgt); err != nil { + if err := os.MkdirAll(tgt, 0750); err != nil { + log.WithError(err).WithFields(log.Fields{ + "target": tgt, + }).Fatal("tar mkdirall") + } + } + dl = append(dl, tgt) + case tar.TypeReg: + f, err := os.OpenFile(filepath.Clean(tgt), os.O_CREATE|os.O_RDWR, os.FileMode(hdr.Mode)) + if err != nil { + log.WithError(err).WithFields(log.Fields{ + "target": tgt, + }).Error("tar open file") + } else { + + // copy over contents + if _, err := io.CopyN(f, tr, 2e+9 /*2GB*/); err != io.EOF { + log.WithError(err).WithFields(log.Fields{ + "target": tgt, + }).Fatal("tar io.Copy()") + } + } + hacks.CloseCheckErr(f, tgt) + if strings.HasSuffix(tgt, "layer.tar") { // deflate container image layer + ifl, idl := extractTar(tgt, tempDir) + fl = append(fl, ifl...) + dl = append(dl, idl...) + } else { + fl = append(fl, tgt) + } + } + } + return fl, dl +} + +func saveImageToTar(imageName string, cli *client.Client, tempDir string) string { + imgdata, err := cli.ImageSave(context.Background(), []string{imageName}) + if err != nil { + log.WithError(err).Fatal("could not save image") + } + defer func() { + if err := imgdata.Close(); err != nil { + kg.Warnf("Error closing io stream %s\n", err) + } + }() + + tarname := filepath.Join(tempDir, randString(8)+".tar") + + f, err := os.Create(filepath.Clean(tarname)) + if err != nil { + log.WithError(err).Fatal("os create failed") + } + + if _, err := io.CopyN(bufio.NewWriter(f), imgdata, 5e+9 /*5GB*/); err != io.EOF { + log.WithError(err).WithFields(log.Fields{ + "tar": tarname, + }).Fatal("io.CopyN() failed") + } + hacks.CloseCheckErr(f, tarname) + log.WithFields(log.Fields{ + "tar": tarname, + }).Info("dumped image to tar") + return tarname +} diff --git a/recommend/report.go b/recommend/report.go deleted file mode 100644 index 1e52f28e..00000000 --- a/recommend/report.go +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor - -package recommend - -import ( - "errors" - "strings" -) - -/* -ReportInit() -for every image { - ReportStart() - for every policy { - ReportRecord() - } - for every dynamic_admission_controller_policy { - ReportAdmissionControllerRecord() - } - ReportSectEnd() -} -if recommend_generic_admission_controller_policies { - ReportStartGenericAdmissionControllerPolicies() - for every generic_admission_controller_policy { - ReportAdmissionControllerRecord() - } - ReportSectEnd() -} -ReportRender() -*/ - -// Handler interface -var Handler interface{} - -// ReportInit called once per execution -func ReportInit(fname string) { - if Handler != nil { - return - } - if strings.Contains(fname, ".html") { - Handler = NewHTMLReport() - } else { - Handler = NewTextReport() - } -} - -// ReportStart called once per container image at the start -func ReportStart(img *ImageInfo) error { - switch v := Handler.(type) { - case HTMLReport: - return v.Start(img) - case TextReport: - return v.Start(img) - } - return errors.New("unknown reporter type") -} - -// ReportStartGenericAdmissionControllerPolicies called once per generic admission controller policy at the start -func ReportStartGenericAdmissionControllerPolicies() error { - switch v := Handler.(type) { - case HTMLReport: - return v.StartGenericAdmissionControllerPolicies() - case TextReport: - return v.StartGenericAdmissionControllerPolicies() - } - return errors.New("unknown reporter type") -} - -// ReportRecord called once per policy -func ReportRecord(ms MatchSpec, policyName string) error { - switch v := Handler.(type) { - case HTMLReport: - return v.Record(ms, policyName) - case TextReport: - return v.Record(ms, policyName) - } - return errors.New("unknown reporter type") -} - -// ReportAdmissionControllerRecord called once per admission controller policy -func ReportAdmissionControllerRecord(policyFilePath, action string, annotations map[string]string) error { - switch v := Handler.(type) { - case HTMLReport: - return v.RecordAdmissionController(policyFilePath, action, annotations) - case TextReport: - return v.RecordAdmissionController(policyFilePath, action, annotations) - } - return errors.New("unknown reporter type") -} - -// ReportSectEnd called once per container image at the end -func ReportSectEnd() error { - switch v := Handler.(type) { - case HTMLReport: - return v.SectionEnd() - case TextReport: - return v.SectionEnd() - } - return errors.New("unknown reporter type") -} - -// ReportRender called finaly to render the report -func ReportRender(out string) error { - switch v := Handler.(type) { - case HTMLReport: - return v.Render(out) - case TextReport: - return v.Render(out) - } - return errors.New("unknown reporter type") -} diff --git a/recommend/html/css/main.css b/recommend/report/html/css/main.css similarity index 100% rename from recommend/html/css/main.css rename to recommend/report/html/css/main.css diff --git a/recommend/html/footer.html b/recommend/report/html/footer.html similarity index 100% rename from recommend/html/footer.html rename to recommend/report/html/footer.html diff --git a/recommend/html/header.html b/recommend/report/html/header.html similarity index 100% rename from recommend/html/header.html rename to recommend/report/html/header.html diff --git a/recommend/html/images/v38_6837.png b/recommend/report/html/images/v38_6837.png similarity index 100% rename from recommend/html/images/v38_6837.png rename to recommend/report/html/images/v38_6837.png diff --git a/recommend/html/images/v38_7029.png b/recommend/report/html/images/v38_7029.png similarity index 100% rename from recommend/html/images/v38_7029.png rename to recommend/report/html/images/v38_7029.png diff --git a/recommend/html/record.html b/recommend/report/html/record.html similarity index 100% rename from recommend/html/record.html rename to recommend/report/html/record.html diff --git a/recommend/html/sectend.html b/recommend/report/html/sectend.html similarity index 100% rename from recommend/html/sectend.html rename to recommend/report/html/sectend.html diff --git a/recommend/report/html/section.html b/recommend/report/html/section.html new file mode 100644 index 00000000..551474d3 --- /dev/null +++ b/recommend/report/html/section.html @@ -0,0 +1,23 @@ +
+
+
+
+
{{.Name}}
+ {{range .ImgInfo}} + + + + + {{end}} +
{{.Key}}: {{.Val}}
+
+
+
+
+ + + {{range .HdrCols}} + + {{end}} + + diff --git a/recommend/report/report.go b/recommend/report/report.go new file mode 100644 index 00000000..79ce3954 --- /dev/null +++ b/recommend/report/report.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 Authors of KubeArmor + +package report + +import ( + "errors" + "strings" + + "github.com/kubearmor/kubearmor-client/recommend/common" + "github.com/kubearmor/kubearmor-client/recommend/image" +) + +/* +Init() +for every image { + Start() + for every policy { + Record() + } + SectEnd() +} +Render() +*/ + +// Handler interface +var Handler interface{} + +// Init called once per execution +func Init(fname string) { + if Handler != nil { + return + } + if strings.Contains(fname, ".html") { + Handler = NewHTMLReport() + } else { + Handler = NewTextReport() + } +} + +// Start called once per container image at the start +func Start(img *image.Info, options common.Options, currentVersion string) error { + switch v := Handler.(type) { + case HTMLReport: + return v.Start(img, options.OutDir, currentVersion) + case TextReport: + return v.Start(img, options.OutDir, currentVersion) + } + return errors.New("unknown reporter type") +} + +// Record called once per policy +func Record(in interface{}, policyName string) error { + ms := in.(common.MatchSpec) + switch v := Handler.(type) { + case HTMLReport: + return v.Record(ms, policyName) + case TextReport: + return v.Record(ms, policyName) + } + return errors.New("unknown reporter type") +} + +// SectEnd called once per container image at the end +func SectEnd() error { + switch v := Handler.(type) { + case HTMLReport: + return v.SectionEnd() + case TextReport: + return v.SectionEnd() + } + return errors.New("unknown reporter type") +} + +// Render called finaly to render the report +func Render(out string) error { + switch v := Handler.(type) { + case HTMLReport: + return v.Render(out) + case TextReport: + return v.Render(out) + } + return errors.New("unknown reporter type") +} diff --git a/recommend/report_html.go b/recommend/report/report_html.go similarity index 67% rename from recommend/report_html.go rename to recommend/report/report_html.go index f5bdf371..274b8aa3 100644 --- a/recommend/report_html.go +++ b/recommend/report/report_html.go @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2022 Authors of KubeArmor -// Package recommend package -package recommend +// Package report package +package report import ( _ "embed" // need for embedding @@ -13,10 +13,9 @@ import ( "strings" "time" - "github.com/accuknox/auto-policy-discovery/src/types" + "github.com/kubearmor/kubearmor-client/recommend/common" + "github.com/kubearmor/kubearmor-client/recommend/image" log "github.com/sirupsen/logrus" - "golang.org/x/text/cases" - "golang.org/x/text/language" ) // HTMLReport Report in HTML format @@ -73,9 +72,8 @@ type HeaderInfo struct { // SectionInfo Section information type SectionInfo struct { - HdrCols []Col - ImgInfo []Info - GenericAdmissionControllerPolicy bool + HdrCols []Col + ImgInfo []Info } // NewHTMLReport instantiation on new html report @@ -105,7 +103,10 @@ func NewHTMLReport() HTMLReport { ReportTitle: "Security Report", DateTime: time.Now().Format("02-Jan-2006 15:04:05"), } - _ = header.Execute(str, hdri) + err = header.Execute(str, hdri) + if err != nil { + log.WithError(err).Error("failed to execute report header") + } recordcnt := 0 return HTMLReport{ header: header, @@ -119,7 +120,7 @@ func NewHTMLReport() HTMLReport { } // Start of HTML report section -func (r HTMLReport) Start(img *ImageInfo) error { +func (r HTMLReport) Start(img *image.Info, outDir string, currentVersion string) error { seci := SectionInfo{ HdrCols: []Col{ {Name: "POLICY"}, @@ -131,26 +132,14 @@ func (r HTMLReport) Start(img *ImageInfo) error { ImgInfo: []Info{ {Key: "Container", Val: img.RepoTags[0]}, {Key: "OS/Arch/Distro", Val: img.OS + "/" + img.Arch + "/" + img.Distro}, - {Key: "Output Directory", Val: img.getPolicyDir()}, - {Key: "policy-template version", Val: CurrentVersion}, + {Key: "Output Directory", Val: img.GetPolicyDir(outDir)}, + {Key: "policy-template version", Val: currentVersion}, }, } - _ = r.section.Execute(r.outString, seci) - return nil -} - -func (r HTMLReport) StartGenericAdmissionControllerPolicies() error { - secInfo := SectionInfo{ - HdrCols: []Col{ - {Name: "POLICY"}, - {Name: "DESCRIPTION"}, - {Name: "SEVERITY"}, - {Name: "ACTION"}, - {Name: "TAGS"}, - }, - GenericAdmissionControllerPolicy: true, + err := r.section.Execute(r.outString, seci) + if err != nil { + log.WithError(err) } - _ = r.section.Execute(r.outString, secInfo) return nil } @@ -161,11 +150,11 @@ type RecordInfo struct { Policy string Description string PolicyType string - Refs []Ref + Refs []common.Ref } // Record addition of new HTML table row -func (r HTMLReport) Record(ms MatchSpec, policyName string) error { +func (r HTMLReport) Record(ms common.MatchSpec, policyName string) error { *r.RecordCnt = *r.RecordCnt + 1 policy, err := os.ReadFile(filepath.Clean(policyName)) if err != nil { @@ -186,34 +175,11 @@ func (r HTMLReport) Record(ms MatchSpec, policyName string) error { Description: ms.Description.Detailed, Refs: ms.Description.Refs, } - _ = r.record.Execute(r.outString, reci) - return nil -} - -// RecordAdmissionController addition of new HTML table row for admission controller policies -func (r HTMLReport) RecordAdmissionController(policyFilePath, action string, annotations map[string]string) error { - *r.RecordCnt = *r.RecordCnt + 1 - policy, err := os.ReadFile(filepath.Clean(policyFilePath)) + err = r.record.Execute(r.outString, reci) if err != nil { - log.WithError(err).Error(fmt.Sprintf("failed to read policy %s", policyFilePath)) - } - policyFilePath = policyFilePath[strings.LastIndex(policyFilePath, "/")+1:] - reci := RecordInfo{ - RowID: fmt.Sprintf("row%d", *r.RecordCnt), - Rec: []Col{ - {Name: policyFilePath}, - {Name: annotations[types.RecommendedPolicyTitleAnnotation]}, - {Name: "-"}, - {Name: cases.Title(language.English).String(action)}, - {Name: strings.Join(strings.Split(annotations[types.RecommendedPolicyTagsAnnotation], ",")[:], "\n")}, - }, - Policy: string(policy), - PolicyType: "Kyverno Policy", - Description: annotations[types.RecommendedPolicyDescriptionAnnotation], - // TODO: Figure out how to get the references, adding them to annotations would make them too long - Refs: []Ref{}, + log.WithError(err) } - return r.record.Execute(r.outString, reci) + return nil } // SectionEnd end of section of the HTML table @@ -223,13 +189,19 @@ func (r HTMLReport) SectionEnd() error { // Render output the table func (r HTMLReport) Render(out string) error { - _ = r.footer.Execute(r.outString, nil) + err := r.footer.Execute(r.outString, nil) + if err != nil { + log.WithError(err) + } outPath := strings.Join(strings.Split(out, "/")[:len(strings.Split(out, "/"))-1], "/") outPath = outPath + "/.static/" - _ = os.MkdirAll(outPath, 0740) + err = os.MkdirAll(outPath, 0740) + if err != nil { + log.WithError(err) + } if err := os.WriteFile(outPath+"main.css", []byte(mainCSS), 0600); err != nil { log.WithError(err).Error("failed to write file") diff --git a/recommend/report_text.go b/recommend/report/report_text.go similarity index 63% rename from recommend/report_text.go rename to recommend/report/report_text.go index 15dc3118..6570ca97 100644 --- a/recommend/report_text.go +++ b/recommend/report/report_text.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2022 Authors of KubeArmor -package recommend +package report import ( _ "embed" // need for embedding @@ -9,7 +9,8 @@ import ( "os" "strings" - "github.com/accuknox/auto-policy-discovery/src/types" + "github.com/kubearmor/kubearmor-client/recommend/common" + "github.com/kubearmor/kubearmor-client/recommend/image" "github.com/olekukonko/tablewriter" log "github.com/sirupsen/logrus" ) @@ -30,7 +31,7 @@ func NewTextReport() TextReport { } } -func (r TextReport) writeImageSummary(img *ImageInfo) { +func (r TextReport) writeImageSummary(img *image.Info, outDir string, currentVersion string) { t := tablewriter.NewWriter(r.outString) t.SetBorder(false) if img.Deployment != "" { @@ -41,26 +42,14 @@ func (r TextReport) writeImageSummary(img *ImageInfo) { t.Append([]string{"OS", img.OS}) t.Append([]string{"Arch", img.Arch}) t.Append([]string{"Distro", img.Distro}) - t.Append([]string{"Output Directory", img.getPolicyDir()}) - t.Append([]string{"policy-template version", CurrentVersion}) + t.Append([]string{"Output Directory", img.GetPolicyDir(outDir)}) + t.Append([]string{"policy-template version", currentVersion}) t.Render() } // Start Start of the section of the text report -func (r TextReport) Start(img *ImageInfo) error { - r.writeImageSummary(img) - r.table.SetHeader([]string{"Policy", "Short Desc", "Severity", "Action", "Tags"}) - r.table.SetAlignment(tablewriter.ALIGN_LEFT) - r.table.SetRowLine(true) - return nil -} - -func (r TextReport) StartGenericAdmissionControllerPolicies() error { - t := tablewriter.NewWriter(r.outString) - t.SetBorder(false) - t.Rich([]string{"Generic Kyverno Policies"}, []tablewriter.Colors{{tablewriter.Bold}}) - t.Render() - +func (r TextReport) Start(img *image.Info, outDir string, currentVersion string) error { + r.writeImageSummary(img, outDir, currentVersion) r.table.SetHeader([]string{"Policy", "Short Desc", "Severity", "Action", "Tags"}) r.table.SetAlignment(tablewriter.ALIGN_LEFT) r.table.SetRowLine(true) @@ -76,7 +65,7 @@ func (r TextReport) SectionEnd() error { } // Record addition of new text table row -func (r TextReport) Record(ms MatchSpec, policyName string) error { +func (r TextReport) Record(ms common.MatchSpec, policyName string) error { var rec []string policyName = policyName[strings.LastIndex(policyName, "/")+1:] rec = append(rec, wrapPolicyName(policyName, 35)) @@ -88,19 +77,6 @@ func (r TextReport) Record(ms MatchSpec, policyName string) error { return nil } -// RecordAdmissionController adds new row to table for admission controller policies -func (r TextReport) RecordAdmissionController(policyFilePath, action string, annotations map[string]string) error { - var rec []string - policyFilePath = policyFilePath[strings.LastIndex(policyFilePath, "/")+1:] - rec = append(rec, wrapPolicyName(policyFilePath, 35)) - rec = append(rec, annotations[types.RecommendedPolicyTitleAnnotation]) - rec = append(rec, "-") - rec = append(rec, action) - rec = append(rec, strings.Join(strings.Split(annotations[types.RecommendedPolicyTagsAnnotation], ",")[:], "\n")) - r.table.Append(rec) - return nil -} - func wrapPolicyName(name string, limit int) string { line := "" lines := []string{} diff --git a/recommend/runtimePolicy.go b/recommend/runtimePolicy.go deleted file mode 100644 index e28769a0..00000000 --- a/recommend/runtimePolicy.go +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor - -package recommend - -import ( - "context" - "errors" - "fmt" - "os" - "strings" - - opb "github.com/accuknox/auto-policy-discovery/src/protobuf/v1/observability" - pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" - - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" -) - -var saPath []string - -func init() { - saPath = []string{ - "/var/run/secrets/kubernetes.io/serviceaccount/", - "/run/secrets/kubernetes.io/serviceaccount/", - } -} - -// createRuntimePolicy function generates runtime policy for service account -func createRuntimePolicy(img *ImageInfo) error { - var labels string - for key, value := range img.Labels { - labels = strings.TrimPrefix(fmt.Sprintf("%s,%s=%s", labels, key, value), ",") - } - gRPC := "" - if val, ok := os.LookupEnv("DISCOVERY_SERVICE"); ok { - gRPC = val - } else { - gRPC = "localhost:9089" - } - // create a client - conn, err := grpc.Dial(gRPC, grpc.WithTransportCredentials(insecure.NewCredentials())) - if err != nil { - return errors.New("could not connect to the server. Possible troubleshooting:\n- Check if discovery engine is running\n- Create a portforward to discovery engine service using\n\t\033[1mkubectl port-forward -n explorer service/knoxautopolicy --address 0.0.0.0 --address :: 9089:9089\033[0m\n[0m") - } - defer conn.Close() - client := opb.NewObservabilityClient(conn) - podData, err := client.GetPodNames(context.Background(), &opb.Request{ - Label: labels, - NameSpace: img.Namespace, - Aggregate: true, - }) - if err != nil { - return err - } - var resp *opb.Response - var sumResp []*opb.Response - for _, pod := range podData.PodName { - resp, err = client.Summary(context.Background(), &opb.Request{ - PodName: pod, - Label: labels, - NameSpace: img.Namespace, - Type: "file", - Aggregate: true, - }) - if err != nil { - return err - } - sumResp = append(sumResp, resp) - } - - ms := checkProcessFileData(sumResp, img.Distro) - if ms != nil { - img.writePolicyFile(*ms) - } - return nil -} - -func checkProcessFileData(sumResp []*opb.Response, distro string) *MatchSpec { - var filePaths pol.FileType - ref := Ref{ - Name: "MITRE Unsecured Credentials: Container API", - URL: []string{"https://attack.mitre.org/techniques/T1552/007/"}, - } - fromSourceArr := []pol.MatchSourceType{} - ms := MatchSpec{ - Name: "allow-serviceaccount-runtime", - Description: Description{ - Refs: []Ref{ref}, - Tldr: "Kubernetes serviceaccount folder access should be limited", - Detailed: "Adversaries may gather credentials via APIs within a containers environment. APIs in these environments, such as the Docker API and Kubernetes APIs, allow a user to remotely manage their container resources and cluster components. An adversary may access the Docker API to collect logs that contain credentials to cloud, container, and various other resources in the environment. An adversary with sufficient permissions, such as via a pod's service account, may also use the Kubernetes API to retrieve credentials from the Kubernetes API server. These credentials may include those needed for Docker API authentication or secrets from Kubernetes cluster components.", - }, - } - for _, eachResp := range sumResp { - for _, fileData := range eachResp.FileData { - if strings.HasPrefix(fileData.Destination, saPath[0]) || strings.HasPrefix(fileData.Destination, saPath[1]) { - fromSourceArr = append(fromSourceArr, pol.MatchSourceType{ - Path: pol.MatchPathType(fileData.Source), - }) - } - } - } - filePaths.MatchDirectories = append(filePaths.MatchDirectories, pol.FileDirectoryType{ - Directory: pol.MatchDirectoryType(saPath[0]), - FromSource: fromSourceArr, - Recursive: true, - }) - filePaths.MatchDirectories = append(filePaths.MatchDirectories, pol.FileDirectoryType{ - Directory: pol.MatchDirectoryType(saPath[1]), - FromSource: fromSourceArr, - Recursive: true, - }) - ms.Spec = pol.KubeArmorPolicySpec{ - Action: "Allow", - Message: "serviceaccount access detected", - Tags: []string{"KUBERNETES", "SERVICE ACCOUNT", "RUNTIME POLICY"}, - Severity: 1, - File: filePaths, - } - if len(fromSourceArr) < 1 { - ms.Spec.Action = "Block" - ms.Name = "block-serviceaccount-runtime" - ms.Spec.Message = "serviceaccount access blocked" - } - return &ms -} diff --git a/tests/recommend/recommend_test.go b/tests/recommend/recommend_test.go index e7982352..d5828ecc 100644 --- a/tests/recommend/recommend_test.go +++ b/tests/recommend/recommend_test.go @@ -16,17 +16,19 @@ import ( "github.com/google/go-cmp/cmp" "github.com/kubearmor/kubearmor-client/k8s" "github.com/kubearmor/kubearmor-client/recommend" + "github.com/kubearmor/kubearmor-client/recommend/common" + genericpolicies "github.com/kubearmor/kubearmor-client/recommend/engines/generic_policies" . "github.com/onsi/gomega" ) -var testOptions recommend.Options +var testOptions common.Options var err error var client *k8s.Client func compareData(file1, file2 string) bool { - var pol1, pol2 recommend.MatchSpec + var pol1, pol2 common.MatchSpec data1, err := os.ReadFile(filepath.Clean(file1)) if err != nil { return false @@ -74,7 +76,7 @@ var _ = Describe("karmor", func() { }) AfterEach(func() { - testOptions = recommend.Options{} + testOptions = common.Options{} }) Describe("recommend", func() { @@ -83,7 +85,7 @@ var _ = Describe("karmor", func() { It("should fetch the latest policy-template release and modify the rule under ~/.cache/karmor/", func() { //os.MkdirAll(testOptions.OutDir, 0777) - _, err := recommend.DownloadAndUnzipRelease() + _, err := genericpolicies.DownloadAndUnzipRelease() Expect(err).To(BeNil()) files, err := os.ReadDir(fmt.Sprintf("%s/.cache/karmor", os.Getenv("HOME"))) Expect(err).To(BeNil()) @@ -97,7 +99,7 @@ var _ = Describe("karmor", func() { count := 0 It("should fetch the ubuntu:18.04 image and create a directory `ubuntu-18-04` under `out` folder", func() { testOptions.Images = []string{"ubuntu:18.04"} - err = recommend.Recommend(client, testOptions) + err = recommend.Recommend(client, testOptions, genericpolicies.GenericPolicy{}) Expect(err).To(BeNil()) files, err = os.ReadDir(fmt.Sprintf("%s/ubuntu-18-04", testOptions.OutDir)) Expect(len(files)).To(BeNumerically(">=", 1)) @@ -127,7 +129,7 @@ var _ = Describe("karmor", func() { It("should fetch the ubuntu:18.04 image and create a directory `ubuntu-18-04` under `ubuntu-test` folder", func() { testOptions.OutDir = "ubuntu-test" testOptions.Images = []string{"ubuntu:18.04"} - err = recommend.Recommend(client, testOptions) + err = recommend.Recommend(client, testOptions, genericpolicies.GenericPolicy{}) Expect(err).To(BeNil()) files, err = os.ReadDir(fmt.Sprintf("%s/ubuntu-18-04", testOptions.OutDir)) Expect(len(files)).To(BeNumerically(">=", 1)) @@ -157,7 +159,7 @@ var _ = Describe("karmor", func() { It("should fetch the image and create a folder wordpress-mysql-wordpress under `out` directory", func() { testOptions.Labels = []string{"app=wordpress"} testOptions.Namespace = "wordpress-mysql" - err = recommend.Recommend(client, testOptions) + err = recommend.Recommend(client, testOptions, genericpolicies.GenericPolicy{}) Expect(err).To(BeNil()) files, err = os.ReadDir(fmt.Sprintf("%s/wordpress-mysql-wordpress", testOptions.OutDir)) Expect(len(files)).To(BeNumerically(">=", 1)) @@ -189,7 +191,7 @@ var _ = Describe("karmor", func() { testOptions.Labels = []string{"app=wordpress"} testOptions.Namespace = "wordpress-mysql" testOptions.OutDir = "wordpress-test" - err = recommend.Recommend(client, testOptions) + err = recommend.Recommend(client, testOptions, genericpolicies.GenericPolicy{}) Expect(err).To(BeNil()) files, err = os.ReadDir(fmt.Sprintf("%s/wordpress-mysql-wordpress", testOptions.OutDir)) Expect(len(files)).To(BeNumerically(">=", 1)) From 73fec202adc9c4e52aa901f3ab881fac23b34e1d Mon Sep 17 00:00:00 2001 From: Prateek Nandle Date: Thu, 31 Aug 2023 16:37:50 +0530 Subject: [PATCH 2/3] migrate Userhome func to common Signed-off-by: Prateek Nandle --- cmd/recommend.go | 2 +- recommend/common/{policy.go => common.go} | 15 +++++++++++++++ .../engines/generic_policies/policy-templates.go | 15 +-------------- 3 files changed, 17 insertions(+), 15 deletions(-) rename recommend/common/{policy.go => common.go} (81%) diff --git a/cmd/recommend.go b/cmd/recommend.go index 41bd7463..d9949c3f 100644 --- a/cmd/recommend.go +++ b/cmd/recommend.go @@ -49,5 +49,5 @@ func init() { recommendCmd.Flags().StringVarP(&recommendOptions.OutDir, "outdir", "o", "out", "output folder to write policies") recommendCmd.Flags().StringVarP(&recommendOptions.ReportFile, "report", "r", "report.txt", "report file") recommendCmd.Flags().StringSliceVarP(&recommendOptions.Tags, "tag", "t", []string{}, "tags (comma-separated) to apply. Eg. PCI-DSS, MITRE") - recommendCmd.Flags().StringVarP(&recommendOptions.Config, "config", "c", genericpolicies.UserHome()+"/.docker/config.json", "absolute path to image registry configuration file") + recommendCmd.Flags().StringVarP(&recommendOptions.Config, "config", "c", common.UserHome()+"/.docker/config.json", "absolute path to image registry configuration file") } diff --git a/recommend/common/policy.go b/recommend/common/common.go similarity index 81% rename from recommend/common/policy.go rename to recommend/common/common.go index 9865ee17..3c824bc1 100644 --- a/recommend/common/policy.go +++ b/recommend/common/common.go @@ -2,6 +2,9 @@ package common import ( + "os" + "runtime" + pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" ) @@ -41,3 +44,15 @@ type Options struct { ReportFile string Config string } + +// UserHome function returns users home directory +func UserHome() string { + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + return os.Getenv("HOME") +} diff --git a/recommend/engines/generic_policies/policy-templates.go b/recommend/engines/generic_policies/policy-templates.go index 36ae9b99..feeccd78 100644 --- a/recommend/engines/generic_policies/policy-templates.go +++ b/recommend/engines/generic_policies/policy-templates.go @@ -10,7 +10,6 @@ import ( "os" "path" "path/filepath" - "runtime" "strings" "github.com/cavaliergopher/grab/v3" @@ -60,23 +59,11 @@ func CurrentRelease() string { } func getCachePath() string { - cache := fmt.Sprintf("%s/%s", UserHome(), cache) + cache := fmt.Sprintf("%s/%s", common.UserHome(), cache) return cache } -// UserHome function returns users home directory -func UserHome() string { - if runtime.GOOS == "windows" { - home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - if home == "" { - home = os.Getenv("USERPROFILE") - } - return home - } - return os.Getenv("HOME") -} - //go:embed yaml/rules.yaml var policyRulesYAML []byte From 5a5b66f9b5eb27258e79d573916ba6dc1dc655c1 Mon Sep 17 00:00:00 2001 From: Prateek Nandle Date: Mon, 11 Sep 2023 13:43:46 +0530 Subject: [PATCH 3/3] handling comments and empty objects in policy Signed-off-by: Prateek Nandle --- hacks/common.go | 3 +++ recommend/common/common.go | 3 +++ recommend/engines/engine.go | 3 +++ recommend/engines/generic_policies/generic_policies.go | 6 ++++-- recommend/engines/generic_policies/policy-templates.go | 10 +++++----- recommend/image/image.go | 6 +++++- recommend/recommend.go | 3 +++ recommend/registry/registry.go | 2 +- recommend/report/report.go | 2 +- recommend/report/report_html.go | 2 +- recommend/report/report_text.go | 2 +- 11 files changed, 30 insertions(+), 12 deletions(-) diff --git a/hacks/common.go b/hacks/common.go index f90ad7e8..44c5fa21 100644 --- a/hacks/common.go +++ b/hacks/common.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + // Package hacks close the file package hacks diff --git a/recommend/common/common.go b/recommend/common/common.go index 3c824bc1..cf4baeae 100644 --- a/recommend/common/common.go +++ b/recommend/common/common.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + // Package common contains object types used by multiple packages package common diff --git a/recommend/engines/engine.go b/recommend/engines/engine.go index 69427db0..a200a255 100644 --- a/recommend/engines/engine.go +++ b/recommend/engines/engine.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + // Package engines provides interfaces and implementations for policy generation package engines diff --git a/recommend/engines/generic_policies/generic_policies.go b/recommend/engines/generic_policies/generic_policies.go index ffd020ef..03e5f8fd 100644 --- a/recommend/engines/generic_policies/generic_policies.go +++ b/recommend/engines/generic_policies/generic_policies.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + // Package genericpolicies is responsible for creating and managing policies based on policy generator package genericpolicies @@ -9,7 +12,6 @@ import ( "regexp" "strings" - "github.com/fatih/color" "github.com/kubearmor/kubearmor-client/recommend/common" "github.com/kubearmor/kubearmor-client/recommend/image" "github.com/kubearmor/kubearmor-client/recommend/report" @@ -96,7 +98,7 @@ func getPolicyFromImageInfo(img *image.Info, options common.Options) (map[string msMap := make(map[string]interface{}) if img.OS != "linux" { - color.Red("non-linux platforms are not supported, yet.") + log.Errorf("non-linux platforms are not supported, yet.") return nil, nil, nil } diff --git a/recommend/engines/generic_policies/policy-templates.go b/recommend/engines/generic_policies/policy-templates.go index 67b9c7cf..39ea6492 100644 --- a/recommend/engines/generic_policies/policy-templates.go +++ b/recommend/engines/generic_policies/policy-templates.go @@ -1,10 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + package genericpolicies import ( "archive/zip" "context" _ "embed" // need for embedding - "encoding/json" "errors" "fmt" "io" @@ -14,7 +16,8 @@ import ( "path/filepath" "strings" - "github.com/fatih/color" + "github.com/clarketm/json" + "github.com/google/go-github/github" kg "github.com/kubearmor/KubeArmor/KubeArmor/log" pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" @@ -81,18 +84,15 @@ func updateRulesYAML(yamlFile []byte) string { } policyRulesJSON, err := yaml.YAMLToJSON(yamlFile) if err != nil { - color.Red("failed to convert policy rules yaml to json") log.WithError(err).Fatal("failed to convert policy rules yaml to json") } var jsonRaw map[string]json.RawMessage err = json.Unmarshal(policyRulesJSON, &jsonRaw) if err != nil { - color.Red("failed to unmarshal policy rules json") log.WithError(err).Fatal("failed to unmarshal policy rules json") } err = json.Unmarshal(jsonRaw["policyRules"], &policyRules) if err != nil { - color.Red("failed to unmarshal policy rules") log.WithError(err).Fatal("failed to unmarshal policy rules") } return string(jsonRaw["version"]) diff --git a/recommend/image/image.go b/recommend/image/image.go index e075f750..81746f91 100644 --- a/recommend/image/image.go +++ b/recommend/image/image.go @@ -1,9 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + // Package image scan and provide image info package image import ( _ "embed" // need for embedding - "encoding/json" "fmt" "io" "os" @@ -11,6 +13,8 @@ import ( "regexp" "strings" + "github.com/clarketm/json" + "github.com/fatih/color" pol "github.com/kubearmor/KubeArmor/pkg/KubeArmorController/api/security.kubearmor.com/v1" "github.com/kubearmor/kubearmor-client/hacks" diff --git a/recommend/recommend.go b/recommend/recommend.go index 25fafff0..9ab1ed66 100644 --- a/recommend/recommend.go +++ b/recommend/recommend.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Authors of KubeArmor + // Package recommend provides policies by policy generators package recommend diff --git a/recommend/registry/registry.go b/recommend/registry/registry.go index 0dbdd5df..046c32d0 100644 --- a/recommend/registry/registry.go +++ b/recommend/registry/registry.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor +// Copyright 2023 Authors of KubeArmor // Package registry contains scanner for image info package registry diff --git a/recommend/report/report.go b/recommend/report/report.go index 79ce3954..7af5caba 100644 --- a/recommend/report/report.go +++ b/recommend/report/report.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor +// Copyright 2023 Authors of KubeArmor package report diff --git a/recommend/report/report_html.go b/recommend/report/report_html.go index 274b8aa3..74dfa488 100644 --- a/recommend/report/report_html.go +++ b/recommend/report/report_html.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor +// Copyright 2023 Authors of KubeArmor // Package report package package report diff --git a/recommend/report/report_text.go b/recommend/report/report_text.go index 6570ca97..a5b981a9 100644 --- a/recommend/report/report_text.go +++ b/recommend/report/report_text.go @@ -1,5 +1,5 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 Authors of KubeArmor +// Copyright 2023 Authors of KubeArmor package report
{{.Name}}