From fd4a171d435063f852d4c5699ff561c95c5fdfa7 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Thu, 19 Dec 2024 14:57:24 +0200 Subject: [PATCH 1/4] Add `mindev ruletype init` to kick off a rule type This helps folks set up the basic skeleton for ruletype writing. Signed-off-by: Juan Antonio Osorio --- cmd/dev/app/rule_type/init.go | 188 ++++++++++++++++++++++++++++++ cmd/dev/app/rule_type/ruletype.go | 1 + 2 files changed, 189 insertions(+) create mode 100644 cmd/dev/app/rule_type/init.go diff --git a/cmd/dev/app/rule_type/init.go b/cmd/dev/app/rule_type/init.go new file mode 100644 index 0000000000..0a6e41ddea --- /dev/null +++ b/cmd/dev/app/rule_type/init.go @@ -0,0 +1,188 @@ +// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package rule_type + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + + "github.com/spf13/cobra" +) + +// CmdInit is the command for initializing a rule type definition +func CmdInit() *cobra.Command { + initCmd := &cobra.Command{ + Use: "init", + Short: "initialize a rule type definition", + Long: `The 'ruletype init' subcommand allows you to initialize a rule type definition + +The first positional argument is the directory to initialize the rule type in. +The rule type will be initialized in the current directory if no directory is provided. +`, + RunE: initCmdRun, + SilenceUsage: true, + } + + initCmd.Flags().StringP("name", "n", "", "name of the rule type") + initCmd.Flags().BoolP("skip-tests", "s", false, "skip creating test files") + + if err := initCmd.MarkFlagRequired("name"); err != nil { + fmt.Fprintf(os.Stderr, "Error marking flag as required: %s\n", err) + os.Exit(1) + } + + return initCmd +} + +func initCmdRun(cmd *cobra.Command, args []string) error { + name := cmd.Flag("name").Value.String() + skipTests := cmd.Flag("skip-tests").Value.String() == "true" + dir := "." + if len(args) > 0 { + dir = args[0] + } + + if err := validateRuleTypeName(name); err != nil { + return err + } + + ruleTypeFileName := filepath.Join(dir, name+".yaml") + ruleTypeTestFileName := filepath.Join(dir, name+".test.yaml") + ruleTypeTestDataDirName := filepath.Join(dir, name+".testdata") + + if err := assertFilesDontExist( + ruleTypeFileName, ruleTypeTestFileName, ruleTypeTestDataDirName); err != nil { + return err + } + + // Create rule type file + if err := createRuleTypeFile(ruleTypeFileName, name); err != nil { + return err + } + cmd.Printf("Created rule type file: %s\n", ruleTypeFileName) + + if !skipTests { + // Create rule type test file + if err := createRuleTypeTestFile(ruleTypeTestFileName); err != nil { + return err + } + cmd.Printf("Created rule type test file: %s\n", ruleTypeTestFileName) + + // Create rule type test data directory + if err := createRuleTypeTestDataDir(ruleTypeTestDataDirName); err != nil { + return err + } + cmd.Printf("Created rule type test data directory: %s\n", ruleTypeTestDataDirName) + } + + return nil +} + +func validateRuleTypeName(name string) error { + if name == "" { + return errors.New("name cannot be empty") + } + + validName := regexp.MustCompile(`^[a-zA-Z0-9_]+$`) + + // regexp to validate name + if !validName.MatchString(name) { + return errors.New("name must only contain alphanumeric characters and underscores") + } + + return nil +} + +func assertFilesDontExist(files ...string) error { + for _, file := range files { + if _, err := os.Stat(file); err == nil { + + return fmt.Errorf("file %s already exists", file) + } + } + + return nil +} + +func createRuleTypeFile(fileName, name string) error { + return createFileWithContent(fileName, fmt.Sprintf(`--- +version: v1 +release_phase: alpha +type: rule-type +name: %s +display_name: # Display name for the rule type +short_failure_message: # Short message to display when the rule fails +severity: + value: medium +context: {} +description: | # Description of the rule type +guidance: | # Guidance for the rule type. This helps users understand how to fix the issue. +def: + in_entity: repository # The entity type the rule applies to + rule_schema: {} + ingest: + type: git + git: + eval: + type: rego + rego: + type: deny-by-default + def: | + package minder + + import rego.v1 + + default allow := false + + allow if { + true + } + + message := "This is a test message" +`, name)) +} + +func createRuleTypeTestFile(fileName string) error { + return createFileWithContent(fileName, `--- +tests: + - name: "TEST NAME GOES HERE"" + def: {} + params: {} + expect: "pass" + entity: &test-repo + type: repository + entity: + owner: "coolhead" + name: "haze-wave" + # http: + # body_file: HTTP_BODY_FILE + # git: + # repo_base: REPO_BASE_PATH +`) +} + +func createRuleTypeTestDataDir(dirName string) error { + if err := os.Mkdir(dirName, 0750); err != nil { + return fmt.Errorf("error creating directory %s: %w", dirName, err) + } + + return nil +} + +func createFileWithContent(fileName, content string) error { + file, err := os.Create(filepath.Clean(fileName)) + if err != nil { + return fmt.Errorf("error creating file %s: %w", fileName, err) + } + defer file.Close() + + if _, err := file.WriteString(content); err != nil { + return fmt.Errorf("error writing to file %s: %w", fileName, err) + } + + return nil +} diff --git a/cmd/dev/app/rule_type/ruletype.go b/cmd/dev/app/rule_type/ruletype.go index be08c95847..d86e1ed61f 100644 --- a/cmd/dev/app/rule_type/ruletype.go +++ b/cmd/dev/app/rule_type/ruletype.go @@ -16,6 +16,7 @@ func CmdRuleType() *cobra.Command { rtCmd.AddCommand(CmdTest()) rtCmd.AddCommand(CmdLint()) rtCmd.AddCommand(CmdValidateUpdate()) + rtCmd.AddCommand(CmdInit()) return rtCmd } From 4e9c49959dbcb44a3e0021c50de4f60d5c60d2e2 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Tue, 7 Jan 2025 09:18:26 +0200 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Evan Anderson --- cmd/dev/app/rule_type/init.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/dev/app/rule_type/init.go b/cmd/dev/app/rule_type/init.go index 0a6e41ddea..ededb975d5 100644 --- a/cmd/dev/app/rule_type/init.go +++ b/cmd/dev/app/rule_type/init.go @@ -158,9 +158,16 @@ tests: entity: owner: "coolhead" name: "haze-wave" + # When testing a rule, additional content can be supplied + # from files in the `{{ .RuleName }}.testdata` directory. + # File paths below are relative to this directory. # http: - # body_file: HTTP_BODY_FILE + # # Input from the `http` ingest type. + # body_file: HTTP_BODY_FILE # git: + # # Input from the `git` ingest type. Base paths contain + # # directory contents, but do not actually need to be a + # # git repository. # repo_base: REPO_BASE_PATH `) } From e20c2efb1508fc9da22695407206dcf3710d1fe2 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Tue, 7 Jan 2025 09:52:25 +0200 Subject: [PATCH 3/4] More fixes Signed-off-by: Juan Antonio Osorio --- cmd/dev/app/rule_type/init.go | 46 +++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/cmd/dev/app/rule_type/init.go b/cmd/dev/app/rule_type/init.go index ededb975d5..c7c078f87e 100644 --- a/cmd/dev/app/rule_type/init.go +++ b/cmd/dev/app/rule_type/init.go @@ -4,11 +4,13 @@ package rule_type import ( + "bytes" "errors" "fmt" "os" "path/filepath" "regexp" + "text/template" "github.com/spf13/cobra" ) @@ -67,7 +69,7 @@ func initCmdRun(cmd *cobra.Command, args []string) error { if !skipTests { // Create rule type test file - if err := createRuleTypeTestFile(ruleTypeTestFileName); err != nil { + if err := createRuleTypeTestFile(ruleTypeTestFileName, name); err != nil { return err } cmd.Printf("Created rule type test file: %s\n", ruleTypeTestFileName) @@ -87,7 +89,7 @@ func validateRuleTypeName(name string) error { return errors.New("name cannot be empty") } - validName := regexp.MustCompile(`^[a-zA-Z0-9_]+$`) + validName := regexp.MustCompile(`^[A-Za-z][-/[:word:]]*$`) // regexp to validate name if !validName.MatchString(name) { @@ -99,21 +101,25 @@ func validateRuleTypeName(name string) error { func assertFilesDontExist(files ...string) error { for _, file := range files { - if _, err := os.Stat(file); err == nil { - + _, err := os.Stat(file) + if err == nil { return fmt.Errorf("file %s already exists", file) } + + if errors.Is(err, os.ErrPermission) { + return fmt.Errorf("permission denied for file %s", file) + } } return nil } func createRuleTypeFile(fileName, name string) error { - return createFileWithContent(fileName, fmt.Sprintf(`--- + return createFileWithContent(fileName, renderwithRuleTypeName(`--- version: v1 release_phase: alpha type: rule-type -name: %s +name: {{ .RuleName }} display_name: # Display name for the rule type short_failure_message: # Short message to display when the rule fails severity: @@ -146,8 +152,8 @@ def: `, name)) } -func createRuleTypeTestFile(fileName string) error { - return createFileWithContent(fileName, `--- +func createRuleTypeTestFile(fileName, name string) error { + return createFileWithContent(fileName, renderwithRuleTypeName(`--- tests: - name: "TEST NAME GOES HERE"" def: {} @@ -159,17 +165,17 @@ tests: owner: "coolhead" name: "haze-wave" # When testing a rule, additional content can be supplied - # from files in the `{{ .RuleName }}.testdata` directory. + # from files in the "{{ .RuleName }}.testdata" directory. # File paths below are relative to this directory. # http: - # # Input from the `http` ingest type. + # # Input from the "http" ingest type. # body_file: HTTP_BODY_FILE # git: - # # Input from the `git` ingest type. Base paths contain + # # Input from the "git" ingest type. Base paths contain # # directory contents, but do not actually need to be a # # git repository. # repo_base: REPO_BASE_PATH -`) +`, name)) } func createRuleTypeTestDataDir(dirName string) error { @@ -181,15 +187,19 @@ func createRuleTypeTestDataDir(dirName string) error { } func createFileWithContent(fileName, content string) error { - file, err := os.Create(filepath.Clean(fileName)) + return os.WriteFile(fileName, []byte(content), 0644) +} + +func renderwithRuleTypeName(templ, name string) string { + tmpl, err := template.New("ruletype").Parse(templ) if err != nil { - return fmt.Errorf("error creating file %s: %w", fileName, err) + panic(err) } - defer file.Close() - if _, err := file.WriteString(content); err != nil { - return fmt.Errorf("error writing to file %s: %w", fileName, err) + var buf bytes.Buffer + if err := tmpl.Execute(&buf, struct{ RuleName string }{name}); err != nil { + panic(err) } - return nil + return buf.String() } From d5a7798d41429f89712480bddc369d01313c3425 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Thu, 9 Jan 2025 14:54:13 +0200 Subject: [PATCH 4/4] address lint issue Signed-off-by: Juan Antonio Osorio --- cmd/dev/app/rule_type/init.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/dev/app/rule_type/init.go b/cmd/dev/app/rule_type/init.go index c7c078f87e..90e1048a63 100644 --- a/cmd/dev/app/rule_type/init.go +++ b/cmd/dev/app/rule_type/init.go @@ -187,7 +187,7 @@ func createRuleTypeTestDataDir(dirName string) error { } func createFileWithContent(fileName, content string) error { - return os.WriteFile(fileName, []byte(content), 0644) + return os.WriteFile(fileName, []byte(content), 0600) } func renderwithRuleTypeName(templ, name string) string {