Skip to content

Commit 61ff6bb

Browse files
committed
tackle feedback and feature parity
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
1 parent e138c59 commit 61ff6bb

11 files changed

Lines changed: 375 additions & 151 deletions

File tree

app/cli/cmd/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +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"
37+
"github.com/chainloop-dev/chainloop/pkg/policies/engine/builtins"
3838
"github.com/rs/zerolog"
3939
"github.com/spf13/cobra"
4040
"github.com/spf13/viper"

app/cli/internal/policydevel/lint.go

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -161,29 +161,19 @@ func (p *PolicyToLint) loadReferencedPolicyFiles(baseDir string) error {
161161
func (p *PolicyToLint) processFile(filePath string) error {
162162
ext := strings.ToLower(filepath.Ext(filePath))
163163

164-
// WASM files: validate magic bytes
165-
if ext == ".wasm" {
166-
content, err := os.ReadFile(filePath)
167-
if err != nil {
168-
return fmt.Errorf("failed to read WASM file %s: %w", filePath, err)
169-
}
170-
171-
// Verify magic bytes
172-
if engine.DetectPolicyType(content) != engine.PolicyTypeWASM {
173-
return fmt.Errorf("file has .wasm extension but is not a valid WASM file")
174-
}
175-
176-
p.WASMFiles = append(p.WASMFiles, &File{Path: filePath, Content: content})
177-
return nil
178-
}
179-
180-
// Other files: read full content
164+
// Read file content once
181165
content, err := os.ReadFile(filePath)
182166
if err != nil {
183167
return err
184168
}
185169

186170
switch ext {
171+
case ".wasm":
172+
// Verify magic bytes
173+
if engine.DetectPolicyType(content) != engine.PolicyTypeWASM {
174+
return fmt.Errorf("file has .wasm extension but is not a valid WASM file")
175+
}
176+
p.WASMFiles = append(p.WASMFiles, &File{Path: filePath, Content: content})
187177
case ".yaml", ".yml":
188178
p.YAMLFiles = append(p.YAMLFiles, &File{
189179
Path: filePath,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
"context"
20+
21+
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
22+
"google.golang.org/grpc"
23+
)
24+
25+
// DiscoverService wraps the gRPC discover functionality to be shared across engines
26+
type DiscoverService struct {
27+
conn *grpc.ClientConn
28+
}
29+
30+
// NewDiscoverService creates a new discover service
31+
func NewDiscoverService(conn *grpc.ClientConn) *DiscoverService {
32+
return &DiscoverService{conn: conn}
33+
}
34+
35+
// Discover calls the DiscoverPrivate gRPC endpoint to get artifact graph data
36+
func (s *DiscoverService) Discover(ctx context.Context, digest, kind string) (*v1.ReferrerServiceDiscoverPrivateResponse, error) {
37+
if s.conn == nil {
38+
return nil, nil
39+
}
40+
41+
client := v1.NewReferrerServiceClient(s.conn)
42+
return client.DiscoverPrivate(ctx, &v1.ReferrerServiceDiscoverPrivateRequest{
43+
Digest: digest,
44+
Kind: kind,
45+
})
46+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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+
"github.com/open-policy-agent/opa/v1/ast"
23+
"github.com/open-policy-agent/opa/v1/topdown"
24+
"github.com/open-policy-agent/opa/v1/types"
25+
"google.golang.org/grpc"
26+
)
27+
28+
const discoverBuiltinName = "chainloop.discover"
29+
30+
// RegisterDiscoverBuiltin registers chainloop's Discover endpoint as a builtin Rego function with signature:
31+
//
32+
// chainloop.discover(digest, kind)
33+
//
34+
// For instance, to get the references for an CONTAINER_IMAGE material, and fail if any of them is an attestation with policy violations:
35+
// ```
36+
//
37+
// violations contains msg if {
38+
// digest := sprintf("sha256:%s",[input.chainloop_metadata.digest.sha256])
39+
// discovered := chainloop.discover(digest, "")
40+
//
41+
// some ref in discovered.references
42+
// ref.kind == "ATTESTATION"
43+
// ref.metadata.hasPolicyViolations == "true"
44+
//
45+
// 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])
46+
// }
47+
//
48+
// ```
49+
func RegisterDiscoverBuiltin(conn *grpc.ClientConn) error {
50+
return register(&ast.Builtin{
51+
Name: discoverBuiltinName,
52+
Description: "Discovers artifact graph data by calling the Referrer chainloop service",
53+
Decl: types.NewFunction(
54+
types.Args(
55+
types.Named("digest", types.S).Description("digest of the artifact to discover"),
56+
types.Named("kind", types.S).Description("optional filter by kind to disambiguate"),
57+
),
58+
types.Named("response", types.A).Description("response object as in the `chainloop discover` CLI output"),
59+
),
60+
Nondeterministic: true,
61+
}, getDiscoverRegoImpl(conn))
62+
}
63+
64+
func getDiscoverRegoImpl(conn *grpc.ClientConn) topdown.BuiltinFunc {
65+
discoverSvc := NewDiscoverService(conn)
66+
67+
return func(bctx topdown.BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
68+
if len(operands) < 1 {
69+
return errors.New("need at least one operand")
70+
}
71+
72+
var digest, kind ast.String
73+
var ok bool
74+
75+
// Extract digest
76+
digest, ok = operands[0].Value.(ast.String)
77+
if !ok {
78+
return errors.New("digest must be a string")
79+
}
80+
81+
if len(operands) > 1 {
82+
// Extract kind
83+
kind, ok = operands[1].Value.(ast.String)
84+
if !ok {
85+
return errors.New("kind must be a string")
86+
}
87+
}
88+
89+
// Call the shared service
90+
resp, err := discoverSvc.Discover(bctx.Context, string(digest), string(kind))
91+
if err != nil {
92+
return fmt.Errorf("failed to call discover endpoint: %w", err)
93+
}
94+
95+
// call the iterator with the output value
96+
return iter(ast.NewTerm(ast.MustInterfaceToValue(resp.Result)))
97+
}
98+
}
99+
100+
// register is a wrapper around topdown.RegisterBuiltinFunc to handle registration
101+
func register(builtin *ast.Builtin, impl topdown.BuiltinFunc) error {
102+
topdown.RegisterBuiltinFunc(builtin.Name, impl)
103+
ast.RegisterBuiltin(builtin)
104+
return nil
105+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
"context"
20+
"encoding/json"
21+
22+
extism "github.com/extism/go-sdk"
23+
"google.golang.org/grpc"
24+
)
25+
26+
// CreateDiscoverHostFunction creates an Extism host function for the discover builtin
27+
// This allows WASM policies to call chainloop_discover(digest, kind) and get artifact graph data
28+
func CreateDiscoverHostFunction(conn *grpc.ClientConn) extism.HostFunction {
29+
discoverSvc := NewDiscoverService(conn)
30+
31+
return extism.NewHostFunctionWithStack(
32+
"chainloop_discover",
33+
func(ctx context.Context, plugin *extism.CurrentPlugin, stack []uint64) {
34+
// Read digest from WASM memory
35+
digestOffset := stack[0]
36+
digest, err := plugin.ReadString(digestOffset)
37+
if err != nil {
38+
// Return 0 to signal error
39+
stack[0] = 0
40+
return
41+
}
42+
43+
// Read kind from WASM memory (if provided)
44+
var kind string
45+
if len(stack) > 1 && stack[1] != 0 {
46+
kindOffset := stack[1]
47+
kind, _ = plugin.ReadString(kindOffset)
48+
}
49+
50+
// Call shared discover service
51+
resp, err := discoverSvc.Discover(ctx, digest, kind)
52+
if err != nil {
53+
// Return 0 to signal error
54+
stack[0] = 0
55+
return
56+
}
57+
58+
// Serialize response to JSON
59+
jsonData, err := json.Marshal(resp.Result)
60+
if err != nil {
61+
// Return 0 to signal error
62+
stack[0] = 0
63+
return
64+
}
65+
66+
// Write JSON to WASM memory and return offset
67+
offset, err := plugin.WriteBytes(jsonData)
68+
if err != nil {
69+
// Return 0 to signal error
70+
stack[0] = 0
71+
return
72+
}
73+
74+
stack[0] = offset
75+
},
76+
// inputs: digest offset, kind offset
77+
[]extism.ValueType{extism.ValueTypeI64, extism.ValueTypeI64},
78+
// output: json result offset or 0 on error
79+
[]extism.ValueType{extism.ValueTypeI64},
80+
)
81+
}

0 commit comments

Comments
 (0)