From 50d09be9b0b4ac0a60fbbb474136add749ddb53e Mon Sep 17 00:00:00 2001 From: Ali Khlifi <46890720+khlifi411@users.noreply.github.com> Date: Wed, 22 Feb 2023 14:54:22 +0100 Subject: [PATCH] Security Scanners information in Module Template (#1531) * introduce new step in create module command to add security scanning data * fix linting * introduce flag for security scanning config file * make docs * go fmt --- cmd/kyma/alpha/create/module/module.go | 12 ++ cmd/kyma/alpha/create/module/opts.go | 1 + docs/gen-docs/kyma_alpha_create_module.md | 1 + pkg/module/remote.go | 4 - pkg/module/security_scan.go | 162 ++++++++++++++++++++++ pkg/module/template.go | 2 +- 6 files changed, 177 insertions(+), 5 deletions(-) create mode 100644 pkg/module/security_scan.go diff --git a/cmd/kyma/alpha/create/module/module.go b/cmd/kyma/alpha/create/module/module.go index 679ab2b71..8097dcd78 100644 --- a/cmd/kyma/alpha/create/module/module.go +++ b/cmd/kyma/alpha/create/module/module.go @@ -110,6 +110,8 @@ Build module my-domain/modB in version 3.2.1 and push it to a local registry "un ) cmd.Flags().BoolVar(&o.Insecure, "insecure", false, "Use an insecure connection to access the registry.") cmd.Flags().BoolVar(&o.Clean, "clean", false, "Remove the mod-path folder and all its contents at the end.") + cmd.Flags().StringVar(&o.SecurityScanConfig, "sec-scan-cfg", "", "Path to the directory holding "+ + "the security scan configuration file.") return cmd } @@ -189,6 +191,16 @@ func (cmd *command) Run(ctx context.Context, args []string) error { cmd.CurrentStep.Successf("Image created") + /* -- ADD SECURITY SCANNING METADATA -- */ + if cmd.opts.SecurityScanConfig != "" { + cmd.NewStep("Configuring security scanning...") + err = module.AddSecurityScanningMetadata(archive.ComponentDescriptor, modDef, fs, cmd.opts.SecurityScanConfig) + if err != nil { + cmd.CurrentStep.Failure() + return err + } + cmd.CurrentStep.Successf("Security scanning configured") + } /* -- PUSH & TEMPLATE -- */ if cmd.opts.RegistryURL != "" { diff --git a/cmd/kyma/alpha/create/module/opts.go b/cmd/kyma/alpha/create/module/opts.go index 222c36ae7..b5a4b6348 100644 --- a/cmd/kyma/alpha/create/module/opts.go +++ b/cmd/kyma/alpha/create/module/opts.go @@ -31,6 +31,7 @@ type Options struct { Overwrite bool Clean bool RegistryCredSelector string + SecurityScanConfig string } const ( diff --git a/docs/gen-docs/kyma_alpha_create_module.md b/docs/gen-docs/kyma_alpha_create_module.md index 751be4d5c..ee3f852fd 100644 --- a/docs/gen-docs/kyma_alpha_create_module.md +++ b/docs/gen-docs/kyma_alpha_create_module.md @@ -59,6 +59,7 @@ Build module my-domain/modB in version 3.2.1 and push it to a local registry "un --registry string Repository context url for module to upload. The repository url will be automatically added to the repository contexts in the module --registry-cred-selector string label selector to identify a secret of type kubernetes.io/dockerconfigjson (that needs to be created externally) which allows the image to be accessed in private image registries. This can be used if you push your module to a registry with authenticated access. Example: "label1=value1,label2=value2" -r, --resource stringArray Add an extra resource in a new layer with format . It is also possible to provide only a path; name will default to the last path element and type to 'helm-chart' + --sec-scan-cfg string Path to the directory holding the security scan configuration file. -t, --token string Authentication token for the given registry (alternative to basic authentication). --version string Version of the module. This flag is mandatory. ``` diff --git a/pkg/module/remote.go b/pkg/module/remote.go index af876a39c..3326e4e84 100644 --- a/pkg/module/remote.go +++ b/pkg/module/remote.go @@ -80,10 +80,6 @@ func Push(archive *ctf.ComponentArchive, r *Remote, log *zap.SugaredLogger) erro return nil } -func Pull() { - // TODO -} - // UserPass splits the credentials string into user and password. // If the string is empty or can't be split, it returns 2 empty strings. func (r *Remote) UserPass() (string, string) { diff --git a/pkg/module/security_scan.go b/pkg/module/security_scan.go new file mode 100644 index 000000000..793713a4f --- /dev/null +++ b/pkg/module/security_scan.go @@ -0,0 +1,162 @@ +package module + +import ( + "encoding/json" + "errors" + "fmt" + v2 "github.com/gardener/component-spec/bindings-go/apis/v2" + "github.com/gardener/component-spec/bindings-go/ctf" + "github.com/mandelsoft/vfs/pkg/vfs" + "os" + "path" + "sigs.k8s.io/yaml" + "strings" +) + +var ErrFailedToParseImageURL = errors.New("error parsing protecode image URL") + +const ( + secConfigFileName = "sec-scanners-config.yaml" + secScanLabelKey = "scan.security.kyma-project.io" + secLabelKey = "security.kyma-project.io" + secScanEnabled = "enabled" +) + +var labelTemplate = "%s." + secScanLabelKey +var globalLabelTemplate = "%s." + secLabelKey + +func AddSecurityScanningMetadata(descriptor *v2.ComponentDescriptor, modDef *Definition, fs vfs.FileSystem, + securityConfigPath string, +) error { + //parse security config file + config, err := parseSecurityScanConfig(securityConfigPath) + if err != nil { + return err + } + excludedWhitesourcePathPatterns := strings.Join(config.WhiteSource.Exclude, ",") + + // add security scan enabled global label + err = appendLabelToAccessor(descriptor, "scan", secScanEnabled, globalLabelTemplate) + if err != nil { + return err + } + if len(descriptor.Sources) == 0 { + return errors.New("found no sources in component descriptor") + } + //add whitesource sec scan labels + for srcIdx := range descriptor.Sources { + src := &descriptor.Sources[srcIdx] + err := appendLabelToAccessor(src, "language", config.WhiteSource.Language, labelTemplate) + if err != nil { + return err + } + err = appendLabelToAccessor(src, "subprojects", config.WhiteSource.SubProjects, labelTemplate) + if err != nil { + return err + } + err = appendLabelToAccessor(src, "exclude", excludedWhitesourcePathPatterns, labelTemplate) + if err != nil { + return err + } + } + //add protecode sec scan images + if err := appendProtecodeImagesLayers(descriptor, config); err != nil { + return err + } + + return WriteComponentDescriptor(fs, descriptor, modDef.ArchivePath, + ctf.ComponentDescriptorFileName) +} + +func appendProtecodeImagesLayers(descriptor *v2.ComponentDescriptor, config *SecurityScanCfg) error { + for _, imageURL := range config.Protecode { + imageName, err := getImageName(imageURL) + if err != nil { + return err + } + + imageTypeLabel, err := generateOCMLabel("type", "third-party-image", labelTemplate) + if err != nil { + return err + } + imageLayerMetadata := v2.IdentityObjectMeta{ + Name: imageName, + Type: v2.OCIImageType, + Version: descriptor.Version, + Labels: []v2.Label{imageTypeLabel}, + } + imageLayerAccess, err := v2.NewUnstructured(v2.NewOCIRegistryAccess(imageURL)) + if err != nil { + return err + } + protecodeImageLayerResource := v2.Resource{ + IdentityObjectMeta: imageLayerMetadata, + Relation: v2.ExternalRelation, + Access: &imageLayerAccess, + } + descriptor.Resources = append(descriptor.Resources, protecodeImageLayerResource) + } + return nil +} + +func generateOCMLabel(prefix, value, tpl string) (v2.Label, error) { + labelValue, err := json.Marshal(map[string]string{ + prefix: value, + }) + if err != nil { + return v2.Label{}, err + } + return v2.Label{ + Name: fmt.Sprintf(tpl, prefix), + Value: labelValue, + }, nil +} + +func getImageName(imageURL string) (string, error) { + imageTagSlice := strings.Split(imageURL, ":") + if len(imageTagSlice) != 2 { + return "", ErrFailedToParseImageURL + } + repoImageSlice := strings.Split(imageTagSlice[0], "/") + l := len(repoImageSlice) + if l == 0 { + return "", ErrFailedToParseImageURL + } + + return repoImageSlice[l-1], nil +} + +type SecurityScanCfg struct { + ModuleName string `json:"module-name"` + Protecode []string `json:"protecode"` + WhiteSource WhiteSourceSecCfg `json:"whitesource"` +} +type WhiteSourceSecCfg struct { + Language string `json:"language"` + SubProjects string `json:"subprojects"` + Exclude []string `json:"exclude"` +} + +func parseSecurityScanConfig(securityConfigPath string) (*SecurityScanCfg, error) { + configFilePath := path.Join(securityConfigPath, secConfigFileName) + fileBytes, err := os.ReadFile(configFilePath) + if err != nil { + return nil, err + } + secCfg := &SecurityScanCfg{} + if err := yaml.Unmarshal(fileBytes, secCfg); err != nil { + return nil, err + } + return secCfg, nil +} + +func appendLabelToAccessor(labeled v2.LabelsAccessor, prefix, value, tpl string) error { + labels := labeled.GetLabels() + labelValue, err := generateOCMLabel(prefix, value, tpl) + if err != nil { + return err + } + labels = append(labels, labelValue) + labeled.SetLabels(labels) + return nil +} diff --git a/pkg/module/template.go b/pkg/module/template.go index f97ce40b2..ccb1eaa6a 100644 --- a/pkg/module/template.go +++ b/pkg/module/template.go @@ -95,7 +95,7 @@ func Template(archive *ctf.ComponentArchive, channel string, data []byte, regist return w.Bytes(), nil } -// indent prepends the given number of whitespaces to eachline in the given string +// Indent prepends the given number of whitespaces to eachline in the given string func Indent(n int, in string) string { out := strings.Builder{}