Custom Heuristics¶
Build, compile, install. No PRs to core CloudSlash required.
A heuristic plugin is a compiled binary that implements two gRPC methods. The engine discovers it at startup, streams graph nodes to it, and applies its findings alongside the built-in heuristics: indistinguishable from either side.
The Interface¶
service HeuristicPlugin {
rpc Metadata(MetadataRequest) returns (MetadataResponse);
rpc Analyze(stream NodeBatch) returns (stream HeuristicFinding);
}
Metadata() is called once at startup. Declare your name, version, which resource types you want to see, and which node property keys you need. The engine sends only the keys you declare: everything else is stripped before your binary receives it.
Analyze() is a bidirectional stream. The engine sends NodeBatch chunks; you stream HeuristicFinding back for each wasteful resource you detect.
Scaffold¶
This generates a ready-to-compile Go project:
my-rds-scorer/
main.go ← plugin.Serve() boilerplate
heuristic.go ← implement Metadata() and Analyze() here
go.mod ← imports cloudslash/v2/pkg/engine/heuristic/shared
Makefile ← make build, make install
README.md
make build compiles the binary. make install copies it to ./heuristics/ so the daemon picks it up on next start.
Implementing Metadata()¶
func (h *MyHeuristic) Metadata(_ context.Context) (*pb.MetadataResponse, error) {
return &pb.MetadataResponse{
Name: "my-rds-scorer",
Description: "Flags RDS instances with no connections in 30 days",
Version: "1.0.0",
// Only receive these resource types.
ResourceTypeFilter: []string{"AWS::RDS::DBInstance"},
// Only receive these property keys: everything else is stripped.
RequiredProperties: []string{"DBInstanceStatus", "MultiAZ", "Engine"},
// Preferred batch size. Engine caps at its global max (default 500).
PreferredBatchSize: 100,
}, nil
}
Implementing Analyze()¶
func (h *MyHeuristic) Analyze(
ctx context.Context,
send func(*pb.NodeBatch) error,
recv func(*pb.HeuristicFinding) error,
) error {
// Drain all node batches.
var nodes []*pb.GraphNode
for {
batch := &pb.NodeBatch{}
if err := send(batch); err == io.EOF {
break
} else if err != nil {
return err
}
nodes = append(nodes, batch.Nodes...)
}
// Analyse and emit findings.
for _, node := range nodes {
if node.Properties["DBInstanceStatus"] != "available" {
continue
}
// your detection logic here...
if err := recv(&pb.HeuristicFinding{
Crn: node.Crn,
IsWaste: true,
RiskScore: 55, // 0–79; engine caps community plugins at 79
Reason: "RDS instance idle: no connections in 30 days",
ProjectedSavingsUsd: node.CostUsd,
SuggestedAction: "stop",
}); err != nil {
return err
}
}
return nil
}
Security Model¶
Community heuristic plugins operate under four controls:
| Control | What it does |
|---|---|
| Binary signing | SHA-256 manifest verified before subprocess spawns |
| Declared property access | Plugin only receives the keys it listed in RequiredProperties |
| CRN validation | Findings for nodes the plugin was never sent are silently discarded |
| Trust ceiling | RiskScore capped at 79: plugins cannot reach RISK (80+) or CRITICAL (90+) |
The ~/.cloudslash/heuristics/ path enforces binary signing. The ./heuristics/ local dev path skips it so you can iterate without signing every build.
Testing Locally¶
make build && make install
cs scan --mock # your plugin runs against synthetic graph data
cs heuristics list # confirm your plugin appears with ✓ status
Publishing to the Registry¶
Submit a PR to cloudslash-plugins with:
- Pre-built binaries for
linux-amd64,linux-arm64,darwin-amd64,darwin-arm64 - A
manifest.yamlwith your name, version, and per-arch SHA-256 checksums - Your
RequiredPropertieslist (reviewers check for over-broad declarations)
Once merged, users install with: