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: initial work to create assessment results generator #16

Closed
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
213 changes: 213 additions & 0 deletions framework/reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package framework

import (
"context"
"errors"
"fmt"
"time"

"github.com/defenseunicorns/go-oscal/src/pkg/uuid"
oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
"github.com/hashicorp/go-hclog"
"github.com/oscal-compass/compliance-to-policy-go/v2/policy"
"github.com/oscal-compass/oscal-sdk-go/extensions"
"github.com/oscal-compass/oscal-sdk-go/models"
"github.com/oscal-compass/oscal-sdk-go/rules"
"github.com/oscal-compass/oscal-sdk-go/settings"
)

const (
defaultVersion = "0.1.0"
defaultTitle = "Automated Assessment Results"
)

type Reporter struct {
log hclog.Logger
store rules.Store
}

func NewReporter(log hclog.Logger, store rules.Store) *Reporter {
return &Reporter{
store: store,
}
}

type generateOpts struct {
title string
}

func (g *generateOpts) defaults() {
g.title = models.DefaultRequiredString
}

// GenerateOption defineoptions to tune the behavior of
// GenerateAssessmentResults.
type GenerateOption func(opts *generateOpts)

// WithTitle is a GenerateOptions that sets the AssessmentResults title
// in the metadata.
func WithTitle(title string) GenerateOption {
return func(opts *generateOpts) {
opts.title = title
}
}

func (r *Reporter) findControls(implementationSettings settings.ImplementationSettings) oscalTypes.ReviewedControls {

includeControls := []oscalTypes.AssessedControlsSelectControlById{}

for _, controlId := range implementationSettings.AllControls() {
selectedControlById := oscalTypes.AssessedControlsSelectControlById{
ControlId: controlId,
}
includeControls = append(includeControls, selectedControlById)
}

assessedControls := []oscalTypes.AssessedControls{
{
IncludeControls: &includeControls,
},
}

reviewedConrols := oscalTypes.ReviewedControls{
ControlSelections: assessedControls,
}
return reviewedConrols

}

func (r *Reporter) toOscalObservation(observationByCheck policy.ObservationByCheck, ruleSet extensions.RuleSet) (oscalTypes.Observation, error) {

oscalObservation := oscalTypes.Observation{
UUID: uuid.NewUUID(),
Title: observationByCheck.Title,
Description: observationByCheck.Description,
Methods: observationByCheck.Methods,
Collected: observationByCheck.Collected,
}

subjects := make([]oscalTypes.SubjectReference, 0)
for _, subject := range observationByCheck.Subjects {

props := []oscalTypes.Property{
{
Name: "resource-id",
Value: subject.ResourceID,
},
{
Name: "result",
Value: subject.Result.String(),
},
{
Name: "evaluated-on",
Value: subject.EvaluatedOn.String(),
},
{
Name: "reason",
Value: subject.Reason,
},
}

s := oscalTypes.SubjectReference{
SubjectUuid: uuid.NewUUID(),
Title: subject.Title,
Type: subject.Type,
Props: &props,
}
subjects = append(subjects, s)
}
oscalObservation.Subjects = &subjects

relevantEvidences := make([]oscalTypes.RelevantEvidence, 0)
if observationByCheck.RelevantEvidences != nil {
for _, relEv := range observationByCheck.RelevantEvidences {
oscalRelEv := oscalTypes.RelevantEvidence{
Href: relEv.Href,
Description: relEv.Description,
}
relevantEvidences = append(relevantEvidences, oscalRelEv)
}
}
if len(relevantEvidences) > 0 {
oscalObservation.RelevantEvidence = &relevantEvidences
}

props := []oscalTypes.Property{
{
Name: "assessment-rule-id",
Value: ruleSet.Rule.ID,
},
}
oscalObservation.Props = &props

return oscalObservation, nil
}

