From ac1e4ec7976a93e51c74256c8eec30397542f7aa Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Mon, 13 Jan 2025 12:23:16 -0500 Subject: [PATCH 1/8] feat: added OSCALModel impl for compdef --- src/cmd/generate/generate.go | 7 +- src/cmd/generate/system-security-plan.go | 2 +- src/internal/reporting/reporting.go | 7 +- src/pkg/common/oscal/complete-schema.go | 10 +++ src/pkg/common/oscal/component.go | 87 ++++++++++++++++++-- src/pkg/common/oscal/component_test.go | 15 ++-- src/pkg/common/oscal/profile.go | 19 ++--- src/pkg/common/oscal/system-security-plan.go | 73 ++++++++-------- src/test/e2e/outputs_test.go | 7 +- 9 files changed, 151 insertions(+), 76 deletions(-) diff --git a/src/cmd/generate/generate.go b/src/cmd/generate/generate.go index fc771413a..bb2af59c4 100644 --- a/src/cmd/generate/generate.go +++ b/src/cmd/generate/generate.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" "github.com/spf13/cobra" "github.com/defenseunicorns/lula/src/cmd/common" @@ -122,12 +121,8 @@ var generateComponentCmd = &cobra.Command{ message.Fatalf(err, "error creating component - %s\n", err.Error()) } - var model = oscalTypes.OscalModels{ - ComponentDefinition: comp, - } - // Write the component definition to file - err = oscal.WriteOscalModel(opts.OutputFile, &model) + err = oscal.WriteOscalModelNew(opts.OutputFile, comp) if err != nil { message.Fatalf(err, "error writing component to file") } diff --git a/src/cmd/generate/system-security-plan.go b/src/cmd/generate/system-security-plan.go index 3492111d6..ed5943941 100644 --- a/src/cmd/generate/system-security-plan.go +++ b/src/cmd/generate/system-security-plan.go @@ -55,7 +55,7 @@ func GenerateSSPCommand() *cobra.Command { if err != nil { return err } - if modelType != "profile" { + if modelType != oscal.OSCAL_PROFILE { return fmt.Errorf("profile must be a valid OSCAL profile") } diff --git a/src/internal/reporting/reporting.go b/src/internal/reporting/reporting.go index 49c4f0308..58ea86189 100644 --- a/src/internal/reporting/reporting.go +++ b/src/internal/reporting/reporting.go @@ -6,12 +6,13 @@ import ( "fmt" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" + "gopkg.in/yaml.v3" + "github.com/defenseunicorns/lula/src/cmd/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" - "gopkg.in/yaml.v3" ) type ReportData struct { @@ -69,11 +70,11 @@ func handleOSCALModel(oscalModel *oscalTypes.OscalModels, format string, compose } switch modelType { - case "catalog", "profile", "assessment-plan", "assessment-results", "system-security-plan", "poam": + case oscal.OSCAL_CATALOG, oscal.OSCAL_PROFILE, oscal.OSCAL_ASSESSMENT_PLAN, oscal.OSCAL_ASSESSMENT_RESULTS, oscal.OSCAL_SYSTEM_SECURITY_PLAN, oscal.OSCAL_POAM: // If the model type is not supported, stop the spinner with a warning return fmt.Errorf("reporting does not create reports for %s at this time", modelType) - case "component": + case oscal.OSCAL_COMPONENT: spinner.Updatef("Composing Component Definition") err := composer.ComposeComponentDefinitions(context.Background(), oscalModel.ComponentDefinition, "") if err != nil { diff --git a/src/pkg/common/oscal/complete-schema.go b/src/pkg/common/oscal/complete-schema.go index 18cc4c739..f70a3ca74 100644 --- a/src/pkg/common/oscal/complete-schema.go +++ b/src/pkg/common/oscal/complete-schema.go @@ -18,6 +18,16 @@ import ( "github.com/defenseunicorns/lula/src/pkg/message" ) +const ( + OSCAL_COMPONENT = "component" + OSCAL_ASSESSMENT_RESULTS = "assessment-results" + OSCAL_SYSTEM_SECURITY_PLAN = "system-security-plan" + OSCAL_PROFILE = "profile" + OSCAL_CATALOG = "catalog" + OSCAL_POAM = "poam" + OSCAL_ASSESSMENT_PLAN = "assessment-plan" +) + type OSCALModel interface { GetType() string GetCompleteModel() *oscalTypes.OscalModels diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index 11850ad83..8b043bcdc 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -2,6 +2,8 @@ package oscal import ( "fmt" + "os" + "path/filepath" "regexp" "sort" "strings" @@ -11,6 +13,7 @@ import ( oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" "sigs.k8s.io/yaml" + "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/message" ) @@ -30,6 +33,76 @@ type parameter struct { Select *selection } +type ComponentDefinition struct { + Model *oscalTypes.ComponentDefinition +} + +func NewComponentDefinition() *ComponentDefinition { + var compdef ComponentDefinition + compdef.Model = nil + return &compdef +} + +// Create a new ComponentDefinition from a byte array +func (c *ComponentDefinition) NewModel(data []byte) error { + model, err := NewOscalModel(data) + if err != nil { + return err + } + + c.Model = model.ComponentDefinition + + return nil +} + +// Return the type of the component definition +func (*ComponentDefinition) GetType() string { + return OSCAL_COMPONENT +} + +// Returns the complete OSCAL model with component definition +func (c *ComponentDefinition) GetCompleteModel() *oscalTypes.OscalModels { + return &oscalTypes.OscalModels{ + ComponentDefinition: c.Model, + } +} + +// MakeDeterministic ensures the relevant elements of the Component Definition are sorted deterministically +func (c *ComponentDefinition) MakeDeterministic() error { + if c.Model == nil { + return fmt.Errorf("cannot make nil model deterministic") + } + + MakeComponentDeterminstic(c.Model) + return nil +} + +// HandleExisting updates the existing Component Defintion if a file is provided +func (c *ComponentDefinition) HandleExisting(path string) error { + exists, err := common.CheckFileExists(path) + if err != nil { + return err + } + if exists { + path = filepath.Clean(path) + existingFileBytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("error reading file: %v", err) + } + compdef := NewComponentDefinition() + err = compdef.NewModel(existingFileBytes) + if err != nil { + return err + } + model, err := MergeComponentDefinitions(compdef.Model, c.Model) + if err != nil { + return err + } + c.Model = model + } + return nil +} + // NewOscalComponentDefinition consumes a byte array and returns a new single OscalComponentDefinitionModel object // Standard use is to read a file from the filesystem and pass the []byte to this function func NewOscalComponentDefinition(data []byte) (componentDefinition *oscalTypes.ComponentDefinition, err error) { @@ -251,28 +324,28 @@ func mergeLinks(orig []oscalTypes.Link, latest []oscalTypes.Link) *[]oscalTypes. } // Creates a component-definition from a catalog and identified (or all) controls. Allows for specification of what the content of the remarks section should contain. -func ComponentFromCatalog(command string, source string, catalog *oscalTypes.Catalog, componentTitle string, targetControls []string, targetRemarks []string, framework string) (*oscalTypes.ComponentDefinition, error) { +func ComponentFromCatalog(command string, source string, catalog *oscalTypes.Catalog, componentTitle string, targetControls []string, targetRemarks []string, framework string) (*ComponentDefinition, error) { // store all of the implemented requirements implementedRequirements := make([]oscalTypes.ImplementedRequirementControlImplementation, 0) var componentDefinition = &oscalTypes.ComponentDefinition{} if len(targetControls) == 0 { - return componentDefinition, fmt.Errorf("no controls identified for generation") + return nil, fmt.Errorf("no controls identified for generation") } controlsToImplement, err := ResolveCatalogControls(catalog, targetControls, nil) if err != nil { - return componentDefinition, err + return nil, err } if len(controlsToImplement) == 0 { - return componentDefinition, fmt.Errorf("no controls were identified in the catalog from the requirements list: %v\n", targetControls) + return nil, fmt.Errorf("no controls were identified in the catalog from the requirements list: %v\n", targetControls) } for _, control := range controlsToImplement { ir, err := ControlToImplementedRequirement(&control, targetRemarks) if err != nil { - return componentDefinition, fmt.Errorf("error creating implemented requirement: %v", err) + return nil, fmt.Errorf("error creating implemented requirement: %v", err) } implementedRequirements = append(implementedRequirements, ir) } @@ -330,7 +403,9 @@ func ComponentFromCatalog(command string, source string, catalog *oscalTypes.Cat Version: "0.0.1", } - return componentDefinition, nil + return &ComponentDefinition{ + Model: componentDefinition, + }, nil } diff --git a/src/pkg/common/oscal/component_test.go b/src/pkg/common/oscal/component_test.go index 44364c944..f4ec06304 100644 --- a/src/pkg/common/oscal/component_test.go +++ b/src/pkg/common/oscal/component_test.go @@ -112,13 +112,14 @@ func TestNewOscalComponentDefinition(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := oscal.NewOscalComponentDefinition(tt.data) + model := oscal.NewComponentDefinition() + err := model.NewModel(tt.data) if (err != nil) != tt.wantErr { t.Errorf("NewOscalComponentDefinition() error = %v, wantErr %v", err, tt.wantErr) return } - if !reflect.DeepEqual(got, tt.want) && !tt.wantErr { - t.Errorf("NewOscalComponentDefinition() got = %v, want %v", got, tt.want) + if !reflect.DeepEqual(model.Model, tt.want) && !tt.wantErr { + t.Errorf("NewOscalComponentDefinition() got = %v, want %v", model.Model, tt.want) } }) } @@ -198,7 +199,7 @@ func TestComponentFromCatalog(t *testing.T) { } // DeepEqual will be difficult with time/uuid generation - component := (*got.Components)[0] + component := (*got.Model.Components)[0] if component.Title != tt.title { t.Errorf("ComponentFromCatalog() title = %v, want %v", component.Title, tt.title) } @@ -306,7 +307,11 @@ func TestMergeComponentDefinitions(t *testing.T) { generated, _ := oscal.ComponentFromCatalog("Mock Command", tt.source, catalog, tt.title, tt.requirements, tt.remarks, "impact") - merged, err := oscal.MergeComponentDefinitions(validComponent, generated) + if generated == nil { + t.Errorf("ComponentFromCatalog() generated should not be nil") + } + + merged, err := oscal.MergeComponentDefinitions(validComponent, generated.Model) if (err != nil) != tt.wantErr { t.Errorf("MergeComponentDefinitions() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/src/pkg/common/oscal/profile.go b/src/pkg/common/oscal/profile.go index fa2f07e15..1bb2ec8be 100644 --- a/src/pkg/common/oscal/profile.go +++ b/src/pkg/common/oscal/profile.go @@ -10,7 +10,6 @@ import ( "github.com/defenseunicorns/go-oscal/src/pkg/uuid" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" - "gopkg.in/yaml.v3" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/network" @@ -27,7 +26,7 @@ func NewProfile() *Profile { } func (p *Profile) GetType() string { - return "profile" + return OSCAL_PROFILE } func (p *Profile) GetCompleteModel() *oscalTypes.OscalModels { @@ -115,20 +114,12 @@ func (p *Profile) HandleExisting(path string) error { // Create a new profile model func (p *Profile) NewModel(data []byte) error { - - var oscalModels oscalTypes.OscalModels - - err := multiModelValidate(data) - if err != nil { - return err - } - - err = yaml.Unmarshal(data, &oscalModels) + model, err := NewOscalModel(data) if err != nil { return err } - p.Model = oscalModels.Profile + p.Model = model.Profile return nil } @@ -365,9 +356,9 @@ func controlsFromImport(importItem oscalTypes.Import, rootDir string) (controlMa return controlMap, err } switch modelType { - case "profile": + case OSCAL_PROFILE: return ResolveProfileControls(oscalModel.Profile, importItem.Href, rootDir, include, exclude) - case "catalog": + case OSCAL_CATALOG: catalogControls, err := ResolveCatalogControls(oscalModel.Catalog, include, exclude) if err != nil { return nil, err diff --git a/src/pkg/common/oscal/system-security-plan.go b/src/pkg/common/oscal/system-security-plan.go index 05c93b2da..03984e507 100644 --- a/src/pkg/common/oscal/system-security-plan.go +++ b/src/pkg/common/oscal/system-security-plan.go @@ -10,7 +10,6 @@ import ( "github.com/defenseunicorns/go-oscal/src/pkg/uuid" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" - "sigs.k8s.io/yaml" "github.com/defenseunicorns/lula/src/pkg/common" ) @@ -26,7 +25,7 @@ func NewSystemSecurityPlan() *SystemSecurityPlan { } func (ssp *SystemSecurityPlan) GetType() string { - return "system-security-plan" + return OSCAL_SYSTEM_SECURITY_PLAN } func (ssp *SystemSecurityPlan) GetCompleteModel() *oscalTypes.OscalModels { @@ -39,33 +38,34 @@ func (ssp *SystemSecurityPlan) GetCompleteModel() *oscalTypes.OscalModels { func (ssp *SystemSecurityPlan) MakeDeterministic() error { if ssp.Model == nil { return fmt.Errorf("cannot make nil model deterministic") - } else { - // Sort the SystemImplementation.Components by title - slices.SortStableFunc(ssp.Model.SystemImplementation.Components, func(a, b oscalTypes.SystemComponent) int { - return strings.Compare(a.Title, b.Title) - }) - - // Sort the ControlImplementation.ImplementedRequirements by control-id - slices.SortStableFunc(ssp.Model.ControlImplementation.ImplementedRequirements, func(a, b oscalTypes.ImplementedRequirement) int { - return CompareControlsInt(a.ControlId, b.ControlId) - }) - - // Sort the ControlImplementation.ImplementedRequirements.ByComponent by title - for _, implementedRequirement := range ssp.Model.ControlImplementation.ImplementedRequirements { - if implementedRequirement.ByComponents != nil { - slices.SortStableFunc(*implementedRequirement.ByComponents, func(a, b oscalTypes.ByComponent) int { - return strings.Compare(a.ComponentUuid, b.ComponentUuid) - }) - } - } + } - // sort backmatter - if ssp.Model.BackMatter != nil { - backmatter := *ssp.Model.BackMatter - sortBackMatter(&backmatter) - ssp.Model.BackMatter = &backmatter + // Sort the SystemImplementation.Components by title + slices.SortStableFunc(ssp.Model.SystemImplementation.Components, func(a, b oscalTypes.SystemComponent) int { + return strings.Compare(a.Title, b.Title) + }) + + // Sort the ControlImplementation.ImplementedRequirements by control-id + slices.SortStableFunc(ssp.Model.ControlImplementation.ImplementedRequirements, func(a, b oscalTypes.ImplementedRequirement) int { + return CompareControlsInt(a.ControlId, b.ControlId) + }) + + // Sort the ControlImplementation.ImplementedRequirements.ByComponent by title + for _, implementedRequirement := range ssp.Model.ControlImplementation.ImplementedRequirements { + if implementedRequirement.ByComponents != nil { + slices.SortStableFunc(*implementedRequirement.ByComponents, func(a, b oscalTypes.ByComponent) int { + return strings.Compare(a.ComponentUuid, b.ComponentUuid) + }) } } + + // sort backmatter + if ssp.Model.BackMatter != nil { + backmatter := *ssp.Model.BackMatter + sortBackMatter(&backmatter) + ssp.Model.BackMatter = &backmatter + } + return nil } @@ -81,12 +81,12 @@ func (ssp *SystemSecurityPlan) HandleExisting(path string) error { if err != nil { return fmt.Errorf("error reading file: %v", err) } - ssp := NewSystemSecurityPlan() - err = ssp.NewModel(existingFileBytes) + newSsp := NewSystemSecurityPlan() + err = newSsp.NewModel(existingFileBytes) if err != nil { return err } - model, err := MergeSystemSecurityPlanModels(ssp.Model, ssp.Model) + model, err := MergeSystemSecurityPlanModels(ssp.Model, newSsp.Model) if err != nil { return err } @@ -97,19 +97,12 @@ func (ssp *SystemSecurityPlan) HandleExisting(path string) error { // NewModel updates the SSP model with the provided data func (ssp *SystemSecurityPlan) NewModel(data []byte) error { - var oscalModels oscalTypes.OscalModels - - err := multiModelValidate(data) - if err != nil { - return err - } - - err = yaml.Unmarshal(data, &oscalModels) + model, err := NewOscalModel(data) if err != nil { return err } - ssp.Model = oscalModels.SystemSecurityPlan + ssp.Model = model.SystemSecurityPlan return nil } @@ -425,10 +418,10 @@ func RemapSourceToUUID[V any](inMap map[string]V) map[string]V { } switch modelType { - case "profile": + case OSCAL_PROFILE: profile := oscalModel.Profile outMap[profile.UUID] = v - case "catalog": + case OSCAL_CATALOG: catalog := oscalModel.Catalog outMap[catalog.UUID] = v } diff --git a/src/test/e2e/outputs_test.go b/src/test/e2e/outputs_test.go index 87f87071f..c06cfaae6 100644 --- a/src/test/e2e/outputs_test.go +++ b/src/test/e2e/outputs_test.go @@ -47,11 +47,16 @@ func TestOutputs(t *testing.T) { t.Fatal(err) } - compDef, err := oscal.NewOscalComponentDefinition(data) + model, err := oscal.NewOscalModel(data) if err != nil { t.Fatal(err) } + compDef := model.ComponentDefinition + if compDef == nil { + t.Fatal("Expected non-nil component definition") + } + if compDef.Components == nil { t.Fatal("Expected non-nil components") } From 9005d785b74a003bb268015e3855f03ae235d780 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Mon, 13 Jan 2025 13:44:43 -0500 Subject: [PATCH 2/8] fix: err handling --- src/pkg/common/oscal/component_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pkg/common/oscal/component_test.go b/src/pkg/common/oscal/component_test.go index f4ec06304..c9ee04796 100644 --- a/src/pkg/common/oscal/component_test.go +++ b/src/pkg/common/oscal/component_test.go @@ -308,6 +308,9 @@ func TestMergeComponentDefinitions(t *testing.T) { generated, _ := oscal.ComponentFromCatalog("Mock Command", tt.source, catalog, tt.title, tt.requirements, tt.remarks, "impact") if generated == nil { + if tt.wantErr { + return + } t.Errorf("ComponentFromCatalog() generated should not be nil") } From 98bbb25115152b00b7cb6f672ddc2aae7e3e3767 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 14 Jan 2025 09:09:47 -0500 Subject: [PATCH 3/8] test: updated component and ssp tests --- src/pkg/common/oscal/component.go | 20 ++-- src/pkg/common/oscal/component_test.go | 36 +++++++ .../common/oscal/system-security-plan_test.go | 35 +++++++ .../common/oscal/valid-generated-ssp.yaml | 94 +++++++++++++++++++ .../common/oscal/valid-ssp-no-components.yaml | 83 ++++++++++++++++ 5 files changed, 258 insertions(+), 10 deletions(-) create mode 100644 src/test/unit/common/oscal/valid-generated-ssp.yaml create mode 100644 src/test/unit/common/oscal/valid-ssp-no-components.yaml diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index 8b043bcdc..9dc6ae3a8 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -38,9 +38,9 @@ type ComponentDefinition struct { } func NewComponentDefinition() *ComponentDefinition { - var compdef ComponentDefinition - compdef.Model = nil - return &compdef + var compDef ComponentDefinition + compDef.Model = nil + return &compDef } // Create a new ComponentDefinition from a byte array @@ -89,12 +89,12 @@ func (c *ComponentDefinition) HandleExisting(path string) error { if err != nil { return fmt.Errorf("error reading file: %v", err) } - compdef := NewComponentDefinition() - err = compdef.NewModel(existingFileBytes) + compDef := NewComponentDefinition() + err = compDef.NewModel(existingFileBytes) if err != nil { return err } - model, err := MergeComponentDefinitions(compdef.Model, c.Model) + model, err := MergeComponentDefinitions(compDef.Model, c.Model) if err != nil { return err } @@ -122,12 +122,12 @@ func NewOscalComponentDefinition(data []byte) (componentDefinition *oscalTypes.C } // MergeVariadicComponentDefinition merges multiple variadic component definitions into a single component definition -func MergeVariadicComponentDefinition(compdefs ...*oscalTypes.ComponentDefinition) (mergedCompDef *oscalTypes.ComponentDefinition, err error) { - for _, compdef := range compdefs { +func MergeVariadicComponentDefinition(compDefs ...*oscalTypes.ComponentDefinition) (mergedCompDef *oscalTypes.ComponentDefinition, err error) { + for _, compDef := range compDefs { if mergedCompDef == nil { - mergedCompDef = compdef + mergedCompDef = compDef } else { - mergedCompDef, err = MergeComponentDefinitions(mergedCompDef, compdef) + mergedCompDef, err = MergeComponentDefinitions(mergedCompDef, compDef) if err != nil { return nil, err } diff --git a/src/pkg/common/oscal/component_test.go b/src/pkg/common/oscal/component_test.go index c9ee04796..2e2e82c11 100644 --- a/src/pkg/common/oscal/component_test.go +++ b/src/pkg/common/oscal/component_test.go @@ -2,17 +2,20 @@ package oscal_test import ( "os" + "path/filepath" "reflect" "strings" "testing" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" + "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" "github.com/defenseunicorns/lula/src/pkg/common/oscal" ) const validComponentPath = "../../../test/unit/common/oscal/valid-component.yaml" +const validGeneratedComponentPath = "../../../test/unit/common/oscal/valid-generated-component.yaml" const catalogPath = "../../../test/unit/common/oscal/catalog.yaml" // Helper function to load test data @@ -572,3 +575,36 @@ func TestFilterControlImplementations(t *testing.T) { }) } } + +func TestHandleExistingComponent(t *testing.T) { + validComponentBytes := loadTestData(t, validComponentPath) + + var validComponent oscalTypes.OscalCompleteSchema + err := yaml.Unmarshal(validComponentBytes, &validComponent) + require.NoError(t, err) + + t.Run("Handle Existing with no existing data", func(t *testing.T) { + component := oscal.ComponentDefinition{} + component.NewModel(validComponentBytes) + + tmpDir := t.TempDir() + tmpFilePath := filepath.Join(tmpDir, "component.yaml") + + err := component.HandleExisting(tmpFilePath) + require.NoError(t, err) + + // Check length of components are the same + require.Equal(t, len(*validComponent.ComponentDefinition.Components), len(*component.Model.Components)) + }) + + t.Run("Handle Existing with existing data", func(t *testing.T) { + component := oscal.ComponentDefinition{} + component.NewModel(validComponentBytes) + + err := component.HandleExisting(validGeneratedComponentPath) + require.NoError(t, err) + + // Check length of components is 2 + require.Equal(t, 2, len(*component.Model.Components)) + }) +} diff --git a/src/pkg/common/oscal/system-security-plan_test.go b/src/pkg/common/oscal/system-security-plan_test.go index 9797ef178..91efdda16 100644 --- a/src/pkg/common/oscal/system-security-plan_test.go +++ b/src/pkg/common/oscal/system-security-plan_test.go @@ -21,6 +21,8 @@ var ( validProfileRemoteRev4 = "../../../test/unit/common/oscal/valid-profile-remote-rev4.yaml" validProfileNoControls = "../../../test/unit/common/oscal/valid-profile-test-excludes.yaml" validSSP = "../../../test/unit/common/oscal/valid-ssp.yaml" + validSSPNoComponents = "../../../test/unit/common/oscal/valid-ssp-no-components.yaml" + validGeneratedSSP = "../../../test/unit/common/oscal/valid-generated-ssp.yaml" ) func getComponentDefinition(t *testing.T, path string) *oscalTypes.ComponentDefinition { @@ -314,3 +316,36 @@ func TestMergeSystemSecurityPlanModels(t *testing.T) { require.Error(t, err) }) } + +func TestHandleExistingSSP(t *testing.T) { + validSSPBytes := loadTestData(t, validGeneratedSSP) + + var validSSP oscalTypes.OscalCompleteSchema + err := yaml.Unmarshal(validSSPBytes, &validSSP) + require.NoError(t, err) + + t.Run("Handle Existing with no existing data", func(t *testing.T) { + ssp := oscal.SystemSecurityPlan{} + ssp.NewModel(validSSPBytes) + + tmpDir := t.TempDir() + tmpFilePath := filepath.Join(tmpDir, "ssp.yaml") + + err := ssp.HandleExisting(tmpFilePath) + require.NoError(t, err) + + // Check length of components are the same + require.Equal(t, len(validSSP.SystemSecurityPlan.SystemImplementation.Components), len(ssp.Model.SystemImplementation.Components)) + }) + + t.Run("Handle Existing with existing data", func(t *testing.T) { + ssp := oscal.SystemSecurityPlan{} + ssp.NewModel(validSSPBytes) + + err := ssp.HandleExisting(validSSPNoComponents) + require.NoError(t, err) + + // Check length of components is 2 + require.Equal(t, 2, len(ssp.Model.SystemImplementation.Components)) + }) +} diff --git a/src/test/unit/common/oscal/valid-generated-ssp.yaml b/src/test/unit/common/oscal/valid-generated-ssp.yaml new file mode 100644 index 000000000..cde7f3bb8 --- /dev/null +++ b/src/test/unit/common/oscal/valid-generated-ssp.yaml @@ -0,0 +1,94 @@ +system-security-plan: + control-implementation: + description: "" + implemented-requirements: + - by-components: + - component-uuid: 7c02500a-6e33-44e0-82ee-fba0f5ea0cae + description: + uuid: 3c160e95-c390-4468-89cb-2c788da4a6aa + control-id: ac-1 + remarks: | + STATEMENT: + The organization:a. Develops, documents, and disseminates to [Assignment: organization-defined organization-defined personnel or roles]: + 1. An access control policy that addresses purpose, scope, roles, responsibilities, management commitment, coordination among organizational entities, and compliance; and + 2. Procedures to facilitate the implementation of the access control policy and associated access controls; and + b. Reviews and updates the current: + 1. Access control policy [Assignment: organization-defined organization-defined frequency]; and + 2. Access control procedures [Assignment: organization-defined organization-defined frequency]. + uuid: f15996c9-27a1-4375-b0ea-7f6b6643fa91 + - by-components: + - component-uuid: 7c02500a-6e33-44e0-82ee-fba0f5ea0cae + description: + uuid: 5a6f9225-d807-43c4-b8ab-9d097829d239 + control-id: ac-2 + remarks: | + STATEMENT: + The organization:a. Identifies and selects the following types of information system accounts to support organizational missions/business functions: [Assignment: organization-defined organization-defined information system account types]; + b. Assigns account managers for information system accounts; + c. Establishes conditions for group and role membership; + d. Specifies authorized users of the information system, group and role membership, and access authorizations (i.e., privileges) and other attributes (as required) for each account; + e. Requires approvals by [Assignment: organization-defined organization-defined personnel or roles] for requests to create information system accounts; + f. Creates, enables, modifies, disables, and removes information system accounts in accordance with [Assignment: organization-defined organization-defined procedures or conditions]; + g. Monitors the use of information system accounts; + h. Notifies account managers: + 1. When accounts are no longer required; + 2. When users are terminated or transferred; and + 3. When individual information system usage or need-to-know changes; + i. Authorizes access to the information system based on: + 1. A valid access authorization; + 2. Intended system usage; and + 3. Other attributes as required by the organization or associated missions/business functions; + j. Reviews accounts for compliance with account management requirements [Assignment: organization-defined organization-defined frequency]; and + k. Establishes a process for reissuing shared/group account credentials (if deployed) when individuals are removed from the group. + uuid: 71d2b445-bf65-4d11-ab01-a8160c90f08f + - by-components: + - component-uuid: 7c02500a-6e33-44e0-82ee-fba0f5ea0cae + description: + uuid: bb5b0e87-c2bd-47b1-9c6c-af52185d6b45 + control-id: ac-3 + remarks: |- + STATEMENT: + The information system enforces approved authorizations for logical access to information and system resources in accordance with applicable access control policies. + uuid: 45537fe3-b1eb-4d0c-bc7c-c8d021ff805a + import-profile: + href: ./src/test/unit/common/oscal/valid-profile-remote-rev4.yaml + metadata: + last-modified: 2025-01-14T09:03:07.744327-05:00 + oscal-version: 1.1.3 + props: + - name: generation + ns: https://docs.lula.dev/oscal/ns + value: lula generate system-security-plan --profile ./src/test/unit/common/oscal/valid-profile-remote-rev4.yaml --remarks statement --components ./src/test/unit/common/oscal/valid-multi-component.yaml + published: 2025-01-14T09:03:07.744327-05:00 + remarks: System Security Plan generated from Lula + title: System Security Plan + version: 0.0.1 + system-characteristics: + authorization-boundary: + description: "" + description: "" + status: + remarks: 'TODO: Validate state and remove this remark' + state: operational + system-ids: + - id: generated-system + system-information: + information-types: + - description: 'TODO: Update information types' + title: Generated System Information + uuid: 412eaf08-2daf-40b1-86bd-1c03e6721db1 + system-name: Generated System + system-implementation: + components: + - description: Component Description + status: + remarks: 'TODO: Validate state and remove this remark' + state: operational + title: Component A + type: software + uuid: 7c02500a-6e33-44e0-82ee-fba0f5ea0cae + users: + - remarks: 'TODO: Update generated user' + title: Generated User + uuid: 96a9343e-5230-4005-a541-9d53b352ef8c + uuid: 7d82ee4c-b206-4973-91ad-7cb05c62b2d0 diff --git a/src/test/unit/common/oscal/valid-ssp-no-components.yaml b/src/test/unit/common/oscal/valid-ssp-no-components.yaml new file mode 100644 index 000000000..00e1edea3 --- /dev/null +++ b/src/test/unit/common/oscal/valid-ssp-no-components.yaml @@ -0,0 +1,83 @@ +system-security-plan: + control-implementation: + description: "" + implemented-requirements: + - control-id: ac-1 + remarks: | + STATEMENT: + The organization:a. Develops, documents, and disseminates to [Assignment: organization-defined organization-defined personnel or roles]: + 1. An access control policy that addresses purpose, scope, roles, responsibilities, management commitment, coordination among organizational entities, and compliance; and + 2. Procedures to facilitate the implementation of the access control policy and associated access controls; and + b. Reviews and updates the current: + 1. Access control policy [Assignment: organization-defined organization-defined frequency]; and + 2. Access control procedures [Assignment: organization-defined organization-defined frequency]. + uuid: bb9c282d-1302-4931-b6a5-ef73bea10c19 + - control-id: ac-2 + remarks: | + STATEMENT: + The organization:a. Identifies and selects the following types of information system accounts to support organizational missions/business functions: [Assignment: organization-defined organization-defined information system account types]; + b. Assigns account managers for information system accounts; + c. Establishes conditions for group and role membership; + d. Specifies authorized users of the information system, group and role membership, and access authorizations (i.e., privileges) and other attributes (as required) for each account; + e. Requires approvals by [Assignment: organization-defined organization-defined personnel or roles] for requests to create information system accounts; + f. Creates, enables, modifies, disables, and removes information system accounts in accordance with [Assignment: organization-defined organization-defined procedures or conditions]; + g. Monitors the use of information system accounts; + h. Notifies account managers: + 1. When accounts are no longer required; + 2. When users are terminated or transferred; and + 3. When individual information system usage or need-to-know changes; + i. Authorizes access to the information system based on: + 1. A valid access authorization; + 2. Intended system usage; and + 3. Other attributes as required by the organization or associated missions/business functions; + j. Reviews accounts for compliance with account management requirements [Assignment: organization-defined organization-defined frequency]; and + k. Establishes a process for reissuing shared/group account credentials (if deployed) when individuals are removed from the group. + uuid: fc0e50eb-625e-44d2-b641-a0353d343522 + - control-id: ac-3 + remarks: |- + STATEMENT: + The information system enforces approved authorizations for logical access to information and system resources in accordance with applicable access control policies. + uuid: f32f0aa7-635a-4d74-a0de-dfdd3d4a87b5 + import-profile: + href: ./src/test/unit/common/oscal/valid-profile-remote-rev4.yaml + metadata: + last-modified: 2025-01-14T09:08:04.550816-05:00 + oscal-version: 1.1.3 + props: + - name: generation + ns: https://docs.lula.dev/oscal/ns + value: lula generate system-security-plan --profile ./src/test/unit/common/oscal/valid-profile-remote-rev4.yaml --remarks statement + published: 2025-01-14T09:08:04.550816-05:00 + remarks: System Security Plan generated from Lula + title: System Security Plan + version: 0.0.1 + system-characteristics: + authorization-boundary: + description: "" + description: "" + status: + remarks: 'TODO: Validate state and remove this remark' + state: operational + system-ids: + - id: generated-system + system-information: + information-types: + - description: 'TODO: Update information types' + title: Generated System Information + uuid: acf53bb3-270c-4f82-9027-78a57c0f3c39 + system-name: Generated System + system-implementation: + components: + - description: "" + remarks: 'TODO: Update generated component' + status: + remarks: 'TODO: Validate state and remove this remark' + state: operational + title: Generated Component + type: software + uuid: b7fa6f6d-8016-4d61-b161-332a7cd27d36 + users: + - remarks: 'TODO: Update generated user' + title: Generated User + uuid: 63659a2b-ae9f-42a3-91ee-2b23a1717971 + uuid: c04a5bf1-5b02-4120-aa02-b5895e515311 From 1dcd78f71dc4198aee4eb213bedd4fcdeb8f39d3 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 14 Jan 2025 09:49:04 -0500 Subject: [PATCH 4/8] fix: removed NewOscalComponentDefn --- src/pkg/common/oscal/component.go | 19 --------------- src/pkg/common/oscal/component_test.go | 33 ++++++++++++++++---------- 2 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index 9dc6ae3a8..88258acd5 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -11,7 +11,6 @@ import ( "github.com/defenseunicorns/go-oscal/src/pkg/uuid" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" - "sigs.k8s.io/yaml" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/message" @@ -103,24 +102,6 @@ func (c *ComponentDefinition) HandleExisting(path string) error { return nil } -// NewOscalComponentDefinition consumes a byte array and returns a new single OscalComponentDefinitionModel object -// Standard use is to read a file from the filesystem and pass the []byte to this function -func NewOscalComponentDefinition(data []byte) (componentDefinition *oscalTypes.ComponentDefinition, err error) { - var oscalModels oscalTypes.OscalModels - - // validate the data - err = multiModelValidate(data) - if err != nil { - return componentDefinition, err - } - - err = yaml.Unmarshal(data, &oscalModels) - if err != nil { - return componentDefinition, err - } - return oscalModels.ComponentDefinition, nil -} - // MergeVariadicComponentDefinition merges multiple variadic component definitions into a single component definition func MergeVariadicComponentDefinition(compDefs ...*oscalTypes.ComponentDefinition) (mergedCompDef *oscalTypes.ComponentDefinition, err error) { for _, compDef := range compDefs { diff --git a/src/pkg/common/oscal/component_test.go b/src/pkg/common/oscal/component_test.go index 2e2e82c11..734048415 100644 --- a/src/pkg/common/oscal/component_test.go +++ b/src/pkg/common/oscal/component_test.go @@ -69,7 +69,7 @@ func TestBackMatterToMap(t *testing.T) { } } -func TestNewOscalComponentDefinition(t *testing.T) { +func TestNewComponentDefinition(t *testing.T) { validBytes := loadTestData(t, validComponentPath) var validWantSchema oscalTypes.OscalCompleteSchema @@ -118,11 +118,11 @@ func TestNewOscalComponentDefinition(t *testing.T) { model := oscal.NewComponentDefinition() err := model.NewModel(tt.data) if (err != nil) != tt.wantErr { - t.Errorf("NewOscalComponentDefinition() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("NewComponentDefinition() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(model.Model, tt.want) && !tt.wantErr { - t.Errorf("NewOscalComponentDefinition() got = %v, want %v", model.Model, tt.want) + t.Errorf("NewComponentDefinition() got = %v, want %v", model.Model, tt.want) } }) } @@ -298,7 +298,12 @@ func TestMergeComponentDefinitions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - validComponent, _ := oscal.NewOscalComponentDefinition(validBytes) + component := oscal.ComponentDefinition{} + err := component.NewModel(validBytes) + require.NoError(t, err) + + validComponent := component.Model + require.NotNil(t, validComponent) // Get the implemented requirements from existing for comparison existingComponent := (*validComponent.Components)[0] @@ -514,12 +519,14 @@ func TestControlImplementationsToRequirementsMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + component := oscal.ComponentDefinition{} + data := loadTestData(t, tt.filepath) - compdef, err := oscal.NewOscalComponentDefinition(data) + err := component.NewModel(data) + require.NoError(t, err) - if err != nil { - t.Errorf("Expected NewOscalComponentDefinition to execute") - } + compdef := component.Model + require.NotNil(t, compdef) controlMap := oscal.FilterControlImplementations(compdef) var count int @@ -558,12 +565,14 @@ func TestFilterControlImplementations(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + component := oscal.ComponentDefinition{} + data := loadTestData(t, tt.filepath) - compdef, err := oscal.NewOscalComponentDefinition(data) + err := component.NewModel(data) + require.NoError(t, err) - if err != nil { - t.Errorf("Expected NewOscalComponentDefinition to execute") - } + compdef := component.Model + require.NotNil(t, compdef) controlMap := oscal.FilterControlImplementations(compdef) // Now validate the existence of items in the controlMap From 5f6be3f8ce66dac64d22850e21b26be6414ff033 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 14 Jan 2025 13:11:54 -0500 Subject: [PATCH 5/8] feat: added oscalmodel for assessment results --- src/cmd/evaluate/evaluate.go | 23 +++-- src/cmd/tools/print.go | 7 +- src/cmd/validate/validate.go | 7 +- src/internal/tui/component/validate.go | 8 +- src/pkg/common/oscal/assessment-results.go | 86 +++++++++++++++---- .../common/oscal/assessment-results_test.go | 85 ++++++++++++++---- src/pkg/common/oscal/complete-schema.go | 10 +++ src/pkg/common/validation/validation.go | 2 +- src/test/e2e/api_validation_test.go | 16 ++-- .../e2e/composition_component_def_test.go | 11 +-- src/test/e2e/create_resource_data_test.go | 12 +-- src/test/e2e/file_validation_test.go | 24 +++--- .../e2e/multi_resource_validation_test.go | 4 +- src/test/e2e/pod_validation_test.go | 46 ++++------ src/test/e2e/pod_wait_test.go | 11 +-- src/test/e2e/remote_validation_test.go | 9 +- src/test/e2e/resource_data_test.go | 11 +-- src/test/e2e/template_validation_test.go | 4 +- src/test/e2e/validation_composition_test.go | 15 ++-- 19 files changed, 248 insertions(+), 143 deletions(-) diff --git a/src/cmd/evaluate/evaluate.go b/src/cmd/evaluate/evaluate.go index 1c1ae9bbd..7e7d8d1f9 100644 --- a/src/cmd/evaluate/evaluate.go +++ b/src/cmd/evaluate/evaluate.go @@ -7,13 +7,13 @@ import ( "strings" "github.com/defenseunicorns/go-oscal/src/pkg/files" - oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" + "github.com/spf13/cobra" + "github.com/defenseunicorns/lula/src/cmd/common" pkgCommon "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/oscal" "github.com/defenseunicorns/lula/src/pkg/common/result" "github.com/defenseunicorns/lula/src/pkg/message" - "github.com/spf13/cobra" ) var evaluateHelp = ` @@ -81,7 +81,7 @@ func EvaluateCommand() *cobra.Command { return evaluateCmd } -func EvaluateAssessments(assessmentMap map[string]*oscalTypes.AssessmentResults, target string, summary, machine bool) error { +func EvaluateAssessments(assessmentMap map[string]*oscal.AssessmentResults, target string, summary, machine bool) error { // Identify the threshold & latest for comparison resultMap := oscal.FilterResults(assessmentMap) @@ -103,11 +103,7 @@ func EvaluateAssessments(assessmentMap map[string]*oscalTypes.AssessmentResults, // Write each file back in the case of modification for filePath, assessment := range assessmentMap { - model := oscalTypes.OscalCompleteSchema{ - AssessmentResults: assessment, - } - - err := oscal.WriteOscalModel(filePath, &model) + err := oscal.WriteOscalModelNew(filePath, assessment) if err != nil { return err } @@ -241,12 +237,12 @@ func evaluateTarget(target oscal.EvalResult, source string, summary, machine boo // Read many filepaths into a map[filepath]*AssessmentResults // Placing here until otherwise decided on value elsewhere -func readManyAssessmentResults(fileArray []string) (map[string]*oscalTypes.AssessmentResults, error) { +func readManyAssessmentResults(fileArray []string) (map[string]*oscal.AssessmentResults, error) { if len(fileArray) == 0 { return nil, fmt.Errorf("no files provided for evaluation") } - assessmentMap := make(map[string]*oscalTypes.AssessmentResults) + assessmentMap := make(map[string]*oscal.AssessmentResults) for _, fileString := range fileArray { err := files.IsJsonOrYaml(fileString) if err != nil { @@ -257,11 +253,14 @@ func readManyAssessmentResults(fileArray []string) (map[string]*oscalTypes.Asses if err != nil { return nil, err } - assessment, err := oscal.NewAssessmentResults(data) + + var assessment oscal.AssessmentResults + err = assessment.NewModel(data) if err != nil { return nil, err } - assessmentMap[fileString] = assessment + + assessmentMap[fileString] = &assessment } return assessmentMap, nil diff --git a/src/cmd/tools/print.go b/src/cmd/tools/print.go index d9964b304..fc0647e66 100644 --- a/src/cmd/tools/print.go +++ b/src/cmd/tools/print.go @@ -60,14 +60,15 @@ func PrintCommand() *cobra.Command { return fmt.Errorf("error getting assessment directory: %v", err) } - oscalAssessment, err := oscal.NewAssessmentResults(assessmentData) + var assessment oscal.AssessmentResults + err = assessment.NewModel(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) + err = PrintResources(assessment.Model, observationUuid, assessmentDir, outputFile) if err != nil { return fmt.Errorf("error printing resources: %v", err) } @@ -83,7 +84,7 @@ func PrintCommand() *cobra.Command { } // Print the validation - err = PrintValidation(oscalModel.ComponentDefinition, oscalAssessment, observationUuid, outputFile) + err = PrintValidation(oscalModel.ComponentDefinition, assessment.Model, observationUuid, outputFile) if err != nil { return fmt.Errorf("error printing validation: %v", err) } diff --git a/src/cmd/validate/validate.go b/src/cmd/validate/validate.go index 4403b6ab4..4dbdf5640 100644 --- a/src/cmd/validate/validate.go +++ b/src/cmd/validate/validate.go @@ -6,7 +6,6 @@ import ( "fmt" "path/filepath" - oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" "github.com/spf13/cobra" "github.com/defenseunicorns/lula/src/cmd/common" @@ -104,12 +103,8 @@ func ValidateCommand() *cobra.Command { return fmt.Errorf("assessment results are nil") } - var model = oscalTypes.OscalModels{ - AssessmentResults: assessmentResults, - } - // Write the assessment results to file - err = oscal.WriteOscalModel(outputFile, &model) + err = oscal.WriteOscalModelNew(outputFile, assessmentResults) if err != nil { return fmt.Errorf("error writing component to file: %v", err) } diff --git a/src/internal/tui/component/validate.go b/src/internal/tui/component/validate.go index 21f9b1a47..e6a79d90b 100644 --- a/src/internal/tui/component/validate.go +++ b/src/internal/tui/component/validate.go @@ -10,6 +10,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" + "github.com/defenseunicorns/lula/src/internal/tui/common" "github.com/defenseunicorns/lula/src/pkg/common/oscal" requirementstore "github.com/defenseunicorns/lula/src/pkg/common/requirement-store" @@ -73,7 +74,6 @@ func (m ValidateModel) Init() tea.Cmd { func (m ValidateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd - var err error switch msg := msg.(type) { case tea.WindowSizeMsg: @@ -106,10 +106,12 @@ func (m ValidateModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case ValidateStartMsg: validationStart := time.Now() - m.assessmentResults, err = m.RunValidations(m.runExecutable, m.target) + assessmentResults, err := m.RunValidations(m.runExecutable, m.target) if err != nil { common.PrintToLog("error running validations: %v", err) } + m.assessmentResults = assessmentResults.Model + validationDuration := time.Since(validationStart) // just adding a minimum of 2 seconds to the "validating" popup if validationDuration < time.Second*2 { @@ -208,7 +210,7 @@ func (m *ValidateModel) updateSizing(height, width int) { m.width = common.Max(width, minimumWidth) } -func (m *ValidateModel) RunValidations(runExecutable bool, target string) (*oscalTypes.AssessmentResults, error) { +func (m *ValidateModel) RunValidations(runExecutable bool, target string) (*oscal.AssessmentResults, error) { validator, err := validation.New( validation.WithAllowExecution(runExecutable, true), ) diff --git a/src/pkg/common/oscal/assessment-results.go b/src/pkg/common/oscal/assessment-results.go index 820084899..981527113 100644 --- a/src/pkg/common/oscal/assessment-results.go +++ b/src/pkg/common/oscal/assessment-results.go @@ -2,13 +2,14 @@ package oscal import ( "fmt" + "os" + "path/filepath" "slices" "sort" "time" "github.com/defenseunicorns/go-oscal/src/pkg/uuid" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" - "gopkg.in/yaml.v3" "github.com/defenseunicorns/lula/src/config" "github.com/defenseunicorns/lula/src/pkg/common" @@ -24,25 +25,74 @@ type EvalResult struct { Latest *oscalTypes.Result } -// NewAssessmentResults creates a new assessment results object from the given data. -func NewAssessmentResults(data []byte) (*oscalTypes.AssessmentResults, error) { - var oscalModels oscalTypes.OscalModels +type AssessmentResults struct { + Model *oscalTypes.AssessmentResults +} + +func NewAssessmentResults() *AssessmentResults { + var ar AssessmentResults + ar.Model = nil + return &ar +} - err := multiModelValidate(data) +func (a *AssessmentResults) NewModel(data []byte) error { + model, err := NewOscalModel(data) if err != nil { - return nil, err + return err } - err = yaml.Unmarshal(data, &oscalModels) - if err != nil { - fmt.Printf("Error marshalling yaml: %s\n", err.Error()) - return nil, err + a.Model = model.AssessmentResults + if a.Model == nil { + return fmt.Errorf("unable to find assessment results model") } - return oscalModels.AssessmentResults, nil + return nil +} + +func (*AssessmentResults) GetType() string { + return OSCAL_ASSESSMENT_RESULTS } -func GenerateAssessmentResults(results []oscalTypes.Result) (*oscalTypes.AssessmentResults, error) { +func (a *AssessmentResults) GetCompleteModel() *oscalTypes.OscalModels { + return &oscalTypes.OscalModels{ + AssessmentResults: a.Model, + } +} + +func (a *AssessmentResults) MakeDeterministic() error { + if a.Model == nil { + return fmt.Errorf("cannot make nil model deterministic") + } + MakeAssessmentResultsDeterministic(a.Model) + return nil +} + +func (a *AssessmentResults) HandleExisting(path string) error { + exists, err := common.CheckFileExists(path) + if err != nil { + return err + } + if exists { + path = filepath.Clean(path) + existingFileBytes, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("error reading file: %v", err) + } + assessment := NewAssessmentResults() + err = assessment.NewModel(existingFileBytes) + if err != nil { + return err + } + model, err := MergeAssessmentResults(assessment.Model, a.Model) + if err != nil { + return err + } + a.Model = model + } + return nil +} + +func GenerateAssessmentResults(results []oscalTypes.Result) (*AssessmentResults, error) { var assessmentResults = &oscalTypes.AssessmentResults{} // Single time used for all time related fields @@ -65,7 +115,11 @@ func GenerateAssessmentResults(results []oscalTypes.Result) (*oscalTypes.Assessm // Create results object assessmentResults.Results = results - return assessmentResults, nil + // Create the AssessmentResults + var assessment AssessmentResults + assessment.Model = assessmentResults + + return &assessment, nil } func MergeAssessmentResults(original *oscalTypes.AssessmentResults, latest *oscalTypes.AssessmentResults) (*oscalTypes.AssessmentResults, error) { @@ -215,14 +269,14 @@ func MakeAssessmentResultsDeterministic(assessment *oscalTypes.AssessmentResults // filterResults consumes many assessment-results objects and builds out a map of EvalResults filtered by target // this function looks at the target prop as the key in the map -func FilterResults(resultMap map[string]*oscalTypes.AssessmentResults) map[string]EvalResult { +func FilterResults(resultMap map[string]*AssessmentResults) map[string]EvalResult { evalResultMap := make(map[string]EvalResult) for _, assessment := range resultMap { - if assessment == nil { + if assessment == nil || assessment.Model == nil { continue } - for _, result := range assessment.Results { + for _, result := range assessment.Model.Results { if result.Props != nil { var target string hasTarget, targetValue := GetProp("target", LULA_NAMESPACE, result.Props) diff --git a/src/pkg/common/oscal/assessment-results_test.go b/src/pkg/common/oscal/assessment-results_test.go index fefcbb8dc..5effb6d76 100644 --- a/src/pkg/common/oscal/assessment-results_test.go +++ b/src/pkg/common/oscal/assessment-results_test.go @@ -2,6 +2,7 @@ package oscal_test import ( "os" + "path/filepath" "slices" "testing" "time" @@ -10,12 +11,16 @@ import ( oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" "github.com/defenseunicorns/lula/src/internal/testhelpers" "github.com/defenseunicorns/lula/src/pkg/common/oscal" "github.com/defenseunicorns/lula/src/pkg/message" ) +const validAssessmentPath = "../../../test/unit/common/oscal/valid-assessment-results.yaml" +const validMultiAssessmentPath = "../../../test/unit/common/oscal/valid-assessment-results-multi.yaml" + // Create re-usable findings and observations // use those in tests to generate test assessment results var findingMapPass = map[string]oscalTypes.Finding{ @@ -81,12 +86,14 @@ func TestFilterResults(t *testing.T) { // Expecting an error when evaluating assessment without results t.Run("Handle invalid assessment containing no results", func(t *testing.T) { - var assessment = &oscalTypes.AssessmentResults{ - UUID: uuid.NewUUID(), + var assessment = oscal.AssessmentResults{ + Model: &oscalTypes.AssessmentResults{ + UUID: uuid.NewUUID(), + }, } // key name does not matter here - var assessmentMap = map[string]*oscalTypes.AssessmentResults{ - "valid.yaml": assessment, + var assessmentMap = map[string]*oscal.AssessmentResults{ + "valid.yaml": &assessment, } resultMap := oscal.FilterResults(assessmentMap) @@ -110,7 +117,7 @@ func TestFilterResults(t *testing.T) { } // key name does not matter here - var assessmentMap = map[string]*oscalTypes.AssessmentResults{ + var assessmentMap = map[string]*oscal.AssessmentResults{ "valid.yaml": assessment, } @@ -148,7 +155,7 @@ func TestFilterResults(t *testing.T) { } // key name does not matter here - var assessmentMap = map[string]*oscalTypes.AssessmentResults{ + var assessmentMap = map[string]*oscal.AssessmentResults{ "valid.yaml": assessment, "invalid.yaml": assessment2, } @@ -200,7 +207,7 @@ func TestFilterResults(t *testing.T) { } // key name does not matter here - var assessmentMap = map[string]*oscalTypes.AssessmentResults{ + var assessmentMap = map[string]*oscal.AssessmentResults{ "valid.yaml": assessment, "invalid.yaml": assessment2, } @@ -250,15 +257,20 @@ func TestFilterResults(t *testing.T) { } // Update assessment 2 props so that we only have 1 threshold - oscal.UpdateProps("threshold", "docs.lula.dev/ns", "false", assessment2.Results[0].Props) + if assessment2.Model == nil { + t.Fatal("assessment2 model is nil") + } + oscal.UpdateProps("threshold", "docs.lula.dev/ns", "false", assessment2.Model.Results[0].Props) - assessment, err = oscal.MergeAssessmentResults(assessment, assessment2) + assessmentMerged, err := oscal.MergeAssessmentResults(assessment.Model, assessment2.Model) if err != nil { t.Fatalf("error merging assessment results: %v", err) } - var assessmentMap = map[string]*oscalTypes.AssessmentResults{ - "valid.yaml": assessment, + var assessmentMap = map[string]*oscal.AssessmentResults{ + "valid.yaml": { + Model: assessmentMerged, + }, } resultMap := oscal.FilterResults(assessmentMap) @@ -306,21 +318,26 @@ func TestFilterResults(t *testing.T) { } // Update assessment props so that we only have 1 threshold - oscal.UpdateProps("threshold", oscal.LULA_NAMESPACE, "false", assessment.Results[0].Props) + if assessment.Model == nil { + t.Fatal("assessment model is nil") + } + oscal.UpdateProps("threshold", oscal.LULA_NAMESPACE, "false", assessment.Model.Results[0].Props) // TODO: review assumptions made about order of assessments during merge - assessment, err = oscal.MergeAssessmentResults(assessment, assessment2) + assessmentMerged, err := oscal.MergeAssessmentResults(assessment.Model, assessment2.Model) if err != nil { t.Fatalf("error merging assessment results: %v", err) } // Backmatter should be nil - if assessment.BackMatter != nil { + if assessmentMerged.BackMatter != nil { t.Fatalf("Expected backmatter to be nil") } - var assessmentMap = map[string]*oscalTypes.AssessmentResults{ - "valid.yaml": assessment, + var assessmentMap = map[string]*oscal.AssessmentResults{ + "valid.yaml": { + Model: assessmentMerged, + }, } resultMap := oscal.FilterResults(assessmentMap) @@ -665,6 +682,39 @@ func TestGetObservationByUuid(t *testing.T) { }) } +func TestHandleExistingComponent(t *testing.T) { + validAssessmentBytes := loadTestData(t, validAssessmentPath) + + var validAssessment oscalTypes.OscalCompleteSchema + err := yaml.Unmarshal(validAssessmentBytes, &validAssessment) + require.NoError(t, err) + + t.Run("Handle Existing with no existing data", func(t *testing.T) { + var assessment oscal.AssessmentResults + assessment.NewModel(validAssessmentBytes) + + tmpDir := t.TempDir() + tmpFilePath := filepath.Join(tmpDir, "assessment.yaml") + + err := assessment.HandleExisting(tmpFilePath) + require.NoError(t, err) + + // Check length of results are the same + require.Equal(t, len(validAssessment.AssessmentResults.Results), len(assessment.Model.Results)) + }) + + t.Run("Handle Existing with existing data", func(t *testing.T) { + var assessment oscal.AssessmentResults + assessment.NewModel(validAssessmentBytes) + + err := assessment.HandleExisting(validMultiAssessmentPath) + require.NoError(t, err) + + // Check length of results is 3 + require.Equal(t, 3, len(assessment.Model.Results)) + }) +} + func FuzzNewAssessmentResults(f *testing.F) { for _, tc := range []string{"../../../test/unit/common/oscal/valid-assessment-results-multi.yaml", "../../../test/unit/common/oscal/valid-assessment-results-with-resources.yaml"} { @@ -676,6 +726,7 @@ func FuzzNewAssessmentResults(f *testing.F) { f.Fuzz(func(t *testing.T, a []byte) { // errors are ok, just watching for panics. - oscal.NewAssessmentResults(a) + var assessment oscal.AssessmentResults + assessment.NewModel(a) }) } diff --git a/src/pkg/common/oscal/complete-schema.go b/src/pkg/common/oscal/complete-schema.go index 18cc4c739..f70a3ca74 100644 --- a/src/pkg/common/oscal/complete-schema.go +++ b/src/pkg/common/oscal/complete-schema.go @@ -18,6 +18,16 @@ import ( "github.com/defenseunicorns/lula/src/pkg/message" ) +const ( + OSCAL_COMPONENT = "component" + OSCAL_ASSESSMENT_RESULTS = "assessment-results" + OSCAL_SYSTEM_SECURITY_PLAN = "system-security-plan" + OSCAL_PROFILE = "profile" + OSCAL_CATALOG = "catalog" + OSCAL_POAM = "poam" + OSCAL_ASSESSMENT_PLAN = "assessment-plan" +) + type OSCALModel interface { GetType() string GetCompleteModel() *oscalTypes.OscalModels diff --git a/src/pkg/common/validation/validation.go b/src/pkg/common/validation/validation.go index b6ed7bede..5e7c7ef7b 100644 --- a/src/pkg/common/validation/validation.go +++ b/src/pkg/common/validation/validation.go @@ -41,7 +41,7 @@ func New(opts ...Option) (*Validator, error) { return &validator, nil } -func (v *Validator) ValidateOnPath(ctx context.Context, path, target string) (assessmentResult *oscalTypes.AssessmentResults, err error) { +func (v *Validator) ValidateOnPath(ctx context.Context, path, target string) (assessmentResult *oscal.AssessmentResults, err error) { var oscalModel *oscalTypes.OscalCompleteSchema if v.composer == nil { path = filepath.Clean(path) diff --git a/src/test/e2e/api_validation_test.go b/src/test/e2e/api_validation_test.go index 8dcc983d0..c7deb0300 100644 --- a/src/test/e2e/api_validation_test.go +++ b/src/test/e2e/api_validation_test.go @@ -73,11 +73,11 @@ func TestApiValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") @@ -162,11 +162,11 @@ func TestApiValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") @@ -270,9 +270,9 @@ func TestApiValidation_templatedGet(t *testing.T) { assessment, err := validator.ValidateOnPath(context.Background(), tmpl, "") require.NoError(t, err) - require.GreaterOrEqual(t, len(assessment.Results), 1) + require.GreaterOrEqual(t, len(assessment.Model.Results), 1) - result := assessment.Results[0] + result := assessment.Model.Results[0] require.NotNil(t, result.Findings) for _, finding := range *result.Findings { state := finding.Target.Status.State @@ -358,9 +358,9 @@ func TestApiValidation_templatedPost(t *testing.T) { assessment, err := validator.ValidateOnPath(context.Background(), tmpl, "") require.NoError(t, err) - require.GreaterOrEqual(t, len(assessment.Results), 1) + require.GreaterOrEqual(t, len(assessment.Model.Results), 1) - result := assessment.Results[0] + result := assessment.Model.Results[0] require.NotNil(t, result.Findings) for _, finding := range *result.Findings { state := finding.Target.Status.State diff --git a/src/test/e2e/composition_component_def_test.go b/src/test/e2e/composition_component_def_test.go index 8a1c96029..a03d0fac5 100644 --- a/src/test/e2e/composition_component_def_test.go +++ b/src/test/e2e/composition_component_def_test.go @@ -5,14 +5,15 @@ import ( "testing" "time" - "github.com/defenseunicorns/lula/src/pkg/common/composition" - "github.com/defenseunicorns/lula/src/pkg/common/validation" - "github.com/defenseunicorns/lula/src/test/util" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/e2e-framework/klient/wait" "sigs.k8s.io/e2e-framework/klient/wait/conditions" "sigs.k8s.io/e2e-framework/pkg/envconf" "sigs.k8s.io/e2e-framework/pkg/features" + + "github.com/defenseunicorns/lula/src/pkg/common/composition" + "github.com/defenseunicorns/lula/src/pkg/common/validation" + "github.com/defenseunicorns/lula/src/test/util" ) type compDefContextKey string @@ -51,11 +52,11 @@ func TestComponentDefinitionComposition(t *testing.T) { t.Errorf("Error validating component definition: %v", err) } - if assessment.Results == nil { + if assessment.Model.Results == nil { t.Fatal("Expected to have results") } - results := assessment.Results + results := assessment.Model.Results var expectedFindings, expectedObservations int expectedResults := len(results) diff --git a/src/test/e2e/create_resource_data_test.go b/src/test/e2e/create_resource_data_test.go index a522294b0..d0d9eff8a 100644 --- a/src/test/e2e/create_resource_data_test.go +++ b/src/test/e2e/create_resource_data_test.go @@ -49,11 +49,11 @@ func TestCreateResourceDataValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") @@ -108,11 +108,11 @@ func TestCreateResourceDataValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") @@ -174,11 +174,11 @@ func TestDeniedCreateResources(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") diff --git a/src/test/e2e/file_validation_test.go b/src/test/e2e/file_validation_test.go index 474d99fe9..a81b54de6 100644 --- a/src/test/e2e/file_validation_test.go +++ b/src/test/e2e/file_validation_test.go @@ -25,11 +25,11 @@ func TestFileValidation(t *testing.T) { assessment, err := validator.ValidateOnPath(ctx, passDir+oscalFile, "") require.NoError(t, err) - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] require.NotNil(t, result, "Expected findings to be not nil") for _, finding := range *result.Findings { @@ -46,9 +46,9 @@ func TestFileValidation(t *testing.T) { assessment, err := validator.ValidateOnPath(ctx, passDir+kyvernoFile, "") require.NoError(t, err) - require.NotEmpty(t, assessment.Results, "Expected greater than zero results") + require.NotEmpty(t, assessment.Model.Results, "Expected greater than zero results") - result := assessment.Results[0] + result := assessment.Model.Results[0] require.NotNil(t, result, "Expected findings to be not nil") for _, finding := range *result.Findings { @@ -62,9 +62,9 @@ func TestFileValidation(t *testing.T) { require.NoError(t, err) assessment, err := validator.ValidateOnPath(ctx, passDir+"/component-definition-string-file.yaml", "") require.NoError(t, err) - require.NotEmpty(t, assessment.Results, "Expected greater than zero results") + require.NotEmpty(t, assessment.Model.Results, "Expected greater than zero results") - result := assessment.Results[0] + result := assessment.Model.Results[0] require.NotNil(t, result, "Expected findings to be not nil") for _, finding := range *result.Findings { @@ -79,9 +79,9 @@ func TestFileValidation(t *testing.T) { assessment, err := validator.ValidateOnPath(ctx, failDir+oscalFile, "") require.NoError(t, err) - require.NotEmpty(t, assessment.Results, "Expected greater than zero results") + require.NotEmpty(t, assessment.Model.Results, "Expected greater than zero results") - result := assessment.Results[0] + result := assessment.Model.Results[0] require.NotNil(t, result, "Expected findings to be not nil") for _, finding := range *result.Findings { @@ -96,11 +96,11 @@ func TestFileValidation(t *testing.T) { assessment, err := validator.ValidateOnPath(ctx, failDir+kyvernoFile, "") require.NoError(t, err) - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] require.NotNil(t, result, "Expected findings to be not nil") for _, finding := range *result.Findings { @@ -125,8 +125,8 @@ func TestFileValidation(t *testing.T) { require.NoError(t, err) assessment, err := validator.ValidateOnPath(ctx, "scenarios/file-validations/pass/component-definition-remote-files.yaml", "") require.NoError(t, err) - require.Len(t, assessment.Results, 1) - result := assessment.Results[0] + require.Len(t, assessment.Model.Results, 1) + result := assessment.Model.Results[0] require.NotNil(t, result, "Expected findings to be not nil") for _, finding := range *result.Findings { state := finding.Target.Status.State diff --git a/src/test/e2e/multi_resource_validation_test.go b/src/test/e2e/multi_resource_validation_test.go index 64bc29c70..bc704148e 100644 --- a/src/test/e2e/multi_resource_validation_test.go +++ b/src/test/e2e/multi_resource_validation_test.go @@ -121,11 +121,11 @@ func TestMultiResourceValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") diff --git a/src/test/e2e/pod_validation_test.go b/src/test/e2e/pod_validation_test.go index 57655ae41..13160f83f 100644 --- a/src/test/e2e/pod_validation_test.go +++ b/src/test/e2e/pod_validation_test.go @@ -251,11 +251,11 @@ func validatePodLabelPass(ctx context.Context, t *testing.T, oscalPath string) c t.Fatalf("Failed to validate oscal file: %s", revisionOptions.OutputFile) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") @@ -269,36 +269,28 @@ func validatePodLabelPass(ctx context.Context, t *testing.T, oscalPath string) c } // Test report generation - report, err := oscal.GenerateAssessmentResults(assessment.Results) + report, err := oscal.GenerateAssessmentResults(assessment.Model.Results) if err != nil { t.Fatal("Failed generation of Assessment Results object with: ", err) } - var model = oscalTypes.OscalModels{ - AssessmentResults: report, - } - // Write the assessment results to file - err = oscal.WriteOscalModel("sar-test.yaml", &model) + err = oscal.WriteOscalModelNew("sar-test.yaml", report) require.NoError(t, err) - initialResultCount := len(report.Results) + initialResultCount := len(report.Model.Results) //Perform the write operation again and read the file to ensure result was appended - report, err = oscal.GenerateAssessmentResults(assessment.Results) + report, err = oscal.GenerateAssessmentResults(assessment.Model.Results) if err != nil { t.Fatal("Failed generation of Assessment Results object with: ", err) } // Get the UUID of the report results - there should only be one - resultId := report.Results[0].UUID - - model = oscalTypes.OscalModels{ - AssessmentResults: report, - } + resultId := report.Model.Results[0].UUID // Write the assessment results to file - err = oscal.WriteOscalModel("sar-test.yaml", &model) + err = oscal.WriteOscalModelNew("sar-test.yaml", report) require.NoError(t, err) data, err := os.ReadFile("sar-test.yaml") @@ -306,17 +298,18 @@ func validatePodLabelPass(ctx context.Context, t *testing.T, oscalPath string) c t.Fatal(err) } - tempAssessment, err := oscal.NewAssessmentResults(data) + var tempAssessment oscal.AssessmentResults + err = tempAssessment.NewModel(data) if err != nil { t.Fatal(err) } // The number of results in the file should be more than initially - if len(tempAssessment.Results) <= initialResultCount { + if len(tempAssessment.Model.Results) <= initialResultCount { t.Fatal("Failed to append results to existing report") } - if resultId != tempAssessment.Results[0].UUID { + if resultId != tempAssessment.Model.Results[0].UUID { t.Fatal("Failed to prepend results to existing report") } @@ -341,11 +334,11 @@ func validatePodLabelFail(ctx context.Context, t *testing.T, oscalPath string) ( t.Fatalf("Failed to validate oscal file: %s", oscalPath) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") @@ -395,11 +388,11 @@ func validateSaveResources(ctx context.Context, t *testing.T, oscalPath string) t.Fatalf("Failed to validate oscal file: %s", oscalPath) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] // Check that remote files are created for _, o := range *result.Observations { @@ -427,13 +420,8 @@ func validateSaveResources(ctx context.Context, t *testing.T, oscalPath string) } } - // Check that assessment results can be written to file - var model = oscalTypes.OscalModels{ - AssessmentResults: assessment, - } - // Write the assessment results to file - err = oscal.WriteOscalModel(filepath.Join(tempDir, "assessment-results.yaml"), &model) + err = oscal.WriteOscalModelNew(filepath.Join(tempDir, "assessment-results.yaml"), assessment) if err != nil { t.Fatal("error writing assessment results to file") } diff --git a/src/test/e2e/pod_wait_test.go b/src/test/e2e/pod_wait_test.go index 534f856d3..c55332f91 100644 --- a/src/test/e2e/pod_wait_test.go +++ b/src/test/e2e/pod_wait_test.go @@ -4,12 +4,13 @@ import ( "context" "testing" - "github.com/defenseunicorns/lula/src/pkg/common/validation" - "github.com/defenseunicorns/lula/src/pkg/message" - "github.com/defenseunicorns/lula/src/test/util" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/e2e-framework/pkg/envconf" "sigs.k8s.io/e2e-framework/pkg/features" + + "github.com/defenseunicorns/lula/src/pkg/common/validation" + "github.com/defenseunicorns/lula/src/pkg/message" + "github.com/defenseunicorns/lula/src/test/util" ) func TestPodWaitValidation(t *testing.T) { @@ -43,11 +44,11 @@ func TestPodWaitValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") diff --git a/src/test/e2e/remote_validation_test.go b/src/test/e2e/remote_validation_test.go index 87280bb7b..a87e5cb23 100644 --- a/src/test/e2e/remote_validation_test.go +++ b/src/test/e2e/remote_validation_test.go @@ -5,13 +5,14 @@ import ( "testing" "time" - "github.com/defenseunicorns/lula/src/pkg/common/validation" - "github.com/defenseunicorns/lula/src/test/util" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/e2e-framework/klient/wait" "sigs.k8s.io/e2e-framework/klient/wait/conditions" "sigs.k8s.io/e2e-framework/pkg/envconf" "sigs.k8s.io/e2e-framework/pkg/features" + + "github.com/defenseunicorns/lula/src/pkg/common/validation" + "github.com/defenseunicorns/lula/src/test/util" ) func TestRemoteValidation(t *testing.T) { @@ -47,11 +48,11 @@ func TestRemoteValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") diff --git a/src/test/e2e/resource_data_test.go b/src/test/e2e/resource_data_test.go index 202372f9b..47373821d 100644 --- a/src/test/e2e/resource_data_test.go +++ b/src/test/e2e/resource_data_test.go @@ -5,14 +5,15 @@ import ( "testing" "time" - "github.com/defenseunicorns/lula/src/pkg/common/validation" - "github.com/defenseunicorns/lula/src/pkg/message" - "github.com/defenseunicorns/lula/src/test/util" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/e2e-framework/klient/wait" "sigs.k8s.io/e2e-framework/klient/wait/conditions" "sigs.k8s.io/e2e-framework/pkg/envconf" "sigs.k8s.io/e2e-framework/pkg/features" + + "github.com/defenseunicorns/lula/src/pkg/common/validation" + "github.com/defenseunicorns/lula/src/pkg/message" + "github.com/defenseunicorns/lula/src/test/util" ) func TestResourceDataValidation(t *testing.T) { @@ -87,11 +88,11 @@ func TestResourceDataValidation(t *testing.T) { t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") diff --git a/src/test/e2e/template_validation_test.go b/src/test/e2e/template_validation_test.go index 7eda2cf02..7eab57ccc 100644 --- a/src/test/e2e/template_validation_test.go +++ b/src/test/e2e/template_validation_test.go @@ -173,11 +173,11 @@ func validateFindingsSatisfied(ctx context.Context, t *testing.T, oscalPath stri t.Fatalf("Failed to validate oscal file: %s", oscalPath) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") diff --git a/src/test/e2e/validation_composition_test.go b/src/test/e2e/validation_composition_test.go index d725e2352..0f883941f 100644 --- a/src/test/e2e/validation_composition_test.go +++ b/src/test/e2e/validation_composition_test.go @@ -8,17 +8,18 @@ import ( "time" oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" - "github.com/defenseunicorns/lula/src/pkg/common/composition" - "github.com/defenseunicorns/lula/src/pkg/common/oscal" - "github.com/defenseunicorns/lula/src/pkg/common/validation" - validationstore "github.com/defenseunicorns/lula/src/pkg/common/validation-store" - "github.com/defenseunicorns/lula/src/test/util" "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/e2e-framework/klient/wait" "sigs.k8s.io/e2e-framework/klient/wait/conditions" "sigs.k8s.io/e2e-framework/pkg/envconf" "sigs.k8s.io/e2e-framework/pkg/features" + + "github.com/defenseunicorns/lula/src/pkg/common/composition" + "github.com/defenseunicorns/lula/src/pkg/common/oscal" + "github.com/defenseunicorns/lula/src/pkg/common/validation" + validationstore "github.com/defenseunicorns/lula/src/pkg/common/validation-store" + "github.com/defenseunicorns/lula/src/test/util" ) type contextKey string @@ -81,11 +82,11 @@ func validateComposition(ctx context.Context, t *testing.T, oscalPath, expectedF t.Fatal(err) } - if len(assessment.Results) == 0 { + if len(assessment.Model.Results) == 0 { t.Fatal("Expected greater than zero results") } - result := assessment.Results[0] + result := assessment.Model.Results[0] if result.Findings == nil { t.Fatal("Expected findings to be not nil") From 296192fe7f74e16ed7ab194ed51ae9d24b556df7 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Tue, 14 Jan 2025 13:19:26 -0500 Subject: [PATCH 6/8] fix: updated declaration syntax --- src/pkg/common/oscal/component.go | 7 ++++--- src/pkg/common/oscal/component_test.go | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index 88258acd5..395187628 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -384,9 +384,10 @@ func ComponentFromCatalog(command string, source string, catalog *oscalTypes.Cat Version: "0.0.1", } - return &ComponentDefinition{ - Model: componentDefinition, - }, nil + var compDef ComponentDefinition + compDef.Model = componentDefinition + + return &compDef, nil } diff --git a/src/pkg/common/oscal/component_test.go b/src/pkg/common/oscal/component_test.go index 734048415..410c850ea 100644 --- a/src/pkg/common/oscal/component_test.go +++ b/src/pkg/common/oscal/component_test.go @@ -298,7 +298,7 @@ func TestMergeComponentDefinitions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - component := oscal.ComponentDefinition{} + var component oscal.ComponentDefinition err := component.NewModel(validBytes) require.NoError(t, err) @@ -519,7 +519,7 @@ func TestControlImplementationsToRequirementsMap(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - component := oscal.ComponentDefinition{} + var component oscal.ComponentDefinition data := loadTestData(t, tt.filepath) err := component.NewModel(data) @@ -565,7 +565,7 @@ func TestFilterControlImplementations(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - component := oscal.ComponentDefinition{} + var component oscal.ComponentDefinition data := loadTestData(t, tt.filepath) err := component.NewModel(data) @@ -593,7 +593,7 @@ func TestHandleExistingComponent(t *testing.T) { require.NoError(t, err) t.Run("Handle Existing with no existing data", func(t *testing.T) { - component := oscal.ComponentDefinition{} + var component oscal.ComponentDefinition component.NewModel(validComponentBytes) tmpDir := t.TempDir() @@ -607,7 +607,7 @@ func TestHandleExistingComponent(t *testing.T) { }) t.Run("Handle Existing with existing data", func(t *testing.T) { - component := oscal.ComponentDefinition{} + var component oscal.ComponentDefinition component.NewModel(validComponentBytes) err := component.HandleExisting(validGeneratedComponentPath) From c3eb5832261029647ec2300cd3b2d316032e6736 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Wed, 15 Jan 2025 15:27:23 -0500 Subject: [PATCH 7/8] feat: added RewritePaths method on compdef --- src/pkg/common/common.go | 97 +++++++ src/pkg/common/common_test.go | 269 ++++++++++++++++++ src/pkg/common/network/network.go | 32 ++- src/pkg/common/network/network_test.go | 61 ++++ .../common/oscal/assessment-results_test.go | 2 +- src/pkg/common/oscal/component.go | 42 +++ src/pkg/common/oscal/component_test.go | 36 +++ .../oscal/valid-component-local-refs.yaml | 56 ++++ .../oscal/valid-component-refs-from-root.yaml | 56 ++++ 9 files changed, 640 insertions(+), 11 deletions(-) create mode 100644 src/test/unit/common/oscal/valid-component-local-refs.yaml create mode 100644 src/test/unit/common/oscal/valid-component-refs-from-root.yaml diff --git a/src/pkg/common/common.go b/src/pkg/common/common.go index d3a18683b..030818575 100644 --- a/src/pkg/common/common.go +++ b/src/pkg/common/common.go @@ -8,6 +8,7 @@ import ( "io" "os" "path/filepath" + "reflect" "regexp" "strings" @@ -15,6 +16,7 @@ import ( goversion "github.com/hashicorp/go-version" "k8s.io/apimachinery/pkg/util/yaml" + "github.com/defenseunicorns/lula/src/pkg/common/network" "github.com/defenseunicorns/lula/src/pkg/domains/api" "github.com/defenseunicorns/lula/src/pkg/domains/files" kube "github.com/defenseunicorns/lula/src/pkg/domains/kubernetes" @@ -211,3 +213,98 @@ func CleanMultilineString(str string) string { formatted := re.ReplaceAllString(str, "\n") return formatted } + +// RemapPath takes an input path, relative to the baseDir, and remaps it to be relative to the newDir +// Example: path = "folder/file.txt", baseDir = "/home/user/dir", newDir = "/home/user/newDir" +// output path = "../dir/folder/file.txt" +func RemapPath(path string, baseDir string, newDir string) (string, error) { + // Do nothing if the path is a UUID reference + if isUUIDReference(path) { + return path, nil + } + + // Return if the path is a URL or absolute link + localDir := network.GetLocalFileDir(path, baseDir) + if localDir == "" { + return path, nil + } + + // Trim file://, if present + path = strings.TrimPrefix(path, "file://") + + // Find the relative path from newDir to baseDir + relativePath, err := filepath.Rel(newDir, baseDir) + if err != nil { + return "", err + } + + // Append the original relative path to the computed relative path + remappedPath := filepath.Join(relativePath, path) + remappedPath = filepath.Clean(remappedPath) + + return remappedPath, nil +} + +func isUUIDReference(path string) bool { + path = strings.TrimPrefix(path, UUID_PREFIX) + return checkValidUuid(path) +} + +// TraverseAndUpdatePaths uses reflection to traverse the obj based on the path and update file path references +func TraverseAndUpdatePaths(obj interface{}, path string, baseDir string, newDir string) error { + // Split the path into components + components := splitPath(path) + + // Start reflection traversal + return reflectTraverseAndUpdate(reflect.ValueOf(obj), components, baseDir, newDir) +} + +func reflectTraverseAndUpdate(val reflect.Value, components []string, baseDir string, newDir string) error { + if val.Kind() == reflect.Ptr { + val = val.Elem() // Dereference pointer + } + + if val.Kind() == reflect.Slice || val.Kind() == reflect.Array { + // Handle slices/arrays + for i := 0; i < val.Len(); i++ { + err := reflectTraverseAndUpdate(val.Index(i), components, baseDir, newDir) + if err != nil { + return err + } + } + return nil + } + + if val.Kind() == reflect.Struct && len(components) > 0 { + // Handle structs + field := val.FieldByName(components[0]) + if !field.IsValid() { + return fmt.Errorf("field %s not found", components[0]) + } + return reflectTraverseAndUpdate(field, components[1:], baseDir, newDir) + } + + if len(components) == 0 { + if val.Kind() == reflect.String { + // Update the final field (assumed to be a string) + newValue, err := RemapPath(val.String(), baseDir, newDir) + if err != nil { + return fmt.Errorf("error remapping path %s: %v", val.String(), err) + } + if val.CanSet() { + val.SetString(newValue) + return nil + } + return fmt.Errorf("unable to set string value") + } + // if val.Kind() is not a string, we can't update it + return fmt.Errorf("cannot update type %s", val.Kind()) + } + + return nil +} + +func splitPath(path string) []string { + components := strings.Split(path, ".") + return components +} diff --git a/src/pkg/common/common_test.go b/src/pkg/common/common_test.go index ada5802d6..feef54fbb 100644 --- a/src/pkg/common/common_test.go +++ b/src/pkg/common/common_test.go @@ -6,7 +6,9 @@ import ( "path/filepath" "strings" "testing" + "time" + oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-3" kjson "github.com/kyverno/kyverno-json/pkg/apis/policy/v1alpha1" "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" @@ -529,6 +531,273 @@ func TestIsVersionValid(t *testing.T) { } } +func TestRemapPath(t *testing.T) { + tests := []struct { + name string + path string + baseDir string + newDir string + expectedPath string + expectedError bool + }{ + { + name: "Absolute Path", + path: "/path/to/file.txt", + baseDir: "/path/to", + newDir: "/new/path", + expectedPath: "/path/to/file.txt", + expectedError: false, + }, + { + name: "Relative Path", + path: "path/to/file.txt", + baseDir: "/app", + newDir: "/app2", + expectedPath: "../app/path/to/file.txt", + expectedError: false, + }, + { + name: "Relative Path with ..", + path: "../path/to/file.txt", + baseDir: "/app/sub-path", + newDir: "/app", + expectedPath: "path/to/file.txt", + expectedError: false, + }, + { + name: "Relative Path with deep nesting", + path: "../../file.txt", + baseDir: "/app/path/to/caller", + newDir: "/app", + expectedPath: "path/file.txt", + expectedError: false, + }, + { + name: "Relative Path with file://", + path: "file://path/to/file.txt", + baseDir: "/app", + newDir: "/app2", + expectedPath: "../app/path/to/file.txt", + expectedError: false, + }, + { + name: "Absolute Path with file://", + path: "file:///path/to/file.txt", + baseDir: "/path/to", + newDir: "/new/path", + expectedPath: "file:///path/to/file.txt", + expectedError: false, + }, + { + name: "UUID", + path: "#0a2b9722-06ce-446f-85e5-cdfe2fe70975", + baseDir: "/path/to", + newDir: "/new/path", + expectedPath: "#0a2b9722-06ce-446f-85e5-cdfe2fe70975", + expectedError: false, + }, + { + // This doesn't error because there's no check in the function to validate the path + // I think this is ok, because the old path wouldn't work anyway, so the new path not working doesn't matter(?) + name: "Invalid path remap", + path: "../../path/to/file.txt", + baseDir: "/app", + newDir: "/app2", + expectedPath: "../../path/to/file.txt", + expectedError: false, + }, + { + // Similar logic to previous case: This doesn't error because there's no check in the function to validate the path + name: "Invalid path name", + path: "inv@1*d pa#h", + baseDir: "/app", + newDir: "/app2", + expectedPath: "../app/inv@1*d pa#h", + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPath, err := common.RemapPath(tt.path, tt.baseDir, tt.newDir) + if (err != nil) != tt.expectedError { + t.Errorf("RemapPath() error = %v, wantErr %v", err, tt.expectedError) + return + } + require.Equal(t, tt.expectedPath, gotPath) + }) + } + +} + +func TestTraverseAndUpdatePaths(t *testing.T) { + tests := []struct { + name string + obj interface{} + expectedObj interface{} + path string + curDir string + newDir string + expectedError bool + expectedErrorMsg string + }{ + { + name: "Paths nested in list", + path: "Metadata.Links.Href", + obj: &oscalTypes.ComponentDefinition{ + Metadata: oscalTypes.Metadata{ + Title: "Test Title", + Links: &[]oscalTypes.Link{ + { + Href: "https://example.com", + Rel: "no change", + }, + { + Href: "./file.txt", + Rel: "re-map", + }, + }, + }, + }, + expectedObj: &oscalTypes.ComponentDefinition{ + Metadata: oscalTypes.Metadata{ + Title: "Test Title", + Links: &[]oscalTypes.Link{ + { + Href: "https://example.com", + Rel: "no change", + }, + { + Href: "../file.txt", + Rel: "re-map", + }, + }, + }, + }, + curDir: "/app", + newDir: "/app/new", + expectedError: false, + }, + { + name: "Double nested paths in list", + path: "Components.Links.Href", + obj: &oscalTypes.ComponentDefinition{ + Components: &[]oscalTypes.DefinedComponent{ + { + Title: "Test Component", + Links: &[]oscalTypes.Link{ + { + Href: "https://foo.com", + Rel: "no change", + }, + { + Href: "my-file.txt", + Rel: "re-map", + }, + }, + }, + { + Title: "Test Component 2", + Links: &[]oscalTypes.Link{ + { + Href: "path/to/component.yaml", + Rel: "component link change", + }, + }, + }, + }, + }, + expectedObj: &oscalTypes.ComponentDefinition{ + Components: &[]oscalTypes.DefinedComponent{ + { + Title: "Test Component", + Links: &[]oscalTypes.Link{ + { + Href: "https://foo.com", + Rel: "no change", + }, + { + Href: "../my-file.txt", + Rel: "re-map", + }, + }, + }, + { + Title: "Test Component 2", + Links: &[]oscalTypes.Link{ + { + Href: "../path/to/component.yaml", + Rel: "component link change", + }, + }, + }, + }, + }, + curDir: "/app", + newDir: "/app/new", + expectedError: false, + }, + { + name: "Incorrect type", + path: "Metadata.LastModified", + obj: &oscalTypes.ComponentDefinition{ + Metadata: oscalTypes.Metadata{ + Title: "Test Title", + LastModified: time.Now(), + Links: &[]oscalTypes.Link{ + { + Href: "https://example.com", + Rel: "no change", + }, + { + Href: "./file.txt", + Rel: "re-map", + }, + }, + }, + }, + expectedError: true, + expectedErrorMsg: "cannot update type", + }, + { + name: "Not a valid field", + path: "Metadata.Foo", + obj: &oscalTypes.ComponentDefinition{ + Metadata: oscalTypes.Metadata{ + Title: "Test Title", + Links: &[]oscalTypes.Link{ + { + Href: "https://example.com", + Rel: "no change", + }, + { + Href: "./file.txt", + Rel: "re-map", + }, + }, + }, + }, + expectedError: true, + expectedErrorMsg: "field Foo not found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := common.TraverseAndUpdatePaths(tt.obj, tt.path, tt.curDir, tt.newDir) + if (err != nil) != tt.expectedError { + t.Errorf("TraverseAndUpdatePaths() error = %v, wantErr %v", err, tt.expectedError) + return + } + if tt.expectedError { + require.Contains(t, err.Error(), tt.expectedErrorMsg) + return + } + require.Equal(t, tt.expectedObj, tt.obj) + }) + } +} + func FuzzPrefix(f *testing.F) { f.Add("uuid") f.Add("149f0049-7a3c-4e4d-8431-bec3a55f31d9") diff --git a/src/pkg/common/network/network.go b/src/pkg/common/network/network.go index c2b4effa6..1a8c7357c 100644 --- a/src/pkg/common/network/network.go +++ b/src/pkg/common/network/network.go @@ -156,25 +156,37 @@ func FetchLocalFile(url *url.URL, config *fetchOpts) ([]byte, error) { return bytes, err } +// GetLocalFileDir returns the directory of a local file +// Intent of check is to handle different specifications of file paths: +// - file:///path/to/file +// - ./path/to/file +// - /path/to/file +// - https://example.com/path/to/file +// ** This will not work for Windows file paths, but Lula doesn't run on Windows for now func GetLocalFileDir(inputURL, baseDir string) string { url, err := url.Parse(inputURL) if err != nil { return "" } + requestUri := url.RequestURI() - // Intent of check is to handle different specifications of file paths: - // - file:///path/to/file - // - ./path/to/file - // - /path/to/file - // - https://example.com/path/to/file - if url.Scheme == "file" || !url.IsAbs() { - fullPath := filepath.Join(baseDir, url.Host, requestUri) - if _, err := os.Stat(fullPath); err == nil { - return filepath.Dir(fullPath) + if url.Scheme == "file" { + // If the scheme is file, check if the path is absolute (if host is "", it's absolute) + if url.Host == "" { + return "" } + } else if url.Scheme != "" { + return "" } - return "" + + // If the path is not absolute, join it with the baseDir + if filepath.IsAbs(filepath.Join(url.Host, requestUri)) { + return "" + } + + fullPath := filepath.Join(baseDir, url.Host, requestUri) + return filepath.Dir(fullPath) } // ValidateChecksum validates a given checksum against a given []bytes. diff --git a/src/pkg/common/network/network_test.go b/src/pkg/common/network/network_test.go index c0dc1a360..bfdf1b473 100644 --- a/src/pkg/common/network/network_test.go +++ b/src/pkg/common/network/network_test.go @@ -224,3 +224,64 @@ func TestParseChecksum(t *testing.T) { }) } } + +func TestGetLocalFileDir(t *testing.T) { + tests := []struct { + name string + inputFile string + baseDir string + expectedDir string + }{ + { + name: "Absolute Path", + inputFile: "/root/path/to/file.txt", + baseDir: "/root", + expectedDir: "", + }, + { + name: "Relative Path", + inputFile: "path/to/file.txt", + baseDir: "/root", + expectedDir: "/root/path/to", + }, + { + name: "http URL", + inputFile: "https://example.com/path/to/file.txt", + baseDir: "/path/to", + expectedDir: "", + }, + { + name: "Relative Path with ..", + inputFile: "../path/to/file.txt", + baseDir: "/root/path", + expectedDir: "/root/path/to", + }, + { + name: "Relative Path with file://", + inputFile: "file://path/to/file.txt", + baseDir: "/root", + expectedDir: "/root/path/to", + }, + { + name: "Relative Path with file://..", + inputFile: "file://../path/to/file.txt", + baseDir: "/root/path", + expectedDir: "/root/path/to", + }, + { + name: "Absolute Path with file://", + inputFile: "file:///root/path/to/file.txt", + baseDir: "/root", + expectedDir: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotDir := network.GetLocalFileDir(tt.inputFile, tt.baseDir) + if tt.expectedDir != gotDir { + t.Errorf("GetLocalFileDir() gotDir = %v, want %v", gotDir, tt.expectedDir) + } + }) + } +} diff --git a/src/pkg/common/oscal/assessment-results_test.go b/src/pkg/common/oscal/assessment-results_test.go index 5effb6d76..101901718 100644 --- a/src/pkg/common/oscal/assessment-results_test.go +++ b/src/pkg/common/oscal/assessment-results_test.go @@ -682,7 +682,7 @@ func TestGetObservationByUuid(t *testing.T) { }) } -func TestHandleExistingComponent(t *testing.T) { +func TestHandleExistingAssessmentResults(t *testing.T) { validAssessmentBytes := loadTestData(t, validAssessmentPath) var validAssessment oscalTypes.OscalCompleteSchema diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index 395187628..985a7a919 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -102,6 +102,48 @@ func (c *ComponentDefinition) HandleExisting(path string) error { return nil } +// RewritePaths finds all the paths in the component definition relative to the baseDir and updates them to be relative to the newDir +func (c *ComponentDefinition) RewritePaths(baseDir string, newDir string) error { + if c.Model == nil { + return fmt.Errorf("cannot remap paths, model is nil") + } + + // Find all the paths in the component definition + pathsToRemap := []string{ + "BackMatter.Resources.Rlinks.Href", + "Metadata.Links.Href", + "Metadata.Revisions.Links.Href", + "Metadata.Roles.Links.Href", + "Metadata.Locations.Links.Href", + "Metadata.Parties.Links.Href", + "Metadata.ResponsibleParties.Links.Href", + "Metadata.Actions.Links.Href", + "ImportComponentDefinitions.Href", + "Components.Links.Href", + "Components.ResponsibleRoles.Links.Href", + "Components.ControlImplementations.Links.Href", + "Components.ControlImplementations.Source", + "Components.ControlImplementations.ImplementedRequirements.Links.Href", + "Components.ControlImplementations.ImplementedRequirements.ResponsibleRoles.Links.Href", + "Components.ControlImplementations.ImplementedRequirements.Statements.Links.Href", + "Components.ControlImplementations.ImplementedRequirements.Statements.ResponsibleRoles.Links.Href", + "Capabilities.Links.Href", + "Capabilities.ControlImplementations.Links.Href", + "Capabilities.ControlImplementations.Source", + "Capabilities.ControlImplementations.ImplementedRequirements.Links.Href", + "Capabilities.ControlImplementations.ImplementedRequirements.ResponsibleRoles.Links.Href", + "Capabilities.ControlImplementations.ImplementedRequirements.Statements.Links.Href", + "Capabilities.ControlImplementations.ImplementedRequirements.Statements.ResponsibleRoles.Links.Href", + } + + // For pathsToRemap, find all paths in the component defintion and run common.RemapPath on them + for _, path := range pathsToRemap { + common.TraverseAndUpdatePaths(c.Model, path, baseDir, newDir) + } + + return nil +} + // MergeVariadicComponentDefinition merges multiple variadic component definitions into a single component definition func MergeVariadicComponentDefinition(compDefs ...*oscalTypes.ComponentDefinition) (mergedCompDef *oscalTypes.ComponentDefinition, err error) { for _, compDef := range compDefs { diff --git a/src/pkg/common/oscal/component_test.go b/src/pkg/common/oscal/component_test.go index 410c850ea..a69bf5f04 100644 --- a/src/pkg/common/oscal/component_test.go +++ b/src/pkg/common/oscal/component_test.go @@ -617,3 +617,39 @@ func TestHandleExistingComponent(t *testing.T) { require.Equal(t, 2, len(*component.Model.Components)) }) } + +func TestRewritePaths(t *testing.T) { + // Re-write all paths in the component definition to be relative to project root + + // Define the paths, relative to the current directory + componentRel := "../../../test/unit/common/oscal/valid-component-local-refs.yaml" + rootRel := "../../../../README.md" + + // Calculate the absolute paths + componentDirAbs, err := filepath.Abs(componentRel) + require.NoError(t, err) + componentDirAbs = filepath.Dir(componentDirAbs) + + rootDirAbs, err := filepath.Abs(rootRel) + require.NoError(t, err) + rootDirAbs = filepath.Dir(rootDirAbs) + + componentBytes := loadTestData(t, componentRel) + + var component oscal.ComponentDefinition + err = component.NewModel(componentBytes) + require.NoError(t, err) + + err = component.RewritePaths(componentDirAbs, rootDirAbs) + require.NoError(t, err) + + // Get the expected component definition + expectedComponentBytes := loadTestData(t, "../../../test/unit/common/oscal/valid-component-refs-from-root.yaml") + + var expectedComponent oscalTypes.OscalCompleteSchema + err = yaml.Unmarshal(expectedComponentBytes, &expectedComponent) + require.NoError(t, err) + + // Compare the expected and actual component definitions + require.Equal(t, expectedComponent, *component.GetCompleteModel()) +} diff --git a/src/test/unit/common/oscal/valid-component-local-refs.yaml b/src/test/unit/common/oscal/valid-component-local-refs.yaml new file mode 100644 index 000000000..4ae5f32e4 --- /dev/null +++ b/src/test/unit/common/oscal/valid-component-local-refs.yaml @@ -0,0 +1,56 @@ +# add the descriptions inline +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: OSCAL Demo Tool + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.1 + links: + - href: subdir/basic-profile.yaml + rel: profile + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Defense Unicorns + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: software + title: lula + description: | + Defense Unicorns lula + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + links: + - href: ../../../../../README.md + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: valid-profile.yaml + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + remarks: >- + Here are some remarks about this control. + description: >- + Here is a description + links: + - href: "#88AB3470-B96B-4D7C-BC36-02BF9563C46C" + rel: lula + - href: "../validation/validation.opa.yaml" + rel: lula + - href: "file://../validation/validation.kyverno.yaml" + rel: lula + back-matter: + resources: + - uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C + description: Sample back-matter resource + rlinks: + - href: catalog.yaml \ No newline at end of file diff --git a/src/test/unit/common/oscal/valid-component-refs-from-root.yaml b/src/test/unit/common/oscal/valid-component-refs-from-root.yaml new file mode 100644 index 000000000..ebaab6141 --- /dev/null +++ b/src/test/unit/common/oscal/valid-component-refs-from-root.yaml @@ -0,0 +1,56 @@ +# add the descriptions inline +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: OSCAL Demo Tool + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.1 + links: + - href: src/test/unit/common/oscal/subdir/basic-profile.yaml + rel: profile + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Defense Unicorns + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: software + title: lula + description: | + Defense Unicorns lula + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + links: + - href: README.md + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: src/test/unit/common/oscal/valid-profile.yaml + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + remarks: >- + Here are some remarks about this control. + description: >- + Here is a description + links: + - href: "#88AB3470-B96B-4D7C-BC36-02BF9563C46C" + rel: lula + - href: "src/test/unit/common/validation/validation.opa.yaml" + rel: lula + - href: "src/test/unit/common/validation/validation.kyverno.yaml" + rel: lula + back-matter: + resources: + - uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C + description: Sample back-matter resource + rlinks: + - href: src/test/unit/common/oscal/catalog.yaml \ No newline at end of file From 91734a7f01344440be2c96fd9672110ca0b8e7e1 Mon Sep 17 00:00:00 2001 From: Megan Wolf Date: Wed, 15 Jan 2025 15:55:18 -0500 Subject: [PATCH 8/8] fix: err handling --- src/pkg/common/oscal/component.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index 985a7a919..c1e0ef1e7 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -138,7 +138,10 @@ func (c *ComponentDefinition) RewritePaths(baseDir string, newDir string) error // For pathsToRemap, find all paths in the component defintion and run common.RemapPath on them for _, path := range pathsToRemap { - common.TraverseAndUpdatePaths(c.Model, path, baseDir, newDir) + err := common.TraverseAndUpdatePaths(c.Model, path, baseDir, newDir) + if err != nil { + return err + } } return nil