Skip to content

Commit d1dea77

Browse files
authored
chore(policy): Bootstrap WASM policy engine (#2584)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent b27760e commit d1dea77

File tree

21 files changed

+1544
-108
lines changed

21 files changed

+1544
-108
lines changed

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/eval.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025 The Chainloop Authors.
1+
// Copyright 2024-2025 The Chainloop Authors.
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@ import (
1818
"context"
1919
"encoding/json"
2020
"fmt"
21-
"os"
2221

2322
controlplanev1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2423
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
@@ -203,8 +202,3 @@ func craft(materialPath string, kind v1.CraftingSchema_Material_MaterialType, na
203202
}
204203
return m, nil
205204
}
206-
207-
func fileNotExists(path string) bool {
208-
_, err := os.Stat(path)
209-
return os.IsNotExist(err)
210-
}

app/cli/internal/policydevel/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2025 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.

app/cli/internal/policydevel/lint.go

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2025 The Chainloop Authors.
2+
// Copyright 2024-2025 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -26,7 +26,9 @@ import (
2626

2727
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2828
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
29+
"github.com/chainloop-dev/chainloop/pkg/policies/engine"
2930
"github.com/chainloop-dev/chainloop/pkg/resourceloader"
31+
extism "github.com/extism/go-sdk"
3032
opaAst "github.com/open-policy-agent/opa/v1/ast"
3133
"github.com/open-policy-agent/opa/v1/format"
3234
"github.com/styrainc/regal/pkg/config"
@@ -43,6 +45,7 @@ type PolicyToLint struct {
4345
Path string
4446
YAMLFiles []*File
4547
RegoFiles []*File
48+
WASMFiles []*File
4649
Format bool
4750
Config string
4851
Errors []ValidationError
@@ -108,21 +111,21 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) {
108111
return nil, err
109112
}
110113

111-
// Load referenced rego files from all YAML files
112-
if err := policy.loadReferencedRegoFiles(filepath.Dir(resolvedPath)); err != nil {
114+
// Load referenced policy files (rego or wasm) from all YAML files
115+
if err := policy.loadReferencedPolicyFiles(filepath.Dir(resolvedPath)); err != nil {
113116
return nil, err
114117
}
115118

116119
// Verify we found at least one valid file
117-
if len(policy.YAMLFiles) == 0 && len(policy.RegoFiles) == 0 {
118-
return nil, fmt.Errorf("no valid .yaml/.yml or .rego files found")
120+
if len(policy.YAMLFiles) == 0 && len(policy.RegoFiles) == 0 && len(policy.WASMFiles) == 0 {
121+
return nil, fmt.Errorf("no valid .yaml/.yml, .rego, or .wasm files found")
119122
}
120123

121124
return policy, nil
122125
}
123126

124-
// Loads referenced rego files from YAML files in the policy
125-
func (p *PolicyToLint) loadReferencedRegoFiles(baseDir string) error {
127+
// Loads referenced policy files (rego or wasm) from YAML files in the policy
128+
func (p *PolicyToLint) loadReferencedPolicyFiles(baseDir string) error {
126129
seen := make(map[string]struct{})
127130
for _, yamlFile := range p.YAMLFiles {
128131
var parsed v1.Policy
@@ -131,14 +134,14 @@ func (p *PolicyToLint) loadReferencedRegoFiles(baseDir string) error {
131134
continue
132135
}
133136
for _, spec := range parsed.Spec.Policies {
134-
regoPath := spec.GetPath()
135-
if regoPath != "" {
137+
policyPath := spec.GetPath()
138+
if policyPath != "" {
136139
// If path is relative, make it relative to the YAML file's directory
137-
if !filepath.IsAbs(regoPath) {
138-
regoPath = filepath.Join(baseDir, regoPath)
140+
if !filepath.IsAbs(policyPath) {
141+
policyPath = filepath.Join(baseDir, policyPath)
139142
}
140143

141-
resolvedPath, err := resourceloader.GetPathForResource(regoPath)
144+
resolvedPath, err := resourceloader.GetPathForResource(policyPath)
142145
if err != nil {
143146
return err
144147
}
@@ -156,13 +159,21 @@ func (p *PolicyToLint) loadReferencedRegoFiles(baseDir string) error {
156159
}
157160

158161
func (p *PolicyToLint) processFile(filePath string) error {
162+
ext := strings.ToLower(filepath.Ext(filePath))
163+
164+
// Read file content once
159165
content, err := os.ReadFile(filePath)
160166
if err != nil {
161167
return err
162168
}
163169

164-
ext := strings.ToLower(filepath.Ext(filePath))
165170
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})
166177
case ".yaml", ".yml":
167178
p.YAMLFiles = append(p.YAMLFiles, &File{
168179
Path: filePath,
@@ -174,7 +185,7 @@ func (p *PolicyToLint) processFile(filePath string) error {
174185
Content: content,
175186
})
176187
default:
177-
return fmt.Errorf("unsupported file extension %s, must be .yaml/.yml or .rego", ext)
188+
return fmt.Errorf("unsupported file extension %s, must be .yaml/.yml, .rego, or .wasm", ext)
178189
}
179190

180191
return nil
@@ -185,6 +196,11 @@ func (p *PolicyToLint) Validate() {
185196
for _, regoFile := range p.RegoFiles {
186197
p.validateRegoFile(regoFile)
187198
}
199+
200+
// Validate WASM files
201+
for _, wasmFile := range p.WASMFiles {
202+
p.validateWasmFile(wasmFile)
203+
}
188204
}
189205

190206
func (p *PolicyToLint) validateRegoFile(file *File) {
@@ -200,6 +216,35 @@ func (p *PolicyToLint) validateRegoFile(file *File) {
200216
}
201217
}
202218

219+
// validateWasmFile validates a WASM policy file by checking that it exports the required Execute function
220+
func (p *PolicyToLint) validateWasmFile(file *File) {
221+
ctx := context.Background()
222+
223+
// Create Extism manifest
224+
manifest := extism.Manifest{
225+
Wasm: []extism.Wasm{
226+
extism.WasmData{Data: file.Content},
227+
},
228+
}
229+
230+
cfg := extism.PluginConfig{
231+
EnableWasi: true,
232+
}
233+
234+
// Create plugin
235+
plugin, err := extism.NewPlugin(ctx, manifest, cfg, []extism.HostFunction{})
236+
if err != nil {
237+
p.AddError(file.Path, fmt.Sprintf("failed to load WASM module: %v", err), 0)
238+
return
239+
}
240+
defer plugin.Close(ctx)
241+
242+
// Check if Execute function is exported
243+
if !plugin.FunctionExists("Execute") {
244+
p.AddError(file.Path, "WASM module missing required 'Execute' function export", 0)
245+
}
246+
}
247+
203248
func (p *PolicyToLint) validateAndFormatRego(content, path string) string {
204249
// 1. Optionally format
205250
if p.Format {

app/cli/internal/policydevel/templates/example-policy.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ spec:
1818
embedded: |
1919
{{.RegoContent | indent 8}}
2020
{{else -}}
21-
# Path to external Rego policy file
22-
# See docs: https://docs.chainloop.dev/guides/custom-policies#rego-policy-structure
21+
# Path to external policy file (Rego or WASM)
22+
# Rego: https://docs.chainloop.dev/guides/custom-policies#rego-policy-structure
23+
# WASM: https://docs.chainloop.dev/guides/custom-policies#wasm-policy-structure
2324
path: {{.RegoPath}}
2425
{{end -}}

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ require (
7272
github.com/bufbuild/protoyaml-go v0.1.11
7373
github.com/casbin/casbin/v2 v2.103.0
7474
github.com/denisbrodbeck/machineid v1.0.1
75+
github.com/extism/go-sdk v1.7.1
7576
github.com/google/go-github/v66 v66.0.0
7677
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0
7778
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3
@@ -145,6 +146,7 @@ require (
145146
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
146147
github.com/distribution/reference v0.6.0 // indirect
147148
github.com/dustin/go-humanize v1.0.1 // indirect
149+
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
148150
github.com/emirpasic/gods v1.18.1 // indirect
149151
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
150152
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
@@ -168,6 +170,7 @@ require (
168170
github.com/gorilla/handlers v1.5.1 // indirect
169171
github.com/hashicorp/golang-lru v1.0.2 // indirect
170172
github.com/hashicorp/yamux v0.1.2 // indirect
173+
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
171174
github.com/jackc/puddle/v2 v2.2.2 // indirect
172175
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
173176
github.com/jellydator/ttlcache/v3 v3.3.0 // indirect
@@ -209,6 +212,8 @@ require (
209212
github.com/stoewer/go-strcase v1.3.0 // indirect
210213
github.com/styrainc/roast v0.15.0 // indirect
211214
github.com/tchap/go-patricia/v2 v2.3.2 // indirect
215+
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
216+
github.com/tetratelabs/wazero v1.9.0 // indirect
212217
github.com/theupdateframework/go-tuf/v2 v2.0.1 // indirect
213218
github.com/tklauser/go-sysconf v0.3.14 // indirect
214219
github.com/tklauser/numcpus v0.9.0 // indirect
@@ -230,6 +235,7 @@ require (
230235
go.opentelemetry.io/otel/metric v1.36.0 // indirect
231236
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
232237
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
238+
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
233239
go.step.sm/crypto v0.51.2 // indirect
234240
goa.design/goa v2.2.5+incompatible // indirect
235241
gopkg.in/warnings.v0 v0.1.2 // indirect

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD
379379
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
380380
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
381381
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
382+
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
383+
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
382384
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
383385
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
384386
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
@@ -406,6 +408,8 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP
406408
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
407409
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
408410
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
411+
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
412+
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
409413
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
410414
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
411415
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@@ -724,6 +728,8 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJ
724728
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
725729
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
726730
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
731+
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
732+
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
727733
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
728734
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
729735
github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ=
@@ -1227,6 +1233,10 @@ github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/
12271233
github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
12281234
github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
12291235
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=
1236+
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
1237+
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
1238+
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
1239+
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
12301240
github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg=
12311241
github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU=
12321242
github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI=
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+
}

0 commit comments

Comments
 (0)