Skip to content

Commit 2db9c55

Browse files
committed
chore(policy): Bootstrap WASM policy engine
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
1 parent b13f004 commit 2db9c55

17 files changed

Lines changed: 1217 additions & 81 deletions

File tree

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: 28 additions & 13 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,6 +26,7 @@ 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"
3031
opaAst "github.com/open-policy-agent/opa/v1/ast"
3132
"github.com/open-policy-agent/opa/v1/format"
@@ -43,6 +44,7 @@ type PolicyToLint struct {
4344
Path string
4445
YAMLFiles []*File
4546
RegoFiles []*File
47+
WASMFiles []*File
4648
Format bool
4749
Config string
4850
Errors []ValidationError
@@ -108,21 +110,21 @@ func Lookup(absPath, config string, format bool) (*PolicyToLint, error) {
108110
return nil, err
109111
}
110112

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

116118
// 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")
119+
if len(policy.YAMLFiles) == 0 && len(policy.RegoFiles) == 0 && len(policy.WASMFiles) == 0 {
120+
return nil, fmt.Errorf("no valid .yaml/.yml, .rego, or .wasm files found")
119121
}
120122

121123
return policy, nil
122124
}
123125

124-
// Loads referenced rego files from YAML files in the policy
125-
func (p *PolicyToLint) loadReferencedRegoFiles(baseDir string) error {
126+
// Loads referenced policy files (rego or wasm) from YAML files in the policy
127+
func (p *PolicyToLint) loadReferencedPolicyFiles(baseDir string) error {
126128
seen := make(map[string]struct{})
127129
for _, yamlFile := range p.YAMLFiles {
128130
var parsed v1.Policy
@@ -131,14 +133,14 @@ func (p *PolicyToLint) loadReferencedRegoFiles(baseDir string) error {
131133
continue
132134
}
133135
for _, spec := range parsed.Spec.Policies {
134-
regoPath := spec.GetPath()
135-
if regoPath != "" {
136+
policyPath := spec.GetPath()
137+
if policyPath != "" {
136138
// If path is relative, make it relative to the YAML file's directory
137-
if !filepath.IsAbs(regoPath) {
138-
regoPath = filepath.Join(baseDir, regoPath)
139+
if !filepath.IsAbs(policyPath) {
140+
policyPath = filepath.Join(baseDir, policyPath)
139141
}
140142

141-
resolvedPath, err := resourceloader.GetPathForResource(regoPath)
143+
resolvedPath, err := resourceloader.GetPathForResource(policyPath)
142144
if err != nil {
143145
return err
144146
}
@@ -173,8 +175,18 @@ func (p *PolicyToLint) processFile(filePath string) error {
173175
Path: filePath,
174176
Content: content,
175177
})
178+
case ".wasm":
179+
// Detect if file is actually WASM using magic bytes
180+
policyType := engine.DetectPolicyType(content)
181+
if policyType != engine.PolicyTypeWASM {
182+
return fmt.Errorf("file has .wasm extension but is not a valid WASM file")
183+
}
184+
p.WASMFiles = append(p.WASMFiles, &File{
185+
Path: filePath,
186+
Content: content,
187+
})
176188
default:
177-
return fmt.Errorf("unsupported file extension %s, must be .yaml/.yml or .rego", ext)
189+
return fmt.Errorf("unsupported file extension %s, must be .yaml/.yml, .rego, or .wasm", ext)
178190
}
179191

180192
return nil
@@ -185,6 +197,9 @@ func (p *PolicyToLint) Validate() {
185197
for _, regoFile := range p.RegoFiles {
186198
p.validateRegoFile(regoFile)
187199
}
200+
201+
// WASM files are binary and cannot be linted
202+
// Only verify they have valid WASM magic bytes (already done in processFile)
188203
}
189204

190205
func (p *PolicyToLint) validateRegoFile(file *File) {

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=

pkg/policies/engine/detect.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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 engine
17+
18+
// PolicyType represents the type of a policy (Rego or WASM)
19+
type PolicyType string
20+
21+
const (
22+
// PolicyTypeRego indicates a Rego-based policy
23+
PolicyTypeRego PolicyType = "rego"
24+
// PolicyTypeWASM indicates a WASM-based policy
25+
PolicyTypeWASM PolicyType = "wasm"
26+
)
27+
28+
// DetectPolicyType determines the policy type from source bytes
29+
// WASM files start with magic bytes: 0x00 0x61 0x73 0x6d (\0asm)
30+
// as documented at https://webassembly.github.io/spec/core/binary/modules.html#binary-module
31+
func DetectPolicyType(source []byte) PolicyType {
32+
// WASM files start with magic bytes: 0x00 0x61 0x73 0x6d (\0asm)
33+
if len(source) >= 4 &&
34+
source[0] == 0x00 &&
35+
source[1] == 0x61 &&
36+
source[2] == 0x73 &&
37+
source[3] == 0x6d {
38+
return PolicyTypeWASM
39+
}
40+
41+
// Default to Rego (text-based)
42+
return PolicyTypeRego
43+
}

pkg/policies/engine/detect_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 engine
17+
18+
import (
19+
"testing"
20+
21+
"github.com/stretchr/testify/assert"
22+
)
23+
24+
func TestDetectPolicyType(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
source []byte
28+
expected PolicyType
29+
}{
30+
{
31+
name: "WASM magic bytes",
32+
source: []byte{0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00},
33+
expected: PolicyTypeWASM,
34+
},
35+
{
36+
name: "Rego policy",
37+
source: []byte("package main\n\nresult = {\"violations\": []}"),
38+
expected: PolicyTypeRego,
39+
},
40+
{
41+
name: "Empty file",
42+
source: []byte{},
43+
expected: PolicyTypeRego,
44+
},
45+
{
46+
name: "Partial WASM magic bytes (only 3 bytes)",
47+
source: []byte{0x00, 0x61, 0x73},
48+
expected: PolicyTypeRego,
49+
},
50+
{
51+
name: "Incorrect magic bytes",
52+
source: []byte{0x00, 0x61, 0x73, 0x6e},
53+
expected: PolicyTypeRego,
54+
},
55+
{
56+
name: "Binary data that's not WASM",
57+
source: []byte{0xFF, 0xD8, 0xFF, 0xE0}, // JPEG magic bytes
58+
expected: PolicyTypeRego,
59+
},
60+
}
61+
62+
for _, tt := range tests {
63+
t.Run(tt.name, func(t *testing.T) {
64+
result := DetectPolicyType(tt.source)
65+
assert.Equal(t, tt.expected, result)
66+
})
67+
}
68+
}

0 commit comments

Comments
 (0)