diff --git a/docs/cli-commands/lula_tools.md b/docs/cli-commands/lula_tools.md index bd4ae473..cc5a3187 100644 --- a/docs/cli-commands/lula_tools.md +++ b/docs/cli-commands/lula_tools.md @@ -24,6 +24,7 @@ Collection of additional commands to make OSCAL easier * [lula](./lula.md) - Risk Management as Code * [lula tools compose](./lula_tools_compose.md) - compose an OSCAL component definition * [lula tools lint](./lula_tools_lint.md) - Validate OSCAL against schema +* [lula tools print](./lula_tools_print.md) - Print Resources or Lula Validation from an Assessment Observation * [lula tools template](./lula_tools_template.md) - Template an artifact * [lula tools upgrade](./lula_tools_upgrade.md) - Upgrade OSCAL document to a new version if possible. * [lula tools uuidgen](./lula_tools_uuidgen.md) - Generate a UUID diff --git a/docs/cli-commands/lula_tools_print.md b/docs/cli-commands/lula_tools_print.md new file mode 100644 index 00000000..f2e4d328 --- /dev/null +++ b/docs/cli-commands/lula_tools_print.md @@ -0,0 +1,58 @@ +--- +title: lula tools print +description: Lula CLI command reference for lula tools print. +type: docs +--- +## lula tools print + +Print Resources or Lula Validation from an Assessment Observation + +### Synopsis + + +Prints out data about an OSCAL Observation from the OSCAL Assessment Results model. +Given "--resources", the command will print the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. +Given "--validation", the command will print the Lula Validation that generated a given observation, as identified by a given observation, assessment results file, and component definition file. + + +``` +lula tools print [flags] +``` + +### Examples + +``` + +To print resources from lula validation manifest: + lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid + +To print resources from lula validation manifest to output file: + lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json + +To print the lula validation that generated a given observation: + lula tools print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid + +``` + +### Options + +``` + -a, --assessment string the path to an assessment-results file + -c, --component string the path to a validation manifest file + -h, --help help for print + -u, --observation-uuid string the observation uuid + -o, --output-file string the path to write the resources json + -r, --resources true if the user is printing resources + -v, --validation true if the user is printing validation +``` + +### Options inherited from parent commands + +``` + -l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info") +``` + +### SEE ALSO + +* [lula tools](./lula_tools.md) - Collection of additional commands to make OSCAL easier + diff --git a/src/cmd/common/common.go b/src/cmd/common/common.go index 3d66ceca..84f02f97 100644 --- a/src/cmd/common/common.go +++ b/src/cmd/common/common.go @@ -2,9 +2,12 @@ package common import ( "fmt" + "os" "strings" "github.com/defenseunicorns/lula/src/internal/template" + "github.com/defenseunicorns/lula/src/pkg/message" + "github.com/defenseunicorns/lula/src/types" ) func ParseTemplateOverrides(setFlags []string) (map[string]string, error) { @@ -24,3 +27,19 @@ func ParseTemplateOverrides(setFlags []string) (map[string]string, error) { } return overrides, nil } + +// writeResources writes the resources to a file or stdout +func WriteResources(data types.DomainResources, filepath string) error { + jsonData := message.JSONValue(data) + + // If a filepath is provided, write the JSON data to the file. + if filepath != "" { + err := os.WriteFile(filepath, []byte(jsonData), 0600) + if err != nil { + return fmt.Errorf("error writing resource JSON to file: %v", err) + } + } else { + message.Printf("%s", jsonData) + } + return nil +} diff --git a/src/cmd/dev/common.go b/src/cmd/dev/common.go index e8eaaa4f..a2b48020 100644 --- a/src/cmd/dev/common.go +++ b/src/cmd/dev/common.go @@ -7,12 +7,13 @@ import ( "strings" "time" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + "github.com/defenseunicorns/lula/src/config" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/message" "github.com/defenseunicorns/lula/src/types" - "github.com/spf13/cobra" - "sigs.k8s.io/yaml" ) const STDIN = "0" diff --git a/src/cmd/dev/get-resources.go b/src/cmd/dev/get-resources.go index 52d0b61d..e3de5580 100644 --- a/src/cmd/dev/get-resources.go +++ b/src/cmd/dev/get-resources.go @@ -3,12 +3,12 @@ package dev import ( "context" "fmt" - "os" + + "github.com/spf13/cobra" "github.com/defenseunicorns/lula/src/cmd/common" "github.com/defenseunicorns/lula/src/pkg/message" "github.com/defenseunicorns/lula/src/types" - "github.com/spf13/cobra" ) var getResourcesOpts = &flags{} @@ -53,7 +53,10 @@ var getResourcesCmd = &cobra.Command{ message.Fatalf(err, "error running dev get-resources: %v", err) } - writeResources(collection, getResourcesOpts.OutputFile) + err = common.WriteResources(collection, getResourcesOpts.OutputFile) + if err != nil { + message.Fatalf(err, "error writing resources: %v", err) + } spinner.Success() }, @@ -85,18 +88,3 @@ func DevGetResources(ctx context.Context, validationBytes []byte, spinner *messa return *lulaValidation.DomainResources, nil } - -func writeResources(data types.DomainResources, filepath string) { - jsonData := message.JSONValue(data) - - // If a filepath is provided, write the JSON data to the file. - if filepath != "" { - err := os.WriteFile(filepath, []byte(jsonData), 0600) // G306 - if err != nil { - message.Fatalf(err, "error writing resource JSON to file: %v", err) - } - } else { - // Else print to stdout - fmt.Println(jsonData) - } -} diff --git a/src/cmd/tools/print.go b/src/cmd/tools/print.go new file mode 100644 index 00000000..a6ceaae8 --- /dev/null +++ b/src/cmd/tools/print.go @@ -0,0 +1,217 @@ +package tools + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + "github.com/spf13/cobra" + + cmdcommon "github.com/defenseunicorns/lula/src/cmd/common" + "github.com/defenseunicorns/lula/src/pkg/common" + "github.com/defenseunicorns/lula/src/pkg/common/composition" + "github.com/defenseunicorns/lula/src/pkg/common/network" + "github.com/defenseunicorns/lula/src/pkg/common/oscal" + "github.com/defenseunicorns/lula/src/pkg/message" + "github.com/defenseunicorns/lula/src/types" +) + +var printHelp = ` +To print resources from lula validation manifest: + lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid + +To print resources from lula validation manifest to output file: + lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json + +To print the lula validation that generated a given observation: + lula tools print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid +` + +var printCmdLong = ` +Prints out data about an OSCAL Observation from the OSCAL Assessment Results model. +Given "--resources", the command will print the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. +Given "--validation", the command will print the Lula Validation that generated a given observation, as identified by a given observation, assessment results file, and component definition file. +` + +func PrintCommand() *cobra.Command { + var ( + resources bool // -r --resources + validation bool // -v --validation + assessment string // -a --assessment + observationUuid string // -u --observation-uuid + outputFile string // -o --output-file + component string // -c --component + ) + + printCmd := &cobra.Command{ + Use: "print", + Short: "Print Resources or Lula Validation from an Assessment Observation", + Long: printCmdLong, + Example: printHelp, + RunE: func(cmd *cobra.Command, args []string) error { + assessmentData, err := common.ReadFileToBytes(assessment) + if err != nil { + return fmt.Errorf("invalid assessment file: %v", err) + } + + assessmentDir, err := filepath.Abs(filepath.Dir(assessment)) + if err != nil { + return fmt.Errorf("error getting assessment directory: %v", err) + } + + oscalAssessment, err := oscal.NewAssessmentResults(assessmentData) + if err != nil { + return fmt.Errorf("error creating oscal assessment results model: %v", err) + } + + // Print the resources or validation + if resources { + err = PrintResources(oscalAssessment, observationUuid, assessmentDir, outputFile) + if err != nil { + return fmt.Errorf("error printing resources: %v", err) + } + } else if validation { + // Compose the component definition + composer, err := composition.New(composition.WithModelFromLocalPath(component)) + if err != nil { + return fmt.Errorf("error creating new composer: %v", err) + } + oscalModel, err := composer.ComposeFromPath(cmd.Context(), component) + if err != nil { + return fmt.Errorf("error composing model: %v", err) + } + + // Print the validation + err = PrintValidation(oscalModel.ComponentDefinition, oscalAssessment, observationUuid, outputFile) + if err != nil { + return fmt.Errorf("error printing validation: %v", err) + } + } + return nil + }, + } + + // Add flags, set logic for flag behavior + printCmd.Flags().BoolVarP(&resources, "resources", "r", false, "true if the user is printing resources") + printCmd.Flags().BoolVarP(&validation, "validation", "v", false, "true if the user is printing validation") + printCmd.MarkFlagsMutuallyExclusive("resources", "validation") + + printCmd.Flags().StringVarP(&assessment, "assessment", "a", "", "the path to an assessment-results file") + err := printCmd.MarkFlagRequired("assessment") + if err != nil { + message.Fatal(err, "error initializing print-resources command flag: assessment") + } + + printCmd.Flags().StringVarP(&observationUuid, "observation-uuid", "u", "", "the observation uuid") + err = printCmd.MarkFlagRequired("observation-uuid") + if err != nil { + message.Fatal(err, "error initializing required command flag: observation-uuid") + } + + printCmd.Flags().StringVarP(&component, "component", "c", "", "the path to a validation manifest file") + printCmd.MarkFlagsRequiredTogether("validation", "component") + + printCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to write the resources json") + + return printCmd +} + +func init() { + toolsCmd.AddCommand(PrintCommand()) +} + +func PrintResources(assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid, assessmentDir, outputFile string) error { + if assessment == nil { + return fmt.Errorf("assessment is nil") + } + + observation, err := oscal.GetObservationByUuid(assessment, observationUuid) + if err != nil { + return err + } + + // Get the resources from the remote reference + // TODO: will an observation ever have multiple resource links? + resourceCount := 0 + var resource types.DomainResources + + if observation.Links == nil { + return fmt.Errorf("observation does not contain a remote reference") + } + + for _, link := range *observation.Links { + if link.Rel == "lula.resources" { + resourceCount++ + if resourceCount > 1 { + return fmt.Errorf("observation contains multiple remote references, only the first printed") + } + + resourceData, err := network.Fetch(link.Href, network.WithBaseDir(assessmentDir)) + if err != nil { + return fmt.Errorf("error fetching resource: %v", err) + } + + err = json.Unmarshal(resourceData, &resource) + if err != nil { + return fmt.Errorf("error unmarshalling resource: %v", err) + } + } + } + + // Write the resources to a file if found + err = cmdcommon.WriteResources(resource, outputFile) + if err != nil { + return err + } + + return nil +} + +func PrintValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid, outputFile string) error { + if component == nil { + return fmt.Errorf("component definition is nil") + } + + if assessment == nil { + return fmt.Errorf("assessment results is nil") + } + + // Get the observation + observation, err := oscal.GetObservationByUuid(assessment, observationUuid) + if err != nil { + return err + } + + // Get the validation + found, validationUuid := oscal.GetProp("validation", oscal.LULA_NAMESPACE, observation.Props) + if !found { + return fmt.Errorf("no validation linked to observation") + } + + // Find validation ID in the component definition back matter + resourceMap := make(map[string]string) + if component.BackMatter != nil { + resourceMap = oscal.BackMatterToMap(*component.BackMatter) + } + + trimmedId := common.TrimIdPrefix(validationUuid) + + // Find the validation in the map + validation, found := resourceMap[trimmedId] + if !found { + return fmt.Errorf("validation not found in component definition") + } + + // Print the validation + if outputFile == "" { + message.Printf("%s", validation) + } else { + err = os.WriteFile(outputFile, []byte(validation), 0600) + if err != nil { + return fmt.Errorf("error writing validation to file: %v", err) + } + } + return nil +} diff --git a/src/cmd/tools/print_test.go b/src/cmd/tools/print_test.go new file mode 100644 index 00000000..9e50e338 --- /dev/null +++ b/src/cmd/tools/print_test.go @@ -0,0 +1,104 @@ +package tools_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/cmd/tools" + "github.com/defenseunicorns/lula/src/internal/testhelpers" + "github.com/defenseunicorns/lula/src/types" +) + +const ( + assessmentPath = "../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml" + componentPath = "../../test/unit/common/oscal/valid-multi-component-validations.yaml" +) + +func TestPrintResources(t *testing.T) { + t.Parallel() + + oscalModel := testhelpers.OscalFromPath(t, assessmentPath) + assessment := oscalModel.AssessmentResults + require.NotNil(t, assessment) + + t.Run("Test print resources", func(t *testing.T) { + tmpFile := testhelpers.CreateTempFile(t, ".json") + defer os.Remove(tmpFile.Name()) + + err := tools.PrintResources(assessment, "92cb3cad-bbcd-431a-aaa9-cd47275a3982", "../../test/unit/common/oscal", tmpFile.Name()) + require.NoError(t, err) + + // get printed resources + data, err := os.ReadFile(tmpFile.Name()) + require.NoError(t, err) + + var obsResources types.DomainResources + err = json.Unmarshal(data, &obsResources) + require.NoError(t, err) + + // get actual resources + data, err = os.ReadFile("../../test/unit/common/resources/valid-resources.json") + require.NoError(t, err) + + var resources types.DomainResources + err = json.Unmarshal(data, &resources) + require.NoError(t, err) + + require.Equal(t, resources, obsResources) + }) + + t.Run("Test print resources with invalid resources", func(t *testing.T) { + err := tools.PrintResources(assessment, "e1ca2968-8652-41be-a19f-c32bc0b3086c", "../../test/unit/common/oscal", "") + require.ErrorContains(t, err, "error unmarshalling resource") + }) + + t.Run("Test print resources with no resources", func(t *testing.T) { + err := tools.PrintResources(assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "../../test/unit/common/oscal", "") + require.ErrorContains(t, err, "observation does not contain a remote reference") + }) + +} + +func TestPrintValidation(t *testing.T) { + t.Parallel() + + oscalAssessmentModel := testhelpers.OscalFromPath(t, assessmentPath) + assessment := oscalAssessmentModel.AssessmentResults + require.NotNil(t, assessment) + + oscalComponentModel := testhelpers.OscalFromPath(t, componentPath) + component := oscalComponentModel.ComponentDefinition + require.NotNil(t, component) + + t.Run("Test print validation", func(t *testing.T) { + tmpFile := testhelpers.CreateTempFile(t, ".json") + defer os.Remove(tmpFile.Name()) + + err := tools.PrintValidation(component, assessment, "92cb3cad-bbcd-431a-aaa9-cd47275a3982", tmpFile.Name()) + require.NoError(t, err) + + // get printed data + printedData, err := os.ReadFile(tmpFile.Name()) + require.NoError(t, err) + + // get actual data + validationData, err := os.ReadFile("../../test/unit/common/validation/validation.resource-print.yaml") + require.NoError(t, err) + + require.Equal(t, validationData, printedData) + }) + + t.Run("Test print validation with no validation prop", func(t *testing.T) { + err := tools.PrintValidation(component, assessment, "e1ca2968-8652-41be-a19f-c32bc0b3086c", "") + require.ErrorContains(t, err, "no validation linked to observation") + }) + + t.Run("Test print resources with validation not in backmatter", func(t *testing.T) { + err := tools.PrintValidation(component, assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "") + require.ErrorContains(t, err, "validation not found in component definition") + }) + +} diff --git a/src/pkg/common/oscal/assessment-results.go b/src/pkg/common/oscal/assessment-results.go index af8c4f9d..4fbda8b6 100644 --- a/src/pkg/common/oscal/assessment-results.go +++ b/src/pkg/common/oscal/assessment-results.go @@ -365,3 +365,21 @@ func CreateResult(findingMap map[string]oscalTypes_1_1_2.Finding, observations [ return result, nil } + +// GetObservationByUuid returns the observation with the given UUID +func GetObservationByUuid(assessmentResults *oscalTypes_1_1_2.AssessmentResults, observationUuid string) (*oscalTypes_1_1_2.Observation, error) { + if assessmentResults == nil { + return nil, fmt.Errorf("assessment results is nil") + } + + for _, result := range assessmentResults.Results { + if result.Observations != nil { + for _, observation := range *result.Observations { + if observation.UUID == observationUuid { + return &observation, nil + } + } + } + } + return nil, fmt.Errorf("observation with uuid %s not found", observationUuid) +} diff --git a/src/pkg/common/oscal/assessment-results_test.go b/src/pkg/common/oscal/assessment-results_test.go index 3f5938dc..7f79bd51 100644 --- a/src/pkg/common/oscal/assessment-results_test.go +++ b/src/pkg/common/oscal/assessment-results_test.go @@ -7,6 +7,10 @@ import ( "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/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/internal/testhelpers" "github.com/defenseunicorns/lula/src/pkg/common/oscal" "github.com/defenseunicorns/lula/src/pkg/message" ) @@ -639,3 +643,24 @@ func TestCreateResult(t *testing.T) { }) } } + +func TestGetObservationByUuid(t *testing.T) { + t.Parallel() + + oscalModel := testhelpers.OscalFromPath(t, "../../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml") + assessment := oscalModel.AssessmentResults + require.NotNil(t, assessment) + + t.Run("Test get observation by uuid - found", func(t *testing.T) { + observation, err := oscal.GetObservationByUuid(assessment, "92cb3cad-bbcd-431a-aaa9-cd47275a3982") + require.NoError(t, err) + require.NotNil(t, observation) + }) + + t.Run("Test get observation by uuid - not found", func(t *testing.T) { + observation, err := oscal.GetObservationByUuid(assessment, "invalid-uuid") + assert.Nil(t, observation) + require.ErrorContains(t, err, "observation with uuid invalid-uuid not found") + }) + +} diff --git a/src/pkg/message/message.go b/src/pkg/message/message.go index ab9fc090..c0796c45 100644 --- a/src/pkg/message/message.go +++ b/src/pkg/message/message.go @@ -2,6 +2,7 @@ package message import ( + "bytes" "encoding/json" "fmt" "io" @@ -101,6 +102,12 @@ func UseLogFile(inputLogFile *os.File) { } } +// UseBuffer writes output to a buffer +func UseBuffer(buf *bytes.Buffer) { + LogWriter = io.MultiWriter(buf) + pterm.SetDefaultOutput(LogWriter) +} + // SetLogLevel sets the log level. func SetLogLevel(lvl LogLevel) { logLevel = lvl @@ -279,6 +286,10 @@ func JSONValue(value any) string { return string(bytes) } +func Printf(format string, a ...any) { + pterm.Printf(format, a...) +} + // Paragraph formats text into a paragraph matching the TermWidth func Paragraph(format string, a ...any) string { return Paragraphn(TermWidth, format, a...) diff --git a/src/test/e2e/cmd/testdata/tools/print/resources.golden b/src/test/e2e/cmd/testdata/tools/print/resources.golden new file mode 100644 index 00000000..f8161f0a --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/print/resources.golden @@ -0,0 +1,28 @@ +{ + "istioConfig": { + "accessLogFile": "/dev/stdout", + "defaultConfig": { + "discoveryAddress": "istiod.istio-system.svc:15012", + "gatewayTopology": { + "forwardClientCertDetails": "SANITIZE" + }, + "holdApplicationUntilProxyStarts": true, + "tracing": { + "zipkin": { + "address": "zipkin.istio-system:9411" + } + } + }, + "defaultProviders": { + "metrics": [ + "prometheus" + ] + }, + "enablePrometheusMerge": true, + "pathNormalization": { + "normalization": "MERGE_SLASHES" + }, + "rootNamespace": "istio-system", + "trustDomain": "cluster.local" + } +} \ No newline at end of file diff --git a/src/test/e2e/cmd/testdata/tools/print/validation.golden b/src/test/e2e/cmd/testdata/tools/print/validation.golden new file mode 100644 index 00000000..c8790586 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/print/validation.golden @@ -0,0 +1,23 @@ +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" + } + } \ No newline at end of file diff --git a/src/test/e2e/cmd/tools_print_test.go b/src/test/e2e/cmd/tools_print_test.go new file mode 100644 index 00000000..34c97f35 --- /dev/null +++ b/src/test/e2e/cmd/tools_print_test.go @@ -0,0 +1,109 @@ +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" + "github.com/defenseunicorns/lula/src/pkg/message" +) + +func TestDevPrintResourcesCommand(t *testing.T) { + message.NoProgress = true + + test := func(t *testing.T, args ...string) error { + rootCmd := tools.PrintCommand() + + return runCmdTest(t, rootCmd, args...) + } + + testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := tools.PrintCommand() + + return runCmdTestWithGolden(t, "tools/print/", goldenFileName, rootCmd, args...) + } + + t.Run("Print Resources", func(t *testing.T) { + err := testAgainstGolden(t, "resources", "--resources", + "-a", "../../unit/common/oscal/valid-assessment-results-with-resources.yaml", + "-u", "92cb3cad-bbcd-431a-aaa9-cd47275a3982", + ) + require.NoError(t, err) + }) + + t.Run("Print Resources - invalid oscal", func(t *testing.T) { + err := test(t, "--resources", + "-a", "../../unit/common/validation/validation.opa.yaml", + "-u", "92cb3cad-bbcd-431a-aaa9-cd47275a3982", + ) + require.ErrorContains(t, err, "error creating oscal assessment results model") + }) + + t.Run("Print Resources - no uuid", func(t *testing.T) { + err := test(t, "--resources", + "-a", "../../unit/common/oscal/valid-assessment-results-with-resources.yaml", + "-u", "foo", + ) + require.ErrorContains(t, err, "error printing resources") + }) + + t.Run("Print Validation", func(t *testing.T) { + err := testAgainstGolden(t, "validation", "--validation", + "-a", "../../unit/common/oscal/valid-assessment-results-with-resources.yaml", + "-c", "../../unit/common/oscal/valid-multi-component-validations.yaml", + "-u", "92cb3cad-bbcd-431a-aaa9-cd47275a3982", + ) + require.NoError(t, err) + }) + + t.Run("Print Validation non-composed component", func(t *testing.T) { + tempDir := t.TempDir() + outputFile := filepath.Join(tempDir, "output.yaml") + + err := test(t, "--validation", + "-a", "../scenarios/validation-composition/assessment-results.yaml", + "-c", "../scenarios/validation-composition/component-definition.yaml", + "-u", "d328a0a1-630b-40a2-9c9d-4818420a4126", + "-o", outputFile, + ) + + require.NoError(t, err) + + // Check that the output file matches the expected validation + var validation common.Validation + validationBytes, err := os.ReadFile(outputFile) + require.NoErrorf(t, err, "error reading validation output: %v", err) + err = validation.UnmarshalYaml(validationBytes) + require.NoErrorf(t, err, "error unmarshalling validation: %v", err) + + var expectedValidation common.Validation + expectedValidationBytes, err := os.ReadFile("../scenarios/validation-composition/validation.opa.yaml") + require.NoErrorf(t, err, "error reading expected validation: %v", err) + err = expectedValidation.UnmarshalYaml(expectedValidationBytes) + require.NoErrorf(t, err, "error unmarshalling expected validation: %v", err) + + require.Equalf(t, expectedValidation, validation, "expected validation does not match actual validation") + }) + + t.Run("Print Validation - invalid assessment oscal", func(t *testing.T) { + err := test(t, "--validation", + "-a", "../../unit/common/validation/validation.opa.yaml", + "-c", "../../unit/common/oscal/valid-multi-component-validations.yaml", + "-u", "92cb3cad-bbcd-431a-aaa9-cd47275a3982", + ) + require.ErrorContains(t, err, "error creating oscal assessment results model") + }) + + t.Run("Print Validation - no uuid", func(t *testing.T) { + err := test(t, "--validation", + "-a", "../../unit/common/oscal/valid-assessment-results-with-resources.yaml", + "-c", "../../unit/common/oscal/valid-multi-component-validations.yaml", + "-u", "foo", + ) + require.ErrorContains(t, err, "error printing validation") + }) +} diff --git a/src/test/e2e/scenarios/validation-composition/assessment-results.yaml b/src/test/e2e/scenarios/validation-composition/assessment-results.yaml new file mode 100644 index 00000000..7a4599b5 --- /dev/null +++ b/src/test/e2e/scenarios/validation-composition/assessment-results.yaml @@ -0,0 +1,132 @@ +assessment-results: + import-ap: + href: "" + metadata: + last-modified: 2024-11-05T09:29:21.807114-05:00 + oscal-version: 1.1.2 + published: 2024-11-05T09:29:21.807114-05:00 + remarks: Assessment Results generated from Lula + title: '[System Name] Security Assessment Results (SAR)' + version: 0.0.1 + results: + - description: Assessment results for performing Validations with Lula version v0.10.0-15-g2b1bad8 + findings: + - description: | + Control Implementation: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A / Implemented Requirement: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + 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. + related-observations: + - observation-uuid: 672025f8-a7c5-4e97-bd32-07fde6ae1fd4 + - observation-uuid: 52d10fa5-0b87-45f5-9b50-58c8fef64211 + - observation-uuid: 352e5f99-a5dc-4c9e-ba40-c143e4191ebd + - observation-uuid: d328a0a1-630b-40a2-9c9d-4818420a4126 + - observation-uuid: 88c0ebae-f715-4c68-b3c8-460493a902ae + - observation-uuid: 88c0ebae-f715-4c68-b3c8-460493a902ae + - observation-uuid: c6bbc490-a3c8-464c-a929-9022219872a8 + target: + status: + state: satisfied + target-id: ID-1 + type: objective-id + title: 'Validation Result - Control: ID-1' + uuid: 21c82470-a762-49b5-b404-4e5a22604217 + observations: + - collected: 2024-11-05T09:29:21.782807-05:00 + description: | + [TEST]: 2d9858bc-fb54-42e7-a928-43f840ac0ae6 - Kyverno validate pods with label foo=bar + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#2d9858bc-fb54-42e7-a928-43f840ac0ae6' + relevant-evidence: + - description: | + Result: satisfied + remarks: | + labels,foo-label-exists-0,0: PASS + uuid: 352e5f99-a5dc-4c9e-ba40-c143e4191ebd + - collected: 2024-11-05T09:29:21.792788-05:00 + description: | + [TEST]: 7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f - Validate pods with label foo=bar + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#7f4c3b2a-1c3d-4a2b-8b64-3b1f76a8e36f' + relevant-evidence: + - description: | + Result: satisfied + uuid: d328a0a1-630b-40a2-9c9d-4818420a4126 + - collected: 2024-11-05T09:29:21.797056-05:00 + description: | + [TEST]: 9d09b4fc-1a82-4434-9fbe-392935347a84 - Validate pods with label foo=bar + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#9d09b4fc-1a82-4434-9fbe-392935347a84' + relevant-evidence: + - description: | + Result: satisfied + uuid: 88c0ebae-f715-4c68-b3c8-460493a902ae + - collected: 2024-11-05T09:29:21.800271-05:00 + description: | + [TEST]: 88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288 - Kyverno validate pods with label foo=bar + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#88ea1e4c-c1a6-4e55-87c0-ba17c1b7c288' + relevant-evidence: + - description: | + Result: satisfied + remarks: | + labels,foo-label-exists-0,0: PASS + uuid: c6bbc490-a3c8-464c-a929-9022219872a8 + - collected: 2024-11-05T09:29:21.803334-05:00 + description: | + [TEST]: 6c00ae8d-7187-42ab-8d89-f383447a0824 - Validate pods with label foo=bar + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#6c00ae8d-7187-42ab-8d89-f383447a0824' + relevant-evidence: + - description: | + Result: satisfied + uuid: 672025f8-a7c5-4e97-bd32-07fde6ae1fd4 + - collected: 2024-11-05T09:29:21.806843-05:00 + description: | + [TEST]: 7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e - Validate pods with label foo=bar + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#7f4b12a9-3b8f-4a8e-9f6e-8c8f506c851e' + relevant-evidence: + - description: | + Result: satisfied + uuid: 52d10fa5-0b87-45f5-9b50-58c8fef64211 + props: + - name: threshold + ns: https://docs.lula.dev/oscal/ns + value: "false" + - name: target + ns: https://docs.lula.dev/oscal/ns + value: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + reviewed-controls: + control-selections: + - description: Controls Assessed by Lula + include-controls: + - control-id: ID-1 + description: Controls validated + remarks: Validation performed may indicate full or partial satisfaction + start: 2024-11-05T09:29:21.80711-05:00 + title: Lula Validation Result + uuid: 9d54c104-0792-4fbd-9226-faafd36645f3 + uuid: 5f611677-0466-4e50-9a54-765f71381bfd diff --git a/src/test/unit/common/oscal/valid-assessment-results-with-resources.yaml b/src/test/unit/common/oscal/valid-assessment-results-with-resources.yaml new file mode 100644 index 00000000..74dff9af --- /dev/null +++ b/src/test/unit/common/oscal/valid-assessment-results-with-resources.yaml @@ -0,0 +1,156 @@ +assessment-results: + import-ap: + href: "" + metadata: + last-modified: 2024-10-15T10:56:06.577123-04:00 + oscal-version: 1.1.2 + published: 2024-10-15T10:55:51.725572-04:00 + remarks: Assessment Results generated from Lula + title: '[System Name] Security Assessment Results (SAR)' + version: 0.0.1 + results: + - description: Assessment results for performing Validations with Lula version v0.9.1 + findings: + - description: | + Control Implementation: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A / Implemented Requirement: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + related-observations: + - observation-uuid: 92cb3cad-bbcd-431a-aaa9-cd47275a3982 + target: + status: + state: not-satisfied + target-id: ID-1 + type: objective-id + title: 'Validation Result - Control: ID-1' + uuid: 4fe1724e-e63b-45cb-8e3f-2efd96823993 + - description: | + Control Implementation: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A / Implemented Requirement: 86a0e8d9-0ce0-4304-afe7-4c000001e032 + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + related-observations: + - observation-uuid: e1ca2968-8652-41be-a19f-c32bc0b3086c + target: + status: + state: not-satisfied + target-id: ID-2 + type: objective-id + title: 'Validation Result - Control: ID-2' + uuid: a0af133c-a31d-4417-8491-5f1aad3c40cf + observations: + - collected: 2024-10-15T10:56:06.553304-04:00 + description: | + [TEST]: 88AB3470-B96B-4D7C-BC36-02BF9563C46C - lula-validation-1 + links: + - rel: lula.resources + href: file://../resources/valid-resources.json + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#88AB3470-B96B-4D7C-BC36-02BF9563C46C' + relevant-evidence: + - description: | + Result: not-satisfied + remarks: | + validate.msg: Pod label foo is NOT bar + uuid: 92cb3cad-bbcd-431a-aaa9-cd47275a3982 + - collected: 2024-10-15T10:56:06.559086-04:00 + description: | + [TEST]: 01e21994-2cfc-45fb-ac84-d00f2e5912b0 - lula-validation-2 + links: + - rel: lula.resources + href: file://../resources/invalid-resources.txt + methods: + - TEST + relevant-evidence: + - description: | + Result: not-satisfied + uuid: e1ca2968-8652-41be-a19f-c32bc0b3086c + props: + - name: threshold + ns: https://docs.lula.dev/oscal/ns + value: "false" + - name: target + ns: https://docs.lula.dev/oscal/ns + value: https://github.com/defenseunicorns/lula https://github.com/defenseunicorns/lula + reviewed-controls: + control-selections: + - description: Controls Assessed by Lula + include-controls: + - control-id: ID-1 + - control-id: ID-2 + description: Controls validated + remarks: Validation performed may indicate full or partial satisfaction + start: 2024-10-15T10:56:06.560163-04:00 + title: Lula Validation Result + uuid: ab06dbe8-d6a4-47fb-8476-f54809ca61e3 + - description: Assessment results for performing Validations with Lula version v0.9.1 + findings: + - description: | + Control Implementation: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A / Implemented Requirement: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + related-observations: + - observation-uuid: 0e7fc8b3-b230-49f9-be7c-8f011536dfb4 + target: + status: + state: satisfied + target-id: ID-1 + type: objective-id + title: 'Validation Result - Control: ID-1' + uuid: 1389026d-039d-4e2f-97b8-169d75210dc2 + - description: | + Control Implementation: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A / Implemented Requirement: 86a0e8d9-0ce0-4304-afe7-4c000001e032 + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + related-observations: + - observation-uuid: af060637-2899-4f26-ae9d-2c1bbbddc4b0 + target: + status: + state: satisfied + target-id: ID-2 + type: objective-id + title: 'Validation Result - Control: ID-2' + uuid: 0e34e5cb-8eb4-49e4-b1f3-e6836b1fe791 + observations: + - collected: 2024-10-15T10:55:51.721924-04:00 + description: | + [TEST]: 01e21994-2cfc-45fb-ac84-d00f2e5912b0 - lula-validation-2 + methods: + - TEST + props: + - name: validation + ns: https://docs.lula.dev/oscal/ns + value: '#not-found' + relevant-evidence: + - description: | + Result: satisfied + uuid: af060637-2899-4f26-ae9d-2c1bbbddc4b0 + - collected: 2024-10-15T10:55:51.725223-04:00 + description: | + [TEST]: 88AB3470-B96B-4D7C-BC36-02BF9563C46C - lula-validation-1 + methods: + - TEST + relevant-evidence: + - description: | + Result: satisfied + remarks: | + validate.msg: Pod label foo=bar is satisfied + uuid: 0e7fc8b3-b230-49f9-be7c-8f011536dfb4 + props: + - name: threshold + ns: https://docs.lula.dev/oscal/ns + value: "false" + - name: target + ns: https://docs.lula.dev/oscal/ns + value: https://github.com/defenseunicorns/lula https://github.com/defenseunicorns/lula + reviewed-controls: + control-selections: + - description: Controls Assessed by Lula + include-controls: + - control-id: ID-1 + - control-id: ID-2 + description: Controls validated + remarks: Validation performed may indicate full or partial satisfaction + start: 2024-10-15T10:55:51.725567-04:00 + title: Lula Validation Result + uuid: 9e445ab0-360c-456f-888d-37643225c8a7 + uuid: a3d45141-abd1-4a25-82db-05fd3ccf9c53 diff --git a/src/test/unit/common/resources/invalid-resources.txt b/src/test/unit/common/resources/invalid-resources.txt new file mode 100644 index 00000000..40ad6804 --- /dev/null +++ b/src/test/unit/common/resources/invalid-resources.txt @@ -0,0 +1 @@ +some text here. \ No newline at end of file diff --git a/src/test/unit/common/resources/valid-resources.json b/src/test/unit/common/resources/valid-resources.json new file mode 100644 index 00000000..95a53861 --- /dev/null +++ b/src/test/unit/common/resources/valid-resources.json @@ -0,0 +1,28 @@ +{ + "istioConfig": { + "accessLogFile": "/dev/stdout", + "defaultConfig": { + "discoveryAddress": "istiod.istio-system.svc:15012", + "gatewayTopology": { + "forwardClientCertDetails": "SANITIZE" + }, + "holdApplicationUntilProxyStarts": true, + "tracing": { + "zipkin": { + "address": "zipkin.istio-system:9411" + } + } + }, + "defaultProviders": { + "metrics": [ + "prometheus" + ] + }, + "enablePrometheusMerge": true, + "pathNormalization": { + "normalization": "MERGE_SLASHES" + }, + "rootNamespace": "istio-system", + "trustDomain": "cluster.local" + } +} \ No newline at end of file diff --git a/src/test/unit/common/validation/validation.resource-print.yaml b/src/test/unit/common/validation/validation.resource-print.yaml new file mode 100644 index 00000000..c8790586 --- /dev/null +++ b/src/test/unit/common/validation/validation.resource-print.yaml @@ -0,0 +1,23 @@ +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" + } + } \ No newline at end of file diff --git a/src/test/util/utils.go b/src/test/util/utils.go index 97d3bc0f..c53deea8 100644 --- a/src/test/util/utils.go +++ b/src/test/util/utils.go @@ -13,6 +13,8 @@ import ( rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" + + "github.com/defenseunicorns/lula/src/pkg/message" ) func GetDeployment(path string) (*appsv1.Deployment, error) { @@ -131,6 +133,7 @@ func ExecuteCommandC(cmd *cobra.Command, args ...string) (c *cobra.Command, outp cmd.SetOut(buf) cmd.SetErr(buf) cmd.SetArgs(args) + message.UseBuffer(buf) execErr := cmd.Execute()