Skip to content

Commit

Permalink
feat: add rules-to-control mappings to support implementation/require…
Browse files Browse the repository at this point in the history
…ment specific settings (#30)

* feat: add namespace check during rule property search

Adding a trestle-defined namespace check ensures Rule
properties are only processed from trestle authored
documents.

Signed-off-by: Jennifer Power <[email protected]>

* refactor: updates rules.Store for simplification

The All() method is removed from rules.Store to simplify the interface.
The rules.Store interface is for more focused queries.

Indexing is decoupled from the creation of the MemoryStore to
defer indexing until needed.

Signed-off-by: Jennifer Power <[email protected]>

* test: update testdata for a multi validator use case

Signed-off-by: Jennifer Power <[email protected]>

* feat(requirements): adds requirements pkg for control implementation

This change adds support for processing OSCAL Control Implementations
and using that processed data to apply settings and context from requirements to
RuleSet results in a rules.Store implementation. The Settings interface is defined
to allow settings to be applied at the Implementation and Requirement levels.

Signed-off-by: Jennifer Power <[email protected]>

---------

Signed-off-by: Jennifer Power <[email protected]>
  • Loading branch information
jpower432 authored Jan 22, 2025
1 parent a2d1b79 commit cb3a443
Show file tree
Hide file tree
Showing 19 changed files with 1,137 additions and 113 deletions.
58 changes: 58 additions & 0 deletions extensions/props.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package extensions

import (
"strings"

oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
)

// TrestleNameSpace is the generic namespace for trestle-defined property extensions.
const TrestleNameSpace = "https://oscal-compass.github.io/compliance-trestle/schemas/oscal"

// Below are defined oscal.Property names for compass-based extensions.
const (
// RuleIdProp represents the property name for Rule ids.
RuleIdProp = "Rule_Id"
// RuleDescriptionProp represents the property name for Rule descriptions.
RuleDescriptionProp = "Rule_Description"
// CheckIdProp represents the property name for Check ids.
CheckIdProp = "Check_Id"
// CheckDescriptionProp represents the property name for Check descriptions.
CheckDescriptionProp = "Check_Description"
// ParameterIdProp represents the property name for Parameter ids.
ParameterIdProp = "Parameter_Id"
// ParameterDescriptionProp represents the property name for Parameter descriptions.
ParameterDescriptionProp = "Parameter_Description"
// ParameterDefaultProp represents the property name for Parameter default selected values.
ParameterDefaultProp = "Parameter_Value_Default"
// FrameworkProp represents the property name for the control source short name.
FrameworkProp = "Framework_Short_Name"
)

// FindAllProps returns all properties with the given name. If no properties match, nil is returned.
// This function also implicitly checks that the property is a trestle-defined property in the namespace.
func FindAllProps(name string, props []oscalTypes.Property) []oscalTypes.Property {
var matchingProps []oscalTypes.Property
for _, prop := range props {
if prop.Name == name && strings.Contains(prop.Ns, TrestleNameSpace) {
matchingProps = append(matchingProps, prop)
}
}
return matchingProps
}

// GetTrestleProp returned the first property matching the given name and a match is found.
// This function also implicitly checks that the property is a trestle-defined property in the namespace.
func GetTrestleProp(name string, props []oscalTypes.Property) (oscalTypes.Property, bool) {
for _, prop := range props {
if prop.Name == name && strings.Contains(prop.Ns, TrestleNameSpace) {
return prop, true
}
}
return oscalTypes.Property{}, false
}
166 changes: 166 additions & 0 deletions extensions/props_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package extensions

import (
"testing"

oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
"github.com/stretchr/testify/require"
)

func TestGetTrestleProp(t *testing.T) {
tests := []struct {
name string
inputProps []oscalTypes.Property
inputName string
wantProp oscalTypes.Property
wantFound bool
}{
{
name: "Valid/PropFound",
inputName: "testProp1",
inputProps: []oscalTypes.Property{
{
Name: "testProp1",
Value: "testValue",
},
{
Name: "testProp1",
Value: "testValue",
Ns: TrestleNameSpace,
},
},
wantProp: oscalTypes.Property{
Name: "testProp1",
Value: "testValue",
Ns: TrestleNameSpace,
Group: "",
Class: "",
Remarks: "",
},
wantFound: true,
},
{
name: "Valid/PropNotFound",
inputName: "testProp",
inputProps: []oscalTypes.Property{
{
Name: "testProp1",
Value: "testValue",
},
{
Name: "testProp2",
Value: "testValue",
Ns: TrestleNameSpace,
},
},
wantProp: oscalTypes.Property{},
wantFound: false,
},
{
name: "Valid/PropNotFoundNs",
inputName: "testProp1",
inputProps: []oscalTypes.Property{
{
Name: "testProp1",
Value: "testValue",
},
{
Name: "testProp2",
Value: "testValue",
},
},
wantProp: oscalTypes.Property{},
wantFound: false,
},
}

for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
foundProp, found := GetTrestleProp(c.inputName, c.inputProps)
require.Equal(t, c.wantProp, foundProp)
require.Equal(t, c.wantFound, found)
})
}
}

