AI-Powered Trust Evaluation of Open-Source Wasm Modules Before Deployment

AI-Powered Trust Evaluation of Open-Source Wasm Modules Before Deployment

The Problem

Plugin ecosystems built on WebAssembly are proliferating across the infrastructure stack. Envoy proxy filters, Extism plugin frameworks, OPA policy bundles, Fastly Compute@Edge functions, and wasmCloud actors all accept community-authored Wasm modules that run inside production infrastructure with varying degrees of host isolation. These modules are fetched from OCI registries, GitHub releases, community plugin registries, and npm-style package managers for Wasm.

The fundamental review problem is binary opacity. A Python package can be read by any developer; a Wasm binary cannot. The .wasm file is a compact binary encoding of a stack-based virtual machine instruction set. Without tooling, a reviewer cannot determine what host functions the module imports, whether it could exfiltrate data, or whether it contains conditional logic that activates only after a certain number of invocations. The module’s README says it is an “authentication helper” — but the binary may import filesystem read functions, network socket APIs, or environment variable access that have no place in an authentication helper.

Capability analysis tools exist. wasm-tools can disassemble a Wasm binary into the WAT (WebAssembly Text) format and extract the import section. But most teams deploying Wasm plugins do not have this step in their pipeline. They download a module, verify it runs, and deploy it. The supply chain check that catches an npm package with a postinstall script that phones home is entirely absent from the typical Wasm deployment workflow.

The attack surface is real. A malicious Wasm module that passes superficial review could import fd_write to send data through a file descriptor that is mapped to a network socket by the host runtime, read environment variables to exfiltrate API keys visible to the Wasm process, or implement a counter that triggers a payload after N invocations to defeat testing in pre-production environments. AI-assisted analysis of WAT disassembly can now detect these patterns at a semantic level — flagging “this module imports filesystem read operations and then calls a host network function, inconsistent with its stated purpose as an image transformation plugin.”

This article builds the full automated evaluation pipeline: import section analysis, LLM-powered WAT semantic analysis, provenance verification with cosign, source-binary alignment checking, and capability allowlist enforcement at the Wasmtime host level.

Threat Model

Malicious module imports unexpected host capabilities. A Wasm plugin for an Envoy filter is published to a community registry and advertised as a request header normalisation plugin. Its import section includes wasi_snapshot_preview1::fd_write, wasi_snapshot_preview1::path_open, and a custom host function env::send_metrics that the Envoy host exposes. The README does not mention filesystem access. The path_open import enables the module to read the host’s /etc/ directory if the Wasm host’s sandbox configuration permits it. Review of the binary import section would surface this immediately; skipping the review means it goes undetected.

Time-bombed payload activated after N invocations. A Wasm module includes a global counter variable that increments on each function call. After 10,000 invocations — a threshold designed to pass pre-production testing, which typically exercises fewer calls — a branch in the main function activates a secondary code path that was never reached during testing. The secondary path exfiltrates the current request’s Authorization header via a host network call. Because production traffic volume is required to trigger it, the module passes all pre-deployment testing.

Binary does not match published source. A legitimate-looking OSS Wasm plugin is published with source code on GitHub. The published .wasm binary in the GitHub Release is not the output of compiling the published source with the documented toolchain. The source code is clean; the binary contains additional functionality inserted at the binary level after compilation. Without a source-binary alignment check (recompile from source, compare hash), this discrepancy goes undetected.

Transitive component model compromise. A WebAssembly Component Model composition pulls in a sub-component from a registry. The parent component is well-reviewed; the sub-component is not. The sub-component’s imports include capabilities that the parent component’s review never considered because reviewers were not aware of the transitive dependency’s import surface.

Hardening Configuration

Step 1: Import Section Extraction with wasm-tools

The import section is the first and most important thing to examine in any Wasm binary. It declares every host function the module is allowed to call. Any host function not in this list cannot be called at runtime (the host will refuse the import).

# Install wasm-tools
cargo install wasm-tools

# Extract only the import section in text format
wasm-tools dump --imports plugin.wasm

# Full WAT disassembly (can be large for complex modules)
wasm-tools print plugin.wasm -o plugin.wat

# Extract imports as JSON for programmatic processing
wasm-tools objdump --json plugin.wasm | jq '.imports'

