Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 154 additions & 52 deletions cmd/generate-plugin.go
Original file line number Diff line number Diff line change
@@ -1,43 +1,53 @@
package cmd

import (
"bytes"
"errors"
"fmt"
"html/template"
"io"
"os"
"path/filepath"
"strings"

"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"))
Expand All @@ -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
}

Expand All @@ -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") != "" {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
2 changes: 1 addition & 1 deletion cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}

Expand Down
9 changes: 4 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand All @@ -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
10 changes: 4 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
Loading