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
158161func (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
190206func (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+
203248func (p * PolicyToLint ) validateAndFormatRego (content , path string ) string {
204249 // 1. Optionally format
205250 if p .Format {
0 commit comments