# Example output for a suspicious auth plugin:
# {
#   "imports": [
#     { "module": "wasi_snapshot_preview1", "name": "fd_read",  "type": "func" },
#     { "module": "wasi_snapshot_preview1", "name": "fd_write", "type": "func" },
#     { "module": "wasi_snapshot_preview1", "name": "path_open","type": "func" },
#     { "module": "env",                    "name": "get_token", "type": "func" },
#     { "module": "env",                    "name": "http_send", "type": "func" }
#   ]
# }
# An auth plugin needs get_token; it does NOT need path_open or http_send.

The import section analysis script extracts imports and cross-references against a capability allowlist for the module’s declared purpose:

#!/usr/bin/env python3
"""
wasm_trust_eval.py — AI-powered trust evaluation pipeline for Wasm modules.

Pipeline:
  1. Extract import section
  2. LLM analysis of imports vs stated purpose
  3. WAT disassembly + LLM semantic analysis of suspicious functions
  4. cosign provenance verification
  5. Optional source-binary alignment check
  6. Wasmtime capability allowlist generation
  7. Composite trust score
"""

import json
import subprocess
import sys
import hashlib
import tempfile
import shutil
import anthropic
from pathlib import Path
from dataclasses import dataclass, field, asdict
from typing import Optional

client = anthropic.Anthropic()

# WASI function categories for capability classification
WASI_CATEGORIES = {
    "filesystem": [
        "path_open", "path_create_directory", "path_remove_directory",
        "path_rename", "path_unlink_file", "path_readlink", "path_filestat_get",
        "fd_read", "fd_write", "fd_seek", "fd_close", "fd_readdir",
    ],
    "network": [
        "sock_open", "sock_connect", "sock_send", "sock_recv", "sock_shutdown",
        "sock_accept", "sock_bind", "sock_listen",
    ],
    "environment": [
        "environ_get", "environ_sizes_get",
    ],
    "process": [
        "proc_exit", "proc_raise", "sched_yield",
    ],
    "clock": [
        "clock_time_get", "clock_res_get",
    ],
    "random": [
        "random_get",
    ],
    "args": [
        "args_get", "args_sizes_get",
    ],
}

# Expected capabilities per common plugin purpose
PURPOSE_CAPABILITY_PROFILES = {
    "auth": {
        "expected": ["random", "clock"],
        "suspicious": ["filesystem", "network", "environment"],
        "description": "Authentication/authorization plugin",
    },
    "compression": {
        "expected": [],
        "suspicious": ["network", "environment", "process"],
        "description": "Data compression/decompression plugin",
    },
    "logging": {
        "expected": ["clock", "filesystem"],
        "suspicious": ["network", "environment"],
        "description": "Logging/observability plugin",
    },
    "image_transform": {
        "expected": ["random"],
        "suspicious": ["network", "environment", "filesystem"],
        "description": "Image transformation plugin",
    },
    "routing": {
        "expected": ["clock", "random"],
        "suspicious": ["filesystem", "environment"],
        "description": "Request routing/proxy plugin",
    },
    "general": {
        "expected": [],
        "suspicious": [],
        "description": "General purpose (no profile applied)",
    },
}


@dataclass
class WasmImport:
    module: str
    name: str
    import_type: str
    wasi_category: Optional[str] = None

    def __post_init__(self):
        for category, functions in WASI_CATEGORIES.items():
            if self.name in functions:
                self.wasi_category = category
                break


@dataclass
class TrustEvaluation:
    module_path: str
    stated_purpose: str
    imports: list[WasmImport] = field(default_factory=list)
    suspicious_imports: list[str] = field(default_factory=list)
    llm_import_analysis: dict = field(default_factory=dict)
    llm_wat_analysis: dict = field(default_factory=dict)
    provenance_verified: bool = False
    source_match: Optional[bool] = None
    composite_score: float = 0.0
    trust_level: str = "UNTRUSTED"
    allowlist: list[str] = field(default_factory=list)
    evaluation_sha256: str = ""


def extract_imports(wasm_path: str) -> list[WasmImport]:
    """Extract and parse the import section using wasm-tools."""
    try:
        result = subprocess.run(
            ["wasm-tools", "objdump", "--json", wasm_path],
            capture_output=True, text=True, timeout=30, check=True
        )
        data = json.loads(result.stdout)
        imports = []
        for imp in data.get("imports", []):
            imports.append(WasmImport(
                module=imp.get("module", ""),
                name=imp.get("name", ""),
                import_type=imp.get("type", "func"),
            ))
        return imports
    except (subprocess.CalledProcessError, json.JSONDecodeError, FileNotFoundError) as e:
        print(f"Import extraction failed: {e}", file=sys.stderr)
        return []


