From 9c142cc89ea999260fc7f53ba43e459e98174521 Mon Sep 17 00:00:00 2001 From: NoamSDahan Date: Thu, 17 Aug 2023 18:00:45 +0300 Subject: [PATCH 1/4] Add functionality to import scenarios from a local directory - Introduce `import-scenarios` subcommand to import scenarios from a specified local directory. - Implement the `ImportScenarios` method in `registry.go`. --- .../maintenance/commands/import_scenarios.go | 41 +++++++++++++++++++ .../commands/maintenance/maintenanace.go | 1 + registry.go | 4 ++ 3 files changed, 46 insertions(+) create mode 100644 cmd/commands/commands/maintenance/commands/import_scenarios.go diff --git a/cmd/commands/commands/maintenance/commands/import_scenarios.go b/cmd/commands/commands/maintenance/commands/import_scenarios.go new file mode 100644 index 0000000..04da99d --- /dev/null +++ b/cmd/commands/commands/maintenance/commands/import_scenarios.go @@ -0,0 +1,41 @@ +package commands + +import ( + "fmt" + cnappgoat "github.com/ermetic-research/CNAPPgoat" + "github.com/ermetic-research/CNAPPgoat/cmd/commands/common" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var ImportScenariosCommand = &cli.Command{ + Name: "import-scenarios", + Usage: "maintenance subcommand to import scenarios from a local directory to the CNAPPgoat registry", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "directory", + Usage: "Directory to import scenarios from", + Aliases: []string{"d"}, + Value: "", + }, + }, + Before: func(c *cli.Context) error { + if err := common.CommandBefore(c); err != nil { + return err + } + + if c.String("directory") == "" { + return fmt.Errorf("directory flag is required in import-scenarios subcommand") + } + + return nil + }, + Action: func(c *cli.Context) error { + registry := c.Context.Value("CNAPPgoatModuleRegistry").(*cnappgoat.Registry) + logrus.Infof("importing scenarios from %s", c.String("directory")) + if _, err := registry.ImportScenarios(c.String("directory")); err != nil { + return err + } + return nil + }, +} diff --git a/cmd/commands/commands/maintenance/maintenanace.go b/cmd/commands/commands/maintenance/maintenanace.go index c680d2b..c7a9cd0 100644 --- a/cmd/commands/commands/maintenance/maintenanace.go +++ b/cmd/commands/commands/maintenance/maintenanace.go @@ -20,6 +20,7 @@ var MaintenanceCommand = &cli.Command{ plugins.PluginsCommand, stacks.StacksCommand, commands.GetScenariosCommand, + commands.ImportScenariosCommand, }, Hidden: true, } diff --git a/registry.go b/registry.go index 67d6bbb..070c728 100644 --- a/registry.go +++ b/registry.go @@ -117,6 +117,10 @@ func (r *Registry) SetState(scenario *Scenario, state State) error { return r.storage.WriteStateToFile(r.scenarios[scenario.ScenarioParams.ID], state) } +func (r *Registry) ImportScenarios(path string) (map[string]*Scenario, error) { + return r.storage.updateScenariosFromFolder(path) +} + func sortScenarios(scenarios []*Scenario) { sort.Slice(scenarios, func(i, j int) bool { return scenarios[i].ScenarioParams.ID < scenarios[j].ScenarioParams.ID From ed1e5364415b6daa9dd8992cf8520e8061ce600e Mon Sep 17 00:00:00 2001 From: NoamSDahan Date: Sun, 20 Aug 2023 16:42:46 +0300 Subject: [PATCH 2/4] - change typo in name of maintenance.go --- .../commands/maintenance/{maintenanace.go => maintenace.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmd/commands/commands/maintenance/{maintenanace.go => maintenace.go} (100%) diff --git a/cmd/commands/commands/maintenance/maintenanace.go b/cmd/commands/commands/maintenance/maintenace.go similarity index 100% rename from cmd/commands/commands/maintenance/maintenanace.go rename to cmd/commands/commands/maintenance/maintenace.go From d277a7096b2af18503cde42df2c0b31e36a4ffe4 Mon Sep 17 00:00:00 2001 From: NoamSDahan Date: Sun, 20 Aug 2023 17:07:30 +0300 Subject: [PATCH 3/4] - add debug flag to import-scenarios command - improve logging around scenario update --- .../commands/maintenance/commands/import_scenarios.go | 5 +++++ localstorage.go | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cmd/commands/commands/maintenance/commands/import_scenarios.go b/cmd/commands/commands/maintenance/commands/import_scenarios.go index 04da99d..d0d9d34 100644 --- a/cmd/commands/commands/maintenance/commands/import_scenarios.go +++ b/cmd/commands/commands/maintenance/commands/import_scenarios.go @@ -18,6 +18,11 @@ var ImportScenariosCommand = &cli.Command{ Aliases: []string{"d"}, Value: "", }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug mode", + Value: false, + }, }, Before: func(c *cli.Context) error { if err := common.CommandBefore(c); err != nil { diff --git a/localstorage.go b/localstorage.go index 313e06d..a35280f 100644 --- a/localstorage.go +++ b/localstorage.go @@ -201,14 +201,15 @@ func (l *LocalStorage) updateScenariosFromFolder(scenariosFullPath string) (map[ return nil, fmt.Errorf("unable to copy scenario to working directory: %w", err) } scenariosFromWorkDir[scenarioFromScenariosDir.ScenarioParams.ID] = scenarioFromScenariosDir + logrus.Infof("scenario %v exists in the working directory but has changed. Updating.", scenarioFromWorkDir.ScenarioParams.ID) } else { - logrus.Debugf("Scenario %v exists in the working directory and has not changed. Skipping.", scenarioFromWorkDir.ScenarioParams.ID) + logrus.Debugf("scenario %v exists in the working directory and has not changed. Skipping.", scenarioFromWorkDir.ScenarioParams.ID) } } } if !exists { // if the scenario does not exist in the working directory, copy it over - logrus.Debugf("Scenario %v does not exist in the working directory. Copying over.", scenarioFromScenariosDir.ScenarioParams.ID) + logrus.Infof("scenario %v does not exist in the working directory. Copying over.", scenarioFromScenariosDir.ScenarioParams.ID) err = l.copyScenario(scenarioFromScenariosDir) if err != nil { return nil, fmt.Errorf("unable to copy scenario to working directory: %w", err) From 600b3719202c4c7f91a98401c036b1e0b8be714d Mon Sep 17 00:00:00 2001 From: NoamSDahan Date: Mon, 21 Aug 2023 18:35:04 +0300 Subject: [PATCH 4/4] Enhance Scenario Validation and Error Handling This commit enhances scenario validation and error handling across the `localstorage.go` and `scenario.go` files. The key updates include: - Introduced validation checks for `scenarioParams` within the `ReadCnappGoatConfig`, `updateScenariosFromFolder`, and `createScenario` methods in `localstorage.go`. - Enhanced error messages in the `scenario.go` file for improved clarity. - Refactored the panic error handling in the `Module` and `Platform` string representation functions to return an empty string instead. - Improved the unmarshalling logic for runtime options. - Added a comprehensive `IsValid` function for scenario parameters to ensure all necessary fields are present and correctly formatted. --- localstorage.go | 9 ++++++++ scenario.go | 59 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 4 deletions(-) diff --git a/localstorage.go b/localstorage.go index a35280f..30250a6 100644 --- a/localstorage.go +++ b/localstorage.go @@ -144,6 +144,9 @@ func (l *LocalStorage) ReadCnappGoatConfig(scenario *Scenario) (map[string]strin if err = yaml.Unmarshal(data, &cnappGoatConfig); err != nil { return nil, fmt.Errorf("failed to parse Pulumi.yaml: %w", err) } + if err := cnappGoatConfig.ScenarioParams.IsValid(); err != nil { + return nil, fmt.Errorf("scenarioParams field is empty for scenario %v", scenario.Name) + } return cnappGoatConfig.ScenarioParams.Config, nil } @@ -179,6 +182,9 @@ func (l *LocalStorage) updateScenariosFromFolder(scenariosFullPath string) (map[ return nil, fmt.Errorf("scenario folder does not exist, cannot perform UpdateScenarioFolder: %s", scenariosFullPath) } scenariosFromScenarioDir, err := l.loadScenarios(scenariosFullPath) + if err != nil { + return nil, fmt.Errorf("unable to load scenarios from scenario directory: %w", err) + } var scenariosFromWorkDir map[string]*Scenario if l.WorkingDirectoryExists() { scenariosFromWorkDir, err = l.loadScenarios(l.WorkingDir) @@ -288,6 +294,9 @@ func (l *LocalStorage) createScenario(path string) (*Scenario, error) { if err = yaml.Unmarshal(data, &scenario); err != nil { return nil, fmt.Errorf("failed to parse Pulumi.yaml: %w", err) } + if err = scenario.ScenarioParams.IsValid(); err != nil { + return nil, fmt.Errorf("scenarioParams field is empty for scenario %v", scenario.Name) + } scenario.SrcDir = filepath.Dir(path) diff --git a/scenario.go b/scenario.go index 3a59099..2194d20 100644 --- a/scenario.go +++ b/scenario.go @@ -2,6 +2,7 @@ package cnappgoat import ( "errors" + "fmt" "strings" "gopkg.in/yaml.v3" @@ -83,7 +84,7 @@ func (m Module) String() string { case IAC: return "IAC" default: - panic("CNAPPgoat module name not formatted") + return "" } } @@ -110,7 +111,7 @@ func (p Platform) String() string { case GCP: return "GCP" default: - panic("platform name not formatted") + return "" } } @@ -143,7 +144,9 @@ func (r *runtime) UnmarshalYAML(unmarshal func(interface{}) error) error { if name, ok := runtimeObj["name"].(string); ok { r.Name = name } - r.Options = runtimeObj + if options, ok := runtimeObj["options"].(map[string]interface{}); ok { + r.Options = options + } return nil } @@ -169,7 +172,7 @@ func ModuleFromString(name string) (Module, error) { case strings.ToLower(IAC.String()): return IAC, nil default: - return "", errors.New("unknown CNAPPgoat module name: " + name) + return "", errors.New("unknown module name: " + name) } } @@ -200,3 +203,51 @@ func StateFromString(state string) (State, error) { return State{}, errors.New("unknown state: " + state) } } + +func (p scenarioParams) IsValid() error { + if p.Module == "" && p.Platform == "" && p.ID == "" && p.FriendlyName == "" && p.Description == "" && p.ScenarioType == "" { + return errors.New("scenarioParams is empty, yaml is missing value cnappgoat-params") + } + if p.Module == "" { + return errors.New("module is required") + } + if p.Platform == "" { + return errors.New("platform is required") + } + if p.ID == "" { + return errors.New("id is required") + } + if len(strings.Split(p.ID, "-")) < 3 { + return fmt.Errorf("id must be in the format of --, id is: %s", p.ID) + } + // module must be valid + if _, err := ModuleFromString(string(p.Module)); err != nil { + return err + } + // platform must be valid + if _, err := PlatformFromString(string(p.Platform)); err != nil { + return err + } + if !p.Module.Equals(Module(strings.Split(p.ID, "-")[0])) { + return fmt.Errorf("id must start with matching module name: %s, id is: %v", p.Module, p.ID) + } + if !p.Platform.Equals(Platform(strings.Split(p.ID, "-")[1])) { + return fmt.Errorf("id must include matching platform name, id is: %v", p.ID) + } + if p.FriendlyName == "" { + return errors.New("friendlyName is required") + } + if p.Description == "" { + return errors.New("description is required") + } + if p.ScenarioType == "" { + return errors.New("scenarioType is required") + } + if _, err := ModuleFromString(string(p.Module)); err != nil { + return err + } + if _, err := PlatformFromString(string(p.Platform)); err != nil { + return err + } + return nil +}