Skip to content

Commit

Permalink
feat(generate): support for profile model and basic generation (#694)
Browse files Browse the repository at this point in the history
* feat(generate): support for profile model and basic generation

* feat(profile): further refine the profile model abstraction

* feat(generate): profile model unit tests

* fix(generation): fix complete model tests

* fix(profile): working in the mode interface

* fix(profile): support for profile instantiation

* feat(generate): add testing for generate profile

* fix(generate): cleanup and generate cli docs

* fix(docs): fix typo in cli generated docs

* fix(generate): update comments

* fix(generate): cleanup other logic and comments

* fix(generation): update docs, cleanup, deduplicate

* fix(generate): move profile to separate file

* fix(generation): support for merging profiles and replacing imports

* feat(generate): support for includeall in the import

* fix(generate): update golden test

* fix(generate): cover edge case for no specified options

* fix(oscal): fix improperly named import type

* fix(generation): resolve gosec findings
  • Loading branch information
brandtkeller authored Oct 18, 2024
1 parent 41bce03 commit cb4fc6f
Show file tree
Hide file tree
Showing 22 changed files with 951 additions and 100 deletions.
1 change: 1 addition & 0 deletions docs/cli-commands/lula_generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ Generate a specified compliance artifact template

* [lula](./lula.md) - Risk Management as Code
* [lula generate component](./lula_generate_component.md) - Generate a component definition OSCAL template
* [lula generate profile](./lula_generate_profile.md) - Generate a profile OSCAL template

54 changes: 54 additions & 0 deletions docs/cli-commands/lula_generate_profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
title: lula generate profile
description: Lula CLI command reference for <code>lula generate profile</code>.
type: docs
---
## lula generate profile

Generate a profile OSCAL template

### Synopsis

Generation of a Profile OSCAL artifact with controls included or excluded from a source catalog/profile.

```
lula generate profile [flags]
```

### Examples

```
To generate a profile with included controls:
lula generate profile -s <catalog/profile source> -i ac-1,ac-2,ac-3
To specify the name and filetype of the generated artifact:
lula generate profile -s <catalog/profile source> -i ac-1,ac-2,ac-3 -o my_profile.yaml
To generate a profile that includes all controls except a list specified controls:
lula generate profile -s <catalog/profile source> -e ac-1,ac-2,ac-3
```

### Options

```
-a, --all Include all controls from the source catalog/profile
-e, --exclude strings comma delimited list of controls to exclude from the source catalog/profile
-h, --help help for profile
-i, --include strings comma delimited list of controls to include from the source catalog/profile
-o, --output-file string the path to the output file. If not specified, the output file will be directed to stdout
-s, --source string the path to the source catalog/profile
```

### Options inherited from parent commands

```
-f, --input-file string Path to a manifest file
-l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info")
```

### SEE ALSO

* [lula generate](./lula_generate.md) - Generate a specified compliance artifact template

2 changes: 1 addition & 1 deletion docs/oscal/component-definition.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,5 @@ Lula performs a match on the component title and the provided catalog source to
## Example

```bash
./bin/lula generate component -c https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json -r ac-1,ac-3,ac-3.2,ac-4 -o oscal-component.yaml --remarks assessment-objective
lula generate component -c https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json -r ac-1,ac-3,ac-3.2,ac-4 -o oscal-component.yaml --remarks assessment-objective
```
31 changes: 31 additions & 0 deletions docs/oscal/profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Profile

A [Profile](https://pages.nist.gov/OSCAL/resources/concepts/layer/control/profile/) is an OSCAL model for capturing a baseline of selected controls from one or more catalogs or profiles. In lula, the profile model is available for generation and use as a source to other models that allow for specification of a profile or catalog which represents the source of truth for relevant security controls or other organizational policies.

## Structure

The primary structure for the Lula production and operations of the `profile` model for determinism is as follows:
- Imports are sorted by `href` in ascending order
- WithIds are sorted by the associated string id in ascending order
- Back Matter Resources are sorted by `title` in ascending order (Case Sensitive Sorting).

### Reproducibility

The `lula generate` commands are meant to be reproducible. The intent for this generation is to make it easy to update a given model with automation and only inject human intervention as needed.

For profiles, see the metadata props for the `generation` prop. It should look like the following:
```yaml
props:
- name: generation
ns: https://docs.lula.dev/oscal/ns
value: lula generate profile --source catalog.yaml --include ac-1,ac-2,ac-3
```
>[!NOTE]
>The controls specified for inclusion or exclusion during the generation command are not currently validated to exist in the source artifact.
## Example
```bash
lula generate profile -s catalog.yaml -i ac-1,ac-2,ac-3
```
1 change: 1 addition & 0 deletions src/cmd/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func init() {
common.InitViper()

generateCmd.AddCommand(generateComponentCmd)
generateCmd.AddCommand(GenerateProfileCommand())
// generateCmd.AddCommand(generateAssessmentPlanCmd)
// generateCmd.AddCommand(generateSystemSecurityPlanCmd)
// generateCmd.AddCommand(generatePOAMCmd)
Expand Down
96 changes: 96 additions & 0 deletions src/cmd/generate/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package generate

import (
"fmt"
"strings"

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

var profileExample = `
To generate a profile with included controls:
lula generate profile -s <catalog/profile source> -i ac-1,ac-2,ac-3
To specify the name and filetype of the generated artifact:
lula generate profile -s <catalog/profile source> -i ac-1,ac-2,ac-3 -o my_profile.yaml
To generate a profile that includes all controls except a list specified controls:
lula generate profile -s <catalog/profile source> -e ac-1,ac-2,ac-3
`

var profileLong = `Generation of a Profile OSCAL artifact with controls included or excluded from a source catalog/profile.`

func GenerateProfileCommand() *cobra.Command {
var (
source string
outputFile string
include []string
exclude []string
all bool
)

profilecmd := &cobra.Command{
Use: "profile",
Aliases: []string{"p"},
Args: cobra.MaximumNArgs(1),
Short: "Generate a profile OSCAL template",
Long: profileLong,
Example: profileExample,
RunE: func(cmd *cobra.Command, args []string) error {
message.Info("generate profile executed")

if outputFile == "" {
outputFile = "profile.yaml"
}

/// Check if output file contains a valid OSCAL model
_, err := oscal.ValidOSCALModelAtPath(outputFile)
if err != nil {
return fmt.Errorf("invalid OSCAL model at output: %v", err)
}

command := fmt.Sprintf("%s --source %s", cmd.CommandPath(), source)

if len(include) > 0 {
command += fmt.Sprintf(" --include %s", strings.Join(include, ","))
}

if len(exclude) > 0 {
command += fmt.Sprintf(" --exclude %s", strings.Join(exclude, ","))
}

if all {
command += " --all"
}

profile, err := oscal.GenerateProfile(command, source, include, exclude, all)
if err != nil {
return err
}

// Write the component definition to file
err = oscal.WriteOscalModelNew(outputFile, profile)
if err != nil {
message.Fatalf(err, "error writing component to file")
}

return nil
},
}

profilecmd.Flags().StringVarP(&source, "source", "s", "", "the path to the source catalog/profile")
err := profilecmd.MarkFlagRequired("source")
if err != nil {
message.Fatal(err, "error initializing upgrade command flags")
}
profilecmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be directed to stdout")
profilecmd.Flags().StringSliceVarP(&include, "include", "i", []string{}, "comma delimited list of controls to include from the source catalog/profile")
profilecmd.Flags().StringSliceVarP(&exclude, "exclude", "e", []string{}, "comma delimited list of controls to exclude from the source catalog/profile")
profilecmd.Flags().BoolVarP(&all, "all", "a", false, "Include all controls from the source catalog/profile")
profilecmd.MarkFlagsMutuallyExclusive("include", "exclude", "all")
profilecmd.MarkFlagsOneRequired("include", "exclude", "all")

return profilecmd
}
13 changes: 13 additions & 0 deletions src/pkg/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package common
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -191,6 +192,18 @@ func ValidationFromString(raw, uuid string) (validation types.LulaValidation, er
return validation, nil
}

func CheckFileExists(filepath string) (bool, error) {
if _, err := os.Stat(filepath); err == nil {
return true, nil

} else if errors.Is(err, os.ErrNotExist) {
return false, nil

} else {
return false, err
}
}

// CleanMultilineString removes leading and trailing whitespace from a multiline string
func CleanMultilineString(str string) string {
re := regexp.MustCompile(`[ \t]+\r?\n`)
Expand Down
Loading

0 comments on commit cb4fc6f

Please sign in to comment.