Skip to content

Commit 6006bfb

Browse files
committed
add policy eval command
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
1 parent 24cc520 commit 6006bfb

6 files changed

Lines changed: 352 additions & 98 deletions

File tree

app/cli/cmd/policy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,6 @@ func newPolicyCmd() *cobra.Command {
2525
Short: "Craft chainloop policies",
2626
}
2727

28-
cmd.AddCommand(newPolicyDevelopCmd())
28+
cmd.AddCommand(newPolicyEvalCmd(), newPolicyDevelopCmd())
2929
return cmd
3030
}

app/cli/cmd/policy_eval.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package cmd
17+
18+
import (
19+
"fmt"
20+
21+
"github.com/chainloop-dev/chainloop/app/cli/cmd/output"
22+
"github.com/chainloop-dev/chainloop/app/cli/pkg/action"
23+
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
24+
"github.com/spf13/cobra"
25+
)
26+
27+
func newPolicyEvalCmd() *cobra.Command {
28+
var (
29+
materialPath string
30+
kind string
31+
annotations []string
32+
policyPath string
33+
inputs []string
34+
)
35+
36+
cmd := &cobra.Command{
37+
Use: "eval",
38+
Short: "Evaluate a policy",
39+
Long: `Evaluate a policy.
40+
41+
This command uses organization context to evaluate policies.
42+
43+
For offline development and testing with debug capabilities, use 'chainloop policy develop eval' instead.`,
44+
Example: `
45+
chainloop policy eval --policy policy.yaml --input digest=sha256:80058e45a56daa50ae2a130bd1bd13b1fb9aff13a55b2d98615fff6eb3b0fffb`,
46+
Annotations: map[string]string{
47+
useAPIToken: trueString,
48+
},
49+
RunE: func(cmd *cobra.Command, _ []string) error {
50+
opts := &action.PolicyEvaluateOpts{
51+
MaterialPath: materialPath,
52+
Kind: kind,
53+
Annotations: parseKeyValue(annotations),
54+
PolicyPath: policyPath,
55+
Inputs: parseKeyValue(inputs),
56+
}
57+
58+
policyEval, err := action.NewPolicyEvaluate(opts, ActionOpts)
59+
if err != nil {
60+
return err
61+
}
62+
63+
result, err := policyEval.Run(cmd.Context())
64+
if err != nil {
65+
return err
66+
}
67+
68+
return output.EncodeJSON(result)
69+
},
70+
}
71+
72+
cmd.Flags().StringVar(&materialPath, "material", "", "Path to material or attestation file")
73+
cmd.Flags().StringVar(&kind, "kind", "", fmt.Sprintf("Kind of the material: %q", schemaapi.ListAvailableMaterialKind()))
74+
cmd.Flags().StringSliceVar(&annotations, "annotation", []string{}, "Key-value pairs of material annotations (key=value)")
75+
cmd.Flags().StringVarP(&policyPath, "policy", "p", "", "Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml)")
76+
cobra.CheckErr(cmd.MarkFlagRequired("policy"))
77+
cmd.Flags().StringArrayVar(&inputs, "input", []string{}, "Key-value pairs of policy inputs (key=value)")
78+
79+
return cmd
80+
}

