Skip to content
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
70 changes: 20 additions & 50 deletions layer4/assessment.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package layer4

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"runtime"
"time"
)

Expand All @@ -17,70 +14,39 @@ type Assessment struct {
Applicability []string `yaml:"applicability"`
// Description is a human-readable description of the test
Description string `yaml:"description"`
// Result is true if the test passed
// Result is the overall result of the assessment
Result Result `yaml:"result"`
// Message is the human-readable result of the test
Message string `yaml:"message"`
// Steps is a slice of steps that were executed during the test
Steps []AssessmentStep `yaml:"steps"`
// StepsExecuted is the number of steps that were executed during the test
StepsExecuted int `yaml:"steps-executed,omitempty"`
// Procedures is a slice of assessment procedures that were executed during the test
Procedures []*AssessmentProcedure `yaml:"procedures"`
// RunDuration is the time it took to run the test
RunDuration string `yaml:"run-duration,omitempty"`
// Value is the object that was returned during the test
Value interface{} `yaml:"value,omitempty"`
// Changes is a slice of changes that were made during the test
// Changes is a map of changes that were made during the test
Changes map[string]*Change `yaml:"changes,omitempty"`
}

// AssessmentStep is a function type that inspects the provided targetData and returns a Result with a message.
// The message may be an error string or other descriptive text.
type AssessmentStep func(payload interface{}, c map[string]*Change) (Result, string)

func (as AssessmentStep) String() string {
// Get the function pointer correctly
fn := runtime.FuncForPC(reflect.ValueOf(as).Pointer())
if fn == nil {
return "<unknown function>"
}
return fn.Name()
}

func (as AssessmentStep) MarshalJSON() ([]byte, error) {
return json.Marshal(as.String())
}

func (as AssessmentStep) MarshalYAML() (interface{}, error) {
return as.String(), nil
}

