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/docs/reference/domains/file-domain.md b/docs/reference/domains/file-domain.md new file mode 100644 index 00000000..25f5135d --- /dev/null +++ b/docs/reference/domains/file-domain.md @@ -0,0 +1,128 @@ +# File Domain +The File domain allows for validation of arbitrary file contents from a list of supported file types. The file domain can evaluate local files and network files. Files are copied to a temporary directory for evaluation and deleted afterwards. + +## Specification +The File domain specification accepts a descriptive name for the file as well as its path. The filenames and descriptive names must be unique. + +```yaml +domain: + type: file + file-spec: + filepaths: + - name: config + path: grafana.ini +``` + +## Supported File Types +The file domain uses OPA's [conftest](https://conftest.dev) to parse files into a json-compatible format for validations. ∑Both OPA and Kyverno (using [kyverno-json](https://kyverno.github.io/kyverno-json/latest/)) can validate files parsed by the file domain. + +The file domain supports the following file formats for validation: +* CUE +* CycloneDX +* Dockerfile +* EDN +* Environment files (.env) +* HCL and HCL2 +* HOCON +* Ignore files (.gitignore, .dockerignore) +* INI +* JSON +* Jsonnet +* Property files (.properties) +* SPDX +* TextProto (Protocol Buffers) +* TOML +* VCL +* XML +* YAML + +## Validations +When writing validations against files, the filepath `Name` must be included as +the top-level key in the validation. The placement varies between providers. + +Given the following ini file: + +```grafana.ini +[server] +# Protocol (http, https, socket) +protocol = http +``` + +The below Kyverno policy validates the protocol is https by including Grafana as the top-level key under `check`: + +```yaml +metadata: + name: check-grafana-protocol + uuid: ad38ef57-99f6-4ac6-862e-e0bc9f55eebe +domain: + type: file + file-spec: + filepaths: + - name: 'grafana' + path: 'custom.ini' +provider: + type: kyverno + kyverno-spec: + policy: + apiVersion: json.kyverno.io/v1alpha1 + kind: ValidatingPolicy + metadata: + name: grafana-config + spec: + rules: + - name: protocol-is-https + assert: + all: + - check: + grafana: + server: + protocol: https +``` + +While in an OPA policy, the filepath `Name` is the input key to access the config: + +```yaml +metadata: + name: validate-grafana-config + uuid: ad38ef57-99f6-4ac6-862e-e0bc9f55eebe +domain: + type: file + file-spec: + filepaths: + - name: 'grafana' + path: 'custom.ini' +provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + validate if { + check_grafana_config.result + } + msg = check_grafana_config.msg + + config := input["grafana"] + protocol := config.server.protocol + + check_grafana_config = {"result": true, "msg": msg} if { + protocol == "https" + msg := "Server protocol is set to https" + } else = {"result": false, "msg": msg} if { + protocol == "http" + msg := "Grafana protocol is insecure" + } + + output: + validation: validate.validate + observations: + - validate.msg +``` + +## Note on Compose +While the file domain is capable of referencing relative file paths in the `file-spec`, Lula does not de-reference those paths during composition. If you are composing multiple files together, you must either use absolute filepaths (including network filepaths), or ensure that all referenced filepaths are relative to the output directory of the compose command. diff --git a/go.mod b/go.mod index f66d82ce..24348440 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,13 @@ 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/conftest v0.55.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 @@ -34,26 +36,38 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect + cuelang.org/go v0.9.2 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect + github.com/CycloneDX/cyclonedx-go v0.9.0 // indirect github.com/IGLOU-EU/go-wildcard v1.0.3 // indirect + github.com/KeisukeYamashita/go-vcl v0.4.0 // 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/agext/levenshtein v1.2.3 // indirect + github.com/agnivade/levenshtein v1.2.0 // indirect + github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.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 github.com/aymanbagabas/go-udiff v0.2.0 // indirect + github.com/basgys/goxml2json v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bufbuild/protocompile v0.6.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/charmbracelet/x/ansi v0.3.2 // indirect github.com/charmbracelet/x/exp/golden v0.0.0-20240919170804-a4978c8e603a // indirect github.com/charmbracelet/x/term v0.2.0 // indirect - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect + github.com/cockroachdb/apd/v3 v3.2.1 // indirect + github.com/containerd/console v1.0.4 // indirect + github.com/containerd/typeurl/v2 v2.1.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/daviddengcn/go-colortext v1.0.0 // indirect - github.com/distribution/reference v0.5.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.2 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect @@ -61,6 +75,7 @@ require ( github.com/fatih/camelcase v1.0.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-akka/configuration v0.0.0-20200606091224-a002c0330665 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.2 // indirect @@ -73,7 +88,7 @@ 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/go-jsonnet v0.20.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 @@ -83,6 +98,7 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/hcl/v2 v2.17.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/copier v0.4.0 // indirect @@ -103,6 +119,8 @@ require ( github.com/mattn/go-localereader v0.0.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/buildkit v0.15.1 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/spdystream v0.4.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -113,11 +131,12 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect 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 @@ -128,13 +147,17 @@ require ( github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect + github.com/shteou/go-ignore v0.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spdx/tools-golang v0.5.5 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/tmccombs/hcl2json v0.3.1 // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect github.com/vladimirvivien/gexe v0.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect @@ -143,19 +166,20 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea // indirect + github.com/zclconf/go-cty v1.13.2 // indirect go.opentelemetry.io/otel v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect 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 @@ -169,6 +193,8 @@ require ( k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect k8s.io/metrics v0.31.1 // indirect k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect + muzzammil.xyz/jsonc v1.0.0 // indirect + olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect sigs.k8s.io/controller-runtime v0.18.2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.17.2 // indirect diff --git a/go.sum b/go.sum index c08549bd..d118a088 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,21 @@ atomicgo.dev/keyboard v0.2.9 h1:tOsIid3nlPLZ3lwgG8KZMp/SFmr7P0ssEN5JUsm78K8= atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtExQ= atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= +cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2 h1:BnG6pr9TTr6CYlrJznYUDj6V7xldD1W+1iXPum0wT/w= +cuelabs.dev/go/oci/ociregistry v0.0.0-20240404174027-a39bec0462d2/go.mod h1:pK23AUVXuNzzTpfMCA06sxZGeVQ/75FdVtW249de9Uo= +cuelang.org/go v0.9.2 h1:pfNiry2PdRBr02G/aKm5k2vhzmqbAOoaB4WurmEbWvs= +cuelang.org/go v0.9.2/go.mod h1:qpAYsLOf7gTM1YdEg6cxh553uZ4q9ZDWlPbtZr9q1Wk= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/CycloneDX/cyclonedx-go v0.9.0 h1:inaif7qD8bivyxp7XLgxUYtOXWtDez7+j72qKTMQTb8= +github.com/CycloneDX/cyclonedx-go v0.9.0/go.mod h1:NE/EWvzELOFlG6+ljX/QeMlVt9VKcTwu8u0ccsACEsw= github.com/IGLOU-EU/go-wildcard v1.0.3 h1:r8T46+8/9V1STciXJomTWRpPEv4nGJATDbJkdU0Nou0= github.com/IGLOU-EU/go-wildcard v1.0.3/go.mod h1:/qeV4QLmydCbwH0UMQJmXDryrFKJknWi/jjO8IiuQfY= +github.com/KeisukeYamashita/go-vcl v0.4.0 h1:dFxZq2yVeaCWBJAT7Oh9Z+Pp8y32i7b11QHdzsuBcsk= +github.com/KeisukeYamashita/go-vcl v0.4.0/go.mod h1:af2qGlXbsHDQN5abN7hyGNKtGhcFSaDdbLl4sfud+AU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs= @@ -23,12 +34,25 @@ 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/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +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/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= 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= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= @@ -38,10 +62,18 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw= +github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY= +github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -58,23 +90,25 @@ github.com/charmbracelet/bubbletea v1.1.1 h1:KJ2/DnmpfqFtDNVTvYZ6zpPFL9iRCRr0qqK github.com/charmbracelet/bubbletea v1.1.1/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= -github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= -github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/charmbracelet/x/ansi v0.3.2 h1:wsEwgAN+C9U06l9dCVMX0/L3x7ptvY1qmjMwyfE6USY= github.com/charmbracelet/x/ansi v0.3.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b h1:MnAMdlwSltxJyULnrYbkZpp4k58Co7Tah3ciKhSNo0Q= -github.com/charmbracelet/x/exp/golden v0.0.0-20240815200342-61de596daa2b/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= github.com/charmbracelet/x/exp/golden v0.0.0-20240919170804-a4978c8e603a h1:IUy+N6nKpGfijckOe8KGnAQwBUT6xz63n3tbb0Gy8aY= github.com/charmbracelet/x/exp/golden v0.0.0-20240919170804-a4978c8e603a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/teatest v0.0.0-20240918160051-227168dc0568 h1:MWPyZsxZMe/oKhkt5j34zAba/2nXOxWuf3CvQqO8SDA= -github.com/charmbracelet/x/exp/teatest v0.0.0-20240918160051-227168dc0568/go.mod h1:NDRRSMP6bZbCs4jyc4i1/4UG4M+0PEiQdpivQgD0Mio= github.com/charmbracelet/x/exp/teatest v0.0.0-20240919170804-a4978c8e603a h1:sS42HbmCab8rCehUwNO/bQEZQoJ6GavhZyO+245mBwA= github.com/charmbracelet/x/exp/teatest v0.0.0-20240919170804-a4978c8e603a/go.mod h1:NDRRSMP6bZbCs4jyc4i1/4UG4M+0PEiQdpivQgD0Mio= github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= +github.com/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -91,18 +125,22 @@ 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/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/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.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE= github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/proto v1.10.0 h1:pDGyFRVV5RvV+nkBK9iy3q67FBy9Xa7vwrOTE+g5aGw= +github.com/emicklei/proto v1.10.0/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= @@ -121,10 +159,13 @@ github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7Dlme github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-akka/configuration v0.0.0-20200606091224-a002c0330665 h1:Iz3aEheYgn+//VX7VisgCmF/wW3BMtXCLbvHV4jMQJA= +github.com/go-akka/configuration v0.0.0-20200606091224-a002c0330665/go.mod h1:19bUnum2ZAeftfwwLZ/wRe7idyfoW2MfmXO464Hrfbw= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= @@ -142,17 +183,26 @@ github.com/go-openapi/jsonreference v0.20.4 h1:bKlDxQxQJgwpUSgOENiMPzCTBVuc7vTdX github.com/go-openapi/jsonreference v0.20.4/go.mod h1:5pZJyJP2MnYCpoeoMAql78cCHauHj0V9Lhc506VOpw4= github.com/go-openapi/swag v0.22.9 h1:XX2DssF+mQKM2DHsbgZK74y/zj4mo9I99+89xUmuZCE= github.com/go-openapi/swag v0.22.9/go.mod h1:3/OXnFfnMAwBD099SwYRk7GD3xOrr1iL7d/XNLXVVwE= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 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/godoctor/godoctor v0.0.0-20181123222458-69df17f3a6f6/go.mod h1:+tyhT8jBF8E0XvdlSXOSL7Iko7DlNiongHq3q+wcsPs= 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.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= @@ -164,13 +214,17 @@ github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrS github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= -github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/flatbuffers v22.9.29+incompatible h1:3UBb679lq3V/O9rgzoJmnkP1jJzmC9OdFzITUBkLU/A= +github.com/google/flatbuffers v22.9.29+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= +github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -200,8 +254,12 @@ github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKe github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= +github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= +github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= @@ -216,6 +274,7 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -232,12 +291,15 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kyverno/kyverno-json v0.0.3 h1:EImI/YV41dG4hDQer/W0qMZHfxqul1yiHrBEXxFrkGM= github.com/kyverno/kyverno-json v0.0.3/go.mod h1:KUgXPXwUh0Sm/UgtHPomZAfEX8v79I3B5RZbUlzNihg= github.com/kyverno/pkg/ext v0.0.0-20240418121121-df8add26c55c h1:lAolpR9H8BwM5lRRvgCQ8JowswyxZRH+fgtIQzHFVCk= github.com/kyverno/pkg/ext v0.0.0-20240418121121-df8add26c55c/go.mod h1:02vxM0GNXz9+B/i6+rMfWAIwibUuAH+qFsd73IFskgQ= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= @@ -246,10 +308,13 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8 github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= @@ -259,10 +324,17 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/buildkit v0.15.1 h1:J6wrew7hphKqlq1wuu6yaUb/1Ra7gEzDAovylGztAKM= +github.com/moby/buildkit v0.15.1/go.mod h1:Yis8ZMUJTHX9XhH9zVyK2igqSHV3sxi3UN0uztZocZk= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8= github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -288,10 +360,15 @@ 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/conftest v0.55.0 h1:M6QXrrfQjmyFRsy11Q2ucFGNbbelhyaX0vtNcfcYS3I= +github.com/open-policy-agent/conftest v0.55.0/go.mod h1:qL8de2Sr5QsDG0HVM3iZiHS2Qea3bLzut6OsYyiRyEY= +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/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= @@ -301,14 +378,16 @@ 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= github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0 h1:sadMIsgmHpEOGbUs6VtHBXRR1OHevnj7hLx9ZcdNGW4= +github.com/protocolbuffers/txtpbfmt v0.0.0-20230328191034-3462fbc510c0/go.mod h1:jgxiZysxFPM+iWKwQwPR+y+Jvo54ARd4EisXxKYpB5c= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -325,6 +404,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= @@ -335,9 +415,12 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw= github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shteou/go-ignore v0.3.1 h1:/DVY4w06eKliWrbkwKfBHJgUleld+QAlmlQvfRQOigA= +github.com/shteou/go-ignore v0.3.1/go.mod h1:hMVyBe+qt5/Z11W/Fxxf86b5SuL8kM29xNWLYob9Vos= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smarty/assertions v1.15.1 h1:812oFiXI+G55vxsFf+8bIZ1ux30qtkdqzKbEFwyX3Tk= @@ -346,14 +429,24 @@ github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sS github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= +github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= +github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -361,6 +454,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -374,19 +468,34 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= +github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= +github.com/tmccombs/hcl2json v0.3.1 h1:Pf+Lb9OpZ5lkQuIC0BB5txdCQskZ2ud/l8sz/Nkjf3A= +github.com/tmccombs/hcl2json v0.3.1/go.mod h1:ljY0/prd2IFUF3cagQjV3cpPEEQKzqyGqnKI7m5DBVY= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= +github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/vektah/gqlparser v1.2.0/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY= github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -394,6 +503,10 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea h1:CyhwejzVGvZ3Q2PSbQ4NRRYn+ZWv5eS1vlaEusT+bAI= github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea/go.mod h1:eNr558nEUjP8acGw8FFjTeWvSgU1stO7FAO6eknhHe4= +github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= +github.com/zclconf/go-cty v1.6.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o= +github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= +github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= @@ -420,12 +533,14 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 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= @@ -434,17 +549,21 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -452,8 +571,11 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -476,9 +598,10 @@ 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 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= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -488,7 +611,9 @@ golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191127201027-ecd32218bd7f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -499,16 +624,21 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +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.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 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= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -553,6 +683,10 @@ k8s.io/metrics v0.31.1 h1:h4I4dakgh/zKflWYAOQhwf0EXaqy8LxAIyE/GBvxqRc= k8s.io/metrics v0.31.1/go.mod h1:JuH1S9tJiH9q1VCY0yzSCawi7kzNLsDzlWDJN4xR+iA= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +muzzammil.xyz/jsonc v1.0.0 h1:B6kaT3wHueZ87mPz3q1nFuM1BlL32IG0wcq0/uOsQ18= +muzzammil.xyz/jsonc v1.0.0/go.mod h1:rFv8tUUKe+QLh7v02BhfxXEf4ZHhYD7unR93HL/1Uvo= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= +olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= sigs.k8s.io/controller-runtime v0.18.2 h1:RqVW6Kpeaji67CY5nPEfRz6ZfFMk0lWQlNrLqlNpx+Q= sigs.k8s.io/controller-runtime v0.18.2/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw= sigs.k8s.io/e2e-framework v0.4.0 h1:4yYmFDNNoTnazqmZJXQ6dlQF1vrnDbutmxlyvBpC5rY= 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/dev/common.go b/src/cmd/dev/common.go index 407bec9e..d8e065b6 100644 --- a/src/cmd/dev/common.go +++ b/src/cmd/dev/common.go @@ -1,6 +1,7 @@ package dev import ( + "context" "fmt" "io" "strings" @@ -95,7 +96,7 @@ func RunSingleValidation(validationBytes []byte, opts ...types.LulaValidationOpt return lulaValidation, err } - err = lulaValidation.Validate(opts...) + err = lulaValidation.Validate(context.Background(), opts...) if err != nil { return lulaValidation, err } 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 2f13ebd9..e73d1dd5 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" @@ -11,6 +15,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 @@ -32,8 +37,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/lint.go b/src/cmd/tools/lint.go index 6c31f382..b69198d1 100644 --- a/src/cmd/tools/lint.go +++ b/src/cmd/tools/lint.go @@ -25,8 +25,8 @@ To lint existing OSCAL files: ` var lintCmd = &cobra.Command{ - Use: "lint", - Short: "Validate OSCAL against schema", + Use: "lint", + Short: "Validate OSCAL against schema", Long: "Validate OSCAL documents are properly configured against the OSCAL schema", Example: lintHelp, Run: func(cmd *cobra.Command, args []string) { 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..ef9bd297 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" @@ -13,6 +14,7 @@ import ( requirementstore "github.com/defenseunicorns/lula/src/pkg/common/requirement-store" validationstore "github.com/defenseunicorns/lula/src/pkg/common/validation-store" "github.com/defenseunicorns/lula/src/pkg/message" + "github.com/defenseunicorns/lula/src/types" "github.com/spf13/cobra" ) @@ -52,6 +54,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 +68,8 @@ var validateCmd = &cobra.Command{ message.Fatalf(err, "Invalid file extension: %s, requires .json or .yaml", opts.InputFile) } - assessment, err := ValidateOnPath(opts.InputFile, opts.Target) + ctx := context.WithValue(cmd.Context(), types.LulaValidationWorkDir, filepath.Dir(opts.InputFile)) + assessment, err := ValidateOnPath(ctx, opts.InputFile, opts.Target) if err != nil { message.Fatalf(err, "Validation error: %s", err) } @@ -118,23 +127,28 @@ 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 { return assessmentResult, fmt.Errorf("component definition is nil") } - results, err := ValidateOnCompDef(oscalModel.ComponentDefinition, target) + results, err := ValidateOnCompDef(ctx, oscalModel.ComponentDefinition, target) if err != nil { return assessmentResult, err } @@ -150,7 +164,7 @@ func ValidateOnPath(path string, target string) (assessmentResult *oscalTypes_1_ // ValidateOnCompDef takes a single ComponentDefinition object // It will perform a validation and return a slice of results that can be written to an assessment-results object -func ValidateOnCompDef(compDef *oscalTypes_1_1_2.ComponentDefinition, target string) (results []oscalTypes_1_1_2.Result, err error) { +func ValidateOnCompDef(ctx context.Context, compDef *oscalTypes_1_1_2.ComponentDefinition, target string) (results []oscalTypes_1_1_2.Result, err error) { if compDef == nil { return results, fmt.Errorf("cannot validate a component definition that is nil") } @@ -175,7 +189,7 @@ func ValidateOnCompDef(compDef *oscalTypes_1_1_2.ComponentDefinition, target str // this will only produce a single result if target != "" { if controlImplementation, ok := controlImplementations[target]; ok { - findings, observations, err := ValidateOnControlImplementations(&controlImplementation, validationStore, target) + findings, observations, err := ValidateOnControlImplementations(ctx, &controlImplementation, validationStore, target) if err != nil { return results, err } @@ -194,7 +208,7 @@ func ValidateOnCompDef(compDef *oscalTypes_1_1_2.ComponentDefinition, target str // loop over the controlImplementations map & validate // we lose context of source if not contained within the loop for source, controlImplementation := range controlImplementations { - findings, observations, err := ValidateOnControlImplementations(&controlImplementation, validationStore, source) + findings, observations, err := ValidateOnControlImplementations(ctx, &controlImplementation, validationStore, source) if err != nil { return results, err } @@ -212,7 +226,7 @@ func ValidateOnCompDef(compDef *oscalTypes_1_1_2.ComponentDefinition, target str } -func ValidateOnControlImplementations(controlImplementations *[]oscalTypes_1_1_2.ControlImplementationSet, validationStore *validationstore.ValidationStore, target string) (map[string]oscalTypes_1_1_2.Finding, []oscalTypes_1_1_2.Observation, error) { +func ValidateOnControlImplementations(ctx context.Context, controlImplementations *[]oscalTypes_1_1_2.ControlImplementationSet, validationStore *validationstore.ValidationStore, target string) (map[string]oscalTypes_1_1_2.Finding, []oscalTypes_1_1_2.Observation, error) { // Create requirement store for all implemented requirements requirementStore := requirementstore.NewRequirementStore(controlImplementations) @@ -239,7 +253,7 @@ func ValidateOnControlImplementations(controlImplementations *[]oscalTypes_1_1_2 // Run Lula validations and generate observations & findings message.Title("\n📐 Running Validations", "") - observations := validationStore.RunValidations(ConfirmExecution, SaveResources, ResourcesDir) + observations := validationStore.RunValidations(ctx, ConfirmExecution, SaveResources, ResourcesDir) message.Title("\n💡 Findings", "") findings := requirementStore.GenerateFindings(validationStore) @@ -261,7 +275,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..66f45df2 100644 --- a/src/pkg/common/common.go +++ b/src/pkg/common/common.go @@ -7,10 +7,12 @@ import ( "io" "os" "path/filepath" + "regexp" "strings" oscalTypes_1_1_2 "github.com/defenseunicorns/go-oscal/src/types/oscal-1-1-2" "github.com/defenseunicorns/lula/src/pkg/domains/api" + "github.com/defenseunicorns/lula/src/pkg/domains/files" kube "github.com/defenseunicorns/lula/src/pkg/domains/kubernetes" "github.com/defenseunicorns/lula/src/pkg/message" "github.com/defenseunicorns/lula/src/pkg/providers/kyverno" @@ -147,6 +149,8 @@ func GetDomain(domain *Domain, ctx context.Context) (types.Domain, error) { return kube.CreateKubernetesDomain(ctx, domain.KubernetesSpec) case "api": return api.CreateApiDomain(domain.ApiSpec) + case "file": + return files.CreateDomain(domain.FileSpec) default: return nil, fmt.Errorf("domain is unsupported") } @@ -185,3 +189,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 400f407e..54b0e711 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/schemas/validation.json b/src/pkg/common/schemas/validation.json index 2bad3f85..0d733ed4 100644 --- a/src/pkg/common/schemas/validation.json +++ b/src/pkg/common/schemas/validation.json @@ -44,7 +44,8 @@ "type": "string", "enum": [ "kubernetes", - "api" + "api", + "file" ], "description": "The type of domain (Required)" }, @@ -88,6 +89,20 @@ "api-spec" ] } + }, + { + "if": { + "properties": { + "type": { + "const": "file" + } + } + }, + "then": { + "required": [ + "file-spec" + ] + } } ] }, @@ -344,6 +359,25 @@ } } }, + "file-spec": { + "type": "object", + "properties": { + "filepaths": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + } + } + } + } + } + }, "provider": { "type": "object", "properties": { diff --git a/src/pkg/common/types.go b/src/pkg/common/types.go index 208bb369..3260ba9d 100644 --- a/src/pkg/common/types.go +++ b/src/pkg/common/types.go @@ -13,6 +13,7 @@ import ( "github.com/defenseunicorns/lula/src/config" "github.com/defenseunicorns/lula/src/pkg/common/schemas" "github.com/defenseunicorns/lula/src/pkg/domains/api" + "github.com/defenseunicorns/lula/src/pkg/domains/files" kube "github.com/defenseunicorns/lula/src/pkg/domains/kubernetes" "github.com/defenseunicorns/lula/src/pkg/providers/kyverno" "github.com/defenseunicorns/lula/src/pkg/providers/opa" @@ -49,25 +50,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 @@ -84,6 +99,8 @@ type Domain struct { KubernetesSpec *kube.KubernetesSpec `json:"kubernetes-spec,omitempty" yaml:"kubernetes-spec,omitempty"` // ApiSpec is the specification for an API domain, required if type is api ApiSpec *api.ApiSpec `json:"api-spec,omitempty" yaml:"api-spec,omitempty"` + // FileSpec is the specification for a File domain, required if type is file + FileSpec *files.Spec `json:"file-spec,omitempty" yaml:"file-spec,omitempty"` } type Provider struct { @@ -159,3 +176,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/pkg/common/validation-store/validation-store.go b/src/pkg/common/validation-store/validation-store.go index 7f5e78fd..eb89c66f 100644 --- a/src/pkg/common/validation-store/validation-store.go +++ b/src/pkg/common/validation-store/validation-store.go @@ -1,6 +1,7 @@ package validationstore import ( + "context" "fmt" "os" "path/filepath" @@ -105,7 +106,7 @@ func (v *ValidationStore) DryRun() (executable bool, msg string) { } // RunValidations runs the validations in the store -func (v *ValidationStore) RunValidations(confirmExecution, saveResources bool, resourcesDir string) []oscalTypes_1_1_2.Observation { +func (v *ValidationStore) RunValidations(ctx context.Context, confirmExecution, saveResources bool, resourcesDir string) []oscalTypes_1_1_2.Observation { observations := make([]oscalTypes_1_1_2.Observation, 0, len(v.validationMap)) for k, val := range v.validationMap { @@ -113,7 +114,7 @@ func (v *ValidationStore) RunValidations(confirmExecution, saveResources bool, r spinnerMessage := fmt.Sprintf("Running validation %s", k) spinner := message.NewProgressSpinner(spinnerMessage) defer spinner.Stop() - err := val.Validate(types.ExecutionAllowed(confirmExecution)) + err := val.Validate(ctx, types.ExecutionAllowed(confirmExecution)) if err != nil { message.Debugf("Error running validation %s: %v", k, err) // Update validation with failed results diff --git a/src/pkg/common/validation-store/validation-store_test.go b/src/pkg/common/validation-store/validation-store_test.go index dc43051f..73843ebf 100644 --- a/src/pkg/common/validation-store/validation-store_test.go +++ b/src/pkg/common/validation-store/validation-store_test.go @@ -1,6 +1,7 @@ package validationstore_test import ( + "context" "testing" "github.com/defenseunicorns/go-oscal/src/pkg/uuid" @@ -164,7 +165,7 @@ func TestRunValidations(t *testing.T) { v.AddLulaValidation(validation, uuid.NewUUID()) } - observations := v.RunValidations(true, false, "") + observations := v.RunValidations(context.Background(), true, false, "") if len(observations) != tt.expectedObservations { t.Errorf("Expected %d observations, but got %d", tt.expectedObservations, len(observations)) } @@ -180,7 +181,7 @@ func TestGetRelatedObservation(t *testing.T) { v.AddLulaValidation(validationPass, "1") v.AddLulaValidation(validationFail, "2") - v.RunValidations(true, false, "") + v.RunValidations(context.Background(), true, false, "") tests := []struct { name string diff --git a/src/pkg/domains/api/types.go b/src/pkg/domains/api/types.go index bbd28398..bc67c47d 100644 --- a/src/pkg/domains/api/types.go +++ b/src/pkg/domains/api/types.go @@ -1,6 +1,7 @@ package api import ( + "context" "fmt" "github.com/defenseunicorns/lula/src/types" @@ -35,7 +36,7 @@ func CreateApiDomain(spec *ApiSpec) (types.Domain, error) { }, nil } -func (a ApiDomain) GetResources() (types.DomainResources, error) { +func (a ApiDomain) GetResources(_ context.Context) (types.DomainResources, error) { return MakeRequests(a.Spec.Requests) } diff --git a/src/pkg/domains/files/files.go b/src/pkg/domains/files/files.go new file mode 100644 index 00000000..7886ff73 --- /dev/null +++ b/src/pkg/domains/files/files.go @@ -0,0 +1,105 @@ +package files + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/defenseunicorns/lula/src/pkg/common/network" + "github.com/defenseunicorns/lula/src/types" + "github.com/open-policy-agent/conftest/parser" +) + +type Domain struct { + Spec *Spec `json:"spec,omitempty" yaml:"spec,omitempty"` +} + +// GetResources gathers the input files to be tested. +func (d Domain) GetResources(ctx context.Context) (types.DomainResources, error) { + var workDir string + var ok bool + if workDir, ok = ctx.Value(types.LulaValidationWorkDir).(string); !ok { + // if unset, assume lula is working in the same directory the inputFile is in + workDir = "." + } + + // see TODO below: maybe this is a REAL directory? + dst, err := os.MkdirTemp("", "lula-files") + if err != nil { + return nil, err + } + + // TODO? this might be a nice configurable option (for debugging) - store + // the files into a local .lula directory that doesn't necessarily get + // removed. + defer os.RemoveAll(dst) + + // make a map of rel filepaths to the user-supplied name, so we can re-key the DomainResources later on. + filenames := make(map[string]string, len(d.Spec.Filepaths)) + + // Copy files to a temporary location + for _, path := range d.Spec.Filepaths { + file := filepath.Join(workDir, path.Path) + bytes, err := network.Fetch(file) + if err != nil { + return nil, fmt.Errorf("error getting source files: %w", err) + } + + // We'll just use the filename when writing the file so it's easier to reference later + relname := filepath.Base(path.Path) + + err = os.WriteFile(filepath.Join(dst, relname), bytes, 0666) + if err != nil { + return nil, fmt.Errorf("error writing local files: %w", err) + } + // and save this info for later + filenames[relname] = path.Name + } + + // get a list of all the files we just downloaded in the temporary directory + files := make([]string, 0) + err = filepath.WalkDir(dst, func(path string, d fs.DirEntry, err error) error { + if !d.IsDir() { + files = append(files, path) + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("error walking downloaded file tree: %w", err) + } + + // conftest's parser returns a map[string]interface where the filenames are + // the primary map keys. + config, err := parser.ParseConfigurations(files) + if err != nil { + return nil, err + } + + // clean up the resources so it's using the filepath.Name as the map key, + // instead of the file path. + drs := make(types.DomainResources, len(config)) + for k, v := range config { + rel, err := filepath.Rel(dst, k) + if err != nil { + return nil, fmt.Errorf("error determining relative file path: %w", err) + } + drs[filenames[rel]] = v + } + return drs, nil +} + +// IsExecutable returns false; the file domain is read-only. +// +// The files domain will download remote files into a temporary directory if the +// file paths are remote, but that is temporary and it is not mutating existing +// resources. +func (d Domain) IsExecutable() bool { return false } + +func CreateDomain(spec *Spec) (types.Domain, error) { + if len(spec.Filepaths) == 0 { + return nil, fmt.Errorf("file-spec must not be empty") + } + return Domain{spec}, nil +} diff --git a/src/pkg/domains/files/files_test.go b/src/pkg/domains/files/files_test.go new file mode 100644 index 00000000..2824fa0d --- /dev/null +++ b/src/pkg/domains/files/files_test.go @@ -0,0 +1,34 @@ +package files + +import ( + "context" + "testing" + + "github.com/defenseunicorns/lula/src/types" + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" +) + +var _ types.Domain = (*Domain)(nil) + +func TestGetResource(t *testing.T) { + t.Run("local files", func(t *testing.T) { + d := Domain{Spec: &Spec{Filepaths: []FileInfo{ + {Name: "foo.yaml", Path: "foo.yaml"}, + {Name: "bar.json", Path: "bar.json"}, + {Name: "arbitraryname", Path: "nested-directory/baz.hcl2"}, + }}} + + resources, err := d.GetResources(context.WithValue(context.Background(), types.LulaValidationWorkDir, "testdata")) + require.NoError(t, err) + if diff := cmp.Diff(resources, types.DomainResources{ + "bar.json": map[string]interface{}{"cat": "Cheetarah"}, + "foo.yaml": "cat = Li Shou", + "arbitraryname": map[string]any{ + "resource": map[string]any{"catname": map[string]any{"blackcat": map[string]any{"name": "robin"}}}, + }, + }); diff != "" { + t.Fatalf("wrong result:\n%s\n", diff) + } + }) +} diff --git a/src/pkg/domains/files/spec.go b/src/pkg/domains/files/spec.go new file mode 100644 index 00000000..45dd78d0 --- /dev/null +++ b/src/pkg/domains/files/spec.go @@ -0,0 +1,10 @@ +package files + +type Spec struct { + Filepaths []FileInfo `json:"filepaths" yaml:"filepaths"` +} + +type FileInfo struct { + Name string `json:"name" yaml:"name"` + Path string `json:"path" yaml:"path"` +} diff --git a/src/pkg/domains/files/testdata/bar.json b/src/pkg/domains/files/testdata/bar.json new file mode 100644 index 00000000..a30ce675 --- /dev/null +++ b/src/pkg/domains/files/testdata/bar.json @@ -0,0 +1 @@ +{ "cat": "Cheetarah"} \ No newline at end of file diff --git a/src/pkg/domains/files/testdata/foo.yaml b/src/pkg/domains/files/testdata/foo.yaml new file mode 100644 index 00000000..e2723e35 --- /dev/null +++ b/src/pkg/domains/files/testdata/foo.yaml @@ -0,0 +1 @@ +cat = Li Shou \ No newline at end of file diff --git a/src/pkg/domains/files/testdata/nested-directory/baz.hcl2 b/src/pkg/domains/files/testdata/nested-directory/baz.hcl2 new file mode 100644 index 00000000..0635204e --- /dev/null +++ b/src/pkg/domains/files/testdata/nested-directory/baz.hcl2 @@ -0,0 +1,3 @@ +resource "catname" "blackcat" { + name = "robin" +} \ No newline at end of file diff --git a/src/pkg/domains/kubernetes/types.go b/src/pkg/domains/kubernetes/types.go index e276093e..9da54acd 100644 --- a/src/pkg/domains/kubernetes/types.go +++ b/src/pkg/domains/kubernetes/types.go @@ -87,7 +87,7 @@ func CreateKubernetesDomain(ctx context.Context, spec *KubernetesSpec) (types.Do }, nil } -func (k KubernetesDomain) GetResources() (resources types.DomainResources, err error) { +func (k KubernetesDomain) GetResources(_ context.Context) (resources types.DomainResources, err error) { // Evaluate the wait condition if k.Spec.Wait != nil { err := EvaluateWait(*k.Spec.Wait) diff --git a/src/test/e2e/api_validation_test.go b/src/test/e2e/api_validation_test.go index f6b83301..e55db58b 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(context.Background(), 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(context.Background(), 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..05d19d40 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(context.Background(), 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) } @@ -96,7 +101,7 @@ func TestComponentDefinitionComposition(t *testing.T) { t.Errorf("component definition is nil") } - composeResults, err := validate.ValidateOnCompDef(oscalModel.ComponentDefinition, "") + composeResults, err := validate.ValidateOnCompDef(context.Background(), oscalModel.ComponentDefinition, "") 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..b2bbf0c7 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(context.Background(), 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(context.Background(), oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/file_validation_test.go b/src/test/e2e/file_validation_test.go new file mode 100644 index 00000000..275378a4 --- /dev/null +++ b/src/test/e2e/file_validation_test.go @@ -0,0 +1,117 @@ +package test + +import ( + "context" + "testing" + + "github.com/defenseunicorns/lula/src/cmd/validate" + "github.com/defenseunicorns/lula/src/types" +) + +func TestFileValidation(t *testing.T) { + failDir := "./scenarios/file-validations/fail" + passDir := "./scenarios/file-validations/pass" + oscalFile := "/component-definition.yaml" + kyvernoFile := "/oscal-component-kyverno.yaml" + + t.Run("success - opa", func(t *testing.T) { + ctx := context.WithValue(context.Background(), types.LulaValidationWorkDir, passDir) + assessment, err := validate.ValidateOnPath(ctx, passDir+oscalFile, "") + if err != nil { + t.Fatal(err) + } + + if len(assessment.Results) == 0 { + t.Fatal("Expected greater than zero results") + } + + result := assessment.Results[0] + if result.Findings == nil { + t.Fatal("Expected findings to be not nil") + } + + for _, finding := range *result.Findings { + state := finding.Target.Status.State + if state != "satisfied" { + t.Fatal("State should be satisfied, but got :", state) + } + } + }) + t.Run("success - kyverno", func(t *testing.T) { + ctx := context.WithValue(context.Background(), types.LulaValidationWorkDir, passDir) + assessment, err := validate.ValidateOnPath(ctx, passDir+kyvernoFile, "") + if err != nil { + t.Fatal(err) + } + + if len(assessment.Results) == 0 { + t.Fatal("Expected greater than zero results") + } + + result := assessment.Results[0] + if result.Findings == nil { + t.Fatal("Expected findings to be not nil") + } + + for _, finding := range *result.Findings { + state := finding.Target.Status.State + if state != "satisfied" { + t.Fatal("State should be satisfied, but got :", state) + } + } + }) + t.Run("fail - opa", func(t *testing.T) { + ctx := context.WithValue(context.Background(), types.LulaValidationWorkDir, failDir) + assessment, err := validate.ValidateOnPath(ctx, failDir+oscalFile, "") + if err != nil { + t.Fatal(err) + } + + if len(assessment.Results) == 0 { + t.Fatal("Expected greater than zero results") + } + + result := assessment.Results[0] + if result.Findings == nil { + t.Fatal("Expected findings to be not nil") + } + + for _, finding := range *result.Findings { + state := finding.Target.Status.State + if state != "not-satisfied" { + t.Fatal("State should be non-satisfied, but got :", state) + } + } + }) + t.Run("fail - kyverno", func(t *testing.T) { + ctx := context.WithValue(context.Background(), types.LulaValidationWorkDir, failDir) + assessment, err := validate.ValidateOnPath(ctx, failDir+kyvernoFile, "") + if err != nil { + t.Fatal(err) + } + + if len(assessment.Results) == 0 { + t.Fatal("Expected greater than zero results") + } + + result := assessment.Results[0] + if result.Findings == nil { + t.Fatal("Expected findings to be not nil") + } + + for _, finding := range *result.Findings { + state := finding.Target.Status.State + if state != "not-satisfied" { + t.Fatal("State should be non-satisfied, but got :", state) + } + } + }) + + t.Run("invalid input", func(t *testing.T) { + ctx := context.WithValue(context.Background(), types.LulaValidationWorkDir, "scenarios/file-validations/invalid") + _, err := validate.ValidateOnPath(ctx, "scenarios/file-validations/invalid/oscal-component.yaml", "") + if err == nil { + t.Fatal("expected error, got success") + } + }) +} diff --git a/src/test/e2e/multi_resource_validation_test.go b/src/test/e2e/multi_resource_validation_test.go index 1541e428..173ad984 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(context.Background(), oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/outputs_test.go b/src/test/e2e/outputs_test.go index 57692a5f..60c0a02a 100644 --- a/src/test/e2e/outputs_test.go +++ b/src/test/e2e/outputs_test.go @@ -57,7 +57,7 @@ func TestOutputs(t *testing.T) { components := *compDef.Components validationStore := validationstore.NewValidationStoreFromBackMatter(*compDef.BackMatter) - findingMap, observations, err := validate.ValidateOnControlImplementations(components[0].ControlImplementations, validationStore, "") + findingMap, observations, err := validate.ValidateOnControlImplementations(context.Background(), components[0].ControlImplementations, validationStore, "") 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..6f6cc2cd 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(context.Background(), 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(context.Background(), 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(context.Background(), 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..8b183d69 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(context.Background(), 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..3bf36f7c 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(context.Background(), 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..3547164d 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(context.Background(), oscalPath, "") if err != nil { t.Fatal(err) } diff --git a/src/test/e2e/scenarios/file-validations/fail/component-definition.yaml b/src/test/e2e/scenarios/file-validations/fail/component-definition.yaml new file mode 100644 index 00000000..fef12405 --- /dev/null +++ b/src/test/e2e/scenarios/file-validations/fail/component-definition.yaml @@ -0,0 +1,83 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: OSCAL Demo Tool + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.1 + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Defense Unicorns + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: software + title: lula + description: | + Defense Unicorns lula + 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://github.com/defenseunicorns/lula + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + 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. + links: + - href: "#88AB3470-B96B-4D7C-BC36-02BF9563C46C" + rel: lula + back-matter: + resources: + - uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C + rlinks: + - href: lula + description: | + domain: + type: file + file-spec: + filepaths: + - Name: config + path: config.json + provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + validate if { + check_server_protocol.result + } + msg = check_server_protocol.msg + + config := input["config"] + protocol := config.server.protocol + + check_server_protocol = {"result": true, "msg": msg} if { + protocol == "https" + msg := "Server protocol is set to https" + } else = {"result": false, "msg": msg} if { + protocol == "http" + msg := "Server Protocol must be https - http is disallowed" + } + + output: + validation: validate.validate + observations: + - validate.msg \ No newline at end of file diff --git a/src/test/e2e/scenarios/file-validations/fail/config.json b/src/test/e2e/scenarios/file-validations/fail/config.json new file mode 100644 index 00000000..0f433c1e --- /dev/null +++ b/src/test/e2e/scenarios/file-validations/fail/config.json @@ -0,0 +1,5 @@ +{ + "server": { + "protocol": "http" + } +} \ No newline at end of file diff --git a/src/test/e2e/scenarios/file-validations/fail/oscal-component-kyverno.yaml b/src/test/e2e/scenarios/file-validations/fail/oscal-component-kyverno.yaml new file mode 100644 index 00000000..3a3e7561 --- /dev/null +++ b/src/test/e2e/scenarios/file-validations/fail/oscal-component-kyverno.yaml @@ -0,0 +1,69 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: OSCAL Demo Tool + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.1 + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Defense Unicorns + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: software + title: lula + description: | + Defense Unicorns lula + 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://github.com/defenseunicorns/lula + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + 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. + links: + - href: "#88AB3470-B96B-4D7C-BC36-02BF9563C46C" + rel: lula + back-matter: + resources: + - uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C + rlinks: + - href: lula + description: | + domain: + type: file + file-spec: + filepaths: + - name: config + path: config.json + provider: + type: kyverno + kyverno-spec: + policy: + apiVersion: json.kyverno.io/v1alpha1 + kind: ValidatingPolicy + metadata: + name: grafana-config + spec: + rules: + - name: protocol-is-https + assert: + all: + - check: + config: + server: + protocol: https \ No newline at end of file diff --git a/src/test/e2e/standard/testdata/empty.golden b/src/test/e2e/scenarios/file-validations/invalid/oscal-component.yaml similarity index 100% rename from src/test/e2e/standard/testdata/empty.golden rename to src/test/e2e/scenarios/file-validations/invalid/oscal-component.yaml diff --git a/src/test/e2e/scenarios/file-validations/pass/component-definition.yaml b/src/test/e2e/scenarios/file-validations/pass/component-definition.yaml new file mode 100644 index 00000000..fef12405 --- /dev/null +++ b/src/test/e2e/scenarios/file-validations/pass/component-definition.yaml @@ -0,0 +1,83 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: OSCAL Demo Tool + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.1 + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Defense Unicorns + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: software + title: lula + description: | + Defense Unicorns lula + 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://github.com/defenseunicorns/lula + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + 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. + links: + - href: "#88AB3470-B96B-4D7C-BC36-02BF9563C46C" + rel: lula + back-matter: + resources: + - uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C + rlinks: + - href: lula + description: | + domain: + type: file + file-spec: + filepaths: + - Name: config + path: config.json + provider: + type: opa + opa-spec: + rego: | + package validate + import rego.v1 + + # Default values + default validate := false + default msg := "Not evaluated" + + validate if { + check_server_protocol.result + } + msg = check_server_protocol.msg + + config := input["config"] + protocol := config.server.protocol + + check_server_protocol = {"result": true, "msg": msg} if { + protocol == "https" + msg := "Server protocol is set to https" + } else = {"result": false, "msg": msg} if { + protocol == "http" + msg := "Server Protocol must be https - http is disallowed" + } + + output: + validation: validate.validate + observations: + - validate.msg \ No newline at end of file diff --git a/src/test/e2e/scenarios/file-validations/pass/config.json b/src/test/e2e/scenarios/file-validations/pass/config.json new file mode 100644 index 00000000..f551b8d0 --- /dev/null +++ b/src/test/e2e/scenarios/file-validations/pass/config.json @@ -0,0 +1,5 @@ +{ + "server": { + "protocol": "https" + } +} \ No newline at end of file diff --git a/src/test/e2e/scenarios/file-validations/pass/oscal-component-kyverno.yaml b/src/test/e2e/scenarios/file-validations/pass/oscal-component-kyverno.yaml new file mode 100644 index 00000000..3a3e7561 --- /dev/null +++ b/src/test/e2e/scenarios/file-validations/pass/oscal-component-kyverno.yaml @@ -0,0 +1,69 @@ +component-definition: + uuid: E6A291A4-2BC8-43A0-B4B2-FD67CAAE1F8F + metadata: + title: OSCAL Demo Tool + last-modified: "2022-09-13T12:00:00Z" + version: "20220913" + oscal-version: 1.1.1 + parties: + # Should be consistent across all of the packages, but where is ground truth? + - uuid: C18F4A9F-A402-415B-8D13-B51739D689FF + type: organization + name: Defense Unicorns + links: + - href: https://github.com/defenseunicorns/lula + rel: website + components: + - uuid: A9D5204C-7E5B-4C43-BD49-34DF759B9F04 + type: software + title: lula + description: | + Defense Unicorns lula + 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://github.com/defenseunicorns/lula + description: Validate generic security requirements + implemented-requirements: + - uuid: 42C2FFDC-5F05-44DF-A67F-EEC8660AEFFD + control-id: ID-1 + 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. + links: + - href: "#88AB3470-B96B-4D7C-BC36-02BF9563C46C" + rel: lula + back-matter: + resources: + - uuid: 88AB3470-B96B-4D7C-BC36-02BF9563C46C + rlinks: + - href: lula + description: | + domain: + type: file + file-spec: + filepaths: + - name: config + path: config.json + provider: + type: kyverno + kyverno-spec: + policy: + apiVersion: json.kyverno.io/v1alpha1 + kind: ValidatingPolicy + metadata: + name: grafana-config + spec: + rules: + - name: protocol-is-https + assert: + all: + - check: + config: + server: + protocol: https \ No newline at end of file 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/validation_composition_test.go b/src/test/e2e/validation_composition_test.go index e6ff1779..494b50c7 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(context.Background(), 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) } @@ -116,7 +117,7 @@ func validateComposition(ctx context.Context, t *testing.T, oscalPath, expectedF // Create a validation store from the back-matter if it exists validationStore := validationstore.NewValidationStoreFromBackMatter(*compDef.BackMatter) - findingMap, observations, err := validate.ValidateOnControlImplementations(components[0].ControlImplementations, validationStore, "") + findingMap, observations, err := validate.ValidateOnControlImplementations(context.Background(), components[0].ControlImplementations, validationStore, "") if err != nil { t.Fatalf("Error with validateOnControlImplementations: %v", 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) { diff --git a/src/types/context_keys.go b/src/types/context_keys.go new file mode 100644 index 00000000..b96e5dce --- /dev/null +++ b/src/types/context_keys.go @@ -0,0 +1,7 @@ +package types + +type contextKey int + +const ( + LulaValidationWorkDir contextKey = iota +) diff --git a/src/types/types.go b/src/types/types.go index 193a5ce3..7f8b78e5 100644 --- a/src/types/types.go +++ b/src/types/types.go @@ -1,6 +1,7 @@ package types import ( + "context" "encoding/json" "errors" "fmt" @@ -116,7 +117,7 @@ func GetResourcesOnly(onlyResources bool) LulaValidationOption { } // Perform the validation, and store the result in the LulaValidation struct -func (val *LulaValidation) Validate(opts ...LulaValidationOption) error { +func (val *LulaValidation) Validate(ctx context.Context, opts ...LulaValidationOption) error { if !val.Evaluated { var result Result var err error @@ -157,7 +158,7 @@ func (val *LulaValidation) Validate(opts ...LulaValidationOption) error { if config.staticResources != nil { resources = config.staticResources } else { - resources, err = (*val.Domain).GetResources() + resources, err = (*val.Domain).GetResources(ctx) if err != nil { return fmt.Errorf("%w: %v", ErrDomainGetResources, err) } @@ -196,7 +197,7 @@ func (val *LulaValidation) GetDomainResourcesAsJSON() []byte { type DomainResources map[string]interface{} type Domain interface { - GetResources() (DomainResources, error) + GetResources(context.Context) (DomainResources, error) IsExecutable() bool }