# ─── Step 2: LLM Import Analysis ─────────────────────────────────────────────

IMPORT_ANALYSIS_PROMPT = """You are a WebAssembly security analyst evaluating a Wasm plugin's imports.

Module stated purpose: {stated_purpose}

Import section:
{imports_formatted}

For this module, evaluate:
1. Are any imports inconsistent with the stated purpose? List each suspicious import and explain why it is unexpected.
2. Could any combination of imports enable data exfiltration (e.g. read env var + write to socket)?
3. Are there custom host function imports (non-WASI) that are not explained by the module's stated purpose?
4. Rate the overall import risk.

Output valid JSON only:
{{
  "import_risk": "low|medium|high|critical",
  "suspicious_imports": [
    {{"name": "import_name", "reason": "why this is unexpected for the stated purpose"}}
  ],
  "exfiltration_capability": true|false,
  "exfiltration_path": "describe the exfiltration chain if true, else null",
  "custom_host_imports": ["list of non-WASI custom host function imports"],
  "summary": "Two sentences summarising the import section risk",
  "recommend_reject": true|false
}}"""


def analyse_imports_with_llm(imports: list[WasmImport],
                              stated_purpose: str) -> dict:
    """Submit the import section to an LLM for semantic risk analysis."""
    imports_formatted = "\n".join(
        f"  [{imp.module}] {imp.name} ({imp.import_type})"
        + (f" — WASI category: {imp.wasi_category}" if imp.wasi_category else "")
        for imp in imports
    )

    message = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": IMPORT_ANALYSIS_PROMPT.format(
                stated_purpose=stated_purpose,
                imports_formatted=imports_formatted,
            ),
        }],
        system=[{
            "type": "text",
            "text": "You are a precise Wasm security analyst. Output only valid JSON.",
            "cache_control": {"type": "ephemeral"},
        }],
    )

    try:
        return json.loads(message.content[0].text)
    except json.JSONDecodeError:
        return {
            "import_risk": "unknown",
            "suspicious_imports": [],
            "exfiltration_capability": False,
            "summary": "Analysis failed",
            "recommend_reject": False,
        }


# ─── Step 3: WAT Disassembly and Semantic Analysis ───────────────────────────