// NewAssessment creates a new Assessment object and returns a pointer to it.
func NewAssessment(requirementId string, description string, applicability []string, steps []AssessmentStep) (*Assessment, error) {
func NewAssessment(requirementId string, description string, applicability []string, procedures []*AssessmentProcedure) (*Assessment, error) {
a := &Assessment{
RequirementId: requirementId,
Description: description,
Applicability: applicability,
Result: NotRun,
Steps: steps,
Procedures: procedures,
}
err := a.precheck()
return a, err
}

// AddStep queues a new step in the Assessment
func (a *Assessment) AddStep(step AssessmentStep) {
a.Steps = append(a.Steps, step)
}

func (a *Assessment) runStep(targetData interface{}, step AssessmentStep) Result {
a.StepsExecuted++
result, message := step(targetData, a.Changes)
a.Result = UpdateAggregateResult(a.Result, result)
a.Message = message
return result
// AddProcedure queues a new procedure in the Assessment
func (a *Assessment) AddProcedure(procedure AssessmentProcedure) {
a.Procedures = append(a.Procedures, &procedure)
}

// Run will execute all steps, halting if any step does not return layer4.Passed.
// Run executes all assessment procedures using the test method.
func (a *Assessment) Run(targetData interface{}, changesAllowed bool) Result {
if a.Result != NotRun {
return a.Result
Expand All @@ -97,11 +63,15 @@ func (a *Assessment) Run(targetData interface{}, changesAllowed bool) Result {
change.Allow()
}
}
for _, step := range a.Steps {
if a.runStep(targetData, step) == Failed {
return Failed

for _, procedure := range a.Procedures {
if procedure.Method == TestMethod {
result := procedure.RunProcedure(targetData, a.Changes)
a.Result = UpdateAggregateResult(a.Result, result)
a.Message = procedure.Message
}
}

a.RunDuration = time.Since(startTime).String()
return a.Result
}
Expand Down Expand Up @@ -142,10 +112,10 @@ func (a *Assessment) RevertChanges() (corrupted bool) {
// precheck verifies that the assessment has all the required fields.
// It returns an error if the assessment is not valid.
func (a *Assessment) precheck() error {
if a.RequirementId == "" || a.Description == "" || a.Applicability == nil || a.Steps == nil || len(a.Applicability) == 0 || len(a.Steps) == 0 {
if a.RequirementId == "" || a.Description == "" || a.Applicability == nil || a.Procedures == nil || len(a.Applicability) == 0 || len(a.Procedures) == 0 {
message := fmt.Sprintf(
"expected all Assessment fields to have a value, but got: requirementId=len(%v), description=len=(%v), applicability=len(%v), steps=len(%v)",
len(a.RequirementId), len(a.Description), len(a.Applicability), len(a.Steps),
"expected all Assessment fields to have a value, but got: requirementId=len(%v), description=len=(%v), applicability=len(%v), procedures=len(%v)",
len(a.RequirementId), len(a.Description), len(a.Applicability), len(a.Procedures),
)
a.Result = Unknown
a.Message = message
Expand Down
114 changes: 114 additions & 0 deletions layer4/assessment_procedure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package layer4

import (
"encoding/json"
"errors"
"fmt"
"reflect"
"runtime"
)

// AssessmentProcedure is a specific outline defining how to assess a Layer 2 control requirement.
type AssessmentProcedure struct {
// Id is the unique identifier of the assessment procedure being executed
Id string `json:"id" yaml:"id"`
// Name is the human-readable name of the procedure.
Name string `json:"name" yaml:"name"`
// Description is a detailed explanation of the procedure.
Description string `json:"description" yaml:"description"`
// Method describe the high-level method used to determine the results of the procedure
Method Method `yaml:"method"`
// Remediation guide is a URL to remediation guidance associated with the control's assessment requirement and this specific assessment procedure.
RemediationGuide string `json:"remediation-guide,omitempty" yaml:"remediation-guide,omitempty"`
// URL to documentation that describes how the assessment procedure evaluates the control requirement.
Documentation string `json:"documentation,omitempty" yaml:"documentation,omitempty"`
// Run is a boolean indicating whether the procedure was run or not. When run is true, result is expected to be present.
Run bool `json:"run" yaml:"run"`
// Message is the human-readable result of the procedure
Message string `json:"message,omitempty" yaml:"message,omitempty"`
// Result is the outcome of the assessment procedure.
// This field must be present if Run is true.
Result Result `json:"result,omitempty" yaml:"result,omitempty"`
// StepsExecuted is the number of steps that were executed during the assessment execution
StepsExecuted int `json:"-" yaml:"-"`
// Steps define logical steps to inspect the provided targetData and returns a Result with a message.
// The message may be an error string or other descriptive text.
Steps []AssessmentStep
}

// NewProcedure creates a new AssessmentProcedure object and returns a pointer to it.
func NewProcedure(id, name, description string, steps []AssessmentStep) (*AssessmentProcedure, error) {
a := &AssessmentProcedure{
Id: id,
Name: name,
Description: description,
Result: NotRun,
Steps: steps,
}
err := a.precheck()
return a, err
}

// AssessmentStep is a function type that inspects the provided targetData and returns a Result with a message.
// The message may be an error string or other descriptive text.
type AssessmentStep func(payload interface{}, c map[string]*Change) (Result, string)

func (as AssessmentStep) String() string {
// Get the function pointer correctly
fn := runtime.FuncForPC(reflect.ValueOf(as).Pointer())
if fn == nil {
return "<unknown function>"
}
return fn.Name()
}

func (as AssessmentStep) MarshalJSON() ([]byte, error) {
return json.Marshal(as.String())
}

func (as AssessmentStep) MarshalYAML() (interface{}, error) {
return as.String(), nil
}

// AddStep queues a new step in the Assessment Procedure
func (a *AssessmentProcedure) AddStep(step AssessmentStep) {
a.Steps = append(a.Steps, step)
}

// RunProcedure executes all assessment steps, halting if any assessment does not return layer4.Passed.
func (a *AssessmentProcedure) RunProcedure(targetData interface{}, changes map[string]*Change) Result {
a.Run = true

err := a.precheck()
if err != nil {
a.Result = Unknown
return a.Result
}
for _, steps := range a.Steps {
if a.runStep(targetData, changes, steps) == Failed {
return Failed
}
}
return a.Result
}

func (a *AssessmentProcedure) runStep(targetData interface{}, changes map[string]*Change, step AssessmentStep) Result {
a.StepsExecuted++
result, message := step(targetData, changes)
a.Result = UpdateAggregateResult(a.Result, result)
a.Message = message
return result
}

// precheck verifies that the assessment procedure has step fields.
// It returns an error if the assessment is not valid.
func (a *AssessmentProcedure) precheck() error {
if len(a.Steps) == 0 {
message := fmt.Sprintf(
"expected all Assessment Procedure fields steps=len(%v)",
len(a.Steps),
)
return errors.New(message)
}
return nil
}
122 changes: 122 additions & 0 deletions layer4/assessment_procedure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package layer4

import (
"testing"
)

func getProcedures() []struct {
testName string
procedure AssessmentProcedure
numberOfSteps int
numberOfStepsToRun int
expectedResult Result
} {
return []struct {
testName string
procedure AssessmentProcedure
numberOfSteps int
numberOfStepsToRun int
expectedResult Result
}{
{
testName: "Assessment with no steps",
procedure: AssessmentProcedure{},
expectedResult: Unknown,
},
{
testName: "Procedure with one step",
procedure: passingProcedure,
numberOfSteps: 1,
numberOfStepsToRun: 1,
expectedResult: Passed,
},
{
testName: "Procedure and two steps",
procedure: failingProcedure,
numberOfSteps: 2,
numberOfStepsToRun: 1,
expectedResult: Failed,
},
{
testName: "Procedure three steps",
procedure: needsReviewProcedure,
numberOfSteps: 3,
numberOfStepsToRun: 3,
expectedResult: NeedsReview,
},
}
}

// TestRun ensures that Run executes all steps, halting if any Procedure does not return Passed
func TestRunProcedure(t *testing.T) {
for _, data := range getProcedures() {
t.Run(data.testName, func(t *testing.T) {
a := data.procedure // copy the procedure to prevent duplicate executions in the next test
result := a.RunProcedure(nil, nil)
if result != a.Result {
t.Errorf("expected match between Run return value (%s) and assessment Result value (%s)", result, data.expectedResult)
}
if a.StepsExecuted != data.numberOfStepsToRun {
t.Errorf("expected to run %d tests, got %d", data.numberOfStepsToRun, a.StepsExecuted)
}
})
}
}

// TestNewStep ensures that NewStep queues a new step in the Assessment
func TestAddStep(t *testing.T) {
for _, test := range getProcedures() {
t.Run(test.testName, func(t *testing.T) {
if len(test.procedure.Steps) != test.numberOfSteps {
t.Errorf("Bad test data: expected to start with %d, got %d", test.numberOfSteps, len(test.procedure.Steps))
}
test.procedure.AddStep(passingAssessmentStep)
if len(test.procedure.Steps) != test.numberOfSteps+1 {
t.Errorf("expected %d, got %d", test.numberOfSteps, len(test.procedure.Steps))
}
})
}
}

// TestRunStep ensures that runStep runs the step and updates the Assessment Procedure
func TestRunStep(t *testing.T) {
stepsTestData := []struct {
testName string
step AssessmentStep
result Result
}{
{
testName: "Failing step",
step: failingAssessmentStep,
result: Failed,
},
{
testName: "Passing step",
step: passingAssessmentStep,
result: Passed,
},
{
testName: "Needs review step",
step: needsReviewAssessmentStep,
result: NeedsReview,
},
{
testName: "Unknown step",
step: unknownAssessmentStep,
result: Unknown,
},
}
for _, test := range stepsTestData {
t.Run(test.testName, func(t *testing.T) {
anyOldProcedure := AssessmentProcedure{}

result := anyOldProcedure.runStep(nil, nil, test.step)
if result != test.result {
t.Errorf("expected %s, got %s", test.result, result)
}
if anyOldProcedure.Result != test.result {
t.Errorf("expected %s, got %s", test.result, anyOldProcedure.Result)
}
})
}
}
Loading
Loading