Skip to content

Commit

Permalink
feat(generate): add testing for generate profile
Browse files Browse the repository at this point in the history
  • Loading branch information
brandtkeller committed Oct 9, 2024
1 parent d301564 commit 5e694ab
Show file tree
Hide file tree
Showing 14 changed files with 245 additions and 72 deletions.
43 changes: 34 additions & 9 deletions src/cmd/generate/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ var generateComponentCmd = &cobra.Command{
},
}

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
Expand All @@ -143,12 +156,14 @@ func GenerateProfileCommand() *cobra.Command {
exclude []string
)

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

if outputFile == "" {
Expand All @@ -165,7 +180,17 @@ func GenerateProfileCommand() *cobra.Command {
return fmt.Errorf("Output File %s currently exist - cannot merge artifacts\n", outputFile)
}

profile, err := oscal.GenerateProfile(source, include, exclude)
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, ","))
}

profile, err := oscal.GenerateProfile(command, source, include, exclude)
if err != nil {
return err
}
Expand All @@ -180,13 +205,13 @@ func GenerateProfileCommand() *cobra.Command {
},
}

cmd.Flags().StringVarP(&source, "source", "s", "", "the path to the source catalog/profile")
cmd.MarkFlagRequired("source")
cmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be directed to stdout")
cmd.Flags().StringSliceVarP(&include, "include", "i", []string{}, "comma delimited list of controls to include from the source catalog/profile")
cmd.Flags().StringSliceVarP(&exclude, "exclude", "e", []string{}, "comma delimited list of controls to exclude from the source catalog/profile")
profilecmd.Flags().StringVarP(&source, "source", "s", "", "the path to the source catalog/profile")
profilecmd.MarkFlagRequired("source")
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")

return cmd
return profilecmd
}

// var generateAssessmentPlanCmd = &cobra.Command{
Expand Down
14 changes: 12 additions & 2 deletions src/pkg/common/oscal/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (p *Profile) NewModel(data []byte) error {
return nil
}