// Convert PVPResults to OSCAL AsessmentResults
func (r *Reporter) GenerateAssessmentResults(ctx context.Context, planHref string, implementationSettings *settings.ImplementationSettings, results []*policy.PVPResult, opts ...GenerateOption) (oscalTypes.AssessmentResults, error) {

options := generateOpts{}
options.defaults()
for _, opt := range opts {
opt(&options)
}

importAp := oscalTypes.ImportAp{
Href: planHref,
}

metadata := oscalTypes.Metadata{
Title: options.title,
LastModified: time.Now(),
Version: defaultVersion,
OscalVersion: models.OSCALVersion,
}

assessmentResults := oscalTypes.AssessmentResults{
UUID: uuid.NewUUID(),
ImportAp: importAp,
Metadata: metadata,
}

// for each PVPResult.Observation create an OSCAL Observation
oscalObservations := make([]oscalTypes.Observation, 0)
for _, result := range results {

for _, observationByCheck := range result.ObservationsByCheck {
rule, err := r.store.GetByCheckID(ctx, observationByCheck.CheckID)
if err != nil {
if !errors.Is(err, rules.ErrRuleNotFound) {
return assessmentResults, fmt.Errorf("failed to convert observation for check: %w", err)
}
}

obs, err := r.toOscalObservation(observationByCheck, rule)
if err != nil {
return assessmentResults, fmt.Errorf("failed to convert observation for check: %w", err)
}
oscalObservations = append(oscalObservations, obs)
}
}

reviewedConrols := r.findControls(*implementationSettings)

oscalResults := []oscalTypes.Result{
{
UUID: uuid.NewUUID(),
Title: "Automated Assessment Result",
Description: "Assessment Results Automatically Genererated from PVP Results",
Start: time.Now(),
ReviewedControls: reviewedConrols,
Observations: &oscalObservations,
},
}
assessmentResults.Results = oscalResults

return assessmentResults, nil

}
111 changes: 111 additions & 0 deletions framework/reporter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright 2024 The OSCAL Compass Authors
SPDX-License-Identifier: Apache-2.0
*/

package framework

import (
"context"
"os"
"path/filepath"
"testing"

oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
"github.com/oscal-compass/compliance-to-policy-go/v2/policy"
"github.com/oscal-compass/oscal-sdk-go/generators"
"github.com/oscal-compass/oscal-sdk-go/models/components"
"github.com/oscal-compass/oscal-sdk-go/rules"
"github.com/oscal-compass/oscal-sdk-go/settings"
"github.com/stretchr/testify/require"
)

func TestGenereateAssessmentResults(t *testing.T) {

pvpResults := []*policy.PVPResult{
{
ObservationsByCheck: []policy.ObservationByCheck{
{
Title: "etcd_cert_file",
Description: "Ensure that the --cert-file argument is set as appropriate",
CheckID: "etcd_cert_file",
Methods: []string{"test_method_1"},
Subjects: []policy.Subject{{Title: "test_subject_1"}},
},
},
Links: []policy.Link{
{
Href: "https:...",
Description: "test_link_1",
},
},
},
}

compDef := readCompDef(t)

r := Reporter{
store: prepMemoryStore(t, compDef),
}

implementationSettings := prepImplementationSettings(t, compDef)

ar, err := r.GenerateAssessmentResults(context.TODO(), "https://...", &implementationSettings, pvpResults)
require.NoError(t, err)

require.Len(t, ar.Results, 1)

require.Len(t, *ar.Results[0].Observations, 1)

oscalObs := *ar.Results[0].Observations
require.Equal(t, oscalObs[0].Title, pvpResults[0].ObservationsByCheck[0].Title)

}

// Load test component definition JSON
func readCompDef(t *testing.T) oscalTypes.ComponentDefinition {
testDataPath := filepath.Join("../test/testdata", "component-definition-test.json")

file, err := os.Open(testDataPath)
require.NoError(t, err)

definition, err := generators.NewComponentDefinition(file)
require.NoError(t, err)
require.NotNil(t, definition)

return *definition
}

// Create a memory store using test compdef
func prepMemoryStore(t *testing.T, testComp oscalTypes.ComponentDefinition) *rules.MemoryStore {

testMemoryStore := rules.NewMemoryStore()

var comps []components.Component
for _, cp := range *testComp.Components {
adapters := components.NewDefinedComponentAdapter(cp)
comps = append(comps, adapters)
}
err := testMemoryStore.IndexAll(comps)
require.NoError(t, err)

return testMemoryStore
}

// Create implementation settings using test compdef
func prepImplementationSettings(t *testing.T, testComp oscalTypes.ComponentDefinition) settings.ImplementationSettings {

var allImplementations []oscalTypes.ControlImplementationSet
for _, component := range *testComp.Components {
if component.ControlImplementations == nil {
continue
}
allImplementations = append(allImplementations, *component.ControlImplementations...)
}

implementationSettings, err := settings.Framework("cis", allImplementations)
require.NoError(t, err)

return *implementationSettings

}
Loading