From afe8494a38681cd9b1581b72f675b9a84fcafa2b Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 14:32:14 -0400 Subject: [PATCH 01/14] feat(dev): print resources cmd, refactor --- src/cmd/dev/common.go | 43 ++++++++- src/cmd/dev/common_test.go | 32 +++++++ src/cmd/dev/get-resources.go | 21 +---- src/cmd/dev/print-resources.go | 130 ++++++++++++++++++++++++++++ src/cmd/dev/print-resources_test.go | 58 +++++++++++++ src/cmd/dev/print-validation.go | 118 +++++++++++++++++++++++++ 6 files changed, 383 insertions(+), 19 deletions(-) create mode 100644 src/cmd/dev/common_test.go create mode 100644 src/cmd/dev/print-resources.go create mode 100644 src/cmd/dev/print-resources_test.go create mode 100644 src/cmd/dev/print-validation.go diff --git a/src/cmd/dev/common.go b/src/cmd/dev/common.go index e8eaaa4f..01aca72f 100644 --- a/src/cmd/dev/common.go +++ b/src/cmd/dev/common.go @@ -4,15 +4,18 @@ import ( "context" "fmt" "io" + "os" "strings" "time" + oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + "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" @@ -104,3 +107,39 @@ func RunSingleValidation(ctx context.Context, validationBytes []byte, opts ...ty return lulaValidation, 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) +} + +// 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) // G306 + if err != nil { + return fmt.Errorf("error writing resource JSON to file: %v", err) + } + } else { + // fmt.Printf(jsonData) + message.Printf(jsonData) + // message.Detail(jsonData) + } + return nil +} diff --git a/src/cmd/dev/common_test.go b/src/cmd/dev/common_test.go new file mode 100644 index 00000000..5995c349 --- /dev/null +++ b/src/cmd/dev/common_test.go @@ -0,0 +1,32 @@ +package dev_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/internal/testhelpers" +) + +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 := dev.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 := dev.GetObservationByUuid(assessment, "invalid-uuid") + assert.Nil(t, observation) + require.ErrorContains(t, err, "observation with uuid invalid-uuid not found") + }) + +} diff --git a/src/cmd/dev/get-resources.go b/src/cmd/dev/get-resources.go index 52d0b61d..e9e65d6b 100644 --- a/src/cmd/dev/get-resources.go +++ b/src/cmd/dev/get-resources.go @@ -3,7 +3,6 @@ package dev import ( "context" "fmt" - "os" "github.com/defenseunicorns/lula/src/cmd/common" "github.com/defenseunicorns/lula/src/pkg/message" @@ -53,7 +52,10 @@ var getResourcesCmd = &cobra.Command{ message.Fatalf(err, "error running dev get-resources: %v", err) } - writeResources(collection, getResourcesOpts.OutputFile) + err = writeResources(collection, getResourcesOpts.OutputFile) + if err != nil { + message.Fatalf(err, "error writing resources: %v", err) + } spinner.Success() }, @@ -85,18 +87,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/dev/print-resources.go b/src/cmd/dev/print-resources.go new file mode 100644 index 00000000..017c5a96 --- /dev/null +++ b/src/cmd/dev/print-resources.go @@ -0,0 +1,130 @@ +package dev + +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" + + "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 printResourcesHelp = ` +To print resources from lula validation manifest: + lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid + +To print resources from lula validation manifest to output file: + lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json +` + +var printResourcesCmdLong = ` +Print out the the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. +` + +func PrintResourcesCommand() *cobra.Command { + var ( + assessment string // -a --assessment + observationUuid string // -u --observation-uuid + outputFile string // -o --output-file + ) + + printResourcesCmd := &cobra.Command{ + Use: "print-resources", + Short: "Print Resources from a Lula Validation evaluation", + Long: printResourcesCmdLong, + Example: printResourcesHelp, + RunE: func(cmd *cobra.Command, args []string) error { + assessmentData, err := os.ReadFile(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) + } + + err = PrintResources(oscalAssessment, observationUuid, assessmentDir, outputFile) + if err != nil { + return fmt.Errorf("error printing resources: %v", err) + } + return nil + }, + } + + printResourcesCmd.Flags().StringVarP(&assessment, "assessment", "a", "", "the path to an assessment-results file") + printResourcesCmd.Flags().StringVarP(&observationUuid, "observation-uuid", "u", "", "the observation uuid") + printResourcesCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to write the resources json") + err := printResourcesCmd.MarkFlagRequired("assessment") + if err != nil { + message.Fatal(err, "error initializing print-resources command flag: assessment") + } + err = printResourcesCmd.MarkFlagRequired("observation-uuid") + if err != nil { + message.Fatal(err, "error initializing required command flag: observation-uuid") + } + + return printResourcesCmd +} + +func init() { + devCmd.AddCommand(PrintResourcesCommand()) +} + +func PrintResources(assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid, assessmentDir, outputFile string) error { + if assessment == nil { + return fmt.Errorf("assessment is nil") + } + + observation, err := 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 = writeResources(resource, outputFile) + if err != nil { + return err + } + + return nil +} diff --git a/src/cmd/dev/print-resources_test.go b/src/cmd/dev/print-resources_test.go new file mode 100644 index 00000000..ff8f5413 --- /dev/null +++ b/src/cmd/dev/print-resources_test.go @@ -0,0 +1,58 @@ +package dev_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/internal/testhelpers" + "github.com/defenseunicorns/lula/src/types" +) + +func TestPrintResources(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 print resources", func(t *testing.T) { + tmpFile := testhelpers.CreateTempFile(t, ".json") + defer os.Remove(tmpFile.Name()) + + err := dev.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 := dev.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 := dev.PrintResources(assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "../../test/unit/common/oscal", "") + require.ErrorContains(t, err, "observation does not contain a remote reference") + }) + +} diff --git a/src/cmd/dev/print-validation.go b/src/cmd/dev/print-validation.go new file mode 100644 index 00000000..1df4687a --- /dev/null +++ b/src/cmd/dev/print-validation.go @@ -0,0 +1,118 @@ +package dev + +import ( + "fmt" + "os" + + 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/oscal" + "github.com/defenseunicorns/lula/src/pkg/message" + "github.com/spf13/cobra" +) + +type printValidationFlags struct { + component string // -c --component + assessment string // -a --assessment + observationUuid string // -o --observation-uuid +} + +var printValidationOpts = &printValidationFlags{} + +var printValidationHelp = ` +To print a specific lula validation that generated a given observation: + lula dev print-validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid +` + +var printValidationCmdLong = ` +Print out the the Lula Validation that yielded the provided observation +` + +func init() { + printValidationCmd := &cobra.Command{ + Use: "print-validation", + Short: "Print Lula Validation", + Long: printResourcesCmdLong, + Example: printResourcesHelp, + Run: func(cmd *cobra.Command, args []string) { + // Get the component, assessment, validation ID, and result type to lookup the resources + componentData, err := os.ReadFile(printValidationOpts.component) + if err != nil { + message.Fatalf(nil, "Invalid component file: %v", err) + } + + assessmentData, err := os.ReadFile(printValidationOpts.assessment) + if err != nil { + message.Fatalf(nil, "Invalid assessment file: %v", err) + } + + oscalComponent, err := oscal.NewOscalComponentDefinition(componentData) + if err != nil { + message.Fatalf(nil, "Invalid component definition model: %v", err) + } + + oscalAssessment, err := oscal.NewAssessmentResults(assessmentData) + if err != nil { + message.Fatalf(nil, "Invalid assessment results model: %v", err) + } + + printValidation(oscalComponent, oscalAssessment, printValidationOpts.observationUuid) + + }, + } + + devCmd.AddCommand(printValidationCmd) + printValidationCmd.Flags().StringVar(&printValidationOpts.component, "component", "", "the path to a validation manifest file") + printValidationCmd.Flags().StringVar(&printValidationOpts.assessment, "assessment", "", "the path to an assessment-results file") + printValidationCmd.Flags().StringVar(&printValidationOpts.observationUuid, "observation-uuid", "", "the observation uuid") + printValidationCmd.MarkFlagRequired("component") + printValidationCmd.MarkFlagRequired("assessment") + printValidationCmd.MarkFlagRequired("observation-uuid") +} + +func printValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid string) { + if component == nil { + message.Fatalf(nil, "Component definition is nil") + } + + if assessment == nil { + message.Fatalf(nil, "Assessment results is nil") + } + + // Get all observations from the assessment results + observationMap := make(map[string]oscalTypes_1_1_2.Observation) + for _, result := range assessment.Results { + if result.Observations != nil { + for _, observation := range *result.Observations { + observationMap[observation.UUID] = observation + } + } + } + + observation, found := observationMap[observationUuid] + if !found { + message.Fatalf(nil, "Observation not found in assessment results") + } + + found, validationUuid := oscal.GetProp("validation", oscal.LULA_NAMESPACE, observation.Props) + if !found { + message.Fatalf(nil, "No validation linked to observation") + } + + // Get all validations from the component definition + 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 { + message.Fatalf(nil, "Validation not found in component definition") + } + + // Print the validation + fmt.Println(validation) +} From ac0d51b4acebcdae5dcf08f5753e0b480a4076a2 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 14:33:21 -0400 Subject: [PATCH 02/14] test(dev): test for print-resources, msg additions --- src/pkg/message/message.go | 11 ++ src/test/e2e/cmd/dev_print_resources_test.go | 50 ++++++ .../dev/print-resources/resources.golden | 28 ++++ ...lid-assessment-results-with-resources.yaml | 148 ++++++++++++++++++ .../common/resources/invalid-resources.txt | 1 + .../common/resources/valid-resources.json | 28 ++++ src/test/util/utils.go | 3 + 7 files changed, 269 insertions(+) create mode 100644 src/test/e2e/cmd/dev_print_resources_test.go create mode 100644 src/test/e2e/cmd/testdata/dev/print-resources/resources.golden create mode 100644 src/test/unit/common/oscal/valid-assessment-results-with-resources.yaml create mode 100644 src/test/unit/common/resources/invalid-resources.txt create mode 100644 src/test/unit/common/resources/valid-resources.json 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/dev_print_resources_test.go b/src/test/e2e/cmd/dev_print_resources_test.go new file mode 100644 index 00000000..6c736326 --- /dev/null +++ b/src/test/e2e/cmd/dev_print_resources_test.go @@ -0,0 +1,50 @@ +package cmd_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/pkg/message" +) + +func TestDevPrintResourcesCommand(t *testing.T) { + message.NoProgress = true + + test := func(t *testing.T, args ...string) error { + rootCmd := dev.PrintResourcesCommand() + + return runCmdTest(t, rootCmd, args...) + } + + testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := dev.PrintResourcesCommand() + + return runCmdTestWithGolden(t, "dev/print-resources/", goldenFileName, rootCmd, args...) + } + + t.Run("Print Resources", func(t *testing.T) { + err := testAgainstGolden(t, "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, + "-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, + "-a", "../../unit/common/oscal/valid-assessment-results-with-resources.yaml", + "-u", "foo", + ) + require.ErrorContains(t, err, "error printing resources") + }) +} diff --git a/src/test/e2e/cmd/testdata/dev/print-resources/resources.golden b/src/test/e2e/cmd/testdata/dev/print-resources/resources.golden new file mode 100644 index 00000000..f8161f0a --- /dev/null +++ b/src/test/e2e/cmd/testdata/dev/print-resources/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/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..7c00ce6e --- /dev/null +++ b/src/test/unit/common/oscal/valid-assessment-results-with-resources.yaml @@ -0,0 +1,148 @@ +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 + 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 + 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/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() From 9542eaef6031d6e212be4183d8f21af5d5357590 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 14:48:54 -0400 Subject: [PATCH 03/14] feat(dev): print validation cmd --- src/cmd/dev/common.go | 4 +- src/cmd/dev/print-validation.go | 107 ++++++++++++++++++++------------ 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/src/cmd/dev/common.go b/src/cmd/dev/common.go index 01aca72f..94cf57ce 100644 --- a/src/cmd/dev/common.go +++ b/src/cmd/dev/common.go @@ -132,14 +132,12 @@ func writeResources(data types.DomainResources, filepath string) error { // If a filepath is provided, write the JSON data to the file. if filepath != "" { - err := os.WriteFile(filepath, []byte(jsonData), 0600) // G306 + err := os.WriteFile(filepath, []byte(jsonData), 0600) if err != nil { return fmt.Errorf("error writing resource JSON to file: %v", err) } } else { - // fmt.Printf(jsonData) message.Printf(jsonData) - // message.Detail(jsonData) } return nil } diff --git a/src/cmd/dev/print-validation.go b/src/cmd/dev/print-validation.go index 1df4687a..ce749ad5 100644 --- a/src/cmd/dev/print-validation.go +++ b/src/cmd/dev/print-validation.go @@ -3,12 +3,14 @@ package dev import ( "fmt" "os" + "path/filepath" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + "github.com/spf13/cobra" + "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/oscal" "github.com/defenseunicorns/lula/src/pkg/message" - "github.com/spf13/cobra" ) type printValidationFlags struct { @@ -28,78 +30,99 @@ var printValidationCmdLong = ` Print out the the Lula Validation that yielded the provided observation ` -func init() { +func PrintValidationCommand() *cobra.Command { + var ( + component string // -c --component + assessment string // -a --assessment + observationUuid string // -u --observation-uuid + outputFile string // -o --output-file + ) + printValidationCmd := &cobra.Command{ Use: "print-validation", Short: "Print Lula Validation", Long: printResourcesCmdLong, Example: printResourcesHelp, - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { // Get the component, assessment, validation ID, and result type to lookup the resources - componentData, err := os.ReadFile(printValidationOpts.component) + componentData, err := os.ReadFile(component) if err != nil { - message.Fatalf(nil, "Invalid component file: %v", err) + return fmt.Errorf("invalid component file: %v", err) } - assessmentData, err := os.ReadFile(printValidationOpts.assessment) + oscalComponent, err := oscal.NewOscalComponentDefinition(componentData) if err != nil { - message.Fatalf(nil, "Invalid assessment file: %v", err) + return fmt.Errorf("error creating oscal component definition model: %v", err) } - oscalComponent, err := oscal.NewOscalComponentDefinition(componentData) + componentDir, err := filepath.Abs(filepath.Dir(component)) if err != nil { - message.Fatalf(nil, "Invalid component definition model: %v", err) + return fmt.Errorf("error getting component directory: %v", err) + } + + assessmentData, err := os.ReadFile(assessment) + if err != nil { + return fmt.Errorf("invalid assessment file: %v", err) } oscalAssessment, err := oscal.NewAssessmentResults(assessmentData) if err != nil { - message.Fatalf(nil, "Invalid assessment results model: %v", err) + return fmt.Errorf("error creating oscal assessment results model: %v", err) } - printValidation(oscalComponent, oscalAssessment, printValidationOpts.observationUuid) + err = PrintValidation(oscalComponent, oscalAssessment, componentDir, observationUuid, outputFile) + return nil }, } - devCmd.AddCommand(printValidationCmd) - printValidationCmd.Flags().StringVar(&printValidationOpts.component, "component", "", "the path to a validation manifest file") - printValidationCmd.Flags().StringVar(&printValidationOpts.assessment, "assessment", "", "the path to an assessment-results file") - printValidationCmd.Flags().StringVar(&printValidationOpts.observationUuid, "observation-uuid", "", "the observation uuid") - printValidationCmd.MarkFlagRequired("component") - printValidationCmd.MarkFlagRequired("assessment") - printValidationCmd.MarkFlagRequired("observation-uuid") + printValidationCmd.Flags().StringVarP(&component, "component", "c", "", "the path to a validation manifest file") + printValidationCmd.Flags().StringVarP(&assessment, "assessment", "a", "", "the path to an assessment-results file") + printValidationCmd.Flags().StringVarP(&observationUuid, "observation-uuid", "u", "", "the observation uuid") + printValidationCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to write the resources json") + + err := printValidationCmd.MarkFlagRequired("component") + if err != nil { + message.Fatal(err, "error initializing print-resources command flag: assessment") + } + err = printValidationCmd.MarkFlagRequired("assessment") + if err != nil { + message.Fatal(err, "error initializing print-resources command flag: assessment") + } + err = printValidationCmd.MarkFlagRequired("observation-uuid") + if err != nil { + message.Fatal(err, "error initializing required command flag: observation-uuid") + } + + return printValidationCmd +} + +func init() { + devCmd.AddCommand(PrintValidationCommand()) } -func printValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid string) { +func PrintValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment *oscalTypes_1_1_2.AssessmentResults, componentDir, observationUuid, outputFile string) error { if component == nil { - message.Fatalf(nil, "Component definition is nil") + return fmt.Errorf("component definition is nil") } if assessment == nil { - message.Fatalf(nil, "Assessment results is nil") - } - - // Get all observations from the assessment results - observationMap := make(map[string]oscalTypes_1_1_2.Observation) - for _, result := range assessment.Results { - if result.Observations != nil { - for _, observation := range *result.Observations { - observationMap[observation.UUID] = observation - } - } + return fmt.Errorf("assessment results is nil") } - observation, found := observationMap[observationUuid] - if !found { - message.Fatalf(nil, "Observation not found in assessment results") + // Get the observation + observation, err := GetObservationByUuid(assessment, observationUuid) + if err != nil { + return err } + // Get the validation found, validationUuid := oscal.GetProp("validation", oscal.LULA_NAMESPACE, observation.Props) if !found { - message.Fatalf(nil, "No validation linked to observation") + return fmt.Errorf("no validation linked to observation") } - // Get all validations from the component definition + // Find validation ID in the component definition back matter resourceMap := make(map[string]string) if component.BackMatter != nil { resourceMap = oscal.BackMatterToMap(*component.BackMatter) @@ -110,9 +133,17 @@ func printValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment // Find the validation in the map validation, found := resourceMap[trimmedId] if !found { - message.Fatalf(nil, "Validation not found in component definition") + return fmt.Errorf("validation not found in component definition") } // Print the validation - fmt.Println(validation) + if outputFile == "" { + message.Printf(validation) + } else { + err = os.WriteFile(outputFile, []byte(validation), 0600) + if err != nil { + return fmt.Errorf("error writing validation to file: %v", err) + } + } + return nil } From 2652616a037e759f44483a5600a419d15833464a Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 15:15:47 -0400 Subject: [PATCH 04/14] test(dev): test for print-validation --- src/cmd/dev/print-validation.go | 27 +++------- src/cmd/dev/print-validation_test.go | 52 +++++++++++++++++++ ...lid-assessment-results-with-resources.yaml | 8 +++ .../validation/validation.resource-print.yaml | 23 ++++++++ 4 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 src/cmd/dev/print-validation_test.go create mode 100644 src/test/unit/common/validation/validation.resource-print.yaml diff --git a/src/cmd/dev/print-validation.go b/src/cmd/dev/print-validation.go index ce749ad5..0624384f 100644 --- a/src/cmd/dev/print-validation.go +++ b/src/cmd/dev/print-validation.go @@ -3,7 +3,6 @@ package dev import ( "fmt" "os" - "path/filepath" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/spf13/cobra" @@ -13,21 +12,13 @@ import ( "github.com/defenseunicorns/lula/src/pkg/message" ) -type printValidationFlags struct { - component string // -c --component - assessment string // -a --assessment - observationUuid string // -o --observation-uuid -} - -var printValidationOpts = &printValidationFlags{} - var printValidationHelp = ` To print a specific lula validation that generated a given observation: lula dev print-validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid ` var printValidationCmdLong = ` -Print out the the Lula Validation that yielded the provided observation +Prints the Lula Validation from a specified observation. Assumes that the validation is in the back matter of the provided component definition. ` func PrintValidationCommand() *cobra.Command { @@ -41,8 +32,8 @@ func PrintValidationCommand() *cobra.Command { printValidationCmd := &cobra.Command{ Use: "print-validation", Short: "Print Lula Validation", - Long: printResourcesCmdLong, - Example: printResourcesHelp, + Long: printValidationCmdLong, + Example: printValidationHelp, RunE: func(cmd *cobra.Command, args []string) error { // Get the component, assessment, validation ID, and result type to lookup the resources componentData, err := os.ReadFile(component) @@ -55,11 +46,6 @@ func PrintValidationCommand() *cobra.Command { return fmt.Errorf("error creating oscal component definition model: %v", err) } - componentDir, err := filepath.Abs(filepath.Dir(component)) - if err != nil { - return fmt.Errorf("error getting component directory: %v", err) - } - assessmentData, err := os.ReadFile(assessment) if err != nil { return fmt.Errorf("invalid assessment file: %v", err) @@ -70,7 +56,10 @@ func PrintValidationCommand() *cobra.Command { return fmt.Errorf("error creating oscal assessment results model: %v", err) } - err = PrintValidation(oscalComponent, oscalAssessment, componentDir, observationUuid, outputFile) + err = PrintValidation(oscalComponent, oscalAssessment, observationUuid, outputFile) + if err != nil { + return fmt.Errorf("error printing validation: %v", err) + } return nil }, @@ -101,7 +90,7 @@ func init() { devCmd.AddCommand(PrintValidationCommand()) } -func PrintValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment *oscalTypes_1_1_2.AssessmentResults, componentDir, observationUuid, outputFile string) error { +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") } diff --git a/src/cmd/dev/print-validation_test.go b/src/cmd/dev/print-validation_test.go new file mode 100644 index 00000000..c44d598f --- /dev/null +++ b/src/cmd/dev/print-validation_test.go @@ -0,0 +1,52 @@ +package dev_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/internal/testhelpers" +) + +func TestPrintValidation(t *testing.T) { + t.Parallel() + + oscalAssessmentModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml") + assessment := oscalAssessmentModel.AssessmentResults + require.NotNil(t, assessment) + + oscalComponentModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-multi-component-validations.yaml") + 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 := dev.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 := dev.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 := dev.PrintValidation(component, assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "") + require.ErrorContains(t, err, "validation not found in component definition") + }) + +} 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 index 7c00ce6e..74dff9af 100644 --- a/src/test/unit/common/oscal/valid-assessment-results-with-resources.yaml +++ b/src/test/unit/common/oscal/valid-assessment-results-with-resources.yaml @@ -44,6 +44,10 @@ assessment-results: 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 @@ -112,6 +116,10 @@ assessment-results: [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 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 From 2ec2bc842460480fc43e8c772740b507bc5bcfef Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 15:18:55 -0400 Subject: [PATCH 05/14] test(dev): e2e cmd test for print validation --- src/test/e2e/cmd/dev_print_validation_test.go | 53 +++++++++++++++++++ .../dev/print-validation/validation.golden | 23 ++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/test/e2e/cmd/dev_print_validation_test.go create mode 100644 src/test/e2e/cmd/testdata/dev/print-validation/validation.golden diff --git a/src/test/e2e/cmd/dev_print_validation_test.go b/src/test/e2e/cmd/dev_print_validation_test.go new file mode 100644 index 00000000..6c47055f --- /dev/null +++ b/src/test/e2e/cmd/dev_print_validation_test.go @@ -0,0 +1,53 @@ +package cmd_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/pkg/message" +) + +func TestDevPrintValidationCommand(t *testing.T) { + message.NoProgress = true + + test := func(t *testing.T, args ...string) error { + rootCmd := dev.PrintValidationCommand() + + return runCmdTest(t, rootCmd, args...) + } + + testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := dev.PrintValidationCommand() + + return runCmdTestWithGolden(t, "dev/print-validation/", goldenFileName, rootCmd, args...) + } + + t.Run("Print Validation", func(t *testing.T) { + err := testAgainstGolden(t, "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 - invalid assessment oscal", func(t *testing.T) { + err := test(t, + "-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, + "-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/cmd/testdata/dev/print-validation/validation.golden b/src/test/e2e/cmd/testdata/dev/print-validation/validation.golden new file mode 100644 index 00000000..c8790586 --- /dev/null +++ b/src/test/e2e/cmd/testdata/dev/print-validation/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 From faf3c527aa485ac14945ade6c437f30c119849b5 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 16:22:41 -0400 Subject: [PATCH 06/14] docs: updated cli --- docs/cli-commands/lula_dev.md | 2 + docs/cli-commands/lula_dev_print-resources.md | 50 +++++++++++++++++++ .../cli-commands/lula_dev_print-validation.md | 48 ++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 docs/cli-commands/lula_dev_print-resources.md create mode 100644 docs/cli-commands/lula_dev_print-validation.md diff --git a/docs/cli-commands/lula_dev.md b/docs/cli-commands/lula_dev.md index d3051357..108703a4 100644 --- a/docs/cli-commands/lula_dev.md +++ b/docs/cli-commands/lula_dev.md @@ -24,5 +24,7 @@ Collection of dev commands to make dev life easier * [lula](./lula.md) - Risk Management as Code * [lula dev get-resources](./lula_dev_get-resources.md) - Get Resources from a Lula Validation Manifest * [lula dev lint](./lula_dev_lint.md) - Lint validation files against schema +* [lula dev print-resources](./lula_dev_print-resources.md) - Print Resources from a Lula Validation evaluation +* [lula dev print-validation](./lula_dev_print-validation.md) - Print Lula Validation * [lula dev validate](./lula_dev_validate.md) - Run an individual Lula validation. diff --git a/docs/cli-commands/lula_dev_print-resources.md b/docs/cli-commands/lula_dev_print-resources.md new file mode 100644 index 00000000..b938fed8 --- /dev/null +++ b/docs/cli-commands/lula_dev_print-resources.md @@ -0,0 +1,50 @@ +--- +title: lula dev print-resources +description: Lula CLI command reference for lula dev print-resources. +type: docs +--- +## lula dev print-resources + +Print Resources from a Lula Validation evaluation + +### Synopsis + + +Print out the the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. + + +``` +lula dev print-resources [flags] +``` + +### Examples + +``` + +To print resources from lula validation manifest: + lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid + +To print resources from lula validation manifest to output file: + lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json + +``` + +### Options + +``` + -a, --assessment string the path to an assessment-results file + -h, --help help for print-resources + -u, --observation-uuid string the observation uuid + -o, --output-file string the path to write the resources json +``` + +### 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 dev](./lula_dev.md) - Collection of dev commands to make dev life easier + diff --git a/docs/cli-commands/lula_dev_print-validation.md b/docs/cli-commands/lula_dev_print-validation.md new file mode 100644 index 00000000..419b007c --- /dev/null +++ b/docs/cli-commands/lula_dev_print-validation.md @@ -0,0 +1,48 @@ +--- +title: lula dev print-validation +description: Lula CLI command reference for lula dev print-validation. +type: docs +--- +## lula dev print-validation + +Print Lula Validation + +### Synopsis + + +Prints the Lula Validation from a specified observation. Assumes that the validation is in the back matter of the provided component definition. + + +``` +lula dev print-validation [flags] +``` + +### Examples + +``` + +To print a specific lula validation that generated a given observation: + lula dev 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-validation + -u, --observation-uuid string the observation uuid + -o, --output-file string the path to write the resources json +``` + +### 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 dev](./lula_dev.md) - Collection of dev commands to make dev life easier + From 69776fc813a8109e942c0268a7a6a26ba9608245 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 16:29:48 -0400 Subject: [PATCH 07/14] fix: go lint --- src/cmd/dev/common.go | 4 ++-- src/cmd/dev/print-validation.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cmd/dev/common.go b/src/cmd/dev/common.go index 94cf57ce..65d40d05 100644 --- a/src/cmd/dev/common.go +++ b/src/cmd/dev/common.go @@ -132,12 +132,12 @@ func writeResources(data types.DomainResources, filepath string) error { // If a filepath is provided, write the JSON data to the file. if filepath != "" { - err := os.WriteFile(filepath, []byte(jsonData), 0600) + err := os.WriteFile(filepath, []byte(jsonData), 0600) // G306 if err != nil { return fmt.Errorf("error writing resource JSON to file: %v", err) } } else { - message.Printf(jsonData) + message.Printf("%s", jsonData) } return nil } diff --git a/src/cmd/dev/print-validation.go b/src/cmd/dev/print-validation.go index 0624384f..363ae4a5 100644 --- a/src/cmd/dev/print-validation.go +++ b/src/cmd/dev/print-validation.go @@ -127,9 +127,9 @@ func PrintValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment // Print the validation if outputFile == "" { - message.Printf(validation) + message.Printf("%s", validation) } else { - err = os.WriteFile(outputFile, []byte(validation), 0600) + err = os.WriteFile(outputFile, []byte(validation), 0600) // G306 if err != nil { return fmt.Errorf("error writing validation to file: %v", err) } From 57f5749d653b0867ba1bd34e98eb2ca8d744442d Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Thu, 24 Oct 2024 16:35:47 -0400 Subject: [PATCH 08/14] fix: vuln scanner --- src/cmd/dev/common.go | 2 +- src/cmd/dev/print-resources.go | 4 ++-- src/cmd/dev/print-validation.go | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/cmd/dev/common.go b/src/cmd/dev/common.go index 65d40d05..32633ab1 100644 --- a/src/cmd/dev/common.go +++ b/src/cmd/dev/common.go @@ -132,7 +132,7 @@ func writeResources(data types.DomainResources, filepath string) error { // If a filepath is provided, write the JSON data to the file. if filepath != "" { - err := os.WriteFile(filepath, []byte(jsonData), 0600) // G306 + err := os.WriteFile(filepath, []byte(jsonData), 0600) if err != nil { return fmt.Errorf("error writing resource JSON to file: %v", err) } diff --git a/src/cmd/dev/print-resources.go b/src/cmd/dev/print-resources.go index 017c5a96..58b4f2d3 100644 --- a/src/cmd/dev/print-resources.go +++ b/src/cmd/dev/print-resources.go @@ -3,12 +3,12 @@ package dev 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" + "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/network" "github.com/defenseunicorns/lula/src/pkg/common/oscal" "github.com/defenseunicorns/lula/src/pkg/message" @@ -40,7 +40,7 @@ func PrintResourcesCommand() *cobra.Command { Long: printResourcesCmdLong, Example: printResourcesHelp, RunE: func(cmd *cobra.Command, args []string) error { - assessmentData, err := os.ReadFile(assessment) + assessmentData, err := common.ReadFileToBytes(assessment) if err != nil { return fmt.Errorf("invalid assessment file: %v", err) } diff --git a/src/cmd/dev/print-validation.go b/src/cmd/dev/print-validation.go index 363ae4a5..f4de251d 100644 --- a/src/cmd/dev/print-validation.go +++ b/src/cmd/dev/print-validation.go @@ -36,7 +36,7 @@ func PrintValidationCommand() *cobra.Command { Example: printValidationHelp, RunE: func(cmd *cobra.Command, args []string) error { // Get the component, assessment, validation ID, and result type to lookup the resources - componentData, err := os.ReadFile(component) + componentData, err := common.ReadFileToBytes(component) if err != nil { return fmt.Errorf("invalid component file: %v", err) } @@ -46,7 +46,7 @@ func PrintValidationCommand() *cobra.Command { return fmt.Errorf("error creating oscal component definition model: %v", err) } - assessmentData, err := os.ReadFile(assessment) + assessmentData, err := common.ReadFileToBytes(assessment) if err != nil { return fmt.Errorf("invalid assessment file: %v", err) } @@ -129,7 +129,7 @@ func PrintValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment if outputFile == "" { message.Printf("%s", validation) } else { - err = os.WriteFile(outputFile, []byte(validation), 0600) // G306 + err = os.WriteFile(outputFile, []byte(validation), 0600) if err != nil { return fmt.Errorf("error writing validation to file: %v", err) } From 6ddf90fb47f654c4d175b799d8d99fe16a528c30 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Wed, 30 Oct 2024 08:54:56 -0400 Subject: [PATCH 09/14] fix: updated cmd text --- src/cmd/dev/print-resources.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmd/dev/print-resources.go b/src/cmd/dev/print-resources.go index 58b4f2d3..65bcddda 100644 --- a/src/cmd/dev/print-resources.go +++ b/src/cmd/dev/print-resources.go @@ -24,7 +24,7 @@ To print resources from lula validation manifest to output file: ` var printResourcesCmdLong = ` -Print out the the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. +Print out the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. ` func PrintResourcesCommand() *cobra.Command { From 18e2b37d9e7883a1b36184cd5deb36fc1e4f674e Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Wed, 30 Oct 2024 09:27:37 -0400 Subject: [PATCH 10/14] fix: re-orged print commands --- src/cmd/dev/print-resources.go | 130 ----------- src/cmd/dev/print-validation.go | 138 ----------- src/cmd/dev/print-validation_test.go | 52 ----- src/cmd/dev/print.go | 214 ++++++++++++++++++ ...{print-resources_test.go => print_test.go} | 48 +++- src/test/e2e/cmd/dev_print_resources_test.go | 50 ---- ...t_validation_test.go => dev_print_test.go} | 38 +++- .../resources.golden | 0 .../validation.golden | 0 9 files changed, 292 insertions(+), 378 deletions(-) delete mode 100644 src/cmd/dev/print-resources.go delete mode 100644 src/cmd/dev/print-validation.go delete mode 100644 src/cmd/dev/print-validation_test.go create mode 100644 src/cmd/dev/print.go rename src/cmd/dev/{print-resources_test.go => print_test.go} (50%) delete mode 100644 src/test/e2e/cmd/dev_print_resources_test.go rename src/test/e2e/cmd/{dev_print_validation_test.go => dev_print_test.go} (53%) rename src/test/e2e/cmd/testdata/dev/{print-resources => print}/resources.golden (100%) rename src/test/e2e/cmd/testdata/dev/{print-validation => print}/validation.golden (100%) diff --git a/src/cmd/dev/print-resources.go b/src/cmd/dev/print-resources.go deleted file mode 100644 index 65bcddda..00000000 --- a/src/cmd/dev/print-resources.go +++ /dev/null @@ -1,130 +0,0 @@ -package dev - -import ( - "encoding/json" - "fmt" - "path/filepath" - - oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" - "github.com/spf13/cobra" - - "github.com/defenseunicorns/lula/src/pkg/common" - "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 printResourcesHelp = ` -To print resources from lula validation manifest: - lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid - -To print resources from lula validation manifest to output file: - lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json -` - -var printResourcesCmdLong = ` -Print out the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. -` - -func PrintResourcesCommand() *cobra.Command { - var ( - assessment string // -a --assessment - observationUuid string // -u --observation-uuid - outputFile string // -o --output-file - ) - - printResourcesCmd := &cobra.Command{ - Use: "print-resources", - Short: "Print Resources from a Lula Validation evaluation", - Long: printResourcesCmdLong, - Example: printResourcesHelp, - 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) - } - - err = PrintResources(oscalAssessment, observationUuid, assessmentDir, outputFile) - if err != nil { - return fmt.Errorf("error printing resources: %v", err) - } - return nil - }, - } - - printResourcesCmd.Flags().StringVarP(&assessment, "assessment", "a", "", "the path to an assessment-results file") - printResourcesCmd.Flags().StringVarP(&observationUuid, "observation-uuid", "u", "", "the observation uuid") - printResourcesCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to write the resources json") - err := printResourcesCmd.MarkFlagRequired("assessment") - if err != nil { - message.Fatal(err, "error initializing print-resources command flag: assessment") - } - err = printResourcesCmd.MarkFlagRequired("observation-uuid") - if err != nil { - message.Fatal(err, "error initializing required command flag: observation-uuid") - } - - return printResourcesCmd -} - -func init() { - devCmd.AddCommand(PrintResourcesCommand()) -} - -func PrintResources(assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid, assessmentDir, outputFile string) error { - if assessment == nil { - return fmt.Errorf("assessment is nil") - } - - observation, err := 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 = writeResources(resource, outputFile) - if err != nil { - return err - } - - return nil -} diff --git a/src/cmd/dev/print-validation.go b/src/cmd/dev/print-validation.go deleted file mode 100644 index f4de251d..00000000 --- a/src/cmd/dev/print-validation.go +++ /dev/null @@ -1,138 +0,0 @@ -package dev - -import ( - "fmt" - "os" - - oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" - "github.com/spf13/cobra" - - "github.com/defenseunicorns/lula/src/pkg/common" - "github.com/defenseunicorns/lula/src/pkg/common/oscal" - "github.com/defenseunicorns/lula/src/pkg/message" -) - -var printValidationHelp = ` -To print a specific lula validation that generated a given observation: - lula dev print-validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid -` - -var printValidationCmdLong = ` -Prints the Lula Validation from a specified observation. Assumes that the validation is in the back matter of the provided component definition. -` - -func PrintValidationCommand() *cobra.Command { - var ( - component string // -c --component - assessment string // -a --assessment - observationUuid string // -u --observation-uuid - outputFile string // -o --output-file - ) - - printValidationCmd := &cobra.Command{ - Use: "print-validation", - Short: "Print Lula Validation", - Long: printValidationCmdLong, - Example: printValidationHelp, - RunE: func(cmd *cobra.Command, args []string) error { - // Get the component, assessment, validation ID, and result type to lookup the resources - componentData, err := common.ReadFileToBytes(component) - if err != nil { - return fmt.Errorf("invalid component file: %v", err) - } - - oscalComponent, err := oscal.NewOscalComponentDefinition(componentData) - if err != nil { - return fmt.Errorf("error creating oscal component definition model: %v", err) - } - - assessmentData, err := common.ReadFileToBytes(assessment) - if err != nil { - return fmt.Errorf("invalid assessment file: %v", err) - } - - oscalAssessment, err := oscal.NewAssessmentResults(assessmentData) - if err != nil { - return fmt.Errorf("error creating oscal assessment results model: %v", err) - } - - err = PrintValidation(oscalComponent, oscalAssessment, observationUuid, outputFile) - if err != nil { - return fmt.Errorf("error printing validation: %v", err) - } - - return nil - }, - } - - printValidationCmd.Flags().StringVarP(&component, "component", "c", "", "the path to a validation manifest file") - printValidationCmd.Flags().StringVarP(&assessment, "assessment", "a", "", "the path to an assessment-results file") - printValidationCmd.Flags().StringVarP(&observationUuid, "observation-uuid", "u", "", "the observation uuid") - printValidationCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to write the resources json") - - err := printValidationCmd.MarkFlagRequired("component") - if err != nil { - message.Fatal(err, "error initializing print-resources command flag: assessment") - } - err = printValidationCmd.MarkFlagRequired("assessment") - if err != nil { - message.Fatal(err, "error initializing print-resources command flag: assessment") - } - err = printValidationCmd.MarkFlagRequired("observation-uuid") - if err != nil { - message.Fatal(err, "error initializing required command flag: observation-uuid") - } - - return printValidationCmd -} - -func init() { - devCmd.AddCommand(PrintValidationCommand()) -} - -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 := 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/dev/print-validation_test.go b/src/cmd/dev/print-validation_test.go deleted file mode 100644 index c44d598f..00000000 --- a/src/cmd/dev/print-validation_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package dev_test - -import ( - "os" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/defenseunicorns/lula/src/cmd/dev" - "github.com/defenseunicorns/lula/src/internal/testhelpers" -) - -func TestPrintValidation(t *testing.T) { - t.Parallel() - - oscalAssessmentModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml") - assessment := oscalAssessmentModel.AssessmentResults - require.NotNil(t, assessment) - - oscalComponentModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-multi-component-validations.yaml") - 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 := dev.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 := dev.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 := dev.PrintValidation(component, assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "") - require.ErrorContains(t, err, "validation not found in component definition") - }) - -} diff --git a/src/cmd/dev/print.go b/src/cmd/dev/print.go new file mode 100644 index 00000000..2902a796 --- /dev/null +++ b/src/cmd/dev/print.go @@ -0,0 +1,214 @@ +package dev + +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" + + "github.com/defenseunicorns/lula/src/pkg/common" + "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 dev print --resources --assessment /path/to/assessment.yaml --observation-uuid + +To print resources from lula validation manifest to output file: + lula dev 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 dev print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid +` + +var printCmdLong = ` +Print out data about an Observation. +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 { + componentData, err := common.ReadFileToBytes(component) + if err != nil { + return fmt.Errorf("invalid component file: %v", err) + } + + oscalComponent, err := oscal.NewOscalComponentDefinition(componentData) + if err != nil { + return fmt.Errorf("error creating oscal component definition model: %v", err) + } + + err = PrintValidation(oscalComponent, 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() { + devCmd.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 := 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 = 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 := 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/dev/print-resources_test.go b/src/cmd/dev/print_test.go similarity index 50% rename from src/cmd/dev/print-resources_test.go rename to src/cmd/dev/print_test.go index ff8f5413..d3ee788b 100644 --- a/src/cmd/dev/print-resources_test.go +++ b/src/cmd/dev/print_test.go @@ -12,10 +12,15 @@ import ( "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, "../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml") + oscalModel := testhelpers.OscalFromPath(t, assessmentPath) assessment := oscalModel.AssessmentResults require.NotNil(t, assessment) @@ -56,3 +61,44 @@ func TestPrintResources(t *testing.T) { }) } + +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 := dev.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 := dev.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 := dev.PrintValidation(component, assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "") + require.ErrorContains(t, err, "validation not found in component definition") + }) + +} diff --git a/src/test/e2e/cmd/dev_print_resources_test.go b/src/test/e2e/cmd/dev_print_resources_test.go deleted file mode 100644 index 6c736326..00000000 --- a/src/test/e2e/cmd/dev_print_resources_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package cmd_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/defenseunicorns/lula/src/cmd/dev" - "github.com/defenseunicorns/lula/src/pkg/message" -) - -func TestDevPrintResourcesCommand(t *testing.T) { - message.NoProgress = true - - test := func(t *testing.T, args ...string) error { - rootCmd := dev.PrintResourcesCommand() - - return runCmdTest(t, rootCmd, args...) - } - - testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { - rootCmd := dev.PrintResourcesCommand() - - return runCmdTestWithGolden(t, "dev/print-resources/", goldenFileName, rootCmd, args...) - } - - t.Run("Print Resources", func(t *testing.T) { - err := testAgainstGolden(t, "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, - "-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, - "-a", "../../unit/common/oscal/valid-assessment-results-with-resources.yaml", - "-u", "foo", - ) - require.ErrorContains(t, err, "error printing resources") - }) -} diff --git a/src/test/e2e/cmd/dev_print_validation_test.go b/src/test/e2e/cmd/dev_print_test.go similarity index 53% rename from src/test/e2e/cmd/dev_print_validation_test.go rename to src/test/e2e/cmd/dev_print_test.go index 6c47055f..e8a8151c 100644 --- a/src/test/e2e/cmd/dev_print_validation_test.go +++ b/src/test/e2e/cmd/dev_print_test.go @@ -9,23 +9,47 @@ import ( "github.com/defenseunicorns/lula/src/pkg/message" ) -func TestDevPrintValidationCommand(t *testing.T) { +func TestDevPrintResourcesCommand(t *testing.T) { message.NoProgress = true test := func(t *testing.T, args ...string) error { - rootCmd := dev.PrintValidationCommand() + rootCmd := dev.PrintCommand() return runCmdTest(t, rootCmd, args...) } testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { - rootCmd := dev.PrintValidationCommand() + rootCmd := dev.PrintCommand() - return runCmdTestWithGolden(t, "dev/print-validation/", goldenFileName, rootCmd, args...) + return runCmdTestWithGolden(t, "dev/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", + 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", @@ -34,7 +58,7 @@ func TestDevPrintValidationCommand(t *testing.T) { }) t.Run("Print Validation - invalid assessment oscal", func(t *testing.T) { - err := test(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", @@ -43,7 +67,7 @@ func TestDevPrintValidationCommand(t *testing.T) { }) t.Run("Print Validation - no uuid", func(t *testing.T) { - err := test(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", diff --git a/src/test/e2e/cmd/testdata/dev/print-resources/resources.golden b/src/test/e2e/cmd/testdata/dev/print/resources.golden similarity index 100% rename from src/test/e2e/cmd/testdata/dev/print-resources/resources.golden rename to src/test/e2e/cmd/testdata/dev/print/resources.golden diff --git a/src/test/e2e/cmd/testdata/dev/print-validation/validation.golden b/src/test/e2e/cmd/testdata/dev/print/validation.golden similarity index 100% rename from src/test/e2e/cmd/testdata/dev/print-validation/validation.golden rename to src/test/e2e/cmd/testdata/dev/print/validation.golden From 2b1bad8d8ebd08bce3659a8da3c8056f68df67b4 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Wed, 30 Oct 2024 10:56:40 -0400 Subject: [PATCH 11/14] docs: dev print docs --- docs/cli-commands/lula_dev.md | 3 +- docs/cli-commands/lula_dev_print-resources.md | 50 ---------------- .../cli-commands/lula_dev_print-validation.md | 48 --------------- docs/cli-commands/lula_dev_print.md | 58 +++++++++++++++++++ 4 files changed, 59 insertions(+), 100 deletions(-) delete mode 100644 docs/cli-commands/lula_dev_print-resources.md delete mode 100644 docs/cli-commands/lula_dev_print-validation.md create mode 100644 docs/cli-commands/lula_dev_print.md diff --git a/docs/cli-commands/lula_dev.md b/docs/cli-commands/lula_dev.md index 108703a4..23ae2b46 100644 --- a/docs/cli-commands/lula_dev.md +++ b/docs/cli-commands/lula_dev.md @@ -24,7 +24,6 @@ Collection of dev commands to make dev life easier * [lula](./lula.md) - Risk Management as Code * [lula dev get-resources](./lula_dev_get-resources.md) - Get Resources from a Lula Validation Manifest * [lula dev lint](./lula_dev_lint.md) - Lint validation files against schema -* [lula dev print-resources](./lula_dev_print-resources.md) - Print Resources from a Lula Validation evaluation -* [lula dev print-validation](./lula_dev_print-validation.md) - Print Lula Validation +* [lula dev print](./lula_dev_print.md) - Print Resources or Lula Validation from an Assessment Observation * [lula dev validate](./lula_dev_validate.md) - Run an individual Lula validation. diff --git a/docs/cli-commands/lula_dev_print-resources.md b/docs/cli-commands/lula_dev_print-resources.md deleted file mode 100644 index b938fed8..00000000 --- a/docs/cli-commands/lula_dev_print-resources.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -title: lula dev print-resources -description: Lula CLI command reference for lula dev print-resources. -type: docs ---- -## lula dev print-resources - -Print Resources from a Lula Validation evaluation - -### Synopsis - - -Print out the the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file. - - -``` -lula dev print-resources [flags] -``` - -### Examples - -``` - -To print resources from lula validation manifest: - lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid - -To print resources from lula validation manifest to output file: - lula dev print-resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json - -``` - -### Options - -``` - -a, --assessment string the path to an assessment-results file - -h, --help help for print-resources - -u, --observation-uuid string the observation uuid - -o, --output-file string the path to write the resources json -``` - -### 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 dev](./lula_dev.md) - Collection of dev commands to make dev life easier - diff --git a/docs/cli-commands/lula_dev_print-validation.md b/docs/cli-commands/lula_dev_print-validation.md deleted file mode 100644 index 419b007c..00000000 --- a/docs/cli-commands/lula_dev_print-validation.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: lula dev print-validation -description: Lula CLI command reference for lula dev print-validation. -type: docs ---- -## lula dev print-validation - -Print Lula Validation - -### Synopsis - - -Prints the Lula Validation from a specified observation. Assumes that the validation is in the back matter of the provided component definition. - - -``` -lula dev print-validation [flags] -``` - -### Examples - -``` - -To print a specific lula validation that generated a given observation: - lula dev 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-validation - -u, --observation-uuid string the observation uuid - -o, --output-file string the path to write the resources json -``` - -### 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 dev](./lula_dev.md) - Collection of dev commands to make dev life easier - diff --git a/docs/cli-commands/lula_dev_print.md b/docs/cli-commands/lula_dev_print.md new file mode 100644 index 00000000..53aa94ec --- /dev/null +++ b/docs/cli-commands/lula_dev_print.md @@ -0,0 +1,58 @@ +--- +title: lula dev print +description: Lula CLI command reference for lula dev print. +type: docs +--- +## lula dev print + +Print Resources or Lula Validation from an Assessment Observation + +### Synopsis + + +Print out data about an Observation. +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 dev print [flags] +``` + +### Examples + +``` + +To print resources from lula validation manifest: + lula dev print --resources --assessment /path/to/assessment.yaml --observation-uuid + +To print resources from lula validation manifest to output file: + lula dev 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 dev 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 dev](./lula_dev.md) - Collection of dev commands to make dev life easier + From 419d93bb7a4dc076b6f0308c28e93d2ba7db4e88 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 5 Nov 2024 09:53:54 -0500 Subject: [PATCH 12/14] feat(dev): updated dev print validation to work with non-composed compdef --- src/cmd/dev/print.go | 14 +- src/cmd/dev/print_test.go | 32 ++++- src/test/e2e/cmd/dev_print_test.go | 32 +++++ .../assessment-results.yaml | 132 ++++++++++++++++++ 4 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 src/test/e2e/scenarios/validation-composition/assessment-results.yaml diff --git a/src/cmd/dev/print.go b/src/cmd/dev/print.go index 2902a796..9c261fbf 100644 --- a/src/cmd/dev/print.go +++ b/src/cmd/dev/print.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "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" @@ -71,17 +72,18 @@ func PrintCommand() *cobra.Command { return fmt.Errorf("error printing resources: %v", err) } } else if validation { - componentData, err := common.ReadFileToBytes(component) + // Compose the component definition + composer, err := composition.New(composition.WithModelFromLocalPath(component)) if err != nil { - return fmt.Errorf("invalid component file: %v", err) + return fmt.Errorf("error creating new composer: %v", err) } - - oscalComponent, err := oscal.NewOscalComponentDefinition(componentData) + oscalModel, err := composer.ComposeFromPath(cmd.Context(), component) if err != nil { - return fmt.Errorf("error creating oscal component definition model: %v", err) + return fmt.Errorf("error composing model: %v", err) } - err = PrintValidation(oscalComponent, oscalAssessment, observationUuid, outputFile) + // Print the validation + err = PrintValidation(oscalModel.ComponentDefinition, oscalAssessment, observationUuid, outputFile) if err != nil { return fmt.Errorf("error printing validation: %v", err) } diff --git a/src/cmd/dev/print_test.go b/src/cmd/dev/print_test.go index d3ee788b..be24100f 100644 --- a/src/cmd/dev/print_test.go +++ b/src/cmd/dev/print_test.go @@ -13,8 +13,10 @@ import ( ) const ( - assessmentPath = "../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml" - componentPath = "../../test/unit/common/oscal/valid-multi-component-validations.yaml" + assessmentPath = "../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml" + componentPath = "../../test/unit/common/oscal/valid-multi-component-validations.yaml" + nonComposedComponentPath = "../../test/e2e/scenarios/validation-composition/component-definition.yaml" + nonComposedAssessmentPath = "../../test/e2e/scenarios/validation-composition/assessment-results.yaml" ) func TestPrintResources(t *testing.T) { @@ -91,6 +93,32 @@ func TestPrintValidation(t *testing.T) { require.Equal(t, validationData, printedData) }) + t.Run("Test print validation non-composed component", func(t *testing.T) { + nonComposedAssessmentModel := testhelpers.OscalFromPath(t, nonComposedAssessmentPath) + nonComposedAssessment := nonComposedAssessmentModel.AssessmentResults + require.NotNil(t, nonComposedAssessment) + + nonComposedComponentModel := testhelpers.OscalFromPath(t, nonComposedComponentPath) + nonComposedComponent := nonComposedComponentModel.ComponentDefinition + require.NotNil(t, nonComposedComponent) + + tmpFile := testhelpers.CreateTempFile(t, ".json") + defer os.Remove(tmpFile.Name()) + + err := dev.PrintValidation(nonComposedComponent, nonComposedAssessment, "d328a0a1-630b-40a2-9c9d-4818420a4126", 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/e2e/scenarios/validation-composition/validation.opa.yaml") + require.NoError(t, err) + + require.Equal(t, validationData, printedData) + }) + t.Run("Test print validation with no validation prop", func(t *testing.T) { err := dev.PrintValidation(component, assessment, "e1ca2968-8652-41be-a19f-c32bc0b3086c", "") require.ErrorContains(t, err, "no validation linked to observation") diff --git a/src/test/e2e/cmd/dev_print_test.go b/src/test/e2e/cmd/dev_print_test.go index e8a8151c..5b63049e 100644 --- a/src/test/e2e/cmd/dev_print_test.go +++ b/src/test/e2e/cmd/dev_print_test.go @@ -1,11 +1,14 @@ package cmd_test import ( + "os" + "path/filepath" "testing" "github.com/stretchr/testify/require" "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/message" ) @@ -57,6 +60,35 @@ func TestDevPrintResourcesCommand(t *testing.T) { 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", 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 From ba27bd962e7d1e390bd14eeb460154eb9c956d8c Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 5 Nov 2024 09:58:13 -0500 Subject: [PATCH 13/14] fix: print unit test --- src/cmd/dev/print_test.go | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/cmd/dev/print_test.go b/src/cmd/dev/print_test.go index be24100f..d3ee788b 100644 --- a/src/cmd/dev/print_test.go +++ b/src/cmd/dev/print_test.go @@ -13,10 +13,8 @@ import ( ) const ( - assessmentPath = "../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml" - componentPath = "../../test/unit/common/oscal/valid-multi-component-validations.yaml" - nonComposedComponentPath = "../../test/e2e/scenarios/validation-composition/component-definition.yaml" - nonComposedAssessmentPath = "../../test/e2e/scenarios/validation-composition/assessment-results.yaml" + 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) { @@ -93,32 +91,6 @@ func TestPrintValidation(t *testing.T) { require.Equal(t, validationData, printedData) }) - t.Run("Test print validation non-composed component", func(t *testing.T) { - nonComposedAssessmentModel := testhelpers.OscalFromPath(t, nonComposedAssessmentPath) - nonComposedAssessment := nonComposedAssessmentModel.AssessmentResults - require.NotNil(t, nonComposedAssessment) - - nonComposedComponentModel := testhelpers.OscalFromPath(t, nonComposedComponentPath) - nonComposedComponent := nonComposedComponentModel.ComponentDefinition - require.NotNil(t, nonComposedComponent) - - tmpFile := testhelpers.CreateTempFile(t, ".json") - defer os.Remove(tmpFile.Name()) - - err := dev.PrintValidation(nonComposedComponent, nonComposedAssessment, "d328a0a1-630b-40a2-9c9d-4818420a4126", 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/e2e/scenarios/validation-composition/validation.opa.yaml") - require.NoError(t, err) - - require.Equal(t, validationData, printedData) - }) - t.Run("Test print validation with no validation prop", func(t *testing.T) { err := dev.PrintValidation(component, assessment, "e1ca2968-8652-41be-a19f-c32bc0b3086c", "") require.ErrorContains(t, err, "no validation linked to observation") From f05c921db6f36013b6278670a975d4230ed13bb5 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 5 Nov 2024 11:46:12 -0500 Subject: [PATCH 14/14] feat(tools): added print cmds, fcn reorg --- docs/cli-commands/lula_dev.md | 1 - docs/cli-commands/lula_tools.md | 1 + ...{lula_dev_print.md => lula_tools_print.md} | 18 +++++----- src/cmd/common/common.go | 19 ++++++++++ src/cmd/dev/common.go | 36 ------------------- src/cmd/dev/common_test.go | 32 ----------------- src/cmd/dev/get-resources.go | 5 +-- src/cmd/{dev => tools}/print.go | 19 +++++----- src/cmd/{dev => tools}/print_test.go | 16 ++++----- src/pkg/common/oscal/assessment-results.go | 18 ++++++++++ .../common/oscal/assessment-results_test.go | 25 +++++++++++++ .../{dev => tools}/print/resources.golden | 0 .../{dev => tools}/print/validation.golden | 0 ...{dev_print_test.go => tools_print_test.go} | 8 ++--- 14 files changed, 97 insertions(+), 101 deletions(-) rename docs/cli-commands/{lula_dev_print.md => lula_tools_print.md} (65%) delete mode 100644 src/cmd/dev/common_test.go rename src/cmd/{dev => tools}/print.go (89%) rename src/cmd/{dev => tools}/print_test.go (78%) rename src/test/e2e/cmd/testdata/{dev => tools}/print/resources.golden (100%) rename src/test/e2e/cmd/testdata/{dev => tools}/print/validation.golden (100%) rename src/test/e2e/cmd/{dev_print_test.go => tools_print_test.go} (94%) diff --git a/docs/cli-commands/lula_dev.md b/docs/cli-commands/lula_dev.md index 23ae2b46..d3051357 100644 --- a/docs/cli-commands/lula_dev.md +++ b/docs/cli-commands/lula_dev.md @@ -24,6 +24,5 @@ Collection of dev commands to make dev life easier * [lula](./lula.md) - Risk Management as Code * [lula dev get-resources](./lula_dev_get-resources.md) - Get Resources from a Lula Validation Manifest * [lula dev lint](./lula_dev_lint.md) - Lint validation files against schema -* [lula dev print](./lula_dev_print.md) - Print Resources or Lula Validation from an Assessment Observation * [lula dev validate](./lula_dev_validate.md) - Run an individual Lula validation. 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_dev_print.md b/docs/cli-commands/lula_tools_print.md similarity index 65% rename from docs/cli-commands/lula_dev_print.md rename to docs/cli-commands/lula_tools_print.md index 53aa94ec..f2e4d328 100644 --- a/docs/cli-commands/lula_dev_print.md +++ b/docs/cli-commands/lula_tools_print.md @@ -1,22 +1,22 @@ --- -title: lula dev print -description: Lula CLI command reference for lula dev print. +title: lula tools print +description: Lula CLI command reference for lula tools print. type: docs --- -## lula dev print +## lula tools print Print Resources or Lula Validation from an Assessment Observation ### Synopsis -Print out data about an Observation. +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 dev print [flags] +lula tools print [flags] ``` ### Examples @@ -24,13 +24,13 @@ lula dev print [flags] ``` To print resources from lula validation manifest: - lula dev print --resources --assessment /path/to/assessment.yaml --observation-uuid + lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid To print resources from lula validation manifest to output file: - lula dev print --resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json + 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 dev print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid + lula tools print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid ``` @@ -54,5 +54,5 @@ To print the lula validation that generated a given observation: ### SEE ALSO -* [lula dev](./lula_dev.md) - Collection of dev commands to make dev life easier +* [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 32633ab1..a2b48020 100644 --- a/src/cmd/dev/common.go +++ b/src/cmd/dev/common.go @@ -4,11 +4,9 @@ import ( "context" "fmt" "io" - "os" "strings" "time" - oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/spf13/cobra" "sigs.k8s.io/yaml" @@ -107,37 +105,3 @@ func RunSingleValidation(ctx context.Context, validationBytes []byte, opts ...ty return lulaValidation, 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) -} - -// 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_test.go b/src/cmd/dev/common_test.go deleted file mode 100644 index 5995c349..00000000 --- a/src/cmd/dev/common_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package dev_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/defenseunicorns/lula/src/cmd/dev" - "github.com/defenseunicorns/lula/src/internal/testhelpers" -) - -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 := dev.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 := dev.GetObservationByUuid(assessment, "invalid-uuid") - assert.Nil(t, observation) - require.ErrorContains(t, err, "observation with uuid invalid-uuid not found") - }) - -} diff --git a/src/cmd/dev/get-resources.go b/src/cmd/dev/get-resources.go index e9e65d6b..e3de5580 100644 --- a/src/cmd/dev/get-resources.go +++ b/src/cmd/dev/get-resources.go @@ -4,10 +4,11 @@ import ( "context" "fmt" + "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{} @@ -52,7 +53,7 @@ var getResourcesCmd = &cobra.Command{ message.Fatalf(err, "error running dev get-resources: %v", err) } - err = writeResources(collection, getResourcesOpts.OutputFile) + err = common.WriteResources(collection, getResourcesOpts.OutputFile) if err != nil { message.Fatalf(err, "error writing resources: %v", err) } diff --git a/src/cmd/dev/print.go b/src/cmd/tools/print.go similarity index 89% rename from src/cmd/dev/print.go rename to src/cmd/tools/print.go index 9c261fbf..a6ceaae8 100644 --- a/src/cmd/dev/print.go +++ b/src/cmd/tools/print.go @@ -1,4 +1,4 @@ -package dev +package tools import ( "encoding/json" @@ -9,6 +9,7 @@ import ( 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" @@ -19,17 +20,17 @@ import ( var printHelp = ` To print resources from lula validation manifest: - lula dev print --resources --assessment /path/to/assessment.yaml --observation-uuid + lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid To print resources from lula validation manifest to output file: - lula dev print --resources --assessment /path/to/assessment.yaml --observation-uuid --output-file /path/to/output.json + 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 dev print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid + lula tools print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid ` var printCmdLong = ` -Print out data about an Observation. +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. ` @@ -118,7 +119,7 @@ func PrintCommand() *cobra.Command { } func init() { - devCmd.AddCommand(PrintCommand()) + toolsCmd.AddCommand(PrintCommand()) } func PrintResources(assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid, assessmentDir, outputFile string) error { @@ -126,7 +127,7 @@ func PrintResources(assessment *oscalTypes_1_1_2.AssessmentResults, observationU return fmt.Errorf("assessment is nil") } - observation, err := GetObservationByUuid(assessment, observationUuid) + observation, err := oscal.GetObservationByUuid(assessment, observationUuid) if err != nil { return err } @@ -160,7 +161,7 @@ func PrintResources(assessment *oscalTypes_1_1_2.AssessmentResults, observationU } // Write the resources to a file if found - err = writeResources(resource, outputFile) + err = cmdcommon.WriteResources(resource, outputFile) if err != nil { return err } @@ -178,7 +179,7 @@ func PrintValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment } // Get the observation - observation, err := GetObservationByUuid(assessment, observationUuid) + observation, err := oscal.GetObservationByUuid(assessment, observationUuid) if err != nil { return err } diff --git a/src/cmd/dev/print_test.go b/src/cmd/tools/print_test.go similarity index 78% rename from src/cmd/dev/print_test.go rename to src/cmd/tools/print_test.go index d3ee788b..9e50e338 100644 --- a/src/cmd/dev/print_test.go +++ b/src/cmd/tools/print_test.go @@ -1,4 +1,4 @@ -package dev_test +package tools_test import ( "encoding/json" @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/cmd/tools" "github.com/defenseunicorns/lula/src/internal/testhelpers" "github.com/defenseunicorns/lula/src/types" ) @@ -28,7 +28,7 @@ func TestPrintResources(t *testing.T) { tmpFile := testhelpers.CreateTempFile(t, ".json") defer os.Remove(tmpFile.Name()) - err := dev.PrintResources(assessment, "92cb3cad-bbcd-431a-aaa9-cd47275a3982", "../../test/unit/common/oscal", tmpFile.Name()) + err := tools.PrintResources(assessment, "92cb3cad-bbcd-431a-aaa9-cd47275a3982", "../../test/unit/common/oscal", tmpFile.Name()) require.NoError(t, err) // get printed resources @@ -51,12 +51,12 @@ func TestPrintResources(t *testing.T) { }) t.Run("Test print resources with invalid resources", func(t *testing.T) { - err := dev.PrintResources(assessment, "e1ca2968-8652-41be-a19f-c32bc0b3086c", "../../test/unit/common/oscal", "") + 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 := dev.PrintResources(assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "../../test/unit/common/oscal", "") + err := tools.PrintResources(assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "../../test/unit/common/oscal", "") require.ErrorContains(t, err, "observation does not contain a remote reference") }) @@ -77,7 +77,7 @@ func TestPrintValidation(t *testing.T) { tmpFile := testhelpers.CreateTempFile(t, ".json") defer os.Remove(tmpFile.Name()) - err := dev.PrintValidation(component, assessment, "92cb3cad-bbcd-431a-aaa9-cd47275a3982", tmpFile.Name()) + err := tools.PrintValidation(component, assessment, "92cb3cad-bbcd-431a-aaa9-cd47275a3982", tmpFile.Name()) require.NoError(t, err) // get printed data @@ -92,12 +92,12 @@ func TestPrintValidation(t *testing.T) { }) t.Run("Test print validation with no validation prop", func(t *testing.T) { - err := dev.PrintValidation(component, assessment, "e1ca2968-8652-41be-a19f-c32bc0b3086c", "") + 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 := dev.PrintValidation(component, assessment, "af060637-2899-4f26-ae9d-2c1bbbddc4b0", "") + 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/test/e2e/cmd/testdata/dev/print/resources.golden b/src/test/e2e/cmd/testdata/tools/print/resources.golden similarity index 100% rename from src/test/e2e/cmd/testdata/dev/print/resources.golden rename to src/test/e2e/cmd/testdata/tools/print/resources.golden diff --git a/src/test/e2e/cmd/testdata/dev/print/validation.golden b/src/test/e2e/cmd/testdata/tools/print/validation.golden similarity index 100% rename from src/test/e2e/cmd/testdata/dev/print/validation.golden rename to src/test/e2e/cmd/testdata/tools/print/validation.golden diff --git a/src/test/e2e/cmd/dev_print_test.go b/src/test/e2e/cmd/tools_print_test.go similarity index 94% rename from src/test/e2e/cmd/dev_print_test.go rename to src/test/e2e/cmd/tools_print_test.go index 5b63049e..34c97f35 100644 --- a/src/test/e2e/cmd/dev_print_test.go +++ b/src/test/e2e/cmd/tools_print_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" - "github.com/defenseunicorns/lula/src/cmd/dev" + "github.com/defenseunicorns/lula/src/cmd/tools" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/message" ) @@ -16,15 +16,15 @@ func TestDevPrintResourcesCommand(t *testing.T) { message.NoProgress = true test := func(t *testing.T, args ...string) error { - rootCmd := dev.PrintCommand() + rootCmd := tools.PrintCommand() return runCmdTest(t, rootCmd, args...) } testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { - rootCmd := dev.PrintCommand() + rootCmd := tools.PrintCommand() - return runCmdTestWithGolden(t, "dev/print/", goldenFileName, rootCmd, args...) + return runCmdTestWithGolden(t, "tools/print/", goldenFileName, rootCmd, args...) } t.Run("Print Resources", func(t *testing.T) {