func GenerateProfile(source string, include []string, exclude []string) (*Profile, error) {
func GenerateProfile(command string, source string, include []string, exclude []string) (*Profile, error) {

// Create the OSCAL profile type model for use and later assignment to the oscal.Profile implementation
var model oscalTypes.Profile
Expand All @@ -117,15 +117,25 @@ func GenerateProfile(source string, include []string, exclude []string) (*Profil
// Always create a new UUID for the assessment results (for now)
model.UUID = uuid.NewUUID()

// Creation of the generation prop
props := []oscalTypes.Property{
{
Name: "generation",
Ns: LULA_NAMESPACE,
Value: command,
},
}

// Create metadata object with requires fields and a few extras
// Where do we establish what `version` should be?
// Adding props to metadata as it is less available within the model
model.Metadata = oscalTypes.Metadata{
Title: "Profile",
Version: "0.0.1",
OscalVersion: OSCAL_VERSION,
Remarks: "Profile generated from Lula",
Published: &rfc3339Time,
LastModified: rfc3339Time,
Props: &props,
}

// Include would include the specified controls and exclude the rest
Expand Down
22 changes: 10 additions & 12 deletions src/pkg/common/oscal/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

func TestGetType(t *testing.T) {
test := func(t *testing.T, model oscal.Profile, expected string) {
test := func(t *testing.T, model *oscal.Profile, expected string) {
t.Helper()

got := model.GetType()
Expand All @@ -20,23 +20,22 @@ func TestGetType(t *testing.T) {

t.Run("Test populated model", func(t *testing.T) {

var profile = oscal.Profile{
Model: &oscalTypes.Profile{},
}
profile := oscal.NewProfile()
profile.Model = &oscalTypes.Profile{}

test(t, profile, "profile")
})

t.Run("Test unpopulated model", func(t *testing.T) {

var profile = oscal.Profile{}
profile := oscal.NewProfile()

test(t, profile, "profile")
})
}

func TestGetCompleteModel(t *testing.T) {
test := func(t *testing.T, model oscal.Profile, expectedNil bool) {
test := func(t *testing.T, model *oscal.Profile, expectedNil bool) {
t.Helper()

result := model.GetCompleteModel()
Expand All @@ -48,15 +47,14 @@ func TestGetCompleteModel(t *testing.T) {
}

t.Run("Test complete with non-nil model", func(t *testing.T) {
var profile = oscal.Profile{
Model: &oscalTypes.Profile{},
}
profile := oscal.NewProfile()
profile.Model = &oscalTypes.Profile{}
test(t, profile, false)
})

t.Run("Test complete with no model declaration", func(t *testing.T) {
// Expecting a nil model
var profile = oscal.Profile{}
profile := oscal.NewProfile()
test(t, profile, true)
})
}
Expand Down Expand Up @@ -113,7 +111,7 @@ func TestMakeDeterministic(t *testing.T) {
}

t.Run("Profile with included controls", func(t *testing.T) {
profile, err := oscal.GenerateProfile("#a3fb260d-0b89-4a12-b65c-a2737500febc", []string{"ac-4", "ac-1", "ac-7", "ac-3", "ac-2"}, []string{})
profile, err := oscal.GenerateProfile("", "#a3fb260d-0b89-4a12-b65c-a2737500febc", []string{"ac-4", "ac-1", "ac-7", "ac-3", "ac-2"}, []string{})
if err != nil {
t.Fatal(err)
}
Expand All @@ -122,7 +120,7 @@ func TestMakeDeterministic(t *testing.T) {
})

t.Run("Profile with exclude controls", func(t *testing.T) {
profile, err := oscal.GenerateProfile("#a3fb260d-0b89-4a12-b65c-a2737500febc", []string{}, []string{"ac-4", "ac-1", "ac-7", "ac-3", "ac-2"})
profile, err := oscal.GenerateProfile("", "#a3fb260d-0b89-4a12-b65c-a2737500febc", []string{}, []string{"ac-4", "ac-1", "ac-7", "ac-3", "ac-2"})
if err != nil {
t.Fatal(err)
}
Expand Down
70 changes: 68 additions & 2 deletions src/test/e2e/cmd/generate_profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package cmd_test
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"

"github.com/defenseunicorns/lula/src/cmd/generate"
Expand All @@ -18,13 +20,25 @@ func TestGenerateProfileCommand(t *testing.T) {
return runCmdTest(t, rootCmd, args...)
}

testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error {
rootCmd := generate.GenerateProfileCommand()

return runCmdTestWithGolden(t, "generate/", goldenFileName, rootCmd, args...)
}

testAgainstOutputFile := func(t *testing.T, goldenFileName string, args ...string) error {
rootCmd := generate.GenerateProfileCommand()

return runCmdTestWithOutputFile(t, "generate/", goldenFileName, "yaml", rootCmd, args...)
}

t.Run("Generate Profile", func(t *testing.T) {
tempDir := t.TempDir()
outputFile := filepath.Join(tempDir, "output.yaml")

args := []string{
"--source", "../unit/common/oscal/catalog.yaml",
"--include", "ac-1,ac-2,ac-3",
"--source", "../../unit/common/oscal/catalog.yaml",
"--include", "ac-1,ac-3,ac-2",
"-o", outputFile,
}
err := test(t, args...)
Expand All @@ -51,6 +65,58 @@ func TestGenerateProfileCommand(t *testing.T) {
t.Error("expected the profile model to be non-nil")
}

profileModel := complete.Profile

if len(profileModel.Imports) == 0 {
t.Error("expected length of imports to be greater than 0")
}

// Target import item should be the only item in the list
include := profileModel.Imports[0].IncludeControls
controls := *include

if len(controls) != 1 {
t.Error("expected length of controls to be 1")
}
expected := []string{"ac-1", "ac-2", "ac-3"}
ids := controls[0].WithIds
if !reflect.DeepEqual(expected, *ids) {
t.Errorf("expected control id slice to contain %+q, got %+q", expected, *ids)
}
})

t.Run("Generate a profile with included controls", func(t *testing.T) {
args := []string{
"--source", "../../unit/common/oscal/catalog.yaml",
"--include", "ac-1,ac-3,ac-2",
}

err := testAgainstOutputFile(t, "generate-profile", args...)
if err != nil {
t.Errorf("error executing: generate profile %v", strings.Join(args, " "))
}
})

t.Run("Test help", func(t *testing.T) {
err := testAgainstGolden(t, "help", "--help")
if err != nil {
t.Errorf("Expected help message but received an error %v", err)
}
})

t.Run("Test generate - invalid merge error", func(t *testing.T) {
args := []string{
"--source", "../../unit/common/oscal/catalog.yaml",
"--include", "ac-1,ac-3,ac-2",
"-o", "../../unit/common/oscal/valid-profile.yaml",
}
err := test(t, args...)
if err == nil {
t.Error("Expected an error indicating merging profiles is not supported")
}
if !strings.Contains(err.Error(), "cannot merge artifacts") {
t.Errorf("Expected error for merging artifacts - received %v", err.Error())
}
})

}
14 changes: 9 additions & 5 deletions src/test/e2e/cmd/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ func runCmdTestWithOutputFile(t *testing.T, goldenFilePath, goldenFileName, outE
return err
}

// Scrub timestamps
data = scrubTimestamps(data)
// Scrub uniquely generated data
data = scrubData(data)

testGolden(t, goldenFilePath, goldenFileName, string(data))

Expand Down Expand Up @@ -94,7 +94,11 @@ func testGolden(t *testing.T, filePath, filename, got string) {
}
}

func scrubTimestamps(data []byte) []byte {
re := regexp.MustCompile(`(?i)(last-modified:\s*)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[-+]\d{2}:\d{2}|Z)?)`)
return []byte(re.ReplaceAllString(string(data), "${1}XXX"))
func scrubData(data []byte) []byte {
timestamps := regexp.MustCompile(`(?i)(last-modified|published:\s)*(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[-+]\d{2}:\d{2}|Z)?)`)
uuids := regexp.MustCompile(`(?i)(uuid:\s*)([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})`)

output := timestamps.ReplaceAllString(string(data), "${1}XXX")
output = uuids.ReplaceAllString(string(output), "${1}XXX")
return []byte(output)
}
22 changes: 22 additions & 0 deletions src/test/e2e/cmd/testdata/generate/generate-profile.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
profile:
imports:
- href: ../../unit/common/oscal/catalog.yaml
include-controls:
- with-ids:
- ac-1
- ac-2
- ac-3
merge:
as-is: true
metadata:
last-modified: XXX
oscal-version: 1.1.2
props:
- name: generation
ns: https://docs.lula.dev/oscal/ns
value: profile --source ../../unit/common/oscal/catalog.yaml --include ac-1,ac-3,ac-2
published: XXX
remarks: Profile generated from Lula
title: Profile
version: 0.0.1
uuid: XXX
26 changes: 26 additions & 0 deletions src/test/e2e/cmd/testdata/generate/help.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Generation of a Profile OSCAL artifact with controls included or excluded from a source catalog/profile.

Usage:
profile [flags]

Aliases:
profile, p

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


Flags:
-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
Loading

0 comments on commit 5e694ab

Please sign in to comment.