diff --git a/cmd/recommend.go b/cmd/recommend.go
index 19a40218..d9949c3f 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", common.UserHome()+"/.docker/config.json", "absolute path to image registry configuration file")
}
diff --git a/go.mod b/go.mod
index e2f87f2d..d78010a0 100644
--- a/go.mod
+++ b/go.mod
@@ -55,17 +55,14 @@ require (
github.com/kubearmor/KubeArmor/KubeArmor v0.0.0-20230918061249-1d5b51c449bd
github.com/kubearmor/KubeArmor/deployments v0.0.0-20230918135729-00395f443fa0
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 (
@@ -208,6 +205,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
@@ -318,6 +316,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
@@ -333,6 +332,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..44c5fa21
--- /dev/null
+++ b/hacks/common.go
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2023 Authors of KubeArmor
+
+// 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/common.go b/recommend/common/common.go
new file mode 100644
index 00000000..cf4baeae
--- /dev/null
+++ b/recommend/common/common.go
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2023 Authors of KubeArmor
+
+// Package common contains object types used by multiple packages
+package common
+
+import (
+ "os"
+ "runtime"
+
+ 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
+}
+
+// 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/engine.go b/recommend/engines/engine.go
new file mode 100644
index 00000000..a200a255
--- /dev/null
+++ b/recommend/engines/engine.go
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2023 Authors of KubeArmor
+
+// 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..03e5f8fd
--- /dev/null
+++ b/recommend/engines/generic_policies/generic_policies.go
@@ -0,0 +1,129 @@
+// 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
+
+import (
+ _ "embed" // need for embedding
+ "fmt"
+ "path/filepath"
+
+ "regexp"
+ "strings"
+
+ "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 {
+ log.WithError(err).Error("could not download latest policy-templates version")
+ } else {
+ log.WithFields(log.Fields{
+ "Updated Version": LatestVersion,
+ }).Info("policy-templates updated")
+ }
+ 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" {
+ log.Errorf("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 66%
rename from recommend/policyTemplates.go
rename to recommend/engines/generic_policies/policy-templates.go
index 7f1abbcf..39ea6492 100644
--- a/recommend/policyTemplates.go
+++ b/recommend/engines/generic_policies/policy-templates.go
@@ -1,56 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
-// Copyright 2022 Authors of KubeArmor
+// Copyright 2023 Authors of KubeArmor
-package recommend
+package genericpolicies
import (
"archive/zip"
"context"
+ _ "embed" // need for embedding
+ "errors"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
- "runtime"
"strings"
+ "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"
+ "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 {
@@ -64,7 +54,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{}), "\"")
@@ -72,19 +62,40 @@ func CurrentRelease() string {
CurrentVersion = strings.Trim(updateRulesYAML(path), "\"")
}
-
return CurrentVersion
}
-func isLatest() bool {
- LatestVersion = latestRelease()
+func getCachePath() string {
+ cache := fmt.Sprintf("%s/%s", common.UserHome(), cache)
+ return cache
- if LatestVersion == "" {
- // error while fetching latest release tag
- // assume the current release is the latest one
- return true
+}
+
+//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
}
- return (CurrentVersion == LatestVersion)
+ policyRulesJSON, err := yaml.YAMLToJSON(yamlFile)
+ if err != nil {
+ 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 {
+ log.WithError(err).Fatal("failed to unmarshal policy rules json")
+ }
+ err = json.Unmarshal(jsonRaw["policyRules"], &policyRules)
+ if err != nil {
+ log.WithError(err).Fatal("failed to unmarshal policy rules")
+ }
+ return string(jsonRaw["version"])
}
func removeData(file string) error {
@@ -92,10 +103,6 @@ func removeData(file string) error {
return err
}
-func init() {
- CurrentVersion = CurrentRelease()
-}
-
func downloadZip(url string, destination string) error {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
@@ -129,19 +136,36 @@ func downloadZip(url string, destination string) error {
// DownloadAndUnzipRelease downloads the latest version of policy-templates
func DownloadAndUnzipRelease() (string, error) {
+ latestVersion := latestRelease()
+ currentVersion := CurrentRelease()
- LatestVersion = latestRelease()
+ if isLatest() {
+ return latestVersion, nil
+ }
+
+ 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)
zipPath := getCachePath() + ".zip"
err = downloadZip(downloadURL, zipPath)
+
if err != nil {
- _ = removeData(getCachePath())
+ err = removeData(getCachePath())
+ if err != nil {
+ log.WithError(err).Error("failed to remove cache files")
+ }
return "", err
}
@@ -151,11 +175,23 @@ func DownloadAndUnzipRelease() (string, error) {
}
err = removeData(zipPath)
if err != nil {
- return "", err
+ log.WithError(err).Error("failed to remove cache files")
}
- _ = updatePolicyRules(strings.TrimSuffix(zipPath, ".zip"))
- CurrentVersion = CurrentRelease()
- return LatestVersion, nil
+ err = updatePolicyRules(strings.TrimSuffix(zipPath, ".zip"))
+ if err != nil {
+ log.WithError(err).Error("failed to update policy rules")
+ }
+ 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 {
@@ -176,7 +212,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
@@ -197,6 +236,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 {
@@ -218,7 +269,7 @@ func updatePolicyRules(filePath string) error {
}
var yamlFile []byte
- var completePolicy []MatchSpec
+ var completePolicy []common.MatchSpec
var version string
for _, file := range files {
@@ -241,23 +292,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
@@ -273,5 +323,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}}
-
- {{.Key}} |
- : {{.Val}} |
-
- {{end}}
-
- {{end}}
-
-
-
-
-
-
- {{range .HdrCols}}
- {{.Name}} |
- {{end}}
-
-
diff --git a/recommend/image/image.go b/recommend/image/image.go
new file mode 100644
index 00000000..81746f91
--- /dev/null
+++ b/recommend/image/image.go
@@ -0,0 +1,326 @@
+// 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
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "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"
+ "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 681e5782..9ab1ed66 100644
--- a/recommend/recommend.go
+++ b/recommend/recommend.go
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
-// Copyright 2022 Authors of KubeArmor
+// Copyright 2023 Authors of KubeArmor
+// Package recommend provides policies by policy generators
package recommend
import (
@@ -13,34 +14,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 +35,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 +96,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,34 +111,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 {
- log.WithError(err).Error("could not download latest policy-templates version")
- } else {
- log.WithFields(log.Fields{
- "Updated Version": LatestVersion,
- }).Info("policy-templates updated")
+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))
}
- }
- 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{})
@@ -131,6 +151,7 @@ func Recommend(c *k8s.Client, o Options) error {
return err
}
for _, dp := range dps.Items {
+
if !matchLabels(labelMap, dp.Spec.Template.Labels) {
continue
}
@@ -160,79 +181,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)
- }
+ if err = createOutDir(o.OutDir); err != nil {
+ return err
}
- recommendKyvernoPolicies := false
- for _, policy := range options.Policy {
- if policy == KyvernoPolicy {
- recommendKyvernoPolicies = true
+ for _, gen := range policyGenerators {
+ if o.ReportFile != "" {
+ report.Init(o.ReportFile)
}
- }
-
- if recommendKyvernoPolicies {
- err = recommendGenericAdmissionControllerPolicies()
- if err != nil {
- log.Error(err)
+ if err := gen.Init(); err != nil {
+ log.WithError(err).Error("policy generator init failed")
}
- }
-
- 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")
- }
- 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..046c32d0
--- /dev/null
+++ b/recommend/registry/registry.go
@@ -0,0 +1,300 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2023 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 @@
+
+
+
+
+
+ {{range .ImgInfo}}
+
+ {{.Key}} |
+ : {{.Val}} |
+
+ {{end}}
+
+
+
+
+
+
+
+ {{range .HdrCols}}
+ {{.Name}} |
+ {{end}}
+
+
diff --git a/recommend/report/report.go b/recommend/report/report.go
new file mode 100644
index 00000000..7af5caba
--- /dev/null
+++ b/recommend/report/report.go
@@ -0,0 +1,84 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright 2023 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..74dfa488 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
+// Copyright 2023 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 62%
rename from recommend/report_text.go
rename to recommend/report/report_text.go
index 15dc3118..a5b981a9 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
+// Copyright 2023 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))