Skip to content

Commit

Permalink
feat(rules): add rules.Store and in-memory implementation (#12)
Browse files Browse the repository at this point in the history
* feat: add Store interface and default memory-based implementation

The Store interface allows rule information to be pulled from
different types of sources while providing a default implementation
for immediate use.

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

* chore(lint): setting download mode for modules to readonly

Setting this allows validation that go.mod is up to
date in CI

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

* chore: fixes comments on error types under rules

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

* chore: updates Memory with styling fixes

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

* chore: updates license header with copyright info

Formats the license header to be compliant with Apache 2.0
short variant header

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

* fix: corrects the use of errors.Join

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

* chore: adds minor change to indexing component logic

The ruleset does not need to be set in the map until
it is populated

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

* chore: restore Makefile targets

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

* docs: refines Store interface comment for clarity

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

---------

Signed-off-by: Jennifer Power <[email protected]>
  • Loading branch information
jpower432 authored Dec 5, 2024
1 parent 3c288f5 commit 9641ae0
Show file tree
Hide file tree
Showing 17 changed files with 724 additions and 9 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,3 @@ go.work.sum
# IDE things
.idea
.vscode

# OSCAL artifacts
oscal_complete_schema.json
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
run:
# disallows implicit automatic updating of go.mod and fails when any changes to go.mod are needed
modules-download-mode: readonly
linters:
enable:
- errcheck # Checks for unchecked errors
Expand Down
5 changes: 4 additions & 1 deletion extensions/doc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

/*
Package extensions defines types that represent OSCAL-based extensions defined by OSCAL Compass.
Expand Down
7 changes: 5 additions & 2 deletions extensions/rules.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package extensions

// Below are defined oscal.Property names for compass-based extensions
// Below are defined oscal.Property names for compass-based extensions.
const (
RuleIdProp = "Rule_Id"
RuleDescriptionProp = "Rule_Description"
Expand Down
5 changes: 4 additions & 1 deletion generators/doc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

// Package generators generates OSCAL-based objects for use. This includes loading objects from existing content or
// generating sample objects.
Expand Down
5 changes: 4 additions & 1 deletion generators/loader.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package generators

Expand Down
11 changes: 10 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@ module github.com/oscal-compass/oscal-sdk-go

go 1.22.7

require github.com/defenseunicorns/go-oscal v0.6.0
require (
github.com/defenseunicorns/go-oscal v0.6.0
github.com/stretchr/testify v1.9.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/defenseunicorns/go-oscal v0.6.0 h1:eflEKfk7edu4L4kWf6aNQpS94ljfGP8lgWpsPYNtE1Q=
github.com/defenseunicorns/go-oscal v0.6.0/go.mod h1:UHp2yK9ty2mYJDun7oNhbstCq6SAAwP4YGbw9n7uG6o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
7 changes: 7 additions & 0 deletions rules/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

// Package rules defines logic relating to the processing of compass defined Rules.
package rules
25 changes: 25 additions & 0 deletions rules/internal/set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package internal

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

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

// Add adds item into the set s.
func (s Set[T]) Add(item T) {
s[item] = struct{}{}
}

// Has checks if the set contains an item.
func (s Set[T]) Has(item T) bool {
_, ok := s[item]
return ok
}
203 changes: 203 additions & 0 deletions rules/memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package rules

import (
"context"
"errors"
"fmt"

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"
)

var (
// Store interface check
_ Store = (*MemoryStore)(nil)

// ErrRuleNotFound defines an error returned when rule queries fail.
ErrRuleNotFound = errors.New("associated rule object not found")
// ErrComponentsNotFound defines an error returned during MemoryStore creation when the input
// is invalid.
ErrComponentsNotFound = errors.New("no components not found")
)

/*
MemoryStore provides implementation of a memory-based rule.Store.
WARNING: This implementation is not thread safe.
*/
type MemoryStore struct {
// nodes saves the rule ID map keys, which are used with
// the other fields.
nodes map[string]extensions.RuleSet
// ByCheck store a mapping between the checkId and its parent
// ruleId
byCheck map[string]string

// Below contains maps that store information by component and
// component types to form RuleSet with the correct context.

// rulesByComponent stores the component title of any component
// mapped to any relevant rules.
rulesByComponent map[string]Set[string]
// checksByValidationComponent store checkId mapped to validation
// component title to filter check information on rules.
checksByValidationComponent map[string]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{
nodes: make(map[string]extensions.RuleSet),
byCheck: make(map[string]string),
rulesByComponent: make(map[string]Set[string]),
checksByValidationComponent: make(map[string]Set[string]),
}

for _, component := range components {
extractedRules := store.indexComponent(component)
if len(extractedRules) != 0 {
store.rulesByComponent[component.Title] = extractedRules
}
}

return store, nil
}

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

// Catalog all registered check implementations by validation component for filtering in
// `rules.FindByComponent`.
checkIds := NewSet[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)
if !ok {
continue
}

ruleSet, ok := m.nodes[ruleIdProp.Value]
if !ok {
ruleSet = extensions.RuleSet{}
}

// A check may or may not be registered.
placeholderCheck := extensions.Check{}

for prop := range propSet {
switch prop.Name {
case extensions.RuleIdProp:
ruleSet.Rule.ID = prop.Value
case extensions.RuleDescriptionProp:
ruleSet.Rule.Description = prop.Value
case extensions.ParameterIdProp:
if ruleSet.Rule.Parameter == nil {
ruleSet.Rule.Parameter = &extensions.Parameter{}
}
ruleSet.Rule.Parameter.ID = prop.Value
case extensions.ParameterDescriptionProp:
if ruleSet.Rule.Parameter == nil {
ruleSet.Rule.Parameter = &extensions.Parameter{}
}
ruleSet.Rule.Parameter.Description = prop.Value

case extensions.ParameterDefaultProp:
if ruleSet.Rule.Parameter == nil {
ruleSet.Rule.Parameter = &extensions.Parameter{}
}
ruleSet.Rule.Parameter.Value = prop.Value
case extensions.CheckIdProp:
placeholderCheck.ID = prop.Value
case extensions.CheckDescriptionProp:
placeholderCheck.Description = prop.Value
}
}

if placeholderCheck.ID != "" {
ruleSet.Checks = append(ruleSet.Checks, placeholderCheck)
m.byCheck[placeholderCheck.ID] = ruleSet.Rule.ID
}
rules.Add(ruleSet.Rule.ID)
m.nodes[ruleSet.Rule.ID] = ruleSet
}
if len(checkIds) != 0 {
m.checksByValidationComponent[component.Title] = checkIds
}

