Documentation
¶
Index ¶
- func CheckSSRF(ip net.IP) error
- func NewSSRFSafeClient(timeout time.Duration) *http.Client
- func WithInsecureTransport(c *http.Client) func(*WebhookDispatcher)
- type HookResult
- type HookRunner
- func (hr *HookRunner) FireAfterHook(hook *schema.HookConfig, event string, record map[string]any, tenantID string) bool
- func (hr *HookRunner) RunAfterHook(ctx context.Context, hook *schema.HookConfig, event string, ...)
- func (hr *HookRunner) RunBeforeHook(ctx context.Context, hook *schema.HookConfig, payload map[string]any, ...) (*HookResult, error)
- type JSSandbox
- type WasmRunner
- func (wr *WasmRunner) Close(ctx context.Context) error
- func (wr *WasmRunner) CompileCount() int64
- func (wr *WasmRunner) Execute(ctx context.Context, tenantID string, wasmBytes []byte, functionName string, ...) ([]byte, error)
- func (wr *WasmRunner) ExecuteNamed(ctx context.Context, tenantID, moduleName, functionName string, input []byte) ([]byte, error)
- func (wr *WasmRunner) RegisterModule(name string, wasmBytes []byte)
- type WebhookDispatcher
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CheckSSRF ¶
CheckSSRF returns an error if ip should be blocked to prevent Server-Side Request Forgery. Blocked ranges:
func NewSSRFSafeClient ¶
NewSSRFSafeClient returns an http.Client whose dialer rejects private, loopback, and link-local addresses and whose redirect policy refuses all redirects. The given timeout bounds the dial, TLS handshake, response-header wait, and the whole request. Exported so other outbound senders (e.g. the SLO Slack alerter) reuse the exact same egress guard instead of duplicating it.
func WithInsecureTransport ¶
func WithInsecureTransport(c *http.Client) func(*WebhookDispatcher)
WithInsecureTransport returns a dispatcher that bypasses SSRF and HTTPS checks. Use ONLY in tests where the target is a local httptest server.
Types ¶
type HookResult ¶
HookResult is the outcome of a hook script execution.
type HookRunner ¶
type HookRunner struct {
// contains filtered or unexported fields
}
HookRunner orchestrates JS sandbox, WASM runtime, and webhook dispatch for lifecycle hooks.
func NewHookRunner ¶
func NewHookRunner(sandbox *JSSandbox) *HookRunner
NewHookRunner creates a HookRunner backed by the provided sandbox and a production (SSRF-safe, HTTPS-only) webhook dispatcher. No WASM runtime.
func NewHookRunnerWithDispatcher ¶
func NewHookRunnerWithDispatcher(sandbox *JSSandbox, dispatcher *WebhookDispatcher, maxConcurrent int) *HookRunner
NewHookRunnerWithDispatcher creates a HookRunner with an explicit dispatcher and an explicit bound on concurrent async dispatches. maxConcurrent <= 0 falls back to the default. Used by tests to inject an insecure (loopback) dispatcher or a small concurrency bound.
func NewHookRunnerWithWasm ¶
func NewHookRunnerWithWasm(sandbox *JSSandbox, dispatcher *WebhookDispatcher, wasm *WasmRunner) *HookRunner
NewHookRunnerWithWasm creates a HookRunner with a WASM runtime (Capa 3) wired in alongside the JS sandbox and webhook dispatcher. A nil dispatcher falls back to the production dispatcher.
func (*HookRunner) FireAfterHook ¶
func (hr *HookRunner) FireAfterHook(hook *schema.HookConfig, event string, record map[string]any, tenantID string) bool
FireAfterHook dispatches the after_create hook on a bounded background goroutine and returns immediately, so the request never blocks on webhook latency. It returns true if the dispatch was admitted, false if the dispatch pool was saturated and the hook was dropped (logged). A nil hook is a no-op that returns true.
This is the production entry point used by the create handler; prefer it over `go hr.RunAfterHook(...)` so the number of in-flight dispatches stays bounded.
func (*HookRunner) RunAfterHook ¶
func (hr *HookRunner) RunAfterHook( ctx context.Context, hook *schema.HookConfig, event string, record map[string]any, tenantID string, )
RunAfterHook fires an after-hook synchronously on the calling goroutine. event is the REAL lifecycle event ("after_create" | "after_update") so the webhook carries the correct X-Appximo-Event header (SEC-AUDIT-V2 Hallazgo B). Production code should call FireAfterHook (which bounds concurrency and returns immediately); RunAfterHook is the worker it invokes. Only "webhook" after-hooks do anything — js/wasm after-hooks are rejected at schema load (see schema.Validate), so the js/wasm branch here is unreachable from a validated schema and kept fail-safe.
func (*HookRunner) RunBeforeHook ¶
func (hr *HookRunner) RunBeforeHook( ctx context.Context, hook *schema.HookConfig, payload map[string]any, userCtx map[string]any, ) (*HookResult, error)
RunBeforeHook runs the before_create hook synchronously and returns the outcome. A nil hook is a no-op that returns {Proceed:true, Data:payload}. Webhook hooks never block: they always return Proceed:true.
type JSSandbox ¶
type JSSandbox struct {
// contains filtered or unexported fields
}
JSSandbox executes untrusted JS hook scripts in a time-limited Goja VM.
func NewJSSandbox ¶
func NewJSSandbox() *JSSandbox
NewJSSandbox returns a sandbox with 80 ms watchdog and GOMAXPROCS*2 concurrency limit.
func (*JSSandbox) RunHook ¶
func (s *JSSandbox) RunHook( ctx context.Context, script string, payload map[string]any, userCtx map[string]any, ) (*HookResult, error)
RunHook executes script inside a fresh Goja VM with an 80 ms watchdog interrupt. payload is the mutable request data; userCtx carries role/user_id/tenant_id. The script may read and modify `data`, and must set `result.proceed` to false (with `result.error`) to abort the operation. Returns (*goja.InterruptedError, nil) if the watchdog fires.
type WasmRunner ¶
type WasmRunner struct {
// contains filtered or unexported fields
}
WasmRunner executes sandboxed WASM (Capa 3) modules. Each module is compiled once and cached per (tenant, module-hash); instances are created and destroyed per Execute call. Guests get NO filesystem, network, clock-walltime, or syscall access — only the two host functions appximo_log and appximo_now. Execution time is bounded by the caller's context deadline (or wasmDefaultTimeout), and the runtime is configured to abort a running guest when that context is done.
func NewWasmRunner ¶
func NewWasmRunner(ctx context.Context) (*WasmRunner, error)
NewWasmRunner builds a runner with a 16 MiB per-module memory cap and registers the host functions. The provided ctx is used only to instantiate the host module; per-call deadlines are passed to Execute.
func (*WasmRunner) Close ¶
func (wr *WasmRunner) Close(ctx context.Context) error
Close releases the runtime and all cached compiled modules.
func (*WasmRunner) CompileCount ¶
func (wr *WasmRunner) CompileCount() int64
CompileCount returns how many distinct modules have actually been compiled (cache misses). Used by tests to assert the cache prevents recompilation.
func (*WasmRunner) Execute ¶
func (wr *WasmRunner) Execute(ctx context.Context, tenantID string, wasmBytes []byte, functionName string, input []byte) ([]byte, error)
Execute runs functionName (default "transform") in the given module, passing input through the (ptr,len) ABI and returning the bytes the function points to.
ABI: the module must export "memory", "alloc"(size i32)->(ptr i32), and functionName as (ptr i32, len i32)->(outPtr i32, outLen i32). "free" is called if exported. The returned slice is copied out of guest memory before the instance is closed.
Timeout: if ctx has no deadline, wasmDefaultTimeout is applied. A guest that runs past the deadline is aborted and Execute returns an error.
func (*WasmRunner) ExecuteNamed ¶
func (wr *WasmRunner) ExecuteNamed(ctx context.Context, tenantID, moduleName, functionName string, input []byte) ([]byte, error)
ExecuteNamed resolves a pre-loaded module by name and runs functionName on it. Used by the HookRunner for HookConfig.Type == "wasm".
func (*WasmRunner) RegisterModule ¶
func (wr *WasmRunner) RegisterModule(name string, wasmBytes []byte)
RegisterModule pre-loads a module under a name so hooks can reference it by HookConfig.WasmModule. A defensive copy of wasmBytes is stored.
type WebhookDispatcher ¶
type WebhookDispatcher struct {
// contains filtered or unexported fields
}
WebhookDispatcher sends signed HTTP POST notifications to webhook endpoints.
func NewWebhookDispatcher ¶
func NewWebhookDispatcher() *WebhookDispatcher
NewWebhookDispatcher creates a production dispatcher with:
- SSRF egress guard (blocks loopback, private, and link-local IPs)
- HTTPS-only enforcement
- No redirect following (prevents open-redirect SSRF bypasses)
func NewWebhookDispatcherOpts ¶
func NewWebhookDispatcherOpts(opts ...func(*WebhookDispatcher)) *WebhookDispatcher
NewWebhookDispatcherOpts creates a dispatcher with optional overrides.
func (*WebhookDispatcher) Dispatch ¶
func (d *WebhookDispatcher) Dispatch(ctx context.Context, hook *schema.HookConfig, event string, payload map[string]any, tenantID string)
Dispatch sends a signed POST to hook.URL with up to 3 retries (4 total attempts) and exponential backoff (1s, 2s, 4s). Failures are logged but never returned.