Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add rules-to-control mappings to support implementation/requirement specific settings #30

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading