Skip to content

Commit e138c59

Browse files
committed
perform wasm validation on lint
Signed-off-by: Javier Rodriguez <javier@chainloop.dev>
1 parent 5e2965c commit e138c59

2 files changed

Lines changed: 64 additions & 14 deletions

File tree

app/cli/internal/policydevel/lint.go

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal"
2929
"github.com/chainloop-dev/chainloop/pkg/policies/engine"
3030
"github.com/chainloop-dev/chainloop/pkg/resourceloader"
31+
extism "github.com/extism/go-sdk"
3132
opaAst "github.com/open-policy-agent/opa/v1/ast"
3233
"github.com/open-policy-agent/opa/v1/format"
3334
"github.com/styrainc/regal/pkg/config"
@@ -158,12 +159,30 @@ func (p *PolicyToLint) loadReferencedPolicyFiles(baseDir string) error {
158159
}
159160

160161
func (p *PolicyToLint) processFile(filePath string) error {
162+
ext := strings.ToLower(filepath.Ext(filePath))
163+
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
161181
content, err := os.ReadFile(filePath)
162182
if err != nil {
163183
return err
164184
}
165185

166-
ext := strings.ToLower(filepath.Ext(filePath))
167186
switch ext {
168187
case ".yaml", ".yml":
169188
p.YAMLFiles = append(p.YAMLFiles, &File{
@@ -175,16 +194,6 @@ func (p *PolicyToLint) processFile(filePath string) error {
175194
Path: filePath,
176195
Content: content,
177196
})
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-
})
188197
default:
189198
return fmt.Errorf("unsupported file extension %s, must be .yaml/.yml, .rego, or .wasm", ext)
190199
}
@@ -198,8 +207,10 @@ func (p *PolicyToLint) Validate() {
198207
p.validateRegoFile(regoFile)
199208
}
200209

201-
// WASM files are binary and cannot be linted
202-
// Only verify they have valid WASM magic bytes (already done in processFile)
210+
// Validate WASM files
211+
for _, wasmFile := range p.WASMFiles {
212+
p.validateWasmFile(wasmFile)
213+
}
203214
}
204215

205216
func (p *PolicyToLint) validateRegoFile(file *File) {
@@ -215,6 +226,35 @@ func (p *PolicyToLint) validateRegoFile(file *File) {
215226
}
216227
}
217228

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

pkg/policies/engine/wasm/engine.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import (
3030
// Ensure Engine implements PolicyEngine interface
3131
var _ engine.PolicyEngine = (*Engine)(nil)
3232

33+
// entryFunction is the name of the function to call in the WASM module.
34+
// The engine expects this function to be present.
35+
const entryFunction = "Execute"
36+
3337
// Engine implements the PolicyEngine interface for WASM policies
3438
type Engine struct {
3539
executionTimeout time.Duration
@@ -147,6 +151,12 @@ func (e *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte
147151
}
148152
defer plugin.Close(ctx)
149153

154+
// Check if Execute function is exported
155+
if !plugin.FunctionExists(entryFunction) {
156+
e.logger.Error().Str("policy", policy.Name).Str("function", entryFunction).Msg("WASM module missing required function export")
157+
return nil, fmt.Errorf("wasm module validation failed: missing required '%s' function export", entryFunction)
158+
}
159+
150160
// Set up logger for WASM plugin output
151161
plugin.SetLogger(func(level extism.LogLevel, message string) {
152162
switch level {
@@ -172,7 +182,7 @@ func (e *Engine) Verify(ctx context.Context, policy *engine.Policy, input []byte
172182
e.logger.Debug().Str("policy", policy.Name).Dur("timeout", e.executionTimeout).Msg("Executing WASM policy with raw material input")
173183

174184
// Pass raw material bytes as input (args are in config)
175-
exit, output, err := plugin.CallWithContext(execCtx, "Execute", input)
185+
exit, output, err := plugin.CallWithContext(execCtx, entryFunction, input)
176186
if err != nil {
177187
// Parse the error to provide user-friendly messages
178188
parsedErr := parseWasmError(err)

0 commit comments

Comments
 (0)