func TestFindAllProps(t *testing.T) {
tests := []struct {
name string
inputName string
inputProps []oscalTypes.Property
wantProps []oscalTypes.Property
}{
{
name: "Valid/PropsFound",
inputName: "testProp1",
inputProps: []oscalTypes.Property{
{
Name: "testProp1",
Value: "testValue1",
Ns: TrestleNameSpace,
},
{
Name: "testProp1",
Value: "testValue2",
Ns: TrestleNameSpace,
},
{
Name: "testProp1",
Value: "testValue3",
},
},
wantProps: []oscalTypes.Property{
{
Name: "testProp1",
Value: "testValue1",
Ns: TrestleNameSpace,
Group: "",
Class: "",
Remarks: "",
},
{
Name: "testProp1",
Value: "testValue2",
Ns: TrestleNameSpace,
Group: "",
Class: "",
Remarks: "",
},
},
},
{
name: "Valid/NoPropsFound",
inputName: "testProp3",
inputProps: []oscalTypes.Property{
{
Name: "testProp1",
Value: "testValue1",
Ns: TrestleNameSpace,
},
{
Name: "testProp1",
Value: "testValue2",
Ns: TrestleNameSpace,
},
{
Name: "testProp1",
Value: "testValue3",
Ns: TrestleNameSpace,
},
},
wantProps: []oscalTypes.Property(nil),
},
}

for _, c := range tests {
t.Run(c.name, func(t *testing.T) {
foundProps := FindAllProps(c.inputName, c.inputProps)
require.Equal(t, c.wantProps, foundProps)
})
}
}
11 changes: 0 additions & 11 deletions extensions/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,6 @@

package extensions

// Below are defined oscal.Property names for compass-based extensions.
const (
RuleIdProp = "Rule_Id"
RuleDescriptionProp = "Rule_Description"
CheckIdProp = "Check_Id"
CheckDescriptionProp = "Check_Description"
ParameterIdProp = "Parameter_Id"
ParameterDescriptionProp = "Parameter_Description"
ParameterDefaultProp = "Parameter_Value_Default"
)

