From c5df8154c7ef1547e4d0f68a994656c1c25ea2ab Mon Sep 17 00:00:00 2001 From: Jennifer Power <barnabei.jennifer@gmail.com> Date: Fri, 14 Feb 2025 17:58:14 -0500 Subject: [PATCH 1/6] chore: add test data with example SSP Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com> --- testdata/component-definition-test.json | 7 + testdata/test-ssp.json | 246 ++++++++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 testdata/test-ssp.json diff --git a/testdata/component-definition-test.json b/testdata/component-definition-test.json index 03138ce..4c51c9b 100644 --- a/testdata/component-definition-test.json +++ b/testdata/component-definition-test.json @@ -80,6 +80,13 @@ "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", "value": "etcd_key_file" } + ], + "statements": [ + { + "statement-id": "CIS-2.1_smt", + "uuid": "cb9219b1-e51c-4680-abb0-616a43bbfbb2", + "description": "" + } ] } ] diff --git a/testdata/test-ssp.json b/testdata/test-ssp.json new file mode 100644 index 0000000..2676da5 --- /dev/null +++ b/testdata/test-ssp.json @@ -0,0 +1,246 @@ +{ + "system-security-plan": { + "uuid": "05bc8eb4-4a8a-4b54-8c10-ee3eba2c401f", + "metadata": { + "title": "Test SSP", + "last-modified": "2023-04-27T15:44:08.070614+10:00", + "version": "0.1.0", + "oscal-version": "1.1.2" + }, + "import-profile": { + "href": "profiles/example/profile.json" + }, + "system-characteristics": { + "system-ids": [ + { + "id": "REPLACE_ME" + } + ], + "system-name": "REPLACE_ME", + "description": "REPLACE_ME", + "security-sensitivity-level": "REPLACE_ME", + "system-information": { + "information-types": [ + { + "title": "REPLACE_ME", + "description": "REPLACE_ME", + "confidentiality-impact": { + "base": "REPLACE_ME" + }, + "integrity-impact": { + "base": "REPLACE_ME" + }, + "availability-impact": { + "base": "REPLACE_ME" + } + } + ] + }, + "security-impact-level": { + "security-objective-confidentiality": "REPLACE_ME", + "security-objective-integrity": "REPLACE_ME", + "security-objective-availability": "REPLACE_ME" + }, + "status": { + "state": "operational" + }, + "authorization-boundary": { + "description": "REPLACE_ME" + } + }, + "system-implementation": { + "users": [ + { + "uuid": "fce50e22-a3c4-4ee6-b397-f4b94e0d5e2d" + } + ], + "components": [ + { + "uuid": "4e19131e-b361-4f0e-8262-02bf4456202e", + "type": "service", + "title": "Example Service", + "description": "An example service for SSP testing", + "props": [ + { + "name": "Rule_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "rule-1", + "remarks": "rule_set_00" + }, + { + "name": "Rule_Description", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "Rule 1 description", + "remarks": "rule_set_00" + }, + { + "name": "Parameter_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "param-1", + "remarks": "rule_set_00" + }, + { + "name": "Parameter_Description", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "Param 1 description", + "remarks": "rule_set_00" + }, + { + "name": "Parameter_Value_Alternatives", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "1, 2, 3", + "remarks": "rule_set_00" + }, + { + "name": "Rule_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "rule-2", + "remarks": "rule_set_01" + }, + { + "name": "Rule_Description", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "Rule 2 description", + "remarks": "rule_set_01" + } + ], + "status": { + "state": "REPLACE_ME" + } + }, + { + "uuid": "ceb0b4b0-8b3c-4e71-8874-57d42c0f36e3", + "type": "this-system", + "title": "This System", + "description": "", + "status": { + "state": "REPLACE_ME" + } + }, + { + "uuid": "701c70f1-482b-42b0-a419-9870158cd9e2", + "type": "validation", + "title": "Validator", + "description": "An example validation component", + "props": [ + { + "name": "Rule_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "rule-1", + "remarks": "rule_set_00" + }, + { + "name": "Rule_Description", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "Rule 1 description", + "remarks": "rule_set_00" + }, + { + "name": "Check_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "check-1", + "remarks": "rule_set_00" + }, + { + "name": "Check_Description", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "Check 1 Description", + "remarks": "rule_set_00" + } + ] + } + ] + }, + "control-implementation": { + "description": "This is an example control implementation for the system.", + "implemented-requirements": [ + { + "uuid": "db7b97db-dadc-4afd-850a-245ca09cb811", + "control-id": "ex-1", + "statements": [ + { + "statement-id": "ex-1_smt", + "uuid": "7ad47329-dc55-4196-a19d-178a8fe7438e", + "by-components": [ + { + "component-uuid": "a95533ab-9427-4abe-820f-0b571bacfe6d", + "uuid": "a64681b2-fbcb-46eb-90fd-0d55aa74ac7c", + "description": "Example 1 Statement Implementation", + "props": [ + { + "name": "Rule_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "rule-1" + } + ] + } + ] + } + ], + "by-components": [ + { + "component-uuid": "4e19131e-b361-4f0e-8262-02bf4456202e", + "uuid": "126b5dcd-30cc-4521-9aa8-5f9f6781a6c4", + "description": "Example 1 implementation", + "props": [ + { + "name": "Rule_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "rule-2" + } + ], + "implementation-status": { + "state": "planned" + } + }, + { + "component-uuid": "ceb0b4b0-8b3c-4e71-8874-57d42c0f36e3", + "uuid": "04deed1d-87c2-4ec6-8a6c-5973511d8758", + "description": "", + "implementation-status": { + "state": "planned" + } + } + ] + }, + { + "uuid": "08e93a77-16e3-4881-9694-e77f114a164b", + "control-id": "ex-2", + "by-components": [ + { + "component-uuid": "4e19131e-b361-4f0e-8262-02bf4456202e", + "uuid": "d93f7198-5ea9-4add-a279-7428098e9b48", + "description": "Example 2 implementation", + "props": [ + { + "name": "Rule_Id", + "ns": "https://oscal-compass.github.io/compliance-trestle/schemas/oscal/cd", + "value": "rule-1" + } + ], + "set-parameters": [ + { + "param-id": "param-1", + "values": [ + "2" + ] + } + ], + "implementation-status": { + "state": "planned" + } + }, + { + "component-uuid": "ceb0b4b0-8b3c-4e71-8874-57d42c0f36e3", + "uuid": "7c045a8d-74c6-4047-a17f-b6661660b332", + "description": "", + "implementation-status": { + "state": "planned" + } + } + ] + } + ] + } + } +} \ No newline at end of file From 20e8e26980224d2d00be577388c5f5470cd766de Mon Sep 17 00:00:00 2001 From: Jennifer Power <barnabei.jennifer@gmail.com> Date: Fri, 14 Feb 2025 18:00:27 -0500 Subject: [PATCH 2/6] feat: add generic implementation of ControlImplementation types To reuse existing Implementation parsing logic implemented for Component Definitions, generic implementation of ControlImplementation, ImplementedRequirement, and Statements are added. Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com> --- models/components/component_definition.go | 188 ++++++++++++++++ .../components/component_definition_test.go | 75 +++++++ models/components/defined.go | 69 ------ models/components/defined_test.go | 41 ---- models/components/doc.go | 2 +- models/components/implementation.go | 50 +++++ models/components/ssp.go | 204 ++++++++++++++++++ models/components/ssp_test.go | 67 ++++++ 8 files changed, 585 insertions(+), 111 deletions(-) create mode 100644 models/components/component_definition.go create mode 100644 models/components/component_definition_test.go delete mode 100644 models/components/defined.go delete mode 100644 models/components/defined_test.go create mode 100644 models/components/implementation.go create mode 100644 models/components/ssp.go create mode 100644 models/components/ssp_test.go diff --git a/models/components/component_definition.go b/models/components/component_definition.go new file mode 100644 index 0000000..6dda888 --- /dev/null +++ b/models/components/component_definition.go @@ -0,0 +1,188 @@ +/* + Copyright 2025 The OSCAL Compass Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package components + +import oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + +// Interface checks for the implementation for Component Definition structures +var ( + _ Component = (*DefinedComponentAdapter)(nil) + _ Implementation = (*ControlImplementationSetAdapter)(nil) + _ Requirement = (*ImplementedRequirementImplementationAdapter)(nil) + _ Statement = (*ControlStatementAdapter)(nil) +) + +const defaultState = "operational" + +// DefinedComponentAdapter wrapped an OSCAL DefinedComponent to +// provide methods for compatibility with Component. +type DefinedComponentAdapter struct { + definedComp oscalTypes.DefinedComponent +} + +// NewDefinedComponentAdapter returns an initialized DefinedComponentAdapter from a given +// DefinedComponent. +func NewDefinedComponentAdapter(definedComponent oscalTypes.DefinedComponent) *DefinedComponentAdapter { + return &DefinedComponentAdapter{ + definedComp: definedComponent, + } +} + +func (d *DefinedComponentAdapter) UUID() string { + return d.definedComp.UUID +} + +func (d *DefinedComponentAdapter) Title() string { + return d.definedComp.Title +} + +func (d *DefinedComponentAdapter) Type() ComponentType { + return ComponentType(d.definedComp.Type) +} + +func (d *DefinedComponentAdapter) AsDefinedComponent() (oscalTypes.DefinedComponent, bool) { + return d.definedComp, true +} + +func (d *DefinedComponentAdapter) AsSystemComponent() (oscalTypes.SystemComponent, bool) { + return oscalTypes.SystemComponent{ + Description: d.definedComp.Description, + Links: d.definedComp.Links, + Props: d.definedComp.Props, + Protocols: d.definedComp.Protocols, + Purpose: d.definedComp.Purpose, + Remarks: d.definedComp.Remarks, + ResponsibleRoles: d.definedComp.ResponsibleRoles, + Status: oscalTypes.SystemComponentStatus{ + State: defaultState, + }, + Title: d.definedComp.Title, + Type: d.definedComp.Type, + UUID: d.definedComp.UUID, + }, true +} + +func (d *DefinedComponentAdapter) Props() []oscalTypes.Property { + if d.definedComp.Props == nil { + return []oscalTypes.Property{} + } + return *d.definedComp.Props +} + +// ControlImplementationSetAdapter wraps an OSCAL ControlImplementationSet to provide +// methods for compatibility with Implementation. +type ControlImplementationSetAdapter struct { + controlImp oscalTypes.ControlImplementationSet +} + +// NewControlImplementationSetAdapter returns an initialized ControlImplementationAdapterSet from a given +// ControlImplementation from an OSCAL Component Definition. +func NewControlImplementationSetAdapter(controlImp oscalTypes.ControlImplementationSet) *ControlImplementationSetAdapter { + return &ControlImplementationSetAdapter{ + controlImp: controlImp, + } +} + +func (c *ControlImplementationSetAdapter) Requirements() []Requirement { + var requirements []Requirement + for _, requirement := range c.controlImp.ImplementedRequirements { + requirementAdapter := NewImplementedRequirementImplementationAdapter(requirement) + requirements = append(requirements, requirementAdapter) + } + return requirements +} + +func (c *ControlImplementationSetAdapter) SetParameters() []oscalTypes.SetParameter { + if c.controlImp.SetParameters == nil { + return []oscalTypes.SetParameter{} + } + return *c.controlImp.SetParameters +} + +func (c *ControlImplementationSetAdapter) Props() []oscalTypes.Property { + if c.controlImp.Props == nil { + return []oscalTypes.Property{} + } + return *c.controlImp.Props +} + +// ImplementedRequirementImplementationAdapter wraps an OSCAL ImplementedRequirementImplementation to provide +// methods for compatibility with Requirement. +type ImplementedRequirementImplementationAdapter struct { + impReq oscalTypes.ImplementedRequirementControlImplementation +} + +// NewImplementedRequirementImplementationAdapter returns an initialized ImplementedRequirementImplementationAdapter from a given +// ImplementedRequirementImplementation from an OSCAL Component Definition. +func NewImplementedRequirementImplementationAdapter(impReq oscalTypes.ImplementedRequirementControlImplementation) *ImplementedRequirementImplementationAdapter { + return &ImplementedRequirementImplementationAdapter{ + impReq: impReq, + } +} + +func (i *ImplementedRequirementImplementationAdapter) ControlID() string { + return i.impReq.ControlId +} + +func (i *ImplementedRequirementImplementationAdapter) UUID() string { + return i.impReq.UUID +} + +func (i *ImplementedRequirementImplementationAdapter) SetParameters() []oscalTypes.SetParameter { + if i.impReq.SetParameters == nil { + return []oscalTypes.SetParameter{} + } + return *i.impReq.SetParameters +} + +func (i *ImplementedRequirementImplementationAdapter) Props() []oscalTypes.Property { + if i.impReq.Props == nil { + return []oscalTypes.Property{} + } + return *i.impReq.Props +} + +func (i *ImplementedRequirementImplementationAdapter) Statements() []Statement { + var statements []Statement + if i.impReq.Statements == nil { + return statements + } + for _, stm := range *i.impReq.Statements { + stmAdapter := NewControlStatementAdapter(stm) + statements = append(statements, stmAdapter) + } + + return statements +} + +// ControlStatementAdapter wraps an OSCAL ControlStatement to provide +// methods for compatibility with Statement. +type ControlStatementAdapter struct { + stm oscalTypes.ControlStatementImplementation +} + +// NewControlStatementAdapter returns an initialized ControlStatementAdapter from a given +// ControlStatement from an OSCAL Component Definition. +func NewControlStatementAdapter(statement oscalTypes.ControlStatementImplementation) *ControlStatementAdapter { + return &ControlStatementAdapter{ + stm: statement, + } +} + +func (c *ControlStatementAdapter) StatementID() string { + return c.stm.StatementId +} + +func (c *ControlStatementAdapter) UUID() string { + return c.stm.UUID +} + +func (c *ControlStatementAdapter) Props() []oscalTypes.Property { + if c.stm.Props == nil { + return []oscalTypes.Property{} + } + return *c.stm.Props +} diff --git a/models/components/component_definition_test.go b/models/components/component_definition_test.go new file mode 100644 index 0000000..137c070 --- /dev/null +++ b/models/components/component_definition_test.go @@ -0,0 +1,75 @@ +/* + Copyright 2025 The OSCAL Compass Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package components + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/oscal-compass/oscal-sdk-go/models" + "github.com/oscal-compass/oscal-sdk-go/validation" +) + +func TestDefinedComponentAdapter(t *testing.T) { + testDataPath := filepath.Join("../../testdata", "component-definition-test.json") + + file, err := os.Open(testDataPath) + require.NoError(t, err) + definition, err := models.NewComponentDefinition(file, validation.NoopValidator{}) + require.NoError(t, err) + require.NotNil(t, definition) + require.NotNil(t, definition.Components) + comps := *definition.Components + require.Len(t, comps, 3) + adapter := NewDefinedComponentAdapter(comps[0]) + require.Equal(t, "TestKubernetes", adapter.Title()) + require.Equal(t, Service, adapter.Type()) + require.Equal(t, "c8106bc8-5174-4e86-91a4-52f2fe0ed027", adapter.UUID()) + require.Len(t, adapter.Props(), 6) + systemComp, ok := adapter.AsSystemComponent() + require.True(t, ok) + require.Equal(t, adapter.UUID(), systemComp.UUID) + definedComp, ok := adapter.AsDefinedComponent() + require.True(t, ok) + require.Equal(t, adapter.UUID(), definedComp.UUID) +} + +func TestControlImplementationSetAdapter(t *testing.T) { + + testDataPath := filepath.Join("../../testdata", "component-definition-test.json") + + file, err := os.Open(testDataPath) + require.NoError(t, err) + definition, err := models.NewComponentDefinition(file, validation.NoopValidator{}) + require.NoError(t, err) + require.NotNil(t, definition) + require.NotNil(t, definition.Components) + comps := *definition.Components + require.Len(t, comps, 3) + + comp := comps[0] + require.NotNil(t, comp.ControlImplementations) + implementations := *comp.ControlImplementations + adapter := NewControlImplementationSetAdapter(implementations[0]) + require.Len(t, adapter.Props(), 0) + require.Len(t, adapter.SetParameters(), 1) + require.Len(t, adapter.Requirements(), 1) + + impReq := adapter.Requirements()[0] + require.Len(t, impReq.SetParameters(), 0) + require.Len(t, impReq.Props(), 2) + require.Equal(t, "a1b5b713-52c7-46fb-ab57-ebac7f576b23", impReq.UUID()) + require.Equal(t, "CIS-2.1", impReq.ControlID()) + require.Len(t, impReq.Statements(), 1) + + statement := impReq.Statements()[0] + require.Len(t, statement.Props(), 0) + require.Equal(t, "cb9219b1-e51c-4680-abb0-616a43bbfbb2", statement.UUID()) + require.Equal(t, "CIS-2.1_smt", statement.StatementID()) +} diff --git a/models/components/defined.go b/models/components/defined.go deleted file mode 100644 index 5c5b681..0000000 --- a/models/components/defined.go +++ /dev/null @@ -1,69 +0,0 @@ -/* - Copyright 2025 The OSCAL Compass Authors - SPDX-License-Identifier: Apache-2.0 -*/ - -package components - -import ( - oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" - - "github.com/oscal-compass/oscal-sdk-go/models" -) - -var _ Component = (*DefinedComponentAdapter)(nil) - -// DefinedComponentAdapter wrapped an OSCAL DefinedComponent to -// provide methods for compatibility with Component. -type DefinedComponentAdapter struct { - definedComp oscalTypes.DefinedComponent -} - -// NewDefinedComponentAdapter returns an initialized DefinedComponentAdapter from a given -// DefinedComponent. -func NewDefinedComponentAdapter(definedComponent oscalTypes.DefinedComponent) *DefinedComponentAdapter { - return &DefinedComponentAdapter{ - definedComp: definedComponent, - } -} - -func (d *DefinedComponentAdapter) UUID() string { - return d.definedComp.UUID -} - -func (d *DefinedComponentAdapter) Title() string { - return d.definedComp.Title -} - -func (d *DefinedComponentAdapter) Type() ComponentType { - return ComponentType(d.definedComp.Type) -} - -func (d *DefinedComponentAdapter) AsDefinedComponent() (oscalTypes.DefinedComponent, bool) { - return d.definedComp, true -} - -func (d *DefinedComponentAdapter) AsSystemComponent() (oscalTypes.SystemComponent, bool) { - return oscalTypes.SystemComponent{ - Description: d.definedComp.Description, - Links: d.definedComp.Links, - Props: d.definedComp.Props, - Protocols: d.definedComp.Protocols, - Purpose: d.definedComp.Purpose, - Remarks: d.definedComp.Remarks, - ResponsibleRoles: d.definedComp.ResponsibleRoles, - Status: oscalTypes.SystemComponentStatus{ - State: models.SampleRequiredString, - }, - Title: d.definedComp.Title, - Type: d.definedComp.Type, - UUID: d.definedComp.UUID, - }, true -} - -func (d *DefinedComponentAdapter) Props() []oscalTypes.Property { - if d.definedComp.Props == nil { - return []oscalTypes.Property{} - } - return *d.definedComp.Props -} diff --git a/models/components/defined_test.go b/models/components/defined_test.go deleted file mode 100644 index f140f02..0000000 --- a/models/components/defined_test.go +++ /dev/null @@ -1,41 +0,0 @@ -/* - Copyright 2025 The OSCAL Compass Authors - SPDX-License-Identifier: Apache-2.0 -*/ - -package components - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/oscal-compass/oscal-sdk-go/models" - "github.com/oscal-compass/oscal-sdk-go/validation" -) - -func TestDefinedComponentAdapter(t *testing.T) { - testDataPath := filepath.Join("../../testdata", "component-definition-test.json") - - file, err := os.Open(testDataPath) - require.NoError(t, err) - definition, err := models.NewComponentDefinition(file, validation.NoopValidator{}) - require.NoError(t, err) - require.NotNil(t, definition) - require.NotNil(t, definition.Components) - comps := *definition.Components - require.Len(t, comps, 3) - adapter := NewDefinedComponentAdapter(comps[0]) - require.Equal(t, "TestKubernetes", adapter.Title()) - require.Equal(t, Service, adapter.Type()) - require.Equal(t, "c8106bc8-5174-4e86-91a4-52f2fe0ed027", adapter.UUID()) - require.Len(t, adapter.Props(), 6) - systemComp, ok := adapter.AsSystemComponent() - require.True(t, ok) - require.Equal(t, adapter.UUID(), systemComp.UUID) - definedComp, ok := adapter.AsDefinedComponent() - require.True(t, ok) - require.Equal(t, adapter.UUID(), definedComp.UUID) -} diff --git a/models/components/doc.go b/models/components/doc.go index 866601c..637c69d 100644 --- a/models/components/doc.go +++ b/models/components/doc.go @@ -4,5 +4,5 @@ */ // Package components defines logic for working with different defined OSCAL component types. -// Supported components type include DefinedComponent and will include SystemComponent. +// This provides a generic implementations of Components and associated Control Implementations. package components diff --git a/models/components/implementation.go b/models/components/implementation.go new file mode 100644 index 0000000..ee73985 --- /dev/null +++ b/models/components/implementation.go @@ -0,0 +1,50 @@ +/* + Copyright 2025 The OSCAL Compass Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package components + +import oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + +// Implementation is an interface representing a generic control implementation for +// a component that is present in an OSCAL Component Definition and an OSCAL SSP in +// different forms. +type Implementation interface { + // Requirements returns a slice of implemented requirements associated to + // the implementation. + Requirements() []Requirement + // SetParameters returns a list of OSCAL set-parameters associated with the implementation. + SetParameters() []oscalTypes.SetParameter + // Props returns a list of OSCAL properties associated with the implementation. + Props() []oscalTypes.Property +} + +// Requirement is an interface representing a generic implemented requirement +// for a component that is present in an OSCAL Component Definition and an OSCAL SSP in +// different forms. +type Requirement interface { + // ControlID returns the associated human-readable identifier for the requirement. + ControlID() string + // UUID returns the requirement assembly UUID + UUID() string + // SetParameters returns a list of OSCAL set-parameters associated with the requirement. + SetParameters() []oscalTypes.SetParameter + // Props returns a list of OSCAL properties associated with the requirement. + Props() []oscalTypes.Property + // Statements returns a slice of statements or implemented requirement parts associated + // with an implemented requirement. + Statements() []Statement +} + +// Statement is an interface representing an implemented statement or +// sub-requirement for a component that is present in an OSCAL Component Definition and an OSCAL SSP in +// different forms. +type Statement interface { + // StatementID returns the associated human-readable identifier for the statement. + StatementID() string + // UUID returns the statement assembly UUID + UUID() string + // Props returns a list of OSCAL properties associated with the statement. + Props() []oscalTypes.Property +} diff --git a/models/components/ssp.go b/models/components/ssp.go new file mode 100644 index 0000000..9f9987d --- /dev/null +++ b/models/components/ssp.go @@ -0,0 +1,204 @@ +/* + Copyright 2025 The OSCAL Compass Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package components + +import oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + +// Interface checks for the implementations for SSP structures +var ( + _ Component = (*SystemComponentAdapter)(nil) + _ Implementation = (*ControlImplementationAdapter)(nil) + _ Requirement = (*ImplementedRequirementAdapter)(nil) + _ Statement = (*StatementAdapter)(nil) +) + +// SystemComponentAdapter wraps an OSCAL SystemComponent to +// provide methods for compatibility with Component. +type SystemComponentAdapter struct { + systemComp oscalTypes.SystemComponent +} + +// NewSystemComponentAdapter returns an initialized SystemComponentAdapter from a given +// SystemComponent. +func NewSystemComponentAdapter(systemComponent oscalTypes.SystemComponent) *SystemComponentAdapter { + return &SystemComponentAdapter{ + systemComp: systemComponent, + } +} + +func (s *SystemComponentAdapter) UUID() string { + return s.systemComp.UUID +} + +func (s *SystemComponentAdapter) Title() string { + return s.systemComp.Title +} + +func (s *SystemComponentAdapter) Type() ComponentType { + return ComponentType(s.systemComp.Type) +} + +func (s *SystemComponentAdapter) AsDefinedComponent() (oscalTypes.DefinedComponent, bool) { + return oscalTypes.DefinedComponent{ + Description: s.systemComp.Description, + Links: s.systemComp.Links, + Props: s.systemComp.Props, + Protocols: s.systemComp.Protocols, + Purpose: s.systemComp.Purpose, + Remarks: s.systemComp.Remarks, + ResponsibleRoles: s.systemComp.ResponsibleRoles, + Title: s.systemComp.Title, + Type: s.systemComp.Type, + UUID: s.systemComp.UUID, + }, true +} + +func (s *SystemComponentAdapter) AsSystemComponent() (oscalTypes.SystemComponent, bool) { + return s.systemComp, true +} + +func (s *SystemComponentAdapter) Props() []oscalTypes.Property { + if s.systemComp.Props == nil { + return []oscalTypes.Property{} + } + return *s.systemComp.Props +} + +// ControlImplementationAdapter wraps an OSCAL ControlImplementation to provide +// methods for compatibility with Implementation. +type ControlImplementationAdapter struct { + controlImp oscalTypes.ControlImplementation +} + +// NewControlImplementationAdapter returns an initialized ControlImplementationAdapter from a given +// ControlImplementation from an OSCAL SSP. +func NewControlImplementationAdapter(controlImp oscalTypes.ControlImplementation) *ControlImplementationAdapter { + return &ControlImplementationAdapter{ + controlImp: controlImp, + } +} + +func (c *ControlImplementationAdapter) Requirements() []Requirement { + var requirements []Requirement + for _, requirement := range c.controlImp.ImplementedRequirements { + requirementAdapter := NewImplementedRequirementAdapter(requirement) + requirements = append(requirements, requirementAdapter) + } + return requirements +} + +func (c *ControlImplementationAdapter) SetParameters() []oscalTypes.SetParameter { + if c.controlImp.SetParameters == nil { + return []oscalTypes.SetParameter{} + } + return *c.controlImp.SetParameters +} + +func (c *ControlImplementationAdapter) Props() []oscalTypes.Property { + // TODO: Where does this go in the SSP? + return []oscalTypes.Property{} +} + +// ImplementedRequirementAdapter wraps an OSCAL ImplementedRequirement to provide +// methods for compatibility with Requirement. +type ImplementedRequirementAdapter struct { + impReq oscalTypes.ImplementedRequirement +} + +// NewImplementedRequirementAdapter returns an initialized ImplementedRequirementAdapter from a given +// ImplementedRequirement from an OSCAL SSP. +func NewImplementedRequirementAdapter(impReq oscalTypes.ImplementedRequirement) *ImplementedRequirementAdapter { + return &ImplementedRequirementAdapter{ + impReq: impReq, + } +} + +func (i *ImplementedRequirementAdapter) ControlID() string { + return i.impReq.ControlId +} + +func (i *ImplementedRequirementAdapter) UUID() string { + return i.impReq.UUID +} + +func (i *ImplementedRequirementAdapter) SetParameters() []oscalTypes.SetParameter { + if i.impReq.SetParameters == nil { + return []oscalTypes.SetParameter{} + } + return *i.impReq.SetParameters +} + +func (i *ImplementedRequirementAdapter) Props() []oscalTypes.Property { + var oscalProps []oscalTypes.Property + if i.impReq.Props != nil { + oscalProps = append(oscalProps, *i.impReq.Props...) + } + + if i.impReq.ByComponents == nil { + + return oscalProps + } + + for _, byComp := range *i.impReq.ByComponents { + if byComp.Props != nil { + oscalProps = append(oscalProps, *byComp.Props...) + } + } + return oscalProps +} + +func (i *ImplementedRequirementAdapter) Statements() []Statement { + var statements []Statement + if i.impReq.Statements == nil { + return statements + } + for _, stm := range *i.impReq.Statements { + stmAdapter := NewStatementAdapter(stm) + statements = append(statements, stmAdapter) + } + + return statements +} + +// StatementAdapter wraps an OSCAL Statement to provide +// methods for compatibility with Statement. +type StatementAdapter struct { + stm oscalTypes.Statement +} + +// NewStatementAdapter returns an initialized StatementAdapter from a given +// Statement from an OSCAL SSP. +func NewStatementAdapter(statement oscalTypes.Statement) *StatementAdapter { + return &StatementAdapter{ + stm: statement, + } +} + +func (s *StatementAdapter) StatementID() string { + return s.stm.StatementId +} + +func (s *StatementAdapter) UUID() string { + return s.stm.UUID +} + +func (s *StatementAdapter) Props() []oscalTypes.Property { + var oscalProps []oscalTypes.Property + if s.stm.Props != nil { + oscalProps = append(oscalProps, *s.stm.Props...) + } + + if s.stm.ByComponents == nil { + return oscalProps + } + + for _, byComp := range *s.stm.ByComponents { + if byComp.Props != nil { + oscalProps = append(oscalProps, *byComp.Props...) + } + } + return oscalProps +} diff --git a/models/components/ssp_test.go b/models/components/ssp_test.go new file mode 100644 index 0000000..3a345d3 --- /dev/null +++ b/models/components/ssp_test.go @@ -0,0 +1,67 @@ +/* + Copyright 2025 The OSCAL Compass Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package components + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/oscal-compass/oscal-sdk-go/models" + "github.com/oscal-compass/oscal-sdk-go/validation" +) + +func TestSystemComponentAdapter(t *testing.T) { + testDataPath := filepath.Join("../../testdata", "test-ssp.json") + + file, err := os.Open(testDataPath) + require.NoError(t, err) + ssp, err := models.NewSystemSecurityPlan(file, validation.NoopValidator{}) + require.NoError(t, err) + require.NotNil(t, ssp) + + require.Len(t, ssp.SystemImplementation.Components, 3) + adapter := NewSystemComponentAdapter(ssp.SystemImplementation.Components[0]) + require.Equal(t, "Example Service", adapter.Title()) + require.Equal(t, Service, adapter.Type()) + require.Equal(t, "4e19131e-b361-4f0e-8262-02bf4456202e", adapter.UUID()) + require.Len(t, adapter.Props(), 7) + systemComp, ok := adapter.AsSystemComponent() + require.True(t, ok) + require.Equal(t, adapter.UUID(), systemComp.UUID) + definedComp, ok := adapter.AsDefinedComponent() + require.True(t, ok) + require.Equal(t, adapter.UUID(), definedComp.UUID) +} + +func TestControlImplementationAdapter(t *testing.T) { + testDataPath := filepath.Join("../../testdata", "test-ssp.json") + + file, err := os.Open(testDataPath) + require.NoError(t, err) + ssp, err := models.NewSystemSecurityPlan(file, validation.NoopValidator{}) + require.NoError(t, err) + require.NotNil(t, ssp) + + adapter := NewControlImplementationAdapter(ssp.ControlImplementation) + require.Len(t, adapter.Requirements(), 2) + require.Len(t, adapter.SetParameters(), 0) + require.Len(t, adapter.Props(), 0) + + impReq := adapter.Requirements()[0] + require.Len(t, impReq.SetParameters(), 0) + require.Len(t, impReq.Props(), 1) + require.Equal(t, "db7b97db-dadc-4afd-850a-245ca09cb811", impReq.UUID()) + require.Equal(t, "ex-1", impReq.ControlID()) + require.Len(t, impReq.Statements(), 1) + + statement := impReq.Statements()[0] + require.Len(t, statement.Props(), 1) + require.Equal(t, "7ad47329-dc55-4196-a19d-178a8fe7438e", statement.UUID()) + require.Equal(t, "ex-1_smt", statement.StatementID()) +} From 95a302744b5f65e8263503a7505606e7e09ccadc Mon Sep 17 00:00:00 2001 From: Jennifer Power <barnabei.jennifer@gmail.com> Date: Fri, 14 Feb 2025 18:04:30 -0500 Subject: [PATCH 3/6] feat: add generic implementation as inputs for Settings creation Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com> --- settings/doc.go | 5 +-- settings/factory.go | 61 ++++++++++++++------------------- settings/factory_test.go | 4 ++- settings/framework.go | 6 ++-- settings/implementation.go | 23 ++++++------- settings/implementation_test.go | 4 ++- settings/settings.go | 7 +++- settings/settings_test.go | 2 +- 8 files changed, 57 insertions(+), 55 deletions(-) diff --git a/settings/doc.go b/settings/doc.go index c5b7041..9339e99 100644 --- a/settings/doc.go +++ b/settings/doc.go @@ -3,6 +3,7 @@ SPDX-License-Identifier: Apache-2.0 */ -// Package settings defines logic relating to processing implementation and -// implemented requirements for framework or requirement specific settings for the usage of RuleSets. +// Package settings defines logic relating to compliance framework specific +// settings for tailoring available component RuleSets for specific implementation or +// assessment. package settings diff --git a/settings/factory.go b/settings/factory.go index 0a358a3..521896e 100644 --- a/settings/factory.go +++ b/settings/factory.go @@ -10,6 +10,7 @@ import ( "github.com/oscal-compass/oscal-sdk-go/extensions" "github.com/oscal-compass/oscal-sdk-go/internal/set" + "github.com/oscal-compass/oscal-sdk-go/models/components" ) // NewSettings returns a new Settings instance with given rules and associated rule parameters. @@ -22,26 +23,23 @@ func NewSettings(rules map[string]struct{}, parameters map[string]string) Settin // NewImplementationSettings returns ImplementationSettings populated with data from an OSCAL Control Implementation // Set and the nested Implemented Requirements. -func NewImplementationSettings(controlImplementation oscalTypes.ControlImplementationSet) *ImplementationSettings { +func NewImplementationSettings(controlImplementation components.Implementation) *ImplementationSettings { implementation := &ImplementationSettings{ implementedReqSettings: make(map[string]Settings), settings: NewSettings(set.New[string](), make(map[string]string)), controlsByRules: make(map[string]set.Set[string]), controlsById: make(map[string]oscalTypes.AssessedControlsSelectControlById), } - if controlImplementation.SetParameters != nil { - setParameters(*controlImplementation.SetParameters, implementation.settings.selectedParameters) - } + setParameters(controlImplementation.SetParameters(), implementation.settings.selectedParameters) - for _, implementedReq := range controlImplementation.ImplementedRequirements { - newRequirementForImplementation(implementedReq, implementation) + for _, requirement := range controlImplementation.Requirements() { + newRequirementForImplementation(requirement, implementation) } return implementation } -// NewAssessmentActivitiesSettings returns a new Setting populated based on data from OSCAL Assessment Plan -// Activities. +// NewAssessmentActivitiesSettings returns a new Setting populate based on data from OSCAL Activities // // The mapping between a RuleSet and Activity is as follows: // Activity -> Rule @@ -71,10 +69,12 @@ func NewAssessmentActivitiesSettings(assessmentActivities []oscalTypes.Activity) } } -// newRequirementForImplementation adds a new Setting to an existing ImplementationSettings and updates all related fields. -func newRequirementForImplementation(implementedReq oscalTypes.ImplementedRequirementControlImplementation, implementation *ImplementationSettings) { +// newRequirementForImplementation adds a new Setting to an existing ImplementationSettings and updates all related +// +// fields. +func newRequirementForImplementation(implementedReq components.Requirement, implementation *ImplementationSettings) { implementedControl := oscalTypes.AssessedControlsSelectControlById{ - ControlId: implementedReq.ControlId, + ControlId: implementedReq.ControlID(), } requirement := settingsFromImplementedRequirement(implementedReq) @@ -85,44 +85,35 @@ func newRequirementForImplementation(implementedReq oscalTypes.ImplementedRequir if !ok { controlSet = set.New[string]() } - controlSet.Add(implementedReq.ControlId) + controlSet.Add(implementedReq.ControlID()) implementation.controlsByRules[mappedRule] = controlSet - implementation.controlsById[implementedReq.ControlId] = implementedControl + implementation.controlsById[implementedReq.ControlID()] = implementedControl implementation.settings.mappedRules.Add(mappedRule) } - implementation.implementedReqSettings[implementedReq.ControlId] = requirement + implementation.implementedReqSettings[implementedReq.ControlID()] = requirement } } // settingsFromImplementedRequirement returns Settings populated with data from an // OSCAL Implemented Requirement. -func settingsFromImplementedRequirement(implementedReq oscalTypes.ImplementedRequirementControlImplementation) Settings { +func settingsFromImplementedRequirement(implementedReq components.Requirement) Settings { requirement := NewSettings(set.New[string](), make(map[string]string)) - if implementedReq.Props != nil { - mappedRulesProps := extensions.FindAllProps(*implementedReq.Props, extensions.WithName(extensions.RuleIdProp)) - for _, mappedRule := range mappedRulesProps { - requirement.mappedRules.Add(mappedRule.Value) - } - } - - if implementedReq.SetParameters != nil { - setParameters(*implementedReq.SetParameters, requirement.selectedParameters) + mappedRulesProps := extensions.FindAllProps(implementedReq.Props(), extensions.WithName(extensions.RuleIdProp)) + for _, mappedRule := range mappedRulesProps { + requirement.mappedRules.Add(mappedRule.Value) } - if implementedReq.Statements != nil { - for _, stm := range *implementedReq.Statements { - if stm.Props != nil { - mappedRulesProps := extensions.FindAllProps(*stm.Props, extensions.WithName(extensions.RuleIdProp)) - if len(mappedRulesProps) == 0 { - continue - } - for _, mappedRule := range mappedRulesProps { - requirement.mappedRules.Add(mappedRule.Value) - } - } + setParameters(implementedReq.SetParameters(), requirement.selectedParameters) + for _, stm := range implementedReq.Statements() { + mappedRulesStmProps := extensions.FindAllProps(stm.Props(), extensions.WithName(extensions.RuleIdProp)) + if len(mappedRulesStmProps) == 0 { + continue + } + for _, mappedRule := range mappedRulesStmProps { + requirement.mappedRules.Add(mappedRule.Value) } } diff --git a/settings/factory_test.go b/settings/factory_test.go index 7bf7612..f309f91 100644 --- a/settings/factory_test.go +++ b/settings/factory_test.go @@ -13,6 +13,7 @@ import ( "github.com/oscal-compass/oscal-sdk-go/extensions" "github.com/oscal-compass/oscal-sdk-go/internal/set" + "github.com/oscal-compass/oscal-sdk-go/models/components" ) func TestSettingsFromImplementedRequirements(t *testing.T) { @@ -109,7 +110,8 @@ func TestSettingsFromImplementedRequirements(t *testing.T) { for _, c := range tests { t.Run(c.name, func(t *testing.T) { - gotSettings := settingsFromImplementedRequirement(c.inputRequirement) + adapter := components.NewImplementedRequirementImplementationAdapter(c.inputRequirement) + gotSettings := settingsFromImplementedRequirement(adapter) require.Equal(t, c.wantSettings, gotSettings) }) } diff --git a/settings/framework.go b/settings/framework.go index 0dda6ba..8bb53eb 100644 --- a/settings/framework.go +++ b/settings/framework.go @@ -13,6 +13,7 @@ import ( oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/oscal-compass/oscal-sdk-go/extensions" + "github.com/oscal-compass/oscal-sdk-go/models/components" ) // GetFrameworkShortName returns the human-readable short name for the control source in a @@ -52,11 +53,12 @@ func Framework(framework string, controlImplementations []oscalTypes.ControlImpl for _, controlImplementation := range controlImplementations { frameworkShortName, found := GetFrameworkShortName(controlImplementation) + implementationAdapter := components.NewControlImplementationSetAdapter(controlImplementation) if found && frameworkShortName == framework { if implementationSettings == nil { - implementationSettings = NewImplementationSettings(controlImplementation) + implementationSettings = NewImplementationSettings(implementationAdapter) } else { - implementationSettings.merge(controlImplementation) + implementationSettings.merge(implementationAdapter) } } } diff --git a/settings/implementation.go b/settings/implementation.go index f7ab8e7..310eac1 100644 --- a/settings/implementation.go +++ b/settings/implementation.go @@ -11,6 +11,7 @@ import ( oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/oscal-compass/oscal-sdk-go/internal/set" + "github.com/oscal-compass/oscal-sdk-go/models/components" ) // ImplementationSettings defines settings for RuleSets defined at the control @@ -79,18 +80,16 @@ func (i *ImplementationSettings) ApplicableControls(ruleId string) ([]oscalTypes // merge another ImplementationSettings into the ImplementationSettings. Existing settings at the // requirements level are also merged. -func (i *ImplementationSettings) merge(inputImplementation oscalTypes.ControlImplementationSet) { - if inputImplementation.SetParameters != nil { - setParameters(*inputImplementation.SetParameters, i.settings.selectedParameters) - } +func (i *ImplementationSettings) merge(inputImplementation components.Implementation) { + setParameters(inputImplementation.SetParameters(), i.settings.selectedParameters) - for _, implementedReq := range inputImplementation.ImplementedRequirements { - requirement, ok := i.implementedReqSettings[implementedReq.ControlId] + for _, requirement := range inputImplementation.Requirements() { + reqSettings, ok := i.implementedReqSettings[requirement.ControlID()] if !ok { - newRequirementForImplementation(implementedReq, i) + newRequirementForImplementation(requirement, i) } else { - inputRequirement := settingsFromImplementedRequirement(implementedReq) + inputRequirement := settingsFromImplementedRequirement(requirement) if len(inputRequirement.mappedRules) == 0 { continue } @@ -100,15 +99,15 @@ func (i *ImplementationSettings) merge(inputImplementation oscalTypes.ControlImp if !ok { controlSet = set.New[string]() } - controlSet.Add(implementedReq.ControlId) + controlSet.Add(requirement.ControlID()) i.controlsByRules[mappedRule] = controlSet i.settings.mappedRules.Add(mappedRule) - requirement.mappedRules.Add(mappedRule) + reqSettings.mappedRules.Add(mappedRule) } for name, value := range inputRequirement.selectedParameters { - requirement.selectedParameters[name] = value + reqSettings.selectedParameters[name] = value } - i.implementedReqSettings[implementedReq.ControlId] = requirement + i.implementedReqSettings[requirement.ControlID()] = reqSettings } } } diff --git a/settings/implementation_test.go b/settings/implementation_test.go index dad280e..934a795 100644 --- a/settings/implementation_test.go +++ b/settings/implementation_test.go @@ -16,6 +16,7 @@ import ( "github.com/oscal-compass/oscal-sdk-go/extensions" "github.com/oscal-compass/oscal-sdk-go/internal/set" "github.com/oscal-compass/oscal-sdk-go/models" + "github.com/oscal-compass/oscal-sdk-go/models/components" "github.com/oscal-compass/oscal-sdk-go/validation" ) @@ -234,7 +235,8 @@ func TestMerge(t *testing.T) { for _, c := range tests { t.Run(c.name, func(t *testing.T) { testSettings := prepSettings(t) - testSettings.merge(c.inputImplementation) + adapter := components.NewControlImplementationSetAdapter(c.inputImplementation) + testSettings.merge(adapter) require.Equal(t, c.wantSettings, *testSettings) }) } diff --git a/settings/settings.go b/settings/settings.go index 5dfd595..6f068b9 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -7,6 +7,7 @@ package settings import ( "context" + "errors" "fmt" "github.com/oscal-compass/oscal-sdk-go/extensions" @@ -14,6 +15,10 @@ import ( "github.com/oscal-compass/oscal-sdk-go/rules" ) +// ErrRulesNotFound defines an error returned when there are not intersecting ruleSet store +// for a component and in the given Settings. +var ErrRulesNotFound = errors.New("no rules found with criteria") + // Settings defines settings for RuleSets to tune options based in the // target baseline or compliance goals. type Settings struct { @@ -64,7 +69,7 @@ func ApplyToComponent(ctx context.Context, componentId string, store rules.Store resolvedRules = append(resolvedRules, ruleSet) } if len(resolvedRules) == 0 { - return []extensions.RuleSet{}, fmt.Errorf("no rules found with criteria for component %s", componentId) + return []extensions.RuleSet{}, fmt.Errorf("component %s: %w", componentId, ErrRulesNotFound) } return resolvedRules, nil } diff --git a/settings/settings_test.go b/settings/settings_test.go index f2e39e6..1ebeebe 100644 --- a/settings/settings_test.go +++ b/settings/settings_test.go @@ -84,7 +84,7 @@ func TestApplyToComponents(t *testing.T) { "doesnotexists": struct{}{}, }, }, - expError: "no rules found with criteria for component testComponent1", + expError: "component testComponent1: no rules found with criteria", }, } From 6285eeb5ec274617c6b8472ebccf0f448fb34206 Mon Sep 17 00:00:00 2001 From: Jennifer Power <barnabei.jennifer@gmail.com> Date: Fri, 14 Feb 2025 18:04:54 -0500 Subject: [PATCH 4/6] feat: add top-level transformer for OSCAL SSP to OSCAL AP Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com> --- transformers/transformer_test.go | 29 ++++++++++++++++++++++++++++- transformers/transformers.go | 22 ++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/transformers/transformer_test.go b/transformers/transformer_test.go index 4d69a14..7118184 100644 --- a/transformers/transformer_test.go +++ b/transformers/transformer_test.go @@ -34,7 +34,6 @@ func TestComponentDefinitionsToAssessmentPlan(t *testing.T) { require.Len(t, *plan.LocalDefinitions.Activities, 2) require.Len(t, *plan.AssessmentAssets.Components, 2) require.Len(t, *plan.AssessmentSubjects, 1) - require.Len(t, *plan.AssessmentAssets.Components, 2) require.Len(t, plan.ReviewedControls.ControlSelections, 1) require.Len(t, *plan.Tasks, 1) tasks := *plan.Tasks @@ -47,3 +46,31 @@ func TestComponentDefinitionsToAssessmentPlan(t *testing.T) { require.Contains(t, activities, "etcd_cert_file") require.Contains(t, activities, "etcd_key_file") } + +func TestSSPToAssessmentPlan(t *testing.T) { + testDataPath := filepath.Join("../testdata", "test-ssp.json") + + file, err := os.Open(testDataPath) + require.NoError(t, err) + ssp, err := models.NewSystemSecurityPlan(file, validation.NoopValidator{}) + require.NoError(t, err) + require.NotNil(t, ssp) + + plan, err := SSPToAssessmentPlan(context.TODO(), *ssp, "importPath") + require.NoError(t, err) + + require.Len(t, *plan.LocalDefinitions.Activities, 2) + require.Len(t, *plan.AssessmentAssets.Components, 1) + require.Len(t, *plan.AssessmentSubjects, 1) + require.Len(t, plan.ReviewedControls.ControlSelections, 1) + require.Len(t, *plan.Tasks, 1) + tasks := *plan.Tasks + require.Len(t, *tasks[0].AssociatedActivities, 2) + + var activities []string + for _, act := range *plan.LocalDefinitions.Activities { + activities = append(activities, act.Title) + } + require.Contains(t, activities, "rule-1") + require.Contains(t, activities, "rule-2") +} diff --git a/transformers/transformers.go b/transformers/transformers.go index 181698d..7f20daa 100644 --- a/transformers/transformers.go +++ b/transformers/transformers.go @@ -41,3 +41,25 @@ func ComponentDefinitionsToAssessmentPlan(ctx context.Context, definitions []osc } return plans.GenerateAssessmentPlan(ctx, allComponents, *implementationSettings) } + +// SSPToAssessmentPlan transforms the data from a System Security Plan at a given import location to a single OSCAL Assessment Plan. +func SSPToAssessmentPlan(ctx context.Context, ssp oscalTypes.SystemSecurityPlan, sspImportPath string) (*oscalTypes.AssessmentPlan, error) { + var allComponents []components.Component + for _, sysComp := range ssp.SystemImplementation.Components { + componentAdapter := components.NewSystemComponentAdapter(sysComp) + // Skip any components that don't have attached rules + // For an SSP, this is likely the "This System" component + if len(componentAdapter.Props()) == 0 || componentAdapter.Title() == "This System" { + continue + } + allComponents = append(allComponents, componentAdapter) + } + implementationAdapter := components.NewControlImplementationAdapter(ssp.ControlImplementation) + implementationSettings := settings.NewImplementationSettings(implementationAdapter) + + if implementationSettings == nil { + return nil, fmt.Errorf("cannot transform ssp for at path %s", sspImportPath) + } + + return plans.GenerateAssessmentPlan(ctx, allComponents, *implementationSettings, plans.WithImport(sspImportPath)) +} From 2b7f2d0b39310f82621c70897f24f2fef5b4ba6d Mon Sep 17 00:00:00 2001 From: Jennifer Power <barnabei.jennifer@gmail.com> Date: Fri, 14 Feb 2025 18:05:19 -0500 Subject: [PATCH 5/6] fix: updates Assessment Platform to remove empty component array Assessment Platform is a required field under Assessment Assests but component are not. This updated that field to ensure there are no empty component lists. Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com> --- models/plans/plan.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/models/plans/plan.go b/models/plans/plan.go index 1c1308b..6a650a6 100644 --- a/models/plans/plan.go +++ b/models/plans/plan.go @@ -115,11 +115,7 @@ func GenerateAssessmentPlan(ctx context.Context, comps []components.Component, i } assessmentAssets := AssessmentAssets(comps) - taskAssessmentSubject := oscalTypes.AssessmentSubject{ - IncludeSubjects: &subjectSelectors, - Type: defaultSubjectType, - } - *ruleBasedTask.Subjects = append(*ruleBasedTask.Subjects, taskAssessmentSubject) + *ruleBasedTask.Subjects = append(*ruleBasedTask.Subjects, oscalTypes.AssessmentSubject{IncludeSubjects: &subjectSelectors}) metadata := models.NewSampleMetadata() metadata.Title = options.title @@ -151,7 +147,7 @@ func newTask() oscalTypes.Task { UUID: uuid.NewUUID(), Title: "Automated Assessment", Type: defaultTaskType, - Description: "Evaluation of defined rules for components.", + Description: "Evaluation of defined rules for applicable comps.", Subjects: &[]oscalTypes.AssessmentSubject{}, AssociatedActivities: &[]oscalTypes.AssociatedActivity{}, } @@ -296,12 +292,20 @@ func AssessmentAssets(comps []components.Component) oscalTypes.AssessmentAssets } } + // AssessmentPlatforms is a required field under AssessmentAssets assessmentPlatform := oscalTypes.AssessmentPlatform{ - UUID: uuid.NewUUID(), - Title: models.SampleRequiredString, - UsesComponents: &usedComponents, + UUID: uuid.NewUUID(), + Title: models.SampleRequiredString, + } + + if len(usedComponents) == 0 { + return oscalTypes.AssessmentAssets{ + AssessmentPlatforms: []oscalTypes.AssessmentPlatform{assessmentPlatform}, + } } + + assessmentPlatform.UsesComponents = &usedComponents assessmentAssets := oscalTypes.AssessmentAssets{ Components: &systemComponents, AssessmentPlatforms: []oscalTypes.AssessmentPlatform{assessmentPlatform}, From c7ab3b95cf172c655e49838617b83be970590881 Mon Sep 17 00:00:00 2001 From: Jennifer Power <barnabei.jennifer@gmail.com> Date: Thu, 6 Mar 2025 20:36:03 -0500 Subject: [PATCH 6/6] fix: updates testdata and logic to fix validation error Creates a NilOrEmpty function to ensure any optional and empty slices are not set. Signed-off-by: Jennifer Power <barnabei.jennifer@gmail.com> --- models/components/ssp_test.go | 2 +- models/plans/plan.go | 22 ++++++++-------- models/utils.go | 14 +++++++++++ models/utils_test.go | 43 ++++++++++++++++++++++++++++++++ testdata/test-ssp.json | 17 ++++++++++--- transformers/transformer_test.go | 14 +++++++++++ 6 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 models/utils.go create mode 100644 models/utils_test.go diff --git a/models/components/ssp_test.go b/models/components/ssp_test.go index 3a345d3..2adfb3b 100644 --- a/models/components/ssp_test.go +++ b/models/components/ssp_test.go @@ -50,7 +50,7 @@ func TestControlImplementationAdapter(t *testing.T) { adapter := NewControlImplementationAdapter(ssp.ControlImplementation) require.Len(t, adapter.Requirements(), 2) - require.Len(t, adapter.SetParameters(), 0) + require.Len(t, adapter.SetParameters(), 1) require.Len(t, adapter.Props(), 0) impReq := adapter.Requirements()[0] diff --git a/models/plans/plan.go b/models/plans/plan.go index 6a650a6..4645b17 100644 --- a/models/plans/plan.go +++ b/models/plans/plan.go @@ -115,7 +115,11 @@ func GenerateAssessmentPlan(ctx context.Context, comps []components.Component, i } assessmentAssets := AssessmentAssets(comps) - *ruleBasedTask.Subjects = append(*ruleBasedTask.Subjects, oscalTypes.AssessmentSubject{IncludeSubjects: &subjectSelectors}) + taskSubjects := oscalTypes.AssessmentSubject{ + IncludeSubjects: &subjectSelectors, + Type: defaultSubjectType, + } + *ruleBasedTask.Subjects = append(*ruleBasedTask.Subjects, taskSubjects) metadata := models.NewSampleMetadata() metadata.Title = options.title @@ -147,7 +151,7 @@ func newTask() oscalTypes.Task { UUID: uuid.NewUUID(), Title: "Automated Assessment", Type: defaultTaskType, - Description: "Evaluation of defined rules for applicable comps.", + Description: "Evaluation of defined rules for components.", Subjects: &[]oscalTypes.AssessmentSubject{}, AssociatedActivities: &[]oscalTypes.AssociatedActivity{}, } @@ -194,7 +198,7 @@ func ActivitiesForComponent(ctx context.Context, targetComponentID string, store Props: &[]oscalTypes.Property{methodProp}, RelatedControls: &relatedControls, Title: rule.Rule.ID, - Steps: &steps, + Steps: models.NilIfEmpty(&steps), } if rule.Rule.Parameter != nil { @@ -295,17 +299,11 @@ func AssessmentAssets(comps []components.Component) oscalTypes.AssessmentAssets // AssessmentPlatforms is a required field under AssessmentAssets assessmentPlatform := oscalTypes.AssessmentPlatform{ - UUID: uuid.NewUUID(), - Title: models.SampleRequiredString, - } - - if len(usedComponents) == 0 { - return oscalTypes.AssessmentAssets{ - AssessmentPlatforms: []oscalTypes.AssessmentPlatform{assessmentPlatform}, - } + UUID: uuid.NewUUID(), + Title: models.SampleRequiredString, + UsesComponents: models.NilIfEmpty(&usedComponents), } - assessmentPlatform.UsesComponents = &usedComponents assessmentAssets := oscalTypes.AssessmentAssets{ Components: &systemComponents, AssessmentPlatforms: []oscalTypes.AssessmentPlatform{assessmentPlatform}, diff --git a/models/utils.go b/models/utils.go new file mode 100644 index 0000000..53a71b7 --- /dev/null +++ b/models/utils.go @@ -0,0 +1,14 @@ +/* + Copyright 2024 The OSCAL Compass Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package models + +// NilIfEmpty returns nil if the slice is empty, otherwise returns the original slice. +func NilIfEmpty[T any](slice *[]T) *[]T { + if slice == nil || len(*slice) == 0 { + return nil + } + return slice +} diff --git a/models/utils_test.go b/models/utils_test.go new file mode 100644 index 0000000..407a7a3 --- /dev/null +++ b/models/utils_test.go @@ -0,0 +1,43 @@ +/* + Copyright 2024 The OSCAL Compass Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package models + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNilIfEmpty(t *testing.T) { + tests := []struct { + name string + slice *[]string + wantSlice *[]string + }{ + { + name: "Valid/NilSlice", + slice: nil, + wantSlice: nil, + }, + { + name: "Valid/EmptySlice", + slice: &[]string{}, + wantSlice: nil, + }, + { + name: "Valid/NonEmptySlice", + slice: &[]string{"test"}, + wantSlice: &[]string{"test"}, + }, + } + + for _, c := range tests { + t.Run(c.name, func(t *testing.T) { + gotSlice := NilIfEmpty(c.slice) + require.Equal(t, c.wantSlice, gotSlice) + }) + } +} diff --git a/testdata/test-ssp.json b/testdata/test-ssp.json index 2676da5..0c61fe7 100644 --- a/testdata/test-ssp.json +++ b/testdata/test-ssp.json @@ -105,7 +105,7 @@ } ], "status": { - "state": "REPLACE_ME" + "state": "operational" } }, { @@ -114,7 +114,7 @@ "title": "This System", "description": "", "status": { - "state": "REPLACE_ME" + "state": "operational" } }, { @@ -147,12 +147,23 @@ "value": "Check 1 Description", "remarks": "rule_set_00" } - ] + ], + "status": { + "state": "operational" + } } ] }, "control-implementation": { "description": "This is an example control implementation for the system.", + "set-parameters": [ + { + "param-id": "param-1", + "values": [ + "2" + ] + } + ], "implemented-requirements": [ { "uuid": "db7b97db-dadc-4afd-850a-245ca09cb811", diff --git a/transformers/transformer_test.go b/transformers/transformer_test.go index 7118184..83f78f4 100644 --- a/transformers/transformer_test.go +++ b/transformers/transformer_test.go @@ -45,6 +45,13 @@ func TestComponentDefinitionsToAssessmentPlan(t *testing.T) { } require.Contains(t, activities, "etcd_cert_file") require.Contains(t, activities, "etcd_key_file") + + // Validate against the schema + validator := validation.NewSchemaValidator() + oscalModels := oscalTypes.OscalModels{ + AssessmentPlan: plan, + } + require.NoError(t, validator.Validate(oscalModels)) } func TestSSPToAssessmentPlan(t *testing.T) { @@ -73,4 +80,11 @@ func TestSSPToAssessmentPlan(t *testing.T) { } require.Contains(t, activities, "rule-1") require.Contains(t, activities, "rule-2") + + // Validate against the schema + validator := validation.NewSchemaValidator() + oscalModels := oscalTypes.OscalModels{ + AssessmentPlan: plan, + } + require.NoError(t, validator.Validate(oscalModels)) }