Skip to content

Commit 0057253

Browse files
committed
feat: adds transformers from OSCAL AP to OSCAL AR
Signed-off-by: Jennifer Power <[email protected]>
1 parent 521b82a commit 0057253

File tree

7 files changed

+655
-1
lines changed

7 files changed

+655
-1
lines changed

extensions/props.go

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ const (
3535
// TestParameterClass represents the property class for all test parameters
3636
// in OSCAL Activity types in Assessment Plans.
3737
TestParameterClass = "test-parameter"
38+
// AssessmentRuleIdProp represent the property name for a rule associated to an OSCAL
39+
// Observation.
40+
AssessmentRuleIdProp = "assessment-rule-id"
3841
)
3942

4043
type findOptions struct {

internal/results/doc.go

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/*
2+
Copyright 2025 The OSCAL Compass Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
// Package results defines logic for working with OSCAL Assessment Results.
7+
package results

internal/results/observations.go

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
Copyright 2025 The OSCAL Compass Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package results
7+
8+
import (
9+
"github.com/defenseunicorns/go-oscal/src/pkg/uuid"
10+
oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
11+
12+
"github.com/oscal-compass/oscal-sdk-go/extensions"
13+
)
14+
15+
// observationsManager indexes and manages OSCAL Observations
16+
// to support Assessment Result generation.
17+
type observationsManager struct {
18+
observationsByCheck map[string]oscalTypes.Observation
19+
actorsByCheck map[string]string
20+
}
21+
22+
// newObservationManager create an observationManager struct loaded with
23+
// actor information from the Assessment Plan Assessment Assets.
24+
func newObservationManager(plan oscalTypes.AssessmentPlan) *observationsManager {
25+
// Index validation components to set the Actor information
26+
m := &observationsManager{
27+
observationsByCheck: make(map[string]oscalTypes.Observation),
28+
actorsByCheck: make(map[string]string),
29+
}
30+
if plan.AssessmentAssets != nil && plan.AssessmentAssets.Components != nil {
31+
for _, comp := range *plan.AssessmentAssets.Components {
32+
if comp.Props == nil {
33+
continue
34+
}
35+
checkProps := extensions.FindAllProps(*comp.Props, extensions.WithName(extensions.CheckIdProp))
36+
for _, check := range checkProps {
37+
m.actorsByCheck[check.Value] = comp.UUID
38+
}
39+
}
40+
}
41+
return m
42+
}
43+
44+
// load indexing and updates a set of given observations.
45+
func (o *observationsManager) load(observations []oscalTypes.Observation) {
46+
for _, observation := range observations {
47+
o.updateObservation(&observation)
48+
}
49+
}
50+
51+
// createOrGet return an existing observation or a newly created one.
52+
func (o *observationsManager) createOrGet(checkId string) oscalTypes.Observation {
53+
observation, ok := o.observationsByCheck[checkId]
54+
if ok {
55+
return observation
56+
}
57+
58+
emptyObservation := oscalTypes.Observation{
59+
UUID: uuid.NewUUID(),
60+
Title: checkId,
61+
}
62+
o.updateObservation(&emptyObservation)
63+
return emptyObservation
64+
}
65+
66+
// updateObservation with Origin Actor information
67+
func (o *observationsManager) updateObservation(observation *oscalTypes.Observation) {
68+
actor, found := o.actorsByCheck[observation.Title]
69+
if found {
70+
origins := []oscalTypes.Origin{
71+
{
72+
Actors: []oscalTypes.OriginActor{
73+
{
74+
Type: defaultActor,
75+
ActorUuid: actor,
76+
},
77+
},
78+
},
79+
}
80+
observation.Origins = &origins
81+
}
82+
o.observationsByCheck[observation.Title] = *observation
83+
}

internal/results/results.go

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
Copyright 2025 The OSCAL Compass Authors
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package results
7+
8+
import (
9+
"fmt"
10+
"time"
11+
12+
"github.com/defenseunicorns/go-oscal/src/pkg/uuid"
13+
oscalTypes "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
14+
15+
"github.com/oscal-compass/oscal-sdk-go/extensions"
16+
"github.com/oscal-compass/oscal-sdk-go/models"
17+
)
18+
19+
const defaultActor = "tool"
20+
21+
type generateOpts struct {
22+
title string
23+
importAP string
24+
observations []oscalTypes.Observation
25+
}
26+
27+
func (g *generateOpts) defaults() {
28+
g.title = models.SampleRequiredString
29+
g.importAP = models.SampleRequiredString
30+
}
31+
32+
// GenerateOption defines an option to tune the behavior of the
33+
// GenerateAssessmentPlan function.
34+
type GenerateOption func(opts *generateOpts)
35+
36+
// WithTitle is a GenerateOption that sets the AssessmentPlan title
37+
// in the metadata.
38+
func WithTitle(title string) GenerateOption {
39+
return func(opts *generateOpts) {
40+
opts.title = title
41+
}
42+
}
43+
44+
// WithImport is a GenerateOption that sets the AssessmentPlan
45+
// ImportAP Href value.
46+
func WithImport(importAP string) GenerateOption {
47+
return func(opts *generateOpts) {
48+
opts.importAP = importAP
49+
}
50+
}
51+
52+
// WithObservations is a GenerateOption that adds pre-processed OSCAL Observations
53+
// to Assessment Results for associated to Assessment Plan Activities.
54+
func WithObservations(observations []oscalTypes.Observation) GenerateOption {
55+
return func(opts *generateOpts) {
56+
opts.observations = observations
57+
}
58+
}
59+
60+
// GenerateAssessmentResults generates an AssessmentPlan for a set of Components and ImplementationSettings. The chosen inputs allow an Assessment Plan to be generated from
61+
// a set of OSCAL ComponentDefinitions or a SystemSecurityPlan.
62+
//
63+
// If `WithImport` is not set, all input components are set as Components in the Local Definitions.
64+
// If `WithObservations is not set, default behavior is to create a new, empty Observation for each activity step with the step.Title as the
65+
// Observation title.
66+
func GenerateAssessmentResults(plan oscalTypes.AssessmentPlan, opts ...GenerateOption) (*oscalTypes.AssessmentResults, error) {
67+
options := generateOpts{}
68+
options.defaults()
69+
for _, opt := range opts {
70+
opt(&options)
71+
}
72+
73+
metadata := models.NewSampleMetadata()
74+
metadata.Title = options.title
75+
76+
assessmentResults := &oscalTypes.AssessmentResults{
77+
UUID: uuid.NewUUID(),
78+
ImportAp: oscalTypes.ImportAp{
79+
Href: options.importAP,
80+
},
81+
Metadata: metadata,
82+
Results: make([]oscalTypes.Result, 0), // Required field
83+
}
84+
85+
if plan.Tasks == nil {
86+
return assessmentResults, fmt.Errorf("assessment plan tasks cannot be empty")
87+
}
88+
tasks := *plan.Tasks
89+
90+
observationManager := newObservationManager(plan)
91+
if options.observations != nil {
92+
observationManager.load(options.observations)
93+
}
94+
95+
activitiesByUUID := make(map[string]oscalTypes.Activity)
96+
if plan.LocalDefinitions != nil || plan.LocalDefinitions.Activities != nil {
97+
for _, activity := range *plan.LocalDefinitions.Activities {
98+
activitiesByUUID[activity.UUID] = activity
99+
}
100+
}
101+
102+
// Process each task in the assessment plan
103+
for _, task := range tasks {
104+
result := oscalTypes.Result{
105+
Title: fmt.Sprintf("Result For Task %q", task.Title),
106+
Description: fmt.Sprintf("OSCAL Assessment Result For Task %q", task.Title),
107+
Start: time.Now(),
108+
UUID: uuid.NewUUID(),
109+
}
110+
111+
// Some initial checks before proceeding with the rest
112+
if task.AssociatedActivities == nil {
113+
assessmentResults.Results = append(assessmentResults.Results, result)
114+
continue
115+
}
116+
117+
// Observations associated to the tasks found through
118+
// checks.
119+
var reviewedControls oscalTypes.ReviewedControls
120+
var associatedObservations []oscalTypes.Observation
121+
for _, assocActivity := range *task.AssociatedActivities {
122+
activity := activitiesByUUID[assocActivity.ActivityUuid]
123+
124+
if activity.RelatedControls != nil {
125+
reviewedControls.ControlSelections = append(reviewedControls.ControlSelections, activity.RelatedControls.ControlSelections...)
126+
}
127+
128+
if activity.Steps != nil {
129+
relatedTask := oscalTypes.RelatedTask{
130+
TaskUuid: task.UUID,
131+
Subjects: &assocActivity.Subjects,
132+
}
133+
134+
// Activity Title == Rule
135+
// One Observation per Activity Step
136+
// Observation Title == Check
137+
for _, step := range *activity.Steps {
138+
observation := observationManager.createOrGet(step.Title)
139+
140+
if activity.Props != nil {
141+
methods := extensions.FindAllProps(*activity.Props, extensions.WithName("method"), extensions.WithNamespace(""))
142+
for _, method := range methods {
143+
observation.Methods = append(observation.Methods, method.Value)
144+
}
145+
}
146+
147+
if observation.Origins != nil && len(*observation.Origins) == 1 {
148+
origin := *observation.Origins
149+
if origin[0].RelatedTasks == nil {
150+
origin[0].RelatedTasks = &[]oscalTypes.RelatedTask{}
151+
}
152+
*origin[0].RelatedTasks = append(*origin[0].RelatedTasks, relatedTask)
153+
}
154+
associatedObservations = append(associatedObservations, observation)
155+
}
156+
}
157+
}
158+
159+
result.ReviewedControls = reviewedControls
160+
if len(associatedObservations) > 0 {
161+
result.Observations = &associatedObservations
162+
}
163+
assessmentResults.Results = append(assessmentResults.Results, result)
164+
}
165+
166+
return assessmentResults, nil
167+
}

0 commit comments

Comments
 (0)