From c6eac9814181c4e7bd4cea717db8d27f471fbeb7 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 24 Sep 2024 06:23:26 -0400 Subject: [PATCH 01/11] feat(compose): initial templating --- docs/oscal/ns/resource-type.md | 26 +++ go.sum | 6 - lula-config.yaml | 23 +- src/cmd/tools/compose.go | 161 ++++++++------ src/cmd/tools/template.go | 18 +- src/cmd/validate/validate.go | 4 +- src/internal/template/template.go | 27 +++ src/pkg/common/common.go | 8 + src/pkg/common/composition/composition.go | 86 +++++--- .../common/composition/composition_test.go | 204 ++++++++++++------ src/pkg/common/composition/options.go | 75 +++++++ src/pkg/common/composition/resource-store.go | 51 ++++- src/pkg/common/network/network.go | 52 ++++- src/pkg/common/types.go | 26 +-- .../e2e/{standard => cmd}/lula-config.yaml | 0 src/test/e2e/cmd/main_test.go | 110 ++++++++++ .../tools/compose/composed-file.golden | 66 ++++++ .../cmd/testdata/tools/compose/help.golden | 30 +++ .../testdata/tools/template}/help.golden | 0 .../tools/template}/validation.golden | 0 .../tools/template}/validation_all.golden | 0 .../template}/validation_constants.golden | 0 .../template}/validation_non_sensitive.golden | 0 .../template}/validation_with_env_vars.golden | 0 .../template}/validation_with_set.golden | 0 src/test/e2e/cmd/tools_compose_test.go | 93 ++++++++ .../tools_template_test.go} | 44 +--- src/test/e2e/standard/testdata/empty.golden | 0 .../component-definition-all-local.yaml | 1 + ...nent-definition-import-nested-compdef.yaml | 18 ++ ...-definition-local-and-remote-template.yaml | 75 +++++++ .../validation.trailing-spaces.yaml | 27 +++ 32 files changed, 986 insertions(+), 245 deletions(-) create mode 100644 docs/oscal/ns/resource-type.md create mode 100644 src/pkg/common/composition/options.go rename src/test/e2e/{standard => cmd}/lula-config.yaml (100%) create mode 100644 src/test/e2e/cmd/main_test.go create mode 100644 src/test/e2e/cmd/testdata/tools/compose/composed-file.golden create mode 100644 src/test/e2e/cmd/testdata/tools/compose/help.golden rename src/test/e2e/{standard/testdata => cmd/testdata/tools/template}/help.golden (100%) rename src/test/e2e/{standard/testdata => cmd/testdata/tools/template}/validation.golden (100%) rename src/test/e2e/{standard/testdata => cmd/testdata/tools/template}/validation_all.golden (100%) rename src/test/e2e/{standard/testdata => cmd/testdata/tools/template}/validation_constants.golden (100%) rename src/test/e2e/{standard/testdata => cmd/testdata/tools/template}/validation_non_sensitive.golden (100%) rename src/test/e2e/{standard/testdata => cmd/testdata/tools/template}/validation_with_env_vars.golden (100%) rename src/test/e2e/{standard/testdata => cmd/testdata/tools/template}/validation_with_set.golden (100%) create mode 100644 src/test/e2e/cmd/tools_compose_test.go rename src/test/e2e/{standard/template_test.go => cmd/tools_template_test.go} (73%) delete mode 100644 src/test/e2e/standard/testdata/empty.golden create mode 100644 src/test/unit/common/composition/component-definition-import-nested-compdef.yaml create mode 100644 src/test/unit/common/composition/component-definition-local-and-remote-template.yaml create mode 100644 src/test/unit/common/validation/validation.trailing-spaces.yaml diff --git a/docs/oscal/ns/resource-type.md b/docs/oscal/ns/resource-type.md new file mode 100644 index 00000000..43712d75 --- /dev/null +++ b/docs/oscal/ns/resource-type.md @@ -0,0 +1,26 @@ +# Resource-Type + +The Back-Matter OSCAL structures are used to store a collection of resources that may be referenced from within the OSCAL document instance. For Lula's purposes, the `resources` stored in the back-matter are used in the following ways: + +- To store the "Lula Validation" artifacts +- To store templated "Lula Validation" artifacts + +To identify these types of resources, the `resource-type` prop is used. + +## Example + +For a valid Lula Validation, the `resource-type` prop would be set to `validation`: +```yaml +props: + - name: resource-type + ns: https://docs.lula.dev/oscal/ns + value: "validation" +``` + +For a templated Lula Validation, the `resource-type` prop would be set to `validation-template`: +```yaml +props: + - name: resource-type + ns: https://docs.lula.dev/oscal/ns + value: "validation-template" +``` diff --git a/go.sum b/go.sum index c08549bd..3f2ee55e 100644 --- a/go.sum +++ b/go.sum @@ -58,16 +58,10 @@ github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqK github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= -github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= -github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/golden v0.0.0-20240919170804-a4978c8e603a h1:IUy+N6nKpGfijckOe8KGnAQwBUT6xz63n3tbb0Gy8aY= github.com/charmbracelet/x/exp/golden v0.0.0-20240919170804-a4978c8e603a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/teatest v0.0.0-20240918160051-227168dc0568 h1:MWPyZsxZMe/oKhkt5j34zAba/2nXOxWuf3CvQqO8SDA= -github.com/charmbracelet/x/exp/teatest v0.0.0-20240918160051-227168dc0568/go.mod h1:NDRRSMP6bZbCs4jyc4i1/4UG4M+0PEiQdpivQgD0Mio= github.com/charmbracelet/x/exp/teatest v0.0.0-20240919170804-a4978c8e603a h1:sS42HbmCab8rCehUwNO/bQEZQoJ6GavhZyO+245mBwA= github.com/charmbracelet/x/exp/teatest v0.0.0-20240919170804-a4978c8e603a/go.mod h1:NDRRSMP6bZbCs4jyc4i1/4UG4M+0PEiQdpivQgD0Mio= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= diff --git a/lula-config.yaml b/lula-config.yaml index da38ff76..0908f49d 100644 --- a/lula-config.yaml +++ b/lula-config.yaml @@ -1 +1,22 @@ -log_level: info \ No newline at end of file +log_level: info + +constants: + type: software + title: lula + + resources: + name: test-pod-label + namespace: validation-test + exemptions: + - one + - two + - three + +variables: + - key: some_lula_secret + sensitive: true + - key: some_env_var + default: this-should-be-overridden + +# log_level: info +target: il5 diff --git a/src/cmd/tools/compose.go b/src/cmd/tools/compose.go index 1668ae4b..ce256ac1 100644 --- a/src/cmd/tools/compose.go +++ b/src/cmd/tools/compose.go @@ -1,7 +1,7 @@ package tools import ( - "errors" + "context" "fmt" "os" "path/filepath" @@ -14,13 +14,6 @@ import ( "github.com/spf13/cobra" ) -type composeFlags struct { - InputFile string // -f --input-file - OutputFile string // -o --output-file -} - -var composeOpts = &composeFlags{} - var composeHelp = ` To compose an OSCAL Model: lula tools compose -f ./oscal-component.yaml @@ -28,65 +21,113 @@ To compose an OSCAL Model: To indicate a specific output file: lula tools compose -f ./oscal-component.yaml -o composed-oscal-component.yaml ` -var composeCmd = &cobra.Command{ - Use: "compose", - Short: "compose an OSCAL component definition", - Long: "Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability.", - Example: composeHelp, - Run: func(cmd *cobra.Command, args []string) { - composeSpinner := message.NewProgressSpinner("Composing %s", composeOpts.InputFile) - defer composeSpinner.Stop() - - if composeOpts.InputFile == "" { - message.Fatal(errors.New("flag input-file is not set"), - "Please specify an input file with the -f flag") - } - - outputFile := composeOpts.OutputFile - if outputFile == "" { - outputFile = GetDefaultOutputFile(composeOpts.InputFile) - } - - err := Compose(composeOpts.InputFile, outputFile) - if err != nil { - message.Fatalf(err, "Composition error: %s", err) - } - - message.Infof("Composed OSCAL Component Definition to: %s", outputFile) - composeSpinner.Success() - }, + +var composeLong = ` +Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability. + +Supports templating of the composed component definition with the following configuration options: +- To compose with templating applied, specify '--render, -r' with values of 'all', 'non-sensitive', 'constants', or 'masked' (choice will depend on the use case for the composed content) +- To render Lula Validations include '--render-validations' +- To perform any manual overrides to the template data, specify '--set, -s' with the format '.const.key=value' or '.var.key=value' +` + +func ComposeCommand() *cobra.Command { + var ( + inputFile string // -f --input-file + outputFile string // -o --output-file + setOpts []string // -s --set + renderTypeString string // -r --render + renderValidations bool // --render-validations + ) + + var cmd = &cobra.Command{ + Use: "compose", + Short: "compose an OSCAL component definition", + Long: composeLong, + Example: composeHelp, + RunE: func(cmd *cobra.Command, args []string) error { + composeSpinner := message.NewProgressSpinner("Composing %s", inputFile) + defer composeSpinner.Stop() + + // TODO: check if remote or local? + _, err := os.Stat(inputFile) + if os.IsNotExist(err) { + return fmt.Errorf("input-file: %v does not exist - unable to digest document", inputFile) + } + + // Update path if relative + path := inputFile + if filepath.IsLocal(inputFile) { + path = filepath.Join(filepath.Dir(inputFile), filepath.Base(inputFile)) + } + + if outputFile == "" { + outputFile = GetDefaultOutputFile(inputFile) + } + + opts := []composition.Option{ + composition.WithModelFromPath(path), + composition.WithTemplateRenderer(renderTypeString, renderValidations, setOpts), + } + + compositionCtx, err := composition.New(context.Background(), opts...) + if err != nil { + return fmt.Errorf("error creating composition context: %v", err) + } + + err = compositionCtx.ComposeFromPath(path) + if err != nil { + return fmt.Errorf("error composing model: %v", err) + } + + // Write the composed OSCAL model to a file + err = oscal.WriteOscalModel(outputFile, compositionCtx.GetModel()) + if err != nil { + return fmt.Errorf("error writing composed model: %v", err) + } + + message.Infof("Composed OSCAL Component Definition to: %s", outputFile) + composeSpinner.Success() + + return nil + }, + } + cmd.Flags().StringVarP(&inputFile, "input-file", "f", "", "the path to the target OSCAL component definition") + cmd.MarkFlagRequired("input-file") + cmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be the original filename with `-composed` appended") + cmd.Flags().StringVarP(&renderTypeString, "render", "r", "", "values to render the template with, options are: masked, constants, non-sensitive, all") + cmd.Flags().StringSliceVarP(&setOpts, "set", "s", []string{}, "set value overrides for templated data") + cmd.Flags().BoolVar(&renderValidations, "render-validations", false, "extend render to remote Lula Validations") + + return cmd } func init() { common.InitViper() - - toolsCmd.AddCommand(composeCmd) - - composeCmd.Flags().StringVarP(&composeOpts.InputFile, "input-file", "f", "", "the path to the target OSCAL component definition") - composeCmd.Flags().StringVarP(&composeOpts.OutputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be the original filename with `-composed` appended") + toolsCmd.AddCommand(ComposeCommand()) } // Compose composes an OSCAL model from a file path -func Compose(inputFile, outputFile string) error { - _, err := os.Stat(inputFile) - if os.IsNotExist(err) { - return fmt.Errorf("input file: %v does not exist - unable to compose document", inputFile) - } - - // Compose the OSCAL model - model, err := composition.ComposeFromPath(inputFile) - if err != nil { - return err - } - - // Write the composed OSCAL model to a file - err = oscal.WriteOscalModel(outputFile, model) - if err != nil { - return err - } - - return nil -} +// func Compose(inputFile, outputFile string, templateRenderer *template.TemplateRenderer, renderType template.RenderType) error { +// _, err := os.Stat(inputFile) +// if os.IsNotExist(err) { +// return fmt.Errorf("input file: %v does not exist - unable to compose document", inputFile) +// } + +// // Compose the OSCAL model +// model, err := composition.ComposeFromPath(inputFile) +// if err != nil { +// return err +// } + +// // Write the composed OSCAL model to a file +// err = oscal.WriteOscalModel(outputFile, model) +// if err != nil { +// return err +// } + +// return nil +// } // GetDefaultOutputFile returns the default output file name func GetDefaultOutputFile(inputFile string) string { diff --git a/src/cmd/tools/template.go b/src/cmd/tools/template.go index 4b1520f1..eb85fbaf 100644 --- a/src/cmd/tools/template.go +++ b/src/cmd/tools/template.go @@ -3,7 +3,6 @@ package tools import ( "fmt" "os" - "strings" "github.com/defenseunicorns/go-oscal/src/pkg/files" "github.com/defenseunicorns/lula/src/cmd/common" @@ -52,9 +51,10 @@ func TemplateCommand() *cobra.Command { } // Validate render type - renderType, err := parseRenderType(renderTypeString) + renderType, err := template.ParseRenderType(renderTypeString) if err != nil { message.Warnf("invalid render type, defaulting to masked: %v", err) + renderType = template.MASKED } // Get constants and variables for templating from viper config @@ -114,17 +114,3 @@ func init() { common.InitViper() toolsCmd.AddCommand(TemplateCommand()) } - -func parseRenderType(item string) (template.RenderType, error) { - switch strings.ToLower(item) { - case "masked": - return template.MASKED, nil - case "constants": - return template.CONSTANTS, nil - case "non-sensitive": - return template.NONSENSITIVE, nil - case "all": - return template.ALL, nil - } - return template.MASKED, fmt.Errorf("invalid render type: %s", item) -} diff --git a/src/cmd/validate/validate.go b/src/cmd/validate/validate.go index 7ca481e9..bde76aff 100644 --- a/src/cmd/validate/validate.go +++ b/src/cmd/validate/validate.go @@ -8,7 +8,6 @@ import ( "github.com/defenseunicorns/go-oscal/src/pkg/files" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/cmd/common" - "github.com/defenseunicorns/lula/src/pkg/common/composition" "github.com/defenseunicorns/lula/src/pkg/common/oscal" requirementstore "github.com/defenseunicorns/lula/src/pkg/common/requirement-store" validationstore "github.com/defenseunicorns/lula/src/pkg/common/validation-store" @@ -125,7 +124,8 @@ func ValidateOnPath(path string, target string) (assessmentResult *oscalTypes_1_ return assessmentResult, fmt.Errorf("path: %v does not exist - unable to digest document", path) } - oscalModel, err := composition.ComposeFromPath(path) + // oscalModel, err := composition.ComposeFromPath(path) + oscalModel := &oscalTypes_1_1_2.OscalCompleteSchema{} if err != nil { return assessmentResult, err } diff --git a/src/internal/template/template.go b/src/internal/template/template.go index 865c4e46..b98687d6 100644 --- a/src/internal/template/template.go +++ b/src/internal/template/template.go @@ -247,6 +247,33 @@ func GetEnvVars(prefix string) map[string]string { return envMap } +// IsTemplate checks if the given string contains valid template syntax +func IsTemplate(data string) bool { + // Check for basic template syntax markers + if !strings.Contains(data, "{{") || !strings.Contains(data, "}}") { + return false + } + + // Attempt to parse the template + tpl := createTemplate() + _, err := tpl.Parse(data) + return err == nil +} + +func ParseRenderType(item string) (RenderType, error) { + switch strings.ToLower(item) { + case "masked": + return MASKED, nil + case "constants": + return CONSTANTS, nil + case "non-sensitive": + return NONSENSITIVE, nil + case "all": + return ALL, nil + } + return "", fmt.Errorf("invalid render type: %s", item) +} + // createTemplate creates a new template object func createTemplate() *template.Template { // Register custom template functions diff --git a/src/pkg/common/common.go b/src/pkg/common/common.go index 21989281..52769344 100644 --- a/src/pkg/common/common.go +++ b/src/pkg/common/common.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "regexp" "strings" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" @@ -185,3 +186,10 @@ func ValidationFromString(raw, uuid string) (validation types.LulaValidation, er return validation, nil } + +// CleanMultilineString removes leading and trailing whitespace from a multiline string +func CleanMultilineString(str string) string { + re := regexp.MustCompile(`[ \t]+\r?\n`) + formatted := re.ReplaceAllString(str, "\n") + return formatted +} diff --git a/src/pkg/common/composition/composition.go b/src/pkg/common/composition/composition.go index 65ca4b68..b8c5629c 100644 --- a/src/pkg/common/composition/composition.go +++ b/src/pkg/common/composition/composition.go @@ -2,14 +2,15 @@ package composition import ( "bytes" + "context" "fmt" "io" "os" - "path/filepath" "github.com/defenseunicorns/go-oscal/src/pkg/uuid" "github.com/defenseunicorns/go-oscal/src/pkg/versioning" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + "github.com/defenseunicorns/lula/src/internal/template" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/network" "github.com/defenseunicorns/lula/src/pkg/common/oscal" @@ -17,44 +18,70 @@ import ( k8syaml "k8s.io/apimachinery/pkg/util/yaml" ) +type RenderedContent string + +type CompositionContext struct { + bctx context.Context + model *oscalTypes_1_1_2.OscalCompleteSchema + modelDir string + templateRenderer *template.TemplateRenderer + renderTemplate bool + renderRemote bool + renderType template.RenderType +} + +func New(ctx context.Context, opts ...Option) (*CompositionContext, error) { + var compositionCtx CompositionContext + + for _, opt := range opts { + if err := opt(&compositionCtx); err != nil { + return nil, err + } + } + + return &compositionCtx, nil +} + +func (ctx *CompositionContext) GetModel() *oscalTypes_1_1_2.OscalCompleteSchema { + return ctx.model +} + // ComposeFromPath composes an OSCAL model from a file path -func ComposeFromPath(inputFile string) (model *oscalTypes_1_1_2.OscalCompleteSchema, err error) { - data, err := os.ReadFile(inputFile) +func (ctx *CompositionContext) ComposeFromPath(path string) (err error) { + data, err := os.ReadFile(path) if err != nil { - return nil, err + return err } - // Change Cwd to the directory of the component definition - // This is needed to resolve relative paths in the remote validations - dirPath := filepath.Dir(inputFile) - message.Infof("changing cwd to %s", dirPath) - resetCwd, err := common.SetCwdToFileDir(dirPath) - if err != nil { - return nil, err + // Template if renderTemplate is true -> Only renders the local data (e.g., what is in the file) + if ctx.renderTemplate { + data, err = ctx.templateRenderer.Render(string(data), ctx.renderType) + if err != nil { + return err + } } - defer resetCwd() - model, err = oscal.NewOscalModel(data) + ctx.model, err = oscal.NewOscalModel(data) if err != nil { - return nil, err + return err } - err = ComposeComponentDefinitions(model.ComponentDefinition) + err = ctx.ComposeComponentDefinitions(ctx.model.ComponentDefinition, ctx.modelDir) if err != nil { - return nil, err + return err } - return model, nil + return nil } // ComposeComponentDefinitions composes an OSCAL component definition by adding the remote resources to the back matter and updating with back matter links. -func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) error { +func (ctx *CompositionContext) ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { if compDef == nil { return fmt.Errorf("component definition is nil") } // Compose the component validations - err := ComposeComponentValidations(compDef) + err := ctx.ComposeComponentValidations(compDef, baseDir) if err != nil { return err } @@ -73,12 +100,19 @@ func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) if compDef.ImportComponentDefinitions != nil { for _, importComponentDef := range *compDef.ImportComponentDefinitions { - // Fetch the response - response, err := network.Fetch(importComponentDef.Href) + response, err := network.Fetch(importComponentDef.Href, network.WithBaseDir(baseDir)) if err != nil { return err } + // template here if remote is specified + if ctx.renderRemote { + response, err = ctx.templateRenderer.Render(string(response), ctx.renderType) + if err != nil { + return err + } + } + // Handle multi-docs componentDefs, err := readComponentDefinitionsFromYaml(response) if err != nil { @@ -86,7 +120,9 @@ func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) } // Unmarshal the component definition for _, importDef := range componentDefs { - err = ComposeComponentDefinitions(importDef) + // Reconcile the base directory from the import component definition href + baseDir = network.GetLocalFileDir(importComponentDef.Href, baseDir) + err = ctx.ComposeComponentDefinitions(importDef, baseDir) if err != nil { return err } @@ -107,13 +143,13 @@ func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) } // ComposeComponentValidations compiles the component validations by adding the remote resources to the back matter and updating with back matter links. -func ComposeComponentValidations(compDef *oscalTypes_1_1_2.ComponentDefinition) error { +func (ctx *CompositionContext) ComposeComponentValidations(compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { if compDef == nil { return fmt.Errorf("component definition is nil") } - resourceMap := NewResourceStoreFromBackMatter(compDef.BackMatter) + resourceMap := NewResourceStoreFromBackMatter(ctx, compDef.BackMatter) // If there are no components, there is nothing to do if compDef.Components == nil { @@ -133,7 +169,7 @@ func ComposeComponentValidations(compDef *oscalTypes_1_1_2.ComponentDefinition) for _, link := range *implementedRequirement.Links { if common.IsLulaLink(link) { - ids, err := resourceMap.AddFromLink(&link) + ids, err := resourceMap.AddFromLink(&link, baseDir) if err != nil { // return err newId := uuid.NewUUID() diff --git a/src/pkg/common/composition/composition_test.go b/src/pkg/common/composition/composition_test.go index 85e15561..17fbdbab 100644 --- a/src/pkg/common/composition/composition_test.go +++ b/src/pkg/common/composition/composition_test.go @@ -1,12 +1,13 @@ package composition_test import ( + "context" "os" + "path/filepath" "reflect" "testing" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" - "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/composition" "gopkg.in/yaml.v3" ) @@ -19,11 +20,34 @@ const ( localAndRemote = "../../../test/unit/common/composition/component-definition-local-and-remote.yaml" subComponentDef = "../../../test/unit/common/composition/component-definition-import-compdefs.yaml" compDefMultiImport = "../../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" + + // TODO: add tests for templating + compDefNestedImport = "../../../test/unit/common/composition/component-definition-import-nested-compdef" + compDefMultiTmpl = "../../../test/unit/common/composition/component-definition-local-and-remote-template.yaml" + + // Also, add cmd tests...? compare golden composed file? ) func TestComposeFromPath(t *testing.T) { + test := func(t *testing.T, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { + t.Helper() + + options := append([]composition.Option{composition.WithModelFromPath(path)}, opts...) + ctx, err := composition.New(context.Background(), options...) + if err != nil { + return nil, err + } + + err = ctx.ComposeFromPath(path) + if err != nil { + return nil, err + } + + return ctx.GetModel(), nil + } + t.Run("No imports, local validations", func(t *testing.T) { - model, err := composition.ComposeFromPath(allLocal) + model, err := test(t, allLocal) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -33,7 +57,7 @@ func TestComposeFromPath(t *testing.T) { }) t.Run("No imports, local validations, bad href", func(t *testing.T) { - model, err := composition.ComposeFromPath(allLocalBadHref) + model, err := test(t, allLocalBadHref) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -43,7 +67,17 @@ func TestComposeFromPath(t *testing.T) { }) t.Run("No imports, remote validations", func(t *testing.T) { - model, err := composition.ComposeFromPath(allRemote) + model, err := test(t, allRemote) + if err != nil { + t.Fatalf("Error composing component definitions: %v", err) + } + if model == nil { + t.Error("expected the model to be composed") + } + }) + + t.Run("Imports, no components", func(t *testing.T) { + model, err := test(t, allRemote) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -53,7 +87,7 @@ func TestComposeFromPath(t *testing.T) { }) t.Run("No imports, bad remote validations", func(t *testing.T) { - model, err := composition.ComposeFromPath(allRemoteBadHref) + model, err := test(t, allRemoteBadHref) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -63,14 +97,14 @@ func TestComposeFromPath(t *testing.T) { }) t.Run("Errors when file does not exist", func(t *testing.T) { - _, err := composition.ComposeFromPath("nonexistent") + _, err := test(t, "nonexistent") if err == nil { t.Error("expected an error") } }) t.Run("Resolves relative paths", func(t *testing.T) { - model, err := composition.ComposeFromPath(localAndRemote) + model, err := test(t, localAndRemote) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -81,21 +115,41 @@ func TestComposeFromPath(t *testing.T) { } func TestComposeComponentDefinitions(t *testing.T) { + test := func(t *testing.T, compDef *oscalTypes_1_1_2.ComponentDefinition, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { + t.Helper() + + options := append([]composition.Option{composition.WithModelFromPath(path)}, opts...) + ctx, err := composition.New(context.Background(), options...) + if err != nil { + return nil, err + } + + baseDir := filepath.Dir(path) + + err = ctx.ComposeComponentDefinitions(compDef, baseDir) + if err != nil { + return nil, err + } + + return ctx.GetModel(), nil + } + t.Run("No imports, local validations", func(t *testing.T) { og := getComponentDef(allLocal, t) compDef := getComponentDef(allLocal, t) - reset, err := common.SetCwdToFileDir(allLocal) - defer reset() - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - err = composition.ComposeComponentDefinitions(compDef) + + model, err := test(t, compDef, allLocal) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + // Only the last-modified timestamp should be different - if !reflect.DeepEqual(*og.BackMatter, *compDef.BackMatter) { + if !reflect.DeepEqual(*og.BackMatter, *compDefComposed.BackMatter) { t.Error("expected the back matter to be unchanged") } }) @@ -103,17 +157,18 @@ func TestComposeComponentDefinitions(t *testing.T) { t.Run("No imports, remote validations", func(t *testing.T) { og := getComponentDef(allRemote, t) compDef := getComponentDef(allRemote, t) - reset, err := common.SetCwdToFileDir(allRemote) - defer reset() - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - err = composition.ComposeComponentDefinitions(compDef) + + model, err := test(t, compDef, allRemote) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } - if reflect.DeepEqual(*og, *compDef) { + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if reflect.DeepEqual(*og, *compDefComposed) { t.Errorf("expected component definition to have changed.") } }) @@ -121,21 +176,22 @@ func TestComposeComponentDefinitions(t *testing.T) { t.Run("Imports, no components", func(t *testing.T) { og := getComponentDef(subComponentDef, t) compDef := getComponentDef(subComponentDef, t) - reset, err := common.SetCwdToFileDir(subComponentDef) - defer reset() - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - err = composition.ComposeComponentDefinitions(compDef) + + model, err := test(t, compDef, subComponentDef) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } - if compDef.Components == og.Components { + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == og.Components { t.Error("expected there to be components") } - if compDef.BackMatter == og.BackMatter { + if compDefComposed.BackMatter == og.BackMatter { t.Error("expected the back matter to be changed") } }) @@ -143,47 +199,68 @@ func TestComposeComponentDefinitions(t *testing.T) { t.Run("imports, no components, multiple component definitions from import", func(t *testing.T) { og := getComponentDef(compDefMultiImport, t) compDef := getComponentDef(compDefMultiImport, t) - reset, err := common.SetCwdToFileDir(compDefMultiImport) - defer reset() - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - err = composition.ComposeComponentDefinitions(compDef) + + model, err := test(t, compDef, subComponentDef) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } - if compDef.Components == og.Components { + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == og.Components { t.Error("expected there to be components") } - if compDef.BackMatter == og.BackMatter { + if compDefComposed.BackMatter == og.BackMatter { t.Error("expected the back matter to be changed") } - if len(*compDef.Components) != 1 { - t.Error("expected there to be 2 components") + if len(*compDefComposed.Components) != 1 { + t.Error("expected there to be 1 component") } }) } func TestCompileComponentValidations(t *testing.T) { + test := func(t *testing.T, compDef *oscalTypes_1_1_2.ComponentDefinition, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { + t.Helper() + + options := append([]composition.Option{composition.WithModelFromPath(path)}, opts...) + ctx, err := composition.New(context.Background(), options...) + if err != nil { + return nil, err + } + + baseDir := filepath.Dir(path) + + err = ctx.ComposeComponentValidations(compDef, baseDir) + if err != nil { + return nil, err + } + + return ctx.GetModel(), nil + } t.Run("all local", func(t *testing.T) { og := getComponentDef(allLocal, t) compDef := getComponentDef(allLocal, t) - reset, err := common.SetCwdToFileDir(allLocal) - defer reset() + + model, err := test(t, compDef, allLocal) if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) + t.Fatalf("error composing validations: %v", err) } - err = composition.ComposeComponentValidations(compDef) - if err != nil { - t.Fatalf("Error compiling component validations: %v", err) + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") } // Only the last-modified timestamp should be different - if !reflect.DeepEqual(*og.BackMatter, *compDef.BackMatter) { + if !reflect.DeepEqual(*og.BackMatter, *compDefComposed.BackMatter) { t.Error("expected the back matter to be unchanged") } }) @@ -191,24 +268,26 @@ func TestCompileComponentValidations(t *testing.T) { t.Run("all remote", func(t *testing.T) { og := getComponentDef(allRemote, t) compDef := getComponentDef(allRemote, t) - reset, err := common.SetCwdToFileDir(allRemote) - defer reset() + + model, err := test(t, compDef, allRemote) if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) + t.Fatalf("error composing validations: %v", err) } - err = composition.ComposeComponentValidations(compDef) - if err != nil { - t.Fatalf("Error compiling component validations: %v", err) + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") } - if reflect.DeepEqual(*og, *compDef) { + + if reflect.DeepEqual(*og, *compDefComposed) { t.Error("expected the component definition to be changed") } - if compDef.BackMatter == nil { + if compDefComposed.BackMatter == nil { t.Error("expected the component definition to have back matter") } - if og.Metadata.LastModified == compDef.Metadata.LastModified { + if og.Metadata.LastModified == compDefComposed.Metadata.LastModified { t.Error("expected the component definition to have a different last modified timestamp") } }) @@ -216,17 +295,18 @@ func TestCompileComponentValidations(t *testing.T) { t.Run("local and remote", func(t *testing.T) { og := getComponentDef(localAndRemote, t) compDef := getComponentDef(localAndRemote, t) - reset, err := common.SetCwdToFileDir(localAndRemote) - defer reset() + + model, err := test(t, compDef, localAndRemote) if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) + t.Fatalf("error composing validations: %v", err) } - err = composition.ComposeComponentValidations(compDef) - if err != nil { - t.Fatalf("Error compiling component validations: %v", err) + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") } - if reflect.DeepEqual(*og, *compDef) { + if reflect.DeepEqual(*og, *compDefComposed) { t.Error("expected the component definition to be changed") } }) diff --git a/src/pkg/common/composition/options.go b/src/pkg/common/composition/options.go new file mode 100644 index 00000000..d06b8e33 --- /dev/null +++ b/src/pkg/common/composition/options.go @@ -0,0 +1,75 @@ +package composition + +import ( + "fmt" + "path/filepath" + + "github.com/defenseunicorns/go-oscal/src/pkg/files" + "github.com/defenseunicorns/lula/src/cmd/common" + "github.com/defenseunicorns/lula/src/internal/template" + "github.com/defenseunicorns/lula/src/pkg/message" +) + +type Option func(*CompositionContext) error + +// TODO: add remote option? +func WithModelFromPath(path string) Option { + return func(ctx *CompositionContext) error { + if err := files.IsJsonOrYaml(path); err != nil { + return fmt.Errorf("invalid file extension: %s, requires .json or .yaml", path) + } + ctx.modelDir = filepath.Dir(path) + return nil + } +} + +func WithTemplateRenderer(renderTypeString string, renderRemote bool, setOpts []string) Option { + return func(ctx *CompositionContext) error { + if renderTypeString == "" { + if len(setOpts) > 0 { + message.Warn("`render` not specified, the --set options will be ignored") + } + if renderRemote { + message.Warn("`render` not specified, `render-remote` will be ignored") + } + return nil + } + + ctx.renderTemplate = true + ctx.renderRemote = renderRemote + + // Get the template render type + renderType, err := template.ParseRenderType(renderTypeString) + if err != nil { + message.Warnf("invalid render type, defaulting to non-sensitive: %v", err) + renderType = template.NONSENSITIVE + } + ctx.renderType = renderType + + // Get constants and variables for templating from viper config + constants, variables, err := common.GetTemplateConfig() + if err != nil { + return fmt.Errorf("error getting template config: %v", err) + } + + // Get overrides from setOpts flag + overrides, err := common.ParseTemplateOverrides(setOpts) + if err != nil { + return fmt.Errorf("error parsing template overrides: %v", err) + } + + // Handles merging viper config file data + environment variables + // Throws an error if config keys are invalid for templating + templateData, err := template.CollectTemplatingData(constants, variables, overrides) + if err != nil { + return fmt.Errorf("error collecting templating data: %v", err) + } + + // need to update the template with the templateString... + tr := template.NewTemplateRenderer(templateData) + + ctx.templateRenderer = tr + + return nil + } +} diff --git a/src/pkg/common/composition/resource-store.go b/src/pkg/common/composition/resource-store.go index 82d077fb..1bf0f203 100644 --- a/src/pkg/common/composition/resource-store.go +++ b/src/pkg/common/composition/resource-store.go @@ -3,9 +3,11 @@ package composition import ( "fmt" + "github.com/defenseunicorns/go-oscal/src/pkg/uuid" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/network" + "github.com/defenseunicorns/lula/src/pkg/common/oscal" ) // ResourceStore is a store of resources. @@ -13,13 +15,16 @@ type ResourceStore struct { existing map[string]*oscalTypes_1_1_2.Resource fetched map[string]*oscalTypes_1_1_2.Resource hrefIdMap map[string][]string + cctx *CompositionContext } // NewResourceStoreFromBackMatter creates a new resource store from the back matter of a component definition. -func NewResourceStoreFromBackMatter(backMatter *oscalTypes_1_1_2.BackMatter) *ResourceStore { +func NewResourceStoreFromBackMatter(cctx *CompositionContext, backMatter *oscalTypes_1_1_2.BackMatter) *ResourceStore { store := &ResourceStore{ - existing: make(map[string]*oscalTypes_1_1_2.Resource), - fetched: make(map[string]*oscalTypes_1_1_2.Resource), + existing: make(map[string]*oscalTypes_1_1_2.Resource), + fetched: make(map[string]*oscalTypes_1_1_2.Resource), + hrefIdMap: make(map[string][]string), + cctx: cctx, } if backMatter != nil && *backMatter.Resources != nil { @@ -94,7 +99,7 @@ func (s *ResourceStore) Has(id string) bool { } // AddFromLink adds resources from a link to the store. -func (s *ResourceStore) AddFromLink(link *oscalTypes_1_1_2.Link) (ids []string, err error) { +func (s *ResourceStore) AddFromLink(link *oscalTypes_1_1_2.Link, baseDir string) (ids []string, err error) { if link == nil { return nil, fmt.Errorf("link is nil") } @@ -112,29 +117,36 @@ func (s *ResourceStore) AddFromLink(link *oscalTypes_1_1_2.Link) (ids []string, return ids, err } - return s.fetchFromRemoteLink(link) + return s.fetchFromRemoteLink(link, baseDir) } -func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link) (ids []string, err error) { +// fetchFromRemoteLink expects a link to a remote validation or validation template +func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link, baseDir string) (ids []string, err error) { wantedId := common.TrimIdPrefix(link.ResourceFragment) - validationBytes, err := network.Fetch(link.Href) + validationBytes, err := network.Fetch(link.Href, network.WithBaseDir(baseDir)) if err != nil { - return nil, err + return nil, fmt.Errorf("error fetching remote resource: %v", err) + } + + if s.cctx.renderRemote { + validationBytes, err = s.cctx.templateRenderer.Render(string(validationBytes), s.cctx.renderType) + if err != nil { + return nil, err + } } validationArr, err := common.ReadValidationsFromYaml(validationBytes) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to read validations from link: %v", err) } isSingleValidation := len(validationArr) == 1 for _, validation := range validationArr { resource, err := validation.ToResource() if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create validation resource: %v", err) } - s.AddFetched(resource) if wantedId == resource.UUID || wantedId == common.WILDCARD || isSingleValidation { @@ -142,5 +154,22 @@ func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link) (ids [] } } + s.SetHrefIds(link.Href, ids) + return ids, err } + +func createTemplateResource(data []byte) *oscalTypes_1_1_2.Resource { + return &oscalTypes_1_1_2.Resource{ + Title: "Validation Template", + UUID: uuid.NewUUID(), + Description: common.CleanMultilineString(string(data)), + Props: &[]oscalTypes_1_1_2.Property{ + { + Name: "resource-type", + Value: "validation-template", + Ns: oscal.LULA_NAMESPACE, + }, + }, + } +} diff --git a/src/pkg/common/network/network.go b/src/pkg/common/network/network.go index 400f407e..9137c16b 100644 --- a/src/pkg/common/network/network.go +++ b/src/pkg/common/network/network.go @@ -66,11 +66,35 @@ func ParseChecksum(src string) (*url.URL, string, error) { return url, checksum, nil } +type fetchOpts struct { + baseDir string +} + +type FetchOption func(*fetchOpts) error + +func WithBaseDir(baseDir string) FetchOption { + return func(opts *fetchOpts) error { + // check if baseDir is a valid directory + if _, err := os.Stat(baseDir); err != nil { + return err + } + opts.baseDir = baseDir + return nil + } +} + +// TODO: add more options for timeout, retries, etc. + // Fetch fetches the response body from a given URL after validating it. // If the URL scheme is "file", the file is fetched from the local filesystem. // If the URL scheme is "http", "https", or "ftp", the file is fetched from the remote server. // If the URL has a checksum, the file is validated against the checksum. -func Fetch(inputURL string) (bytes []byte, err error) { +func Fetch(inputURL string, opts ...FetchOption) (bytes []byte, err error) { + config := &fetchOpts{} + for _, opt := range opts { + opt(config) + } + url, checksum, err := ParseChecksum(inputURL) if err != nil { return bytes, err @@ -78,7 +102,7 @@ func Fetch(inputURL string) (bytes []byte, err error) { // If the URL is a file, fetch the file from the local filesystem if url.Scheme == "file" { - bytes, err = FetchLocalFile(url) + bytes, err = FetchLocalFile(url, config) if err != nil { return bytes, err } @@ -114,7 +138,7 @@ func Fetch(inputURL string) (bytes []byte, err error) { // FetchLocalFile fetches a local file from a given URL. // If the URL scheme is not "file", an error is returned. // If the URL is relative, the component definition directory is prepended if set, otherwise the current working directory is prepended. -func FetchLocalFile(url *url.URL) ([]byte, error) { +func FetchLocalFile(url *url.URL, config *fetchOpts) ([]byte, error) { if url.Scheme != "file" { return nil, errors.New("expected file URL scheme") } @@ -122,18 +146,28 @@ func FetchLocalFile(url *url.URL) ([]byte, error) { // If the request uri is absolute, use it directly if _, err := os.Stat(requestUri); err != nil { - // if relative pre-pend cwd - cwd, err := os.Getwd() - if err != nil { - return nil, err - } - requestUri = filepath.Join(cwd, requestUri) + requestUri = filepath.Join(config.baseDir, url.Host, requestUri) } bytes, err := os.ReadFile(requestUri) return bytes, err } +func GetLocalFileDir(inputURL, baseDir string) string { + url, err := url.Parse(inputURL) + if err != nil { + return "" + } + requestUri := url.RequestURI() + + if url.Scheme == "file" { + if _, err := os.Stat(requestUri); err != nil { + return filepath.Dir(filepath.Join(baseDir, url.Host, requestUri)) + } + } + return "" +} + // ValidateChecksum validates a given checksum against a given []bytes. // Supports MD5, SHA-1, SHA-256, and SHA-512. // Returns an error if the hash does not match. diff --git a/src/pkg/common/types.go b/src/pkg/common/types.go index 208bb369..5438d86c 100644 --- a/src/pkg/common/types.go +++ b/src/pkg/common/types.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "regexp" "strings" "github.com/defenseunicorns/go-oscal/src/pkg/uuid" @@ -49,25 +48,26 @@ func (v *Validation) MarshalYaml() ([]byte, error) { // ToResource converts a Validation object to a Resource object func (v *Validation) ToResource() (resource *oscalTypes_1_1_2.Resource, err error) { - resource = &oscalTypes_1_1_2.Resource{} - resource.Title = v.Metadata.Name + resourceUuid := uuid.NewUUID() if v.Metadata.UUID != "" { - resource.UUID = v.Metadata.UUID - } else { - resource.UUID = uuid.NewUUID() - } - // If the provider is opa, trim whitespace from the rego - if v.Provider != nil && v.Provider.OpaSpec != nil { - re := regexp.MustCompile(`[ \t]+\r?\n`) - v.Provider.OpaSpec.Rego = re.ReplaceAllString(v.Provider.OpaSpec.Rego, "\n") + resourceUuid = v.Metadata.UUID } validationBytes, err := v.MarshalYaml() if err != nil { return nil, err } - resource.Description = string(validationBytes) - return resource, nil + + if v.Provider != nil && v.Provider.OpaSpec != nil { + // Clean multiline string in rego + CleanMultilineString(v.Provider.OpaSpec.Rego) + } + + return &oscalTypes_1_1_2.Resource{ + Title: v.Metadata.Name, + UUID: resourceUuid, + Description: CleanMultilineString(string(validationBytes)), + }, nil } // Metadata is a structure that contains the name and uuid of a validation diff --git a/src/test/e2e/standard/lula-config.yaml b/src/test/e2e/cmd/lula-config.yaml similarity index 100% rename from src/test/e2e/standard/lula-config.yaml rename to src/test/e2e/cmd/lula-config.yaml diff --git a/src/test/e2e/cmd/main_test.go b/src/test/e2e/cmd/main_test.go new file mode 100644 index 00000000..2a3184e5 --- /dev/null +++ b/src/test/e2e/cmd/main_test.go @@ -0,0 +1,110 @@ +package cmd_test + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/defenseunicorns/lula/src/cmd" + "github.com/defenseunicorns/lula/src/test/util" +) + +var updateGolden = flag.Bool("update", false, "update golden files") + +func TestMain(m *testing.M) { + flag.Parse() + m.Run() +} + +func runCmdTest(t *testing.T, goldenFileName string, expectError bool, cmdArgs ...string) error { + t.Helper() + + rootCmd := cmd.RootCommand() + + _, output, err := util.ExecuteCommand(rootCmd, cmdArgs...) + if err != nil { + if !expectError { + return err + } else { + return nil + } + } + + if !expectError { + testGolden(t, goldenFileName, output) + } + + return nil +} + +func runCmdTestWithOutputFile(t *testing.T, goldenFileName string, outExt string, expectError bool, cmdArgs ...string) error { + t.Helper() + + tempFileName := fmt.Sprintf("output-%s.%s", goldenFileName, outExt) + defer os.Remove(tempFileName) + + rootCmd := cmd.RootCommand() + + cmdArgs = append(cmdArgs, "-o", tempFileName) + _, _, err := util.ExecuteCommand(rootCmd, cmdArgs...) + if err != nil { + if !expectError { + return err + } else { + return nil + } + } + + // Read the output file + data, err := os.ReadFile(tempFileName) + if err != nil { + return err + } + + // Scrub timestamps + data = scrubTimestamps(data) + + if !expectError { + testGolden(t, goldenFileName, string(data)) + } + + return nil +} + +func testGolden(t *testing.T, filename, got string) { + t.Helper() + + got = strings.ReplaceAll(got, "\r\n", "\n") + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + goldenPath := filepath.Join(wd, "testdata", filename+".golden") + + if *updateGolden { + if err := os.MkdirAll(filepath.Dir(goldenPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(goldenPath, []byte(got), 0o600); err != nil { + t.Fatal(err) + } + } + + wantBytes, _ := os.ReadFile(goldenPath) + + want := string(wantBytes) + + if got != want { + t.Fatalf("`%s` does not match.\n\nWant:\n\n%s\n\nGot:\n\n%s", goldenPath, want, got) + } +} + +func scrubTimestamps(data []byte) []byte { + re := regexp.MustCompile(`(?i)(last-modified:\s*)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[-+]\d{2}:\d{2})`) + return []byte(re.ReplaceAllString(string(data), "${1}XXX")) +} diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file.golden new file mode 100644 index 00000000..b76bdf55 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file.golden @@ -0,0 +1,66 @@ +component-definition: + back-matter: + resources: + - description: |- + domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + group: + version: v1 + resource: pods + namespaces: [validation-test] + provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + rlinks: + - href: lula.dev + uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' + rel: lula + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/help.golden b/src/test/e2e/cmd/testdata/tools/compose/help.golden new file mode 100644 index 00000000..68d0f9d1 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/help.golden @@ -0,0 +1,30 @@ + +Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability. + +Supports templating of the composed component definition with the following configuration options: +- To compose with templating applied, specify '--render, -r' with values of 'all', 'non-sensitive', 'constants', or 'masked' (choice will depend on the use case for the composed content) +- To render Lula Validations include '--render-validations' +- To perform any manual overrides to the template data, specify '--set, -s' with the format '.const.key=value' or '.var.key=value' + +Usage: + lula tools compose [flags] + +Examples: + +To compose an OSCAL Model: + lula tools compose -f ./oscal-component.yaml + +To indicate a specific output file: + lula tools compose -f ./oscal-component.yaml -o composed-oscal-component.yaml + + +Flags: + -h, --help help for compose + -f, --input-file string the path to the target OSCAL component definition + -o, --output-file -composed the path to the output file. If not specified, the output file will be the original filename with -composed appended + -r, --render string values to render the template with, options are: masked, constants, non-sensitive, all + --render-validations extend render to remote Lula Validations + -s, --set strings set value overrides for templated data + +Global Flags: + -l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info") diff --git a/src/test/e2e/standard/testdata/help.golden b/src/test/e2e/cmd/testdata/tools/template/help.golden similarity index 100% rename from src/test/e2e/standard/testdata/help.golden rename to src/test/e2e/cmd/testdata/tools/template/help.golden diff --git a/src/test/e2e/standard/testdata/validation.golden b/src/test/e2e/cmd/testdata/tools/template/validation.golden similarity index 100% rename from src/test/e2e/standard/testdata/validation.golden rename to src/test/e2e/cmd/testdata/tools/template/validation.golden diff --git a/src/test/e2e/standard/testdata/validation_all.golden b/src/test/e2e/cmd/testdata/tools/template/validation_all.golden similarity index 100% rename from src/test/e2e/standard/testdata/validation_all.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_all.golden diff --git a/src/test/e2e/standard/testdata/validation_constants.golden b/src/test/e2e/cmd/testdata/tools/template/validation_constants.golden similarity index 100% rename from src/test/e2e/standard/testdata/validation_constants.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_constants.golden diff --git a/src/test/e2e/standard/testdata/validation_non_sensitive.golden b/src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden similarity index 100% rename from src/test/e2e/standard/testdata/validation_non_sensitive.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden diff --git a/src/test/e2e/standard/testdata/validation_with_env_vars.golden b/src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden similarity index 100% rename from src/test/e2e/standard/testdata/validation_with_env_vars.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden diff --git a/src/test/e2e/standard/testdata/validation_with_set.golden b/src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden similarity index 100% rename from src/test/e2e/standard/testdata/validation_with_set.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden diff --git a/src/test/e2e/cmd/tools_compose_test.go b/src/test/e2e/cmd/tools_compose_test.go new file mode 100644 index 00000000..edffb55d --- /dev/null +++ b/src/test/e2e/cmd/tools_compose_test.go @@ -0,0 +1,93 @@ +package cmd_test + +import ( + "testing" +) + +func TestToolsComposeCommand(t *testing.T) { + + test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { + t.Helper() + + cmdArgs := []string{"tools", "compose"} + cmdArgs = append(cmdArgs, args...) + + return runCmdTest(t, "tools/compose/"+goldenFileName, expectError, cmdArgs...) + } + + testVsOutput := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { + t.Helper() + + cmdArgs := []string{"tools", "compose"} + cmdArgs = append(cmdArgs, args...) + + return runCmdTestWithOutputFile(t, "tools/compose/"+goldenFileName, "yaml", expectError, cmdArgs...) + } + + t.Run("Compose Validation", func(t *testing.T) { + err := testVsOutput(t, "composed-file", false, "-f", "../../unit/common/composition/component-definition-all-local.yaml") + if err != nil { + t.Fatal(err) + } + }) + + // t.Run("Template Validation with env vars", func(t *testing.T) { + // os.Setenv("LULA_VAR_SOME_ENV_VAR", "my-env-var") + // defer os.Unsetenv("LULA_VAR_SOME_ENV_VAR") + // err := test(t, "validation_with_env_vars", false, "-f", "../../unit/common/validation/validation.tmpl.yaml") + // if err != nil { + // t.Fatal(err) + // } + // }) + + // t.Run("Template Validation with set", func(t *testing.T) { + // err := test(t, "validation_with_set", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", ".const.resources.name=foo") + // if err != nil { + // t.Fatal(err) + // } + // }) + + // t.Run("Template Validation for all", func(t *testing.T) { + // os.Setenv("LULA_VAR_SOME_LULA_SECRET", "env-secret") + // defer os.Unsetenv("LULA_VAR_SOME_LULA_SECRET") + // err := test(t, "validation_all", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "all") + // if err != nil { + // t.Fatal(err) + // } + // }) + + // t.Run("Template Validation for non-sensitive", func(t *testing.T) { + // err := test(t, "validation_non_sensitive", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "non-sensitive") + // if err != nil { + // t.Fatal(err) + // } + // }) + + // t.Run("Template Validation for constants", func(t *testing.T) { + // err := test(t, "validation_constants", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "constants") + // if err != nil { + // t.Fatal(err) + // } + // }) + + t.Run("Test help", func(t *testing.T) { + err := test(t, "help", false, "--help") + if err != nil { + t.Fatal(err) + } + }) + + // t.Run("Template Validation - invalid file error", func(t *testing.T) { + // err := test(t, "empty", true, "-f", "not-a-file.yaml") + // if err != nil { + // t.Fatal(err) + // } + // }) + + // t.Run("Template Validation - invalid file schema error", func(t *testing.T) { + // err := test(t, "empty", true, "-f", "../../unit/common/validation/validation.bad.tmpl.yaml") + // if err != nil { + // t.Fatal(err) + // } + // }) +} diff --git a/src/test/e2e/standard/template_test.go b/src/test/e2e/cmd/tools_template_test.go similarity index 73% rename from src/test/e2e/standard/template_test.go rename to src/test/e2e/cmd/tools_template_test.go index d2f84528..12ca7024 100644 --- a/src/test/e2e/standard/template_test.go +++ b/src/test/e2e/cmd/tools_template_test.go @@ -1,19 +1,14 @@ -package test +package cmd_test import ( - "flag" "os" - "path/filepath" "testing" - - "github.com/defenseunicorns/lula/src/cmd" - "github.com/defenseunicorns/lula/src/test/util" ) -var updateGolden = flag.Bool("update", false, "update golden files") +// var updateGolden = flag.Bool("update", false, "update golden files") -func TestTemplateCommand(t *testing.T) { +func TestToolsTemplateCommand(t *testing.T) { test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { t.Helper() @@ -21,38 +16,7 @@ func TestTemplateCommand(t *testing.T) { cmdArgs := []string{"tools", "template"} cmdArgs = append(cmdArgs, args...) - cmd := cmd.RootCommand() - - _, output, err := util.ExecuteCommand(cmd, cmdArgs...) - if err != nil { - if !expectError { - return err - } else { - return nil - } - } - - if !expectError { - goldenFile := filepath.Join("testdata", goldenFileName+".golden") - - if *updateGolden && !expectError { - err = os.WriteFile(goldenFile, []byte(output), 0644) - if err != nil { - return err - } - } - - expected, err := os.ReadFile(goldenFile) - if err != nil { - return err - } - - if output != string(expected) { - t.Fatalf("Expected:\n%s\n - Got \n%s\n", expected, output) - } - } - - return nil + return runCmdTest(t, "tools/template/"+goldenFileName, expectError, cmdArgs...) } t.Run("Template Validation", func(t *testing.T) { diff --git a/src/test/e2e/standard/testdata/empty.golden b/src/test/e2e/standard/testdata/empty.golden deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/unit/common/composition/component-definition-all-local.yaml b/src/test/unit/common/composition/component-definition-all-local.yaml index 0b795d29..3288f5f7 100644 --- a/src/test/unit/common/composition/component-definition-all-local.yaml +++ b/src/test/unit/common/composition/component-definition-all-local.yaml @@ -36,6 +36,7 @@ component-definition: links: - href: "#a7377430-2328-4dc4-a9e2-b3f31dc1dff9" rel: lula + resource-fragment: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 back-matter: resources: - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 diff --git a/src/test/unit/common/composition/component-definition-import-nested-compdef.yaml b/src/test/unit/common/composition/component-definition-import-nested-compdef.yaml new file mode 100644 index 00000000..e310fa13 --- /dev/null +++ b/src/test/unit/common/composition/component-definition-import-nested-compdef.yaml @@ -0,0 +1,18 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + import-component-definitions: + - href: file://./component-definition-all-local.yaml + - href: file://../../../e2e/scenarios/validation-composition/component-definition.yaml + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website diff --git a/src/test/unit/common/composition/component-definition-local-and-remote-template.yaml b/src/test/unit/common/composition/component-definition-local-and-remote-template.yaml new file mode 100644 index 00000000..cabdbaa1 --- /dev/null +++ b/src/test/unit/common/composition/component-definition-local-and-remote-template.yaml @@ -0,0 +1,75 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: {{ .const.type }} + title: {{ .const.title }} + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + description: >- + This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: "#a7377430-2328-4dc4-a9e2-b3f31dc1dff9" + text: local template validation + rel: lula + - href: "../validation/validation.trailing-spaces.yaml" + text: validation with spaces that need to be cleaned + rel: lula + - href: https://raw.githubusercontent.com/defenseunicorns/lula/main/src/test/unit/common/validation/validation.tmpl.yaml + text: validation with template from remote + rel: lula + back-matter: + resources: + - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 + rlinks: + - href: lula.dev + description: >- + domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + group: + version: v1 + resource: {{ .const.resources.name }} + namespaces: [{{ .const.resources.namespace }}] + provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } diff --git a/src/test/unit/common/validation/validation.trailing-spaces.yaml b/src/test/unit/common/validation/validation.trailing-spaces.yaml new file mode 100644 index 00000000..f8a6449d --- /dev/null +++ b/src/test/unit/common/validation/validation.trailing-spaces.yaml @@ -0,0 +1,27 @@ +lula-version: ">=v0.2.0" +metadata: + name: Validate pods with label foo=bar + uuid: 123e4567-e89b-12d3-a456-426655440000 +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + version: v1 + resource: pods + namespaces: [validation-test] +provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } From 24a4b0f7c88577ae9e6ad10c87ae6b2922ce6310 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 26 Sep 2024 21:07:26 -0400 Subject: [PATCH 02/11] feat(compose): updated functions --- src/pkg/common/composition/composition.go | 18 +-- src/pkg/common/composition/options.go | 8 +- src/pkg/common/composition/resource-store.go | 3 +- src/pkg/common/types.go | 26 ++-- ...on-local-and-remote-template-composed.yaml | 138 ++++++++++++++++++ 5 files changed, 170 insertions(+), 23 deletions(-) create mode 100644 src/test/unit/common/composition/component-definition-local-and-remote-template-composed.yaml diff --git a/src/pkg/common/composition/composition.go b/src/pkg/common/composition/composition.go index b8c5629c..022a6db3 100644 --- a/src/pkg/common/composition/composition.go +++ b/src/pkg/common/composition/composition.go @@ -21,13 +21,13 @@ import ( type RenderedContent string type CompositionContext struct { - bctx context.Context - model *oscalTypes_1_1_2.OscalCompleteSchema - modelDir string - templateRenderer *template.TemplateRenderer - renderTemplate bool - renderRemote bool - renderType template.RenderType + bctx context.Context + model *oscalTypes_1_1_2.OscalCompleteSchema + modelDir string + templateRenderer *template.TemplateRenderer + renderTemplate bool + renderValidations bool + renderType template.RenderType } func New(ctx context.Context, opts ...Option) (*CompositionContext, error) { @@ -105,8 +105,8 @@ func (ctx *CompositionContext) ComposeComponentDefinitions(compDef *oscalTypes_1 return err } - // template here if remote is specified - if ctx.renderRemote { + // template here if renderTemplate is true + if ctx.renderTemplate { response, err = ctx.templateRenderer.Render(string(response), ctx.renderType) if err != nil { return err diff --git a/src/pkg/common/composition/options.go b/src/pkg/common/composition/options.go index d06b8e33..b9a4bb75 100644 --- a/src/pkg/common/composition/options.go +++ b/src/pkg/common/composition/options.go @@ -23,20 +23,20 @@ func WithModelFromPath(path string) Option { } } -func WithTemplateRenderer(renderTypeString string, renderRemote bool, setOpts []string) Option { +func WithTemplateRenderer(renderTypeString string, renderValidations bool, setOpts []string) Option { return func(ctx *CompositionContext) error { if renderTypeString == "" { if len(setOpts) > 0 { message.Warn("`render` not specified, the --set options will be ignored") } - if renderRemote { - message.Warn("`render` not specified, `render-remote` will be ignored") + if renderValidations { + message.Warn("`render` not specified, `render-validations` will be ignored") } return nil } ctx.renderTemplate = true - ctx.renderRemote = renderRemote + ctx.renderValidations = renderValidations // Get the template render type renderType, err := template.ParseRenderType(renderTypeString) diff --git a/src/pkg/common/composition/resource-store.go b/src/pkg/common/composition/resource-store.go index 1bf0f203..cfb8a7d6 100644 --- a/src/pkg/common/composition/resource-store.go +++ b/src/pkg/common/composition/resource-store.go @@ -129,7 +129,8 @@ func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link, baseDir return nil, fmt.Errorf("error fetching remote resource: %v", err) } - if s.cctx.renderRemote { + // template here if renderValidations is true + if s.cctx.renderValidations { validationBytes, err = s.cctx.templateRenderer.Render(string(validationBytes), s.cctx.renderType) if err != nil { return nil, err diff --git a/src/pkg/common/types.go b/src/pkg/common/types.go index 5438d86c..a1f30e91 100644 --- a/src/pkg/common/types.go +++ b/src/pkg/common/types.go @@ -49,8 +49,21 @@ func (v *Validation) MarshalYaml() ([]byte, error) { // ToResource converts a Validation object to a Resource object func (v *Validation) ToResource() (resource *oscalTypes_1_1_2.Resource, err error) { resourceUuid := uuid.NewUUID() - if v.Metadata.UUID != "" { - resourceUuid = v.Metadata.UUID + title := "Lula Validation" + if v.Metadata != nil { + if v.Metadata.UUID != "" { + resourceUuid = v.Metadata.UUID + } + if v.Metadata.Name != "" { + title = v.Metadata.Name + } + } + + if v.Provider != nil { + if v.Provider.OpaSpec != nil { + // Clean multiline string in rego + CleanMultilineString(v.Provider.OpaSpec.Rego) + } } validationBytes, err := v.MarshalYaml() @@ -58,15 +71,10 @@ func (v *Validation) ToResource() (resource *oscalTypes_1_1_2.Resource, err erro return nil, err } - if v.Provider != nil && v.Provider.OpaSpec != nil { - // Clean multiline string in rego - CleanMultilineString(v.Provider.OpaSpec.Rego) - } - return &oscalTypes_1_1_2.Resource{ - Title: v.Metadata.Name, + Title: title, UUID: resourceUuid, - Description: CleanMultilineString(string(validationBytes)), + Description: string(validationBytes), }, nil } diff --git a/src/test/unit/common/composition/component-definition-local-and-remote-template-composed.yaml b/src/test/unit/common/composition/component-definition-local-and-remote-template-composed.yaml new file mode 100644 index 00000000..161db100 --- /dev/null +++ b/src/test/unit/common/composition/component-definition-local-and-remote-template-composed.yaml @@ -0,0 +1,138 @@ +component-definition: + back-matter: + resources: + - description: |- + domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + group: + version: v1 + resource: test-pod-label + namespaces: [validation-test] + provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + rlinks: + - href: lula.dev + uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} + type: opa + title: Lula Validation + uuid: 0f284e29-8348-47ba-abe3-909cc97d893e + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>=v0.2.0' + metadata: + name: Validate pods with label foo=bar + uuid: 123e4567-e89b-12d3-a456-426655440000 + provider: + opa-spec: + rego: "package validate \n\nimport future.keywords.every\n\nvalidate {\n every + pod in input.podsvt {\n podLabel := pod.metadata.labels.foo\n podLabel + == \"bar\"\n }\n}\n" + type: opa + title: Validate pods with label foo=bar + uuid: 123e4567-e89b-12d3-a456-426655440000 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' + rel: lula + text: local template validation + - href: '#123e4567-e89b-12d3-a456-426655440000' + rel: lula + text: validation with spaces that need to be cleaned + - href: '#0f284e29-8348-47ba-abe3-909cc97d893e' + rel: lula + text: validation with template from remote + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: 2024-09-26T21:05:22.599443-04:00 + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F From 74dfc3bd3666100c3bfb2255813dc0e4d960d5d0 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Fri, 27 Sep 2024 14:30:46 -0400 Subject: [PATCH 03/11] feat(compose): updating tests --- src/cmd/internal.go | 4 +- src/cmd/root.go | 52 ++-- src/cmd/tools/compose.go | 78 +++--- src/cmd/tools/compose_test.go | 7 +- src/pkg/common/composition/composition.go | 42 ++- .../common/composition/composition_test.go | 33 ++- src/pkg/common/composition/options.go | 35 ++- src/pkg/common/oscal/complete-schema.go | 21 ++ src/pkg/common/types.go | 15 +- src/test/e2e/cmd/lula-config.yaml | 1 + src/test/e2e/cmd/main_test.go | 16 +- ...dation-templated-missing-validation.golden | 111 ++++++++ ...ated-no-validation-templated-valid.golden} | 40 ++- .../compose/composed-file-templated.golden | 193 +++++++++++++ .../cmd/testdata/tools/compose/help.golden | 5 +- .../cmd/testdata/tools/template/help.golden | 5 +- src/test/e2e/cmd/tools_compose_test.go | 99 +++---- src/test/e2e/cmd/tools_template_test.go | 9 +- ...nition-import-nested-compdef-composed.yaml | 263 ++++++++++++++++++ ...nition-import-nested-compdef-template.yaml | 18 ++ ...nition-template-valid-validation-tmpl.yaml | 78 ++++++ ...aml => component-definition-template.yaml} | 3 + .../validation/valid-validation.tmpl.yaml | 30 ++ src/test/util/utils.go | 6 +- 24 files changed, 938 insertions(+), 226 deletions(-) create mode 100644 src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden rename src/test/{unit/common/composition/component-definition-local-and-remote-template-composed.yaml => e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden} (78%) create mode 100644 src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden create mode 100644 src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml create mode 100644 src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml create mode 100644 src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml rename src/test/unit/common/composition/{component-definition-local-and-remote-template.yaml => component-definition-template.yaml} (95%) create mode 100644 src/test/unit/common/validation/valid-validation.tmpl.yaml diff --git a/src/cmd/internal.go b/src/cmd/internal.go index 90fe93fa..00aefbb5 100644 --- a/src/cmd/internal.go +++ b/src/cmd/internal.go @@ -22,6 +22,8 @@ var genCLIDocs = &cobra.Command{ Use: "gen-cli-docs", Short: "Generate CLI command documentation", RunE: func(_ *cobra.Command, _ []string) error { + rootCmd := newRootCmd() // Create a new root command instance + // Don't include the datestamp in the output rootCmd.DisableAutoGenTag = true @@ -72,7 +74,5 @@ type: docs } func init() { - rootCmd.AddCommand(internalCmd) - internalCmd.AddCommand(genCLIDocs) } diff --git a/src/cmd/root.go b/src/cmd/root.go index 2f13ebd9..db916bae 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -15,28 +15,15 @@ import ( var LogLevelCLI string -var rootCmd = &cobra.Command{ - Use: "lula", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - common.SetupClI(LogLevelCLI) - }, - Short: "Risk Management as Code", - Long: `Real Time Risk Transparency through automated validation`, -} - -func RootCommand() *cobra.Command { - - cmd := rootCmd - - return cmd -} - -func Execute() { - - cobra.CheckErr(rootCmd.Execute()) -} - -func init() { +func newRootCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "lula", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + common.SetupClI(LogLevelCLI) + }, + Short: "Risk Management as Code", + Long: `Real Time Risk Transparency through automated validation`, + } v := common.InitViper() @@ -47,10 +34,21 @@ func init() { console.ConsoleCommand(), } - rootCmd.AddCommand(commands...) - tools.Include(rootCmd) - version.Include(rootCmd) - dev.Include(rootCmd) + cmd.AddCommand(commands...) + tools.Include(cmd) + version.Include(cmd) + dev.Include(cmd) + cmd.AddCommand(internalCmd) + + cmd.PersistentFlags().StringVarP(&LogLevelCLI, "log-level", "l", v.GetString(common.VLogLevel), "Log level when running Lula. Valid options are: warn, info, debug, trace") + + return cmd +} + +func RootCommand() *cobra.Command { + return newRootCmd() +} - rootCmd.PersistentFlags().StringVarP(&LogLevelCLI, "log-level", "l", v.GetString(common.VLogLevel), "Log level when running Lula. Valid options are: warn, info, debug, trace") +func Execute() { + cobra.CheckErr(newRootCmd().Execute()) } diff --git a/src/cmd/tools/compose.go b/src/cmd/tools/compose.go index ce256ac1..16ed0bff 100644 --- a/src/cmd/tools/compose.go +++ b/src/cmd/tools/compose.go @@ -3,7 +3,6 @@ package tools import ( "context" "fmt" - "os" "path/filepath" "strings" @@ -49,41 +48,38 @@ func ComposeCommand() *cobra.Command { composeSpinner := message.NewProgressSpinner("Composing %s", inputFile) defer composeSpinner.Stop() - // TODO: check if remote or local? - _, err := os.Stat(inputFile) - if os.IsNotExist(err) { - return fmt.Errorf("input-file: %v does not exist - unable to digest document", inputFile) - } - - // Update path if relative - path := inputFile + // Update input/output paths if filepath.IsLocal(inputFile) { - path = filepath.Join(filepath.Dir(inputFile), filepath.Base(inputFile)) + inputFile = filepath.Join(filepath.Dir(inputFile), filepath.Base(inputFile)) } if outputFile == "" { outputFile = GetDefaultOutputFile(inputFile) + } else if filepath.IsLocal(outputFile) { + outputFile = filepath.Join(filepath.Dir(outputFile), filepath.Base(outputFile)) } - opts := []composition.Option{ - composition.WithModelFromPath(path), - composition.WithTemplateRenderer(renderTypeString, renderValidations, setOpts), + // Check if output file contains a valid OSCAL model + _, err := oscal.ValidOSCALModelAtPath(outputFile) + if err != nil { + message.Fatalf(err, "Output file %s is not a valid OSCAL model: %v", outputFile, err) } - compositionCtx, err := composition.New(context.Background(), opts...) + // Compose the OSCAL model + constants, variables, err := common.GetTemplateConfig() if err != nil { - return fmt.Errorf("error creating composition context: %v", err) + return fmt.Errorf("error getting template config: %v", err) } - err = compositionCtx.ComposeFromPath(path) - if err != nil { - return fmt.Errorf("error composing model: %v", err) + opts := []composition.Option{ + composition.WithModelFromLocalPath(inputFile), + composition.WithRenderSettings(renderTypeString, renderValidations), + composition.WithTemplateRenderer(renderTypeString, constants, variables, setOpts), } - // Write the composed OSCAL model to a file - err = oscal.WriteOscalModel(outputFile, compositionCtx.GetModel()) + err = Compose(cmd.Context(), inputFile, outputFile, opts...) if err != nil { - return fmt.Errorf("error writing composed model: %v", err) + return fmt.Errorf("error composing model: %v", err) } message.Infof("Composed OSCAL Component Definition to: %s", outputFile) @@ -108,26 +104,26 @@ func init() { } // Compose composes an OSCAL model from a file path -// func Compose(inputFile, outputFile string, templateRenderer *template.TemplateRenderer, renderType template.RenderType) error { -// _, err := os.Stat(inputFile) -// if os.IsNotExist(err) { -// return fmt.Errorf("input file: %v does not exist - unable to compose document", inputFile) -// } - -// // Compose the OSCAL model -// model, err := composition.ComposeFromPath(inputFile) -// if err != nil { -// return err -// } - -// // Write the composed OSCAL model to a file -// err = oscal.WriteOscalModel(outputFile, model) -// if err != nil { -// return err -// } - -// return nil -// } +func Compose(ctx context.Context, inputFile, outputFile string, opts ...composition.Option) error { + // Compose the OSCAL model + compositionCtx, err := composition.New(opts...) + if err != nil { + return fmt.Errorf("error creating composition context: %v", err) + } + + model, err := compositionCtx.ComposeFromPath(ctx, inputFile) + if err != nil { + return err + } + + // Write the composed OSCAL model to a file + err = oscal.WriteOscalModel(outputFile, model) + if err != nil { + return err + } + + return nil +} // GetDefaultOutputFile returns the default output file name func GetDefaultOutputFile(inputFile string) string { diff --git a/src/cmd/tools/compose_test.go b/src/cmd/tools/compose_test.go index 5d366623..853c6610 100644 --- a/src/cmd/tools/compose_test.go +++ b/src/cmd/tools/compose_test.go @@ -1,11 +1,13 @@ package tools_test import ( + "context" "os" "path/filepath" "testing" "github.com/defenseunicorns/lula/src/cmd/tools" + "github.com/defenseunicorns/lula/src/pkg/common/composition" "github.com/defenseunicorns/lula/src/pkg/common/oscal" ) @@ -18,9 +20,10 @@ func TestComposeComponentDefinition(t *testing.T) { t.Parallel() tempDir := t.TempDir() outputFile := filepath.Join(tempDir, "output.yaml") + ctx := context.Background() t.Run("composes valid component definition", func(t *testing.T) { - err := tools.Compose(validInputFile, outputFile) + err := tools.Compose(ctx, validInputFile, outputFile, composition.WithModelFromLocalPath(validInputFile)) if err != nil { t.Fatalf("error composing component definition: %s", err) } @@ -44,7 +47,7 @@ func TestComposeComponentDefinition(t *testing.T) { }) t.Run("invalid component definition throws error", func(t *testing.T) { - err := tools.Compose(invalidInputFile, outputFile) + err := tools.Compose(ctx, invalidInputFile, outputFile, composition.WithModelFromLocalPath(invalidInputFile)) if err == nil { t.Fatal("expected error composing invalid component definition") } diff --git a/src/pkg/common/composition/composition.go b/src/pkg/common/composition/composition.go index 022a6db3..8e724f4f 100644 --- a/src/pkg/common/composition/composition.go +++ b/src/pkg/common/composition/composition.go @@ -21,8 +21,6 @@ import ( type RenderedContent string type CompositionContext struct { - bctx context.Context - model *oscalTypes_1_1_2.OscalCompleteSchema modelDir string templateRenderer *template.TemplateRenderer renderTemplate bool @@ -30,7 +28,7 @@ type CompositionContext struct { renderType template.RenderType } -func New(ctx context.Context, opts ...Option) (*CompositionContext, error) { +func New(opts ...Option) (*CompositionContext, error) { var compositionCtx CompositionContext for _, opt := range opts { @@ -42,46 +40,42 @@ func New(ctx context.Context, opts ...Option) (*CompositionContext, error) { return &compositionCtx, nil } -func (ctx *CompositionContext) GetModel() *oscalTypes_1_1_2.OscalCompleteSchema { - return ctx.model -} - // ComposeFromPath composes an OSCAL model from a file path -func (ctx *CompositionContext) ComposeFromPath(path string) (err error) { +func (cc *CompositionContext) ComposeFromPath(ctx context.Context, path string) (model *oscalTypes_1_1_2.OscalCompleteSchema, err error) { data, err := os.ReadFile(path) if err != nil { - return err + return nil, err } // Template if renderTemplate is true -> Only renders the local data (e.g., what is in the file) - if ctx.renderTemplate { - data, err = ctx.templateRenderer.Render(string(data), ctx.renderType) + if cc.renderTemplate { + data, err = cc.templateRenderer.Render(string(data), cc.renderType) if err != nil { - return err + return nil, err } } - ctx.model, err = oscal.NewOscalModel(data) + model, err = oscal.NewOscalModel(data) if err != nil { - return err + return nil, err } - err = ctx.ComposeComponentDefinitions(ctx.model.ComponentDefinition, ctx.modelDir) + err = cc.ComposeComponentDefinitions(ctx, model.ComponentDefinition, cc.modelDir) if err != nil { - return err + return nil, err } - return nil + return model, nil } // ComposeComponentDefinitions composes an OSCAL component definition by adding the remote resources to the back matter and updating with back matter links. -func (ctx *CompositionContext) ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { +func (cc *CompositionContext) ComposeComponentDefinitions(ctx context.Context, compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { if compDef == nil { return fmt.Errorf("component definition is nil") } // Compose the component validations - err := ctx.ComposeComponentValidations(compDef, baseDir) + err := cc.ComposeComponentValidations(ctx, compDef, baseDir) if err != nil { return err } @@ -106,8 +100,8 @@ func (ctx *CompositionContext) ComposeComponentDefinitions(compDef *oscalTypes_1 } // template here if renderTemplate is true - if ctx.renderTemplate { - response, err = ctx.templateRenderer.Render(string(response), ctx.renderType) + if cc.renderTemplate { + response, err = cc.templateRenderer.Render(string(response), cc.renderType) if err != nil { return err } @@ -122,7 +116,7 @@ func (ctx *CompositionContext) ComposeComponentDefinitions(compDef *oscalTypes_1 for _, importDef := range componentDefs { // Reconcile the base directory from the import component definition href baseDir = network.GetLocalFileDir(importComponentDef.Href, baseDir) - err = ctx.ComposeComponentDefinitions(importDef, baseDir) + err = cc.ComposeComponentDefinitions(ctx, importDef, baseDir) if err != nil { return err } @@ -143,13 +137,13 @@ func (ctx *CompositionContext) ComposeComponentDefinitions(compDef *oscalTypes_1 } // ComposeComponentValidations compiles the component validations by adding the remote resources to the back matter and updating with back matter links. -func (ctx *CompositionContext) ComposeComponentValidations(compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { +func (cc *CompositionContext) ComposeComponentValidations(ctx context.Context, compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { if compDef == nil { return fmt.Errorf("component definition is nil") } - resourceMap := NewResourceStoreFromBackMatter(ctx, compDef.BackMatter) + resourceMap := NewResourceStoreFromBackMatter(cc, compDef.BackMatter) // If there are no components, there is nothing to do if compDef.Components == nil { diff --git a/src/pkg/common/composition/composition_test.go b/src/pkg/common/composition/composition_test.go index 17fbdbab..d9a600cb 100644 --- a/src/pkg/common/composition/composition_test.go +++ b/src/pkg/common/composition/composition_test.go @@ -22,7 +22,7 @@ const ( compDefMultiImport = "../../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" // TODO: add tests for templating - compDefNestedImport = "../../../test/unit/common/composition/component-definition-import-nested-compdef" + compDefNestedImport = "../../../test/unit/common/composition/component-definition-import-nested-compdef.yaml" compDefMultiTmpl = "../../../test/unit/common/composition/component-definition-local-and-remote-template.yaml" // Also, add cmd tests...? compare golden composed file? @@ -31,19 +31,20 @@ const ( func TestComposeFromPath(t *testing.T) { test := func(t *testing.T, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { t.Helper() + ctx := context.Background() - options := append([]composition.Option{composition.WithModelFromPath(path)}, opts...) - ctx, err := composition.New(context.Background(), options...) + options := append([]composition.Option{composition.WithModelFromLocalPath(path)}, opts...) + cc, err := composition.New(options...) if err != nil { return nil, err } - err = ctx.ComposeFromPath(path) + model, err := cc.ComposeFromPath(ctx, path) if err != nil { return nil, err } - return ctx.GetModel(), nil + return model, nil } t.Run("No imports, local validations", func(t *testing.T) { @@ -117,21 +118,24 @@ func TestComposeFromPath(t *testing.T) { func TestComposeComponentDefinitions(t *testing.T) { test := func(t *testing.T, compDef *oscalTypes_1_1_2.ComponentDefinition, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { t.Helper() + ctx := context.Background() - options := append([]composition.Option{composition.WithModelFromPath(path)}, opts...) - ctx, err := composition.New(context.Background(), options...) + options := append([]composition.Option{composition.WithModelFromLocalPath(path)}, opts...) + cc, err := composition.New(options...) if err != nil { return nil, err } baseDir := filepath.Dir(path) - err = ctx.ComposeComponentDefinitions(compDef, baseDir) + err = cc.ComposeComponentDefinitions(ctx, compDef, baseDir) if err != nil { return nil, err } - return ctx.GetModel(), nil + return &oscalTypes_1_1_2.OscalCompleteSchema{ + ComponentDefinition: compDef, + }, nil } t.Run("No imports, local validations", func(t *testing.T) { @@ -228,21 +232,24 @@ func TestComposeComponentDefinitions(t *testing.T) { func TestCompileComponentValidations(t *testing.T) { test := func(t *testing.T, compDef *oscalTypes_1_1_2.ComponentDefinition, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { t.Helper() + ctx := context.Background() - options := append([]composition.Option{composition.WithModelFromPath(path)}, opts...) - ctx, err := composition.New(context.Background(), options...) + options := append([]composition.Option{composition.WithModelFromLocalPath(path)}, opts...) + cc, err := composition.New(options...) if err != nil { return nil, err } baseDir := filepath.Dir(path) - err = ctx.ComposeComponentValidations(compDef, baseDir) + err = cc.ComposeComponentValidations(ctx, compDef, baseDir) if err != nil { return nil, err } - return ctx.GetModel(), nil + return &oscalTypes_1_1_2.OscalCompleteSchema{ + ComponentDefinition: compDef, + }, nil } t.Run("all local", func(t *testing.T) { diff --git a/src/pkg/common/composition/options.go b/src/pkg/common/composition/options.go index b9a4bb75..51d95384 100644 --- a/src/pkg/common/composition/options.go +++ b/src/pkg/common/composition/options.go @@ -2,9 +2,9 @@ package composition import ( "fmt" + "os" "path/filepath" - "github.com/defenseunicorns/go-oscal/src/pkg/files" "github.com/defenseunicorns/lula/src/cmd/common" "github.com/defenseunicorns/lula/src/internal/template" "github.com/defenseunicorns/lula/src/pkg/message" @@ -13,28 +13,29 @@ import ( type Option func(*CompositionContext) error // TODO: add remote option? -func WithModelFromPath(path string) Option { +func WithModelFromLocalPath(path string) Option { return func(ctx *CompositionContext) error { - if err := files.IsJsonOrYaml(path); err != nil { - return fmt.Errorf("invalid file extension: %s, requires .json or .yaml", path) + _, err := os.Stat(path) + if os.IsNotExist(err) { + return fmt.Errorf("input-file: %v does not exist - unable to digest document", path) } + ctx.modelDir = filepath.Dir(path) + return nil } } -func WithTemplateRenderer(renderTypeString string, renderValidations bool, setOpts []string) Option { +func WithRenderSettings(renderTypeString string, renderValidations bool) Option { return func(ctx *CompositionContext) error { if renderTypeString == "" { - if len(setOpts) > 0 { - message.Warn("`render` not specified, the --set options will be ignored") - } + ctx.renderTemplate = false + ctx.renderValidations = false if renderValidations { message.Warn("`render` not specified, `render-validations` will be ignored") } return nil } - ctx.renderTemplate = true ctx.renderValidations = renderValidations @@ -46,10 +47,18 @@ func WithTemplateRenderer(renderTypeString string, renderValidations bool, setOp } ctx.renderType = renderType - // Get constants and variables for templating from viper config - constants, variables, err := common.GetTemplateConfig() - if err != nil { - return fmt.Errorf("error getting template config: %v", err) + return nil + } +} + +func WithTemplateRenderer(renderTypeString string, constants map[string]interface{}, variables []template.VariableConfig, setOpts []string) Option { + return func(ctx *CompositionContext) error { + if renderTypeString == "" { + ctx.renderTemplate = false + if len(setOpts) > 0 { + message.Warn("`render` not specified, the --set options will be ignored") + } + return nil } // Get overrides from setOpts flag diff --git a/src/pkg/common/oscal/complete-schema.go b/src/pkg/common/oscal/complete-schema.go index 0d768f62..352ac2ff 100644 --- a/src/pkg/common/oscal/complete-schema.go +++ b/src/pkg/common/oscal/complete-schema.go @@ -224,6 +224,27 @@ func GetOscalModel(model *oscalTypes_1_1_2.OscalModels) (modelType string, err e } +// ValidOSCALModelAtPath takes a path and returns a bool indicating if the model exists/is valid +// bool = T/F that oscal model exists, error = if not nil OSCAL model is invalid +func ValidOSCALModelAtPath(path string) (bool, error) { + _, err := os.Stat(path) + if err != nil { + return false, nil + } + + data, err := os.ReadFile(path) + if err != nil { + return true, err + } + + _, err = NewOscalModel(data) + if err != nil { + return true, err + } + + return true, nil +} + // InjectIntoOSCALModel takes a model target and a map[string]interface{} of values to inject into the model func InjectIntoOSCALModel(target *oscalTypes_1_1_2.OscalModels, values map[string]interface{}, path string) (*oscalTypes_1_1_2.OscalModels, error) { // If the target is nil, return an error diff --git a/src/pkg/common/types.go b/src/pkg/common/types.go index a1f30e91..1f9eeb70 100644 --- a/src/pkg/common/types.go +++ b/src/pkg/common/types.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "regexp" "strings" "github.com/defenseunicorns/go-oscal/src/pkg/uuid" @@ -51,18 +52,23 @@ func (v *Validation) ToResource() (resource *oscalTypes_1_1_2.Resource, err erro resourceUuid := uuid.NewUUID() title := "Lula Validation" if v.Metadata != nil { - if v.Metadata.UUID != "" { + if v.Metadata.UUID != "" && checkValidUuid(v.Metadata.UUID) { resourceUuid = v.Metadata.UUID } if v.Metadata.Name != "" { title = v.Metadata.Name } + } else { + v.Metadata = &Metadata{} } + // Update the metadata for the validation + v.Metadata.UUID = resourceUuid + v.Metadata.Name = title if v.Provider != nil { if v.Provider.OpaSpec != nil { // Clean multiline string in rego - CleanMultilineString(v.Provider.OpaSpec.Rego) + v.Provider.OpaSpec.Rego = CleanMultilineString(v.Provider.OpaSpec.Rego) } } @@ -167,3 +173,8 @@ func (validation *Validation) ToLulaValidation(uuid string) (lulaValidation type return lulaValidation, nil } + +func checkValidUuid(uuid string) bool { + re := regexp.MustCompile(`^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[45][0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$`) + return re.MatchString(uuid) +} diff --git a/src/test/e2e/cmd/lula-config.yaml b/src/test/e2e/cmd/lula-config.yaml index 911f39ff..001c2a3f 100644 --- a/src/test/e2e/cmd/lula-config.yaml +++ b/src/test/e2e/cmd/lula-config.yaml @@ -1,6 +1,7 @@ constants: type: software title: lula + templatedCompDefFile: component-definition-template.yaml resources: name: test-pod-label diff --git a/src/test/e2e/cmd/main_test.go b/src/test/e2e/cmd/main_test.go index 2a3184e5..5ff4649d 100644 --- a/src/test/e2e/cmd/main_test.go +++ b/src/test/e2e/cmd/main_test.go @@ -9,8 +9,8 @@ import ( "strings" "testing" - "github.com/defenseunicorns/lula/src/cmd" "github.com/defenseunicorns/lula/src/test/util" + "github.com/spf13/cobra" ) var updateGolden = flag.Bool("update", false, "update golden files") @@ -20,11 +20,7 @@ func TestMain(m *testing.M) { m.Run() } -func runCmdTest(t *testing.T, goldenFileName string, expectError bool, cmdArgs ...string) error { - t.Helper() - - rootCmd := cmd.RootCommand() - +func runCmdTest(t *testing.T, goldenFileName string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { _, output, err := util.ExecuteCommand(rootCmd, cmdArgs...) if err != nil { if !expectError { @@ -41,14 +37,10 @@ func runCmdTest(t *testing.T, goldenFileName string, expectError bool, cmdArgs . return nil } -func runCmdTestWithOutputFile(t *testing.T, goldenFileName string, outExt string, expectError bool, cmdArgs ...string) error { - t.Helper() - - tempFileName := fmt.Sprintf("output-%s.%s", goldenFileName, outExt) +func runCmdTestWithOutputFile(t *testing.T, goldenFileName string, outExt string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { + tempFileName := fmt.Sprintf("output.%s", outExt) defer os.Remove(tempFileName) - rootCmd := cmd.RootCommand() - cmdArgs = append(cmdArgs, "-o", tempFileName) _, _, err := util.ExecuteCommand(rootCmd, cmdArgs...) if err != nil { diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden new file mode 100644 index 00000000..5468c44a --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden @@ -0,0 +1,111 @@ +component-definition: + back-matter: + resources: + - description: |- + domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + group: + version: v1 + resource: test-pod-label + namespaces: [validation-test] + provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + rlinks: + - href: lula.dev + uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>=v0.2.0' + metadata: + name: Validate pods with label foo=bar + uuid: 7a4b7514-f83c-4131-b16c-82537676b098 + provider: + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + type: opa + title: Validate pods with label foo=bar + uuid: 7a4b7514-f83c-4131-b16c-82537676b098 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' + rel: lula + text: local template validation + - href: '#dcd13325-6070-43b4-bb4f-94a684f68369' + rel: lula + text: local path template validation + - href: '#7a4b7514-f83c-4131-b16c-82537676b098' + rel: lula + text: validation with spaces that need to be cleaned + - href: '#ee10395d-44eb-4407-9ac2-cd78c3bb470e' + rel: lula + text: validation with template from remote + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/unit/common/composition/component-definition-local-and-remote-template-composed.yaml b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden similarity index 78% rename from src/test/unit/common/composition/component-definition-local-and-remote-template-composed.yaml rename to src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden index 161db100..a5befd7b 100644 --- a/src/test/unit/common/composition/component-definition-local-and-remote-template-composed.yaml +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden @@ -38,13 +38,16 @@ component-definition: name: podvt resource-rule: group: "" - name: test-pod-label + name: '{{ .const.resources.name }}' namespaces: - - validation-test + - '{{ .const.resources.namespace }}' resource: pods version: v1 type: kubernetes lula-version: "" + metadata: + name: Lula Validation + uuid: 8f0fe0ce-14dc-468c-986a-145a3238ed10 provider: opa-spec: rego: | @@ -57,8 +60,8 @@ component-definition: # Validation result validate if { - { "one", "two", "three" } == { "one", "two", "three" } - "this-should-be-overridden" == "my-env-var" + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" "{{ .var.some_lula_secret }}" == "********" } msg = validate.msg @@ -66,7 +69,7 @@ component-definition: value_of_my_secret := {{ .var.some_lula_secret }} type: opa title: Lula Validation - uuid: 0f284e29-8348-47ba-abe3-909cc97d893e + uuid: 8f0fe0ce-14dc-468c-986a-145a3238ed10 - description: | domain: kubernetes-spec: @@ -85,15 +88,23 @@ component-definition: lula-version: '>=v0.2.0' metadata: name: Validate pods with label foo=bar - uuid: 123e4567-e89b-12d3-a456-426655440000 + uuid: 969402b8-7a90-411a-806b-f200c36d60da provider: opa-spec: - rego: "package validate \n\nimport future.keywords.every\n\nvalidate {\n every - pod in input.podsvt {\n podLabel := pod.metadata.labels.foo\n podLabel - == \"bar\"\n }\n}\n" + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } type: opa title: Validate pods with label foo=bar - uuid: 123e4567-e89b-12d3-a456-426655440000 + uuid: 969402b8-7a90-411a-806b-f200c36d60da components: - control-implementations: - description: Validate generic security requirements @@ -104,10 +115,13 @@ component-definition: - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' rel: lula text: local template validation - - href: '#123e4567-e89b-12d3-a456-426655440000' + - href: '#8f0fe0ce-14dc-468c-986a-145a3238ed10' + rel: lula + text: local path template validation + - href: '#969402b8-7a90-411a-806b-f200c36d60da' rel: lula text: validation with spaces that need to be cleaned - - href: '#0f284e29-8348-47ba-abe3-909cc97d893e' + - href: '#db9a9007-dd7d-486a-9f03-18e5b30c80c3' rel: lula text: validation with template from remote uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD @@ -124,7 +138,7 @@ component-definition: type: software uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 metadata: - last-modified: 2024-09-26T21:05:22.599443-04:00 + last-modified: XXX oscal-version: 1.1.2 parties: - links: diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden new file mode 100644 index 00000000..5a432ae6 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden @@ -0,0 +1,193 @@ +component-definition: + back-matter: + resources: + - description: |- + domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + group: + version: v1 + resource: test-pod-label + namespaces: [validation-test] + provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + rlinks: + - href: lula.dev + uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Lula Validation + uuid: b2023350-8b84-47f6-bc51-3c1cf5ec0166 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "" == "********" + } + msg = validate.msg + + value_of_my_secret := + type: opa + title: Lula Validation + uuid: b2023350-8b84-47f6-bc51-3c1cf5ec0166 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Lula Validation + uuid: 3e8fb2f0-ceec-4108-bdbe-a564f35edf66 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "" == "********" + } + msg = validate.msg + + value_of_my_secret := + type: opa + title: Lula Validation + uuid: 3e8fb2f0-ceec-4108-bdbe-a564f35edf66 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>=v0.2.0' + metadata: + name: Validate pods with label foo=bar + uuid: fcd10300-1520-48f7-9f56-f30eeea0420c + provider: + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + type: opa + title: Validate pods with label foo=bar + uuid: fcd10300-1520-48f7-9f56-f30eeea0420c + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' + rel: lula + text: local template validation + - href: '#b2023350-8b84-47f6-bc51-3c1cf5ec0166' + rel: lula + text: local path template validation + - href: '#fcd10300-1520-48f7-9f56-f30eeea0420c' + rel: lula + text: validation with spaces that need to be cleaned + - href: '#3e8fb2f0-ceec-4108-bdbe-a564f35edf66' + rel: lula + text: validation with template from remote + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/help.golden b/src/test/e2e/cmd/testdata/tools/compose/help.golden index 68d0f9d1..0c0f0705 100644 --- a/src/test/e2e/cmd/testdata/tools/compose/help.golden +++ b/src/test/e2e/cmd/testdata/tools/compose/help.golden @@ -7,7 +7,7 @@ Supports templating of the composed component definition with the following conf - To perform any manual overrides to the template data, specify '--set, -s' with the format '.const.key=value' or '.var.key=value' Usage: - lula tools compose [flags] + compose [flags] Examples: @@ -25,6 +25,3 @@ Flags: -r, --render string values to render the template with, options are: masked, constants, non-sensitive, all --render-validations extend render to remote Lula Validations -s, --set strings set value overrides for templated data - -Global Flags: - -l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info") diff --git a/src/test/e2e/cmd/testdata/tools/template/help.golden b/src/test/e2e/cmd/testdata/tools/template/help.golden index 4b858bed..1a3761e3 100644 --- a/src/test/e2e/cmd/testdata/tools/template/help.golden +++ b/src/test/e2e/cmd/testdata/tools/template/help.golden @@ -1,7 +1,7 @@ Resolving templated artifacts with configuration data Usage: - lula tools template [flags] + template [flags] Examples: @@ -27,6 +27,3 @@ Flags: -o, --output-file string the path to the output file. If not specified, the output file will be directed to stdout -r, --render string values to render the template with, options are: masked, constants, non-sensitive, all (default "masked") -s, --set strings set a value in the template data - -Global Flags: - -l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info") diff --git a/src/test/e2e/cmd/tools_compose_test.go b/src/test/e2e/cmd/tools_compose_test.go index edffb55d..00858baf 100644 --- a/src/test/e2e/cmd/tools_compose_test.go +++ b/src/test/e2e/cmd/tools_compose_test.go @@ -2,73 +2,50 @@ package cmd_test import ( "testing" + + "github.com/defenseunicorns/lula/src/cmd/tools" ) func TestToolsComposeCommand(t *testing.T) { test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { - t.Helper() - - cmdArgs := []string{"tools", "compose"} - cmdArgs = append(cmdArgs, args...) + rootCmd := tools.ComposeCommand() - return runCmdTest(t, "tools/compose/"+goldenFileName, expectError, cmdArgs...) + return runCmdTest(t, "tools/compose/"+goldenFileName, expectError, rootCmd, args...) } - testVsOutput := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { - t.Helper() + testAgainstOutputFile := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { + rootCmd := tools.ComposeCommand() - cmdArgs := []string{"tools", "compose"} - cmdArgs = append(cmdArgs, args...) - - return runCmdTestWithOutputFile(t, "tools/compose/"+goldenFileName, "yaml", expectError, cmdArgs...) + return runCmdTestWithOutputFile(t, "tools/compose/"+goldenFileName, "yaml", expectError, rootCmd, args...) } t.Run("Compose Validation", func(t *testing.T) { - err := testVsOutput(t, "composed-file", false, "-f", "../../unit/common/composition/component-definition-all-local.yaml") + err := testAgainstOutputFile(t, "composed-file", false, "-f", "../../unit/common/composition/component-definition-all-local.yaml") if err != nil { t.Fatal(err) } }) - // t.Run("Template Validation with env vars", func(t *testing.T) { - // os.Setenv("LULA_VAR_SOME_ENV_VAR", "my-env-var") - // defer os.Unsetenv("LULA_VAR_SOME_ENV_VAR") - // err := test(t, "validation_with_env_vars", false, "-f", "../../unit/common/validation/validation.tmpl.yaml") - // if err != nil { - // t.Fatal(err) - // } - // }) - - // t.Run("Template Validation with set", func(t *testing.T) { - // err := test(t, "validation_with_set", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", ".const.resources.name=foo") - // if err != nil { - // t.Fatal(err) - // } - // }) - - // t.Run("Template Validation for all", func(t *testing.T) { - // os.Setenv("LULA_VAR_SOME_LULA_SECRET", "env-secret") - // defer os.Unsetenv("LULA_VAR_SOME_LULA_SECRET") - // err := test(t, "validation_all", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "all") - // if err != nil { - // t.Fatal(err) - // } - // }) - - // t.Run("Template Validation for non-sensitive", func(t *testing.T) { - // err := test(t, "validation_non_sensitive", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "non-sensitive") - // if err != nil { - // t.Fatal(err) - // } - // }) + t.Run("Compose Validation with templating", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated", false, "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "all", "--render-validations") + if err != nil { + t.Fatal(err) + } + }) - // t.Run("Template Validation for constants", func(t *testing.T) { - // err := test(t, "validation_constants", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "constants") - // if err != nil { - // t.Fatal(err) - // } - // }) + t.Run("Compose Validation with no templating on validations", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-no-validation-templated-missing-validation", false, "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "all") + if err != nil { + t.Fatal(err) + } + }) + t.Run("Compose Validation with no templating on validations for valid validation template", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-no-validation-templated-valid", false, "-f", "../../unit/common/composition/component-definition-template-valid-validation-tmpl.yaml", "-r", "all") + if err != nil { + t.Fatal(err) + } + }) t.Run("Test help", func(t *testing.T) { err := test(t, "help", false, "--help") @@ -77,17 +54,17 @@ func TestToolsComposeCommand(t *testing.T) { } }) - // t.Run("Template Validation - invalid file error", func(t *testing.T) { - // err := test(t, "empty", true, "-f", "not-a-file.yaml") - // if err != nil { - // t.Fatal(err) - // } - // }) + t.Run("Test Compose - invalid file error", func(t *testing.T) { + err := test(t, "empty", true, "-f", "not-a-file.yaml") + if err != nil { + t.Fatal(err) + } + }) - // t.Run("Template Validation - invalid file schema error", func(t *testing.T) { - // err := test(t, "empty", true, "-f", "../../unit/common/validation/validation.bad.tmpl.yaml") - // if err != nil { - // t.Fatal(err) - // } - // }) + t.Run("Test Compose - invalid file schema error", func(t *testing.T) { + err := test(t, "empty", true, "-f", "../../unit/common/composition/component-definition-template.yaml") + if err != nil { + t.Fatal(err) + } + }) } diff --git a/src/test/e2e/cmd/tools_template_test.go b/src/test/e2e/cmd/tools_template_test.go index 12ca7024..268a87e9 100644 --- a/src/test/e2e/cmd/tools_template_test.go +++ b/src/test/e2e/cmd/tools_template_test.go @@ -4,6 +4,8 @@ import ( "os" "testing" + + "github.com/defenseunicorns/lula/src/cmd/tools" ) // var updateGolden = flag.Bool("update", false, "update golden files") @@ -11,12 +13,9 @@ import ( func TestToolsTemplateCommand(t *testing.T) { test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { - t.Helper() - - cmdArgs := []string{"tools", "template"} - cmdArgs = append(cmdArgs, args...) + rootCmd := tools.TemplateCommand() - return runCmdTest(t, "tools/template/"+goldenFileName, expectError, cmdArgs...) + return runCmdTest(t, "tools/template/"+goldenFileName, expectError, rootCmd, args...) } t.Run("Template Validation", func(t *testing.T) { diff --git a/src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml b/src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml new file mode 100644 index 00000000..35976eb1 --- /dev/null +++ b/src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml @@ -0,0 +1,263 @@ +component-definition: + back-matter: + resources: + - description: |- + domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + group: + version: v1 + resource: pods + namespaces: [validation-test] + provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + rlinks: + - href: lula.dev + uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>= v0.1.0' + metadata: + name: Kyverno validate pods with label foo=bar + uuid: 2d9858bc-fb54-42e7-a928-43f840ac0ae6 + provider: + kyverno-spec: + policy: + apiVersion: json.kyverno.io/v1alpha1 + kind: ValidatingPolicy + metadata: + creationTimestamp: null + name: labels + spec: + rules: + - assert: + all: + - check: + ~.podsvt: + metadata: + labels: + foo: bar + name: foo-label-exists + type: kyverno + title: Kyverno validate pods with label foo=bar + uuid: 2d9858bc-fb54-42e7-a928-43f840ac0ae6 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>= v0.2.0' + metadata: + name: Kyverno validate pods with label foo=bar + uuid: 88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288 + provider: + kyverno-spec: + policy: + apiVersion: json.kyverno.io/v1alpha1 + kind: ValidatingPolicy + metadata: + creationTimestamp: null + name: labels + spec: + rules: + - assert: + all: + - check: + ~.podsvt: + metadata: + labels: + foo: bar + name: foo-label-exists + type: kyverno + title: Kyverno validate pods with label foo=bar + uuid: 88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288 + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>= v0.1.0' + metadata: + name: Validate pods with label foo=bar + uuid: 7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e + provider: + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + type: opa + title: Validate pods with label foo=bar + uuid: 7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>= v0.2.0' + metadata: + name: Validate pods with label foo=bar + uuid: 7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f + provider: + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + type: opa + title: Validate pods with label foo=bar + uuid: 7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podsvt + resource-rule: + group: "" + name: "" + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: '>= v0.2.0' + metadata: + name: Validate pods with label foo=bar + uuid: 9d09b4fc-1a82-4434-9fbe-392935347a84 + provider: + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } + type: opa + title: Validate pods with label foo=bar + uuid: 9d09b4fc-1a82-4434-9fbe-392935347a84 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' + rel: lula + - href: '#7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e' + rel: lula + - href: '#2d9858bc-fb54-42e7-a928-43f840ac0ae6' + rel: lula + - href: '#7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f' + rel: lula + - href: '#9d09b4fc-1a82-4434-9fbe-392935347a84' + rel: lula + - href: '#9d09b4fc-1a82-4434-9fbe-392935347a84' + rel: lula + - href: '#88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288' + rel: lula + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: 2024-09-27T13:33:37.442137-04:00 + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: 955a7c0e-3288-4410-8d6b-199200723a81 diff --git a/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml b/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml new file mode 100644 index 00000000..18d63cfe --- /dev/null +++ b/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml @@ -0,0 +1,18 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + import-component-definitions: + - href: file://./{{ .const.templatedCompDefFile }}.yaml + - href: file://../../../e2e/scenarios/validation-composition/component-definition.yaml + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website diff --git a/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml b/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml new file mode 100644 index 00000000..486a0e54 --- /dev/null +++ b/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml @@ -0,0 +1,78 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: {{ .const.type }} + title: {{ .const.title }} + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + description: >- + This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: "#a7377430-2328-4dc4-a9e2-b3f31dc1dff9" + text: local template validation + rel: lula + - href: "../validation/valid-validation.tmpl.yaml" + text: local path template validation + rel: lula + - href: "../validation/validation.trailing-spaces.yaml" + text: validation with spaces that need to be cleaned + rel: lula + - href: https://raw.githubusercontent.com/defenseunicorns/lula/main/src/test/unit/common/validation/validation.tmpl.yaml + text: validation with template from remote + rel: lula + back-matter: + resources: + - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 + rlinks: + - href: lula.dev + description: >- + domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + group: + version: v1 + resource: {{ .const.resources.name }} + namespaces: [{{ .const.resources.namespace }}] + provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } diff --git a/src/test/unit/common/composition/component-definition-local-and-remote-template.yaml b/src/test/unit/common/composition/component-definition-template.yaml similarity index 95% rename from src/test/unit/common/composition/component-definition-local-and-remote-template.yaml rename to src/test/unit/common/composition/component-definition-template.yaml index cabdbaa1..91577372 100644 --- a/src/test/unit/common/composition/component-definition-local-and-remote-template.yaml +++ b/src/test/unit/common/composition/component-definition-template.yaml @@ -37,6 +37,9 @@ component-definition: - href: "#a7377430-2328-4dc4-a9e2-b3f31dc1dff9" text: local template validation rel: lula + - href: "../validation/validation.tmpl.yaml" + text: local path template validation + rel: lula - href: "../validation/validation.trailing-spaces.yaml" text: validation with spaces that need to be cleaned rel: lula diff --git a/src/test/unit/common/validation/valid-validation.tmpl.yaml b/src/test/unit/common/validation/valid-validation.tmpl.yaml new file mode 100644 index 00000000..b388bc33 --- /dev/null +++ b/src/test/unit/common/validation/valid-validation.tmpl.yaml @@ -0,0 +1,30 @@ +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podvt + resource-rule: + name: "{{ .const.resources.name }}" + version: v1 + resource: pods + namespaces: ["{{ .const.resources.namespace }}"] +provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} \ No newline at end of file diff --git a/src/test/util/utils.go b/src/test/util/utils.go index 978b3990..0b17e576 100644 --- a/src/test/util/utils.go +++ b/src/test/util/utils.go @@ -113,9 +113,9 @@ func GetNamespace(name string) (*v1.Namespace, error) { }, nil } -func ExecuteCommand(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { - _, output, err = ExecuteCommandC(root, args...) - return root, output, err +func ExecuteCommand(cmd *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { + _, output, err = ExecuteCommandC(cmd, args...) + return cmd, output, err } func ExecuteCommandC(cmd *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { From 3ba731b59cef2bd4ee679f58add48541e19a3de3 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Mon, 30 Sep 2024 16:09:28 -0400 Subject: [PATCH 04/11] fix: network things, tests --- __debug_bin2371522808 | 0 docs/oscal/ns/resource-type.md | 26 -- lula-config.yaml | 23 +- src/cmd/common/viper.go | 18 +- src/cmd/internal.go | 4 +- src/cmd/root.go | 52 ++-- src/cmd/tools/compose.go | 26 +- src/cmd/tools/template.go | 10 +- src/cmd/validate/validate.go | 12 +- src/internal/template/template.go | 56 +++- src/pkg/common/composition/composition.go | 4 +- .../common/composition/composition_test.go | 3 +- src/pkg/common/composition/options.go | 6 +- src/pkg/common/network/network.go | 5 +- src/test/e2e/cmd/main_test.go | 21 +- ...dation-templated-missing-validation.golden | 76 +---- ...lated-no-validation-templated-valid.golden | 78 +----- .../composed-file-templated-overrides.golden | 80 ++++++ .../compose/composed-file-templated.golden | 123 +------- .../testdata/tools/template/validation.golden | 3 + .../tools/template/validation_all.golden | 5 +- .../template/validation_constants.golden | 5 +- .../template/validation_non_sensitive.golden | 5 +- .../template/validation_with_env_vars.golden | 3 + .../tools/template/validation_with_set.golden | 3 + src/test/e2e/cmd/tools_compose_test.go | 32 ++- src/test/e2e/cmd/tools_template_test.go | 2 +- ...nition-import-nested-compdef-composed.yaml | 263 ------------------ ...nition-template-valid-validation-tmpl.yaml | 41 +-- .../component-definition-template.yaml | 41 +-- .../common/validation/validation.tmpl.yaml | 3 + 31 files changed, 291 insertions(+), 738 deletions(-) create mode 100644 __debug_bin2371522808 delete mode 100644 docs/oscal/ns/resource-type.md create mode 100644 src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-overrides.golden delete mode 100644 src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml diff --git a/__debug_bin2371522808 b/__debug_bin2371522808 new file mode 100644 index 00000000..e69de29b diff --git a/docs/oscal/ns/resource-type.md b/docs/oscal/ns/resource-type.md deleted file mode 100644 index 43712d75..00000000 --- a/docs/oscal/ns/resource-type.md +++ /dev/null @@ -1,26 +0,0 @@ -# Resource-Type - -The Back-Matter OSCAL structures are used to store a collection of resources that may be referenced from within the OSCAL document instance. For Lula's purposes, the `resources` stored in the back-matter are used in the following ways: - -- To store the "Lula Validation" artifacts -- To store templated "Lula Validation" artifacts - -To identify these types of resources, the `resource-type` prop is used. - -## Example - -For a valid Lula Validation, the `resource-type` prop would be set to `validation`: -```yaml -props: - - name: resource-type - ns: https://docs.lula.dev/oscal/ns - value: "validation" -``` - -For a templated Lula Validation, the `resource-type` prop would be set to `validation-template`: -```yaml -props: - - name: resource-type - ns: https://docs.lula.dev/oscal/ns - value: "validation-template" -``` diff --git a/lula-config.yaml b/lula-config.yaml index 0908f49d..da38ff76 100644 --- a/lula-config.yaml +++ b/lula-config.yaml @@ -1,22 +1 @@ -log_level: info - -constants: - type: software - title: lula - - resources: - name: test-pod-label - namespace: validation-test - exemptions: - - one - - two - - three - -variables: - - key: some_lula_secret - sensitive: true - - key: some_env_var - default: this-should-be-overridden - -# log_level: info -target: il5 +log_level: info \ No newline at end of file diff --git a/src/cmd/common/viper.go b/src/cmd/common/viper.go index 8763620b..6e59dddc 100644 --- a/src/cmd/common/viper.go +++ b/src/cmd/common/viper.go @@ -25,6 +25,12 @@ var ( // Viper configuration error vConfigError error + + // Template config values + TemplateConstants map[string]interface{} + + // Template config values + TemplateVariables []template.VariableConfig ) // InitViper initializes the viper singleton for the CLI @@ -66,6 +72,14 @@ func InitViper() *viper.Viper { // Set default values for viper setDefaults() + // Load template config + constants, variables, err := GetTemplateConfig() + if err != nil { + panic(err) + } + TemplateConstants = constants + TemplateVariables = variables + return v } @@ -76,8 +90,8 @@ func GetViper() *viper.Viper { // GetTemplateConfig loads the constants and variables from the viper config func GetTemplateConfig() (map[string]interface{}, []template.VariableConfig, error) { - var constants map[string]interface{} - var variables []template.VariableConfig + constants := make(map[string]interface{}) + variables := make([]template.VariableConfig, 0) err := v.UnmarshalKey(VConstants, &constants) if err != nil { diff --git a/src/cmd/internal.go b/src/cmd/internal.go index 00aefbb5..90fe93fa 100644 --- a/src/cmd/internal.go +++ b/src/cmd/internal.go @@ -22,8 +22,6 @@ var genCLIDocs = &cobra.Command{ Use: "gen-cli-docs", Short: "Generate CLI command documentation", RunE: func(_ *cobra.Command, _ []string) error { - rootCmd := newRootCmd() // Create a new root command instance - // Don't include the datestamp in the output rootCmd.DisableAutoGenTag = true @@ -74,5 +72,7 @@ type: docs } func init() { + rootCmd.AddCommand(internalCmd) + internalCmd.AddCommand(genCLIDocs) } diff --git a/src/cmd/root.go b/src/cmd/root.go index db916bae..2f13ebd9 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -15,15 +15,28 @@ import ( var LogLevelCLI string -func newRootCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "lula", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - common.SetupClI(LogLevelCLI) - }, - Short: "Risk Management as Code", - Long: `Real Time Risk Transparency through automated validation`, - } +var rootCmd = &cobra.Command{ + Use: "lula", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + common.SetupClI(LogLevelCLI) + }, + Short: "Risk Management as Code", + Long: `Real Time Risk Transparency through automated validation`, +} + +func RootCommand() *cobra.Command { + + cmd := rootCmd + + return cmd +} + +func Execute() { + + cobra.CheckErr(rootCmd.Execute()) +} + +func init() { v := common.InitViper() @@ -34,21 +47,10 @@ func newRootCmd() *cobra.Command { console.ConsoleCommand(), } - cmd.AddCommand(commands...) - tools.Include(cmd) - version.Include(cmd) - dev.Include(cmd) - cmd.AddCommand(internalCmd) - - cmd.PersistentFlags().StringVarP(&LogLevelCLI, "log-level", "l", v.GetString(common.VLogLevel), "Log level when running Lula. Valid options are: warn, info, debug, trace") - - return cmd -} - -func RootCommand() *cobra.Command { - return newRootCmd() -} + rootCmd.AddCommand(commands...) + tools.Include(rootCmd) + version.Include(rootCmd) + dev.Include(rootCmd) -func Execute() { - cobra.CheckErr(newRootCmd().Execute()) + rootCmd.PersistentFlags().StringVarP(&LogLevelCLI, "log-level", "l", v.GetString(common.VLogLevel), "Log level when running Lula. Valid options are: warn, info, debug, trace") } diff --git a/src/cmd/tools/compose.go b/src/cmd/tools/compose.go index 16ed0bff..adb9d04b 100644 --- a/src/cmd/tools/compose.go +++ b/src/cmd/tools/compose.go @@ -39,7 +39,7 @@ func ComposeCommand() *cobra.Command { renderValidations bool // --render-validations ) - var cmd = &cobra.Command{ + var composeCmd = &cobra.Command{ Use: "compose", Short: "compose an OSCAL component definition", Long: composeLong, @@ -65,16 +65,10 @@ func ComposeCommand() *cobra.Command { message.Fatalf(err, "Output file %s is not a valid OSCAL model: %v", outputFile, err) } - // Compose the OSCAL model - constants, variables, err := common.GetTemplateConfig() - if err != nil { - return fmt.Errorf("error getting template config: %v", err) - } - opts := []composition.Option{ composition.WithModelFromLocalPath(inputFile), composition.WithRenderSettings(renderTypeString, renderValidations), - composition.WithTemplateRenderer(renderTypeString, constants, variables, setOpts), + composition.WithTemplateRenderer(renderTypeString, common.TemplateConstants, common.TemplateVariables, setOpts), } err = Compose(cmd.Context(), inputFile, outputFile, opts...) @@ -88,14 +82,14 @@ func ComposeCommand() *cobra.Command { return nil }, } - cmd.Flags().StringVarP(&inputFile, "input-file", "f", "", "the path to the target OSCAL component definition") - cmd.MarkFlagRequired("input-file") - cmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be the original filename with `-composed` appended") - cmd.Flags().StringVarP(&renderTypeString, "render", "r", "", "values to render the template with, options are: masked, constants, non-sensitive, all") - cmd.Flags().StringSliceVarP(&setOpts, "set", "s", []string{}, "set value overrides for templated data") - cmd.Flags().BoolVar(&renderValidations, "render-validations", false, "extend render to remote Lula Validations") - - return cmd + composeCmd.Flags().StringVarP(&inputFile, "input-file", "f", "", "the path to the target OSCAL component definition") + composeCmd.MarkFlagRequired("input-file") + composeCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be the original filename with `-composed` appended") + composeCmd.Flags().StringVarP(&renderTypeString, "render", "r", "", "values to render the template with, options are: masked, constants, non-sensitive, all") + composeCmd.Flags().StringSliceVarP(&setOpts, "set", "s", []string{}, "set value overrides for templated data") + composeCmd.Flags().BoolVar(&renderValidations, "render-validations", false, "extend render to remote Lula Validations") + + return composeCmd } func init() { diff --git a/src/cmd/tools/template.go b/src/cmd/tools/template.go index eb85fbaf..5289faab 100644 --- a/src/cmd/tools/template.go +++ b/src/cmd/tools/template.go @@ -58,10 +58,10 @@ func TemplateCommand() *cobra.Command { } // Get constants and variables for templating from viper config - constants, variables, err := common.GetTemplateConfig() - if err != nil { - return fmt.Errorf("error getting template config: %v", err) - } + // constants, variables, err := common.GetTemplateConfig() + // if err != nil { + // return fmt.Errorf("error getting template config: %v", err) + // } // Get overrides from --set flag overrides, err := common.ParseTemplateOverrides(setOpts) @@ -71,7 +71,7 @@ func TemplateCommand() *cobra.Command { // Handles merging viper config file data + environment variables // Throws an error if config keys are invalid for templating - templateData, err := template.CollectTemplatingData(constants, variables, overrides) + templateData, err := template.CollectTemplatingData(common.TemplateConstants, common.TemplateVariables, overrides) if err != nil { return fmt.Errorf("error collecting templating data: %v", err) } diff --git a/src/cmd/validate/validate.go b/src/cmd/validate/validate.go index bde76aff..b3dc3298 100644 --- a/src/cmd/validate/validate.go +++ b/src/cmd/validate/validate.go @@ -1,6 +1,7 @@ package validate import ( + "context" "fmt" "os" "path/filepath" @@ -8,6 +9,7 @@ import ( "github.com/defenseunicorns/go-oscal/src/pkg/files" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/cmd/common" + "github.com/defenseunicorns/lula/src/pkg/common/composition" "github.com/defenseunicorns/lula/src/pkg/common/oscal" requirementstore "github.com/defenseunicorns/lula/src/pkg/common/requirement-store" validationstore "github.com/defenseunicorns/lula/src/pkg/common/validation-store" @@ -124,10 +126,14 @@ func ValidateOnPath(path string, target string) (assessmentResult *oscalTypes_1_ return assessmentResult, fmt.Errorf("path: %v does not exist - unable to digest document", path) } - // oscalModel, err := composition.ComposeFromPath(path) - oscalModel := &oscalTypes_1_1_2.OscalCompleteSchema{} + compositionCtx, err := composition.New() if err != nil { - return assessmentResult, err + return nil, fmt.Errorf("error creating composition context: %v", err) + } + + oscalModel, err := compositionCtx.ComposeFromPath(context.Background(), path) + if err != nil { + return nil, fmt.Errorf("error composing model: %v", err) } if oscalModel.ComponentDefinition == nil { diff --git a/src/internal/template/template.go b/src/internal/template/template.go index b98687d6..73a94d46 100644 --- a/src/internal/template/template.go +++ b/src/internal/template/template.go @@ -182,7 +182,8 @@ func CollectTemplatingData(constants map[string]interface{}, variables []Variabl return templateData, err } - templateData.Constants = constants + templateData.Constants = deepCopyMap(constants) + for _, variable := range variables { if variable.Sensitive { templateData.SensitiveVariables[variable.Key] = variable.Default @@ -420,3 +421,56 @@ func setNestedValue(m map[string]interface{}, path string, value interface{}) er m[lastKey] = value return nil } + +func deepCopyMap(input map[string]interface{}) map[string]interface{} { + if input == nil { + return nil + } + + // Create a new map to hold the copy + copy := make(map[string]interface{}) + + for key, value := range input { + // Check the type of the value and copy accordingly + switch v := value.(type) { + case map[string]interface{}: + // If the value is a map, recursively deep copy it + copy[key] = deepCopyMap(v) + case []interface{}: + // If the value is a slice, deep copy each element + copy[key] = deepCopySlice(v) + default: + // For other types (e.g., strings, ints), just assign directly + copy[key] = v + } + } + + return copy +} + +// Helper function to deep copy a slice of interface{} +func deepCopySlice(input []interface{}) []interface{} { + if input == nil { + return nil + } + + // Create a new slice to hold the copy + copy := make([]interface{}, len(input)) + + for i, value := range input { + // Check the type of the value and copy accordingly + switch v := value.(type) { + case map[string]interface{}: + // If the value is a map, recursively deep copy it + copy[i] = deepCopyMap(v) + case []interface{}: + // If the value is a slice, deep copy each element + copy[i] = deepCopySlice(v) + default: + // For other types (e.g., strings, ints), just assign directly + copy[i] = v + } + } + + return copy +} diff --git a/src/pkg/common/composition/composition.go b/src/pkg/common/composition/composition.go index 8e724f4f..19a37085 100644 --- a/src/pkg/common/composition/composition.go +++ b/src/pkg/common/composition/composition.go @@ -115,8 +115,8 @@ func (cc *CompositionContext) ComposeComponentDefinitions(ctx context.Context, c // Unmarshal the component definition for _, importDef := range componentDefs { // Reconcile the base directory from the import component definition href - baseDir = network.GetLocalFileDir(importComponentDef.Href, baseDir) - err = cc.ComposeComponentDefinitions(ctx, importDef, baseDir) + importDir := network.GetLocalFileDir(importComponentDef.Href, baseDir) + err = cc.ComposeComponentDefinitions(ctx, importDef, importDir) if err != nil { return err } diff --git a/src/pkg/common/composition/composition_test.go b/src/pkg/common/composition/composition_test.go index d9a600cb..97271bff 100644 --- a/src/pkg/common/composition/composition_test.go +++ b/src/pkg/common/composition/composition_test.go @@ -23,7 +23,8 @@ const ( // TODO: add tests for templating compDefNestedImport = "../../../test/unit/common/composition/component-definition-import-nested-compdef.yaml" - compDefMultiTmpl = "../../../test/unit/common/composition/component-definition-local-and-remote-template.yaml" + compDefNestedTmpl = "../../../test/unit/common/composition/component-definition-import-nested-compdef-template.yaml" + compDefTmpl = "../../../test/unit/common/composition/component-definition-template.yaml" // Also, add cmd tests...? compare golden composed file? ) diff --git a/src/pkg/common/composition/options.go b/src/pkg/common/composition/options.go index 51d95384..e9d75c9f 100644 --- a/src/pkg/common/composition/options.go +++ b/src/pkg/common/composition/options.go @@ -20,7 +20,11 @@ func WithModelFromLocalPath(path string) Option { return fmt.Errorf("input-file: %v does not exist - unable to digest document", path) } - ctx.modelDir = filepath.Dir(path) + absPath, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("error getting absolute path: %v", err) + } + ctx.modelDir = filepath.Dir(absPath) return nil } diff --git a/src/pkg/common/network/network.go b/src/pkg/common/network/network.go index 9137c16b..54b0e711 100644 --- a/src/pkg/common/network/network.go +++ b/src/pkg/common/network/network.go @@ -161,8 +161,9 @@ func GetLocalFileDir(inputURL, baseDir string) string { requestUri := url.RequestURI() if url.Scheme == "file" { - if _, err := os.Stat(requestUri); err != nil { - return filepath.Dir(filepath.Join(baseDir, url.Host, requestUri)) + fullPath := filepath.Join(baseDir, url.Host, requestUri) + if _, err := os.Stat(fullPath); err == nil { + return filepath.Dir(fullPath) } } return "" diff --git a/src/test/e2e/cmd/main_test.go b/src/test/e2e/cmd/main_test.go index 5ff4649d..958d3938 100644 --- a/src/test/e2e/cmd/main_test.go +++ b/src/test/e2e/cmd/main_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/defenseunicorns/lula/src/test/util" + "github.com/google/go-cmp/cmp" "github.com/spf13/cobra" ) @@ -20,7 +21,7 @@ func TestMain(m *testing.M) { m.Run() } -func runCmdTest(t *testing.T, goldenFileName string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { +func runCmdTest(t *testing.T, goldenFilePath, goldenFileName string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { _, output, err := util.ExecuteCommand(rootCmd, cmdArgs...) if err != nil { if !expectError { @@ -31,14 +32,14 @@ func runCmdTest(t *testing.T, goldenFileName string, expectError bool, rootCmd * } if !expectError { - testGolden(t, goldenFileName, output) + testGolden(t, goldenFilePath, goldenFileName, output) } return nil } -func runCmdTestWithOutputFile(t *testing.T, goldenFileName string, outExt string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { - tempFileName := fmt.Sprintf("output.%s", outExt) +func runCmdTestWithOutputFile(t *testing.T, goldenFilePath, goldenFileName string, outExt string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { + tempFileName := fmt.Sprintf("output-%s.%s", goldenFileName, outExt) defer os.Remove(tempFileName) cmdArgs = append(cmdArgs, "-o", tempFileName) @@ -61,13 +62,13 @@ func runCmdTestWithOutputFile(t *testing.T, goldenFileName string, outExt string data = scrubTimestamps(data) if !expectError { - testGolden(t, goldenFileName, string(data)) + testGolden(t, goldenFilePath, goldenFileName, string(data)) } return nil } -func testGolden(t *testing.T, filename, got string) { +func testGolden(t *testing.T, filePath, filename, got string) { t.Helper() got = strings.ReplaceAll(got, "\r\n", "\n") @@ -76,7 +77,7 @@ func testGolden(t *testing.T, filename, got string) { if err != nil { t.Fatal(err) } - goldenPath := filepath.Join(wd, "testdata", filename+".golden") + goldenPath := filepath.Join(wd, "testdata", filePath, filename+".golden") if *updateGolden { if err := os.MkdirAll(filepath.Dir(goldenPath), 0o755); err != nil { @@ -88,11 +89,11 @@ func testGolden(t *testing.T, filename, got string) { } wantBytes, _ := os.ReadFile(goldenPath) - want := string(wantBytes) + diff := cmp.Diff(want, got) - if got != want { - t.Fatalf("`%s` does not match.\n\nWant:\n\n%s\n\nGot:\n\n%s", goldenPath, want, got) + if diff != "" { + t.Fatalf("`%s` does not match.\n\nDiff:\n%s", goldenPath, diff) } } diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden index 5468c44a..5f000801 100644 --- a/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden @@ -1,69 +1,6 @@ component-definition: back-matter: - resources: - - description: |- - domain: - type: kubernetes - kubernetes-spec: - resources: - - name: podsvt - resource-rule: - group: - version: v1 - resource: test-pod-label - namespaces: [validation-test] - provider: - type: opa - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - rlinks: - - href: lula.dev - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>=v0.2.0' - metadata: - name: Validate pods with label foo=bar - uuid: 7a4b7514-f83c-4131-b16c-82537676b098 - provider: - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - type: opa - title: Validate pods with label foo=bar - uuid: 7a4b7514-f83c-4131-b16c-82537676b098 + resources: [] components: - control-implementations: - description: Validate generic security requirements @@ -71,18 +8,9 @@ component-definition: - control-id: ID-1 description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: - - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' - rel: lula - text: local template validation - - href: '#dcd13325-6070-43b4-bb4f-94a684f68369' + - href: '#54e3af9f-01dc-4289-91e8-3bfd0b42da7b' rel: lula text: local path template validation - - href: '#7a4b7514-f83c-4131-b16c-82537676b098' - rel: lula - text: validation with spaces that need to be cleaned - - href: '#ee10395d-44eb-4407-9ac2-cd78c3bb470e' - rel: lula - text: validation with template from remote uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden index a5befd7b..19d3f257 100644 --- a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden @@ -1,34 +1,6 @@ component-definition: back-matter: resources: - - description: |- - domain: - type: kubernetes - kubernetes-spec: - resources: - - name: podsvt - resource-rule: - group: - version: v1 - resource: test-pod-label - namespaces: [validation-test] - provider: - type: opa - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - rlinks: - - href: lula.dev - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 - description: | domain: kubernetes-spec: @@ -47,7 +19,7 @@ component-definition: lula-version: "" metadata: name: Lula Validation - uuid: 8f0fe0ce-14dc-468c-986a-145a3238ed10 + uuid: 307fd191-e6c4-4f7c-b7b8-b79c9c87a450 provider: opa-spec: rego: | @@ -69,42 +41,7 @@ component-definition: value_of_my_secret := {{ .var.some_lula_secret }} type: opa title: Lula Validation - uuid: 8f0fe0ce-14dc-468c-986a-145a3238ed10 - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>=v0.2.0' - metadata: - name: Validate pods with label foo=bar - uuid: 969402b8-7a90-411a-806b-f200c36d60da - provider: - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - type: opa - title: Validate pods with label foo=bar - uuid: 969402b8-7a90-411a-806b-f200c36d60da + uuid: 307fd191-e6c4-4f7c-b7b8-b79c9c87a450 components: - control-implementations: - description: Validate generic security requirements @@ -112,18 +49,9 @@ component-definition: - control-id: ID-1 description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: - - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' - rel: lula - text: local template validation - - href: '#8f0fe0ce-14dc-468c-986a-145a3238ed10' + - href: '#307fd191-e6c4-4f7c-b7b8-b79c9c87a450' rel: lula text: local path template validation - - href: '#969402b8-7a90-411a-806b-f200c36d60da' - rel: lula - text: validation with spaces that need to be cleaned - - href: '#db9a9007-dd7d-486a-9f03-18e5b30c80c3' - rel: lula - text: validation with template from remote uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-overrides.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-overrides.golden new file mode 100644 index 00000000..451aa446 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-overrides.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: foo + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "my-secret" == "********" + } + msg = validate.msg + + value_of_my_secret := my-secret + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden index 5a432ae6..06c97e34 100644 --- a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden @@ -1,75 +1,6 @@ component-definition: back-matter: resources: - - description: |- - domain: - type: kubernetes - kubernetes-spec: - resources: - - name: podsvt - resource-rule: - group: - version: v1 - resource: test-pod-label - namespaces: [validation-test] - provider: - type: opa - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - rlinks: - - href: lula.dev - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podvt - resource-rule: - group: "" - name: test-pod-label - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: "" - metadata: - name: Lula Validation - uuid: b2023350-8b84-47f6-bc51-3c1cf5ec0166 - provider: - opa-spec: - rego: | - package validate - import rego.v1 - - # Default values - default validate := false - default msg := "Not evaluated" - - # Validation result - validate if { - { "one", "two", "three" } == { "one", "two", "three" } - "this-should-be-overridden" == "my-env-var" - "" == "********" - } - msg = validate.msg - - value_of_my_secret := - type: opa - title: Lula Validation - uuid: b2023350-8b84-47f6-bc51-3c1cf5ec0166 - description: | domain: kubernetes-spec: @@ -87,8 +18,8 @@ component-definition: type: kubernetes lula-version: "" metadata: - name: Lula Validation - uuid: 3e8fb2f0-ceec-4108-bdbe-a564f35edf66 + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 provider: opa-spec: rego: | @@ -109,43 +40,8 @@ component-definition: value_of_my_secret := type: opa - title: Lula Validation - uuid: 3e8fb2f0-ceec-4108-bdbe-a564f35edf66 - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>=v0.2.0' - metadata: - name: Validate pods with label foo=bar - uuid: fcd10300-1520-48f7-9f56-f30eeea0420c - provider: - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - type: opa - title: Validate pods with label foo=bar - uuid: fcd10300-1520-48f7-9f56-f30eeea0420c + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 components: - control-implementations: - description: Validate generic security requirements @@ -153,18 +49,9 @@ component-definition: - control-id: ID-1 description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: - - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' - rel: lula - text: local template validation - - href: '#b2023350-8b84-47f6-bc51-3c1cf5ec0166' + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' rel: lula text: local path template validation - - href: '#fcd10300-1520-48f7-9f56-f30eeea0420c' - rel: lula - text: validation with spaces that need to be cleaned - - href: '#3e8fb2f0-ceec-4108-bdbe-a564f35edf66' - rel: lula - text: validation with template from remote uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A diff --git a/src/test/e2e/cmd/testdata/tools/template/validation.golden b/src/test/e2e/cmd/testdata/tools/template/validation.golden index ff319eb2..1ef42a2a 100644 --- a/src/test/e2e/cmd/testdata/tools/template/validation.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation.golden @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/e2e/cmd/testdata/tools/template/validation_all.golden b/src/test/e2e/cmd/testdata/tools/template/validation_all.golden index dc22f17b..d2a0b80f 100644 --- a/src/test/e2e/cmd/testdata/tools/template/validation_all.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_all.golden @@ -1,10 +1,13 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: resources: - name: podvt resource-rule: - name: foo + name: test-pod-label version: v1 resource: pods namespaces: [validation-test] diff --git a/src/test/e2e/cmd/testdata/tools/template/validation_constants.golden b/src/test/e2e/cmd/testdata/tools/template/validation_constants.golden index 91e814e7..08c2c483 100644 --- a/src/test/e2e/cmd/testdata/tools/template/validation_constants.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_constants.golden @@ -1,10 +1,13 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: resources: - name: podvt resource-rule: - name: foo + name: test-pod-label version: v1 resource: pods namespaces: [validation-test] diff --git a/src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden b/src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden index e1282c92..bfcf2ab0 100644 --- a/src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden @@ -1,10 +1,13 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: resources: - name: podvt resource-rule: - name: foo + name: test-pod-label version: v1 resource: pods namespaces: [validation-test] diff --git a/src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden b/src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden index c429cbc2..58d0d839 100644 --- a/src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden b/src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden index f7be2dc4..7b382763 100644 --- a/src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/e2e/cmd/tools_compose_test.go b/src/test/e2e/cmd/tools_compose_test.go index 00858baf..456b4d80 100644 --- a/src/test/e2e/cmd/tools_compose_test.go +++ b/src/test/e2e/cmd/tools_compose_test.go @@ -11,37 +11,57 @@ func TestToolsComposeCommand(t *testing.T) { test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { rootCmd := tools.ComposeCommand() - return runCmdTest(t, "tools/compose/"+goldenFileName, expectError, rootCmd, args...) + return runCmdTest(t, "tools/compose/", goldenFileName, expectError, rootCmd, args...) } testAgainstOutputFile := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { rootCmd := tools.ComposeCommand() - return runCmdTestWithOutputFile(t, "tools/compose/"+goldenFileName, "yaml", expectError, rootCmd, args...) + return runCmdTestWithOutputFile(t, "tools/compose/", goldenFileName, "yaml", expectError, rootCmd, args...) } t.Run("Compose Validation", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file", false, "-f", "../../unit/common/composition/component-definition-all-local.yaml") + err := testAgainstOutputFile(t, "composed-file", false, + "-f", "../../unit/common/composition/component-definition-all-local.yaml") if err != nil { t.Fatal(err) } }) t.Run("Compose Validation with templating", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated", false, "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "all", "--render-validations") + err := testAgainstOutputFile(t, "composed-file-templated", false, + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "all", + "--render-validations") + if err != nil { + t.Fatal(err) + } + }) + + t.Run("Compose Validation with templating and overrides", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-overrides", false, + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "all", + "--render-validations", + "--set", ".const.resources.name=foo,.var.some_lula_secret=my-secret") if err != nil { t.Fatal(err) } }) t.Run("Compose Validation with no templating on validations", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-no-validation-templated-missing-validation", false, "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "all") + err := testAgainstOutputFile(t, "composed-file-no-validation-templated-missing-validation", false, + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "all") if err != nil { t.Fatal(err) } }) + t.Run("Compose Validation with no templating on validations for valid validation template", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated-no-validation-templated-valid", false, "-f", "../../unit/common/composition/component-definition-template-valid-validation-tmpl.yaml", "-r", "all") + err := testAgainstOutputFile(t, "composed-file-templated-no-validation-templated-valid", false, + "-f", "../../unit/common/composition/component-definition-template-valid-validation-tmpl.yaml", + "-r", "all") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/cmd/tools_template_test.go b/src/test/e2e/cmd/tools_template_test.go index 268a87e9..188e2b8b 100644 --- a/src/test/e2e/cmd/tools_template_test.go +++ b/src/test/e2e/cmd/tools_template_test.go @@ -15,7 +15,7 @@ func TestToolsTemplateCommand(t *testing.T) { test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { rootCmd := tools.TemplateCommand() - return runCmdTest(t, "tools/template/"+goldenFileName, expectError, rootCmd, args...) + return runCmdTest(t, "tools/template/", goldenFileName, expectError, rootCmd, args...) } t.Run("Template Validation", func(t *testing.T) { diff --git a/src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml b/src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml deleted file mode 100644 index 35976eb1..00000000 --- a/src/test/unit/common/composition/component-definition-import-nested-compdef-composed.yaml +++ /dev/null @@ -1,263 +0,0 @@ -component-definition: - back-matter: - resources: - - description: |- - domain: - type: kubernetes - kubernetes-spec: - resources: - - name: podsvt - resource-rule: - group: - version: v1 - resource: pods - namespaces: [validation-test] - provider: - type: opa - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - rlinks: - - href: lula.dev - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>= v0.1.0' - metadata: - name: Kyverno validate pods with label foo=bar - uuid: 2d9858bc-fb54-42e7-a928-43f840ac0ae6 - provider: - kyverno-spec: - policy: - apiVersion: json.kyverno.io/v1alpha1 - kind: ValidatingPolicy - metadata: - creationTimestamp: null - name: labels - spec: - rules: - - assert: - all: - - check: - ~.podsvt: - metadata: - labels: - foo: bar - name: foo-label-exists - type: kyverno - title: Kyverno validate pods with label foo=bar - uuid: 2d9858bc-fb54-42e7-a928-43f840ac0ae6 - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>= v0.2.0' - metadata: - name: Kyverno validate pods with label foo=bar - uuid: 88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288 - provider: - kyverno-spec: - policy: - apiVersion: json.kyverno.io/v1alpha1 - kind: ValidatingPolicy - metadata: - creationTimestamp: null - name: labels - spec: - rules: - - assert: - all: - - check: - ~.podsvt: - metadata: - labels: - foo: bar - name: foo-label-exists - type: kyverno - title: Kyverno validate pods with label foo=bar - uuid: 88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288 - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>= v0.1.0' - metadata: - name: Validate pods with label foo=bar - uuid: 7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e - provider: - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - type: opa - title: Validate pods with label foo=bar - uuid: 7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>= v0.2.0' - metadata: - name: Validate pods with label foo=bar - uuid: 7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f - provider: - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - type: opa - title: Validate pods with label foo=bar - uuid: 7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f - - description: | - domain: - kubernetes-spec: - create-resources: null - resources: - - description: "" - name: podsvt - resource-rule: - group: "" - name: "" - namespaces: - - validation-test - resource: pods - version: v1 - type: kubernetes - lula-version: '>= v0.2.0' - metadata: - name: Validate pods with label foo=bar - uuid: 9d09b4fc-1a82-4434-9fbe-392935347a84 - provider: - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - type: opa - title: Validate pods with label foo=bar - uuid: 9d09b4fc-1a82-4434-9fbe-392935347a84 - components: - - control-implementations: - - description: Validate generic security requirements - implemented-requirements: - - control-id: ID-1 - description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. - links: - - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' - rel: lula - - href: '#7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e' - rel: lula - - href: '#2d9858bc-fb54-42e7-a928-43f840ac0ae6' - rel: lula - - href: '#7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f' - rel: lula - - href: '#9d09b4fc-1a82-4434-9fbe-392935347a84' - rel: lula - - href: '#9d09b4fc-1a82-4434-9fbe-392935347a84' - rel: lula - - href: '#88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288' - rel: lula - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD - source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A - description: | - Lula - the Compliance Validator - purpose: Validate compliance controls - responsible-roles: - - party-uuids: - - C18F4A9F-A402-415B-8D13-B51739D689FF - role-id: provider - title: lula - type: software - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 - metadata: - last-modified: 2024-09-27T13:33:37.442137-04:00 - oscal-version: 1.1.2 - parties: - - links: - - href: https://github.com/defenseunicorns/lula - rel: website - name: Lula Development - type: organization - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF - title: Lula Demo - version: "20220913" - uuid: 955a7c0e-3288-4410-8d6b-199200723a81 diff --git a/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml b/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml index 486a0e54..5f583fef 100644 --- a/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml +++ b/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml @@ -34,45 +34,6 @@ component-definition: description: >- This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: - - href: "#a7377430-2328-4dc4-a9e2-b3f31dc1dff9" - text: local template validation - rel: lula - href: "../validation/valid-validation.tmpl.yaml" text: local path template validation - rel: lula - - href: "../validation/validation.trailing-spaces.yaml" - text: validation with spaces that need to be cleaned - rel: lula - - href: https://raw.githubusercontent.com/defenseunicorns/lula/main/src/test/unit/common/validation/validation.tmpl.yaml - text: validation with template from remote - rel: lula - back-matter: - resources: - - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 - rlinks: - - href: lula.dev - description: >- - domain: - type: kubernetes - kubernetes-spec: - resources: - - name: podsvt - resource-rule: - group: - version: v1 - resource: {{ .const.resources.name }} - namespaces: [{{ .const.resources.namespace }}] - provider: - type: opa - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } + rel: lula \ No newline at end of file diff --git a/src/test/unit/common/composition/component-definition-template.yaml b/src/test/unit/common/composition/component-definition-template.yaml index 91577372..ae1d984b 100644 --- a/src/test/unit/common/composition/component-definition-template.yaml +++ b/src/test/unit/common/composition/component-definition-template.yaml @@ -34,45 +34,6 @@ component-definition: description: >- This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: - - href: "#a7377430-2328-4dc4-a9e2-b3f31dc1dff9" - text: local template validation - rel: lula - href: "../validation/validation.tmpl.yaml" text: local path template validation - rel: lula - - href: "../validation/validation.trailing-spaces.yaml" - text: validation with spaces that need to be cleaned - rel: lula - - href: https://raw.githubusercontent.com/defenseunicorns/lula/main/src/test/unit/common/validation/validation.tmpl.yaml - text: validation with template from remote - rel: lula - back-matter: - resources: - - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 - rlinks: - - href: lula.dev - description: >- - domain: - type: kubernetes - kubernetes-spec: - resources: - - name: podsvt - resource-rule: - group: - version: v1 - resource: {{ .const.resources.name }} - namespaces: [{{ .const.resources.namespace }}] - provider: - type: opa - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } + rel: lula \ No newline at end of file diff --git a/src/test/unit/common/validation/validation.tmpl.yaml b/src/test/unit/common/validation/validation.tmpl.yaml index f9eb5baf..fa6979ce 100644 --- a/src/test/unit/common/validation/validation.tmpl.yaml +++ b/src/test/unit/common/validation/validation.tmpl.yaml @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: From ab9960da07b3d21975d9f8aa41a38a6382fa15c5 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Mon, 30 Sep 2024 16:21:10 -0400 Subject: [PATCH 05/11] fix: validate --- __debug_bin2371522808 | 0 go.mod | 2 +- src/cmd/validate/validate.go | 8 ++++---- 3 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 __debug_bin2371522808 diff --git a/__debug_bin2371522808 b/__debug_bin2371522808 deleted file mode 100644 index e69de29b..00000000 diff --git a/go.mod b/go.mod index f66d82ce..2d90dd28 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/charmbracelet/x/exp/teatest v0.0.0-20240919170804-a4978c8e603a github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/defenseunicorns/go-oscal v0.6.0 + github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-version v1.7.0 github.com/kyverno/kyverno-json v0.0.3 github.com/mattn/go-runewidth v0.0.16 @@ -73,7 +74,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect diff --git a/src/cmd/validate/validate.go b/src/cmd/validate/validate.go index b3dc3298..75934b14 100644 --- a/src/cmd/validate/validate.go +++ b/src/cmd/validate/validate.go @@ -61,7 +61,7 @@ var validateCmd = &cobra.Command{ message.Fatalf(err, "Invalid file extension: %s, requires .json or .yaml", opts.InputFile) } - assessment, err := ValidateOnPath(opts.InputFile, opts.Target) + assessment, err := ValidateOnPath(cmd.Context(), opts.InputFile, opts.Target) if err != nil { message.Fatalf(err, "Validation error: %s", err) } @@ -119,19 +119,19 @@ func ValidateCommand() *cobra.Command { // ValidateOnPath takes 1 -> N paths to OSCAL component-definition files // It will then read those files to perform validation and return an ResultObject -func ValidateOnPath(path string, target string) (assessmentResult *oscalTypes_1_1_2.AssessmentResults, err error) { +func ValidateOnPath(ctx context.Context, path string, target string) (assessmentResult *oscalTypes_1_1_2.AssessmentResults, err error) { _, err = os.Stat(path) if os.IsNotExist(err) { return assessmentResult, fmt.Errorf("path: %v does not exist - unable to digest document", path) } - compositionCtx, err := composition.New() + compositionCtx, err := composition.New(composition.WithModelFromLocalPath(path)) if err != nil { return nil, fmt.Errorf("error creating composition context: %v", err) } - oscalModel, err := compositionCtx.ComposeFromPath(context.Background(), path) + oscalModel, err := compositionCtx.ComposeFromPath(ctx, path) if err != nil { return nil, fmt.Errorf("error composing model: %v", err) } From 4e1a53a415caea245fef1f95e3f2917860bb6b1c Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 1 Oct 2024 13:16:29 -0400 Subject: [PATCH 06/11] feat(compose): tests for compose with templating --- docs/getting-started/configuration.md | 3 + docs/getting-started/templating.md | 260 ++++++++++++++++++ src/pkg/common/common_test.go | 6 +- .../common/composition/composition_test.go | 162 +++++++++-- src/pkg/common/network/network_test.go | 10 +- src/pkg/common/schemas/schema_test.go | 2 +- src/test/e2e/api_validation_test.go | 4 +- src/test/e2e/cmd/lula-config.yaml | 2 +- .../composed-file-templated-constants.golden | 80 ++++++ ... => composed-file-templated-masked.golden} | 45 ++- ...lated-no-validation-templated-valid.golden | 10 +- ...mposed-file-templated-non-sensitive.golden | 80 ++++++ src/test/e2e/cmd/tools_compose_test.go | 57 ++-- .../e2e/composition_component_def_test.go | 9 +- src/test/e2e/create_resource_data_test.go | 4 +- .../e2e/multi_resource_validation_test.go | 2 +- src/test/e2e/pod_validation_test.go | 14 +- src/test/e2e/pod_wait_test.go | 2 +- src/test/e2e/remote_validation_test.go | 2 +- src/test/e2e/resource_data_test.go | 2 +- .../component-definition.yaml | 3 + src/test/e2e/validation_composition_test.go | 17 +- ...nition-import-nested-compdef-template.yaml | 5 +- .../validation/valid-validation.tmpl.yaml | 3 + .../common/validation/validation.kyverno.yaml | 2 +- ...pa.validation.yaml => validation.opa.yaml} | 2 +- .../validation.trailing-spaces.yaml | 2 +- 27 files changed, 710 insertions(+), 80 deletions(-) create mode 100644 docs/getting-started/templating.md create mode 100644 src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-constants.golden rename src/test/e2e/cmd/testdata/tools/compose/{composed-file-no-validation-templated-missing-validation.golden => composed-file-templated-masked.golden} (50%) create mode 100644 src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-non-sensitive.golden rename src/test/unit/common/validation/{opa.validation.yaml => validation.opa.yaml} (92%) diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 3df15a2d..8ccc9105 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -118,6 +118,9 @@ provider: The constant's keys should be in the format `.const.` and should not contain any '-' or '.' characters, as this will not respect the go text/template format. +> [!IMPORTANT] +> Due to viper limitations, all constants should be referenced in the template as lowercase values. + #### Variables A sample `variables` section of a `lula-config.yaml` file is as follows: diff --git a/docs/getting-started/templating.md b/docs/getting-started/templating.md new file mode 100644 index 00000000..f1bad776 --- /dev/null +++ b/docs/getting-started/templating.md @@ -0,0 +1,260 @@ +# Templating + +Lula supports composition of both Component Definition and Lula Validation template files. See the [configuration](./configuration.md) documentation for more information on how to configure Lula to use templating. See the [compose CLI command](../cli-commands/lula_tools_compose.md) documentation for more information on the `lula tools compose` command flags to control how templating is applied. + +## Component Definition Templating + +Component Definition templates can be used to create modular component definitions using values from the `lula-config.yaml` file. + +Example: +```yaml +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: {{ .const.title }} + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 + parties: + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: {{ .const.website }} + rel: website +``` + +lula-config.yaml: +```yaml +constants: + title: Lula Demo + website: https://github.com/defenseunicorns/lula +``` + +When this is `composed` with templating applied (`lula tools compose -f --render all`) with the associated `lula-config.yaml`, the resulting component definition will be: + +```yaml +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 + parties: + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website +``` + +## Validation Templating + +Validation templates can be used to create modular Lula Validations using values from the `lula-config.yaml` file. These can be composed into the component definition using the `lula tools compose` command. + +Example: +```yaml +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: {{ .const.type }} + title: {{ .const.title }} + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + description: >- + This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: "./validation.tmpl.yaml" + text: local path template validation + rel: lula +``` + +Where `./validation.tmpl.yaml` is: +```yaml +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podvt + resource-rule: + name: {{ .const.resources.name }} + version: v1 + resource: pods + namespaces: [{{ .const.resources.namespace }}] +provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} +``` + +Executing `lula tools compose -f ./component-definition.yaml --render all --render-validations` will result in: + +```yaml +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "" == "********" + } + msg = validate.msg + + value_of_my_secret := + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F +``` + +### Composing Validation Templates + +If validations are composed into a component definition AND the validation is still intended to be a template, it must be a valid yaml document. For example, the above `validation.tmpl.yaml` is invalid yaml, as the `resource-rule.name` field is not ecapsulated in quotes. A valid yaml version of the above template would be: + +```yaml +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podvt + resource-rule: + name: "{{ .const.resources.name }}" + version: v1 + resource: pods + namespaces: ["{{ .const.resources.namespace }}"] +provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} +``` \ No newline at end of file diff --git a/src/pkg/common/common_test.go b/src/pkg/common/common_test.go index f5fdd1ee..8243bf9e 100644 --- a/src/pkg/common/common_test.go +++ b/src/pkg/common/common_test.go @@ -347,7 +347,7 @@ func TestValidationToResource(t *testing.T) { t.Parallel() validation := &common.Validation{ Metadata: &common.Metadata{ - UUID: "1234", + UUID: "1f639c6b-4e86-4c66-88b2-22dbf6d7ac02", Name: "Test Validation", }, Provider: &common.Provider{ @@ -395,8 +395,8 @@ func TestValidationToResource(t *testing.T) { t.Errorf("ToResource() error = %v", err) } - if resource.UUID == validation.Metadata.UUID { - t.Errorf("ToResource() description = \"\", want a valid UUID") + if resource.UUID != validation.Metadata.UUID { + t.Errorf("ToResource() resource UUID %s should match created validation UUID %s", resource.UUID, validation.Metadata.UUID) } }) diff --git a/src/pkg/common/composition/composition_test.go b/src/pkg/common/composition/composition_test.go index 97271bff..e026a9c4 100644 --- a/src/pkg/common/composition/composition_test.go +++ b/src/pkg/common/composition/composition_test.go @@ -8,25 +8,22 @@ import ( "testing" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + "github.com/defenseunicorns/lula/src/internal/template" "github.com/defenseunicorns/lula/src/pkg/common/composition" "gopkg.in/yaml.v3" ) const ( - allRemote = "../../../test/e2e/scenarios/validation-composition/component-definition.yaml" - allRemoteBadHref = "../../../test/e2e/scenarios/validation-composition/component-definition-bad-href.yaml" - allLocal = "../../../test/unit/common/composition/component-definition-all-local.yaml" - allLocalBadHref = "../../../test/unit/common/composition/component-definition-all-local-bad-href.yaml" - localAndRemote = "../../../test/unit/common/composition/component-definition-local-and-remote.yaml" - subComponentDef = "../../../test/unit/common/composition/component-definition-import-compdefs.yaml" - compDefMultiImport = "../../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" - - // TODO: add tests for templating + allRemote = "../../../test/e2e/scenarios/validation-composition/component-definition.yaml" + allRemoteBadHref = "../../../test/e2e/scenarios/validation-composition/component-definition-bad-href.yaml" + allLocal = "../../../test/unit/common/composition/component-definition-all-local.yaml" + allLocalBadHref = "../../../test/unit/common/composition/component-definition-all-local-bad-href.yaml" + localAndRemote = "../../../test/unit/common/composition/component-definition-local-and-remote.yaml" + subComponentDef = "../../../test/unit/common/composition/component-definition-import-compdefs.yaml" + compDefMultiImport = "../../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" compDefNestedImport = "../../../test/unit/common/composition/component-definition-import-nested-compdef.yaml" - compDefNestedTmpl = "../../../test/unit/common/composition/component-definition-import-nested-compdef-template.yaml" compDefTmpl = "../../../test/unit/common/composition/component-definition-template.yaml" - - // Also, add cmd tests...? compare golden composed file? + compDefNestedTmpl = "../../../test/unit/common/composition/component-definition-import-nested-compdef-template.yaml" ) func TestComposeFromPath(t *testing.T) { @@ -78,8 +75,8 @@ func TestComposeFromPath(t *testing.T) { } }) - t.Run("Imports, no components", func(t *testing.T) { - model, err := test(t, allRemote) + t.Run("Nested imports, no components", func(t *testing.T) { + model, err := test(t, compDefNestedImport) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -98,6 +95,100 @@ func TestComposeFromPath(t *testing.T) { } }) + t.Run("Templated component definition, error", func(t *testing.T) { + model, err := test(t, compDefTmpl) + if err == nil { + t.Fatalf("Should encounter error composing component definitions: %v", err) + } + if model != nil { + t.Error("expected the model not to be composed") + } + }) + + // Test the templating of the component definition where the validation is not rendered -> empty resources in backmatter + t.Run("Templated component definition with nested imports, validations rendered", func(t *testing.T) { + tmplOpts := []composition.Option{ + composition.WithRenderSettings("constants", true), + composition.WithTemplateRenderer("constants", map[string]interface{}{ + "templated_comp_def": interface{}("component-definition-template.yaml"), + "type": interface{}("software"), + "title": interface{}("lula"), + }, []template.VariableConfig{}, []string{}), + } + + model, err := test(t, compDefTmpl, tmplOpts...) + if err != nil { + t.Fatalf("Error composing component definitions: %v", err) + } + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == nil { + t.Error("expected the component definition to have components") + } + + if compDefComposed.BackMatter == nil { + t.Error("expected the component definition to have back matter") + } + + if compDefComposed.BackMatter.Resources == nil { + t.Error("expected the component definition to have back matter resources") + } + + if len(*compDefComposed.BackMatter.Resources) != 0 { + t.Error("expected the back matter to contain 0 resources (validation)") + } + }) + + // Test the templating of the component definition with nested templated imports + t.Run("Templated component definition with nested imports, validations rendered", func(t *testing.T) { + tmplOpts := []composition.Option{ + composition.WithRenderSettings("constants", true), + composition.WithTemplateRenderer("constants", map[string]interface{}{ + "templated_comp_def": interface{}("component-definition-template.yaml"), + "type": interface{}("software"), + "title": interface{}("lula"), + "resources": interface{}(map[string]interface{}{ + "name": interface{}("test-pod-label"), + "namespace": interface{}("validation-test"), + "exemptions": []interface{}{ + interface{}("one"), + interface{}("two"), + interface{}("three"), + }, + }), + }, []template.VariableConfig{}, []string{}), + } + model, err := test(t, compDefNestedTmpl, tmplOpts...) + if err != nil { + t.Fatalf("Error composing component definitions: %v", err) + } + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == nil { + t.Error("expected the component definition to have components") + } + + if compDefComposed.BackMatter == nil { + t.Error("expected the component definition to have back matter") + } + + if compDefComposed.BackMatter.Resources == nil { + t.Error("expected the component definition to have back matter resources") + } + + if len(*compDefComposed.BackMatter.Resources) != 1 { + t.Error("expected the back matter to contain 1 resource (validation)") + } + }) + t.Run("Errors when file does not exist", func(t *testing.T) { _, err := test(t, "nonexistent") if err == nil { @@ -205,7 +296,7 @@ func TestComposeComponentDefinitions(t *testing.T) { og := getComponentDef(compDefMultiImport, t) compDef := getComponentDef(compDefMultiImport, t) - model, err := test(t, compDef, subComponentDef) + model, err := test(t, compDef, compDefMultiImport) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -228,9 +319,48 @@ func TestComposeComponentDefinitions(t *testing.T) { } }) + // Both "imported" components have the same component (by UUID), so those are merged + // Both components have the same control-impementation (by control ID, not UUID), those are merged + // All validations are linked to that single control-implementation + t.Run("nested imports, directory changes", func(t *testing.T) { + og := getComponentDef(compDefNestedImport, t) + compDef := getComponentDef(compDefNestedImport, t) + + model, err := test(t, compDef, compDefNestedImport) + if err != nil { + t.Fatalf("Error composing component definitions: %v", err) + } + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == og.Components { + t.Error("expected there to be new components") + } + + if compDefComposed.BackMatter == og.BackMatter { + t.Error("expected the back matter to be changed") + } + + components := *compDefComposed.Components + if len(components) != 1 { + t.Error("expected there to be 1 component") + } + + if len(*components[0].ControlImplementations) != 1 { + t.Error("expected there to be 1 control implementation") + } + + if len(*compDefComposed.BackMatter.Resources) != 7 { + t.Error("expected the back matter to contain 7 resources (validations)") + } + }) + } -func TestCompileComponentValidations(t *testing.T) { +func TestComposeComponentValidations(t *testing.T) { test := func(t *testing.T, compDef *oscalTypes_1_1_2.ComponentDefinition, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { t.Helper() ctx := context.Background() diff --git a/src/pkg/common/network/network_test.go b/src/pkg/common/network/network_test.go index b62bed4c..1b18b589 100644 --- a/src/pkg/common/network/network_test.go +++ b/src/pkg/common/network/network_test.go @@ -48,13 +48,13 @@ func TestParseUrl(t *testing.T) { }, { name: "File url", - input: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", + input: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", wantErr: false, wantChecksum: false, }, { name: "With Checksum", - input: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", + input: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", wantErr: false, wantChecksum: true, }, @@ -94,12 +94,12 @@ func TestFetch(t *testing.T) { }, { name: "File", - url: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", + url: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", wantErr: false, }, { name: "File with checksum SHA-256", - url: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", + url: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", wantErr: false, }, { @@ -109,7 +109,7 @@ func TestFetch(t *testing.T) { }, { name: "Invalid Sha", - url: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@2d4c18916f2fd70f9488b76690c2eed06789d5fd12e06152a01a8ef7600c41ef", + url: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@2d4c18916f2fd70f9488b76690c2eed06789d5fd12e06152a01a8ef7600c41ef", wantErr: true, }, } diff --git a/src/pkg/common/schemas/schema_test.go b/src/pkg/common/schemas/schema_test.go index e825c9e4..cf94ec12 100644 --- a/src/pkg/common/schemas/schema_test.go +++ b/src/pkg/common/schemas/schema_test.go @@ -65,7 +65,7 @@ func TestListSchemas(t *testing.T) { func TestValidate(t *testing.T) { t.Parallel() // Enable parallel execution of tests - validationPath := "../../../test/unit/common/validation/opa.validation.yaml" + validationPath := "../../../test/unit/common/validation/validation.opa.yaml" validationData, err := os.ReadFile(validationPath) if err != nil { t.Errorf("Expected no error, got %v", err) diff --git a/src/test/e2e/api_validation_test.go b/src/test/e2e/api_validation_test.go index f6b83301..4bb9f868 100644 --- a/src/test/e2e/api_validation_test.go +++ b/src/test/e2e/api_validation_test.go @@ -55,7 +55,7 @@ func TestApiValidation(t *testing.T) { oscalPath := "./scenarios/api-field/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } @@ -139,7 +139,7 @@ func TestApiValidation(t *testing.T) { oscalPath := "./scenarios/api-field/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/cmd/lula-config.yaml b/src/test/e2e/cmd/lula-config.yaml index 001c2a3f..2506d95b 100644 --- a/src/test/e2e/cmd/lula-config.yaml +++ b/src/test/e2e/cmd/lula-config.yaml @@ -1,7 +1,7 @@ constants: type: software title: lula - templatedCompDefFile: component-definition-template.yaml + templated_comp_def: component-definition-template.yaml resources: name: test-pod-label diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-constants.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-constants.golden new file mode 100644 index 00000000..6b4f3b9e --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-constants.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-masked.golden similarity index 50% rename from src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden rename to src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-masked.golden index 5f000801..289f1908 100644 --- a/src/test/e2e/cmd/testdata/tools/compose/composed-file-no-validation-templated-missing-validation.golden +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-masked.golden @@ -1,6 +1,47 @@ component-definition: back-matter: - resources: [] + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "********" == "********" + } + msg = validate.msg + + value_of_my_secret := ******** + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 components: - control-implementations: - description: Validate generic security requirements @@ -8,7 +49,7 @@ component-definition: - control-id: ID-1 description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: - - href: '#54e3af9f-01dc-4289-91e8-3bfd0b42da7b' + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' rel: lula text: local path template validation uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden index 19d3f257..72f8ebc6 100644 --- a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden @@ -18,8 +18,8 @@ component-definition: type: kubernetes lula-version: "" metadata: - name: Lula Validation - uuid: 307fd191-e6c4-4f7c-b7b8-b79c9c87a450 + name: Test validation with templating + uuid: 458d2d84-b7f2-4679-8964-6f9a9dfe51eb provider: opa-spec: rego: | @@ -40,8 +40,8 @@ component-definition: value_of_my_secret := {{ .var.some_lula_secret }} type: opa - title: Lula Validation - uuid: 307fd191-e6c4-4f7c-b7b8-b79c9c87a450 + title: Test validation with templating + uuid: 458d2d84-b7f2-4679-8964-6f9a9dfe51eb components: - control-implementations: - description: Validate generic security requirements @@ -49,7 +49,7 @@ component-definition: - control-id: ID-1 description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: - - href: '#307fd191-e6c4-4f7c-b7b8-b79c9c87a450' + - href: '#458d2d84-b7f2-4679-8964-6f9a9dfe51eb' rel: lula text: local path template validation uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-non-sensitive.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-non-sensitive.golden new file mode 100644 index 00000000..9f5cf2ea --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-non-sensitive.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/tools_compose_test.go b/src/test/e2e/cmd/tools_compose_test.go index 456b4d80..cf72a9b9 100644 --- a/src/test/e2e/cmd/tools_compose_test.go +++ b/src/test/e2e/cmd/tools_compose_test.go @@ -4,9 +4,11 @@ import ( "testing" "github.com/defenseunicorns/lula/src/cmd/tools" + "github.com/defenseunicorns/lula/src/pkg/message" ) func TestToolsComposeCommand(t *testing.T) { + message.NoProgress = true test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { rootCmd := tools.ComposeCommand() @@ -24,37 +26,58 @@ func TestToolsComposeCommand(t *testing.T) { err := testAgainstOutputFile(t, "composed-file", false, "-f", "../../unit/common/composition/component-definition-all-local.yaml") if err != nil { - t.Fatal(err) + t.Error(err) } }) - t.Run("Compose Validation with templating", func(t *testing.T) { + t.Run("Compose Validation with templating - all", func(t *testing.T) { err := testAgainstOutputFile(t, "composed-file-templated", false, "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "all", "--render-validations") if err != nil { - t.Fatal(err) + t.Error(err) } }) - t.Run("Compose Validation with templating and overrides", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated-overrides", false, + t.Run("Compose Validation with templating - non-sensitive", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-non-sensitive", false, "-f", "../../unit/common/composition/component-definition-template.yaml", - "-r", "all", - "--render-validations", - "--set", ".const.resources.name=foo,.var.some_lula_secret=my-secret") + "-r", "non-sensitive", + "--render-validations") if err != nil { - t.Fatal(err) + t.Error(err) } }) - t.Run("Compose Validation with no templating on validations", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-no-validation-templated-missing-validation", false, + t.Run("Compose Validation with templating - constants", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-constants", false, "-f", "../../unit/common/composition/component-definition-template.yaml", - "-r", "all") + "-r", "constants", + "--render-validations") + if err != nil { + t.Error(err) + } + }) + + t.Run("Compose Validation with templating - masked", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-masked", false, + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "masked", + "--render-validations") + if err != nil { + t.Error(err) + } + }) + + t.Run("Compose Validation with templating and overrides", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-overrides", false, + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "all", + "--render-validations", + "--set", ".const.resources.name=foo,.var.some_lula_secret=my-secret") if err != nil { - t.Fatal(err) + t.Error(err) } }) @@ -63,28 +86,28 @@ func TestToolsComposeCommand(t *testing.T) { "-f", "../../unit/common/composition/component-definition-template-valid-validation-tmpl.yaml", "-r", "all") if err != nil { - t.Fatal(err) + t.Error(err) } }) t.Run("Test help", func(t *testing.T) { err := test(t, "help", false, "--help") if err != nil { - t.Fatal(err) + t.Error(err) } }) t.Run("Test Compose - invalid file error", func(t *testing.T) { err := test(t, "empty", true, "-f", "not-a-file.yaml") if err != nil { - t.Fatal(err) + t.Error("expected error, got nil") } }) t.Run("Test Compose - invalid file schema error", func(t *testing.T) { err := test(t, "empty", true, "-f", "../../unit/common/composition/component-definition-template.yaml") if err != nil { - t.Fatal(err) + t.Error(err) } }) } diff --git a/src/test/e2e/composition_component_def_test.go b/src/test/e2e/composition_component_def_test.go index 5e39f487..ce14ef64 100644 --- a/src/test/e2e/composition_component_def_test.go +++ b/src/test/e2e/composition_component_def_test.go @@ -42,7 +42,7 @@ func TestComponentDefinitionComposition(t *testing.T) { compDefPath := "../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" // Validate results using ValidateOnPath - assessment, err := validate.ValidateOnPath(compDefPath, "") + assessment, err := validate.ValidateOnPath(ctx, compDefPath, "") if err != nil { t.Errorf("Error validating component definition: %v", err) } @@ -87,7 +87,12 @@ func TestComponentDefinitionComposition(t *testing.T) { } // Compare validation results to a composed component definition - oscalModel, err := composition.ComposeFromPath(compDefPath) + compositionCtx, err := composition.New(composition.WithModelFromLocalPath(compDefPath)) + if err != nil { + t.Errorf("error creating composition context: %v", err) + } + + oscalModel, err := compositionCtx.ComposeFromPath(ctx, compDefPath) if err != nil { t.Error(err) } diff --git a/src/test/e2e/create_resource_data_test.go b/src/test/e2e/create_resource_data_test.go index b9b12344..0ef33f5c 100644 --- a/src/test/e2e/create_resource_data_test.go +++ b/src/test/e2e/create_resource_data_test.go @@ -40,7 +40,7 @@ func TestCreateResourceDataValidation(t *testing.T) { validate.RunNonInteractively = true validate.SaveResources = false - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } @@ -114,7 +114,7 @@ func TestDeniedCreateResources(t *testing.T) { // Check that validation fails validate.ConfirmExecution = false validate.RunNonInteractively = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/multi_resource_validation_test.go b/src/test/e2e/multi_resource_validation_test.go index 1541e428..fd296665 100644 --- a/src/test/e2e/multi_resource_validation_test.go +++ b/src/test/e2e/multi_resource_validation_test.go @@ -103,7 +103,7 @@ func TestMultiResourceValidation(t *testing.T) { oscalPath := "./scenarios/multi-resource/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/pod_validation_test.go b/src/test/e2e/pod_validation_test.go index a192c8af..e1052cbb 100644 --- a/src/test/e2e/pod_validation_test.go +++ b/src/test/e2e/pod_validation_test.go @@ -92,12 +92,12 @@ func TestPodLabelValidation(t *testing.T) { }). Assess("Validate pod label", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/pod-label/oscal-component.yaml" - validatePodLabelFail(t, oscalPath) + validatePodLabelFail(ctx, t, oscalPath) return ctx }). Assess("Validate pod label (Kyverno)", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/pod-label/oscal-component-kyverno.yaml" - validatePodLabelFail(t, oscalPath) + validatePodLabelFail(ctx, t, oscalPath) return ctx }). Teardown(func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { @@ -130,7 +130,7 @@ func TestPodLabelValidation(t *testing.T) { }). Assess("All not-satisfied", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/pod-label/oscal-component-all-bad.yaml" - findings, observations := validatePodLabelFail(t, oscalPath) + findings, observations := validatePodLabelFail(ctx, t, oscalPath) observationRemarksMap := generateObservationRemarksMap(*observations) for _, f := range *findings { @@ -228,7 +228,7 @@ func validatePodLabelPass(ctx context.Context, t *testing.T, config *envconf.Con } message.Infof("Successfully upgraded %s to %s with OSCAL version %s %s\n", oscalPath, revisionOptions.OutputFile, revisionResponse.Reviser.GetSchemaVersion(), revisionResponse.Reviser.GetModelType()) - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatalf("Failed to validate oscal file: %s", oscalPath) } @@ -315,13 +315,13 @@ func validatePodLabelPass(ctx context.Context, t *testing.T, config *envconf.Con return ctx } -func validatePodLabelFail(t *testing.T, oscalPath string) (*[]oscalTypes_1_1_2.Finding, *[]oscalTypes_1_1_2.Observation) { +func validatePodLabelFail(ctx context.Context, t *testing.T, oscalPath string) (*[]oscalTypes_1_1_2.Finding, *[]oscalTypes_1_1_2.Observation) { message.NoProgress = true validate.ConfirmExecution = false validate.RunNonInteractively = true validate.SaveResources = false - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatalf("Failed to validate oscal file: %s", oscalPath) } @@ -367,7 +367,7 @@ func validateSaveResources(ctx context.Context, t *testing.T, oscalPath string) validate.ResourcesDir = tempDir // Validate on path - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatalf("Failed to validate oscal file: %s", oscalPath) } diff --git a/src/test/e2e/pod_wait_test.go b/src/test/e2e/pod_wait_test.go index 181146f9..faca5c15 100644 --- a/src/test/e2e/pod_wait_test.go +++ b/src/test/e2e/pod_wait_test.go @@ -32,7 +32,7 @@ func TestPodWaitValidation(t *testing.T) { oscalPath := "./scenarios/wait-field/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/remote_validation_test.go b/src/test/e2e/remote_validation_test.go index aa460532..087ca22b 100644 --- a/src/test/e2e/remote_validation_test.go +++ b/src/test/e2e/remote_validation_test.go @@ -36,7 +36,7 @@ func TestRemoteValidation(t *testing.T) { Assess("Validate local validation file", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/remote-validations/component-definition.yaml" - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/resource_data_test.go b/src/test/e2e/resource_data_test.go index 1fdc6dbe..ebea164a 100644 --- a/src/test/e2e/resource_data_test.go +++ b/src/test/e2e/resource_data_test.go @@ -71,7 +71,7 @@ func TestResourceDataValidation(t *testing.T) { oscalPath := "./scenarios/resource-data/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/scenarios/validation-composition/component-definition.yaml b/src/test/e2e/scenarios/validation-composition/component-definition.yaml index 79e91b40..6c07936d 100644 --- a/src/test/e2e/scenarios/validation-composition/component-definition.yaml +++ b/src/test/e2e/scenarios/validation-composition/component-definition.yaml @@ -6,6 +6,9 @@ component-definition: - control-id: ID-1 description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: + # validation from local, change directory + - href: file://../../../unit/common/validation/validation.opa.yaml + rel: lula # remote opa validation - href: https://raw.githubusercontent.com/defenseunicorns/lula/main/src/test/e2e/scenarios/dev-validate/validation.yaml rel: lula diff --git a/src/test/e2e/validation_composition_test.go b/src/test/e2e/validation_composition_test.go index e6ff1779..6f8d8af8 100644 --- a/src/test/e2e/validation_composition_test.go +++ b/src/test/e2e/validation_composition_test.go @@ -3,12 +3,12 @@ package test import ( "context" "os" + "path/filepath" "testing" "time" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/cmd/validate" - "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/composition" "github.com/defenseunicorns/lula/src/pkg/common/oscal" validationstore "github.com/defenseunicorns/lula/src/pkg/common/validation-store" @@ -71,7 +71,7 @@ func validateComposition(ctx context.Context, t *testing.T, oscalPath, expectedF t.Error(err) } - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } @@ -98,15 +98,16 @@ func validateComposition(ctx context.Context, t *testing.T, oscalPath, expectedF if err != nil { t.Error(err) } - reset, err := common.SetCwdToFileDir(oscalPath) - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - defer reset() compDef := oscalModel.ComponentDefinition - err = composition.ComposeComponentValidations(compDef) + compositionCtx, err := composition.New(composition.WithModelFromLocalPath(oscalPath)) + if err != nil { + t.Errorf("error creating composition context: %v", err) + } + + baseDir := filepath.Dir(oscalPath) + err = compositionCtx.ComposeComponentValidations(ctx, compDef, baseDir) if err != nil { t.Error(err) } diff --git a/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml b/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml index 18d63cfe..8e5091c1 100644 --- a/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml +++ b/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml @@ -1,8 +1,9 @@ component-definition: uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F import-component-definitions: - - href: file://./{{ .const.templatedCompDefFile }}.yaml - - href: file://../../../e2e/scenarios/validation-composition/component-definition.yaml + - href: file://./{{ .const.templated_comp_def }} + - href: file://../oscal/valid-generated-component.yaml + metadata: title: Lula Demo last-modified: "2022-09-13T12:00:00Z" diff --git a/src/test/unit/common/validation/valid-validation.tmpl.yaml b/src/test/unit/common/validation/valid-validation.tmpl.yaml index b388bc33..442b6861 100644 --- a/src/test/unit/common/validation/valid-validation.tmpl.yaml +++ b/src/test/unit/common/validation/valid-validation.tmpl.yaml @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 458d2d84-b7f2-4679-8964-6f9a9dfe51eb domain: type: kubernetes kubernetes-spec: diff --git a/src/test/unit/common/validation/validation.kyverno.yaml b/src/test/unit/common/validation/validation.kyverno.yaml index e0e6de7c..c4ea4896 100644 --- a/src/test/unit/common/validation/validation.kyverno.yaml +++ b/src/test/unit/common/validation/validation.kyverno.yaml @@ -1,7 +1,7 @@ lula-version: ">= v0.1.0" metadata: name: Kyverno validate pods with label foo=bar - uuid: 123e4567-e89b-12d3-a456-426614174000 + uuid: 386aafd8-a80d-4ad7-8844-d7b14a432187 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/unit/common/validation/opa.validation.yaml b/src/test/unit/common/validation/validation.opa.yaml similarity index 92% rename from src/test/unit/common/validation/opa.validation.yaml rename to src/test/unit/common/validation/validation.opa.yaml index a46af206..1185c575 100644 --- a/src/test/unit/common/validation/opa.validation.yaml +++ b/src/test/unit/common/validation/validation.opa.yaml @@ -1,7 +1,7 @@ lula-version: ">=v0.2.0" metadata: name: Validate pods with label foo=bar - uuid: 123e4567-e89b-12d3-a456-426655440000 + uuid: 6c00ae8d-7187-42ab-8d89-f383447a0824 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/unit/common/validation/validation.trailing-spaces.yaml b/src/test/unit/common/validation/validation.trailing-spaces.yaml index f8a6449d..b18627ea 100644 --- a/src/test/unit/common/validation/validation.trailing-spaces.yaml +++ b/src/test/unit/common/validation/validation.trailing-spaces.yaml @@ -1,7 +1,7 @@ lula-version: ">=v0.2.0" metadata: name: Validate pods with label foo=bar - uuid: 123e4567-e89b-12d3-a456-426655440000 + uuid: a6bded80-1717-45fc-afd9-c5d62607eb71 domain: type: kubernetes kubernetes-spec: From aadf64979a20a8b04666c64aa528397116e43d26 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 1 Oct 2024 13:56:25 -0400 Subject: [PATCH 07/11] fix: scrub runner timestamp format --- src/test/e2e/cmd/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/e2e/cmd/main_test.go b/src/test/e2e/cmd/main_test.go index 958d3938..2dee1337 100644 --- a/src/test/e2e/cmd/main_test.go +++ b/src/test/e2e/cmd/main_test.go @@ -98,6 +98,6 @@ func testGolden(t *testing.T, filePath, filename, got string) { } func scrubTimestamps(data []byte) []byte { - re := regexp.MustCompile(`(?i)(last-modified:\s*)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[-+]\d{2}:\d{2})`) + re := regexp.MustCompile(`(?i)(last-modified:\s*)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[-+]\d{2}:\d{2}|Z)?)`) return []byte(re.ReplaceAllString(string(data), "${1}XXX")) } From 98e754138cd96e2d92a3bb858dff1c95f1af8d9f Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 3 Oct 2024 08:12:00 -0400 Subject: [PATCH 08/11] fix: input/output filepaths --- src/cmd/tools/compose.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/cmd/tools/compose.go b/src/cmd/tools/compose.go index adb9d04b..27c882f9 100644 --- a/src/cmd/tools/compose.go +++ b/src/cmd/tools/compose.go @@ -44,23 +44,29 @@ func ComposeCommand() *cobra.Command { Short: "compose an OSCAL component definition", Long: composeLong, Example: composeHelp, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) (err error) { composeSpinner := message.NewProgressSpinner("Composing %s", inputFile) defer composeSpinner.Stop() // Update input/output paths if filepath.IsLocal(inputFile) { - inputFile = filepath.Join(filepath.Dir(inputFile), filepath.Base(inputFile)) + inputFile, err = filepath.Abs(inputFile) + if err != nil { + return fmt.Errorf("error getting absolute path: %v", err) + } } if outputFile == "" { outputFile = GetDefaultOutputFile(inputFile) } else if filepath.IsLocal(outputFile) { - outputFile = filepath.Join(filepath.Dir(outputFile), filepath.Base(outputFile)) + outputFile, err = filepath.Abs(outputFile) + if err != nil { + return fmt.Errorf("error getting absolute path: %v", err) + } } // Check if output file contains a valid OSCAL model - _, err := oscal.ValidOSCALModelAtPath(outputFile) + _, err = oscal.ValidOSCALModelAtPath(outputFile) if err != nil { message.Fatalf(err, "Output file %s is not a valid OSCAL model: %v", outputFile, err) } From 0883844f81adaa7fe5038dd2fafeadcf9c266739 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 3 Oct 2024 09:26:33 -0400 Subject: [PATCH 09/11] fix: removed duplicative/non useful path cmds --- src/cmd/tools/compose.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/cmd/tools/compose.go b/src/cmd/tools/compose.go index 27c882f9..9a0e243d 100644 --- a/src/cmd/tools/compose.go +++ b/src/cmd/tools/compose.go @@ -48,21 +48,8 @@ func ComposeCommand() *cobra.Command { composeSpinner := message.NewProgressSpinner("Composing %s", inputFile) defer composeSpinner.Stop() - // Update input/output paths - if filepath.IsLocal(inputFile) { - inputFile, err = filepath.Abs(inputFile) - if err != nil { - return fmt.Errorf("error getting absolute path: %v", err) - } - } - if outputFile == "" { outputFile = GetDefaultOutputFile(inputFile) - } else if filepath.IsLocal(outputFile) { - outputFile, err = filepath.Abs(outputFile) - if err != nil { - return fmt.Errorf("error getting absolute path: %v", err) - } } // Check if output file contains a valid OSCAL model From d8aa430a40ded7338519524e0c565e59d872f8cf Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 3 Oct 2024 19:45:43 -0400 Subject: [PATCH 10/11] fix: compose and template test updates --- src/cmd/tools/compose.go | 41 +++---- src/cmd/tools/compose_test.go | 55 ---------- src/cmd/tools/template.go | 6 - .../common/composition/composition_test.go | 4 +- src/pkg/common/oscal/component.go | 12 +- src/test/e2e/cmd/main_test.go | 33 +++--- .../tools/compose/composed-file.golden | 66 ----------- src/test/e2e/cmd/tools_compose_test.go | 103 ++++++++++-------- src/test/e2e/cmd/tools_template_test.go | 70 ++++++------ 9 files changed, 130 insertions(+), 260 deletions(-) delete mode 100644 src/cmd/tools/compose_test.go delete mode 100644 src/test/e2e/cmd/testdata/tools/compose/composed-file.golden diff --git a/src/cmd/tools/compose.go b/src/cmd/tools/compose.go index 9a0e243d..79a68981 100644 --- a/src/cmd/tools/compose.go +++ b/src/cmd/tools/compose.go @@ -1,7 +1,6 @@ package tools import ( - "context" "fmt" "path/filepath" "strings" @@ -55,7 +54,7 @@ func ComposeCommand() *cobra.Command { // Check if output file contains a valid OSCAL model _, err = oscal.ValidOSCALModelAtPath(outputFile) if err != nil { - message.Fatalf(err, "Output file %s is not a valid OSCAL model: %v", outputFile, err) + return fmt.Errorf("invalid OSCAL model at output file: %v", err) } opts := []composition.Option{ @@ -64,9 +63,21 @@ func ComposeCommand() *cobra.Command { composition.WithTemplateRenderer(renderTypeString, common.TemplateConstants, common.TemplateVariables, setOpts), } - err = Compose(cmd.Context(), inputFile, outputFile, opts...) + // Compose the OSCAL model + compositionCtx, err := composition.New(opts...) if err != nil { - return fmt.Errorf("error composing model: %v", err) + return fmt.Errorf("error creating composition context: %v", err) + } + + model, err := compositionCtx.ComposeFromPath(cmd.Context(), inputFile) + if err != nil { + return fmt.Errorf("error composing model from path: %v", err) + } + + // Write the composed OSCAL model to a file + err = oscal.WriteOscalModel(outputFile, model) + if err != nil { + return fmt.Errorf("error writing composed model: %v", err) } message.Infof("Composed OSCAL Component Definition to: %s", outputFile) @@ -90,28 +101,6 @@ func init() { toolsCmd.AddCommand(ComposeCommand()) } -// Compose composes an OSCAL model from a file path -func Compose(ctx context.Context, inputFile, outputFile string, opts ...composition.Option) error { - // Compose the OSCAL model - compositionCtx, err := composition.New(opts...) - if err != nil { - return fmt.Errorf("error creating composition context: %v", err) - } - - model, err := compositionCtx.ComposeFromPath(ctx, inputFile) - if err != nil { - return err - } - - // Write the composed OSCAL model to a file - err = oscal.WriteOscalModel(outputFile, model) - if err != nil { - return err - } - - return nil -} - // GetDefaultOutputFile returns the default output file name func GetDefaultOutputFile(inputFile string) string { return strings.TrimSuffix(inputFile, filepath.Ext(inputFile)) + "-composed" + filepath.Ext(inputFile) diff --git a/src/cmd/tools/compose_test.go b/src/cmd/tools/compose_test.go deleted file mode 100644 index 853c6610..00000000 --- a/src/cmd/tools/compose_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package tools_test - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/defenseunicorns/lula/src/cmd/tools" - "github.com/defenseunicorns/lula/src/pkg/common/composition" - "github.com/defenseunicorns/lula/src/pkg/common/oscal" -) - -var ( - validInputFile = "../../test/unit/common/composition/component-definition-import-compdefs.yaml" - invalidInputFile = "../../test/unit/common/valid-api-spec.yaml" -) - -func TestComposeComponentDefinition(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() - outputFile := filepath.Join(tempDir, "output.yaml") - ctx := context.Background() - - t.Run("composes valid component definition", func(t *testing.T) { - err := tools.Compose(ctx, validInputFile, outputFile, composition.WithModelFromLocalPath(validInputFile)) - if err != nil { - t.Fatalf("error composing component definition: %s", err) - } - - compiledBytes, err := os.ReadFile(outputFile) - if err != nil { - t.Fatalf("error reading composed component definition: %s", err) - } - compiledModel, err := oscal.NewOscalModel(compiledBytes) - if err != nil { - t.Fatalf("error creating oscal model from composed component definition: %s", err) - } - - if compiledModel.ComponentDefinition.BackMatter.Resources == nil { - t.Fatal("composed component definition is nil") - } - - if len(*compiledModel.ComponentDefinition.BackMatter.Resources) <= 1 { - t.Fatalf("expected 2 resources, got %d", len(*compiledModel.ComponentDefinition.BackMatter.Resources)) - } - }) - - t.Run("invalid component definition throws error", func(t *testing.T) { - err := tools.Compose(ctx, invalidInputFile, outputFile, composition.WithModelFromLocalPath(invalidInputFile)) - if err == nil { - t.Fatal("expected error composing invalid component definition") - } - }) -} diff --git a/src/cmd/tools/template.go b/src/cmd/tools/template.go index 790427d1..c6ffea5d 100644 --- a/src/cmd/tools/template.go +++ b/src/cmd/tools/template.go @@ -57,12 +57,6 @@ func TemplateCommand() *cobra.Command { renderType = template.MASKED } - // Get constants and variables for templating from viper config - // constants, variables, err := common.GetTemplateConfig() - // if err != nil { - // return fmt.Errorf("error getting template config: %v", err) - // } - // Get overrides from --set flag overrides, err := common.ParseTemplateOverrides(setOpts) if err != nil { diff --git a/src/pkg/common/composition/composition_test.go b/src/pkg/common/composition/composition_test.go index e026a9c4..ac1037e8 100644 --- a/src/pkg/common/composition/composition_test.go +++ b/src/pkg/common/composition/composition_test.go @@ -106,7 +106,7 @@ func TestComposeFromPath(t *testing.T) { }) // Test the templating of the component definition where the validation is not rendered -> empty resources in backmatter - t.Run("Templated component definition with nested imports, validations rendered", func(t *testing.T) { + t.Run("Templated component definition with nested imports, validations not rendered - no resources", func(t *testing.T) { tmplOpts := []composition.Option{ composition.WithRenderSettings("constants", true), composition.WithTemplateRenderer("constants", map[string]interface{}{ @@ -181,7 +181,7 @@ func TestComposeFromPath(t *testing.T) { } if compDefComposed.BackMatter.Resources == nil { - t.Error("expected the component definition to have back matter resources") + t.Fatalf("expected the component definition to have back matter resources") } if len(*compDefComposed.BackMatter.Resources) != 1 { diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index eccac3ee..a5c5c265 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -592,10 +592,14 @@ func MakeComponentDeterminstic(component *oscalTypes_1_1_2.ComponentDefinition) backmatter := *component.BackMatter if backmatter.Resources != nil { resources := *backmatter.Resources - sort.Slice(resources, func(i, j int) bool { - return resources[i].Title < resources[j].Title - }) - backmatter.Resources = &resources + if len(resources) == 0 { + backmatter.Resources = nil + } else { + sort.Slice(resources, func(i, j int) bool { + return resources[i].Title < resources[j].Title + }) + backmatter.Resources = &resources + } } component.BackMatter = &backmatter } diff --git a/src/test/e2e/cmd/main_test.go b/src/test/e2e/cmd/main_test.go index 2dee1337..4cf4b2e3 100644 --- a/src/test/e2e/cmd/main_test.go +++ b/src/test/e2e/cmd/main_test.go @@ -21,35 +21,34 @@ func TestMain(m *testing.M) { m.Run() } -func runCmdTest(t *testing.T, goldenFilePath, goldenFileName string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { - _, output, err := util.ExecuteCommand(rootCmd, cmdArgs...) +func runCmdTest(t *testing.T, rootCmd *cobra.Command, cmdArgs ...string) error { + _, _, err := util.ExecuteCommand(rootCmd, cmdArgs...) if err != nil { - if !expectError { - return err - } else { - return nil - } + return err } - if !expectError { - testGolden(t, goldenFilePath, goldenFileName, output) + return nil +} + +func runCmdTestWithGolden(t *testing.T, goldenFilePath, goldenFileName string, rootCmd *cobra.Command, cmdArgs ...string) error { + _, output, err := util.ExecuteCommand(rootCmd, cmdArgs...) + if err != nil { + return err } + testGolden(t, goldenFilePath, goldenFileName, output) + return nil } -func runCmdTestWithOutputFile(t *testing.T, goldenFilePath, goldenFileName string, outExt string, expectError bool, rootCmd *cobra.Command, cmdArgs ...string) error { +func runCmdTestWithOutputFile(t *testing.T, goldenFilePath, goldenFileName, outExt string, rootCmd *cobra.Command, cmdArgs ...string) error { tempFileName := fmt.Sprintf("output-%s.%s", goldenFileName, outExt) defer os.Remove(tempFileName) cmdArgs = append(cmdArgs, "-o", tempFileName) _, _, err := util.ExecuteCommand(rootCmd, cmdArgs...) if err != nil { - if !expectError { - return err - } else { - return nil - } + return err } // Read the output file @@ -61,9 +60,7 @@ func runCmdTestWithOutputFile(t *testing.T, goldenFilePath, goldenFileName strin // Scrub timestamps data = scrubTimestamps(data) - if !expectError { - testGolden(t, goldenFilePath, goldenFileName, string(data)) - } + testGolden(t, goldenFilePath, goldenFileName, string(data)) return nil } diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file.golden deleted file mode 100644 index b76bdf55..00000000 --- a/src/test/e2e/cmd/testdata/tools/compose/composed-file.golden +++ /dev/null @@ -1,66 +0,0 @@ -component-definition: - back-matter: - resources: - - description: |- - domain: - type: kubernetes - kubernetes-spec: - resources: - - name: podsvt - resource-rule: - group: - version: v1 - resource: pods - namespaces: [validation-test] - provider: - type: opa - opa-spec: - rego: | - package validate - - import future.keywords.every - - validate { - every pod in input.podsvt { - podLabel := pod.metadata.labels.foo - podLabel == "bar" - } - } - rlinks: - - href: lula.dev - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 - components: - - control-implementations: - - description: Validate generic security requirements - implemented-requirements: - - control-id: ID-1 - description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. - links: - - href: '#a7377430-2328-4dc4-a9e2-b3f31dc1dff9' - rel: lula - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD - source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A - description: | - Lula - the Compliance Validator - purpose: Validate compliance controls - responsible-roles: - - party-uuids: - - C18F4A9F-A402-415B-8D13-B51739D689FF - role-id: provider - title: lula - type: software - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 - metadata: - last-modified: XXX - oscal-version: 1.1.2 - parties: - - links: - - href: https://github.com/defenseunicorns/lula - rel: website - name: Lula Development - type: organization - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF - title: Lula Demo - version: "20220913" - uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/tools_compose_test.go b/src/test/e2e/cmd/tools_compose_test.go index cf72a9b9..2186059c 100644 --- a/src/test/e2e/cmd/tools_compose_test.go +++ b/src/test/e2e/cmd/tools_compose_test.go @@ -1,113 +1,126 @@ package cmd_test import ( + "os" + "path/filepath" "testing" + "github.com/stretchr/testify/require" + "github.com/defenseunicorns/lula/src/cmd/tools" + "github.com/defenseunicorns/lula/src/pkg/common/oscal" "github.com/defenseunicorns/lula/src/pkg/message" ) func TestToolsComposeCommand(t *testing.T) { message.NoProgress = true - test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { + test := func(t *testing.T, args ...string) error { rootCmd := tools.ComposeCommand() - return runCmdTest(t, "tools/compose/", goldenFileName, expectError, rootCmd, args...) + return runCmdTest(t, rootCmd, args...) } - testAgainstOutputFile := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { + testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { rootCmd := tools.ComposeCommand() - return runCmdTestWithOutputFile(t, "tools/compose/", goldenFileName, "yaml", expectError, rootCmd, args...) + return runCmdTestWithGolden(t, "tools/compose/", goldenFileName, rootCmd, args...) + } + + testAgainstOutputFile := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := tools.ComposeCommand() + + return runCmdTestWithOutputFile(t, "tools/compose/", goldenFileName, "yaml", rootCmd, args...) } t.Run("Compose Validation", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file", false, - "-f", "../../unit/common/composition/component-definition-all-local.yaml") - if err != nil { - t.Error(err) - } + tempDir := t.TempDir() + outputFile := filepath.Join(tempDir, "output.yaml") + + err := test(t, "composed-file", + "-f", "../../unit/common/composition/component-definition-import-compdefs.yaml", + "-o", outputFile, + ) + + require.NoError(t, err) + + // Check that the output file is valid OSCAL + compiledBytes, err := os.ReadFile(outputFile) + require.NoErrorf(t, err, "error reading composed component definition: %v", err) + + compiledModel, err := oscal.NewOscalModel(compiledBytes) + require.NoErrorf(t, err, "error creating oscal model from composed component definition: %v", err) + + require.NotNilf(t, compiledModel.ComponentDefinition, "composed component definition is nil") + + require.Equalf(t, 3, len(*compiledModel.ComponentDefinition.BackMatter.Resources), "expected 3 resources, got %d", len(*compiledModel.ComponentDefinition.BackMatter.Resources)) }) t.Run("Compose Validation with templating - all", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated", false, + err := testAgainstOutputFile(t, "composed-file-templated", "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "all", "--render-validations") - if err != nil { - t.Error(err) - } + require.NoError(t, err) }) t.Run("Compose Validation with templating - non-sensitive", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated-non-sensitive", false, + err := testAgainstOutputFile(t, "composed-file-templated-non-sensitive", "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "non-sensitive", "--render-validations") - if err != nil { - t.Error(err) - } + require.NoError(t, err) }) t.Run("Compose Validation with templating - constants", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated-constants", false, + err := testAgainstOutputFile(t, "composed-file-templated-constants", "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "constants", "--render-validations") - if err != nil { - t.Error(err) - } + require.NoError(t, err) }) t.Run("Compose Validation with templating - masked", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated-masked", false, + err := testAgainstOutputFile(t, "composed-file-templated-masked", "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "masked", "--render-validations") - if err != nil { - t.Error(err) - } + require.NoError(t, err) }) t.Run("Compose Validation with templating and overrides", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated-overrides", false, + err := testAgainstOutputFile(t, "composed-file-templated-overrides", "-f", "../../unit/common/composition/component-definition-template.yaml", "-r", "all", "--render-validations", "--set", ".const.resources.name=foo,.var.some_lula_secret=my-secret") - if err != nil { - t.Error(err) - } + require.NoError(t, err) }) t.Run("Compose Validation with no templating on validations for valid validation template", func(t *testing.T) { - err := testAgainstOutputFile(t, "composed-file-templated-no-validation-templated-valid", false, + err := testAgainstOutputFile(t, "composed-file-templated-no-validation-templated-valid", "-f", "../../unit/common/composition/component-definition-template-valid-validation-tmpl.yaml", "-r", "all") - if err != nil { - t.Error(err) - } + require.NoError(t, err) }) t.Run("Test help", func(t *testing.T) { - err := test(t, "help", false, "--help") - if err != nil { - t.Error(err) - } + err := testAgainstGolden(t, "help", "--help") + require.NoError(t, err) }) t.Run("Test Compose - invalid file error", func(t *testing.T) { - err := test(t, "empty", true, "-f", "not-a-file.yaml") - if err != nil { - t.Error("expected error, got nil") - } + err := test(t, "-f", "not-a-file.yaml") + require.ErrorContains(t, err, "error creating composition context") }) t.Run("Test Compose - invalid file schema error", func(t *testing.T) { - err := test(t, "empty", true, "-f", "../../unit/common/composition/component-definition-template.yaml") - if err != nil { - t.Error(err) - } + err := test(t, "-f", "../../unit/common/composition/component-definition-template.yaml") + require.ErrorContains(t, err, "error composing model from path") + }) + + t.Run("Test Compose - invalid output file", func(t *testing.T) { + err := test(t, "-f", "../../unit/common/composition/component-definition-multi.yaml", "-o", "../../unit/common/validation/validation.opa.yaml") + require.ErrorContains(t, err, "invalid OSCAL model at output file") }) } diff --git a/src/test/e2e/cmd/tools_template_test.go b/src/test/e2e/cmd/tools_template_test.go index 188e2b8b..02a6608d 100644 --- a/src/test/e2e/cmd/tools_template_test.go +++ b/src/test/e2e/cmd/tools_template_test.go @@ -6,82 +6,76 @@ import ( "testing" "github.com/defenseunicorns/lula/src/cmd/tools" + "github.com/stretchr/testify/require" ) // var updateGolden = flag.Bool("update", false, "update golden files") func TestToolsTemplateCommand(t *testing.T) { - test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { + test := func(t *testing.T, args ...string) error { rootCmd := tools.TemplateCommand() - return runCmdTest(t, "tools/template/", goldenFileName, expectError, rootCmd, args...) + return runCmdTest(t, rootCmd, args...) + } + + testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := tools.TemplateCommand() + + return runCmdTestWithGolden(t, "tools/template/", goldenFileName, rootCmd, args...) } t.Run("Template Validation", func(t *testing.T) { - err := test(t, "validation", false, "-f", "../../unit/common/validation/validation.tmpl.yaml") - if err != nil { - t.Fatal(err) - } + err := testAgainstGolden(t, "validation", "-f", "../../unit/common/validation/validation.tmpl.yaml") + require.NoError(t, err) }) t.Run("Template Validation with env vars", func(t *testing.T) { os.Setenv("LULA_VAR_SOME_ENV_VAR", "my-env-var") defer os.Unsetenv("LULA_VAR_SOME_ENV_VAR") - err := test(t, "validation_with_env_vars", false, "-f", "../../unit/common/validation/validation.tmpl.yaml") - if err != nil { - t.Fatal(err) - } + err := testAgainstGolden(t, "validation_with_env_vars", "-f", "../../unit/common/validation/validation.tmpl.yaml") + require.NoError(t, err) }) t.Run("Template Validation with set", func(t *testing.T) { - err := test(t, "validation_with_set", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", ".const.resources.name=foo") - if err != nil { - t.Fatal(err) - } + err := testAgainstGolden(t, "validation_with_set", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", ".const.resources.name=foo") + require.NoError(t, err) }) t.Run("Template Validation for all", func(t *testing.T) { os.Setenv("LULA_VAR_SOME_LULA_SECRET", "env-secret") defer os.Unsetenv("LULA_VAR_SOME_LULA_SECRET") - err := test(t, "validation_all", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "all") - if err != nil { - t.Fatal(err) - } + err := testAgainstGolden(t, "validation_all", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "all") + require.NoError(t, err) }) t.Run("Template Validation for non-sensitive", func(t *testing.T) { - err := test(t, "validation_non_sensitive", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "non-sensitive") - if err != nil { - t.Fatal(err) - } + err := testAgainstGolden(t, "validation_non_sensitive", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "non-sensitive") + require.NoError(t, err) }) t.Run("Template Validation for constants", func(t *testing.T) { - err := test(t, "validation_constants", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "constants") - if err != nil { - t.Fatal(err) - } + err := testAgainstGolden(t, "validation_constants", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "constants") + require.NoError(t, err) }) t.Run("Test help", func(t *testing.T) { - err := test(t, "help", false, "--help") - if err != nil { - t.Fatal(err) - } + err := testAgainstGolden(t, "help", "--help") + require.NoError(t, err) }) t.Run("Template Validation - invalid file error", func(t *testing.T) { - err := test(t, "empty", true, "-f", "not-a-file.yaml") - if err != nil { - t.Fatal(err) - } + err := test(t, "-f", "not-a-file.yaml") + require.ErrorContains(t, err, "error reading file") + }) + + t.Run("Template Validation - invalid set opts", func(t *testing.T) { + err := test(t, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", "not-valid") + require.ErrorContains(t, err, "error parsing template overrides") }) t.Run("Template Validation - invalid file schema error", func(t *testing.T) { - err := test(t, "empty", true, "-f", "../../unit/common/validation/validation.bad.tmpl.yaml") - if err != nil { - t.Fatal(err) - } + err := test(t, "-f", "../../unit/common/validation/validation.bad.tmpl.yaml") + require.ErrorContains(t, err, "error rendering template") }) } From 0d421cede2c83683ab01221e03a9dcb969ffa152 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 8 Oct 2024 09:58:09 -0400 Subject: [PATCH 11/11] fix: docs, removed dead code --- docs/cli-commands/lula_tools_compose.md | 10 ++++++++++ src/pkg/common/composition/resource-store.go | 17 ----------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/docs/cli-commands/lula_tools_compose.md b/docs/cli-commands/lula_tools_compose.md index 0573382a..5769e3a7 100644 --- a/docs/cli-commands/lula_tools_compose.md +++ b/docs/cli-commands/lula_tools_compose.md @@ -9,8 +9,15 @@ compose an OSCAL component definition ### Synopsis + Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability. +Supports templating of the composed component definition with the following configuration options: +- To compose with templating applied, specify '--render, -r' with values of 'all', 'non-sensitive', 'constants', or 'masked' (choice will depend on the use case for the composed content) +- To render Lula Validations include '--render-validations' +- To perform any manual overrides to the template data, specify '--set, -s' with the format '.const.key=value' or '.var.key=value' + + ``` lula tools compose [flags] ``` @@ -33,6 +40,9 @@ To indicate a specific output file: -h, --help help for compose -f, --input-file string the path to the target OSCAL component definition -o, --output-file -composed the path to the output file. If not specified, the output file will be the original filename with -composed appended + -r, --render string values to render the template with, options are: masked, constants, non-sensitive, all + --render-validations extend render to remote Lula Validations + -s, --set strings set value overrides for templated data ``` ### Options inherited from parent commands diff --git a/src/pkg/common/composition/resource-store.go b/src/pkg/common/composition/resource-store.go index cfb8a7d6..02a050d7 100644 --- a/src/pkg/common/composition/resource-store.go +++ b/src/pkg/common/composition/resource-store.go @@ -3,11 +3,9 @@ package composition import ( "fmt" - "github.com/defenseunicorns/go-oscal/src/pkg/uuid" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/network" - "github.com/defenseunicorns/lula/src/pkg/common/oscal" ) // ResourceStore is a store of resources. @@ -159,18 +157,3 @@ func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link, baseDir return ids, err } - -func createTemplateResource(data []byte) *oscalTypes_1_1_2.Resource { - return &oscalTypes_1_1_2.Resource{ - Title: "Validation Template", - UUID: uuid.NewUUID(), - Description: common.CleanMultilineString(string(data)), - Props: &[]oscalTypes_1_1_2.Property{ - { - Name: "resource-type", - Value: "validation-template", - Ns: oscal.LULA_NAMESPACE, - }, - }, - } -}