diff --git a/app/cli/cmd/policy_develop_eval.go b/app/cli/cmd/policy_develop_eval.go index 0b0565939..cd1e56fee 100644 --- a/app/cli/cmd/policy_develop_eval.go +++ b/app/cli/cmd/policy_develop_eval.go @@ -27,12 +27,14 @@ import ( func newPolicyDevelopEvalCmd() *cobra.Command { var ( - materialPath string - kind string - annotations []string - policyPath string - inputs []string - allowedHostnames []string + materialPath string + kind string + annotations []string + policyPath string + inputs []string + allowedHostnames []string + projectName string + projectVersionName string ) cmd := &cobra.Command{ @@ -41,18 +43,20 @@ func newPolicyDevelopEvalCmd() *cobra.Command { Long: `Perform a full evaluation of the policy against the provided material type. The command checks if there is a path in the policy for the specified kind and evaluates the policy against the provided material or attestation.`, - Example: ` + Example: ` # Evaluate policy against a material file chainloop policy develop eval --policy policy.yaml --material sbom.json --kind SBOM_CYCLONEDX_JSON --annotation key1=value1,key2=value2 --input key3=value3`, RunE: func(_ *cobra.Command, _ []string) error { opts := &action.PolicyEvalOpts{ - MaterialPath: materialPath, - Kind: kind, - Annotations: parseKeyValue(annotations), - PolicyPath: policyPath, - Inputs: parseKeyValue(inputs), - AllowedHostnames: allowedHostnames, - Debug: flagDebug, + MaterialPath: materialPath, + Kind: kind, + Annotations: parseKeyValue(annotations), + PolicyPath: policyPath, + Inputs: parseKeyValue(inputs), + AllowedHostnames: allowedHostnames, + Debug: flagDebug, + ProjectName: projectName, + ProjectVersionName: projectVersionName, } policyEval, err := action.NewPolicyEval(opts, ActionOpts) @@ -76,6 +80,8 @@ evaluates the policy against the provided material or attestation.`, cmd.Flags().StringVarP(&policyPath, "policy", "p", "policy.yaml", "Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml, chainloop://my-stored-policy)") cmd.Flags().StringArrayVar(&inputs, "input", []string{}, "Key-value pairs of policy inputs (key=value)") cmd.Flags().StringSliceVar(&allowedHostnames, "allowed-hostnames", []string{}, "Additional hostnames allowed for http.send requests in policies") + cmd.Flags().StringVar(&projectName, "project", "", "Project name to use as engine context for chainloop.* built-ins") + cmd.Flags().StringVar(&projectVersionName, "project-version", "", "Project version to use as engine context for chainloop.* built-ins") return cmd } diff --git a/app/cli/documentation/cli-reference.mdx b/app/cli/documentation/cli-reference.mdx index c8d3914c6..538879bfe 100755 --- a/app/cli/documentation/cli-reference.mdx +++ b/app/cli/documentation/cli-reference.mdx @@ -2940,6 +2940,8 @@ Options --kind string Kind of the material: ["ARTIFACT" "ATTESTATION" "BLACKDUCK_SCA_JSON" "CHAINLOOP_AI_AGENT_CONFIG" "CHAINLOOP_AI_CODING_SESSION" "CHAINLOOP_PR_INFO" "CHAINLOOP_RUNNER_CONTEXT" "CONTAINER_IMAGE" "CSAF_INFORMATIONAL_ADVISORY" "CSAF_SECURITY_ADVISORY" "CSAF_SECURITY_INCIDENT_RESPONSE" "CSAF_VEX" "EVIDENCE" "GHAS_CODE_SCAN" "GHAS_DEPENDENCY_SCAN" "GHAS_SECRET_SCAN" "GITLAB_SECURITY_REPORT" "GITLEAKS_JSON" "HELM_CHART" "JACOCO_XML" "JUNIT_XML" "OPENVEX" "SARIF" "SBOM_CYCLONEDX_JSON" "SBOM_SPDX_JSON" "SLSA_PROVENANCE" "STRING" "TWISTCLI_SCAN_JSON" "ZAP_DAST_ZIP"] --material string Path to material or attestation file -p, --policy string Policy reference (./my-policy.yaml, https://my-domain.com/my-policy.yaml, chainloop://my-stored-policy) (default "policy.yaml") +--project string Project name to use as engine context for chainloop.* built-ins +--project-version string Project version to use as engine context for chainloop.* built-ins ``` Options inherited from parent commands diff --git a/app/cli/internal/policydevel/eval.go b/app/cli/internal/policydevel/eval.go index 7d01faa83..859c1393d 100644 --- a/app/cli/internal/policydevel/eval.go +++ b/app/cli/internal/policydevel/eval.go @@ -37,15 +37,17 @@ const ( ) type EvalOptions struct { - PolicyPath string - MaterialKind string - Annotations map[string]string - MaterialPath string - Inputs map[string]string - AllowedHostnames []string - Debug bool - AttestationClient controlplanev1.AttestationServiceClient - ControlPlaneConn *grpc.ClientConn + PolicyPath string + MaterialKind string + Annotations map[string]string + MaterialPath string + Inputs map[string]string + AllowedHostnames []string + Debug bool + AttestationClient controlplanev1.AttestationServiceClient + ControlPlaneConn *grpc.ClientConn + ProjectName string + ProjectVersionName string } type EvalResult struct { @@ -80,7 +82,7 @@ func Evaluate(opts *EvalOptions, logger zerolog.Logger) (*EvalSummary, error) { material.Annotations = opts.Annotations // 3. Verify material against policy - summary, err := verifyMaterial(policies, material, opts.MaterialPath, opts.Debug, opts.AllowedHostnames, opts.AttestationClient, opts.ControlPlaneConn, &logger) + summary, err := verifyMaterial(policies, material, opts.MaterialPath, opts.Debug, opts.AllowedHostnames, opts.AttestationClient, opts.ControlPlaneConn, opts.ProjectName, opts.ProjectVersionName, &logger) if err != nil { return nil, err } @@ -108,7 +110,7 @@ func createPolicies(policyPath string, inputs map[string]string) (*v1.Policies, }, nil } -func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materialPath string, debug bool, allowedHostnames []string, attestationClient controlplanev1.AttestationServiceClient, grpcConn *grpc.ClientConn, logger *zerolog.Logger) (*EvalSummary, error) { +func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materialPath string, debug bool, allowedHostnames []string, attestationClient controlplanev1.AttestationServiceClient, grpcConn *grpc.ClientConn, projectName, projectVersion string, logger *zerolog.Logger) (*EvalSummary, error) { var opts []policies.PolicyVerifierOption if len(allowedHostnames) > 0 { opts = append(opts, policies.WithAllowedHostnames(allowedHostnames...)) @@ -117,6 +119,9 @@ func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materi opts = append(opts, policies.WithIncludeRawData(debug)) opts = append(opts, policies.WithEnablePrint(enablePrint)) opts = append(opts, policies.WithGRPCConn(grpcConn)) + if projectName != "" || projectVersion != "" { + opts = append(opts, policies.WithProjectContext(projectName, projectVersion)) + } v := policies.NewPolicyVerifier(pol, attestationClient, logger, opts...) policyEvs, err := v.VerifyMaterial(context.Background(), material, materialPath) diff --git a/app/cli/pkg/action/policy_develop_eval.go b/app/cli/pkg/action/policy_develop_eval.go index 91d1f5fcd..16b8681db 100644 --- a/app/cli/pkg/action/policy_develop_eval.go +++ b/app/cli/pkg/action/policy_develop_eval.go @@ -22,13 +22,15 @@ import ( ) type PolicyEvalOpts struct { - MaterialPath string - Kind string - Annotations map[string]string - PolicyPath string - Inputs map[string]string - AllowedHostnames []string - Debug bool + MaterialPath string + Kind string + Annotations map[string]string + PolicyPath string + Inputs map[string]string + AllowedHostnames []string + Debug bool + ProjectName string + ProjectVersionName string } type PolicyEval struct { @@ -50,15 +52,17 @@ func (action *PolicyEval) Run() (*policydevel.EvalSummary, error) { } evalOpts := &policydevel.EvalOptions{ - PolicyPath: action.opts.PolicyPath, - MaterialKind: action.opts.Kind, - Annotations: action.opts.Annotations, - MaterialPath: action.opts.MaterialPath, - Inputs: action.opts.Inputs, - AllowedHostnames: action.opts.AllowedHostnames, - Debug: action.opts.Debug, - AttestationClient: attClient, - ControlPlaneConn: action.CPConnection, + PolicyPath: action.opts.PolicyPath, + MaterialKind: action.opts.Kind, + Annotations: action.opts.Annotations, + MaterialPath: action.opts.MaterialPath, + Inputs: action.opts.Inputs, + AllowedHostnames: action.opts.AllowedHostnames, + Debug: action.opts.Debug, + AttestationClient: attClient, + ControlPlaneConn: action.CPConnection, + ProjectName: action.opts.ProjectName, + ProjectVersionName: action.opts.ProjectVersionName, } // Evaluate policy diff --git a/pkg/attestation/crafter/crafter.go b/pkg/attestation/crafter/crafter.go index 652c51cbc..25bf1f460 100644 --- a/pkg/attestation/crafter/crafter.go +++ b/pkg/attestation/crafter/crafter.go @@ -722,6 +722,8 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M return i.MaterialName == m.Name }) + projectName, projectVersion := c.projectContext() + pgv := policies.NewPolicyGroupVerifier( c.CraftingState.GetPolicyGroups(), c.CraftingState.GetPolicies(), @@ -729,6 +731,7 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), + policies.WithProjectContext(projectName, projectVersion), ) policyGroupResults, err := pgv.VerifyMaterial(ctx, mt, value) if err != nil { @@ -746,6 +749,7 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), + policies.WithProjectContext(projectName, projectVersion), ) policyResults, err := pv.VerifyMaterial(ctx, mt, value) if err != nil { @@ -773,6 +777,21 @@ func (c *Crafter) addMaterial(ctx context.Context, m *schemaapi.CraftingSchema_M return mt, nil } +// projectContext returns the project name and version from the workflow +// metadata so policy verifiers can pass them to the engine. Either may be +// empty (e.g. dry-run before workflow metadata is populated); built-ins +// must degrade gracefully in that case. +func (c *Crafter) projectContext() (string, string) { + wf := c.CraftingState.GetAttestation().GetWorkflow() + version := wf.GetVersion().GetVersion() + if version == "" { + // Fall back to the deprecated flat field for state written by older clients. + //nolint:staticcheck // intentional fallback for backwards compatibility + version = wf.GetProjectVersion() + } + return wf.GetProject(), version +} + // policyEvalMatches returns true if two policy evaluations refer to the same policy // with the same arguments. It treats nil and empty maps as equivalent to handle // protojson round-trip serialization where empty maps are omitted. @@ -783,11 +802,14 @@ func policyEvalMatches(a, b *api.PolicyEvaluation) bool { // EvaluateAttestationPolicies evaluates the attestation-level policies and stores them in the attestation state. // The phase parameter controls which policies are evaluated based on their attestation_phases spec field. func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID string, statement *intoto.Statement, phase policies.EvalPhase) error { + projectName, projectVersion := c.projectContext() + // evaluate attestation-level policies pv := policies.NewPolicyVerifier(c.CraftingState.GetPolicies(), c.attClient, c.Logger, policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), policies.WithEvalPhase(phase), + policies.WithProjectContext(projectName, projectVersion), ) policyEvaluations, err := pv.VerifyStatement(ctx, statement) if err != nil { @@ -798,6 +820,7 @@ func (c *Crafter) EvaluateAttestationPolicies(ctx context.Context, attestationID policies.WithAllowedHostnames(c.CraftingState.Attestation.PoliciesAllowedHostnames...), policies.WithDefaultGate(c.CraftingState.Attestation.GetBlockOnPolicyViolation()), policies.WithEvalPhase(phase), + policies.WithProjectContext(projectName, projectVersion), ) policyGroupResults, err := pgv.VerifyStatement(ctx, statement) if err != nil { diff --git a/pkg/policies/engine/engine.go b/pkg/policies/engine/engine.go index 030f08544..0bda2936e 100644 --- a/pkg/policies/engine/engine.go +++ b/pkg/policies/engine/engine.go @@ -37,6 +37,14 @@ type CommonEngineOptions struct { IncludeRawData bool EnablePrint bool ControlPlaneConnection *grpc.ClientConn + // ProjectName / ProjectVersionName carry the project + version this engine + // instance is evaluating policies for. They are surfaced to chainloop.* built-ins + // via the per-evaluation context.Context (see builtins.WithProjectContext) so a + // built-in like chainloop.findings can scope its query without the rego author + // having to pass the values explicitly. Either may be empty (e.g. local dev + // eval without flags) — built-ins must degrade gracefully in that case. + ProjectName string + ProjectVersionName string } // Option is a unified functional option for configuring policy engines @@ -106,6 +114,17 @@ func WithGRPCConn(conn *grpc.ClientConn) Option { } } +// WithProjectContext sets the project name and version that this engine +// instance is evaluating policies for. The values are propagated to chainloop.* +// built-ins through the per-evaluation context so they can scope queries +// (e.g. chainloop.findings) without the rego author passing them explicitly. +func WithProjectContext(name, version string) Option { + return func(opts *Options) { + opts.ProjectName = name + opts.ProjectVersionName = version + } +} + // ApplyOptions applies options and returns the configured Options // This automatically appends BaseAllowedHostnames to any user-provided hostnames func ApplyOptions(opts ...Option) *Options { diff --git a/pkg/policies/engine/rego/builtins/context.go b/pkg/policies/engine/rego/builtins/context.go new file mode 100644 index 000000000..65d7f1d26 --- /dev/null +++ b/pkg/policies/engine/rego/builtins/context.go @@ -0,0 +1,44 @@ +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package builtins + +import "context" + +// ProjectContext carries the project + version a policy is being evaluated against. +// It is attached to the per-evaluation context.Context by the rego engine so that +// chainloop.* built-ins can scope their requests (e.g. chainloop.findings) without +// requiring the rego author to pass project_name / project_version_name explicitly. +// +// Values may be empty when the engine has no project context (e.g. a local +// `chainloop policy develop eval` without --project flags). Built-ins must +// degrade gracefully in that case rather than erroring. +type ProjectContext struct { + Name string + Version string +} + +type projectContextKey struct{} + +// WithProjectContext returns a derived context carrying the given project context. +func WithProjectContext(ctx context.Context, pc ProjectContext) context.Context { + return context.WithValue(ctx, projectContextKey{}, pc) +} + +// ProjectContextFromContext returns the project context attached to ctx, or the +// zero value if none was set. The bool reports whether a value was present. +func ProjectContextFromContext(ctx context.Context) (ProjectContext, bool) { + pc, ok := ctx.Value(projectContextKey{}).(ProjectContext) + return pc, ok +} diff --git a/pkg/policies/engine/rego/builtins/context_test.go b/pkg/policies/engine/rego/builtins/context_test.go new file mode 100644 index 000000000..dad6b351f --- /dev/null +++ b/pkg/policies/engine/rego/builtins/context_test.go @@ -0,0 +1,64 @@ +// Copyright 2026 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package builtins + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestProjectContext(t *testing.T) { + tests := []struct { + name string + setup func() context.Context + wantName string + wantVersion string + wantOK bool + }{ + { + name: "no project context attached", + setup: context.Background, + wantOK: false, + }, + { + name: "context with project + version", + setup: func() context.Context { + return WithProjectContext(context.Background(), ProjectContext{Name: "my-app", Version: "v1.2.3"}) + }, + wantName: "my-app", + wantVersion: "v1.2.3", + wantOK: true, + }, + { + name: "context with only project name", + setup: func() context.Context { + return WithProjectContext(context.Background(), ProjectContext{Name: "my-app"}) + }, + wantName: "my-app", + wantOK: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pc, ok := ProjectContextFromContext(tt.setup()) + assert.Equal(t, tt.wantOK, ok) + assert.Equal(t, tt.wantName, pc.Name) + assert.Equal(t, tt.wantVersion, pc.Version) + }) + } +} diff --git a/pkg/policies/engine/rego/rego.go b/pkg/policies/engine/rego/rego.go index 12d4481bf..e29ca82bb 100644 --- a/pkg/policies/engine/rego/rego.go +++ b/pkg/policies/engine/rego/rego.go @@ -88,7 +88,25 @@ func (p *regoOutputHook) Print(_ print.Context, msg string) error { //nolint:for // Force interface var _ engine.PolicyEngine = (*Engine)(nil) +// withProjectContext attaches the engine's per-evaluation project name / version +// to ctx so chainloop.* built-ins can read them from bctx.Context. Skipped when +// the engine was created without project context (e.g. local dev eval). +func (r *Engine) withProjectContext(ctx context.Context) context.Context { + if r.CommonEngineOptions == nil { + return ctx + } + if r.ProjectName == "" && r.ProjectVersionName == "" { + return ctx + } + return builtins.WithProjectContext(ctx, builtins.ProjectContext{ + Name: r.ProjectName, + Version: r.ProjectVersionName, + }) +} + func (r *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte, args map[string]any) (*engine.EvaluationResult, error) { + ctx = r.withProjectContext(ctx) + policyString := string(policy.Source) parsedModule, err := ast.ParseModule(policy.Name, policyString) if err != nil { @@ -323,6 +341,8 @@ func getRuleName(packagePath ast.Ref, rule string) string { // MatchesParameters evaluates the matches_parameters rule in a rego policy. // The function creates an input object with policy parameters and expected parameters. func (r *Engine) MatchesParameters(ctx context.Context, policy *engine.Policy, evaluationParams, expectedParams map[string]string) (bool, error) { + ctx = r.withProjectContext(ctx) + policyString := string(policy.Source) parsedModule, err := ast.ParseModule(policy.Name, policyString) if err != nil { @@ -358,6 +378,8 @@ func (r *Engine) MatchesParameters(ctx context.Context, policy *engine.Policy, e // MatchesEvaluation evaluates the matches_evaluation rule in a rego policy. // Creates an input object with expected parameters and policy violations. func (r *Engine) MatchesEvaluation(ctx context.Context, policy *engine.Policy, violations []string, expectedParams map[string]string) (bool, error) { + ctx = r.withProjectContext(ctx) + policyString := string(policy.Source) parsedModule, err := ast.ParseModule(policy.Name, policyString) if err != nil { diff --git a/pkg/policies/engine/rego/rego_test.go b/pkg/policies/engine/rego/rego_test.go index f14f4daaf..a977e465a 100644 --- a/pkg/policies/engine/rego/rego_test.go +++ b/pkg/policies/engine/rego/rego_test.go @@ -534,6 +534,61 @@ violations contains msg if { }) } +func TestRego_ProjectContextPlumbing(t *testing.T) { + // Custom built-in that captures the project context attached to bctx.Context + // and exposes the result so the policy can pivot on it. + var capturedCtx builtins.ProjectContext + var capturedOK bool + + require.NoError(t, builtins.Register(&ast.Builtin{ + Name: "test.capture_project_ctx", + Decl: types.NewFunction(types.Args(types.S), types.S), + }, func(bctx topdown.BuiltinContext, _ []*ast.Term, iter func(*ast.Term) error) error { + capturedCtx, capturedOK = builtins.ProjectContextFromContext(bctx.Context) + return iter(ast.StringTerm("ok")) + })) + + regoContent := []byte(`package test +import rego.v1 + +result := { + "violations": violations, + "skipped": false +} + +violations contains msg if { + val := test.capture_project_ctx("noop") + val != "ok" + msg := "Capture failed" +}`) + + policy := &engine.Policy{Name: "ctx-test", Source: regoContent} + + t.Run("engine without project context leaves ctx empty", func(t *testing.T) { + capturedCtx, capturedOK = builtins.ProjectContext{}, false + + r := NewEngine() + _, err := r.Verify(context.TODO(), policy, []byte(`{"kind": "test"}`), nil) + require.NoError(t, err) + + assert.False(t, capturedOK, "no project context should be attached") + assert.Empty(t, capturedCtx.Name) + assert.Empty(t, capturedCtx.Version) + }) + + t.Run("engine with project context propagates to builtin", func(t *testing.T) { + capturedCtx, capturedOK = builtins.ProjectContext{}, false + + r := NewEngine(engine.WithProjectContext("my-app", "v1.2.3")) + _, err := r.Verify(context.TODO(), policy, []byte(`{"kind": "test"}`), nil) + require.NoError(t, err) + + require.True(t, capturedOK, "project context should be attached") + assert.Equal(t, "my-app", capturedCtx.Name) + assert.Equal(t, "v1.2.3", capturedCtx.Version) + }) +} + func TestRego_StructuredViolations(t *testing.T) { tests := []struct { name string diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index b1dcdb9e2..ffa33f86e 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -85,32 +85,36 @@ const ( var defaultMaxConcurrency = max(runtime.NumCPU(), 5) type PolicyVerifier struct { - policies *v1.Policies - logger *zerolog.Logger - client v13.AttestationServiceClient - grpcConn *grpc.ClientConn - allowedHostnames []string - defaultGate bool - includeRawData bool - enablePrint bool - evalPhase EvalPhase - maxConcurrency int - policyCache cache.Cache[*policyWithReference] - groupCache cache.Cache[*groupWithReference] + policies *v1.Policies + logger *zerolog.Logger + client v13.AttestationServiceClient + grpcConn *grpc.ClientConn + allowedHostnames []string + defaultGate bool + includeRawData bool + enablePrint bool + evalPhase EvalPhase + maxConcurrency int + policyCache cache.Cache[*policyWithReference] + groupCache cache.Cache[*groupWithReference] + projectName string + projectVersionName string } var _ Verifier = (*PolicyVerifier)(nil) type PolicyVerifierOptions struct { - AllowedHostnames []string - DefaultGate bool - IncludeRawData bool - EnablePrint bool - GRPCConn *grpc.ClientConn - EvalPhase EvalPhase - MaxConcurrency int - PolicyCache cache.Cache[*policyWithReference] - GroupCache cache.Cache[*groupWithReference] + AllowedHostnames []string + DefaultGate bool + IncludeRawData bool + EnablePrint bool + GRPCConn *grpc.ClientConn + EvalPhase EvalPhase + MaxConcurrency int + PolicyCache cache.Cache[*policyWithReference] + GroupCache cache.Cache[*groupWithReference] + ProjectName string + ProjectVersionName string } type PolicyVerifierOption func(*PolicyVerifierOptions) @@ -169,6 +173,17 @@ func WithGroupCache(c cache.Cache[*groupWithReference]) PolicyVerifierOption { } } +// WithProjectContext sets the project name and version that this verifier is +// evaluating policies for. The values are forwarded to the underlying policy +// engine so chainloop.* built-ins can scope their queries automatically. +// Either may be empty, in which case built-ins must degrade gracefully. +func WithProjectContext(name, version string) PolicyVerifierOption { + return func(o *PolicyVerifierOptions) { + o.ProjectName = name + o.ProjectVersionName = version + } +} + const defaultPolicyCacheTTL = 5 * time.Minute func NewPolicyVerifier(policies *v1.Policies, client v13.AttestationServiceClient, logger *zerolog.Logger, opts ...PolicyVerifierOption) *PolicyVerifier { @@ -191,18 +206,20 @@ func NewPolicyVerifier(policies *v1.Policies, client v13.AttestationServiceClien } return &PolicyVerifier{ - policies: policies, - client: client, - logger: logger, - grpcConn: options.GRPCConn, - allowedHostnames: options.AllowedHostnames, - defaultGate: options.DefaultGate, - includeRawData: options.IncludeRawData, - enablePrint: options.EnablePrint, - evalPhase: options.EvalPhase, - maxConcurrency: maxConcurrency, - policyCache: options.PolicyCache, - groupCache: options.GroupCache, + policies: policies, + client: client, + logger: logger, + grpcConn: options.GRPCConn, + allowedHostnames: options.AllowedHostnames, + defaultGate: options.DefaultGate, + includeRawData: options.IncludeRawData, + enablePrint: options.EnablePrint, + evalPhase: options.EvalPhase, + maxConcurrency: maxConcurrency, + policyCache: options.PolicyCache, + groupCache: options.GroupCache, + projectName: options.ProjectName, + projectVersionName: options.ProjectVersionName, } } @@ -576,6 +593,10 @@ func (pv *PolicyVerifier) executeScript(ctx context.Context, script *engine.Poli opts = append(opts, engine.WithGRPCConn(pv.grpcConn)) } + if pv.projectName != "" || pv.projectVersionName != "" { + opts = append(opts, engine.WithProjectContext(pv.projectName, pv.projectVersionName)) + } + switch policyType { case engine.PolicyTypeRego: policyEngine = rego.NewEngine(opts...)