WAT_ANALYSIS_PROMPT = """You are a WebAssembly security analyst reviewing disassembled WAT code.

Module stated purpose: {stated_purpose}
Suspicious imports flagged in previous analysis: {suspicious_imports}

WAT disassembly (selected suspicious function bodies):
```wat
{wat_excerpt}

Analyse the disassembled code for:

  1. Counter-based conditional execution (global counter incremented per call, branch after threshold)
  2. Obfuscated control flow that could hide payload activation logic
  3. Data encoding patterns (base64, XOR, shift operations on strings) that might encode exfiltrated data
  4. Use of the suspicious imports identified above — do they appear in call chains that handle sensitive data?
  5. Dead code sections that are never called from the exported functions

Output valid JSON only: {{ “counter_based_activation”: true|false, “counter_description”: “describe if true, else null”, “obfuscated_flow”: true|false, “encoding_patterns”: [“list of patterns found”], “suspicious_import_usage”: [ {{“import”: “name”, “call_chain”: “brief description of how/where it is called”}} ], “dead_code_sections”: true|false, “overall_wat_risk”: “low|medium|high|critical”, “summary”: “Three sentences summarising the semantic risk from the WAT analysis” }}“”"

def extract_suspicious_function_bodies(wat_path: str, suspicious_import_names: list[str], max_chars: int = 8000) -> str: “”" Extract WAT function bodies that reference suspicious imports. Returns a truncated excerpt for LLM analysis. “”" wat_content = Path(wat_path).read_text(errors=“replace”) lines = wat_content.splitlines()

# Find function definitions that contain references to suspicious imports target_sections = [] in_func = False func_depth = 0 current_func_lines = [] func_contains_target = False

for line in lines: stripped = line.strip()

if stripped.startswith(“(func”): in_func = True func_depth = 1 current_func_lines = [line] func_contains_target = any(name in line for name in suspicious_import_names) elif in_func: current_func_lines.append(line) func_depth += stripped.count(“(”) - stripped.count(“)”) if any(name in line for name in suspicious_import_names): func_contains_target = True if func_depth <= 0: if func_contains_target: target_sections.append(“\n”.join(current_func_lines)) in_func = False current_func_lines = [] func_contains_target = False

excerpt = “\n\n”.join(target_sections) return excerpt[:max_chars] if len(excerpt) > max_chars else excerpt

def analyse_wat_with_llm(wasm_path: str, stated_purpose: str, suspicious_imports: list[str]) -> dict: “”“Disassemble the module to WAT and run LLM semantic analysis.”“” with tempfile.NamedTemporaryFile(suffix=“.wat”, delete=False) as wat_file: wat_path = wat_file.name

try: subprocess.run( [“wasm-tools”, “print”, wasm_path, “-o”, wat_path], check=True, timeout=60, capture_output=True )

wat_excerpt = extract_suspicious_function_bodies(wat_path, suspicious_imports) if not wat_excerpt: # Fall back to first 8000 chars of full WAT if no suspicious functions isolated wat_excerpt = Path(wat_path).read_text(errors=“replace”)[:8000]

message = client.messages.create( model=“claude-opus-4-5”, max_tokens=1024, messages=[{ “role”: “user”, “content”: WAT_ANALYSIS_PROMPT.format( stated_purpose=stated_purpose, suspicious_imports=", ".join(suspicious_imports) or “none”, wat_excerpt=wat_excerpt, ), }], system=[{ “type”: “text”, “text”: “You are a precise Wasm security analyst. Output only valid JSON.”, “cache_control”: {“type”: “ephemeral”}, }], ) return json.loads(message.content[0].text)

except (subprocess.CalledProcessError, json.JSONDecodeError) as e: return { “overall_wat_risk”: “unknown”, “counter_based_activation”: False, “summary”: f"WAT analysis failed: {e}", } finally: Path(wat_path).unlink(missing_ok=True)

─── Step 4: cosign Provenance Verification ──────────────────────────────────

def verify_cosign_signature(wasm_path: str, publisher_key: str) -> bool: “”" Verify the Wasm binary’s cosign signature against the publisher’s public key. Returns True if signature verification succeeds. “”" try: result = subprocess.run( [ “cosign”, “verify-blob”, “–key”, publisher_key, “–signature”, f"{wasm_path}.sig", wasm_path, ], capture_output=True, text=True, timeout=30 ) if result.returncode == 0: print(“cosign signature verified”) return True else: print(f"cosign verification failed: {result.stderr}“) return False except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f"cosign not available or failed: {e}”) return False

def verify_registry_digest(wasm_path: str, expected_digest: str) -> bool: “”“Verify the module binary digest matches the registry-published digest.”“” sha256 = hashlib.sha256(Path(wasm_path).read_bytes()).hexdigest() if f"sha256:{sha256}" == expected_digest: print(f"Binary digest matches registry: {sha256[:16]}…“) return True print(f"DIGEST MISMATCH: got sha256:{sha256}, expected {expected_digest}”) return False

─── Step 5: Source-Binary Alignment Check ───────────────────────────────────

def source_binary_alignment_check(wasm_path: str, source_repo: str, source_ref: str, build_command: str, output_wasm: str) -> Optional[bool]: “”" Clone the source at the specified ref, rebuild with the documented toolchain, and compare the binary hash. Returns True if they match, False if not, None if the check cannot be completed. “”" with tempfile.TemporaryDirectory() as tmpdir: try: # Clone at the specific tag/commit subprocess.run( [“git”, “clone”, “–depth”, “1”, “–branch”, source_ref, source_repo, tmpdir], check=True, timeout=120, capture_output=True ) # Run the documented build command subprocess.run( build_command, shell=True, cwd=tmpdir, check=True, timeout=300, capture_output=True ) rebuilt_path = Path(tmpdir) / output_wasm if not rebuilt_path.exists(): print(f"Build output not found at {rebuilt_path}") return None

original_hash = hashlib.sha256(Path(wasm_path).read_bytes()).hexdigest() rebuilt_hash = hashlib.sha256(rebuilt_path.read_bytes()).hexdigest()

if original_hash == rebuilt_hash: print(f"Source-binary alignment VERIFIED: {original_hash[:16]}…“) return True else: print(f"Source-binary MISMATCH: published={original_hash[:16]}… rebuilt={rebuilt_hash[:16]}…”) return False

except subprocess.CalledProcessError as e: print(f"Source-binary check failed: {e}") return None

─── Step 6: Capability Allowlist for Wasmtime ───────────────────────────────

def generate_wasmtime_allowlist(imports: list[WasmImport], approved_imports: list[str]) -> str: “”" Generate a Wasmtime CLI configuration that restricts the module to only the pre-approved imports, denying any imports the module tries to use that are not on the allowlist. “”" approved_set = set(approved_imports) denied = [ imp for imp in imports if imp.name not in approved_set and imp.module.startswith(“wasi”) ]

config_lines = [ “# Wasmtime capability allowlist”, “# Generated by wasm_trust_eval.py — review before deployment”, “”, “[[module]]”, “# Only allow explicitly approved WASI imports”, ]

for imp in imports: if imp.name in approved_set: config_lines.append(f"allow_wasi = true # {imp.module}::{imp.name}")

if denied: config_lines.append(“”) config_lines.append(“# The following imports are DENIED (not in allowlist):”) for imp in denied: config_lines.append(f"# DENIED: {imp.module}::{imp.name}")

return “\n”.join(config_lines)

def generate_wasmtime_run_command(wasm_path: str, approved_imports: list[WasmImport]) -> str: “”“Generate a wasmtime run command with explicit capability grants.”“” # Map WASI categories to wasmtime CLI flags category_flags = { “filesystem”: “–dir=.::”, # Restrict to current directory only “environment”: “–env KEY=VALUE”, # Must be explicit “network”: “–tcplisten=127.0.0.1:0”, # Restrict to loopback if needed }

approved_categories = {imp.wasi_category for imp in approved_imports if imp.wasi_category} flags = [] for category in approved_categories: if category in category_flags: flags.append(category_flags[category])

flags_str = " “.join(flags) if flags else “–” return f"wasmtime run {flags_str} {wasm_path}”

─── Composite Trust Score ────────────────────────────────────────────────────

def compute_trust_score(evaluation: TrustEvaluation) -> tuple[float, str]: “”“Compute composite trust score from all evaluation components.”“” score = 100.0

# Import analysis penalties import_risk = evaluation.llm_import_analysis.get(“import_risk”, “unknown”) risk_penalties = {“low”: 0, “medium”: -15, “high”: -30, “critical”: -50, “unknown”: -10} score += risk_penalties.get(import_risk, -10)

if evaluation.llm_import_analysis.get(“exfiltration_capability”): score -= 25

if evaluation.llm_import_analysis.get(“recommend_reject”): score -= 20

# WAT analysis penalties wat_risk = evaluation.llm_wat_analysis.get(“overall_wat_risk”, “unknown”) score += risk_penalties.get(wat_risk, -10)

if evaluation.llm_wat_analysis.get(“counter_based_activation”): score -= 30

if evaluation.llm_wat_analysis.get(“obfuscated_flow”): score -= 20

# Provenance bonus/penalty if evaluation.provenance_verified: score += 10 else: score -= 20

# Source alignment if evaluation.source_match is True: score += 15 elif evaluation.source_match is False: score -= 40 # Binary mismatch is a strong rejection signal

score = max(0.0, min(100.0, score))

if score >= 80: trust_level = “TRUSTED” elif score >= 60: trust_level = “CONDITIONAL” elif score >= 40: trust_level = “REVIEW_REQUIRED” else: trust_level = “REJECTED”

return round(score, 1), trust_level

─── Full Pipeline ────────────────────────────────────────────────────────────

def evaluate_wasm_module( wasm_path: str, stated_purpose: str, publisher_key: str = “”, expected_digest: str = “”, source_repo: str = “”, source_ref: str = “”, build_command: str = “”, build_output: str = “”, ) -> TrustEvaluation: “”“Run the full trust evaluation pipeline for a Wasm module.”“” print(f"\n=== Evaluating {wasm_path} ===") module_hash = hashlib.sha256(Path(wasm_path).read_bytes()).hexdigest()

eval_result = TrustEvaluation( module_path=wasm_path, stated_purpose=stated_purpose, evaluation_sha256=module_hash, )

# 1. Extract imports print(“Step 1: Extracting imports…”) eval_result.imports = extract_imports(wasm_path) print(f" Found {len(eval_result.imports)} imports")

# 2. LLM import analysis print(“Step 2: LLM import analysis…”) eval_result.llm_import_analysis = analyse_imports_with_llm( eval_result.imports, stated_purpose ) suspicious = [ s[“name”] for s in eval_result.llm_import_analysis.get(“suspicious_imports”, []) ] eval_result.suspicious_imports = suspicious print(f" Import risk: {eval_result.llm_import_analysis.get(‘import_risk’)}")

# 3. WAT semantic analysis (only if suspicious imports found) if suspicious or eval_result.llm_import_analysis.get(“import_risk”) in (“medium”, “high”, “critical”): print(“Step 3: WAT disassembly and semantic analysis…”) eval_result.llm_wat_analysis = analyse_wat_with_llm( wasm_path, stated_purpose, suspicious ) print(f" WAT risk: {eval_result.llm_wat_analysis.get(‘overall_wat_risk’)}") else: eval_result.llm_wat_analysis = {“overall_wat_risk”: “low”, “summary”: “Skipped — no suspicious imports”}

# 4. cosign provenance print(“Step 4: Provenance verification…”) if publisher_key and Path(f"{wasm_path}.sig").exists(): eval_result.provenance_verified = verify_cosign_signature(wasm_path, publisher_key) elif expected_digest: eval_result.provenance_verified = verify_registry_digest(wasm_path, expected_digest) else: print(" No signature or digest provided — provenance unverified")

# 5. Source-binary alignment (optional, slow) if source_repo and source_ref and build_command: print(“Step 5: Source-binary alignment check…”) eval_result.source_match = source_binary_alignment_check( wasm_path, source_repo, source_ref, build_command, build_output )

# 6. Compute composite score eval_result.composite_score, eval_result.trust_level = compute_trust_score(eval_result)

# 7. Generate allowlist for approved imports approved_imports = [ imp.name for imp in eval_result.imports if imp.name not in suspicious ] eval_result.allowlist = approved_imports

print(f"\nTrust score: {eval_result.composite_score}/100 ({eval_result.trust_level})") return eval_result

if name == “main”: result = evaluate_wasm_module( wasm_path=sys.argv[1], stated_purpose=sys.argv[2] if len(sys.argv) > 2 else “general purpose plugin”, publisher_key=sys.argv[3] if len(sys.argv) > 3 else “”, ) print(json.dumps(asdict(result), indent=2, default=str))


### GitHub Actions Workflow

```yaml
# .github/workflows/wasm-trust-eval.yml
name: Wasm Module Trust Evaluation

on:
  pull_request:
    paths:
      - 'plugins/**/*.wasm'
      - 'filters/**/*.wasm'
  workflow_dispatch:
    inputs:
      wasm_path:
        description: 'Path to the Wasm module to evaluate'
        required: true
      stated_purpose:
        description: 'Module stated purpose (e.g. auth, compression, routing)'
        required: true

jobs:
  evaluate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install wasm-tools
        run: cargo install wasm-tools --locked

      - name: Install cosign
        uses: sigstore/cosign-installer@v3

      - name: Evaluate Wasm module
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PUBLISHER_KEY: ${{ secrets.WASM_PUBLISHER_KEY }}
        run: |
          python scripts/wasm_trust_eval.py \
            "${{ github.event.inputs.wasm_path }}" \
            "${{ github.event.inputs.stated_purpose }}" \
            "$PUBLISHER_KEY" \
            | tee evaluation-result.json

      - name: Check trust level
        run: |
          TRUST=$(jq -r '.trust_level' evaluation-result.json)
          SCORE=$(jq -r '.composite_score' evaluation-result.json)
          echo "Trust level: $TRUST (score: $SCORE)"
          if [ "$TRUST" = "REJECTED" ]; then
            echo "::error::Module rejected — trust score below threshold"
            exit 1
          elif [ "$TRUST" = "REVIEW_REQUIRED" ]; then
            echo "::warning::Module requires manual security review before deployment"
          fi

      - name: Upload evaluation report
        uses: actions/upload-artifact@v4
        with:
          name: wasm-trust-evaluation
          path: evaluation-result.json

Wasmtime Capability Enforcement

Run the module with explicit capability grants derived from the approved import allowlist:

# Run an auth plugin with only approved capabilities (no filesystem, no network)
wasmtime run \
  --deny-unknown-imports \
  --wasm-features=all \
  auth-plugin.wasm

# For a plugin that legitimately needs filesystem access (e.g. reading a config file)
# restrict to a specific directory, never the full filesystem
wasmtime run \
  --dir=/etc/plugin-config::/config \
  --deny-unknown-imports \
  config-reader-plugin.wasm

# WasmEdge equivalent with capability restriction
wasmedge --dir /tmp::/tmp \
  --env "PLUGIN_ENV_KEY=value" \
  plugin.wasm

Expected Behaviour After Hardening

After the pipeline is integrated into the CI/CD workflow, every Wasm module submitted for deployment goes through the full five-step evaluation before it reaches a staging environment. The evaluation runs in approximately two to four minutes for a typical plugin: thirty seconds for import extraction and LLM analysis, ninety seconds for WAT disassembly and semantic analysis of suspicious functions, and thirty seconds for cosign verification.

Modules with low import risk and verified provenance receive a TRUSTED classification (score ≥ 80) and deploy automatically. Modules with unexpected imports or unverified provenance receive a CONDITIONAL or REVIEW_REQUIRED classification, triggering a mandatory security engineer review before the deployment can proceed. Modules where the LLM WAT analysis detects counter-based activation or binary-source mismatch receive REJECTED status and block the deployment pipeline.

The allowlist generated for each module is used to configure the Wasmtime host’s import restrictions, ensuring that even a module that passes review cannot use host capabilities that were not explicitly approved during evaluation. This defence-in-depth means that a module which slips through evaluation with an undetected capability still has that capability denied at runtime.

Trade-offs and Operational Considerations

Consideration Detail
WAT disassembly size Complex modules compiled from Rust or C++ can produce WAT files in the megabyte range. The pipeline limits LLM analysis to suspicious function bodies, but for very large modules this extraction may miss relevant code in functions that don’t directly call suspicious imports but serve as call-chain intermediaries.
Source-binary alignment is toolchain-sensitive Reproducing a binary exactly requires the same compiler version, optimisation flags, and target triple. Minor toolchain differences produce different but functionally identical binaries that fail the hash check. Use this check as a red flag indicator, not as a hard gate, unless the publisher documents their exact build environment.
LLM analysis of obfuscated WAT A sufficiently obfuscated Wasm module can defeat semantic analysis — function names are stripped, variable names are lost, and control flow can be deliberately obscured. The LLM will flag obfuscation as a risk signal, but cannot fully deobfuscate a deliberately hidden payload.
cosign adoption gap Many community Wasm plugin publishers do not sign their releases with cosign or OCI signing. The absence of a signature is currently the norm, not a red flag. Weight provenance absence conservatively (score reduction) but not as a hard rejection.
Component model transitive analysis The pipeline evaluates a single Wasm module or component. For composed components under the WebAssembly Component Model, each sub-component should be evaluated independently. The pipeline does not currently traverse component compositions automatically.
Performance impact of evaluation in CI The LLM API calls add two to three minutes to the pipeline. For repositories that commit Wasm modules frequently, parallelize evaluations across modules. Cache results keyed on the module’s SHA-256 hash to avoid re-evaluating identical binaries.

Failure Modes

Failure Mode Likelihood Impact Detection Mitigation
Malicious import hidden in a sub-function not reached during WAT excerpt extraction Medium High — malicious capability analysis incomplete Post-deployment runtime monitoring for unexpected capability use Wasmtime capability allowlist at runtime is the defence-in-depth backstop; monitor for import-denied events
Counter-based payload uses a non-obvious counter (wall clock, not invocation count) Low High — time-based trigger evades invocation testing WAT analysis may detect clock imports + branching; flag for manual review Mandate manual review for any module with both clock access and conditional branching
cosign key compromise at the publisher Very Low Critical — all modules signed by the compromised key must be re-verified Publisher announces key rotation; monitor key transparency logs Implement cosign keyless signing via Sigstore with OIDC identity binding, removing single-key dependency
wasm-tools unable to parse non-standard binary (Wasm GC extension, etc.) Low Medium — evaluation skips import extraction for that module Parse error logged Block deployment if import extraction fails; require manual analysis for unanalysable modules
LLM identifies false-positive suspicious import (e.g. crypto plugin legitimately needs random) Medium Low — unnecessary review overhead Review queue tracks false positive rate Tune the purpose-capability profiles to include expected imports for common plugin types
Build environment irreproducibility causes false source-binary mismatch High Low — false alarm requiring manual verification Mismatch logged with both hashes Use diffoscope to compare binaries at the instruction level before concluding mismatch is malicious