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(dev): observation print commands #762

Merged
merged 18 commits into from
Nov 7, 2024
Merged
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
1 change: 1 addition & 0 deletions docs/cli-commands/lula_tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Collection of additional commands to make OSCAL easier
* [lula](./lula.md) - Risk Management as Code
* [lula tools compose](./lula_tools_compose.md) - compose an OSCAL component definition
* [lula tools lint](./lula_tools_lint.md) - Validate OSCAL against schema
* [lula tools print](./lula_tools_print.md) - Print Resources or Lula Validation from an Assessment Observation
* [lula tools template](./lula_tools_template.md) - Template an artifact
* [lula tools upgrade](./lula_tools_upgrade.md) - Upgrade OSCAL document to a new version if possible.
* [lula tools uuidgen](./lula_tools_uuidgen.md) - Generate a UUID
Expand Down
58 changes: 58 additions & 0 deletions docs/cli-commands/lula_tools_print.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: lula tools print
description: Lula CLI command reference for <code>lula tools print</code>.
type: docs
---
## lula tools print

Print Resources or Lula Validation from an Assessment Observation

### Synopsis


Prints out data about an OSCAL Observation from the OSCAL Assessment Results model.
Given "--resources", the command will print the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file.
Given "--validation", the command will print the Lula Validation that generated a given observation, as identified by a given observation, assessment results file, and component definition file.


```
lula tools print [flags]
```

### Examples

```

To print resources from lula validation manifest:
lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid <observation-uuid>

To print resources from lula validation manifest to output file:
lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid <observation-uuid> --output-file /path/to/output.json

To print the lula validation that generated a given observation:
lula tools print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid <observation-uuid>

```

### Options

```
-a, --assessment string the path to an assessment-results file
-c, --component string the path to a validation manifest file
-h, --help help for print
-u, --observation-uuid string the observation uuid
-o, --output-file string the path to write the resources json
-r, --resources true if the user is printing resources
-v, --validation true if the user is printing validation
```

### Options inherited from parent commands

```
-l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info")
```

### SEE ALSO

* [lula tools](./lula_tools.md) - Collection of additional commands to make OSCAL easier

5 changes: 3 additions & 2 deletions src/cmd/dev/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import (
"strings"
"time"

"github.com/spf13/cobra"
"sigs.k8s.io/yaml"

"github.com/defenseunicorns/lula/src/config"
"github.com/defenseunicorns/lula/src/pkg/common"
"github.com/defenseunicorns/lula/src/pkg/message"
"github.com/defenseunicorns/lula/src/types"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
)

const STDIN = "0"
Expand Down
24 changes: 6 additions & 18 deletions src/cmd/dev/get-resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package dev
import (
"context"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/defenseunicorns/lula/src/cmd/common"
"github.com/defenseunicorns/lula/src/pkg/message"
"github.com/defenseunicorns/lula/src/types"
"github.com/spf13/cobra"
)

