Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions cmd/validate/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"runtime/trace"
"slices"
"sort"
"strings"

Expand Down Expand Up @@ -58,24 +59,30 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
filterType: "include-exclude", // Default to include-exclude filter
}
Comment thread
st3penta marked this conversation as resolved.
cmd := &cobra.Command{
Use: "input",
Use: "input [file ...] [flags]",
Short: "Validate arbitrary JSON or yaml file input conformance with the provided policies",
Long: hd.Doc(`
Validate conformance of arbitrary JSON or yaml file input with the provided policies

For each file, validation is performed to determine if the file conforms to rego policies
defined in the EnterpriseContractPolicy.

Input files can be specified as positional arguments or via the --file flag (deprecated).
If both are provided, the values are combined.
`),
Example: hd.Doc(`
Use an EnterpriseContractPolicy spec from a local YAML file to validate a single file
ec validate input --file /path/to/file.json --policy my-policy.yaml
Validate a single file using positional arguments
ec validate input /path/to/file.json --policy my-policy.yaml
Comment thread
st3penta marked this conversation as resolved.

Use an EnterpriseContractPolicy spec from a local YAML file to validate multiple files
The file flag can be repeated for multiple input files.
ec validate input --file /path/to/file.yaml --file /path/to/file2.yaml --policy my-policy.yaml
Validate multiple files using positional arguments
ec validate input /path/to/file.yaml /path/to/file2.yaml --policy my-policy.yaml

Validate all YAML files in a directory using shell glob expansion
ec validate input *.yaml --policy my-policy.yaml

Use an EnterpriseContractPolicy spec from a local YAML file to validate multiple files
The file flag can take a comma separated series of files.
Use the deprecated --file flag (still functional, multiple forms supported)
ec validate input --file /path/to/file.json --policy my-policy.yaml
ec validate input --file /path/to/file.yaml --file /path/to/file2.yaml --policy my-policy.yaml
ec validate input --file="/path/to/file.json,/path/to/file2.json" --policy my-policy.yaml

Use a git url for the policy configuration. In the first example there should be a '.ec/policy.yaml'
Expand All @@ -84,12 +91,36 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
of the git repo. For git repos not hosted on 'github.com' or 'gitlab.com', prefix the url with
'git::'. For the policy configuration files you can use json instead of yaml if you prefer.

ec validate input --file /path/to/file.json --policy github.com/user/repo//default?ref=main
ec validate input /path/to/file.json --policy github.com/user/repo//default?ref=main
Comment thread
st3penta marked this conversation as resolved.

ec validate input --file /path/to/file.yaml --policy github.com/user/repo
ec validate input /path/to/file.yaml --policy github.com/user/repo

`),
PreRunE: func(cmd *cobra.Command, args []string) (allErrors error) {
// Merge positional arguments into filePaths
data.filePaths = append(data.filePaths, args...)

// Deduplicate file paths
seen := make(map[string]struct{}, len(data.filePaths))
unique := make([]string, 0, len(data.filePaths))
for _, f := range data.filePaths {
if _, ok := seen[f]; !ok {
seen[f] = struct{}{}
unique = append(unique, f)
}
}
data.filePaths = unique

// Strip empty and whitespace-only entries
data.filePaths = slices.DeleteFunc(data.filePaths, func(s string) bool {
return strings.TrimSpace(s) == ""
})

if len(data.filePaths) == 0 {
Comment thread
st3penta marked this conversation as resolved.
Comment thread
st3penta marked this conversation as resolved.
allErrors = errors.Join(allErrors, fmt.Errorf("at least one input file must be specified as a positional argument or via the --file flag"))
return
}

ctx := cmd.Context()

policyConfiguration, err := validate_utils.GetPolicyConfig(ctx, data.policyConfiguration)
Expand Down Expand Up @@ -231,7 +262,7 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
},
Comment thread
st3penta marked this conversation as resolved.
}

cmd.Flags().StringSliceVarP(&data.filePaths, "file", "f", data.filePaths, "path to input YAML/JSON file (required)")
cmd.Flags().StringSliceVarP(&data.filePaths, "file", "f", data.filePaths, "DEPRECATED: use positional arguments instead. Path to input YAML/JSON file")

