Skip to content

Commit 2bb154d

Browse files
authored
feat(policies): Chainloop discover custom builtin (#2558)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent 9c9c55a commit 2bb154d

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

app/cli/cmd/root.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/chainloop-dev/chainloop/app/cli/pkg/plugins"
3535
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
3636
"github.com/chainloop-dev/chainloop/pkg/grpcconn"
37+
"github.com/chainloop-dev/chainloop/pkg/policies/engine/rego/builtins"
3738
"github.com/rs/zerolog"
3839
"github.com/spf13/cobra"
3940
"github.com/spf13/viper"
@@ -156,6 +157,11 @@ func NewRootCmd(l zerolog.Logger) *cobra.Command {
156157

157158
ActionOpts = newActionOpts(logger, conn, authToken)
158159

160+
// Add custom builtins to rego engine
161+
if err = builtins.RegisterDiscoverBuiltin(conn); err != nil {
162+
return fmt.Errorf("failed to register discover builtin: %w", err)
163+
}
164+
159165
if !isTelemetryDisabled() {
160166
logger.Debug().Msg("Telemetry enabled, to disable it use DO_NOT_TRACK=1")
161167

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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 builtins
17+
18+
import (
19+
"errors"
20+
"fmt"
21+
22+
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
23+
"github.com/open-policy-agent/opa/v1/ast"
24+
"github.com/open-policy-agent/opa/v1/topdown"
25+
"github.com/open-policy-agent/opa/v1/types"
26+
"google.golang.org/grpc"
27+
)
28+
29+
const discoverBuiltinName = "chainloop.discover"
30+
31+
// RegisterDiscoverBuiltin is used to register chainloop's Discover endpoint as a builtin Rego function with signature:
32+
//
33+
// chainloop.discover(digest, kind)
34+
//
35+
// For instance, to get the references for an CONTAINER_IMAGE material, and fail if any of them is an attestation with policy violations:
36+
// ```
37+
//
38+
// violations contains msg if {
39+
// digest := sprintf("sha256:%s",[input.chainloop_metadata.digest.sha256])
40+
// discovered := chainloop.discover(digest, "")
41+
//
42+
// some ref in discovered.references
43+
// ref.kind == "ATTESTATION"
44+
// ref.metadata.hasPolicyViolations == "true"
45+
//
46+
// msg:= sprintf("attestation with digest %s contains policy violations [name: %s, project: %s, org: %s]", [ref.digest, ref.metadata.name, ref.metadata.project, ref.metadata.organization])
47+
// }
48+
//
49+
// ```
50+
func RegisterDiscoverBuiltin(conn *grpc.ClientConn) error {
51+
return Register(&ast.Builtin{
52+
Name: discoverBuiltinName,
53+
Description: "Discovers artifact graph data by calling the Referrer chainloop service",
54+
Decl: types.NewFunction(
55+
types.Args(
56+
types.Named("digest", types.S).Description("digest of the artifact to discover"),
57+
types.Named("kind", types.S).Description("optional filter by kind to disambiguate"),
58+
),
59+
types.Named("response", types.A).Description("response object as in the `chainloop discover` CLI output"),
60+
),
61+
Nondeterministic: true,
62+
}, getDiscoverImpl(conn))
63+
}
64+
65+
func getDiscoverImpl(conn *grpc.ClientConn) topdown.BuiltinFunc {
66+
return func(bctx topdown.BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
67+
if len(operands) < 1 {
68+
return errors.New("need at least one operand")
69+
}
70+
71+
var digest, kind ast.String
72+
var ok bool
73+
74+
// Extract digest
75+
digest, ok = operands[0].Value.(ast.String)
76+
if !ok {
77+
return errors.New("digest must be a string")
78+
}
79+
80+
if len(operands) > 1 {
81+
// Extract kind
82+
kind, ok = operands[1].Value.(ast.String)
83+
if !ok {
84+
return errors.New("kind must be a string")
85+
}
86+
}
87+
88+
// Call the service
89+
client := v1.NewReferrerServiceClient(conn)
90+
resp, err := client.DiscoverPrivate(bctx.Context, &v1.ReferrerServiceDiscoverPrivateRequest{
91+
Digest: string(digest), Kind: string(kind),
92+
})
93+
94+
if err != nil {
95+
return fmt.Errorf("failed to call discover endpoint: %w", err)
96+
}
97+
98+
// call the iterator with the output value
99+
return iter(ast.NewTerm(ast.MustInterfaceToValue(resp.Result)))
100+
}
101+
}

0 commit comments

Comments
 (0)