From 00df62902c4222f01613b30eed0c48015c8501cb Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 1 Dec 2025 10:23:30 +0100 Subject: [PATCH 1/5] feat(policy-devel): Adapt policy develop init to WASM Signed-off-by: Javier Rodriguez --- app/cli/cmd/policy_develop_init.go | 28 +- app/cli/internal/policydevel/init.go | 244 +++++++++++++++--- .../templates/{ => rego}/example-policy.rego | 0 .../templates/{ => rego}/example-policy.yaml | 0 .../policydevel/templates/wasm-go/Makefile | 49 ++++ .../policydevel/templates/wasm-go/go.mod.tmpl | 7 + .../templates/wasm-go/policy.go.tmpl | 81 ++++++ .../policydevel/templates/wasm-go/policy.yaml | 17 ++ .../policydevel/templates/wasm-js/esbuild.js | 28 ++ .../templates/wasm-js/package.json | 12 + .../policydevel/templates/wasm-js/policy.d.ts | 44 ++++ .../policydevel/templates/wasm-js/policy.js | 91 +++++++ .../policydevel/templates/wasm-js/policy.yaml | 17 ++ app/cli/pkg/action/policy_develop_init.go | 8 + 14 files changed, 576 insertions(+), 50 deletions(-) rename app/cli/internal/policydevel/templates/{ => rego}/example-policy.rego (100%) rename app/cli/internal/policydevel/templates/{ => rego}/example-policy.yaml (100%) create mode 100644 app/cli/internal/policydevel/templates/wasm-go/Makefile create mode 100644 app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl create mode 100644 app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl create mode 100644 app/cli/internal/policydevel/templates/wasm-go/policy.yaml create mode 100644 app/cli/internal/policydevel/templates/wasm-js/esbuild.js create mode 100644 app/cli/internal/policydevel/templates/wasm-js/package.json create mode 100644 app/cli/internal/policydevel/templates/wasm-js/policy.d.ts create mode 100644 app/cli/internal/policydevel/templates/wasm-js/policy.js create mode 100644 app/cli/internal/policydevel/templates/wasm-js/policy.yaml diff --git a/app/cli/cmd/policy_develop_init.go b/app/cli/cmd/policy_develop_init.go index 77bdb93ed..15a171645 100644 --- a/app/cli/cmd/policy_develop_init.go +++ b/app/cli/cmd/policy_develop_init.go @@ -29,29 +29,41 @@ func newPolicyDevelopInitCmd() *cobra.Command { name string description string directory string + policyType string ) cmd := &cobra.Command{ Use: "init", Short: "Initialize a new policy", Long: `Initialize a new policy by creating template policy files in the specified directory. -By default, it creates chainloop-policy.yaml and chainloop-policy.rego files.`, - Example: ` - # Initialize in current directory with separate files + +Policy Types: + rego - Create a Rego-based policy using Open Policy Agent (default) + wasm-go - Create a WebAssembly policy using Go and TinyGo + wasm-js - Create a WebAssembly policy using JavaScript and Extism`, + Example: ` # Initialize a Rego policy in current directory (default) chainloop policy develop init - # Initialize in specific directory with embedded format and policy name - chainloop policy develop init --directory ./policies --embedded --name mypolicy`, + # Initialize a Rego policy with custom name + chainloop policy develop init --type rego --name my-policy + + # Initialize a WASM Go policy + chainloop policy develop init --type wasm-go --name my-policy --directory ./policies + + # Initialize a WASM JS policy + chainloop policy develop init --type wasm-js --name validation --description "My validation policy"`, RunE: func(_ *cobra.Command, _ []string) error { if directory == "" { directory = "." } + opts := &action.PolicyInitOpts{ Force: force, Embedded: embedded, Name: name, Description: description, Directory: directory, + PolicyType: policyType, } policyInit, err := action.NewPolicyInit(opts, ActionOpts) @@ -70,9 +82,11 @@ By default, it creates chainloop-policy.yaml and chainloop-policy.rego files.`, } cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite existing files") - cmd.Flags().BoolVar(&embedded, "embedded", false, "initialize an embedded policy (single YAML file)") + cmd.Flags().BoolVar(&embedded, "embedded", false, "initialize an embedded policy (single YAML file, Rego only)") cmd.Flags().StringVar(&name, "name", "", "name of the policy") cmd.Flags().StringVar(&description, "description", "", "description of the policy") - cmd.Flags().StringVar(&directory, "directory", "", "directory for policy") + cmd.Flags().StringVar(&directory, "directory", "", "directory for policy files") + cmd.Flags().StringVar(&policyType, "type", "", "policy type: rego (default), wasm-go, or wasm-js") + return cmd } diff --git a/app/cli/internal/policydevel/init.go b/app/cli/internal/policydevel/init.go index ded515c37..70edef5e4 100644 --- a/app/cli/internal/policydevel/init.go +++ b/app/cli/internal/policydevel/init.go @@ -28,9 +28,37 @@ import ( //go:embed templates/* var templateFS embed.FS +// PolicyType represents the type of policy to initialize +type PolicyType string + +const ( + PolicyTypeRego PolicyType = "rego" + PolicyTypeWasmGo PolicyType = "wasm-go" + PolicyTypeWasmJS PolicyType = "wasm-js" +) + const ( - policyTemplateRegoPath = "templates/example-policy.rego" - policyTemplatePath = "templates/example-policy.yaml" + // Rego templates + regoTemplateDir = "templates/rego" + regoPolicyFile = "example-policy.rego" + regoYAMLFile = "example-policy.yaml" + + // WASM Go templates + wasmGoTemplateDir = "templates/wasm-go" + wasmGoPolicyFile = "policy.go.tmpl" + wasmGoModFile = "go.mod.tmpl" + wasmGoYAMLFile = "policy.yaml" + wasmGoMakefileFile = "Makefile" + + // WASM JS templates + wasmJSTemplateDir = "templates/wasm-js" + wasmJSPolicyFile = "policy.js" + wasmJSPackageFile = "package.json" + wasmJSEsbuildFile = "esbuild.js" + wasmJSDTSFile = "policy.d.ts" + wasmJSYAMLFile = "policy.yaml" + + // Defaults defaultPolicyName = "policy" defaultPolicyDescription = "Chainloop validation policy" defaultMaterialKind = "SBOM_CYCLONEDX_JSON" @@ -45,13 +73,9 @@ type TemplateData struct { MaterialKind string } -type Content struct { - YAML string - Rego string -} - type InitOptions struct { Directory string + PolicyType PolicyType Embedded bool Force bool Name string @@ -59,76 +83,210 @@ type InitOptions struct { } func Initialize(opts *InitOptions) error { - content, err := loadAndProcessTemplates(opts) + // Default to Rego if no type specified + if opts.PolicyType == "" { + opts.PolicyType = PolicyTypeRego + } + + // Route to appropriate initializer based on policy type + switch opts.PolicyType { + case PolicyTypeRego: + return initializeRegoPolicy(opts) + case PolicyTypeWasmGo: + return initializeWasmGoPolicy(opts) + case PolicyTypeWasmJS: + return initializeWasmJSPolicy(opts) + default: + return fmt.Errorf("unsupported policy type: %s", opts.PolicyType) + } +} + +// initializeRegoPolicy creates a Rego-based policy +func initializeRegoPolicy(opts *InitOptions) error { + // Load templates + regoContent, err := templateFS.ReadFile(filepath.Join(regoTemplateDir, regoPolicyFile)) if err != nil { - return fmt.Errorf("failed to process templates: %w", err) + return fmt.Errorf("failed to read Rego template: %w", err) } + yamlContent, err := templateFS.ReadFile(filepath.Join(regoTemplateDir, regoYAMLFile)) + if err != nil { + return fmt.Errorf("failed to read YAML template: %w", err) + } + + // Prepare template data + data := &TemplateData{ + Name: getPolicyName(opts.Name), + Description: getPolicyDescription(opts.Description), + RegoPath: sanitizeName(getPolicyName(opts.Name)) + ".rego", + RegoContent: string(regoContent), + Embedded: opts.Embedded, + MaterialKind: defaultMaterialKind, + } + + // Process YAML template + yamlProcessed, err := executeTemplate(string(yamlContent), data) + if err != nil { + return fmt.Errorf("failed to process YAML template: %w", err) + } + + // Prepare files to write files := make(map[string]string) - fileNameBase := sanitizeName(getPolicyName(opts.Name)) + fileNameBase := sanitizeName(data.Name) - if opts.Embedded { - files[fileNameBase+".yaml"] = content.YAML - } else { - files[fileNameBase+".yaml"] = content.YAML - files[fileNameBase+".rego"] = content.Rego + files[fileNameBase+".yaml"] = yamlProcessed + if !opts.Embedded { + files[fileNameBase+".rego"] = data.RegoContent } return writeFiles(opts.Directory, files, opts.Force) } -func getPolicyName(name string) string { - if name == "" { - return defaultPolicyName +// initializeWasmGoPolicy creates a WASM Go-based policy +func initializeWasmGoPolicy(opts *InitOptions) error { + // Load templates + policyContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoPolicyFile)) + if err != nil { + return fmt.Errorf("failed to read Go policy template: %w", err) } - return name -} -func getPolicyDescription(description string) string { - if description == "" { - return defaultPolicyDescription + goModContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoModFile)) + if err != nil { + return fmt.Errorf("failed to read go.mod template: %w", err) } - return description + + yamlContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoYAMLFile)) + if err != nil { + return fmt.Errorf("failed to read YAML template: %w", err) + } + + makefileContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoMakefileFile)) + if err != nil { + return fmt.Errorf("failed to read Makefile template: %w", err) + } + + // Prepare template data + data := &TemplateData{ + Name: getPolicyName(opts.Name), + Description: getPolicyDescription(opts.Description), + MaterialKind: defaultMaterialKind, + } + + // Process templates + policyProcessed, err := executeTemplate(string(policyContent), data) + if err != nil { + return fmt.Errorf("failed to process policy template: %w", err) + } + + goModProcessed, err := executeTemplate(string(goModContent), data) + if err != nil { + return fmt.Errorf("failed to process go.mod template: %w", err) + } + + yamlProcessed, err := executeTemplate(string(yamlContent), data) + if err != nil { + return fmt.Errorf("failed to process YAML template: %w", err) + } + + makefileProcessed, err := executeTemplate(string(makefileContent), data) + if err != nil { + return fmt.Errorf("failed to process Makefile template: %w", err) + } + + // Prepare files to write + files := map[string]string{ + "policy.go": policyProcessed, + "go.mod": goModProcessed, + "policy.yaml": yamlProcessed, + "Makefile": makefileProcessed, + } + + return writeFiles(opts.Directory, files, opts.Force) } -func loadAndProcessTemplates(opts *InitOptions) (*Content, error) { - regoContent, err := templateFS.ReadFile(policyTemplateRegoPath) +// initializeWasmJSPolicy creates a WASM JavaScript-based policy +func initializeWasmJSPolicy(opts *InitOptions) error { + // Load templates + policyContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSPolicyFile)) if err != nil { - return nil, fmt.Errorf("failed to read Rego template: %w", err) + return fmt.Errorf("failed to read JS policy template: %w", err) } + packageContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSPackageFile)) + if err != nil { + return fmt.Errorf("failed to read package.json template: %w", err) + } + + esbuildContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSEsbuildFile)) + if err != nil { + return fmt.Errorf("failed to read esbuild.js template: %w", err) + } + + dtsContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSDTSFile)) + if err != nil { + return fmt.Errorf("failed to read policy.d.ts template: %w", err) + } + + yamlContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSYAMLFile)) + if err != nil { + return fmt.Errorf("failed to read YAML template: %w", err) + } + + // Prepare template data data := &TemplateData{ Name: getPolicyName(opts.Name), Description: getPolicyDescription(opts.Description), - RegoPath: sanitizeName(getPolicyName(opts.Name)) + ".rego", - RegoContent: string(regoContent), - Embedded: opts.Embedded, MaterialKind: defaultMaterialKind, } - // Process main template - content, err := templateFS.ReadFile(policyTemplatePath) + // Process templates + policyProcessed, err := executeTemplate(string(policyContent), data) if err != nil { - return nil, fmt.Errorf("failed to read policy template: %w", err) + return fmt.Errorf("failed to process policy template: %w", err) } - yamlContent, err := executeTemplate(string(content), data) + packageProcessed, err := executeTemplate(string(packageContent), data) if err != nil { - return nil, fmt.Errorf("failed to execute template: %w", err) + return fmt.Errorf("failed to process package.json template: %w", err) } - // For non-embedded case, we still need the Rego content to write to file - if !opts.Embedded { - return &Content{ - YAML: yamlContent, - Rego: data.RegoContent, - }, nil + dtsProcessed, err := executeTemplate(string(dtsContent), data) + if err != nil { + return fmt.Errorf("failed to process policy.d.ts template: %w", err) + } + + yamlProcessed, err := executeTemplate(string(yamlContent), data) + if err != nil { + return fmt.Errorf("failed to process YAML template: %w", err) + } + + // Prepare files to write (esbuild.js doesn't need template processing) + files := map[string]string{ + "policy.js": policyProcessed, + "package.json": packageProcessed, + "esbuild.js": string(esbuildContent), + "policy.d.ts": dtsProcessed, + "policy.yaml": yamlProcessed, + } + + return writeFiles(opts.Directory, files, opts.Force) +} + +func getPolicyName(name string) string { + if name == "" { + return defaultPolicyName } + return name +} - return &Content{YAML: yamlContent}, nil +func getPolicyDescription(description string) string { + if description == "" { + return defaultPolicyDescription + } + return description } -// Add custom template functions +// executeTemplate processes a template with the given data func executeTemplate(content string, data *TemplateData) (string, error) { tmpl := template.New("policy").Funcs(template.FuncMap{ "sanitize": sanitizeName, diff --git a/app/cli/internal/policydevel/templates/example-policy.rego b/app/cli/internal/policydevel/templates/rego/example-policy.rego similarity index 100% rename from app/cli/internal/policydevel/templates/example-policy.rego rename to app/cli/internal/policydevel/templates/rego/example-policy.rego diff --git a/app/cli/internal/policydevel/templates/example-policy.yaml b/app/cli/internal/policydevel/templates/rego/example-policy.yaml similarity index 100% rename from app/cli/internal/policydevel/templates/example-policy.yaml rename to app/cli/internal/policydevel/templates/rego/example-policy.yaml diff --git a/app/cli/internal/policydevel/templates/wasm-go/Makefile b/app/cli/internal/policydevel/templates/wasm-go/Makefile new file mode 100644 index 000000000..bf9e2ffc6 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-go/Makefile @@ -0,0 +1,49 @@ +# Makefile for {{.Name}} WASM policy +# +# Copyright 2025 The Chainloop Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +.PHONY: build clean lint help + +# Default target +.DEFAULT_GOAL := help + +# Build the WASM policy +build: ## Build the WASM policy + @echo "Building WASM policy..." + tinygo build -target=wasi -buildmode=c-shared -o policy.wasm policy.go + @echo "✓ Built policy.wasm" + +# Clean build artifacts +clean: ## Remove build artifacts + @echo "Cleaning build artifacts..." + rm -f policy.wasm + @echo "Cleaned" + +# Download dependencies +deps: ## Download Go dependencies + @echo "Downloading dependencies..." + go mod download + @echo "Dependencies downloaded" + +# Format Go code +fmt: ## Format Go code + @echo "Formatting Go code..." + gofmt -w . + @echo "Code formatted" + +# Show help +help: ## Show this help message + @echo "Available targets:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' diff --git a/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl b/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl new file mode 100644 index 000000000..c13258645 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl @@ -0,0 +1,7 @@ +module {{.Name | sanitize}} + +go 1.25 + +require github.com/chainloop-dev/wasi-policy-sdks/go v0.0.0 + +require github.com/extism/go-pdk v1.1.3 // indirect diff --git a/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl b/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl new file mode 100644 index 000000000..add7fb632 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl @@ -0,0 +1,81 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// {{.Description}} +// +// Build: +// tinygo build -target=wasi -o policy.wasm policy.go +// +// Test: +// chainloop policy develop eval --policy policy.yaml --material --kind {{.MaterialKind}} +package main + +import ( + chainlooppolicy "github.com/chainloop-dev/wasi-policy-sdks/go" +) + +// Input represents the expected input structure +type Input struct { + // Define your input fields here + // Example: Message string `json:"message"` +} + +//export Execute +func Execute() int32 { + return chainlooppolicy.Run(func() { + // Extract policy arguments (optional configuration) + args, err := chainlooppolicy.GetArgs() + if err != nil { + chainlooppolicy.OutputResult(chainlooppolicy.Skip(err.Error())) + return + } + + chainlooppolicy.LogInfo("Executing policy: {{.Name}}") + + // Parse material + var input Input + if err := chainlooppolicy.GetMaterialJSON(&input); err != nil { + chainlooppolicy.OutputResult(chainlooppolicy.Skip(err.Error())) + return + } + + // Validate + result := validate(input, args) + + // Output result + chainlooppolicy.OutputResult(result) + }) +} + +// validate checks the input for compliance +func validate(input Input, args map[string]string) chainlooppolicy.Result { + result := chainlooppolicy.Success() + + // Add your validation logic here + // Example: + // if input.Message == "" { + // result.AddViolation("message cannot be empty") + // } + + if result.HasViolations() { + chainlooppolicy.LogError("Validation failed with %d violations", len(result.Violations)) + } else { + chainlooppolicy.LogInfo("Validation passed") + } + + return result +} + +func main() {} diff --git a/app/cli/internal/policydevel/templates/wasm-go/policy.yaml b/app/cli/internal/policydevel/templates/wasm-go/policy.yaml new file mode 100644 index 000000000..517ee1a54 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-go/policy.yaml @@ -0,0 +1,17 @@ +# Policy generated by Chainloop CLI +# +# For WASM policy examples and reference: +# https://github.com/chainloop-dev/chainloop/tree/main/docs/examples/policies +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: {{.Name | sanitize}} + description: {{.Description | trimSpace}} +spec: + policies: + # Type of artifact to validate + # See docs: https://docs.chainloop.dev/concepts/material-types + - kind: {{.MaterialKind}} + # Path to WASM policy file + # See docs: https://docs.chainloop.dev/guides/custom-policies#wasm-policy-structure + path: policy.wasm diff --git a/app/cli/internal/policydevel/templates/wasm-js/esbuild.js b/app/cli/internal/policydevel/templates/wasm-js/esbuild.js new file mode 100644 index 000000000..78f632647 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-js/esbuild.js @@ -0,0 +1,28 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const esbuild = require('esbuild'); + +esbuild.build({ + entryPoints: ['policy.js'], + outdir: 'dist', + bundle: true, + sourcemap: false, + minify: false, + format: 'cjs', + target: ['es2020'], + platform: 'node', + external: [] +}).catch(() => process.exit(1)); diff --git a/app/cli/internal/policydevel/templates/wasm-js/package.json b/app/cli/internal/policydevel/templates/wasm-js/package.json new file mode 100644 index 000000000..8d386ee85 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-js/package.json @@ -0,0 +1,12 @@ +{ + "name": "{{.Name | sanitize}}", + "version": "1.0.0", + "description": "{{.Description | trimSpace}}", + "private": true, + "scripts": { + "build": "node esbuild.js && extism-js dist/policy.js -i policy.d.ts -o policy.wasm" + }, + "devDependencies": { + "esbuild": "^0.19.0" + } +} diff --git a/app/cli/internal/policydevel/templates/wasm-js/policy.d.ts b/app/cli/internal/policydevel/templates/wasm-js/policy.d.ts new file mode 100644 index 000000000..4b7e8acac --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-js/policy.d.ts @@ -0,0 +1,44 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * WebAssembly module interface for the discover policy example. + * This file is required by extism-js compiler to generate proper WASM exports. + */ + +/** + * Host functions provided by Chainloop runtime. + * These functions are implemented in the host (Chainloop engine) and callable from WASM. + */ +declare module "extism:host" { +// declare module "env" { + interface user { + /** + * Discover builtin function for exploring the artifact graph. + * @param digestOffset - Memory offset of the digest string + * @param kindOffset - Memory offset of the kind string (optional, can be 0) + * @returns Memory offset of the JSON result, or 0 on error + */ + chainloop_discover(digestOffset: I64, kindOffset: I64): I64; + } +} + +declare module "main" { + /** + * Main policy execution function exported to WASM. + * @returns Exit code (0 for success, 1 for error) + */ + export function Execute(): I32; +} \ No newline at end of file diff --git a/app/cli/internal/policydevel/templates/wasm-js/policy.js b/app/cli/internal/policydevel/templates/wasm-js/policy.js new file mode 100644 index 000000000..7fb908007 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-js/policy.js @@ -0,0 +1,91 @@ +// +// Copyright 2025 The Chainloop Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * {{.Description}} + * + * Build: + * npm install + * npm run build + * + * Test: + * chainloop policy develop eval --policy policy.yaml --material --kind {{.MaterialKind}} + */ + +const { + getMaterialJSON, + getArgs, + success, + skip, + outputResult, + logInfo, + logError, + run +} = require('chainloop-sdk'); + +/** + * Main policy execution function. + * This is the entry point called by the Chainloop engine. + */ +function Execute() { + return run(() => { + // Get policy arguments (optional configuration) + const args = getArgs(); + + logInfo('Executing policy: {{.Name}}'); + + // Parse material + const input = getMaterialJSON(); + + // Check if material has the expected structure + if (!input) { + outputResult(skip("Material is empty or invalid")); + return; + } + + // Validate + const result = validate(input, args); + + // Output result + outputResult(result); + }); +} + +/** + * Validates the input for compliance. + * + * @param {Object} input - The input object to validate + * @param {Object} args - Policy arguments + * @returns {Object} The validation result + */ +function validate(input, args) { + const result = success(); + + // Add your validation logic here + // Example: + // if (!input.message || input.message === "") { + // result.addViolation("message cannot be empty"); + // } + + if (result.hasViolations()) { + logError(`Validation failed with ${result.violations.length} violations`); + } else { + logInfo('Validation passed'); + } + + return result; +} + +module.exports = { Execute }; diff --git a/app/cli/internal/policydevel/templates/wasm-js/policy.yaml b/app/cli/internal/policydevel/templates/wasm-js/policy.yaml new file mode 100644 index 000000000..517ee1a54 --- /dev/null +++ b/app/cli/internal/policydevel/templates/wasm-js/policy.yaml @@ -0,0 +1,17 @@ +# Policy generated by Chainloop CLI +# +# For WASM policy examples and reference: +# https://github.com/chainloop-dev/chainloop/tree/main/docs/examples/policies +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: {{.Name | sanitize}} + description: {{.Description | trimSpace}} +spec: + policies: + # Type of artifact to validate + # See docs: https://docs.chainloop.dev/concepts/material-types + - kind: {{.MaterialKind}} + # Path to WASM policy file + # See docs: https://docs.chainloop.dev/guides/custom-policies#wasm-policy-structure + path: policy.wasm diff --git a/app/cli/pkg/action/policy_develop_init.go b/app/cli/pkg/action/policy_develop_init.go index 735d3efc8..81f36ff23 100644 --- a/app/cli/pkg/action/policy_develop_init.go +++ b/app/cli/pkg/action/policy_develop_init.go @@ -27,6 +27,7 @@ type PolicyInitOpts struct { Name string Description string Directory string + PolicyType string // rego, wasm-go, wasm-js } type PolicyInit struct { @@ -42,8 +43,15 @@ func NewPolicyInit(opts *PolicyInitOpts, actionOpts *ActionsOpts) (*PolicyInit, } func (action *PolicyInit) Run() error { + // Parse policy type + var policyType policydevel.PolicyType + if action.opts.PolicyType != "" { + policyType = policydevel.PolicyType(action.opts.PolicyType) + } + initOpts := &policydevel.InitOptions{ Directory: action.opts.Directory, + PolicyType: policyType, Embedded: action.opts.Embedded, Force: action.opts.Force, Name: action.opts.Name, From 15419f98f454e4d3b793dac3baa3b6e38219e579 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Mon, 1 Dec 2025 10:26:23 +0100 Subject: [PATCH 2/5] modify version Signed-off-by: Javier Rodriguez --- app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl | 2 +- app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl b/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl index c13258645..b3dd317bb 100644 --- a/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl +++ b/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl @@ -2,6 +2,6 @@ module {{.Name | sanitize}} go 1.25 -require github.com/chainloop-dev/wasi-policy-sdks/go v0.0.0 +require github.com/chainloop-dev/chainloop/sdks/go v1.61.0 require github.com/extism/go-pdk v1.1.3 // indirect diff --git a/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl b/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl index add7fb632..9d60d9581 100644 --- a/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl +++ b/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl @@ -23,7 +23,7 @@ package main import ( - chainlooppolicy "github.com/chainloop-dev/wasi-policy-sdks/go" + chainlooppolicy "github.com/chainloop-dev/chainloop/sdks/go" ) // Input represents the expected input structure From e2dcc7cd58eb2f5d88cce3e4e004b0492ca59eac Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 3 Dec 2025 17:00:10 +0100 Subject: [PATCH 3/5] remove tests fix templates Signed-off-by: Javier Rodriguez --- app/cli/internal/policydevel/init_test.go | 26 ------------------- .../policydevel/templates/wasm-go/go.mod.tmpl | 2 +- .../templates/wasm-go/policy.go.tmpl | 2 +- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/app/cli/internal/policydevel/init_test.go b/app/cli/internal/policydevel/init_test.go index ddd537383..a88e20b63 100644 --- a/app/cli/internal/policydevel/init_test.go +++ b/app/cli/internal/policydevel/init_test.go @@ -107,32 +107,6 @@ func TestInitialize(t *testing.T) { }) } -func TestLoadAndProcessTemplates(t *testing.T) { - t.Run("embedded rego", func(t *testing.T) { - opts := &InitOptions{ - Embedded: true, - Name: "embedded-test", - } - - content, err := loadAndProcessTemplates(opts) - require.NoError(t, err) - assert.NotEmpty(t, content.YAML) - assert.Empty(t, content.Rego) // Rego file should be empty for embedded - }) - - t.Run("separate rego file", func(t *testing.T) { - opts := &InitOptions{ - Embedded: false, - Name: "separate-rego-test", - } - - content, err := loadAndProcessTemplates(opts) - require.NoError(t, err) - assert.NotEmpty(t, content.YAML) - assert.NotEmpty(t, content.Rego) - }) -} - func TestExecuteTemplate(t *testing.T) { testCases := []struct { name string diff --git a/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl b/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl index b3dd317bb..1e388685b 100644 --- a/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl +++ b/app/cli/internal/policydevel/templates/wasm-go/go.mod.tmpl @@ -2,6 +2,6 @@ module {{.Name | sanitize}} go 1.25 -require github.com/chainloop-dev/chainloop/sdks/go v1.61.0 +require github.com/chainloop-dev/chainloop/policies/go v1.61.0 require github.com/extism/go-pdk v1.1.3 // indirect diff --git a/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl b/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl index 9d60d9581..d931b6122 100644 --- a/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl +++ b/app/cli/internal/policydevel/templates/wasm-go/policy.go.tmpl @@ -23,7 +23,7 @@ package main import ( - chainlooppolicy "github.com/chainloop-dev/chainloop/sdks/go" + chainlooppolicy "github.com/chainloop-dev/chainloop/policies/go" ) // Input represents the expected input structure From 7165583baf2dc1cffa4e3685db1cdbc27dad412a Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 3 Dec 2025 17:20:02 +0100 Subject: [PATCH 4/5] format file Signed-off-by: Javier Rodriguez --- app/cli/internal/policydevel/init.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/cli/internal/policydevel/init.go b/app/cli/internal/policydevel/init.go index 70edef5e4..1fc31108c 100644 --- a/app/cli/internal/policydevel/init.go +++ b/app/cli/internal/policydevel/init.go @@ -39,9 +39,9 @@ const ( const ( // Rego templates - regoTemplateDir = "templates/rego" - regoPolicyFile = "example-policy.rego" - regoYAMLFile = "example-policy.yaml" + regoTemplateDir = "templates/rego" + regoPolicyFile = "example-policy.rego" + regoYAMLFile = "example-policy.yaml" // WASM Go templates wasmGoTemplateDir = "templates/wasm-go" From 32ec22d5cf5a16f6086c83a3575d49bd35d48651 Mon Sep 17 00:00:00 2001 From: Javier Rodriguez Date: Wed, 3 Dec 2025 17:21:18 +0100 Subject: [PATCH 5/5] refactor Signed-off-by: Javier Rodriguez --- app/cli/internal/policydevel/init.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/cli/internal/policydevel/init.go b/app/cli/internal/policydevel/init.go index 1fc31108c..e53338379 100644 --- a/app/cli/internal/policydevel/init.go +++ b/app/cli/internal/policydevel/init.go @@ -38,6 +38,9 @@ const ( ) const ( + // General to all templates + policyYAMLFile = "policy.yaml" + // Rego templates regoTemplateDir = "templates/rego" regoPolicyFile = "example-policy.rego" @@ -47,7 +50,6 @@ const ( wasmGoTemplateDir = "templates/wasm-go" wasmGoPolicyFile = "policy.go.tmpl" wasmGoModFile = "go.mod.tmpl" - wasmGoYAMLFile = "policy.yaml" wasmGoMakefileFile = "Makefile" // WASM JS templates @@ -56,7 +58,6 @@ const ( wasmJSPackageFile = "package.json" wasmJSEsbuildFile = "esbuild.js" wasmJSDTSFile = "policy.d.ts" - wasmJSYAMLFile = "policy.yaml" // Defaults defaultPolicyName = "policy" @@ -155,7 +156,7 @@ func initializeWasmGoPolicy(opts *InitOptions) error { return fmt.Errorf("failed to read go.mod template: %w", err) } - yamlContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, wasmGoYAMLFile)) + yamlContent, err := templateFS.ReadFile(filepath.Join(wasmGoTemplateDir, policyYAMLFile)) if err != nil { return fmt.Errorf("failed to read YAML template: %w", err) } @@ -227,7 +228,7 @@ func initializeWasmJSPolicy(opts *InitOptions) error { return fmt.Errorf("failed to read policy.d.ts template: %w", err) } - yamlContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, wasmJSYAMLFile)) + yamlContent, err := templateFS.ReadFile(filepath.Join(wasmJSTemplateDir, policyYAMLFile)) if err != nil { return fmt.Errorf("failed to read YAML template: %w", err) }