diff --git a/cmd/generate-plugin.go b/cmd/generate-plugin.go index ea24969..03609bc 100644 --- a/cmd/generate-plugin.go +++ b/cmd/generate-plugin.go @@ -1,8 +1,11 @@ package cmd import ( + "bytes" + "errors" "fmt" "html/template" + "io" "os" "path/filepath" "strings" @@ -10,34 +13,41 @@ import ( "github.com/go-git/go-git/v5" "github.com/spf13/cobra" "github.com/spf13/viper" + "gopkg.in/yaml.v3" - "github.com/revanite-io/sci/pkg/layer2" + "github.com/ossf/gemara/layer2" + sdkutils "github.com/privateerproj/privateer-sdk/utils" ) type CatalogData struct { layer2.Catalog - ServiceName string - TestSuites map[string][]string + ServiceName string + Requirements []string + ApplicabilityCategories []string + StrippedName string } -var TemplatesDir string -var SourcePath string -var OutputDir string - -// versionCmd represents the version command -var genPluginCmd = &cobra.Command{ - Use: "generate-plugin", - Short: "Generate a new plugin", - Run: func(cmd *cobra.Command, args []string) { - generatePlugin() - }, -} +var ( + TemplatesDir string + SourcePath string + OutputDir string + ServiceName string + + // versionCmd represents the version command + genPluginCmd = &cobra.Command{ + Use: "generate-plugin", + Short: "Generate a new plugin", + Run: func(cmd *cobra.Command, args []string) { + generatePlugin() + }, + } +) func init() { - genPluginCmd.PersistentFlags().StringP("source-path", "p", "", "The source file to generate the plugin from.") - genPluginCmd.PersistentFlags().StringP("local-templates", "", "", "Path to a directory to use instead of downloading the latest templates.") - genPluginCmd.PersistentFlags().StringP("service-name", "n", "", "The name of the service (e.g. 'ECS, AKS, GCS').") - genPluginCmd.PersistentFlags().StringP("output-dir", "o", "generated-plugin/", "Pathname for the generated plugin.") + genPluginCmd.PersistentFlags().StringP("source-path", "p", "", "The source file to generate the plugin from") + genPluginCmd.PersistentFlags().StringP("local-templates", "", "", "Path to a directory to use instead of downloading the latest templates") + genPluginCmd.PersistentFlags().StringP("service-name", "n", "", "The name of the service (e.g. 'ECS, AKS, GCS')") + genPluginCmd.PersistentFlags().StringP("output-dir", "o", "generated-plugin/", "Pathname for the generated plugin") _ = viper.BindPFlag("source-path", genPluginCmd.PersistentFlags().Lookup("source-path")) _ = viper.BindPFlag("local-templates", genPluginCmd.PersistentFlags().Lookup("local-templates")) @@ -53,14 +63,18 @@ func generatePlugin() { logger.Error(err.Error()) return } - data, err := readData() + data := CatalogData{} + data.ServiceName = ServiceName + + err = data.LoadFile("file://" + SourcePath) if err != nil { logger.Error(err.Error()) return } - data.ServiceName = viper.GetString("service-name") - if data.ServiceName == "" { - logger.Error("--service-name is required to generate a plugin.") + + err = data.getAssessmentRequirements() + if err != nil { + logger.Error(err.Error()) return } @@ -83,12 +97,22 @@ func generatePlugin() { if err != nil { logger.Error("Error walking through templates directory: %s", err) } + + err = writeCatalogFile(&data.Catalog) + if err != nil { + logger.Error("Failed to write catalog to file: %s", err) + } } func setupTemplatingEnvironment() error { SourcePath = viper.GetString("source-path") if SourcePath == "" { - return fmt.Errorf("--source-path is required to generate a plugin from a control set from local file or URL") + return fmt.Errorf("required: --servicesource-path is required to generate a plugin from a control set from local file or URL") + } + + ServiceName = viper.GetString("service-name") + if ServiceName == "" { + return fmt.Errorf("required: --serviceservice-name is required to generate a plugin") } if viper.GetString("local-templates") != "" { @@ -130,26 +154,36 @@ func generateFileFromTemplate(data CatalogData, templatePath, OutputDir string) return fmt.Errorf("error reading template file %s: %w", templatePath, err) } + // Determine relative path from templates dir so we can preserve subdirs in output + relativePath, err := filepath.Rel(TemplatesDir, templatePath) + if err != nil { + return fmt.Errorf("error calculating relative path for %s: %w", templatePath, err) + } + + // If the template is not a text template, copy it over as-is (preserve mode) + if filepath.Ext(templatePath) != ".txt" { + return copyNonTemplateFile(templatePath, filepath.Join(OutputDir, relativePath)) + } + tmpl, err := template.New("plugin").Funcs(template.FuncMap{ - "as_text": func(s string) template.HTML { - s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " ")) - return template.HTML(s) + "as_text": func(in string) template.HTML { + return template.HTML( + strings.TrimSpace( + strings.ReplaceAll(in, "\n", " "))) }, - "as_id": func(s string) string { - return strings.TrimSpace( - strings.ReplaceAll( - strings.ReplaceAll(s, ".", "_"), "-", "_")) + "default": func(in string, out string) string { + if in != "" { + return in + } + return out }, + "snake_case": snakeCase, + "simplifiedName": simplifiedName, }).Parse(string(templateContent)) if err != nil { return fmt.Errorf("error parsing template file %s: %w", templatePath, err) } - relativePath, err := filepath.Rel(TemplatesDir, templatePath) - if err != nil { - return err - } - outputPath := filepath.Join(OutputDir, strings.TrimSuffix(relativePath, ".txt")) err = os.MkdirAll(filepath.Dir(outputPath), os.ModePerm) @@ -177,26 +211,94 @@ func generateFileFromTemplate(data CatalogData, templatePath, OutputDir string) return nil } -func readData() (data CatalogData, err error) { - err = data.LoadControlFamiliesFile(SourcePath) +func (c *CatalogData) getAssessmentRequirements() error { + for _, family := range c.ControlFamilies { + for _, control := range family.Controls { + for _, requirement := range control.AssessmentRequirements { + c.Requirements = append(c.Requirements, requirement.Id) + // Add applicability categories if unique + for _, a := range requirement.Applicability { + if !sdkutils.StringSliceContains(c.ApplicabilityCategories, a) { + c.ApplicabilityCategories = append(c.ApplicabilityCategories, a) + } + } + } + } + } + if len(c.Requirements) == 0 { + return errors.New("no requirements retrieved from catalog") + } + return nil +} + +func writeCatalogFile(catalog *layer2.Catalog) error { + var b bytes.Buffer + yamlEncoder := yaml.NewEncoder(&b) + yamlEncoder.SetIndent(2) // this is the line that sets the indentation + err := yamlEncoder.Encode(catalog) if err != nil { - return + return fmt.Errorf("error marshaling YAML: %w", err) } - data.TestSuites = make(map[string][]string) + dirPath := filepath.Join(OutputDir, "data", simplifiedName(catalog.Metadata.Id, catalog.Metadata.Version)) + filePath := filepath.Join(dirPath, "catalog.yaml") - for i, family := range data.ControlFamilies { - for j := range family.Controls { - for _, testReq := range data.ControlFamilies[i].Controls[j].Requirements { - // Add the test ID to the TestSuites map for each TLP level - for _, tlpLevel := range testReq.Applicability { - if data.TestSuites[tlpLevel] == nil { - data.TestSuites[tlpLevel] = []string{} - } - data.TestSuites[tlpLevel] = append(data.TestSuites[tlpLevel], testReq.ID) - } - } + err = os.MkdirAll(dirPath, os.ModePerm) + if err != nil { + return fmt.Errorf("error creating directories for %s: %w", filePath, err) + } + + if err := os.WriteFile(filePath, b.Bytes(), 0644); err != nil { + return fmt.Errorf("error writing YAML file: %w", err) + } + + return nil +} + +func snakeCase(in string) string { + return strings.TrimSpace( + strings.ReplaceAll( + strings.ReplaceAll(in, ".", "_"), "-", "_")) +} + +func simplifiedName(catalogId string, catalogVersion string) string { + return fmt.Sprintf("%s_%s", snakeCase(catalogId), snakeCase(catalogVersion)) +} + +func copyNonTemplateFile(templatePath, relativePath string) error { + outputPath := filepath.Join(OutputDir, relativePath) + if err := os.MkdirAll(filepath.Dir(outputPath), os.ModePerm); err != nil { + return fmt.Errorf("error creating directories for %s: %w", outputPath, err) + } + + // Copy file contents + srcFile, err := os.Open(templatePath) + if err != nil { + return fmt.Errorf("error opening source file %s: %w", templatePath, err) + } + defer func() { + err := srcFile.Close() + if err != nil { + logger.Error("error closing output file %s: %w", templatePath, err) } + }() + + dstFile, err := os.Create(outputPath) + if err != nil { + return fmt.Errorf("error creating destination file %s: %w", outputPath, err) } - return + defer func() { + _ = dstFile.Close() + }() + + if _, err := io.Copy(dstFile, srcFile); err != nil { + return fmt.Errorf("error copying file to %s: %w", outputPath, err) + } + + // Try to preserve file mode from source + if fi, err := os.Stat(templatePath); err == nil { + _ = os.Chmod(outputPath, fi.Mode()) + } + + return nil } diff --git a/cmd/install.go b/cmd/install.go index 0751c41..3b6f994 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -20,6 +20,6 @@ var installCmd = &cobra.Command{ } func init() { - installCmd.PersistentFlags().BoolP("store", "s", false, "Github repo to source the plugin from.") + installCmd.PersistentFlags().BoolP("store", "s", false, "Github repo to source the plugin from") _ = viper.BindPFlag("store", installCmd.PersistentFlags().Lookup("store")) } diff --git a/cmd/list.go b/cmd/list.go index 5b3e222..5de9b39 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -44,7 +44,7 @@ var listCmd = &cobra.Command{ func init() { rootCmd.AddCommand(listCmd) - listCmd.PersistentFlags().BoolP("all", "a", false, "Review the Fleet! List all plugins that have been installed or requested in the current config.") + listCmd.PersistentFlags().BoolP("all", "a", false, "Review the Fleet! List all plugins that have been installed or requested in the current config") _ = viper.BindPFlag("all", listCmd.PersistentFlags().Lookup("all")) } diff --git a/go.mod b/go.mod index d2d8939..9ad0003 100644 --- a/go.mod +++ b/go.mod @@ -8,10 +8,11 @@ require ( github.com/go-git/go-git/v5 v5.16.3 github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.7.0 - github.com/privateerproj/privateer-sdk v1.6.0 - github.com/revanite-io/sci v0.1.8 + github.com/ossf/gemara v0.12.1 + github.com/privateerproj/privateer-sdk v1.9.0 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -38,7 +39,6 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/oklog/run v1.1.0 // indirect - github.com/ossf/gemara v0.10.1 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect @@ -61,8 +61,7 @@ require ( google.golang.org/grpc v1.70.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) // For SDK Development Only -// replace github.com/privateerproj/privateer-sdk => ./privateer-sdk +// replace github.com/privateerproj/privateer-sdk => ../privateer-sdk diff --git a/go.sum b/go.sum index ba8bf33..e5cc60f 100644 --- a/go.sum +++ b/go.sum @@ -94,8 +94,8 @@ github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/ossf/gemara v0.10.1 h1:rvM8s/dAqF0QkCtztwgx92o/hWukRdS4rzsTpRT9chY= -github.com/ossf/gemara v0.10.1/go.mod h1:FRRem1gQ9m+c3QiBLN/PkL/RfzyNpF3aO7AWqZVzerg= +github.com/ossf/gemara v0.12.1 h1:Cyiytndw3HnyrctXE/iV4OzZURwypie2lmI7bf1bLAs= +github.com/ossf/gemara v0.12.1/go.mod h1:rY4YvaWvOSJthTE2jHudjwcCRIQ31Y7GpEc3pyJPIPM= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= @@ -105,10 +105,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/privateerproj/privateer-sdk v1.6.0 h1:lljDUiesQEhgSH/6ZX+LRu+DeJPj1wHBJUAj+A6PAbc= -github.com/privateerproj/privateer-sdk v1.6.0/go.mod h1:jNQQqTxvEnQBvR/BuRrbxMt8wxe7fX6mOC7PBTYknVI= -github.com/revanite-io/sci v0.1.8 h1:JmVHJu2TX42WlNEVtOufxmyCi8PYXEZmXfk2ClfYsnI= -github.com/revanite-io/sci v0.1.8/go.mod h1:KNBMtb28TKYJ0aq6P0jX1XaIBYQdAziTvnI7uU2H+5Q= +github.com/privateerproj/privateer-sdk v1.9.0 h1:nBsbMBPJKU0fay5Lj2VBd07+I0W6+tl658SV4eMQqo8= +github.com/privateerproj/privateer-sdk v1.9.0/go.mod h1:ngK2WiDbMywbUqji2X24Bbs4GMjK2j5vrjqgl2thBlo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/test/data/CCC.VPC_2025.01.yaml b/test/data/OSPS_Baseline_AC_2025_02.yaml similarity index 100% rename from test/data/CCC.VPC_2025.01.yaml rename to test/data/OSPS_Baseline_AC_2025_02.yaml