var getResourcesOpts = &flags{}
Expand Down Expand Up @@ -52,7 +52,10 @@ var getResourcesCmd = &cobra.Command{

// do not perform the write if there is nothing to write (likely error)
if collection != nil {
writeResources(collection, getResourcesOpts.OutputFile)
errWrite := types.WriteResources(collection, getResourcesOpts.OutputFile)
if errWrite != nil {
message.Fatalf(errWrite, "error writing resources: %v", err)
}
}

if err != nil {
Expand Down Expand Up @@ -92,18 +95,3 @@ func DevGetResources(ctx context.Context, validationBytes []byte, spinner *messa

return *lulaValidation.DomainResources, nil
}

func writeResources(data types.DomainResources, filepath string) {
jsonData := message.JSONValue(data)

// If a filepath is provided, write the JSON data to the file.
if filepath != "" {
err := os.WriteFile(filepath, []byte(jsonData), 0600) // G306
if err != nil {
message.Fatalf(err, "error writing resource JSON to file: %v", err)
}
} else {
// Else print to stdout
fmt.Println(jsonData)
}
}
216 changes: 216 additions & 0 deletions src/cmd/tools/print.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package tools

import (
"encoding/json"
"fmt"
"os"
"path/filepath"

oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2"
"github.com/spf13/cobra"

"github.com/defenseunicorns/lula/src/pkg/common"
"github.com/defenseunicorns/lula/src/pkg/common/composition"
"github.com/defenseunicorns/lula/src/pkg/common/network"
"github.com/defenseunicorns/lula/src/pkg/common/oscal"
"github.com/defenseunicorns/lula/src/pkg/message"
"github.com/defenseunicorns/lula/src/types"
)

var printHelp = `
To print resources from lula validation manifest:
lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid <observation-uuid>

To print resources from lula validation manifest to output file:
lula tools print --resources --assessment /path/to/assessment.yaml --observation-uuid <observation-uuid> --output-file /path/to/output.json

To print the lula validation that generated a given observation:
lula tools print --validation --component /path/to/component.yaml --assessment /path/to/assessment.yaml --observation-uuid <observation-uuid>
`

var printCmdLong = `
Prints out data about an OSCAL Observation from the OSCAL Assessment Results model.
Given "--resources", the command will print the JSON resources input that were provided to a Lula Validation, as identified by a given observation and assessment results file.
Given "--validation", the command will print the Lula Validation that generated a given observation, as identified by a given observation, assessment results file, and component definition file.
`

func PrintCommand() *cobra.Command {
var (
resources bool // -r --resources
validation bool // -v --validation
assessment string // -a --assessment
observationUuid string // -u --observation-uuid
outputFile string // -o --output-file
component string // -c --component
)

printCmd := &cobra.Command{
Use: "print",
Short: "Print Resources or Lula Validation from an Assessment Observation",
Long: printCmdLong,
Example: printHelp,
RunE: func(cmd *cobra.Command, args []string) error {
assessmentData, err := common.ReadFileToBytes(assessment)
if err != nil {
return fmt.Errorf("invalid assessment file: %v", err)
}

assessmentDir, err := filepath.Abs(filepath.Dir(assessment))
if err != nil {
return fmt.Errorf("error getting assessment directory: %v", err)
}

oscalAssessment, err := oscal.NewAssessmentResults(assessmentData)
if err != nil {
return fmt.Errorf("error creating oscal assessment results model: %v", err)
}

// Print the resources or validation
if resources {
err = PrintResources(oscalAssessment, observationUuid, assessmentDir, outputFile)
if err != nil {
return fmt.Errorf("error printing resources: %v", err)
}
} else if validation {
// Compose the component definition
composer, err := composition.New(composition.WithModelFromLocalPath(component))
if err != nil {
return fmt.Errorf("error creating new composer: %v", err)
}
oscalModel, err := composer.ComposeFromPath(cmd.Context(), component)
if err != nil {
return fmt.Errorf("error composing model: %v", err)
}

// Print the validation
err = PrintValidation(oscalModel.ComponentDefinition, oscalAssessment, observationUuid, outputFile)
if err != nil {
return fmt.Errorf("error printing validation: %v", err)
}
}
return nil
},
}

// Add flags, set logic for flag behavior
printCmd.Flags().BoolVarP(&resources, "resources", "r", false, "true if the user is printing resources")
printCmd.Flags().BoolVarP(&validation, "validation", "v", false, "true if the user is printing validation")
printCmd.MarkFlagsMutuallyExclusive("resources", "validation")

printCmd.Flags().StringVarP(&assessment, "assessment", "a", "", "the path to an assessment-results file")
err := printCmd.MarkFlagRequired("assessment")
if err != nil {
message.Fatal(err, "error initializing print-resources command flag: assessment")
}

printCmd.Flags().StringVarP(&observationUuid, "observation-uuid", "u", "", "the observation uuid")
err = printCmd.MarkFlagRequired("observation-uuid")
if err != nil {
message.Fatal(err, "error initializing required command flag: observation-uuid")
}

printCmd.Flags().StringVarP(&component, "component", "c", "", "the path to a validation manifest file")
printCmd.MarkFlagsRequiredTogether("validation", "component")

printCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to write the resources json")

return printCmd
}

func init() {
toolsCmd.AddCommand(PrintCommand())
}

func PrintResources(assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid, assessmentDir, outputFile string) error {
if assessment == nil {
return fmt.Errorf("assessment is nil")
}

observation, err := oscal.GetObservationByUuid(assessment, observationUuid)
if err != nil {
return err
}

// Get the resources from the remote reference
// TODO: will an observation ever have multiple resource links?
resourceCount := 0
var resource types.DomainResources

if observation.Links == nil {
return fmt.Errorf("observation does not contain a remote reference")
}

for _, link := range *observation.Links {
if link.Rel == "lula.resources" {
resourceCount++
if resourceCount > 1 {
return fmt.Errorf("observation contains multiple remote references, only the first printed")
}

resourceData, err := network.Fetch(link.Href, network.WithBaseDir(assessmentDir))
if err != nil {
return fmt.Errorf("error fetching resource: %v", err)
}

err = json.Unmarshal(resourceData, &resource)
if err != nil {
return fmt.Errorf("error unmarshalling resource: %v", err)
}
}
}

// Write the resources to a file if found
err = types.WriteResources(resource, outputFile)
if err != nil {
return err
}

return nil
}

func PrintValidation(component *oscalTypes_1_1_2.ComponentDefinition, assessment *oscalTypes_1_1_2.AssessmentResults, observationUuid, outputFile string) error {
if component == nil {
return fmt.Errorf("component definition is nil")
}

if assessment == nil {
return fmt.Errorf("assessment results is nil")
}

// Get the observation
observation, err := oscal.GetObservationByUuid(assessment, observationUuid)
if err != nil {
return err
}

// Get the validation
found, validationUuid := oscal.GetProp("validation", oscal.LULA_NAMESPACE, observation.Props)
if !found {
return fmt.Errorf("no validation linked to observation")
}

// Find validation ID in the component definition back matter
resourceMap := make(map[string]string)
if component.BackMatter != nil {
resourceMap = oscal.BackMatterToMap(*component.BackMatter)
}

trimmedId := common.TrimIdPrefix(validationUuid)

// Find the validation in the map
validation, found := resourceMap[trimmedId]
if !found {
return fmt.Errorf("validation not found in component definition")
}

// Print the validation
if outputFile == "" {
message.Printf("%s", validation)
} else {
err = os.WriteFile(outputFile, []byte(validation), 0600)
if err != nil {
return fmt.Errorf("error writing validation to file: %v", err)
}
}
return nil
}
Loading