// RuleSet defines a Rule instance with associated
// Check implementation data.
type RuleSet struct {
Expand Down
18 changes: 15 additions & 3 deletions rules/internal/set.go → internal/set/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
SPDX-License-Identifier: Apache-2.0
*/

package internal
package set

// Set represents a set data structure.
type Set[T comparable] map[T]struct{}

// NewSet returns an initialized set.
func NewSet[T comparable]() Set[T] {
// New NewSet returns an initialized set.
func New[T comparable]() Set[T] {
return make(Set[T])
}

Expand All @@ -23,3 +23,15 @@ func (s Set[T]) Has(item T) bool {
_, ok := s[item]
return ok
}

// Intersect returns a new Set representing the intersection
// between two sets.
func (s Set[T]) Intersect(other Set[T]) Set[T] {
newSet := New[T]()
for elem := range s {
if _, ok := other[elem]; ok {
newSet.Add(elem)
}
}
return newSet
}
54 changes: 23 additions & 31 deletions rules/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
oscal112 "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/rules/internal"
"github.com/oscal-compass/oscal-sdk-go/internal/set"
)

var (
Expand All @@ -27,10 +27,8 @@ var (
ErrComponentsNotFound = errors.New("no components not found")
)

/*
MemoryStore provides implementation of a memory-based rule.Store.
WARNING: This implementation is not thread safe.
*/
// MemoryStore implements the Store interface using an in-memory map-based data structure.
// WARNING: This implementation is not thread safe.
type MemoryStore struct {
// nodes saves the rule ID map keys, which are used with
// the other fields.
Expand All @@ -44,48 +42,50 @@ type MemoryStore struct {

// rulesByComponent stores the component title of any component
// mapped to any relevant rules.
rulesByComponent map[string]Set[string]
rulesByComponent map[string]set.Set[string]
// checksByValidationComponent store checkId mapped to validation
// component title to filter check information on rules.
checksByValidationComponent map[string]Set[string]
checksByValidationComponent map[string]set.Set[string]
}

// NewMemoryStoreFromComponents creates a new memory-based rule finder.
func NewMemoryStoreFromComponents(components []oscal112.DefinedComponent) (*MemoryStore, error) {
if len(components) == 0 {
return nil, fmt.Errorf("failed to create memory store from components: %w", ErrComponentsNotFound)
}
store := &MemoryStore{
// NewMemoryStore creates a new memory-based Store.
func NewMemoryStore() *MemoryStore {
return &MemoryStore{
nodes: make(map[string]extensions.RuleSet),
byCheck: make(map[string]string),
rulesByComponent: make(map[string]Set[string]),
checksByValidationComponent: make(map[string]Set[string]),
rulesByComponent: make(map[string]set.Set[string]),
checksByValidationComponent: make(map[string]set.Set[string]),
}
}

// IndexAll indexes rule information from OSCAL Components.
func (m *MemoryStore) IndexAll(components []oscal112.DefinedComponent) error {
if len(components) == 0 {
return fmt.Errorf("failed to index components: %w", ErrComponentsNotFound)
}
for _, component := range components {
extractedRules := store.indexComponent(component)
extractedRules := m.indexComponent(component)
if len(extractedRules) != 0 {
store.rulesByComponent[component.Title] = extractedRules
m.rulesByComponent[component.Title] = extractedRules
}
}

return store, nil
return nil
}

func (m *MemoryStore) indexComponent(component oscal112.DefinedComponent) Set[string] {
rules := NewSet[string]()
func (m *MemoryStore) indexComponent(component oscal112.DefinedComponent) set.Set[string] {
rules := set.New[string]()
if component.Props == nil {
return rules
}

// Catalog all registered check implementations by validation component for filtering in
// `rules.FindByComponent`.
checkIds := NewSet[string]()
checkIds := set.New[string]()

// Each rule set is linked by a group id in the property remarks
byRemarks := groupPropsByRemarks(*component.Props)
for _, propSet := range byRemarks {
ruleIdProp, ok := findProp(extensions.RuleIdProp, propSet)
ruleIdProp, ok := getProp(extensions.RuleIdProp, propSet)
if !ok {
continue
}
Expand Down Expand Up @@ -193,11 +193,3 @@ func (m *MemoryStore) FindByComponent(ctx context.Context, componentId string) (

return ruleSets, nil
}

func (m *MemoryStore) All(ctx context.Context) ([]extensions.RuleSet, error) {
var ruleSets []extensions.RuleSet
for _, rule := range m.nodes {
ruleSets = append(ruleSets, rule)
}
return ruleSets, nil
}
Loading

0 comments on commit cb3a443

Please sign in to comment.