app/cli/documentation/cli-reference.mdx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3017,6 +3017,61 @@ Options inherited from parent commands
30173017
-y, --yes Skip confirmation
30183018
```
30193019

3020+
### chainloop policy eval
3021+
3022+
Evaluate policy using control plane
3023+
3024+
Synopsis
3025+
3026+
Evaluate a policy using the control plane.
3027+
3028+
This command connects to your Chainloop control plane to evaluate policies
3029+
with your organization's security settings (allowed hostnames, etc.).
3030+
3031+
For offline development and testing with debug capabilities, use 'chainloop policy develop eval' instead.
3032+
3033+
```
3034+
chainloop policy eval [flags]
3035+
```
3036+
3037+
Examples
3038+
3039+
```
3040+
3041+
Evaluate a generic policy with inputs
3042+
chainloop policy eval --policy ./my-policy.yaml --input key1=value1,key2=value2
3043+
3044+
Evaluate a remote policy
3045+
chainloop policy eval --policy https://my-domain.com/policy.yaml --input env=prod
3046+
```
3047+
3048+
Options
3049+
3050+
```
3051+
--annotation strings Key-value pairs of material annotations (not yet supported, use 'policy develop eval' for material evaluation)
3052+
-h, --help help for eval
3053+
--input stringArray Key-value pairs of policy inputs (key=value)
3054+
--kind string Kind of the material (not yet supported, use 'policy develop eval' for material evaluation)
3055+
--material string Path to material or attestation file (not yet supported, use 'policy develop eval' for material evaluation)
3056+
-p, --policy string Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml)
3057+
```
3058+
3059+
Options inherited from parent commands
3060+
3061+
```
3062+
--artifact-cas string URL for the Artifacts Content Addressable Storage API ($CHAINLOOP_ARTIFACT_CAS_API) (default "api.cas.chainloop.dev:443")
3063+
--artifact-cas-ca string CUSTOM CA file for the Artifacts CAS API (optional) ($CHAINLOOP_ARTIFACT_CAS_API_CA)
3064+
-c, --config string Path to an existing config file (default is $HOME/.config/chainloop/config.toml)
3065+
--control-plane string URL for the Control Plane API ($CHAINLOOP_CONTROL_PLANE_API) (default "api.cp.chainloop.dev:443")
3066+
--control-plane-ca string CUSTOM CA file for the Control Plane API (optional) ($CHAINLOOP_CONTROL_PLANE_API_CA)
3067+
--debug Enable debug/verbose logging mode
3068+
-i, --insecure Skip TLS transport during connection to the control plane ($CHAINLOOP_API_INSECURE)
3069+
-n, --org string organization name
3070+
-o, --output string Output format, valid options are json and table (default "table")
3071+
-t, --token string API token. NOTE: Alternatively use the env variable CHAINLOOP_TOKEN
3072+
-y, --yes Skip confirmation
3073+
```
3074+
30203075
### chainloop policy help
30213076

30223077
Help about any command

app/cli/pkg/action/policy_eval.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package action
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
23+
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
24+
attestationapi "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
25+
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials"
26+
"github.com/chainloop-dev/chainloop/pkg/casclient"
27+
"github.com/chainloop-dev/chainloop/pkg/policies"
28+
)
29+
30+
type PolicyEvaluateOpts struct {
31+
MaterialPath string
32+
Kind string
33+
Annotations map[string]string
34+
PolicyPath string
35+
Inputs map[string]string
36+
}
37+
38+
type PolicyEvaluate struct {
39+
*ActionsOpts
40+
opts *PolicyEvaluateOpts
41+
}
42+
43+
func NewPolicyEvaluate(opts *PolicyEvaluateOpts, actionOpts *ActionsOpts) (*PolicyEvaluate, error) {
44+
if actionOpts.CPConnection == nil {
45+
return nil, fmt.Errorf("control plane connection is required")
46+
}
47+
48+
return &PolicyEvaluate{
49+
ActionsOpts: actionOpts,
50+
opts: opts,
51+
}, nil
52+
}
53+
54+
func (action *PolicyEvaluate) Run(ctx context.Context) (*attestationapi.PolicyEvaluation, error) {
55+
// 1. Get organization settings
56+
contextClient := pb.NewContextServiceClient(action.CPConnection)
57+
contextResp, err := contextClient.Current(ctx, &pb.ContextServiceCurrentRequest{})
58+
if err != nil {
59+
return nil, fmt.Errorf("fetching organization settings: %w", err)
60+
}
61+
62+
if contextResp.Result == nil || contextResp.Result.CurrentMembership == nil || contextResp.Result.CurrentMembership.Org == nil {
63+
return nil, fmt.Errorf("no organization context found")
64+
}
65+
66+
org := contextResp.Result.CurrentMembership.Org
67+
allowedHostnames := org.PolicyAllowedHostnames
68+
69+
// 2. Create policy attachment
70+
ref := action.opts.PolicyPath
71+
scheme, _ := policies.RefParts(action.opts.PolicyPath)
72+
if scheme == "" {
73+
// If no scheme, assume it's a file path and add file:// prefix
74+
ref = fmt.Sprintf("file://%s", action.opts.PolicyPath)
75+
}
76+
77+
policyAttachment := &schemaapi.PolicyAttachment{
78+
Policy: &schemaapi.PolicyAttachment_Ref{Ref: ref},
79+
With: action.opts.Inputs,
80+
}
81+
82+
// 3. Create policies structure based on whether we have a material
83+
var pol *schemaapi.Policies
84+
if action.opts.MaterialPath != "" {
85+
// Material-based evaluation
86+
pol = &schemaapi.Policies{
87+
Materials: []*schemaapi.PolicyAttachment{policyAttachment},
88+
}
89+
} else {
90+
// Generic evaluation
91+
pol = &schemaapi.Policies{}
92+
}
93+
94+
// 4. Create policy verifier with organization's allowed hostnames
95+
verifierOpts := []policies.PolicyVerifierOption{
96+
policies.WithIncludeRawData(false),
97+
policies.WithEnablePrint(false),
98+
policies.WithGRPCConn(action.CPConnection),
99+
}
100+
if len(allowedHostnames) > 0 {
101+
verifierOpts = append(verifierOpts, policies.WithAllowedHostnames(allowedHostnames...))
102+
}
103+
104+
attClient := pb.NewAttestationServiceClient(action.CPConnection)
105+
verifier := policies.NewPolicyVerifier(pol, attClient, &action.Logger, verifierOpts...)
106+
107+
// 5. Evaluate: either material-based or generic
108+
if action.opts.MaterialPath != "" {
109+
// Material-based evaluation
110+
material, err := action.craftMaterial(ctx)
111+
if err != nil {
112+
return nil, fmt.Errorf("crafting material: %w", err)
113+
}
114+
material.Annotations = action.opts.Annotations
115+
116+
policyEvs, err := verifier.VerifyMaterial(ctx, material, action.opts.MaterialPath)
117+
if err != nil {
118+
return nil, fmt.Errorf("evaluating policy against material: %w", err)
119+
}
120+
121+
if len(policyEvs) == 0 || policyEvs[0] == nil {
122+
return nil, fmt.Errorf("no execution branch matched, or all of them were ignored, for kind %s", material.MaterialType.String())
123+
}
124+
125+
return policyEvs[0], nil
126+
}
127+
128+
// Generic evaluation
129+
policyEv, err := verifier.EvaluateGeneric(ctx, policyAttachment)
130+
if err != nil {
131+
return nil, fmt.Errorf("evaluating policy: %w", err)
132+
}
133+
134+
if policyEv == nil {
135+
return nil, fmt.Errorf("no execution branch matched, or all of them were ignored")
136+
}
137+
138+
return policyEv, nil
139+
}
140+
141+
func (action *PolicyEvaluate) craftMaterial(ctx context.Context) (*attestationapi.Attestation_Material, error) {
142+
backend := &casclient.CASBackend{
143+
Name: "backend",
144+
MaxSize: 0,
145+
Uploader: nil, // Skip uploads
146+
}
147+
148+
// Explicit kind
149+
if action.opts.Kind != "" {
150+
kind, ok := schemaapi.CraftingSchema_Material_MaterialType_value[action.opts.Kind]
151+
if !ok {
152+
return nil, fmt.Errorf("invalid material kind: %s", action.opts.Kind)
153+
}
154+
return action.craft(ctx, schemaapi.CraftingSchema_Material_MaterialType(kind), "material", backend)
155+
}
156+
157+
// Auto-detect kind
158+
for _, kind := range schemaapi.CraftingMaterialInValidationOrder {
159+
m, err := action.craft(ctx, kind, "auto-detected-material", backend)
160+
if err == nil {
161+
return m, nil
162+
}
163+
}
164+
165+
return nil, fmt.Errorf("could not auto-detect material kind for: %s", action.opts.MaterialPath)
166+
}
167+
168+
func (action *PolicyEvaluate) craft(ctx context.Context, kind schemaapi.CraftingSchema_Material_MaterialType, name string, backend *casclient.CASBackend) (*attestationapi.Attestation_Material, error) {
169+
materialSchema := &schemaapi.CraftingSchema_Material{
170+
Type: kind,
171+
Name: name,
172+
}
173+
174+
m, err := materials.Craft(ctx, materialSchema, action.opts.MaterialPath, backend, nil, &action.Logger)
175+
if err != nil {
176+
return nil, fmt.Errorf("failed to craft material (kind=%s): %w", kind.String(), err)
177+
}
178+
return m, nil
179+
}

0 commit comments

Comments
 (0)