diff --git a/.github/workflows/goreleaser-check.yaml b/.github/workflows/goreleaser-check.yaml new file mode 100644 index 00000000..f2387d3a --- /dev/null +++ b/.github/workflows/goreleaser-check.yaml @@ -0,0 +1,29 @@ +name: GoReleaser Check + +on: + push: + paths: + - '.goreleaser.yaml' + pull_request: + paths: + - '.goreleaser.yaml' + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + with: + fetch-depth: 0 + + - name: Setup golang + uses: ./.github/actions/golang + + - name: Install GoReleaser + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 + with: + install-only: true + + - name: Run GoReleaser Check + run: goreleaser check diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 194c91fc..fd8759dc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -92,12 +92,22 @@ jobs: name: build-artifacts path: bin/ + - name: Get Brew tap repo token + id: brew-tap-token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + with: + app-id: ${{ secrets.HOMEBREW_TAP_WORKFLOW_GITHUB_APP_ID }} + private-key: ${{ secrets.HOMEBREW_TAP_WORKFLOW_GITHUB_APP_SECRET }} + owner: defenseunicorns + repositories: homebrew-tap + # Create the GitHub release notes - name: Run GoReleaser uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 with: distribution: goreleaser version: latest - args: release --clean --verbose + args: release --clean --verbose --config .goreleaser.yaml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN}} + HOMEBREW_TAP_GITHUB_TOKEN: ${{ steps.brew-tap-token.outputs.token }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index adb5fe51..562c5824 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,3 +1,5 @@ +version: 2 + before: hooks: - go mod tidy @@ -36,12 +38,47 @@ sboms: - "sbom_{{ .ProjectName }}_{{ .Tag }}_{{- title .Os }}_{{ .Arch }}.sbom" snapshot: - name_template: "{{ incpatch .Version }}-snapshot" + version_template: "{{ incpatch .Version }}-snapshot" # Use the auto-generated changelog github provides changelog: use: github-native +brews: + - name: lula + repository: + owner: defenseunicorns + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + branch: "{{ .ProjectName }}-{{ .Tag }}" + pull_request: + enabled: true + base: + branch: main + owner: defenseunicorns + name: homebrew-tap + commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" + homepage: "https://lula.dev" + description: "The Compliance Validator" + + # NOTE: We are using .Version instead of .Tag because homebrew has weird semver parsing rules and won't be able to + # install versioned releases that has a `v` character before the version number. + - name: "lula@{{ .Version }}" + repository: + owner: defenseunicorns + name: homebrew-tap + token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" + branch: "{{ .ProjectName }}-{{ .Tag }}" + pull_request: + enabled: true + base: + branch: main + owner: defenseunicorns + name: homebrew-tap + commit_msg_template: "Brew formula update for {{ .ProjectName }} versioned release {{ .Tag }}" + homepage: "https://lula.dev" + description: "The Compliance Validator" + # Generate a GitHub release and publish the release for the tag release: github: diff --git a/docs/cli-commands/lula_tools_compose.md b/docs/cli-commands/lula_tools_compose.md index 0573382a..5769e3a7 100644 --- a/docs/cli-commands/lula_tools_compose.md +++ b/docs/cli-commands/lula_tools_compose.md @@ -9,8 +9,15 @@ compose an OSCAL component definition ### Synopsis + Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability. +Supports templating of the composed component definition with the following configuration options: +- To compose with templating applied, specify '--render, -r' with values of 'all', 'non-sensitive', 'constants', or 'masked' (choice will depend on the use case for the composed content) +- To render Lula Validations include '--render-validations' +- To perform any manual overrides to the template data, specify '--set, -s' with the format '.const.key=value' or '.var.key=value' + + ``` lula tools compose [flags] ``` @@ -33,6 +40,9 @@ To indicate a specific output file: -h, --help help for compose -f, --input-file string the path to the target OSCAL component definition -o, --output-file -composed the path to the output file. If not specified, the output file will be the original filename with -composed appended + -r, --render string values to render the template with, options are: masked, constants, non-sensitive, all + --render-validations extend render to remote Lula Validations + -s, --set strings set value overrides for templated data ``` ### Options inherited from parent commands diff --git a/docs/community-and-contribution/release-process.md b/docs/community-and-contribution/release-process.md index afcced8f..c9da4204 100644 --- a/docs/community-and-contribution/release-process.md +++ b/docs/community-and-contribution/release-process.md @@ -20,7 +20,8 @@ The most important prefixes you should have in mind are: ### How can I influence the version number for a release? -PR titles should also follow this pattern and are linted using [commitlint](https://commitlint.js.org/). The PR title will determine the version bump. When a PR is merged (squashed) release-please will kick off a release PR. When that release PR is approved and merged, release-please will create a draft release. Once that draft release is published go-releaser with build and publish the assets. +PR titles should also follow this pattern and are linted using [commitlint](https://commitlint.js.org/). The PR title will determine the version bump. When a PR is merged (squashed) release-please will kick off a release PR. When that release PR is approved and merged, release-please will create a draft release. Once that draft release is published go-releaser with build and publish the assets, including creating a release in our Homebrew tap repository: [https://github.com/defenseunicorns/homebrew-tap](https://github.com/defenseunicorns/homebrew-tap) + - Pre-v1.0.0 release-please is configured to bump minors on breaking changes and patches otherwise. per [release-please-config](https://github.com/defenseunicorns/lula/blob/main/release-please-config.json) ### How do I fix a release issue? @@ -42,8 +43,10 @@ The CHANGELOG is not required to be updated, only the release notes must be upda #### Other issues and helpful tips -- Confirm that the goreleaser configuration is valid by using the [goreleaser cli](https://goreleaser.com/cmd/goreleaser_check/?h=valid) +- Manual approach: Confirm that the goreleaser configuration is valid by using the [goreleaser cli](https://goreleaser.com/cmd/goreleaser_check/?h=valid). ```sh goreleaser check .goreleaser.yaml [flags] ``` + +- Automated approach: On Push and Pull Request the [GoReleaserGitHub Action Workflow](./github/workflows/goreleaser-check.yaml) will run the `goreleaser check` command diff --git a/docs/getting-started/configuration.md b/docs/getting-started/configuration.md index 3df15a2d..8ccc9105 100644 --- a/docs/getting-started/configuration.md +++ b/docs/getting-started/configuration.md @@ -118,6 +118,9 @@ provider: The constant's keys should be in the format `.const.` and should not contain any '-' or '.' characters, as this will not respect the go text/template format. +> [!IMPORTANT] +> Due to viper limitations, all constants should be referenced in the template as lowercase values. + #### Variables A sample `variables` section of a `lula-config.yaml` file is as follows: diff --git a/docs/getting-started/templating.md b/docs/getting-started/templating.md new file mode 100644 index 00000000..f1bad776 --- /dev/null +++ b/docs/getting-started/templating.md @@ -0,0 +1,260 @@ +# Templating + +Lula supports composition of both Component Definition and Lula Validation template files. See the [configuration](./configuration.md) documentation for more information on how to configure Lula to use templating. See the [compose CLI command](../cli-commands/lula_tools_compose.md) documentation for more information on the `lula tools compose` command flags to control how templating is applied. + +## Component Definition Templating + +Component Definition templates can be used to create modular component definitions using values from the `lula-config.yaml` file. + +Example: +```yaml +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: {{ .const.title }} + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 + parties: + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: {{ .const.website }} + rel: website +``` + +lula-config.yaml: +```yaml +constants: + title: Lula Demo + website: https://github.com/defenseunicorns/lula +``` + +When this is `composed` with templating applied (`lula tools compose -f --render all`) with the associated `lula-config.yaml`, the resulting component definition will be: + +```yaml +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 + parties: + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website +``` + +## Validation Templating + +Validation templates can be used to create modular Lula Validations using values from the `lula-config.yaml` file. These can be composed into the component definition using the `lula tools compose` command. + +Example: +```yaml +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: {{ .const.type }} + title: {{ .const.title }} + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + description: >- + This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: "./validation.tmpl.yaml" + text: local path template validation + rel: lula +``` + +Where `./validation.tmpl.yaml` is: +```yaml +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podvt + resource-rule: + name: {{ .const.resources.name }} + version: v1 + resource: pods + namespaces: [{{ .const.resources.namespace }}] +provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} +``` + +Executing `lula tools compose -f ./component-definition.yaml --render all --render-validations` will result in: + +```yaml +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "" == "********" + } + msg = validate.msg + + value_of_my_secret := + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F +``` + +### Composing Validation Templates + +If validations are composed into a component definition AND the validation is still intended to be a template, it must be a valid yaml document. For example, the above `validation.tmpl.yaml` is invalid yaml, as the `resource-rule.name` field is not ecapsulated in quotes. A valid yaml version of the above template would be: + +```yaml +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podvt + resource-rule: + name: "{{ .const.resources.name }}" + version: v1 + resource: pods + namespaces: ["{{ .const.resources.namespace }}"] +provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} +``` \ No newline at end of file diff --git a/go.mod b/go.mod index f66d82ce..f09b6327 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,12 @@ require ( github.com/charmbracelet/x/exp/teatest v0.0.0-20240919170804-a4978c8e603a github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/defenseunicorns/go-oscal v0.6.0 + github.com/google/go-cmp v0.6.0 github.com/hashicorp/go-version v1.7.0 github.com/kyverno/kyverno-json v0.0.3 github.com/mattn/go-runewidth v0.0.16 github.com/muesli/termenv v0.15.2 - github.com/open-policy-agent/opa v0.68.0 + github.com/open-policy-agent/opa v0.69.0 github.com/pterm/pterm v0.12.79 github.com/sergi/go-diff v1.3.1 github.com/spf13/cobra v1.8.1 @@ -38,7 +39,7 @@ require ( github.com/IGLOU-EU/go-wildcard v1.0.3 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect - github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/agnivade/levenshtein v1.2.0 // indirect github.com/aquilax/truncate v1.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -73,7 +74,6 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect @@ -117,7 +117,7 @@ require ( github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.20.2 // indirect + github.com/prometheus/client_golang v1.20.4 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect @@ -149,13 +149,13 @@ require ( go.opentelemetry.io/otel/trace v1.28.0 // indirect go.starlark.net v0.0.0-20240123142251-f86470692795 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.26.0 // indirect + golang.org/x/crypto v0.27.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.23.0 // indirect + golang.org/x/term v0.24.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.6.0 // indirect google.golang.org/protobuf v1.34.2 // indirect diff --git a/go.sum b/go.sum index 3f2ee55e..c2ee2f39 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,8 @@ github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= -github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= -github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/aquilax/truncate v1.0.0 h1:UgIGS8U/aZ4JyOJ2h3xcF5cSQ06+gGBnjxH2RUHJe0U= github.com/aquilax/truncate v1.0.0/go.mod h1:BeMESIDMlvlS3bmg4BVvBbbZUNwWtS8uzYPAKXwwhLw= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= @@ -85,8 +85,8 @@ github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0 github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= -github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= @@ -143,8 +143,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= -github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -282,8 +282,8 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= -github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2eCJYSqJQ= -github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w= +github.com/open-policy-agent/opa v0.69.0 h1:s2igLw2Z6IvGWGuXSfugWkVultDMsM9pXiDuMp7ckWw= +github.com/open-policy-agent/opa v0.69.0/go.mod h1:+qyXJGkpEJ6kpB1kGo8JSwHtVXbTdsGdQYPWWNYNj+4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= @@ -295,8 +295,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= -github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI= +github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= @@ -418,8 +418,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -435,8 +435,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -470,8 +470,8 @@ golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -494,12 +494,12 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= -google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.0 h1:IdH9y6PF5MPSdAntIcpjQ+tXO41pcQsfZV2RxtQgVcw= +google.golang.org/grpc v1.67.0/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/src/cmd/common/viper.go b/src/cmd/common/viper.go index 8763620b..6e59dddc 100644 --- a/src/cmd/common/viper.go +++ b/src/cmd/common/viper.go @@ -25,6 +25,12 @@ var ( // Viper configuration error vConfigError error + + // Template config values + TemplateConstants map[string]interface{} + + // Template config values + TemplateVariables []template.VariableConfig ) // InitViper initializes the viper singleton for the CLI @@ -66,6 +72,14 @@ func InitViper() *viper.Viper { // Set default values for viper setDefaults() + // Load template config + constants, variables, err := GetTemplateConfig() + if err != nil { + panic(err) + } + TemplateConstants = constants + TemplateVariables = variables + return v } @@ -76,8 +90,8 @@ func GetViper() *viper.Viper { // GetTemplateConfig loads the constants and variables from the viper config func GetTemplateConfig() (map[string]interface{}, []template.VariableConfig, error) { - var constants map[string]interface{} - var variables []template.VariableConfig + constants := make(map[string]interface{}) + variables := make([]template.VariableConfig, 0) err := v.UnmarshalKey(VConstants, &constants) if err != nil { diff --git a/src/cmd/generate/generate.go b/src/cmd/generate/generate.go index 4ef0ebe7..5fb08e35 100644 --- a/src/cmd/generate/generate.go +++ b/src/cmd/generate/generate.go @@ -67,6 +67,12 @@ var generateComponentCmd = &cobra.Command{ var remarks []string var title = "Component Title" + // Check if output file contains a valid OSCAL model + _, err := oscal.ValidOSCALModelAtPath(opts.OutputFile) + if err != nil { + message.Fatalf(err, "Output file %s is not a valid OSCAL model: %v", opts.OutputFile, err) + } + // check for Catalog Source - this field is required if componentOpts.CatalogSource == "" { message.Fatal(fmt.Errorf("no catalog source provided"), "generate component requires a catalog input source") diff --git a/src/cmd/root.go b/src/cmd/root.go index 28e5e789..95af6f79 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -1,7 +1,11 @@ package cmd import ( - "github.com/spf13/cobra" + "context" + "fmt" + "os" + "os/signal" + "syscall" "github.com/defenseunicorns/lula/src/cmd/common" "github.com/defenseunicorns/lula/src/cmd/console" @@ -12,6 +16,7 @@ import ( "github.com/defenseunicorns/lula/src/cmd/tools" "github.com/defenseunicorns/lula/src/cmd/validate" "github.com/defenseunicorns/lula/src/cmd/version" + "github.com/spf13/cobra" ) var LogLevelCLI string @@ -33,8 +38,22 @@ func RootCommand() *cobra.Command { } func Execute() { + ctx, cancel := context.WithCancel(context.Background()) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM) + + go func() { + select { + case <-c: + fmt.Println("Got signal, shutting down...") + cancel() + os.Exit(2) + case <-ctx.Done(): + return + } + }() - cobra.CheckErr(rootCmd.Execute()) + cobra.CheckErr(rootCmd.ExecuteContext(ctx)) } func init() { diff --git a/src/cmd/tools/compose.go b/src/cmd/tools/compose.go index 1668ae4b..79a68981 100644 --- a/src/cmd/tools/compose.go +++ b/src/cmd/tools/compose.go @@ -1,9 +1,7 @@ package tools import ( - "errors" "fmt" - "os" "path/filepath" "strings" @@ -14,13 +12,6 @@ import ( "github.com/spf13/cobra" ) -type composeFlags struct { - InputFile string // -f --input-file - OutputFile string // -o --output-file -} - -var composeOpts = &composeFlags{} - var composeHelp = ` To compose an OSCAL Model: lula tools compose -f ./oscal-component.yaml @@ -28,64 +19,86 @@ To compose an OSCAL Model: To indicate a specific output file: lula tools compose -f ./oscal-component.yaml -o composed-oscal-component.yaml ` -var composeCmd = &cobra.Command{ - Use: "compose", - Short: "compose an OSCAL component definition", - Long: "Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability.", - Example: composeHelp, - Run: func(cmd *cobra.Command, args []string) { - composeSpinner := message.NewProgressSpinner("Composing %s", composeOpts.InputFile) - defer composeSpinner.Stop() - - if composeOpts.InputFile == "" { - message.Fatal(errors.New("flag input-file is not set"), - "Please specify an input file with the -f flag") - } - - outputFile := composeOpts.OutputFile - if outputFile == "" { - outputFile = GetDefaultOutputFile(composeOpts.InputFile) - } - - err := Compose(composeOpts.InputFile, outputFile) - if err != nil { - message.Fatalf(err, "Composition error: %s", err) - } - - message.Infof("Composed OSCAL Component Definition to: %s", outputFile) - composeSpinner.Success() - }, -} - -func init() { - common.InitViper() - - toolsCmd.AddCommand(composeCmd) - composeCmd.Flags().StringVarP(&composeOpts.InputFile, "input-file", "f", "", "the path to the target OSCAL component definition") - composeCmd.Flags().StringVarP(&composeOpts.OutputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be the original filename with `-composed` appended") -} - -// Compose composes an OSCAL model from a file path -func Compose(inputFile, outputFile string) error { - _, err := os.Stat(inputFile) - if os.IsNotExist(err) { - return fmt.Errorf("input file: %v does not exist - unable to compose document", inputFile) - } +var composeLong = ` +Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability. - // Compose the OSCAL model - model, err := composition.ComposeFromPath(inputFile) - if err != nil { - return err - } +Supports templating of the composed component definition with the following configuration options: +- To compose with templating applied, specify '--render, -r' with values of 'all', 'non-sensitive', 'constants', or 'masked' (choice will depend on the use case for the composed content) +- To render Lula Validations include '--render-validations' +- To perform any manual overrides to the template data, specify '--set, -s' with the format '.const.key=value' or '.var.key=value' +` - // Write the composed OSCAL model to a file - err = oscal.WriteOscalModel(outputFile, model) - if err != nil { - return err +func ComposeCommand() *cobra.Command { + var ( + inputFile string // -f --input-file + outputFile string // -o --output-file + setOpts []string // -s --set + renderTypeString string // -r --render + renderValidations bool // --render-validations + ) + + var composeCmd = &cobra.Command{ + Use: "compose", + Short: "compose an OSCAL component definition", + Long: composeLong, + Example: composeHelp, + RunE: func(cmd *cobra.Command, args []string) (err error) { + composeSpinner := message.NewProgressSpinner("Composing %s", inputFile) + defer composeSpinner.Stop() + + if outputFile == "" { + outputFile = GetDefaultOutputFile(inputFile) + } + + // Check if output file contains a valid OSCAL model + _, err = oscal.ValidOSCALModelAtPath(outputFile) + if err != nil { + return fmt.Errorf("invalid OSCAL model at output file: %v", err) + } + + opts := []composition.Option{ + composition.WithModelFromLocalPath(inputFile), + composition.WithRenderSettings(renderTypeString, renderValidations), + composition.WithTemplateRenderer(renderTypeString, common.TemplateConstants, common.TemplateVariables, setOpts), + } + + // Compose the OSCAL model + compositionCtx, err := composition.New(opts...) + if err != nil { + return fmt.Errorf("error creating composition context: %v", err) + } + + model, err := compositionCtx.ComposeFromPath(cmd.Context(), inputFile) + if err != nil { + return fmt.Errorf("error composing model from path: %v", err) + } + + // Write the composed OSCAL model to a file + err = oscal.WriteOscalModel(outputFile, model) + if err != nil { + return fmt.Errorf("error writing composed model: %v", err) + } + + message.Infof("Composed OSCAL Component Definition to: %s", outputFile) + composeSpinner.Success() + + return nil + }, } + composeCmd.Flags().StringVarP(&inputFile, "input-file", "f", "", "the path to the target OSCAL component definition") + composeCmd.MarkFlagRequired("input-file") + composeCmd.Flags().StringVarP(&outputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be the original filename with `-composed` appended") + composeCmd.Flags().StringVarP(&renderTypeString, "render", "r", "", "values to render the template with, options are: masked, constants, non-sensitive, all") + composeCmd.Flags().StringSliceVarP(&setOpts, "set", "s", []string{}, "set value overrides for templated data") + composeCmd.Flags().BoolVar(&renderValidations, "render-validations", false, "extend render to remote Lula Validations") + + return composeCmd +} - return nil +func init() { + common.InitViper() + toolsCmd.AddCommand(ComposeCommand()) } // GetDefaultOutputFile returns the default output file name diff --git a/src/cmd/tools/compose_test.go b/src/cmd/tools/compose_test.go deleted file mode 100644 index 5d366623..00000000 --- a/src/cmd/tools/compose_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package tools_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/defenseunicorns/lula/src/cmd/tools" - "github.com/defenseunicorns/lula/src/pkg/common/oscal" -) - -var ( - validInputFile = "../../test/unit/common/composition/component-definition-import-compdefs.yaml" - invalidInputFile = "../../test/unit/common/valid-api-spec.yaml" -) - -func TestComposeComponentDefinition(t *testing.T) { - t.Parallel() - tempDir := t.TempDir() - outputFile := filepath.Join(tempDir, "output.yaml") - - t.Run("composes valid component definition", func(t *testing.T) { - err := tools.Compose(validInputFile, outputFile) - if err != nil { - t.Fatalf("error composing component definition: %s", err) - } - - compiledBytes, err := os.ReadFile(outputFile) - if err != nil { - t.Fatalf("error reading composed component definition: %s", err) - } - compiledModel, err := oscal.NewOscalModel(compiledBytes) - if err != nil { - t.Fatalf("error creating oscal model from composed component definition: %s", err) - } - - if compiledModel.ComponentDefinition.BackMatter.Resources == nil { - t.Fatal("composed component definition is nil") - } - - if len(*compiledModel.ComponentDefinition.BackMatter.Resources) <= 1 { - t.Fatalf("expected 2 resources, got %d", len(*compiledModel.ComponentDefinition.BackMatter.Resources)) - } - }) - - t.Run("invalid component definition throws error", func(t *testing.T) { - err := tools.Compose(invalidInputFile, outputFile) - if err == nil { - t.Fatal("expected error composing invalid component definition") - } - }) -} diff --git a/src/cmd/tools/template.go b/src/cmd/tools/template.go index e4df09cb..c6ffea5d 100644 --- a/src/cmd/tools/template.go +++ b/src/cmd/tools/template.go @@ -3,7 +3,6 @@ package tools import ( "fmt" "os" - "strings" "github.com/defenseunicorns/go-oscal/src/pkg/files" "github.com/defenseunicorns/lula/src/cmd/common" @@ -52,15 +51,10 @@ func TemplateCommand() *cobra.Command { } // Validate render type - renderType, err := parseRenderType(renderTypeString) + renderType, err := template.ParseRenderType(renderTypeString) if err != nil { message.Warnf("invalid render type, defaulting to masked: %v", err) - } - - // Get constants and variables for templating from viper config - constants, variables, err := common.GetTemplateConfig() - if err != nil { - return fmt.Errorf("error getting template config: %v", err) + renderType = template.MASKED } // Get overrides from --set flag @@ -71,7 +65,7 @@ func TemplateCommand() *cobra.Command { // Handles merging viper config file data + environment variables // Throws an error if config keys are invalid for templating - templateData, err := template.CollectTemplatingData(constants, variables, overrides) + templateData, err := template.CollectTemplatingData(common.TemplateConstants, common.TemplateVariables, overrides) if err != nil { return fmt.Errorf("error collecting templating data: %v", err) } @@ -114,17 +108,3 @@ func init() { common.InitViper() toolsCmd.AddCommand(TemplateCommand()) } - -func parseRenderType(item string) (template.RenderType, error) { - switch strings.ToLower(item) { - case "masked": - return template.MASKED, nil - case "constants": - return template.CONSTANTS, nil - case "non-sensitive": - return template.NONSENSITIVE, nil - case "all": - return template.ALL, nil - } - return template.MASKED, fmt.Errorf("invalid render type: %s", item) -} diff --git a/src/cmd/validate/validate.go b/src/cmd/validate/validate.go index 7ca481e9..d1da332f 100644 --- a/src/cmd/validate/validate.go +++ b/src/cmd/validate/validate.go @@ -1,6 +1,7 @@ package validate import ( + "context" "fmt" "os" "path/filepath" @@ -52,6 +53,12 @@ var validateCmd = &cobra.Command{ outputFile = getDefaultOutputFile(opts.InputFile) } + // Check if output file contains a valid OSCAL model + _, err := oscal.ValidOSCALModelAtPath(outputFile) + if err != nil { + message.Fatalf(err, "Output file %s is not a valid OSCAL model: %v", outputFile, err) + } + if SaveResources { ResourcesDir = filepath.Join(filepath.Dir(outputFile)) } @@ -60,7 +67,7 @@ var validateCmd = &cobra.Command{ message.Fatalf(err, "Invalid file extension: %s, requires .json or .yaml", opts.InputFile) } - assessment, err := ValidateOnPath(opts.InputFile, opts.Target) + assessment, err := ValidateOnPath(cmd.Context(), opts.InputFile, opts.Target) if err != nil { message.Fatalf(err, "Validation error: %s", err) } @@ -118,16 +125,21 @@ func ValidateCommand() *cobra.Command { // ValidateOnPath takes 1 -> N paths to OSCAL component-definition files // It will then read those files to perform validation and return an ResultObject -func ValidateOnPath(path string, target string) (assessmentResult *oscalTypes_1_1_2.AssessmentResults, err error) { +func ValidateOnPath(ctx context.Context, path string, target string) (assessmentResult *oscalTypes_1_1_2.AssessmentResults, err error) { _, err = os.Stat(path) if os.IsNotExist(err) { return assessmentResult, fmt.Errorf("path: %v does not exist - unable to digest document", path) } - oscalModel, err := composition.ComposeFromPath(path) + compositionCtx, err := composition.New(composition.WithModelFromLocalPath(path)) if err != nil { - return assessmentResult, err + return nil, fmt.Errorf("error creating composition context: %v", err) + } + + oscalModel, err := compositionCtx.ComposeFromPath(ctx, path) + if err != nil { + return nil, fmt.Errorf("error composing model: %v", err) } if oscalModel.ComponentDefinition == nil { @@ -261,7 +273,7 @@ func ValidateOnControlImplementations(controlImplementations *[]oscalTypes_1_1_2 return findings, observations, nil } -// GetDefaultOutputFile returns the default output file name +// getDefaultOutputFile returns the default output file name and checks if the file already exists func getDefaultOutputFile(inputFile string) string { dirPath := filepath.Dir(inputFile) filename := "assessment-results" + filepath.Ext(inputFile) diff --git a/src/internal/template/template.go b/src/internal/template/template.go index fa916252..85df96b0 100644 --- a/src/internal/template/template.go +++ b/src/internal/template/template.go @@ -182,7 +182,8 @@ func CollectTemplatingData(constants map[string]interface{}, variables []Variabl return templateData, err } - templateData.Constants = constants + templateData.Constants = deepCopyMap(constants) + for _, variable := range variables { if variable.Sensitive { templateData.SensitiveVariables[variable.Key] = variable.Default @@ -247,6 +248,33 @@ func GetEnvVars(prefix string) map[string]string { return envMap } +// IsTemplate checks if the given string contains valid template syntax +func IsTemplate(data string) bool { + // Check for basic template syntax markers + if !strings.Contains(data, "{{") || !strings.Contains(data, "}}") { + return false + } + + // Attempt to parse the template + tpl := createTemplate() + _, err := tpl.Parse(data) + return err == nil +} + +func ParseRenderType(item string) (RenderType, error) { + switch strings.ToLower(item) { + case "masked": + return MASKED, nil + case "constants": + return CONSTANTS, nil + case "non-sensitive": + return NONSENSITIVE, nil + case "all": + return ALL, nil + } + return "", fmt.Errorf("invalid render type: %s", item) +} + // createTemplate creates a new template object func createTemplate() *template.Template { // Register custom template functions @@ -395,3 +423,56 @@ func setNestedValue(m map[string]interface{}, path string, value interface{}) er m[lastKey] = value return nil } + +func deepCopyMap(input map[string]interface{}) map[string]interface{} { + if input == nil { + return nil + } + + // Create a new map to hold the copy + copy := make(map[string]interface{}) + + for key, value := range input { + // Check the type of the value and copy accordingly + switch v := value.(type) { + case map[string]interface{}: + // If the value is a map, recursively deep copy it + copy[key] = deepCopyMap(v) + case []interface{}: + // If the value is a slice, deep copy each element + copy[key] = deepCopySlice(v) + default: + // For other types (e.g., strings, ints), just assign directly + copy[key] = v + } + } + + return copy +} + +// Helper function to deep copy a slice of interface{} +func deepCopySlice(input []interface{}) []interface{} { + if input == nil { + return nil + } + + // Create a new slice to hold the copy + copy := make([]interface{}, len(input)) + + for i, value := range input { + // Check the type of the value and copy accordingly + switch v := value.(type) { + case map[string]interface{}: + // If the value is a map, recursively deep copy it + copy[i] = deepCopyMap(v) + case []interface{}: + // If the value is a slice, deep copy each element + copy[i] = deepCopySlice(v) + default: + // For other types (e.g., strings, ints), just assign directly + copy[i] = v + } + } + + return copy +} diff --git a/src/internal/testhelpers/testhelpers.go b/src/internal/testhelpers/testhelpers.go index 936ab224..c9aa9c5b 100644 --- a/src/internal/testhelpers/testhelpers.go +++ b/src/internal/testhelpers/testhelpers.go @@ -4,7 +4,10 @@ import ( "fmt" "os" "testing" + "time" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/x/exp/teatest" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/pkg/common/oscal" ) @@ -32,3 +35,45 @@ func CreateTempFile(t *testing.T, ext string) *os.File { return tempFile } + +// RunTestModelView runs a test model view with a given model and messages, impelements a retry loop if final model is nil +func RunTestModelView(t *testing.T, m tea.Model, reset func() tea.Model, msgs []tea.Msg, timeout time.Duration, maxRetries, height, width int) error { + + testModelView := func(t *testing.T, try int) (bool, error) { + tm := teatest.NewTestModel(t, m, teatest.WithInitialTermSize(width, height)) + + for _, msg := range msgs { + tm.Send(msg) + time.Sleep(time.Millisecond * time.Duration(50*try)) + } + + if err := tm.Quit(); err != nil { + return false, err + } + + fm := tm.FinalModel(t, teatest.WithFinalTimeout(timeout)) + + if fm == nil { + return true, nil + } + + teatest.RequireEqualOutput(t, []byte(fm.View())) + + return false, nil + } + + for i := 0; i < maxRetries; i++ { + retry, err := testModelView(t, i+1) + if retry { + if reset != nil { + m = reset() + } + continue + } + if err != nil { + return err + } + break + } + return nil +} diff --git a/src/internal/tui/assessment_results/assessment-results_test.go b/src/internal/tui/assessment_results/assessment-results_test.go new file mode 100644 index 00000000..5dc56898 --- /dev/null +++ b/src/internal/tui/assessment_results/assessment-results_test.go @@ -0,0 +1,40 @@ +package assessmentresults_test + +import ( + "testing" + "time" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/defenseunicorns/lula/src/internal/testhelpers" + assessmentresults "github.com/defenseunicorns/lula/src/internal/tui/assessment_results" + "github.com/defenseunicorns/lula/src/internal/tui/common" + "github.com/muesli/termenv" +) + +const ( + timeout = time.Second * 20 + maxRetries = 3 + height = common.DefaultHeight + width = common.DefaultWidth + + validAssessmentResults = "../../../test/unit/common/oscal/valid-assessment-results.yaml" +) + +func init() { + lipgloss.SetColorProfile(termenv.Ascii) +} + +// TestAssessmentResultsBasicView tests that the model is created correctly from an assessment results model +func TestAssessmentResultsBasicView(t *testing.T) { + oscalModel := testhelpers.OscalFromPath(t, validAssessmentResults) + model := assessmentresults.NewAssessmentResultsModel(oscalModel.AssessmentResults) + model.Open(height, width) + + msgs := []tea.Msg{} + + err := testhelpers.RunTestModelView(t, model, nil, msgs, timeout, maxRetries, height, width) + if err != nil { + t.Fatal(err) + } +} diff --git a/src/internal/tui/assessment_results/testdata/TestAssessmentResultsBasicView.golden b/src/internal/tui/assessment_results/testdata/TestAssessmentResultsBasicView.golden new file mode 100644 index 00000000..02d559d7 --- /dev/null +++ b/src/internal/tui/assessment_results/testdata/TestAssessmentResultsBasicView.golden @@ -0,0 +1,56 @@ + ? toggle help + ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ + Selected Result │Lula Validation Result - 41787700-2a4c-…│ Compare Result │No Result Selected │ + ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ +╭───────────────╮ ╭─────────╮ +│ Findings List ├─────────────────────────────╮ │ Summary ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +╰───────────────╯ ╰─────────╯ + │ │ │ ⚠️ Summary Under Construction ⚠️ │ + │ 1 item │ │ │ + │ │ │ │ + │ ID-1 │ │ │ + │ not-satisfied │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ │ ╭──────────────╮ + │ │ │ Observations ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰──────────────╯ + │ │ │ ⚠️ Observations Under Construction ⚠️ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ ↑/k up • ↓/j down • ↳ confirm • ? toggle │ │ │ + ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/component/component_test.go b/src/internal/tui/component/component_test.go index cb6fa8a4..fa85ea3c 100644 --- a/src/internal/tui/component/component_test.go +++ b/src/internal/tui/component/component_test.go @@ -3,19 +3,127 @@ package component_test import ( "os" "testing" + "time" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/internal/testhelpers" + "github.com/defenseunicorns/lula/src/internal/tui/common" "github.com/defenseunicorns/lula/src/internal/tui/component" "github.com/defenseunicorns/lula/src/pkg/common/oscal" + "github.com/muesli/termenv" ) +const ( + timeout = time.Second * 20 + maxRetries = 3 + height = common.DefaultHeight + width = common.DefaultWidth + + validCompDef = "../../../test/unit/common/oscal/valid-generated-component.yaml" + validCompDefValidations = "../../../test/unit/common/oscal/valid-component.yaml" + validCompDefMulti = "../../../test/unit/common/oscal/valid-multi-component.yaml" + validCompDefMultiValidations = "../../../test/unit/common/oscal/valid-multi-component-validations.yaml" +) + +func init() { + lipgloss.SetColorProfile(termenv.Ascii) +} + +// TestComponentDefinitionBasicView tests that the model is created correctly from a component definition with validations +func TestComponentDefinitionBasicView(t *testing.T) { + oscalModel := testhelpers.OscalFromPath(t, validCompDef) + model := component.NewComponentDefinitionModel(oscalModel.ComponentDefinition) + model.Open(height, width) + + msgs := []tea.Msg{} + + err := testhelpers.RunTestModelView(t, model, nil, msgs, timeout, maxRetries, height, width) + if err != nil { + t.Fatal(err) + } +} + +// TestComponentDefinitionComponentSwitch tests that the component picker executes correctly +func TestComponentDefinitionComponentSwitch(t *testing.T) { + oscalModel := testhelpers.OscalFromPath(t, validCompDefMulti) + model := component.NewComponentDefinitionModel(oscalModel.ComponentDefinition) + model.Open(height, width) + + msgs := []tea.Msg{ + tea.KeyMsg{Type: tea.KeyRight}, // Select component + tea.KeyMsg{Type: tea.KeyEnter}, // enter component selection overlay + tea.KeyMsg{Type: tea.KeyDown}, // navigate down + tea.KeyMsg{Type: tea.KeyEnter}, // select new component, exit overlay + tea.KeyMsg{Type: tea.KeyRight}, // Select framework + tea.KeyMsg{Type: tea.KeyRight}, // Select control + tea.KeyMsg{Type: tea.KeyEnter}, // Open control + } + + err := testhelpers.RunTestModelView(t, model, nil, msgs, timeout, maxRetries, height, width) + if err != nil { + t.Fatal(err) + } +} + +// TestComponentControlSelect tests that the user can navigate to a control, select it, and see expected +// remarks, description, and validations +func TestComponentControlSelect(t *testing.T) { + oscalModel := testhelpers.OscalFromPath(t, validCompDefMulti) + model := component.NewComponentDefinitionModel(oscalModel.ComponentDefinition) + model.Open(height, width) + + msgs := []tea.Msg{ + tea.KeyMsg{Type: tea.KeyRight}, // Select component + tea.KeyMsg{Type: tea.KeyRight}, // Select framework + tea.KeyMsg{Type: tea.KeyRight}, // Select control + tea.KeyMsg{Type: tea.KeyEnter}, // Open control + } + + err := testhelpers.RunTestModelView(t, model, nil, msgs, timeout, maxRetries, height, width) + if err != nil { + t.Fatal(err) + } +} + +// TestEditViewComponentDefinitionModel tests that the editing views of the component definition model are correct +func TestEditViewComponentDefinitionModel(t *testing.T) { + oscalModel := testhelpers.OscalFromPath(t, validCompDefValidations) + model := component.NewComponentDefinitionModel(oscalModel.ComponentDefinition) + model.Open(height, width) + + msgs := []tea.Msg{ + tea.KeyMsg{Type: tea.KeyRight}, // Select component + tea.KeyMsg{Type: tea.KeyRight}, // Select framework + tea.KeyMsg{Type: tea.KeyRight}, // Select control + tea.KeyMsg{Type: tea.KeyEnter}, // Open control + tea.KeyMsg{Type: tea.KeyRight}, // Navigate to remarks + tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}, // Edit remarks + tea.KeyMsg{Type: tea.KeyCtrlE}, // Newline + tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t', 'e', 's', 't'}}, // Add "test" to remarks + tea.KeyMsg{Type: tea.KeyEnter}, // Confirm edit + } + + reset := func() tea.Model { + resetOscalModel := testhelpers.OscalFromPath(t, validCompDefValidations) + resetModel := component.NewComponentDefinitionModel(resetOscalModel.ComponentDefinition) + resetModel.Open(height, width) + return resetModel + } + + err := testhelpers.RunTestModelView(t, model, reset, msgs, timeout, maxRetries, height, width) + if err != nil { + t.Fatal(err) + } +} + // TestEditComponentDefinitionModel tests that a component definition model can be modified, written, and re-read func TestEditComponentDefinitionModel(t *testing.T) { tempOscalFile := testhelpers.CreateTempFile(t, "yaml") defer os.Remove(tempOscalFile.Name()) - oscalModel := testhelpers.OscalFromPath(t, "../../../test/unit/common/oscal/valid-generated-component.yaml") + oscalModel := testhelpers.OscalFromPath(t, validCompDef) model := component.NewComponentDefinitionModel(oscalModel.ComponentDefinition) testControlId := "ac-1" diff --git a/src/internal/tui/component/testdata/TestComponentControlSelect.golden b/src/internal/tui/component/testdata/TestComponentControlSelect.golden new file mode 100644 index 00000000..16e1886e --- /dev/null +++ b/src/internal/tui/component/testdata/TestComponentControlSelect.golden @@ -0,0 +1,56 @@ + ↳ select • ↑/k move up • ↓/j move down • / filter • ? toggle help + ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ + Selected Component │Component A - 7c02500a-6e33-44e0-82ee-f…│ Selected Framework │https://raw.githubusercontent.com/usnis…│ + ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ +╭───────────────╮ ╭─────────╮ +│ Controls List ├─────────────────────────────╮ │ Remarks ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +╰───────────────╯ ╰─────────╯ + │ │ │ STATEMENT: │ + │ 3 items │ │ The organization:a. Develops, documents, and disseminates to [Assignment: organization-defined organization-defined personnel or roles]: │ + │ │ │ 1. An access control policy that addresses purpose, scope, roles, responsibilities, management commitment, coordination among │ + │ │ ac-1 │ │ organizational entities, and compliance; and │ + │ │ 67dd59c4-0340-4aed-a49d-002815b50157 │ │ 2. Procedures to facilitate the implementation of the access control policy and associated access controls; and │ + │ │ │ b. Reviews and updates the current: │ + │ ac-2 │ │ 1. Access control policy [Assignment: organization-defined organization-defined frequency]; and │ + │ 663e7c26-3bfe-4c71-b423-10d8338d5445 │ │ 2. Access control procedures [Assignment: organization-defined organization-defined frequency]. │ + │ │ │ │ + │ ac-3 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ 07e1e996-5ae7-4b0b-b4c0-01f35729e442 │ ╭─────────────╮ + │ │ │ Description ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰─────────────╯ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ │ ╭─────────────╮ + │ │ │ Validations ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰─────────────╯ + │ │ │ │ + │ │ │ No items │ + │ │ │ │ + │ │ │ No items. │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/testdata/TestNewAssessmentResultsModel.golden b/src/internal/tui/component/testdata/TestComponentDefinitionBasicView.golden similarity index 50% rename from src/internal/tui/testdata/TestNewAssessmentResultsModel.golden rename to src/internal/tui/component/testdata/TestComponentDefinitionBasicView.golden index e838673b..5394da3f 100644 --- a/src/internal/tui/testdata/TestNewAssessmentResultsModel.golden +++ b/src/internal/tui/component/testdata/TestComponentDefinitionBasicView.golden @@ -1,59 +1,56 @@ -╭─────────────────────╮╭───────────────────╮╭────────────────────╮╭────────────────╮╭───────────────────────────╮╭─────────╮╭─────────╮ -│ ComponentDefinition ││ AssessmentResults ││ SystemSecurityPlan ││ AssessmentPlan ││ PlanOfActionAndMilestones ││ Catalog ││ Profile │ -┴─────────────────────┴┘ └┴────────────────────┴┴────────────────┴┴───────────────────────────┴┴─────────┴┴─────────┴───────────────────────────────────────────────────────────────── - ? toggle help - ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ - Selected Result │Lula Validation Result - 41787700-2a4c-…│ Compare Result │No Result Selected │ - ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ - ╭───────────────╮ ╭─────────╮ - │ Findings List ├─────────────────────────────╮ │ Summary ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - ╰───────────────╯ ╰─────────╯ - │ │ │ ⚠️ Summary Under Construction ⚠️ │ - │ 1 item │ │ │ - │ │ │ │ - │ ID-1 │ │ │ - │ not-satisfied │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - │ │ ╭──────────────╮ - │ │ │ Observations ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ ╰──────────────╯ - │ │ │ ⚠️ Observations Under Construction ⚠️ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ ↑/k up • ↓/j down • ↳ confirm • ? toggle │ │ │ - ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file + ←/h, →/l navigation • tab/shift+tab switch models • ? toggle help + ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ + Selected Component │Component Title - e8011225-75bc-43e5-98…│ Selected Framework │https://raw.githubusercontent.com/usnis…│ + ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ +╭───────────────╮ ╭─────────╮ +│ Controls List ├─────────────────────────────╮ │ Remarks ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +╰───────────────╯ ╰─────────╯ + │ │ │ │ + │ 4 items │ │ │ + │ │ │ │ + │ ac-1 │ │ │ + │ 84517036-ea65-4bfa-992d-f89a1b0d9822 │ │ │ + │ │ │ │ + │ ac-3 │ │ │ + │ 0d4fe96a-df1c-4199-87eb-cf0c1385e9ab │ │ │ + │ │ │ │ + │ ac-3.2 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ 2131d7da-19a3-462f-a0bd-2345e5098ea5 │ ╭─────────────╮ + │ │ │ Description ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ ac-4 │ ╰─────────────╯ + │ 3ee0aedf-d047-4902-9d77-4d7f0072f213 │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ │ ╭─────────────╮ + │ │ │ Validations ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰─────────────╯ + │ │ │ │ + │ │ │ No items │ + │ │ │ │ + │ │ │ No items. │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/component/testdata/TestComponentDefinitionComponentSwitch.golden b/src/internal/tui/component/testdata/TestComponentDefinitionComponentSwitch.golden new file mode 100644 index 00000000..597cceab --- /dev/null +++ b/src/internal/tui/component/testdata/TestComponentDefinitionComponentSwitch.golden @@ -0,0 +1,56 @@ + ↳ select • ↑/k move up • ↓/j move down • / filter • ? toggle help + ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ + Selected Component │Component B - 4cb1810c-d0d8-404e-b346-5…│ Selected Framework │https://raw.githubusercontent.com/usnis…│ + ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ +╭───────────────╮ ╭─────────╮ +│ Controls List ├─────────────────────────────╮ │ Remarks ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +╰───────────────╯ ╰─────────╯ + │ │ │ STATEMENT: │ + │ 3 items │ │ The information system enforces approved authorizations for controlling the flow of information within the system and between interconnected │ + │ │ │ systems based on [Assignment: organization-defined organization-defined information flow control policies]. │ + │ │ ac-4 │ │ │ + │ │ ea9f3b4d-64c2-4631-ace5-55428552f9aa │ │ │ + │ │ │ │ + │ ac-5 │ │ │ + │ 1976b301-115f-48a4-b847-3374aa3b98d5 │ │ │ + │ │ │ │ + │ ac-6 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ be129429-290b-4516-9390-f4d38067fbd0 │ ╭─────────────╮ + │ │ │ Description ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰─────────────╯ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ │ ╭─────────────╮ + │ │ │ Validations ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰─────────────╯ + │ │ │ │ + │ │ │ No items │ + │ │ │ │ + │ │ │ No items. │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/component/testdata/TestEditViewComponentDefinitionModel.golden b/src/internal/tui/component/testdata/TestEditViewComponentDefinitionModel.golden new file mode 100644 index 00000000..86722a19 --- /dev/null +++ b/src/internal/tui/component/testdata/TestEditViewComponentDefinitionModel.golden @@ -0,0 +1,56 @@ + e edit • ctrl+s save • ←/h, →/l navigation • tab/shift+tab switch models • ? toggle help + ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ + Selected Component │lula - A9D5204C-7E5B-4C43-BD49-34DF759B…│ Selected Framework │https://github.com/defenseunicorns/lula │ + ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ +╭───────────────╮ ╭─────────╮ +│ Controls List ├─────────────────────────────╮ │ Remarks ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +╰───────────────╯ ╰─────────╯ + │ │ │ Here are some remarks about this control. │ + │ 1 item │ │ test │ + │ │ │ │ + │ │ ID-1 │ │ │ + │ │ 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ │ ╭─────────────╮ + │ │ │ Description ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰─────────────╯ + │ │ │ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim │ + │ │ │ veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in │ + │ │ │ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia │ + │ │ │ deserunt mollit anim id est laborum. │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ + │ │ ╭─────────────╮ + │ │ │ Validations ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ + │ │ ╰─────────────╯ + │ │ │ │ + │ │ │ 1 item │ + │ │ │ │ + │ │ │ Validate pods with label foo=bar │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + │ │ │ │ + ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/model_test.go b/src/internal/tui/model_test.go index 89fa7e77..c366beb2 100644 --- a/src/internal/tui/model_test.go +++ b/src/internal/tui/model_test.go @@ -7,159 +7,35 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/x/exp/teatest" "github.com/defenseunicorns/lula/src/internal/testhelpers" "github.com/defenseunicorns/lula/src/internal/tui" "github.com/defenseunicorns/lula/src/internal/tui/common" "github.com/muesli/termenv" ) -const timeout = time.Second * 20 +const ( + timeout = time.Second * 20 + maxRetries = 3 + height = common.DefaultHeight + width = common.DefaultWidth +) func init() { lipgloss.SetColorProfile(termenv.Ascii) - tea.Sequence() } -// TestNewComponentDefinitionModel tests that the NewOSCALModel creates the expected model from component definition file -func TestNewComponentDefinitionModel(t *testing.T) { +// TestNewOSCALModel tests that the NewOSCALModel creates the expected model from component definition file +func TestNewOSCALModel(t *testing.T) { tempLog := testhelpers.CreateTempFile(t, "log") defer os.Remove(tempLog.Name()) oscalModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-component.yaml") model := tui.NewOSCALModel(oscalModel, "", tempLog) - testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(common.DefaultWidth, common.DefaultHeight)) - - if err := testModel.Quit(); err != nil { - t.Fatal(err) - } - - if testModel == nil { - t.Fatal("testModel is nil") - } - - fm := testModel.FinalModel(t, teatest.WithFinalTimeout(timeout)) - - teatest.RequireEqualOutput(t, []byte(fm.View())) -} - -// TestMultiComponentDefinitionModel tests that the NewOSCALModel creates the expected model from component definition file -// and checks the component selection overlay -> new component section -func TestMultiComponentDefinitionModel(t *testing.T) { - tempLog := testhelpers.CreateTempFile(t, "log") - defer os.Remove(tempLog.Name()) - - oscalModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-multi-component.yaml") - model := tui.NewOSCALModel(oscalModel, "", tempLog) - testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(common.DefaultWidth, common.DefaultHeight)) - - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select component - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // enter component selection overlay - testModel.Send(tea.KeyMsg{Type: tea.KeyDown}) // navigate down - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // select new component, exit overlay - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select framework - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select control - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Open control - - if err := testModel.Quit(); err != nil { - t.Fatal(err) - } - - if testModel == nil { - t.Fatal("testModel is nil") - } - - fm := testModel.FinalModel(t, teatest.WithFinalTimeout(timeout)) - - teatest.RequireEqualOutput(t, []byte(fm.View())) -} - -// TestNewAssessmentResultsModel tests that the NewOSCALModel creates the expected model from assessment results file -func TestNewAssessmentResultsModel(t *testing.T) { - tempLog := testhelpers.CreateTempFile(t, "log") - defer os.Remove(tempLog.Name()) - - oscalModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-assessment-results.yaml") - model := tui.NewOSCALModel(oscalModel, "", tempLog) - testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(common.DefaultWidth, common.DefaultHeight)) - - testModel.Send(tea.KeyMsg{Type: tea.KeyTab}) + msgs := []tea.Msg{} - if err := testModel.Quit(); err != nil { + err := testhelpers.RunTestModelView(t, model, nil, msgs, timeout, maxRetries, height, width) + if err != nil { t.Fatal(err) } - - if testModel == nil { - t.Fatal("testModel is nil") - } - - fm := testModel.FinalModel(t, teatest.WithFinalTimeout(timeout)) - - teatest.RequireEqualOutput(t, []byte(fm.View())) -} - -// TestComponentControlSelect tests that the user can navigate to a control, select it, and see expected -// remarks, description, and validations -func TestComponentControlSelect(t *testing.T) { - tempLog := testhelpers.CreateTempFile(t, "log") - defer os.Remove(tempLog.Name()) - - oscalModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-component.yaml") - model := tui.NewOSCALModel(oscalModel, "", tempLog) - testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(common.DefaultWidth, common.DefaultHeight)) - - // Navigate to the control - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select component - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select framework - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select control - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Open control - - if err := testModel.Quit(); err != nil { - t.Fatal(err) - } - - if testModel == nil { - t.Fatal("testModel is nil") - } - - fm := testModel.FinalModel(t, teatest.WithFinalTimeout(timeout)) - - teatest.RequireEqualOutput(t, []byte(fm.View())) -} - -// TestEditViewComponentDefinitionModel tests that the editing views of the component definition model are correct -func TestEditViewComponentDefinitionModel(t *testing.T) { - tempLog := testhelpers.CreateTempFile(t, "log") - defer os.Remove(tempLog.Name()) - tempOscalFile := testhelpers.CreateTempFile(t, "yaml") - defer os.Remove(tempOscalFile.Name()) - - oscalModel := testhelpers.OscalFromPath(t, "../../test/unit/common/oscal/valid-component.yaml") - model := tui.NewOSCALModel(oscalModel, tempOscalFile.Name(), tempLog) - - testModel := teatest.NewTestModel(t, model, teatest.WithInitialTermSize(common.DefaultWidth, common.DefaultHeight)) - - // Edit the remarks - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select component - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select framework - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Select control - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Open control - testModel.Send(tea.KeyMsg{Type: tea.KeyRight}) // Navigate to remarks - testModel.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'e'}}) // Edit remarks - testModel.Send(tea.KeyMsg{Type: tea.KeyCtrlE}) // Newline - testModel.Send(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'t', 'e', 's', 't'}}) // Add "test" to remarks - testModel.Send(tea.KeyMsg{Type: tea.KeyEnter}) // Open control - - if err := testModel.Quit(); err != nil { - t.Fatal(err) - } - - if testModel == nil { - t.Fatal("testModel is nil") - } - - fm := testModel.FinalModel(t, teatest.WithFinalTimeout(timeout)) - - teatest.RequireEqualOutput(t, []byte(fm.View())) } diff --git a/src/internal/tui/testdata/TestComponentControlSelect.golden b/src/internal/tui/testdata/TestComponentControlSelect.golden deleted file mode 100644 index 921e81ae..00000000 --- a/src/internal/tui/testdata/TestComponentControlSelect.golden +++ /dev/null @@ -1,59 +0,0 @@ -╭─────────────────────╮╭───────────────────╮╭────────────────────╮╭────────────────╮╭───────────────────────────╮╭─────────╮╭─────────╮ -│ ComponentDefinition ││ AssessmentResults ││ SystemSecurityPlan ││ AssessmentPlan ││ PlanOfActionAndMilestones ││ Catalog ││ Profile │ -┘ └┴───────────────────┴┴────────────────────┴┴────────────────┴┴───────────────────────────┴┴─────────┴┴─────────┴───────────────────────────────────────────────────────────────── - ↳ select • ↑/k move up • ↓/j move down • / filter • ? toggle help - ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ - Selected Component │lula - A9D5204C-7E5B-4C43-BD49-34DF759B…│ Selected Framework │https://github.com/defenseunicorns/lula │ - ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ - ╭───────────────╮ ╭─────────╮ - │ Controls List ├─────────────────────────────╮ │ Remarks ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - ╰───────────────╯ ╰─────────╯ - │ │ │ Here are some remarks about this control. │ - │ 1 item │ │ │ - │ │ │ │ - │ │ ID-1 │ │ │ - │ │ 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - │ │ ╭─────────────╮ - │ │ │ Description ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ ╰─────────────╯ - │ │ │ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim │ - │ │ │ veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in │ - │ │ │ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia │ - │ │ │ deserunt mollit anim id est laborum. │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - │ │ ╭─────────────╮ - │ │ │ Validations ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ ╰─────────────╯ - │ │ │ │ - │ │ │ 1 item │ - │ │ │ │ - │ │ │ Validate pods with label foo=bar │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/testdata/TestEditViewComponentDefinitionModel.golden b/src/internal/tui/testdata/TestEditViewComponentDefinitionModel.golden deleted file mode 100644 index 3480acee..00000000 --- a/src/internal/tui/testdata/TestEditViewComponentDefinitionModel.golden +++ /dev/null @@ -1,59 +0,0 @@ -╭─────────────────────╮╭───────────────────╮╭────────────────────╮╭────────────────╮╭───────────────────────────╮╭─────────╮╭─────────╮ -│ ComponentDefinition ││ AssessmentResults ││ SystemSecurityPlan ││ AssessmentPlan ││ PlanOfActionAndMilestones ││ Catalog ││ Profile │ -┘ └┴───────────────────┴┴────────────────────┴┴────────────────┴┴───────────────────────────┴┴─────────┴┴─────────┴───────────────────────────────────────────────────────────────── - e edit • ctrl+s save • ←/h, →/l navigation • tab/shift+tab switch models • ? toggle help - ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ - Selected Component │lula - A9D5204C-7E5B-4C43-BD49-34DF759B…│ Selected Framework │https://github.com/defenseunicorns/lula │ - ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ - ╭───────────────╮ ╭─────────╮ - │ Controls List ├─────────────────────────────╮ │ Remarks ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - ╰───────────────╯ ╰─────────╯ - │ │ │ Here are some remarks about this control. │ - │ 1 item │ │ test │ - │ │ │ │ - │ │ ID-1 │ │ │ - │ │ 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - │ │ ╭─────────────╮ - │ │ │ Description ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ ╰─────────────╯ - │ │ │ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim │ - │ │ │ veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in │ - │ │ │ voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia │ - │ │ │ deserunt mollit anim id est laborum. │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - │ │ ╭─────────────╮ - │ │ │ Validations ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ ╰─────────────╯ - │ │ │ │ - │ │ │ 1 item │ - │ │ │ │ - │ │ │ Validate pods with label foo=bar │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/testdata/TestMultiComponentDefinitionModel.golden b/src/internal/tui/testdata/TestMultiComponentDefinitionModel.golden deleted file mode 100644 index 2eecc66d..00000000 --- a/src/internal/tui/testdata/TestMultiComponentDefinitionModel.golden +++ /dev/null @@ -1,59 +0,0 @@ -╭─────────────────────╮╭───────────────────╮╭────────────────────╮╭────────────────╮╭───────────────────────────╮╭─────────╮╭─────────╮ -│ ComponentDefinition ││ AssessmentResults ││ SystemSecurityPlan ││ AssessmentPlan ││ PlanOfActionAndMilestones ││ Catalog ││ Profile │ -┘ └┴───────────────────┴┴────────────────────┴┴────────────────┴┴───────────────────────────┴┴─────────┴┴─────────┴───────────────────────────────────────────────────────────────── - ↳ select • ↑/k move up • ↓/j move down • / filter • ? toggle help - ╭────────────────────────────────────────╮ ╭────────────────────────────────────────╮ - Selected Component │Component A - 7c02500a-6e33-44e0-82ee-f…│ Selected Framework │https://raw.githubusercontent.com/usnis…│ - ╰────────────────────────────────────────╯ ╰────────────────────────────────────────╯ - ╭───────────────╮ ╭─────────╮ - │ Controls List ├─────────────────────────────╮ │ Remarks ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - ╰───────────────╯ ╰─────────╯ - │ │ │ STATEMENT: │ - │ 3 items │ │ The organization:a. Develops, documents, and disseminates to [Assignment: organization-defined organization-defined personnel or roles]: │ - │ │ │ 1. An access control policy that addresses purpose, scope, roles, responsibilities, management commitment, coordination among │ - │ │ ac-1 │ │ organizational entities, and compliance; and │ - │ │ 67dd59c4-0340-4aed-a49d-002815b50157 │ │ 2. Procedures to facilitate the implementation of the access control policy and associated access controls; and │ - │ │ │ b. Reviews and updates the current: │ - │ ac-2 │ │ 1. Access control policy [Assignment: organization-defined organization-defined frequency]; and │ - │ 663e7c26-3bfe-4c71-b423-10d8338d5445 │ │ 2. Access control procedures [Assignment: organization-defined organization-defined frequency]. │ - │ │ │ │ - │ ac-3 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - │ 07e1e996-5ae7-4b0b-b4c0-01f35729e442 │ ╭─────────────╮ - │ │ │ Description ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ ╰─────────────╯ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ - │ │ ╭─────────────╮ - │ │ │ Validations ├─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ ╰─────────────╯ - │ │ │ │ - │ │ │ No items │ - │ │ │ │ - │ │ │ No items. │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - │ │ │ │ - ╰────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/src/internal/tui/testdata/TestNewComponentDefinitionModel.golden b/src/internal/tui/testdata/TestNewOSCALModel.golden similarity index 100% rename from src/internal/tui/testdata/TestNewComponentDefinitionModel.golden rename to src/internal/tui/testdata/TestNewOSCALModel.golden diff --git a/src/pkg/common/common.go b/src/pkg/common/common.go index 21989281..52769344 100644 --- a/src/pkg/common/common.go +++ b/src/pkg/common/common.go @@ -7,6 +7,7 @@ import ( "io" "os" "path/filepath" + "regexp" "strings" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" @@ -185,3 +186,10 @@ func ValidationFromString(raw, uuid string) (validation types.LulaValidation, er return validation, nil } + +// CleanMultilineString removes leading and trailing whitespace from a multiline string +func CleanMultilineString(str string) string { + re := regexp.MustCompile(`[ \t]+\r?\n`) + formatted := re.ReplaceAllString(str, "\n") + return formatted +} diff --git a/src/pkg/common/common_test.go b/src/pkg/common/common_test.go index f5fdd1ee..8243bf9e 100644 --- a/src/pkg/common/common_test.go +++ b/src/pkg/common/common_test.go @@ -347,7 +347,7 @@ func TestValidationToResource(t *testing.T) { t.Parallel() validation := &common.Validation{ Metadata: &common.Metadata{ - UUID: "1234", + UUID: "1f639c6b-4e86-4c66-88b2-22dbf6d7ac02", Name: "Test Validation", }, Provider: &common.Provider{ @@ -395,8 +395,8 @@ func TestValidationToResource(t *testing.T) { t.Errorf("ToResource() error = %v", err) } - if resource.UUID == validation.Metadata.UUID { - t.Errorf("ToResource() description = \"\", want a valid UUID") + if resource.UUID != validation.Metadata.UUID { + t.Errorf("ToResource() resource UUID %s should match created validation UUID %s", resource.UUID, validation.Metadata.UUID) } }) diff --git a/src/pkg/common/composition/composition.go b/src/pkg/common/composition/composition.go index 65ca4b68..19a37085 100644 --- a/src/pkg/common/composition/composition.go +++ b/src/pkg/common/composition/composition.go @@ -2,14 +2,15 @@ package composition import ( "bytes" + "context" "fmt" "io" "os" - "path/filepath" "github.com/defenseunicorns/go-oscal/src/pkg/uuid" "github.com/defenseunicorns/go-oscal/src/pkg/versioning" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" + "github.com/defenseunicorns/lula/src/internal/template" "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/network" "github.com/defenseunicorns/lula/src/pkg/common/oscal" @@ -17,29 +18,49 @@ import ( k8syaml "k8s.io/apimachinery/pkg/util/yaml" ) +type RenderedContent string + +type CompositionContext struct { + modelDir string + templateRenderer *template.TemplateRenderer + renderTemplate bool + renderValidations bool + renderType template.RenderType +} + +func New(opts ...Option) (*CompositionContext, error) { + var compositionCtx CompositionContext + + for _, opt := range opts { + if err := opt(&compositionCtx); err != nil { + return nil, err + } + } + + return &compositionCtx, nil +} + // ComposeFromPath composes an OSCAL model from a file path -func ComposeFromPath(inputFile string) (model *oscalTypes_1_1_2.OscalCompleteSchema, err error) { - data, err := os.ReadFile(inputFile) +func (cc *CompositionContext) ComposeFromPath(ctx context.Context, path string) (model *oscalTypes_1_1_2.OscalCompleteSchema, err error) { + data, err := os.ReadFile(path) if err != nil { return nil, err } - // Change Cwd to the directory of the component definition - // This is needed to resolve relative paths in the remote validations - dirPath := filepath.Dir(inputFile) - message.Infof("changing cwd to %s", dirPath) - resetCwd, err := common.SetCwdToFileDir(dirPath) - if err != nil { - return nil, err + // Template if renderTemplate is true -> Only renders the local data (e.g., what is in the file) + if cc.renderTemplate { + data, err = cc.templateRenderer.Render(string(data), cc.renderType) + if err != nil { + return nil, err + } } - defer resetCwd() model, err = oscal.NewOscalModel(data) if err != nil { return nil, err } - err = ComposeComponentDefinitions(model.ComponentDefinition) + err = cc.ComposeComponentDefinitions(ctx, model.ComponentDefinition, cc.modelDir) if err != nil { return nil, err } @@ -48,13 +69,13 @@ func ComposeFromPath(inputFile string) (model *oscalTypes_1_1_2.OscalCompleteSch } // ComposeComponentDefinitions composes an OSCAL component definition by adding the remote resources to the back matter and updating with back matter links. -func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) error { +func (cc *CompositionContext) ComposeComponentDefinitions(ctx context.Context, compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { if compDef == nil { return fmt.Errorf("component definition is nil") } // Compose the component validations - err := ComposeComponentValidations(compDef) + err := cc.ComposeComponentValidations(ctx, compDef, baseDir) if err != nil { return err } @@ -73,12 +94,19 @@ func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) if compDef.ImportComponentDefinitions != nil { for _, importComponentDef := range *compDef.ImportComponentDefinitions { - // Fetch the response - response, err := network.Fetch(importComponentDef.Href) + response, err := network.Fetch(importComponentDef.Href, network.WithBaseDir(baseDir)) if err != nil { return err } + // template here if renderTemplate is true + if cc.renderTemplate { + response, err = cc.templateRenderer.Render(string(response), cc.renderType) + if err != nil { + return err + } + } + // Handle multi-docs componentDefs, err := readComponentDefinitionsFromYaml(response) if err != nil { @@ -86,7 +114,9 @@ func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) } // Unmarshal the component definition for _, importDef := range componentDefs { - err = ComposeComponentDefinitions(importDef) + // Reconcile the base directory from the import component definition href + importDir := network.GetLocalFileDir(importComponentDef.Href, baseDir) + err = cc.ComposeComponentDefinitions(ctx, importDef, importDir) if err != nil { return err } @@ -107,13 +137,13 @@ func ComposeComponentDefinitions(compDef *oscalTypes_1_1_2.ComponentDefinition) } // ComposeComponentValidations compiles the component validations by adding the remote resources to the back matter and updating with back matter links. -func ComposeComponentValidations(compDef *oscalTypes_1_1_2.ComponentDefinition) error { +func (cc *CompositionContext) ComposeComponentValidations(ctx context.Context, compDef *oscalTypes_1_1_2.ComponentDefinition, baseDir string) error { if compDef == nil { return fmt.Errorf("component definition is nil") } - resourceMap := NewResourceStoreFromBackMatter(compDef.BackMatter) + resourceMap := NewResourceStoreFromBackMatter(cc, compDef.BackMatter) // If there are no components, there is nothing to do if compDef.Components == nil { @@ -133,7 +163,7 @@ func ComposeComponentValidations(compDef *oscalTypes_1_1_2.ComponentDefinition) for _, link := range *implementedRequirement.Links { if common.IsLulaLink(link) { - ids, err := resourceMap.AddFromLink(&link) + ids, err := resourceMap.AddFromLink(&link, baseDir) if err != nil { // return err newId := uuid.NewUUID() diff --git a/src/pkg/common/composition/composition_test.go b/src/pkg/common/composition/composition_test.go index 85e15561..ac1037e8 100644 --- a/src/pkg/common/composition/composition_test.go +++ b/src/pkg/common/composition/composition_test.go @@ -1,29 +1,52 @@ package composition_test import ( + "context" "os" + "path/filepath" "reflect" "testing" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" - "github.com/defenseunicorns/lula/src/pkg/common" + "github.com/defenseunicorns/lula/src/internal/template" "github.com/defenseunicorns/lula/src/pkg/common/composition" "gopkg.in/yaml.v3" ) const ( - allRemote = "../../../test/e2e/scenarios/validation-composition/component-definition.yaml" - allRemoteBadHref = "../../../test/e2e/scenarios/validation-composition/component-definition-bad-href.yaml" - allLocal = "../../../test/unit/common/composition/component-definition-all-local.yaml" - allLocalBadHref = "../../../test/unit/common/composition/component-definition-all-local-bad-href.yaml" - localAndRemote = "../../../test/unit/common/composition/component-definition-local-and-remote.yaml" - subComponentDef = "../../../test/unit/common/composition/component-definition-import-compdefs.yaml" - compDefMultiImport = "../../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" + allRemote = "../../../test/e2e/scenarios/validation-composition/component-definition.yaml" + allRemoteBadHref = "../../../test/e2e/scenarios/validation-composition/component-definition-bad-href.yaml" + allLocal = "../../../test/unit/common/composition/component-definition-all-local.yaml" + allLocalBadHref = "../../../test/unit/common/composition/component-definition-all-local-bad-href.yaml" + localAndRemote = "../../../test/unit/common/composition/component-definition-local-and-remote.yaml" + subComponentDef = "../../../test/unit/common/composition/component-definition-import-compdefs.yaml" + compDefMultiImport = "../../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" + compDefNestedImport = "../../../test/unit/common/composition/component-definition-import-nested-compdef.yaml" + compDefTmpl = "../../../test/unit/common/composition/component-definition-template.yaml" + compDefNestedTmpl = "../../../test/unit/common/composition/component-definition-import-nested-compdef-template.yaml" ) func TestComposeFromPath(t *testing.T) { + test := func(t *testing.T, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { + t.Helper() + ctx := context.Background() + + options := append([]composition.Option{composition.WithModelFromLocalPath(path)}, opts...) + cc, err := composition.New(options...) + if err != nil { + return nil, err + } + + model, err := cc.ComposeFromPath(ctx, path) + if err != nil { + return nil, err + } + + return model, nil + } + t.Run("No imports, local validations", func(t *testing.T) { - model, err := composition.ComposeFromPath(allLocal) + model, err := test(t, allLocal) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -33,7 +56,7 @@ func TestComposeFromPath(t *testing.T) { }) t.Run("No imports, local validations, bad href", func(t *testing.T) { - model, err := composition.ComposeFromPath(allLocalBadHref) + model, err := test(t, allLocalBadHref) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -43,7 +66,17 @@ func TestComposeFromPath(t *testing.T) { }) t.Run("No imports, remote validations", func(t *testing.T) { - model, err := composition.ComposeFromPath(allRemote) + model, err := test(t, allRemote) + if err != nil { + t.Fatalf("Error composing component definitions: %v", err) + } + if model == nil { + t.Error("expected the model to be composed") + } + }) + + t.Run("Nested imports, no components", func(t *testing.T) { + model, err := test(t, compDefNestedImport) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -53,7 +86,7 @@ func TestComposeFromPath(t *testing.T) { }) t.Run("No imports, bad remote validations", func(t *testing.T) { - model, err := composition.ComposeFromPath(allRemoteBadHref) + model, err := test(t, allRemoteBadHref) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -62,15 +95,109 @@ func TestComposeFromPath(t *testing.T) { } }) + t.Run("Templated component definition, error", func(t *testing.T) { + model, err := test(t, compDefTmpl) + if err == nil { + t.Fatalf("Should encounter error composing component definitions: %v", err) + } + if model != nil { + t.Error("expected the model not to be composed") + } + }) + + // Test the templating of the component definition where the validation is not rendered -> empty resources in backmatter + t.Run("Templated component definition with nested imports, validations not rendered - no resources", func(t *testing.T) { + tmplOpts := []composition.Option{ + composition.WithRenderSettings("constants", true), + composition.WithTemplateRenderer("constants", map[string]interface{}{ + "templated_comp_def": interface{}("component-definition-template.yaml"), + "type": interface{}("software"), + "title": interface{}("lula"), + }, []template.VariableConfig{}, []string{}), + } + + model, err := test(t, compDefTmpl, tmplOpts...) + if err != nil { + t.Fatalf("Error composing component definitions: %v", err) + } + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == nil { + t.Error("expected the component definition to have components") + } + + if compDefComposed.BackMatter == nil { + t.Error("expected the component definition to have back matter") + } + + if compDefComposed.BackMatter.Resources == nil { + t.Error("expected the component definition to have back matter resources") + } + + if len(*compDefComposed.BackMatter.Resources) != 0 { + t.Error("expected the back matter to contain 0 resources (validation)") + } + }) + + // Test the templating of the component definition with nested templated imports + t.Run("Templated component definition with nested imports, validations rendered", func(t *testing.T) { + tmplOpts := []composition.Option{ + composition.WithRenderSettings("constants", true), + composition.WithTemplateRenderer("constants", map[string]interface{}{ + "templated_comp_def": interface{}("component-definition-template.yaml"), + "type": interface{}("software"), + "title": interface{}("lula"), + "resources": interface{}(map[string]interface{}{ + "name": interface{}("test-pod-label"), + "namespace": interface{}("validation-test"), + "exemptions": []interface{}{ + interface{}("one"), + interface{}("two"), + interface{}("three"), + }, + }), + }, []template.VariableConfig{}, []string{}), + } + model, err := test(t, compDefNestedTmpl, tmplOpts...) + if err != nil { + t.Fatalf("Error composing component definitions: %v", err) + } + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == nil { + t.Error("expected the component definition to have components") + } + + if compDefComposed.BackMatter == nil { + t.Error("expected the component definition to have back matter") + } + + if compDefComposed.BackMatter.Resources == nil { + t.Fatalf("expected the component definition to have back matter resources") + } + + if len(*compDefComposed.BackMatter.Resources) != 1 { + t.Error("expected the back matter to contain 1 resource (validation)") + } + }) + t.Run("Errors when file does not exist", func(t *testing.T) { - _, err := composition.ComposeFromPath("nonexistent") + _, err := test(t, "nonexistent") if err == nil { t.Error("expected an error") } }) t.Run("Resolves relative paths", func(t *testing.T) { - model, err := composition.ComposeFromPath(localAndRemote) + model, err := test(t, localAndRemote) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } @@ -81,21 +208,44 @@ func TestComposeFromPath(t *testing.T) { } func TestComposeComponentDefinitions(t *testing.T) { + test := func(t *testing.T, compDef *oscalTypes_1_1_2.ComponentDefinition, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { + t.Helper() + ctx := context.Background() + + options := append([]composition.Option{composition.WithModelFromLocalPath(path)}, opts...) + cc, err := composition.New(options...) + if err != nil { + return nil, err + } + + baseDir := filepath.Dir(path) + + err = cc.ComposeComponentDefinitions(ctx, compDef, baseDir) + if err != nil { + return nil, err + } + + return &oscalTypes_1_1_2.OscalCompleteSchema{ + ComponentDefinition: compDef, + }, nil + } + t.Run("No imports, local validations", func(t *testing.T) { og := getComponentDef(allLocal, t) compDef := getComponentDef(allLocal, t) - reset, err := common.SetCwdToFileDir(allLocal) - defer reset() - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - err = composition.ComposeComponentDefinitions(compDef) + + model, err := test(t, compDef, allLocal) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + // Only the last-modified timestamp should be different - if !reflect.DeepEqual(*og.BackMatter, *compDef.BackMatter) { + if !reflect.DeepEqual(*og.BackMatter, *compDefComposed.BackMatter) { t.Error("expected the back matter to be unchanged") } }) @@ -103,17 +253,18 @@ func TestComposeComponentDefinitions(t *testing.T) { t.Run("No imports, remote validations", func(t *testing.T) { og := getComponentDef(allRemote, t) compDef := getComponentDef(allRemote, t) - reset, err := common.SetCwdToFileDir(allRemote) - defer reset() - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - err = composition.ComposeComponentDefinitions(compDef) + + model, err := test(t, compDef, allRemote) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } - if reflect.DeepEqual(*og, *compDef) { + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if reflect.DeepEqual(*og, *compDefComposed) { t.Errorf("expected component definition to have changed.") } }) @@ -121,21 +272,22 @@ func TestComposeComponentDefinitions(t *testing.T) { t.Run("Imports, no components", func(t *testing.T) { og := getComponentDef(subComponentDef, t) compDef := getComponentDef(subComponentDef, t) - reset, err := common.SetCwdToFileDir(subComponentDef) - defer reset() - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - err = composition.ComposeComponentDefinitions(compDef) + + model, err := test(t, compDef, subComponentDef) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } - if compDef.Components == og.Components { + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == og.Components { t.Error("expected there to be components") } - if compDef.BackMatter == og.BackMatter { + if compDefComposed.BackMatter == og.BackMatter { t.Error("expected the back matter to be changed") } }) @@ -143,47 +295,110 @@ func TestComposeComponentDefinitions(t *testing.T) { t.Run("imports, no components, multiple component definitions from import", func(t *testing.T) { og := getComponentDef(compDefMultiImport, t) compDef := getComponentDef(compDefMultiImport, t) - reset, err := common.SetCwdToFileDir(compDefMultiImport) - defer reset() + + model, err := test(t, compDef, compDefMultiImport) if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) + t.Fatalf("Error composing component definitions: %v", err) + } + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == og.Components { + t.Error("expected there to be components") + } + + if compDefComposed.BackMatter == og.BackMatter { + t.Error("expected the back matter to be changed") + } + + if len(*compDefComposed.Components) != 1 { + t.Error("expected there to be 1 component") } - err = composition.ComposeComponentDefinitions(compDef) + }) + + // Both "imported" components have the same component (by UUID), so those are merged + // Both components have the same control-impementation (by control ID, not UUID), those are merged + // All validations are linked to that single control-implementation + t.Run("nested imports, directory changes", func(t *testing.T) { + og := getComponentDef(compDefNestedImport, t) + compDef := getComponentDef(compDefNestedImport, t) + + model, err := test(t, compDef, compDefNestedImport) if err != nil { t.Fatalf("Error composing component definitions: %v", err) } - if compDef.Components == og.Components { - t.Error("expected there to be components") + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") + } + + if compDefComposed.Components == og.Components { + t.Error("expected there to be new components") } - if compDef.BackMatter == og.BackMatter { + if compDefComposed.BackMatter == og.BackMatter { t.Error("expected the back matter to be changed") } - if len(*compDef.Components) != 1 { - t.Error("expected there to be 2 components") + components := *compDefComposed.Components + if len(components) != 1 { + t.Error("expected there to be 1 component") + } + + if len(*components[0].ControlImplementations) != 1 { + t.Error("expected there to be 1 control implementation") + } + + if len(*compDefComposed.BackMatter.Resources) != 7 { + t.Error("expected the back matter to contain 7 resources (validations)") } }) } -func TestCompileComponentValidations(t *testing.T) { +func TestComposeComponentValidations(t *testing.T) { + test := func(t *testing.T, compDef *oscalTypes_1_1_2.ComponentDefinition, path string, opts ...composition.Option) (*oscalTypes_1_1_2.OscalCompleteSchema, error) { + t.Helper() + ctx := context.Background() + + options := append([]composition.Option{composition.WithModelFromLocalPath(path)}, opts...) + cc, err := composition.New(options...) + if err != nil { + return nil, err + } + + baseDir := filepath.Dir(path) + + err = cc.ComposeComponentValidations(ctx, compDef, baseDir) + if err != nil { + return nil, err + } + + return &oscalTypes_1_1_2.OscalCompleteSchema{ + ComponentDefinition: compDef, + }, nil + } t.Run("all local", func(t *testing.T) { og := getComponentDef(allLocal, t) compDef := getComponentDef(allLocal, t) - reset, err := common.SetCwdToFileDir(allLocal) - defer reset() + + model, err := test(t, compDef, allLocal) if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) + t.Fatalf("error composing validations: %v", err) } - err = composition.ComposeComponentValidations(compDef) - if err != nil { - t.Fatalf("Error compiling component validations: %v", err) + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") } // Only the last-modified timestamp should be different - if !reflect.DeepEqual(*og.BackMatter, *compDef.BackMatter) { + if !reflect.DeepEqual(*og.BackMatter, *compDefComposed.BackMatter) { t.Error("expected the back matter to be unchanged") } }) @@ -191,24 +406,26 @@ func TestCompileComponentValidations(t *testing.T) { t.Run("all remote", func(t *testing.T) { og := getComponentDef(allRemote, t) compDef := getComponentDef(allRemote, t) - reset, err := common.SetCwdToFileDir(allRemote) - defer reset() + + model, err := test(t, compDef, allRemote) if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) + t.Fatalf("error composing validations: %v", err) } - err = composition.ComposeComponentValidations(compDef) - if err != nil { - t.Fatalf("Error compiling component validations: %v", err) + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") } - if reflect.DeepEqual(*og, *compDef) { + + if reflect.DeepEqual(*og, *compDefComposed) { t.Error("expected the component definition to be changed") } - if compDef.BackMatter == nil { + if compDefComposed.BackMatter == nil { t.Error("expected the component definition to have back matter") } - if og.Metadata.LastModified == compDef.Metadata.LastModified { + if og.Metadata.LastModified == compDefComposed.Metadata.LastModified { t.Error("expected the component definition to have a different last modified timestamp") } }) @@ -216,17 +433,18 @@ func TestCompileComponentValidations(t *testing.T) { t.Run("local and remote", func(t *testing.T) { og := getComponentDef(localAndRemote, t) compDef := getComponentDef(localAndRemote, t) - reset, err := common.SetCwdToFileDir(localAndRemote) - defer reset() + + model, err := test(t, compDef, localAndRemote) if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) + t.Fatalf("error composing validations: %v", err) } - err = composition.ComposeComponentValidations(compDef) - if err != nil { - t.Fatalf("Error compiling component validations: %v", err) + + compDefComposed := model.ComponentDefinition + if compDefComposed == nil { + t.Error("expected the component definition to be non-nil") } - if reflect.DeepEqual(*og, *compDef) { + if reflect.DeepEqual(*og, *compDefComposed) { t.Error("expected the component definition to be changed") } }) diff --git a/src/pkg/common/composition/options.go b/src/pkg/common/composition/options.go new file mode 100644 index 00000000..e9d75c9f --- /dev/null +++ b/src/pkg/common/composition/options.go @@ -0,0 +1,88 @@ +package composition + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/defenseunicorns/lula/src/cmd/common" + "github.com/defenseunicorns/lula/src/internal/template" + "github.com/defenseunicorns/lula/src/pkg/message" +) + +type Option func(*CompositionContext) error + +// TODO: add remote option? +func WithModelFromLocalPath(path string) Option { + return func(ctx *CompositionContext) error { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return fmt.Errorf("input-file: %v does not exist - unable to digest document", path) + } + + absPath, err := filepath.Abs(path) + if err != nil { + return fmt.Errorf("error getting absolute path: %v", err) + } + ctx.modelDir = filepath.Dir(absPath) + + return nil + } +} + +func WithRenderSettings(renderTypeString string, renderValidations bool) Option { + return func(ctx *CompositionContext) error { + if renderTypeString == "" { + ctx.renderTemplate = false + ctx.renderValidations = false + if renderValidations { + message.Warn("`render` not specified, `render-validations` will be ignored") + } + return nil + } + ctx.renderTemplate = true + ctx.renderValidations = renderValidations + + // Get the template render type + renderType, err := template.ParseRenderType(renderTypeString) + if err != nil { + message.Warnf("invalid render type, defaulting to non-sensitive: %v", err) + renderType = template.NONSENSITIVE + } + ctx.renderType = renderType + + return nil + } +} + +func WithTemplateRenderer(renderTypeString string, constants map[string]interface{}, variables []template.VariableConfig, setOpts []string) Option { + return func(ctx *CompositionContext) error { + if renderTypeString == "" { + ctx.renderTemplate = false + if len(setOpts) > 0 { + message.Warn("`render` not specified, the --set options will be ignored") + } + return nil + } + + // Get overrides from setOpts flag + overrides, err := common.ParseTemplateOverrides(setOpts) + if err != nil { + return fmt.Errorf("error parsing template overrides: %v", err) + } + + // Handles merging viper config file data + environment variables + // Throws an error if config keys are invalid for templating + templateData, err := template.CollectTemplatingData(constants, variables, overrides) + if err != nil { + return fmt.Errorf("error collecting templating data: %v", err) + } + + // need to update the template with the templateString... + tr := template.NewTemplateRenderer(templateData) + + ctx.templateRenderer = tr + + return nil + } +} diff --git a/src/pkg/common/composition/resource-store.go b/src/pkg/common/composition/resource-store.go index 82d077fb..02a050d7 100644 --- a/src/pkg/common/composition/resource-store.go +++ b/src/pkg/common/composition/resource-store.go @@ -13,13 +13,16 @@ type ResourceStore struct { existing map[string]*oscalTypes_1_1_2.Resource fetched map[string]*oscalTypes_1_1_2.Resource hrefIdMap map[string][]string + cctx *CompositionContext } // NewResourceStoreFromBackMatter creates a new resource store from the back matter of a component definition. -func NewResourceStoreFromBackMatter(backMatter *oscalTypes_1_1_2.BackMatter) *ResourceStore { +func NewResourceStoreFromBackMatter(cctx *CompositionContext, backMatter *oscalTypes_1_1_2.BackMatter) *ResourceStore { store := &ResourceStore{ - existing: make(map[string]*oscalTypes_1_1_2.Resource), - fetched: make(map[string]*oscalTypes_1_1_2.Resource), + existing: make(map[string]*oscalTypes_1_1_2.Resource), + fetched: make(map[string]*oscalTypes_1_1_2.Resource), + hrefIdMap: make(map[string][]string), + cctx: cctx, } if backMatter != nil && *backMatter.Resources != nil { @@ -94,7 +97,7 @@ func (s *ResourceStore) Has(id string) bool { } // AddFromLink adds resources from a link to the store. -func (s *ResourceStore) AddFromLink(link *oscalTypes_1_1_2.Link) (ids []string, err error) { +func (s *ResourceStore) AddFromLink(link *oscalTypes_1_1_2.Link, baseDir string) (ids []string, err error) { if link == nil { return nil, fmt.Errorf("link is nil") } @@ -112,29 +115,37 @@ func (s *ResourceStore) AddFromLink(link *oscalTypes_1_1_2.Link) (ids []string, return ids, err } - return s.fetchFromRemoteLink(link) + return s.fetchFromRemoteLink(link, baseDir) } -func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link) (ids []string, err error) { +// fetchFromRemoteLink expects a link to a remote validation or validation template +func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link, baseDir string) (ids []string, err error) { wantedId := common.TrimIdPrefix(link.ResourceFragment) - validationBytes, err := network.Fetch(link.Href) + validationBytes, err := network.Fetch(link.Href, network.WithBaseDir(baseDir)) if err != nil { - return nil, err + return nil, fmt.Errorf("error fetching remote resource: %v", err) + } + + // template here if renderValidations is true + if s.cctx.renderValidations { + validationBytes, err = s.cctx.templateRenderer.Render(string(validationBytes), s.cctx.renderType) + if err != nil { + return nil, err + } } validationArr, err := common.ReadValidationsFromYaml(validationBytes) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to read validations from link: %v", err) } isSingleValidation := len(validationArr) == 1 for _, validation := range validationArr { resource, err := validation.ToResource() if err != nil { - return nil, err + return nil, fmt.Errorf("unable to create validation resource: %v", err) } - s.AddFetched(resource) if wantedId == resource.UUID || wantedId == common.WILDCARD || isSingleValidation { @@ -142,5 +153,7 @@ func (s *ResourceStore) fetchFromRemoteLink(link *oscalTypes_1_1_2.Link) (ids [] } } + s.SetHrefIds(link.Href, ids) + return ids, err } diff --git a/src/pkg/common/network/network.go b/src/pkg/common/network/network.go index d218c296..aa0d77b1 100644 --- a/src/pkg/common/network/network.go +++ b/src/pkg/common/network/network.go @@ -66,11 +66,35 @@ func ParseChecksum(src string) (*url.URL, string, error) { return url, checksum, nil } +type fetchOpts struct { + baseDir string +} + +type FetchOption func(*fetchOpts) error + +func WithBaseDir(baseDir string) FetchOption { + return func(opts *fetchOpts) error { + // check if baseDir is a valid directory + if _, err := os.Stat(baseDir); err != nil { + return err + } + opts.baseDir = baseDir + return nil + } +} + +// TODO: add more options for timeout, retries, etc. + // Fetch fetches the response body from a given URL after validating it. // If the URL scheme is "file", the file is fetched from the local filesystem. // If the URL scheme is "http", "https", or "ftp", the file is fetched from the remote server. // If the URL has a checksum, the file is validated against the checksum. -func Fetch(inputURL string) (bytes []byte, err error) { +func Fetch(inputURL string, opts ...FetchOption) (bytes []byte, err error) { + config := &fetchOpts{} + for _, opt := range opts { + opt(config) + } + url, checksum, err := ParseChecksum(inputURL) if err != nil { return bytes, err @@ -78,7 +102,7 @@ func Fetch(inputURL string) (bytes []byte, err error) { // If the URL is a file, fetch the file from the local filesystem if url.Scheme == "file" { - bytes, err = FetchLocalFile(url) + bytes, err = FetchLocalFile(url, config) if err != nil { return bytes, err } @@ -114,7 +138,7 @@ func Fetch(inputURL string) (bytes []byte, err error) { // FetchLocalFile fetches a local file from a given URL. // If the URL scheme is not "file", an error is returned. // If the URL is relative, the component definition directory is prepended if set, otherwise the current working directory is prepended. -func FetchLocalFile(url *url.URL) ([]byte, error) { +func FetchLocalFile(url *url.URL, config *fetchOpts) ([]byte, error) { if url.Scheme != "file" { return nil, errors.New("expected file URL scheme") } @@ -122,18 +146,29 @@ func FetchLocalFile(url *url.URL) ([]byte, error) { // If the request uri is absolute, use it directly if _, err := os.Stat(requestUri); err != nil { - // if relative pre-pend cwd - cwd, err := os.Getwd() - if err != nil { - return nil, err - } - requestUri = filepath.Join(cwd, requestUri) + requestUri = filepath.Join(config.baseDir, url.Host, requestUri) } bytes, err := os.ReadFile(requestUri) return bytes, err } +func GetLocalFileDir(inputURL, baseDir string) string { + url, err := url.Parse(inputURL) + if err != nil { + return "" + } + requestUri := url.RequestURI() + + if url.Scheme == "file" { + fullPath := filepath.Join(baseDir, url.Host, requestUri) + if _, err := os.Stat(fullPath); err == nil { + return filepath.Dir(fullPath) + } + } + return "" +} + // ValidateChecksum validates a given checksum against a given []bytes. // Supports MD5, SHA-1, SHA-256, and SHA-512. // Returns an error if the hash does not match. diff --git a/src/pkg/common/network/network_test.go b/src/pkg/common/network/network_test.go index b62bed4c..1b18b589 100644 --- a/src/pkg/common/network/network_test.go +++ b/src/pkg/common/network/network_test.go @@ -48,13 +48,13 @@ func TestParseUrl(t *testing.T) { }, { name: "File url", - input: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", + input: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", wantErr: false, wantChecksum: false, }, { name: "With Checksum", - input: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", + input: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", wantErr: false, wantChecksum: true, }, @@ -94,12 +94,12 @@ func TestFetch(t *testing.T) { }, { name: "File", - url: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", + url: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml", wantErr: false, }, { name: "File with checksum SHA-256", - url: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", + url: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@394f5efa7aa5c3163a631d0f2640efe836af07c77fa7b27749f00819dd869058", wantErr: false, }, { @@ -109,7 +109,7 @@ func TestFetch(t *testing.T) { }, { name: "Invalid Sha", - url: "file://../../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@2d4c18916f2fd70f9488b76690c2eed06789d5fd12e06152a01a8ef7600c41ef", + url: "file://../../../test/e2e/scenarios/remote-validations/validation.opa.yaml@2d4c18916f2fd70f9488b76690c2eed06789d5fd12e06152a01a8ef7600c41ef", wantErr: true, }, } diff --git a/src/pkg/common/oscal/complete-schema.go b/src/pkg/common/oscal/complete-schema.go index 0d768f62..792e0d37 100644 --- a/src/pkg/common/oscal/complete-schema.go +++ b/src/pkg/common/oscal/complete-schema.go @@ -52,16 +52,16 @@ func WriteOscalModel(filePath string, model *oscalTypes_1_1_2.OscalModels) error // If the file exists - read the data into the model existingFileBytes, err := os.ReadFile(filePath) if err != nil { - return err + return fmt.Errorf("error reading file: %v", err) } existingModel, err := NewOscalModel(existingFileBytes) if err != nil { - return err + return fmt.Errorf("error getting existing model: %v", err) } existingModelType, err := GetOscalModel(existingModel) if err != nil { - return nil + return fmt.Errorf("error getting existing model type: %v", err) } if existingModelType != modelType { @@ -224,6 +224,27 @@ func GetOscalModel(model *oscalTypes_1_1_2.OscalModels) (modelType string, err e } +// ValidOSCALModelAtPath takes a path and returns a bool indicating if the model exists/is valid +// bool = T/F that oscal model exists, error = if not nil OSCAL model is invalid +func ValidOSCALModelAtPath(path string) (bool, error) { + _, err := os.Stat(path) + if err != nil { + return false, nil + } + + data, err := os.ReadFile(path) + if err != nil { + return true, err + } + + _, err = NewOscalModel(data) + if err != nil { + return true, err + } + + return true, nil +} + // InjectIntoOSCALModel takes a model target and a map[string]interface{} of values to inject into the model func InjectIntoOSCALModel(target *oscalTypes_1_1_2.OscalModels, values map[string]interface{}, path string) (*oscalTypes_1_1_2.OscalModels, error) { // If the target is nil, return an error diff --git a/src/pkg/common/oscal/component.go b/src/pkg/common/oscal/component.go index eccac3ee..a5c5c265 100644 --- a/src/pkg/common/oscal/component.go +++ b/src/pkg/common/oscal/component.go @@ -592,10 +592,14 @@ func MakeComponentDeterminstic(component *oscalTypes_1_1_2.ComponentDefinition) backmatter := *component.BackMatter if backmatter.Resources != nil { resources := *backmatter.Resources - sort.Slice(resources, func(i, j int) bool { - return resources[i].Title < resources[j].Title - }) - backmatter.Resources = &resources + if len(resources) == 0 { + backmatter.Resources = nil + } else { + sort.Slice(resources, func(i, j int) bool { + return resources[i].Title < resources[j].Title + }) + backmatter.Resources = &resources + } } component.BackMatter = &backmatter } diff --git a/src/pkg/common/schemas/schema_test.go b/src/pkg/common/schemas/schema_test.go index e825c9e4..cf94ec12 100644 --- a/src/pkg/common/schemas/schema_test.go +++ b/src/pkg/common/schemas/schema_test.go @@ -65,7 +65,7 @@ func TestListSchemas(t *testing.T) { func TestValidate(t *testing.T) { t.Parallel() // Enable parallel execution of tests - validationPath := "../../../test/unit/common/validation/opa.validation.yaml" + validationPath := "../../../test/unit/common/validation/validation.opa.yaml" validationData, err := os.ReadFile(validationPath) if err != nil { t.Errorf("Expected no error, got %v", err) diff --git a/src/pkg/common/types.go b/src/pkg/common/types.go index 208bb369..1f9eeb70 100644 --- a/src/pkg/common/types.go +++ b/src/pkg/common/types.go @@ -49,25 +49,39 @@ func (v *Validation) MarshalYaml() ([]byte, error) { // ToResource converts a Validation object to a Resource object func (v *Validation) ToResource() (resource *oscalTypes_1_1_2.Resource, err error) { - resource = &oscalTypes_1_1_2.Resource{} - resource.Title = v.Metadata.Name - if v.Metadata.UUID != "" { - resource.UUID = v.Metadata.UUID + resourceUuid := uuid.NewUUID() + title := "Lula Validation" + if v.Metadata != nil { + if v.Metadata.UUID != "" && checkValidUuid(v.Metadata.UUID) { + resourceUuid = v.Metadata.UUID + } + if v.Metadata.Name != "" { + title = v.Metadata.Name + } } else { - resource.UUID = uuid.NewUUID() + v.Metadata = &Metadata{} } - // If the provider is opa, trim whitespace from the rego - if v.Provider != nil && v.Provider.OpaSpec != nil { - re := regexp.MustCompile(`[ \t]+\r?\n`) - v.Provider.OpaSpec.Rego = re.ReplaceAllString(v.Provider.OpaSpec.Rego, "\n") + // Update the metadata for the validation + v.Metadata.UUID = resourceUuid + v.Metadata.Name = title + + if v.Provider != nil { + if v.Provider.OpaSpec != nil { + // Clean multiline string in rego + v.Provider.OpaSpec.Rego = CleanMultilineString(v.Provider.OpaSpec.Rego) + } } validationBytes, err := v.MarshalYaml() if err != nil { return nil, err } - resource.Description = string(validationBytes) - return resource, nil + + return &oscalTypes_1_1_2.Resource{ + Title: title, + UUID: resourceUuid, + Description: string(validationBytes), + }, nil } // Metadata is a structure that contains the name and uuid of a validation @@ -159,3 +173,8 @@ func (validation *Validation) ToLulaValidation(uuid string) (lulaValidation type return lulaValidation, nil } + +func checkValidUuid(uuid string) bool { + re := regexp.MustCompile(`^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[45][0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$`) + return re.MatchString(uuid) +} diff --git a/src/test/e2e/api_validation_test.go b/src/test/e2e/api_validation_test.go index f6b83301..4bb9f868 100644 --- a/src/test/e2e/api_validation_test.go +++ b/src/test/e2e/api_validation_test.go @@ -55,7 +55,7 @@ func TestApiValidation(t *testing.T) { oscalPath := "./scenarios/api-field/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } @@ -139,7 +139,7 @@ func TestApiValidation(t *testing.T) { oscalPath := "./scenarios/api-field/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/standard/lula-config.yaml b/src/test/e2e/cmd/lula-config.yaml similarity index 84% rename from src/test/e2e/standard/lula-config.yaml rename to src/test/e2e/cmd/lula-config.yaml index 911f39ff..2506d95b 100644 --- a/src/test/e2e/standard/lula-config.yaml +++ b/src/test/e2e/cmd/lula-config.yaml @@ -1,6 +1,7 @@ constants: type: software title: lula + templated_comp_def: component-definition-template.yaml resources: name: test-pod-label diff --git a/src/test/e2e/cmd/main_test.go b/src/test/e2e/cmd/main_test.go new file mode 100644 index 00000000..4cf4b2e3 --- /dev/null +++ b/src/test/e2e/cmd/main_test.go @@ -0,0 +1,100 @@ +package cmd_test + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + "github.com/defenseunicorns/lula/src/test/util" + "github.com/google/go-cmp/cmp" + "github.com/spf13/cobra" +) + +var updateGolden = flag.Bool("update", false, "update golden files") + +func TestMain(m *testing.M) { + flag.Parse() + m.Run() +} + +func runCmdTest(t *testing.T, rootCmd *cobra.Command, cmdArgs ...string) error { + _, _, err := util.ExecuteCommand(rootCmd, cmdArgs...) + if err != nil { + return err + } + + return nil +} + +func runCmdTestWithGolden(t *testing.T, goldenFilePath, goldenFileName string, rootCmd *cobra.Command, cmdArgs ...string) error { + _, output, err := util.ExecuteCommand(rootCmd, cmdArgs...) + if err != nil { + return err + } + + testGolden(t, goldenFilePath, goldenFileName, output) + + return nil +} + +func runCmdTestWithOutputFile(t *testing.T, goldenFilePath, goldenFileName, outExt string, rootCmd *cobra.Command, cmdArgs ...string) error { + tempFileName := fmt.Sprintf("output-%s.%s", goldenFileName, outExt) + defer os.Remove(tempFileName) + + cmdArgs = append(cmdArgs, "-o", tempFileName) + _, _, err := util.ExecuteCommand(rootCmd, cmdArgs...) + if err != nil { + return err + } + + // Read the output file + data, err := os.ReadFile(tempFileName) + if err != nil { + return err + } + + // Scrub timestamps + data = scrubTimestamps(data) + + testGolden(t, goldenFilePath, goldenFileName, string(data)) + + return nil +} + +func testGolden(t *testing.T, filePath, filename, got string) { + t.Helper() + + got = strings.ReplaceAll(got, "\r\n", "\n") + + wd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + goldenPath := filepath.Join(wd, "testdata", filePath, filename+".golden") + + if *updateGolden { + if err := os.MkdirAll(filepath.Dir(goldenPath), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(goldenPath, []byte(got), 0o600); err != nil { + t.Fatal(err) + } + } + + wantBytes, _ := os.ReadFile(goldenPath) + want := string(wantBytes) + diff := cmp.Diff(want, got) + + if diff != "" { + t.Fatalf("`%s` does not match.\n\nDiff:\n%s", goldenPath, diff) + } +} + +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")) +} diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-constants.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-constants.golden new file mode 100644 index 00000000..6b4f3b9e --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-constants.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-masked.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-masked.golden new file mode 100644 index 00000000..289f1908 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-masked.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "********" == "********" + } + msg = validate.msg + + value_of_my_secret := ******** + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden new file mode 100644 index 00000000..72f8ebc6 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-no-validation-templated-valid.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: '{{ .const.resources.name }}' + namespaces: + - '{{ .const.resources.namespace }}' + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 458d2d84-b7f2-4679-8964-6f9a9dfe51eb + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} + type: opa + title: Test validation with templating + uuid: 458d2d84-b7f2-4679-8964-6f9a9dfe51eb + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#458d2d84-b7f2-4679-8964-6f9a9dfe51eb' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-non-sensitive.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-non-sensitive.golden new file mode 100644 index 00000000..9f5cf2ea --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-non-sensitive.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-overrides.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-overrides.golden new file mode 100644 index 00000000..451aa446 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated-overrides.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: foo + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "my-secret" == "********" + } + msg = validate.msg + + value_of_my_secret := my-secret + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden new file mode 100644 index 00000000..06c97e34 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/composed-file-templated.golden @@ -0,0 +1,80 @@ +component-definition: + back-matter: + resources: + - description: | + domain: + kubernetes-spec: + create-resources: null + resources: + - description: "" + name: podvt + resource-rule: + group: "" + name: test-pod-label + namespaces: + - validation-test + resource: pods + version: v1 + type: kubernetes + lula-version: "" + metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + provider: + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { "one", "two", "three" } + "this-should-be-overridden" == "my-env-var" + "" == "********" + } + msg = validate.msg + + value_of_my_secret := + type: opa + title: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 + components: + - control-implementations: + - description: Validate generic security requirements + implemented-requirements: + - control-id: ID-1 + description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: '#99fc662c-109a-4e26-8398-75f3db67f862' + rel: lula + text: local path template validation + uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF + role-id: provider + title: lula + type: software + uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + metadata: + last-modified: XXX + oscal-version: 1.1.2 + parties: + - links: + - href: https://github.com/defenseunicorns/lula + rel: website + name: Lula Development + type: organization + uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + title: Lula Demo + version: "20220913" + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F diff --git a/src/test/e2e/cmd/testdata/tools/compose/help.golden b/src/test/e2e/cmd/testdata/tools/compose/help.golden new file mode 100644 index 00000000..0c0f0705 --- /dev/null +++ b/src/test/e2e/cmd/testdata/tools/compose/help.golden @@ -0,0 +1,27 @@ + +Lula Composition of an OSCAL component definition. Used to compose remote validations within a component definition in order to resolve any references for portability. + +Supports templating of the composed component definition with the following configuration options: +- To compose with templating applied, specify '--render, -r' with values of 'all', 'non-sensitive', 'constants', or 'masked' (choice will depend on the use case for the composed content) +- To render Lula Validations include '--render-validations' +- To perform any manual overrides to the template data, specify '--set, -s' with the format '.const.key=value' or '.var.key=value' + +Usage: + compose [flags] + +Examples: + +To compose an OSCAL Model: + lula tools compose -f ./oscal-component.yaml + +To indicate a specific output file: + lula tools compose -f ./oscal-component.yaml -o composed-oscal-component.yaml + + +Flags: + -h, --help help for compose + -f, --input-file string the path to the target OSCAL component definition + -o, --output-file -composed the path to the output file. If not specified, the output file will be the original filename with -composed appended + -r, --render string values to render the template with, options are: masked, constants, non-sensitive, all + --render-validations extend render to remote Lula Validations + -s, --set strings set value overrides for templated data diff --git a/src/test/e2e/standard/testdata/help.golden b/src/test/e2e/cmd/testdata/tools/template/help.golden similarity index 87% rename from src/test/e2e/standard/testdata/help.golden rename to src/test/e2e/cmd/testdata/tools/template/help.golden index 4b858bed..1a3761e3 100644 --- a/src/test/e2e/standard/testdata/help.golden +++ b/src/test/e2e/cmd/testdata/tools/template/help.golden @@ -1,7 +1,7 @@ Resolving templated artifacts with configuration data Usage: - lula tools template [flags] + template [flags] Examples: @@ -27,6 +27,3 @@ Flags: -o, --output-file string the path to the output file. If not specified, the output file will be directed to stdout -r, --render string values to render the template with, options are: masked, constants, non-sensitive, all (default "masked") -s, --set strings set a value in the template data - -Global Flags: - -l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info") diff --git a/src/test/e2e/standard/testdata/validation.golden b/src/test/e2e/cmd/testdata/tools/template/validation.golden similarity index 87% rename from src/test/e2e/standard/testdata/validation.golden rename to src/test/e2e/cmd/testdata/tools/template/validation.golden index ff319eb2..1ef42a2a 100644 --- a/src/test/e2e/standard/testdata/validation.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation.golden @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/e2e/standard/testdata/validation_all.golden b/src/test/e2e/cmd/testdata/tools/template/validation_all.golden similarity index 83% rename from src/test/e2e/standard/testdata/validation_all.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_all.golden index dc22f17b..d2a0b80f 100644 --- a/src/test/e2e/standard/testdata/validation_all.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_all.golden @@ -1,10 +1,13 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: resources: - name: podvt resource-rule: - name: foo + name: test-pod-label version: v1 resource: pods namespaces: [validation-test] diff --git a/src/test/e2e/standard/testdata/validation_constants.golden b/src/test/e2e/cmd/testdata/tools/template/validation_constants.golden similarity index 84% rename from src/test/e2e/standard/testdata/validation_constants.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_constants.golden index 91e814e7..08c2c483 100644 --- a/src/test/e2e/standard/testdata/validation_constants.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_constants.golden @@ -1,10 +1,13 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: resources: - name: podvt resource-rule: - name: foo + name: test-pod-label version: v1 resource: pods namespaces: [validation-test] diff --git a/src/test/e2e/standard/testdata/validation_non_sensitive.golden b/src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden similarity index 84% rename from src/test/e2e/standard/testdata/validation_non_sensitive.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden index e1282c92..bfcf2ab0 100644 --- a/src/test/e2e/standard/testdata/validation_non_sensitive.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_non_sensitive.golden @@ -1,10 +1,13 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: resources: - name: podvt resource-rule: - name: foo + name: test-pod-label version: v1 resource: pods namespaces: [validation-test] diff --git a/src/test/e2e/standard/testdata/validation_with_env_vars.golden b/src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden similarity index 87% rename from src/test/e2e/standard/testdata/validation_with_env_vars.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden index c429cbc2..58d0d839 100644 --- a/src/test/e2e/standard/testdata/validation_with_env_vars.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_with_env_vars.golden @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/e2e/standard/testdata/validation_with_set.golden b/src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden similarity index 87% rename from src/test/e2e/standard/testdata/validation_with_set.golden rename to src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden index f7be2dc4..7b382763 100644 --- a/src/test/e2e/standard/testdata/validation_with_set.golden +++ b/src/test/e2e/cmd/testdata/tools/template/validation_with_set.golden @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/e2e/cmd/tools_compose_test.go b/src/test/e2e/cmd/tools_compose_test.go new file mode 100644 index 00000000..2186059c --- /dev/null +++ b/src/test/e2e/cmd/tools_compose_test.go @@ -0,0 +1,126 @@ +package cmd_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/defenseunicorns/lula/src/cmd/tools" + "github.com/defenseunicorns/lula/src/pkg/common/oscal" + "github.com/defenseunicorns/lula/src/pkg/message" +) + +func TestToolsComposeCommand(t *testing.T) { + message.NoProgress = true + + test := func(t *testing.T, args ...string) error { + rootCmd := tools.ComposeCommand() + + return runCmdTest(t, rootCmd, args...) + } + + testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := tools.ComposeCommand() + + return runCmdTestWithGolden(t, "tools/compose/", goldenFileName, rootCmd, args...) + } + + testAgainstOutputFile := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := tools.ComposeCommand() + + return runCmdTestWithOutputFile(t, "tools/compose/", goldenFileName, "yaml", rootCmd, args...) + } + + t.Run("Compose Validation", func(t *testing.T) { + tempDir := t.TempDir() + outputFile := filepath.Join(tempDir, "output.yaml") + + err := test(t, "composed-file", + "-f", "../../unit/common/composition/component-definition-import-compdefs.yaml", + "-o", outputFile, + ) + + require.NoError(t, err) + + // Check that the output file is valid OSCAL + compiledBytes, err := os.ReadFile(outputFile) + require.NoErrorf(t, err, "error reading composed component definition: %v", err) + + compiledModel, err := oscal.NewOscalModel(compiledBytes) + require.NoErrorf(t, err, "error creating oscal model from composed component definition: %v", err) + + require.NotNilf(t, compiledModel.ComponentDefinition, "composed component definition is nil") + + require.Equalf(t, 3, len(*compiledModel.ComponentDefinition.BackMatter.Resources), "expected 3 resources, got %d", len(*compiledModel.ComponentDefinition.BackMatter.Resources)) + }) + + t.Run("Compose Validation with templating - all", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated", + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "all", + "--render-validations") + require.NoError(t, err) + }) + + t.Run("Compose Validation with templating - non-sensitive", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-non-sensitive", + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "non-sensitive", + "--render-validations") + require.NoError(t, err) + }) + + t.Run("Compose Validation with templating - constants", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-constants", + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "constants", + "--render-validations") + require.NoError(t, err) + }) + + t.Run("Compose Validation with templating - masked", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-masked", + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "masked", + "--render-validations") + require.NoError(t, err) + }) + + t.Run("Compose Validation with templating and overrides", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-overrides", + "-f", "../../unit/common/composition/component-definition-template.yaml", + "-r", "all", + "--render-validations", + "--set", ".const.resources.name=foo,.var.some_lula_secret=my-secret") + require.NoError(t, err) + }) + + t.Run("Compose Validation with no templating on validations for valid validation template", func(t *testing.T) { + err := testAgainstOutputFile(t, "composed-file-templated-no-validation-templated-valid", + "-f", "../../unit/common/composition/component-definition-template-valid-validation-tmpl.yaml", + "-r", "all") + require.NoError(t, err) + }) + + t.Run("Test help", func(t *testing.T) { + err := testAgainstGolden(t, "help", "--help") + require.NoError(t, err) + }) + + t.Run("Test Compose - invalid file error", func(t *testing.T) { + err := test(t, "-f", "not-a-file.yaml") + require.ErrorContains(t, err, "error creating composition context") + }) + + t.Run("Test Compose - invalid file schema error", func(t *testing.T) { + err := test(t, "-f", "../../unit/common/composition/component-definition-template.yaml") + require.ErrorContains(t, err, "error composing model from path") + }) + + t.Run("Test Compose - invalid output file", func(t *testing.T) { + err := test(t, "-f", "../../unit/common/composition/component-definition-multi.yaml", "-o", "../../unit/common/validation/validation.opa.yaml") + require.ErrorContains(t, err, "invalid OSCAL model at output file") + }) +} diff --git a/src/test/e2e/cmd/tools_template_test.go b/src/test/e2e/cmd/tools_template_test.go new file mode 100644 index 00000000..02a6608d --- /dev/null +++ b/src/test/e2e/cmd/tools_template_test.go @@ -0,0 +1,81 @@ +package cmd_test + +import ( + "os" + + "testing" + + "github.com/defenseunicorns/lula/src/cmd/tools" + "github.com/stretchr/testify/require" +) + +// var updateGolden = flag.Bool("update", false, "update golden files") + +func TestToolsTemplateCommand(t *testing.T) { + + test := func(t *testing.T, args ...string) error { + rootCmd := tools.TemplateCommand() + + return runCmdTest(t, rootCmd, args...) + } + + testAgainstGolden := func(t *testing.T, goldenFileName string, args ...string) error { + rootCmd := tools.TemplateCommand() + + return runCmdTestWithGolden(t, "tools/template/", goldenFileName, rootCmd, args...) + } + + t.Run("Template Validation", func(t *testing.T) { + err := testAgainstGolden(t, "validation", "-f", "../../unit/common/validation/validation.tmpl.yaml") + require.NoError(t, err) + }) + + t.Run("Template Validation with env vars", func(t *testing.T) { + os.Setenv("LULA_VAR_SOME_ENV_VAR", "my-env-var") + defer os.Unsetenv("LULA_VAR_SOME_ENV_VAR") + err := testAgainstGolden(t, "validation_with_env_vars", "-f", "../../unit/common/validation/validation.tmpl.yaml") + require.NoError(t, err) + }) + + t.Run("Template Validation with set", func(t *testing.T) { + err := testAgainstGolden(t, "validation_with_set", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", ".const.resources.name=foo") + require.NoError(t, err) + }) + + t.Run("Template Validation for all", func(t *testing.T) { + os.Setenv("LULA_VAR_SOME_LULA_SECRET", "env-secret") + defer os.Unsetenv("LULA_VAR_SOME_LULA_SECRET") + err := testAgainstGolden(t, "validation_all", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "all") + require.NoError(t, err) + }) + + t.Run("Template Validation for non-sensitive", func(t *testing.T) { + err := testAgainstGolden(t, "validation_non_sensitive", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "non-sensitive") + require.NoError(t, err) + }) + + t.Run("Template Validation for constants", func(t *testing.T) { + err := testAgainstGolden(t, "validation_constants", "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "constants") + require.NoError(t, err) + }) + + t.Run("Test help", func(t *testing.T) { + err := testAgainstGolden(t, "help", "--help") + require.NoError(t, err) + }) + + t.Run("Template Validation - invalid file error", func(t *testing.T) { + err := test(t, "-f", "not-a-file.yaml") + require.ErrorContains(t, err, "error reading file") + }) + + t.Run("Template Validation - invalid set opts", func(t *testing.T) { + err := test(t, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", "not-valid") + require.ErrorContains(t, err, "error parsing template overrides") + }) + + t.Run("Template Validation - invalid file schema error", func(t *testing.T) { + err := test(t, "-f", "../../unit/common/validation/validation.bad.tmpl.yaml") + require.ErrorContains(t, err, "error rendering template") + }) +} diff --git a/src/test/e2e/composition_component_def_test.go b/src/test/e2e/composition_component_def_test.go index 5e39f487..ce14ef64 100644 --- a/src/test/e2e/composition_component_def_test.go +++ b/src/test/e2e/composition_component_def_test.go @@ -42,7 +42,7 @@ func TestComponentDefinitionComposition(t *testing.T) { compDefPath := "../../test/unit/common/composition/component-definition-import-multi-compdef.yaml" // Validate results using ValidateOnPath - assessment, err := validate.ValidateOnPath(compDefPath, "") + assessment, err := validate.ValidateOnPath(ctx, compDefPath, "") if err != nil { t.Errorf("Error validating component definition: %v", err) } @@ -87,7 +87,12 @@ func TestComponentDefinitionComposition(t *testing.T) { } // Compare validation results to a composed component definition - oscalModel, err := composition.ComposeFromPath(compDefPath) + compositionCtx, err := composition.New(composition.WithModelFromLocalPath(compDefPath)) + if err != nil { + t.Errorf("error creating composition context: %v", err) + } + + oscalModel, err := compositionCtx.ComposeFromPath(ctx, compDefPath) if err != nil { t.Error(err) } diff --git a/src/test/e2e/create_resource_data_test.go b/src/test/e2e/create_resource_data_test.go index b9b12344..0ef33f5c 100644 --- a/src/test/e2e/create_resource_data_test.go +++ b/src/test/e2e/create_resource_data_test.go @@ -40,7 +40,7 @@ func TestCreateResourceDataValidation(t *testing.T) { validate.RunNonInteractively = true validate.SaveResources = false - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } @@ -114,7 +114,7 @@ func TestDeniedCreateResources(t *testing.T) { // Check that validation fails validate.ConfirmExecution = false validate.RunNonInteractively = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/multi_resource_validation_test.go b/src/test/e2e/multi_resource_validation_test.go index 1541e428..fd296665 100644 --- a/src/test/e2e/multi_resource_validation_test.go +++ b/src/test/e2e/multi_resource_validation_test.go @@ -103,7 +103,7 @@ func TestMultiResourceValidation(t *testing.T) { oscalPath := "./scenarios/multi-resource/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/pod_validation_test.go b/src/test/e2e/pod_validation_test.go index a192c8af..e1052cbb 100644 --- a/src/test/e2e/pod_validation_test.go +++ b/src/test/e2e/pod_validation_test.go @@ -92,12 +92,12 @@ func TestPodLabelValidation(t *testing.T) { }). Assess("Validate pod label", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/pod-label/oscal-component.yaml" - validatePodLabelFail(t, oscalPath) + validatePodLabelFail(ctx, t, oscalPath) return ctx }). Assess("Validate pod label (Kyverno)", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/pod-label/oscal-component-kyverno.yaml" - validatePodLabelFail(t, oscalPath) + validatePodLabelFail(ctx, t, oscalPath) return ctx }). Teardown(func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { @@ -130,7 +130,7 @@ func TestPodLabelValidation(t *testing.T) { }). Assess("All not-satisfied", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/pod-label/oscal-component-all-bad.yaml" - findings, observations := validatePodLabelFail(t, oscalPath) + findings, observations := validatePodLabelFail(ctx, t, oscalPath) observationRemarksMap := generateObservationRemarksMap(*observations) for _, f := range *findings { @@ -228,7 +228,7 @@ func validatePodLabelPass(ctx context.Context, t *testing.T, config *envconf.Con } message.Infof("Successfully upgraded %s to %s with OSCAL version %s %s\n", oscalPath, revisionOptions.OutputFile, revisionResponse.Reviser.GetSchemaVersion(), revisionResponse.Reviser.GetModelType()) - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatalf("Failed to validate oscal file: %s", oscalPath) } @@ -315,13 +315,13 @@ func validatePodLabelPass(ctx context.Context, t *testing.T, config *envconf.Con return ctx } -func validatePodLabelFail(t *testing.T, oscalPath string) (*[]oscalTypes_1_1_2.Finding, *[]oscalTypes_1_1_2.Observation) { +func validatePodLabelFail(ctx context.Context, t *testing.T, oscalPath string) (*[]oscalTypes_1_1_2.Finding, *[]oscalTypes_1_1_2.Observation) { message.NoProgress = true validate.ConfirmExecution = false validate.RunNonInteractively = true validate.SaveResources = false - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatalf("Failed to validate oscal file: %s", oscalPath) } @@ -367,7 +367,7 @@ func validateSaveResources(ctx context.Context, t *testing.T, oscalPath string) validate.ResourcesDir = tempDir // Validate on path - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatalf("Failed to validate oscal file: %s", oscalPath) } diff --git a/src/test/e2e/pod_wait_test.go b/src/test/e2e/pod_wait_test.go index 181146f9..faca5c15 100644 --- a/src/test/e2e/pod_wait_test.go +++ b/src/test/e2e/pod_wait_test.go @@ -32,7 +32,7 @@ func TestPodWaitValidation(t *testing.T) { oscalPath := "./scenarios/wait-field/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/remote_validation_test.go b/src/test/e2e/remote_validation_test.go index aa460532..087ca22b 100644 --- a/src/test/e2e/remote_validation_test.go +++ b/src/test/e2e/remote_validation_test.go @@ -36,7 +36,7 @@ func TestRemoteValidation(t *testing.T) { Assess("Validate local validation file", func(ctx context.Context, t *testing.T, config *envconf.Config) context.Context { oscalPath := "./scenarios/remote-validations/component-definition.yaml" - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/resource_data_test.go b/src/test/e2e/resource_data_test.go index 1fdc6dbe..ebea164a 100644 --- a/src/test/e2e/resource_data_test.go +++ b/src/test/e2e/resource_data_test.go @@ -71,7 +71,7 @@ func TestResourceDataValidation(t *testing.T) { oscalPath := "./scenarios/resource-data/oscal-component.yaml" message.NoProgress = true - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/scenarios/validation-composition/component-definition.yaml b/src/test/e2e/scenarios/validation-composition/component-definition.yaml index 79e91b40..6c07936d 100644 --- a/src/test/e2e/scenarios/validation-composition/component-definition.yaml +++ b/src/test/e2e/scenarios/validation-composition/component-definition.yaml @@ -6,6 +6,9 @@ component-definition: - control-id: ID-1 description: This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. links: + # validation from local, change directory + - href: file://../../../unit/common/validation/validation.opa.yaml + rel: lula # remote opa validation - href: https://raw.githubusercontent.com/defenseunicorns/lula/main/src/test/e2e/scenarios/dev-validate/validation.yaml rel: lula diff --git a/src/test/e2e/standard/template_test.go b/src/test/e2e/standard/template_test.go deleted file mode 100644 index d2f84528..00000000 --- a/src/test/e2e/standard/template_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package test - -import ( - "flag" - "os" - "path/filepath" - - "testing" - - "github.com/defenseunicorns/lula/src/cmd" - "github.com/defenseunicorns/lula/src/test/util" -) - -var updateGolden = flag.Bool("update", false, "update golden files") - -func TestTemplateCommand(t *testing.T) { - - test := func(t *testing.T, goldenFileName string, expectError bool, args ...string) error { - t.Helper() - - cmdArgs := []string{"tools", "template"} - cmdArgs = append(cmdArgs, args...) - - cmd := cmd.RootCommand() - - _, output, err := util.ExecuteCommand(cmd, cmdArgs...) - if err != nil { - if !expectError { - return err - } else { - return nil - } - } - - if !expectError { - goldenFile := filepath.Join("testdata", goldenFileName+".golden") - - if *updateGolden && !expectError { - err = os.WriteFile(goldenFile, []byte(output), 0644) - if err != nil { - return err - } - } - - expected, err := os.ReadFile(goldenFile) - if err != nil { - return err - } - - if output != string(expected) { - t.Fatalf("Expected:\n%s\n - Got \n%s\n", expected, output) - } - } - - return nil - } - - t.Run("Template Validation", func(t *testing.T) { - err := test(t, "validation", false, "-f", "../../unit/common/validation/validation.tmpl.yaml") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Template Validation with env vars", func(t *testing.T) { - os.Setenv("LULA_VAR_SOME_ENV_VAR", "my-env-var") - defer os.Unsetenv("LULA_VAR_SOME_ENV_VAR") - err := test(t, "validation_with_env_vars", false, "-f", "../../unit/common/validation/validation.tmpl.yaml") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Template Validation with set", func(t *testing.T) { - err := test(t, "validation_with_set", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--set", ".const.resources.name=foo") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Template Validation for all", func(t *testing.T) { - os.Setenv("LULA_VAR_SOME_LULA_SECRET", "env-secret") - defer os.Unsetenv("LULA_VAR_SOME_LULA_SECRET") - err := test(t, "validation_all", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "all") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Template Validation for non-sensitive", func(t *testing.T) { - err := test(t, "validation_non_sensitive", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "non-sensitive") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Template Validation for constants", func(t *testing.T) { - err := test(t, "validation_constants", false, "-f", "../../unit/common/validation/validation.tmpl.yaml", "--render", "constants") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Test help", func(t *testing.T) { - err := test(t, "help", false, "--help") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Template Validation - invalid file error", func(t *testing.T) { - err := test(t, "empty", true, "-f", "not-a-file.yaml") - if err != nil { - t.Fatal(err) - } - }) - - t.Run("Template Validation - invalid file schema error", func(t *testing.T) { - err := test(t, "empty", true, "-f", "../../unit/common/validation/validation.bad.tmpl.yaml") - if err != nil { - t.Fatal(err) - } - }) -} diff --git a/src/test/e2e/standard/testdata/empty.golden b/src/test/e2e/standard/testdata/empty.golden deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/e2e/validation_composition_test.go b/src/test/e2e/validation_composition_test.go index e6ff1779..6f8d8af8 100644 --- a/src/test/e2e/validation_composition_test.go +++ b/src/test/e2e/validation_composition_test.go @@ -3,12 +3,12 @@ package test import ( "context" "os" + "path/filepath" "testing" "time" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/cmd/validate" - "github.com/defenseunicorns/lula/src/pkg/common" "github.com/defenseunicorns/lula/src/pkg/common/composition" "github.com/defenseunicorns/lula/src/pkg/common/oscal" validationstore "github.com/defenseunicorns/lula/src/pkg/common/validation-store" @@ -71,7 +71,7 @@ func validateComposition(ctx context.Context, t *testing.T, oscalPath, expectedF t.Error(err) } - assessment, err := validate.ValidateOnPath(oscalPath, "") + assessment, err := validate.ValidateOnPath(ctx, oscalPath, "") if err != nil { t.Fatal(err) } @@ -98,15 +98,16 @@ func validateComposition(ctx context.Context, t *testing.T, oscalPath, expectedF if err != nil { t.Error(err) } - reset, err := common.SetCwdToFileDir(oscalPath) - if err != nil { - t.Fatalf("Error setting cwd to file dir: %v", err) - } - defer reset() compDef := oscalModel.ComponentDefinition - err = composition.ComposeComponentValidations(compDef) + compositionCtx, err := composition.New(composition.WithModelFromLocalPath(oscalPath)) + if err != nil { + t.Errorf("error creating composition context: %v", err) + } + + baseDir := filepath.Dir(oscalPath) + err = compositionCtx.ComposeComponentValidations(ctx, compDef, baseDir) if err != nil { t.Error(err) } diff --git a/src/test/unit/common/composition/component-definition-all-local.yaml b/src/test/unit/common/composition/component-definition-all-local.yaml index 0b795d29..3288f5f7 100644 --- a/src/test/unit/common/composition/component-definition-all-local.yaml +++ b/src/test/unit/common/composition/component-definition-all-local.yaml @@ -36,6 +36,7 @@ component-definition: links: - href: "#a7377430-2328-4dc4-a9e2-b3f31dc1dff9" rel: lula + resource-fragment: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 back-matter: resources: - uuid: a7377430-2328-4dc4-a9e2-b3f31dc1dff9 diff --git a/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml b/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml new file mode 100644 index 00000000..8e5091c1 --- /dev/null +++ b/src/test/unit/common/composition/component-definition-import-nested-compdef-template.yaml @@ -0,0 +1,19 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + import-component-definitions: + - href: file://./{{ .const.templated_comp_def }} + - href: file://../oscal/valid-generated-component.yaml + + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website diff --git a/src/test/unit/common/composition/component-definition-import-nested-compdef.yaml b/src/test/unit/common/composition/component-definition-import-nested-compdef.yaml new file mode 100644 index 00000000..e310fa13 --- /dev/null +++ b/src/test/unit/common/composition/component-definition-import-nested-compdef.yaml @@ -0,0 +1,18 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + import-component-definitions: + - href: file://./component-definition-all-local.yaml + - href: file://../../../e2e/scenarios/validation-composition/component-definition.yaml + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website diff --git a/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml b/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml new file mode 100644 index 00000000..5f583fef --- /dev/null +++ b/src/test/unit/common/composition/component-definition-template-valid-validation-tmpl.yaml @@ -0,0 +1,39 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: {{ .const.type }} + title: {{ .const.title }} + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + description: >- + This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: "../validation/valid-validation.tmpl.yaml" + text: local path template validation + rel: lula \ No newline at end of file diff --git a/src/test/unit/common/composition/component-definition-template.yaml b/src/test/unit/common/composition/component-definition-template.yaml new file mode 100644 index 00000000..ae1d984b --- /dev/null +++ b/src/test/unit/common/composition/component-definition-template.yaml @@ -0,0 +1,39 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: Lula Demo + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.2 # This version should remain one version behind latest version for `lula dev upgrade` demo + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Lula Development + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: {{ .const.type }} + title: {{ .const.title }} + description: | + Lula - the Compliance Validator + purpose: Validate compliance controls + responsible-roles: + - role-id: provider + party-uuids: + - C18F4A9F-A402-415B-8D13-B51739D689FF # matches parties entry for Defense Unicorns + control-implementations: + - uuid: A584FEDC-8CEA-4B0C-9F07-85C2C4AE751A + source: https://raw.githubusercontent.com/usnistgov/oscal-content/master/nist.gov/SP800-53/rev5/json/NIST_SP-800-53_rev5_catalog.json + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + description: >- + This control validates that the demo-pod pod in the validation-test namespace contains the required pod label foo=bar in order to establish compliance. + links: + - href: "../validation/validation.tmpl.yaml" + text: local path template validation + rel: lula \ No newline at end of file diff --git a/src/test/unit/common/validation/valid-validation.tmpl.yaml b/src/test/unit/common/validation/valid-validation.tmpl.yaml new file mode 100644 index 00000000..442b6861 --- /dev/null +++ b/src/test/unit/common/validation/valid-validation.tmpl.yaml @@ -0,0 +1,33 @@ +metadata: + name: Test validation with templating + uuid: 458d2d84-b7f2-4679-8964-6f9a9dfe51eb +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podvt + resource-rule: + name: "{{ .const.resources.name }}" + version: v1 + resource: pods + namespaces: ["{{ .const.resources.namespace }}"] +provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + # Validation result + validate if { + { "one", "two", "three" } == { {{ .const.resources.exemptions | concatToRegoList }} } + "{{ .var.some_env_var }}" == "my-env-var" + "{{ .var.some_lula_secret }}" == "********" + } + msg = validate.msg + + value_of_my_secret := {{ .var.some_lula_secret }} \ No newline at end of file diff --git a/src/test/unit/common/validation/validation.kyverno.yaml b/src/test/unit/common/validation/validation.kyverno.yaml index e0e6de7c..c4ea4896 100644 --- a/src/test/unit/common/validation/validation.kyverno.yaml +++ b/src/test/unit/common/validation/validation.kyverno.yaml @@ -1,7 +1,7 @@ lula-version: ">= v0.1.0" metadata: name: Kyverno validate pods with label foo=bar - uuid: 123e4567-e89b-12d3-a456-426614174000 + uuid: 386aafd8-a80d-4ad7-8844-d7b14a432187 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/unit/common/validation/opa.validation.yaml b/src/test/unit/common/validation/validation.opa.yaml similarity index 92% rename from src/test/unit/common/validation/opa.validation.yaml rename to src/test/unit/common/validation/validation.opa.yaml index a46af206..1185c575 100644 --- a/src/test/unit/common/validation/opa.validation.yaml +++ b/src/test/unit/common/validation/validation.opa.yaml @@ -1,7 +1,7 @@ lula-version: ">=v0.2.0" metadata: name: Validate pods with label foo=bar - uuid: 123e4567-e89b-12d3-a456-426655440000 + uuid: 6c00ae8d-7187-42ab-8d89-f383447a0824 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/unit/common/validation/validation.tmpl.yaml b/src/test/unit/common/validation/validation.tmpl.yaml index f9eb5baf..fa6979ce 100644 --- a/src/test/unit/common/validation/validation.tmpl.yaml +++ b/src/test/unit/common/validation/validation.tmpl.yaml @@ -1,3 +1,6 @@ +metadata: + name: Test validation with templating + uuid: 99fc662c-109a-4e26-8398-75f3db67f862 domain: type: kubernetes kubernetes-spec: diff --git a/src/test/unit/common/validation/validation.trailing-spaces.yaml b/src/test/unit/common/validation/validation.trailing-spaces.yaml new file mode 100644 index 00000000..b18627ea --- /dev/null +++ b/src/test/unit/common/validation/validation.trailing-spaces.yaml @@ -0,0 +1,27 @@ +lula-version: ">=v0.2.0" +metadata: + name: Validate pods with label foo=bar + uuid: a6bded80-1717-45fc-afd9-c5d62607eb71 +domain: + type: kubernetes + kubernetes-spec: + resources: + - name: podsvt + resource-rule: + version: v1 + resource: pods + namespaces: [validation-test] +provider: + type: opa + opa-spec: + rego: | + package validate + + import future.keywords.every + + validate { + every pod in input.podsvt { + podLabel := pod.metadata.labels.foo + podLabel == "bar" + } + } diff --git a/src/test/util/utils.go b/src/test/util/utils.go index 978b3990..0b17e576 100644 --- a/src/test/util/utils.go +++ b/src/test/util/utils.go @@ -113,9 +113,9 @@ func GetNamespace(name string) (*v1.Namespace, error) { }, nil } -func ExecuteCommand(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { - _, output, err = ExecuteCommandC(root, args...) - return root, output, err +func ExecuteCommand(cmd *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { + _, output, err = ExecuteCommandC(cmd, args...) + return cmd, output, err } func ExecuteCommandC(cmd *cobra.Command, args ...string) (c *cobra.Command, output string, err error) {