Extending
A rule in TinyGo
A complete rule, start to finish. It flags an oversized database instance class.
The manifest
.garboard/rules/instance_size.json:
{
"id": "instance_size",
"severity": "warn",
"title": "Oversized instance class",
"docs": "https://acme.dev/rules/instance-size",
"description": "Finance signed off on sizes up to db.m5.4xlarge."
}
The module
package main
import "encoding/json"
// A bump arena. The host calls alloc once per invocation and throws the
// instance away afterwards, so there is nothing to free.
var arena [1 << 16]byte
//export alloc
func alloc(n int32) *byte { return &arena[0] }
//export garboard_rule
func garboardRule(ptr *byte, length int32) uint64 {
var in struct {
SchemaVersion int `json:"schema_version"`
Resources []struct {
Type string `json:"type"`
File string `json:"file"`
Line int `json:"line"`
StringAttrs map[string]string `json:"string_attrs"`
} `json:"resources"`
}
if json.Unmarshal(arena[:length], &in) != nil || in.SchemaVersion != 1 {
return 0
}
type finding struct {
Title string `json:"title"`
Body string `json:"body"`
File string `json:"file"`
Line int `json:"line"`
}
out := struct {
Findings []finding `json:"findings"`
}{}
for _, r := range in.Resources {
// string_attrs holds LITERAL values only, so a var-driven
// instance_class is simply absent here. We under-detect rather
// than guess — see below.
if r.Type == "aws_db_instance" && r.StringAttrs["instance_class"] == "db.m5.24xlarge" {
out.Findings = append(out.Findings, finding{
Title: "Instance class is not on the approved list",
Body: "db.m5.24xlarge is outside the sizes finance signed off on.",
File: r.File, Line: r.Line, // evidence, or the ranker drops it
})
}
}
if len(out.Findings) == 0 {
return 0
}
// ... marshal, copy into the arena, return the packed pointer/length
}
Two things in that code worth reading twice
File and Line are not optional. A finding without evidence is dropped by the ranker. Not shown with a caveat — dropped. Your rule must carry the file and line or it produces nothing.
string_attrs holds literal values only. An instance_class that comes from var.db_size is absent from the map, not present as the string "var.db_size". So this rule under-detects on variable-driven configuration, and that is the correct behaviour: guessing what a variable resolves to would produce a finding that names a line where the value is not.
Under-detecting is a defensible rule. Guessing is not.
Build
tinygo build -o .garboard/rules/instance_size.wasm -target=wasi ./rule
Test it before a pull request does
garboard gate . --rules .garboard/rules
Run it against a tree you know violates the rule, and a tree you know does not. Both directions matter — a rule that fires on everything is worse than no rule, and you will not discover that from the positive case.
Then put it in shadow for a fortnight before making it live. Your intuition about how often your own rule fires is reliably wrong, and shadow costs nothing to find out. See custom rules.
Rust
The same three exports with #[no_mangle] and extern "C", and wasm32-wasi as the target. The shape is identical; only the syntax differs.