cmd.Flags().StringVarP(&data.policyConfiguration, "policy", "p", data.policyConfiguration, hd.Doc(`
Policy configuration as:
Expand Down Expand Up @@ -275,7 +306,7 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
cmd.Flags().BoolVar(&data.forceColor, "color", false, hd.Doc(`
Enable color when using text output even when the current terminal does not support it`))

Comment thread
st3penta marked this conversation as resolved.
if err := cmd.MarkFlagRequired("file"); err != nil {
if err := cmd.Flags().MarkHidden("file"); err != nil {
Comment thread
st3penta marked this conversation as resolved.
panic(err)
}

Expand Down
183 changes: 182 additions & 1 deletion cmd/validate/input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,14 +269,17 @@ func Test_ValidateInputCmd_NoPolicyProvided(t *testing.T) {
}

func Test_ValidateInputCmd_NoFileProvided(t *testing.T) {
// No SetTestRekorPublicKey call: this test verifies PreRunE returns early
// before policy initialization (which would require Rekor key setup).
cmd, _ := setUpValidateInputCmd(nil, afero.NewMemMapFs())
cmd.SetArgs([]string{
"input",
"--policy", `{"publicKey":"testkey"}`,
})

err := cmd.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "required flag(s) \"file\" not set")
assert.Contains(t, err.Error(), "at least one input file must be specified")
}

func Test_ValidateInputCmd_PolicyParsingError(t *testing.T) {
Expand Down Expand Up @@ -440,6 +443,184 @@ func Test_ValidateInputCmd_DefaultTextOutput(t *testing.T) {
assert.NotContains(t, output, "Results:")
}

func Test_ValidateInputCmd_PositionalArgsVariants(t *testing.T) {
outMock := &output.Output{
PolicyCheck: []evaluator.Outcome{
{
Successes: []evaluator.Result{
{Message: "Pass"},
},
},
},
}

cases := []struct {
name string
files map[string]string
args []string
expectedFiles []string
}{
{
name: "single positional arg",
files: map[string]string{"/input.yaml": "some: data"},
args: []string{
"input", "/input.yaml",
"--policy", `{"publicKey": "testkey"}`,
"--output", "json",
},
expectedFiles: []string{"/input.yaml"},
},
{
name: "multiple positional args",
files: map[string]string{
"/input1.yaml": "some: data",
"/input2.yaml": "other: data",
},
args: []string{
"input", "/input1.yaml", "/input2.yaml",
"--policy", `{"publicKey": "testkey"}`,
"--output", "json",
},
expectedFiles: []string{"/input1.yaml", "/input2.yaml"},
},
{
name: "mixed positional and flag",
files: map[string]string{
"/flag-file.yaml": "some: data",
"/positional-file.yaml": "other: data",
},
args: []string{
"input", "/positional-file.yaml",
"--file", "/flag-file.yaml",
"--policy", `{"publicKey": "testkey"}`,
"--output", "json",
},
expectedFiles: []string{"/flag-file.yaml", "/positional-file.yaml"},
},
{
name: "dedup when same file via both flag and positional",
files: map[string]string{"/shared.yaml": "some: data"},
args: []string{
"input", "/shared.yaml",
"--file", "/shared.yaml",
"--policy", `{"publicKey": "testkey"}`,
"--output", "json",
},
expectedFiles: []string{"/shared.yaml"},
},
{
name: "flag-only (deprecated but functional, no warning)",
files: map[string]string{
"/flag-only.yaml": "some: data",
},
args: []string{
"input",
"--file", "/flag-only.yaml",
"--policy", `{"publicKey": "testkey"}`,
"--output", "json",
},
expectedFiles: []string{"/flag-only.yaml"},
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
fs := afero.NewMemMapFs()
for path, content := range c.files {
require.NoError(t, afero.WriteFile(fs, path, []byte(content), 0644))
}

cmd, buf := setUpValidateInputCmd(mockValidate(outMock, nil), fs)
cmd.SetArgs(c.args)

utils.SetTestRekorPublicKey(t)
err := cmd.Execute()
require.NoError(t, err)

var outJSON map[string]interface{}
err = json.Unmarshal(buf.Bytes(), &outJSON)
require.NoError(t, err)
require.True(t, outJSON["success"].(bool))

inputFiles, ok := outJSON["filepaths"].([]interface{})
require.True(t, ok, "expected 'filepaths' key in output")
require.Len(t, inputFiles, len(c.expectedFiles))

filePaths := []string{}
for _, input := range inputFiles {
f, ok := input.(map[string]interface{})["filepath"].(string)
require.True(t, ok, "expected 'filepath' key in input object")
filePaths = append(filePaths, f)
}
for _, expected := range c.expectedFiles {
assert.Contains(t, filePaths, expected)
}
})
}
}

func Test_ValidateInputCmd_InvalidPositionalArgs(t *testing.T) {
cases := []struct {
name string
validate InputValidationFunc
args []string
needsRekor bool
expectedErr string
}{
{
name: "flag-like positional arg rejected",
Comment thread
st3penta marked this conversation as resolved.
args: []string{
"input", "--polcy", "foo.yaml",
"--policy", `{"publicKey": "testkey"}`,
},
expectedErr: "unknown flag: --polcy",
},
{
name: "single-dash flag-like positional arg rejected",
args: []string{
"input", "-x", "foo.yaml",
"--policy", `{"publicKey": "testkey"}`,
},
expectedErr: "unknown shorthand flag: 'x' in -x",
},
{
name: "empty string positional arg",
args: []string{
"input", "",
"--policy", `{"publicKey": "testkey"}`,
"--output", "json",
},
Comment thread
st3penta marked this conversation as resolved.
expectedErr: "at least one input file must be specified",
},
{
name: "non-existent file path",
validate: mockValidate(nil, errors.New("file not found")),
args: []string{
"input", "/nonexistent/path.yaml",
"--policy", `{"publicKey": "testkey"}`,
"--output", "json",
},
needsRekor: true,
expectedErr: "error validating file /nonexistent/path.yaml",
},
}

for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
cmd, _ := setUpValidateInputCmd(c.validate, afero.NewMemMapFs())
cmd.SetArgs(c.args)

if c.needsRekor {
utils.SetTestRekorPublicKey(t)
}

err := cmd.Execute()
require.Error(t, err)
assert.Contains(t, err.Error(), c.expectedErr)
})
}
}

func Test_ValidateInputCmd_TextOutputWithShowSuccesses(t *testing.T) {
fs := afero.NewMemMapFs()
require.NoError(t, afero.WriteFile(fs, "/input.yaml", []byte("some: data"), 0644))
Expand Down
28 changes: 17 additions & 11 deletions docs/modules/ROOT/pages/ec_validate_input.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,27 @@ Validate conformance of arbitrary JSON or yaml file input with the provided poli
For each file, validation is performed to determine if the file conforms to rego policies
defined in the EnterpriseContractPolicy.

Input files can be specified as positional arguments or via the --file flag (deprecated).
If both are provided, the values are combined.

[source,shell]
----
ec validate input [flags]
ec validate input [file ...] [flags]
----

== Examples
Use an EnterpriseContractPolicy spec from a local YAML file to validate a single file
ec validate input --file /path/to/file.json --policy my-policy.yaml
Validate a single file using positional arguments
ec validate input /path/to/file.json --policy my-policy.yaml

Use an EnterpriseContractPolicy spec from a local YAML file to validate multiple files
The file flag can be repeated for multiple input files.
ec validate input --file /path/to/file.yaml --file /path/to/file2.yaml --policy my-policy.yaml
Validate multiple files using positional arguments
ec validate input /path/to/file.yaml /path/to/file2.yaml --policy my-policy.yaml

Use an EnterpriseContractPolicy spec from a local YAML file to validate multiple files
The file flag can take a comma separated series of files.
Validate all YAML files in a directory using shell glob expansion
ec validate input *.yaml --policy my-policy.yaml

Use the deprecated --file flag (still functional, multiple forms supported)
ec validate input --file /path/to/file.json --policy my-policy.yaml
ec validate input --file /path/to/file.yaml --file /path/to/file2.yaml --policy my-policy.yaml
ec validate input --file="/path/to/file.json,/path/to/file2.json" --policy my-policy.yaml

Use a git url for the policy configuration. In the first example there should be a '.ec/policy.yaml'
Expand All @@ -32,9 +38,9 @@ example there should be a '.ec/policy.yaml' or a 'policy.yaml' file in the top l
of the git repo. For git repos not hosted on 'github.com' or 'gitlab.com', prefix the url with
'git::'. For the policy configuration files you can use json instead of yaml if you prefer.

ec validate input --file /path/to/file.json --policy github.com/user/repo//default?ref=main
ec validate input /path/to/file.json --policy github.com/user/repo//default?ref=main

ec validate input --file /path/to/file.yaml --policy github.com/user/repo
ec validate input /path/to/file.yaml --policy github.com/user/repo


== Options
Expand All @@ -43,7 +49,7 @@ of the git repo. For git repos not hosted on 'github.com' or 'gitlab.com', prefi
--effective-time:: Run policy checks with the provided time. Useful for testing rules with
effective dates in the future. The value can be "now" (default) - for
current time, or a RFC3339 formatted value, e.g. 2022-11-18T00:00:00Z. (Default: now)
-f, --file:: path to input YAML/JSON file (required) (Default: [])
-f, --file:: DEPRECATED: use positional arguments instead. Path to input YAML/JSON file (Default: [])
--filter-type:: Filter type to use for policy evaluation. Options: "include-exclude" (default) or "ec-policy".
- "include-exclude": Uses traditional include/exclude filtering without pipeline intentions
- "ec-policy": Uses Enterprise Contract policy filtering with pipeline intention support (Default: include-exclude)
Expand Down
31 changes: 31 additions & 0 deletions features/__snapshots__/validate_input.snap
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,34 @@ success: true
[TestFeatures/valid policy URL with publicKey in policy config:stderr - 1]

---

[TestFeatures/deprecated file flag backward compatibility:stdout - 1]
{
"success": true,
"filepaths": [
{
"filepath": "pipeline_definition.yaml",
"violations": [],
"warnings": [],
"successes": null,
"success": true,
"success-count": 1
}
],
"policy": {
"sources": [
{
"policy": [
"git::https://${GITHOST}/git/happy-day-policy.git"
]
}
]
},
"ec-version": "${EC_VERSION}",
"effective-time": "${TIMESTAMP}"
}
---

[TestFeatures/deprecated file flag backward compatibility:stderr - 1]

---
Loading
Loading