WASM Guide¶
The ores.wasm module is a WASI preview 1 binary that embeds the complete ORES engine. It runs in any WASI-capable runtime — wasmtime, wasmer, Node.js, Python, Deno, and more.
The module follows a simple stdin/stdout protocol: send a JSON EvaluationRequest to stdin, receive a JSON EvaluationResult from stdout. No network, no filesystem access.
Download¶
The ores.wasm file is attached to every GitHub release:
stdin/stdout Protocol¶
The WASM module is a pure function over standard I/O:
| Stream | Content |
|---|---|
| stdin | A single JSON EvaluationRequest object |
| stdout | A JSON EvaluationResult object on success |
| stderr | A JSON error object on failure: {"error": "<message>"} |
| Exit code | 0 on success, 1 on error |
Zero capabilities required
The module does not make network calls, does not access the filesystem (beyond stdio), and does not require any WASI capabilities beyond standard I/O streams. This makes it safe to run in any sandboxed environment.
Runtime Examples¶
Install wasmtime:
Run an evaluation:
echo '{
"apiVersion": "ores.dev/v1",
"kind": "EvaluationRequest",
"signals": {
"cvss": { "base_score": 9.8 },
"epss": { "probability": 0.91, "percentile": 0.98 },
"threat_intel": { "actively_exploited": true }
}
}' | wasmtime ores.wasm
Output:
{
"apiVersion": "ores.dev/v1",
"kind": "EvaluationResult",
"score": 79,
"label": "high",
"mode": "weighted",
"version": "0.2.0",
"explanation": {
"signals_provided": 3,
"signals_used": 3,
"signals_unknown": 0,
"unknown_signals": [],
"warnings": [],
"confidence": 0.55,
"factors": [...]
}
}
Error handling:
Option 1: Native WASI bindings
const { Engine, Module, Store, WasiCtx } = require('@bytecodealliance/wasmtime');
const { readFileSync } = require('fs');
async function scoreVulnerability(signals) {
const wasmBytes = readFileSync('./ores.wasm');
const engine = new Engine();
const module = new Module(engine, wasmBytes);
const store = new Store(engine);
const request = JSON.stringify({
apiVersion: 'ores.dev/v1',
kind: 'EvaluationRequest',
signals,
});
const inputBytes = Buffer.from(request, 'utf-8');
const wasi = new WasiCtx({
args: ['ores'],
env: {},
stdin: inputBytes,
});
const linker = wasi.linker(store);
const instance = await linker.instantiate(store, module);
wasi.startExecution(store, instance);
const output = wasi.stdout;
return JSON.parse(output.toString('utf-8'));
}
// Usage
scoreVulnerability({
cvss: { base_score: 9.8 },
epss: { probability: 0.91, percentile: 0.98 },
threat_intel: { actively_exploited: true },
asset: { criticality: 'high', network_exposure: true },
}).then(result => {
console.log(`Score: ${result.score} (${result.label})`);
console.log(`Confidence: ${result.explanation.confidence}`);
});
Option 2: Simple subprocess (for scripts and tools)
const { execFileSync } = require('child_process');
function scoreVulnerability(signals) {
const input = JSON.stringify({
apiVersion: 'ores.dev/v1',
kind: 'EvaluationRequest',
signals,
});
const output = execFileSync('wasmtime', ['./ores.wasm'], {
input,
encoding: 'utf-8',
});
return JSON.parse(output);
}
const result = scoreVulnerability({
cvss: { base_score: 7.5 },
nist: { severity: 'high' },
});
console.log(result.score, result.label);
Which approach to choose?
Use native WASI bindings when you need to avoid spawning subprocesses (serverless, web workers, embedded contexts). Use the subprocess approach for CLI tools and scripts where simplicity matters more than startup latency.
Option 1: Native wasmtime bindings
import json
from wasmtime import (
Engine, Module, Store, Linker,
WasiConfig, Config
)
def score_vulnerability(signals: dict) -> dict:
"""Score a vulnerability using the ORES WASM module."""
request = json.dumps({
"apiVersion": "ores.dev/v1",
"kind": "EvaluationRequest",
"signals": signals,
})
config = Config()
engine = Engine(config)
store = Store(engine)
wasi_config = WasiConfig()
wasi_config.stdin_bytes(request.encode("utf-8"))
# Capture stdout into a temporary file
output_path = "/tmp/ores_output.json"
wasi_config.stdout_file(output_path)
store.set_wasi(wasi_config)
linker = Linker(engine)
linker.define_wasi()
with open("ores.wasm", "rb") as f:
wasm_bytes = f.read()
module = Module(engine, wasm_bytes)
instance = linker.instantiate(store, module)
start = instance.exports(store)["_start"]
start(store)
with open(output_path, "r") as f:
return json.load(f)
# Usage
result = score_vulnerability({
"cvss": {"base_score": 9.8},
"epss": {"probability": 0.91, "percentile": 0.98},
"threat_intel": {"actively_exploited": True},
"asset": {
"criticality": "high",
"network_exposure": True,
"data_classification": "pii",
},
})
print(f"Score: {result['score']} ({result['label']})")
print(f"Confidence: {result['explanation']['confidence']}")
for factor in result["explanation"]["factors"]:
print(f" {factor['factor']}: +{factor['contribution']}")
Option 2: Simple subprocess
import json
import subprocess
def score_vulnerability(signals: dict) -> dict:
request = json.dumps({
"apiVersion": "ores.dev/v1",
"kind": "EvaluationRequest",
"signals": signals,
})
result = subprocess.run(
["wasmtime", "ores.wasm"],
input=request.encode("utf-8"),
capture_output=True,
check=True,
)
return json.loads(result.stdout)
result = score_vulnerability({"cvss": {"base_score": 7.5}})
print(result["score"], result["label"])
Module reuse in Python
When scoring many vulnerabilities in a loop, compile the Module once outside the function and pass it in. This avoids re-reading and re-compiling the .wasm file on each call.
Performance Tips¶
Pre-compile with wasmtime AOT¶
Wasmtime supports ahead-of-time compilation to a .cwasm file, eliminating compilation overhead on repeated cold starts:
# Compile once
wasmtime compile ores.wasm -o ores.cwasm
# Run the pre-compiled version (sub-millisecond startup)
echo '...' | wasmtime run --allow-precompiled ores.cwasm
Reuse the Module object¶
When using the wasmtime API directly (Go, Node.js, Python), compile the Module once and reuse it across evaluations. Module compilation is the most expensive step.
# Do this once
module = Module(engine, wasm_bytes)
# Reuse for every evaluation
for signals in vulnerability_list:
store = Store(engine)
# ... instantiate with the same module
When to use the daemon instead¶
If you need to score thousands of vulnerabilities per second, the oresd daemon is the better choice. It amortizes engine initialization across all requests and provides health checks, audit logging, and gRPC support.
| Use case | Recommended approach |
|---|---|
| CI pipelines, scripting | WASM via wasmtime CLI |
| Serverless functions, edge compute | WASM via native bindings |
| Browser or web worker | WASM via native bindings |
| High-throughput backend service | oresd daemon |
| In-process Go application | Go library |
Startup time
The WASM module starts up in under 10 ms on modern hardware with wasmtime. However, this cost is paid on every invocation when using the CLI approach. For latency-sensitive paths, use native bindings with module reuse or switch to the daemon.