Skip to content

Commit 05297c1

Browse files
tookyclaude
andauthored
feat: add --params flag to kosli evaluate commands (#762)
## Summary - Adds `--params` flag to `evaluate trail`, `evaluate trails`, and `evaluate input`, allowing policy authors to pass configuration data (thresholds, expected counts, etc.) without duplicating policy files - Params are available in Rego policies as `data.params`, using OPA's idiomatic data document — existing policies that don't reference `data.params` are completely unaffected - Accepts inline JSON (`--params '{"min_approvers": 2}'`) or file reference (`--params @params.json`) ## Motivation Policies are currently fully static. Thresholds like "zero approvers" or "no critical vulnerabilities" are baked into the `.rego` file. If you want the same policy logic with different tolerances — say, 1 approver in staging, 2 in production — you need separate policy files duplicating the logic. That's the kind of duplication `kosli evaluate` is meant to eliminate. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a0c7dd6 commit 05297c1

12 files changed

Lines changed: 248 additions & 22 deletions

File tree

TODO.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,10 @@
4747
- [x] Refactor: use `cmd.InOrStdin()` for testable stdin
4848
- [x] Refactor: embed `commonEvaluateOptions` to remove flag duplication
4949
- [x] Slice 5: Detect terminal stdin and error when no input is piped
50+
51+
## Add `--params` flag to `kosli evaluate` commands
52+
53+
- [x] Slice 1: `evaluate.Evaluate()` accepts params, passes via OPA data store
54+
- [x] Slice 2: Add `--params` flag across all three commands
55+
- [x] Slice 3: Show params in `--show-input` output
56+
- [x] Slice 4: Update help text and examples

cmd/kosli/evaluate.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ Use ` + "`evaluate input`" + ` to evaluate a local JSON file or stdin without an
1818
1919
The policy must use ` + "`package policy`" + ` and define an ` + "`allow`" + ` rule.
2020
An optional ` + "`violations`" + ` rule (a set of strings) can provide human-readable denial reasons.
21-
The command exits with code 0 when allowed and code 1 when denied.`
21+
The command exits with code 0 when allowed and code 1 when denied.
22+
23+
Use ` + "`--params`" + ` to pass configuration data (thresholds, expected counts, etc.)
24+
to your policy. Params are available as ` + "`data.params`" + ` in Rego, keeping policy
25+
logic reusable across environments with different tolerances.`
2226

2327
func newEvaluateCmd(out io.Writer) *cobra.Command {
2428
cmd := &cobra.Command{

cmd/kosli/evaluateHelpers.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net/http"
88
"net/url"
99
"os"
10+
"strings"
1011

1112
"github.com/kosli-dev/cli/internal/evaluate"
1213
"github.com/kosli-dev/cli/internal/output"
@@ -20,6 +21,7 @@ type commonEvaluateOptions struct {
2021
output string
2122
showInput bool
2223
attestations []string
24+
params string
2325
}
2426

2527
func (o *commonEvaluateOptions) addFlags(cmd *cobra.Command, policyDesc string) {
@@ -28,6 +30,7 @@ func (o *commonEvaluateOptions) addFlags(cmd *cobra.Command, policyDesc string)
2830
cmd.Flags().StringVarP(&o.output, "output", "o", "table", outputFlag)
2931
cmd.Flags().BoolVar(&o.showInput, "show-input", false, "[optional] Include the policy input data in the output.")
3032
cmd.Flags().StringSliceVar(&o.attestations, "attestations", nil, "[optional] Limit which attestations are included. Plain name for trail-level, dot-qualified (artifact.name) for artifact-level.")
33+
cmd.Flags().StringVar(&o.params, "params", "", "[optional] Policy parameters as inline JSON or @file.json. Available in policies as data.params.")
3134
}
3235

3336
func fetchAndEnrichTrail(flowName, trailName string, attestations []string) (interface{}, error) {
@@ -90,13 +93,36 @@ func fetchAndEnrichTrail(flowName, trailName string, attestations []string) (int
9093
return trailData, nil
9194
}
9295

93-
func evaluateAndPrintResult(out io.Writer, policyFile string, input map[string]interface{}, outputFormat string, showInput bool) error {
96+
func parseParams(raw string) (map[string]interface{}, error) {
97+
if raw == "" {
98+
return nil, nil
99+
}
100+
101+
var jsonBytes []byte
102+
if strings.HasPrefix(raw, "@") {
103+
var err error
104+
jsonBytes, err = os.ReadFile(raw[1:])
105+
if err != nil {
106+
return nil, fmt.Errorf("failed to read --params file: %w", err)
107+
}
108+
} else {
109+
jsonBytes = []byte(raw)
110+
}
111+
112+
var params map[string]interface{}
113+
if err := json.Unmarshal(jsonBytes, &params); err != nil {
114+
return nil, fmt.Errorf("failed to parse --params: %w", err)
115+
}
116+
return params, nil
117+
}
118+
119+
func evaluateAndPrintResult(out io.Writer, policyFile string, input map[string]interface{}, outputFormat string, showInput bool, params map[string]interface{}) error {
94120
policySource, err := os.ReadFile(policyFile)
95121
if err != nil {
96122
return fmt.Errorf("failed to read policy file: %w", err)
97123
}
98124

99-
result, err := evaluate.Evaluate(string(policySource), input)
125+
result, err := evaluate.Evaluate(string(policySource), input, params)
100126
if err != nil {
101127
return err
102128
}
@@ -108,6 +134,9 @@ func evaluateAndPrintResult(out io.Writer, policyFile string, input map[string]i
108134
if showInput {
109135
auditResult["input"] = input
110136
}
137+
if showInput && params != nil {
138+
auditResult["params"] = params
139+
}
111140

112141
raw, err := json.Marshal(auditResult)
113142
if err != nil {

cmd/kosli/evaluateInput.go

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ The policy must use ` + "`package policy`" + ` and define an ` + "`allow`" + ` r
2727
An optional ` + "`violations`" + ` rule (a set of strings) can provide human-readable denial reasons.
2828
The command exits with code 0 when allowed and code 1 when denied.
2929
30-
When ` + "`--input-file`" + ` is omitted, JSON is read from stdin.`
30+
When ` + "`--input-file`" + ` is omitted, JSON is read from stdin.
31+
32+
Use ` + "`--params`" + ` to pass configuration data to the policy as ` + "`data.params`" + `.
33+
This accepts inline JSON or a file reference (` + "`@file.json`" + `).`
3134

3235
const evaluateInputExample = `
3336
# capture trail data for local policy iteration:
@@ -49,7 +52,19 @@ kosli evaluate input \
4952
5053
# read input from stdin:
5154
cat trail-data.json | kosli evaluate input \
52-
--policy policy.rego`
55+
--policy policy.rego
56+
57+
# evaluate with policy parameters (inline JSON):
58+
kosli evaluate input \
59+
--input-file trail-data.json \
60+
--policy policy.rego \
61+
--params '{"threshold": 3}'
62+
63+
# evaluate with policy parameters from a file:
64+
kosli evaluate input \
65+
--input-file trail-data.json \
66+
--policy policy.rego \
67+
--params @params.json`
5368

5469
func newEvaluateInputCmd(out io.Writer) *cobra.Command {
5570
o := new(evaluateInputOptions)
@@ -94,7 +109,12 @@ func (o *evaluateInputOptions) run(out io.Writer, in io.Reader) error {
94109
return err
95110
}
96111

97-
return evaluateAndPrintResult(out, o.policyFile, input, o.output, o.showInput)
112+
params, err := parseParams(o.params)
113+
if err != nil {
114+
return err
115+
}
116+
117+
return evaluateAndPrintResult(out, o.policyFile, input, o.output, o.showInput, params)
98118
}
99119

100120
func loadInputFromFile(filePath string) (result map[string]interface{}, err error) {

cmd/kosli/evaluateInput_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,31 @@ func (suite *EvaluateInputCommandTestSuite) TestEvaluateInputCmd() {
8282
{"input.trail.name", "test-trail"},
8383
},
8484
},
85+
{
86+
name: "inline --params overrides policy default threshold",
87+
cmd: `evaluate input --input-file testdata/evaluate/score-input.json --policy testdata/policies/check-params-threshold.rego --params '{"threshold":3}'`,
88+
goldenRegex: `RESULT:\s+ALLOWED`,
89+
},
90+
{
91+
name: "--params from file overrides policy default threshold",
92+
cmd: "evaluate input --input-file testdata/evaluate/score-input.json --policy testdata/policies/check-params-threshold.rego --params @testdata/evaluate/params-low-threshold.json",
93+
goldenRegex: `RESULT:\s+ALLOWED`,
94+
},
95+
{
96+
wantError: true,
97+
name: "--params with invalid JSON returns error",
98+
cmd: "evaluate input --input-file testdata/evaluate/score-input.json --policy testdata/policies/allow-all.rego --params not-json",
99+
goldenRegex: `failed to parse --params`,
100+
},
101+
{
102+
name: "show-input with params includes params in JSON output",
103+
cmd: `evaluate input --input-file testdata/evaluate/score-input.json --policy testdata/policies/check-params-threshold.rego --params '{"threshold":3}' --output json --show-input`,
104+
goldenJson: []jsonCheck{
105+
{"allow", true},
106+
{"input.score", float64(5)},
107+
{"params.threshold", float64(3)},
108+
},
109+
},
85110
}
86111
runTestCmd(suite.T(), tests)
87112
}

cmd/kosli/evaluateTrail.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,22 @@ kosli evaluate trail yourTrailName \
3939
--show-input \
4040
--output json \
4141
--api-token yourAPIToken \
42+
--org yourOrgName
43+
44+
# evaluate a trail with policy parameters (inline JSON):
45+
kosli evaluate trail yourTrailName \
46+
--policy yourPolicyFile.rego \
47+
--flow yourFlowName \
48+
--params '{"min_approvers": 2}' \
49+
--api-token yourAPIToken \
50+
--org yourOrgName
51+
52+
# evaluate a trail with policy parameters from a file:
53+
kosli evaluate trail yourTrailName \
54+
--policy yourPolicyFile.rego \
55+
--flow yourFlowName \
56+
--params @params.json \
57+
--api-token yourAPIToken \
4258
--org yourOrgName`
4359

4460
type evaluateTrailOptions struct {
@@ -81,9 +97,14 @@ func (o *evaluateTrailOptions) run(out io.Writer, args []string) error {
8197
return err
8298
}
8399

100+
params, err := parseParams(o.params)
101+
if err != nil {
102+
return err
103+
}
104+
84105
input := map[string]interface{}{
85106
"trail": trailData,
86107
}
87108

88-
return evaluateAndPrintResult(out, o.policyFile, input, o.output, o.showInput)
109+
return evaluateAndPrintResult(out, o.policyFile, input, o.output, o.showInput, params)
89110
}

cmd/kosli/evaluateTrails.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ kosli evaluate trails yourTrailName1 yourTrailName2 \
4040
--show-input \
4141
--output json \
4242
--api-token yourAPIToken \
43+
--org yourOrgName
44+
45+
# evaluate trails with policy parameters:
46+
kosli evaluate trails yourTrailName1 yourTrailName2 \
47+
--policy yourPolicyFile.rego \
48+
--flow yourFlowName \
49+
--params '{"min_approvers": 2}' \
50+
--api-token yourAPIToken \
4351
--org yourOrgName`
4452

4553
type evaluateTrailsOptions struct {
@@ -86,9 +94,14 @@ func (o *evaluateTrailsOptions) run(out io.Writer, args []string) error {
8694
trails = append(trails, trailData)
8795
}
8896

97+
params, err := parseParams(o.params)
98+
if err != nil {
99+
return err
100+
}
101+
89102
input := map[string]interface{}{
90103
"trails": trails,
91104
}
92105

93-
return evaluateAndPrintResult(out, o.policyFile, input, o.output, o.showInput)
106+
return evaluateAndPrintResult(out, o.policyFile, input, o.output, o.showInput, params)
94107
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"threshold": 3}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"score": 5}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package policy
2+
3+
import rego.v1
4+
5+
default allow := false
6+
7+
default threshold := 10
8+
9+
threshold := data.params.threshold if { data.params.threshold }
10+
11+
allow if { input.score >= threshold }
12+
13+
violations contains msg if {
14+
input.score < threshold
15+
msg := sprintf("score %d is below threshold %d", [input.score, threshold])
16+
}

0 commit comments

Comments
 (0)