Skip to content

Writing Heuristics

Heuristics are CEL expressions evaluated against the live graph. Write one and CloudSlash flags every matching resource on every scan.

The Evaluation Model

The heuristics engine accepts a pointer to the Devi Graph and evaluates registered CEL rules against the property maps of all vertices. Rules execute sequentially; a node can match multiple heuristics.

type HeuristicRule struct {
    Name       string
    Expression *cel.Program
    Severity   int  // 1–100; >= 10 triggers waste flagging
}

Severity levels:

Severity Effect
< 10 Informational: logged, not flagged
10–49 FLAG verdict: waste detected, below action threshold
50–79 DELETE verdict: engine recommends removal
≥ 80 BLOCK / CRITICAL: immediate review required

CEL Expression Variables

The node variable is populated with the full vertex property map during evaluation:

Variable Type Description
node.type string Resource type (e.g., aws_instance, aws_ebs_volume)
node.properties map[string]any All provider-specific resource attributes
node.metrics.cpu_utilization_p99_7d float64 99th percentile CPU over 7 days
node.metrics.network_bytes_7d float64 Total network I/O over 7 days
node.metrics.invocations_90d int64 Lambda/function invocations over 90 days
node.tags map[string]string Resource tags
node.cost float64 Monthly cost estimate in USD
node.region string Cloud region

Writing Heuristic Rules

Declare rules in a YAML file with the rules: key:

# rules.yaml
rules:
  # Orphaned EBS volume: not attached to any instance
  - name: "orphaned_ebs_volume"
    severity: 10
    expression: "node.type == 'aws_ebs_volume' && node.properties['attachment.instance_id'] == null"

  # Idle EC2: consistently low CPU for 7 days
  - name: "idle_ec2_instance"
    severity: 50
    expression: "node.type == 'aws_instance' && node.metrics.cpu_utilization_p99_7d < 2.0"

  # Dead Lambda: zero invocations in 90 days
  - name: "stale_lambda"
    severity: 50
    expression: "node.type == 'aws_lambda_function' && node.metrics.invocations_90d == 0"

  # Oversized RDS: less than 5% CPU, costs more than $500/mo
  - name: "oversized_rds"
    severity: 30
    expression: "node.type == 'aws_db_instance' && node.metrics.cpu_utilization_p99_7d < 5.0 && node.cost > 500.0"

  # Untagged resource: missing required 'env' tag
  - name: "missing_env_tag"
    severity: 5
    expression: "!('env' in node.tags)"

  # gp2 volume: should migrate to gp3 for cost savings
  - name: "legacy_gp2_volume"
    severity: 10
    expression: "node.type == 'aws_ebs_volume' && node.properties['volume_type'] == 'gp2'"

Rule Field Reference

Field Required Type Description
name string Unique rule identifier (snake_case)
expression string CEL expression: must evaluate to bool
severity int Severity score (1–100)
description : string Human-readable rule description
target_type : string Restrict to a specific node type (optimization)

Validation

Rules Before Deployment

Parse and compile CEL expressions without running a scan:

cs policy validate rules.yaml

This catches: - YAML syntax errors - Missing required fields - CEL compile-time type errors - Unbalanced parentheses and unknown variable references

Run cs policy lint rules.yaml for additional checks: - Duplicate rule names - Very short expressions that likely indicate a typo - Missing description fields

Testing

Against Live Data

Dry-run your rules against the last scan snapshot without modifying any state:

cs policy test rules.yaml

Output shows which nodes would match each rule:

Testing rules.yaml against snapshot (10,420 nodes)...

  orphaned_ebs_volume    → 23 matches   (23 new waste nodes flagged)
  idle_ec2_instance      → 7 matches    (7 resources would get DELETE verdict)
  stale_lambda           → 41 matches
  missing_env_tag        → 312 matches  (informational, severity < 10)

Total: 71 waste nodes flagged across 4 rules.

Deploying Heuristics

Via CLI Flag

# Run a one-off scan with custom rules
cs scan --rules rules.yaml --region us-east-1

# Start the daemon with rules loaded
cs daemon --rules rules.yaml

Configuration

Persistent

Copy your rules file to ~/.cloudslash/policies/ and reference it in config.yaml:

# ~/.cloudslash/config.yaml
policies:
  active:
    - my-custom-rules    # loads ~/.cloudslash/policies/my-custom-rules/rules.yaml

REST API

Runtime Injection

Add rules to a running daemon without restarting:

curl -X POST http://localhost:8080/api/v1/policies \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [{
      "name": "idle_ec2_instance",
      "expression": "node.type == \"aws_instance\" && node.metrics.cpu_utilization_p99_7d < 2.0",
      "severity": 50
    }]
  }'

CEL Expression Tips

Null safety: use has() to check for property existence before access:

expression: "has(node.properties.attachment) && node.properties.attachment == null"

String matching: CEL supports contains(), startsWith(), endsWith(), and regex:

expression: "node.type.startsWith('aws_') && node.properties['instance_type'].contains('t2.')"

Multiple conditions: use && and || with explicit parentheses:

expression: "(node.type == 'aws_ebs_volume' || node.type == 'aws_snapshot') && node.cost > 10.0"

Tag map access: check tag existence before reading value:

expression: "'cloudslash:ignore' in node.tags && node.tags['cloudslash:ignore'] == 'true'"

Compilation Behavior

The engine compiles all CEL ASTs at boot. If any expression fails to compile, the daemon logs a fatal error and exits before accepting connections: ensuring no malformed rules silently skip evaluation. Fix the expression and restart.