return rules
}

func (m *MemoryStore) GetByRuleID(_ context.Context, ruleId string) (extensions.RuleSet, error) {
ruleSet, ok := m.nodes[ruleId]
if !ok {
return extensions.RuleSet{}, fmt.Errorf("rule %q: %w", ruleId, ErrRuleNotFound)
}
return ruleSet, nil
}

func (m *MemoryStore) GetByCheckID(ctx context.Context, checkId string) (extensions.RuleSet, error) {
ruleId, ok := m.byCheck[checkId]
if !ok {
return extensions.RuleSet{}, fmt.Errorf("failed to find rule for check %q: %w", checkId, ErrRuleNotFound)
}
return m.GetByRuleID(ctx, ruleId)
}

func (m *MemoryStore) FindByComponent(ctx context.Context, componentId string) ([]extensions.RuleSet, error) {
ruleIds, ok := m.rulesByComponent[componentId]
if !ok {
return nil, fmt.Errorf("failed to find rules for component %q", componentId)
}

var ruleSets []extensions.RuleSet
var errs []error
for ruleId := range ruleIds {
ruleSet, err := m.GetByRuleID(ctx, ruleId)
if err != nil {
errs = append(errs, err)
}

// Make sure we are only returning the relevant checks for this
// component.
if checkIds, ok := m.checksByValidationComponent[componentId]; ok {
filteredChecks := make([]extensions.Check, 0, len(ruleSet.Checks))
for _, check := range ruleSet.Checks {
if checkIds.Has(check.ID) {
filteredChecks = append(filteredChecks, check)
}
}
ruleSet.Checks = filteredChecks
}

ruleSets = append(ruleSets, ruleSet)
}

if len(errs) > 0 {
joinedErr := errors.Join(errs...)
return ruleSets, fmt.Errorf("failed to find rules for component %q: %w", componentId, joinedErr)
}

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 9641ae0

Please sign in to comment.