Skip to content

Cloud Security: API, Infrastructure-as-Code, and Case Automation

This page is the operator reference for automating LimaCharlie Cloud Security: the public REST API surface, the Hive records that ARE the Infrastructure-as-Code surface (providers, policies, saved queries), and the D&R recipes that close the loop between cloud findings and Cases.

The REST API

All Cloud Security routes live under https://api.limacharlie.io/v1/cloudsec/{oid}/… and appear in the public OpenAPI spec at /openapi. Reads require the cloudsec.get permission, finding-triage writes require cloudsec.set, and every route requires the organization to be subscribed to the ext-cloud-security extension (a 403 tells you to subscribe).

The read surface includes: findings (risk-ranked worklist with keyset pagination and server-side filters), findings/facets, findings/classes (the canonical finding-class enum), findings/causes (findings grouped by the object whose single edit fixes them), attack-paths (with the same filter selectors), chokepoints (incl. the principal-exposure metrics), ciem/public-access, ciem/facets, ciem/identities (the filtered, paginated Access list), ciem/identity (the Identity 360 view of one principal, ?urn=), inventory (+inventory/facets), data-security/facets (+data-security/stores), topology (server-side estate aggregates), compliance (+compliance/frameworks, compliance/assignments), policy/vocabulary (the classification-policy vocabulary), providers/manifest (what a provider collects), caasm/assets, caasm/coverage, caasm/policy, overview (incl. the per-tenant usage metering block), risk-trend, changes, scan-status, free-tier (the org's free-tier standing and limits — descriptive, not a gate), query (the graph DSL), and graph/neighbors. There is also a multi-org (no {oid}) fleet/overview route that rolls risk up across every tenant you manage. Three read-only preview POSTs help you author policy before you commit it — simulate/resources (test a classification/coverage/exclusion matcher against stored inventory), simulate/findings (test a suppression matcher against open findings), and policy/suggest (live matcher-value autocomplete from the tenant estate). See the API Reference for the full route list and response shapes.

CSV export

Add ?format=csv to findings, inventory, compliance, or query to stream the result as a CSV attachment instead of JSON. The server walks the full filtered set itself (your filter query parameters apply; paging parameters are ignored), capped at 100,000 rows with a trailing #-comment row as the truncation notice.

curl -H "Authorization: Bearer $JWT" \
  "https://api.limacharlie.io/v1/cloudsec/$OID/findings?severity=CRITICAL&severity=HIGH&format=csv" \
  -o findings.csv

The compliance CSV carries one row per control including the proving finding ids — the auditor-facing evidence export.

Hive is the IaC surface

Cloud Security is configured entirely through Hive records. Anything you can click in the console you can limacharlie hive set — which makes tenant onboarding and multi-tenant policy management a script, not a UI workflow.

Hive Record Purpose
cloudsec_provider one per connection what to collect — one of thirteen connectors spanning cloud infra, identity/IdP, SaaS, AI, and LimaCharlie self-inventory (see Providers for the full list)
cloudsec_policy many, typed by policy_type classification (crown jewels), coverage (EDR expectation — accepted but not yet evaluated; see the Configuration reference), emission (event feed), exclusions (resource escape hatch), suppression (finding disposition rules), compliance (scoped framework assignment)
cloudsec_query one per saved query org-shared saved graph queries (the Query Console library)

limacharlie sync does not cover these hives

The cloudsec_* hives are outside the set limacharlie sync walks, so a config pull/push will not round-trip them. Manage them with explicit limacharlie hive set / hive get calls — the loops below are the pattern.

Onboarding a tenant (recipe)

# 1. Subscribe the org to the extension (billing/enable gate).
limacharlie extension subscribe --name ext-cloud-security --oid $OID

# 2. Connect a provider.
cat > provider.json <<EOF
{
  "provider_type": "gcp",
  "gcp_scope": "organizations/123456789",
  "credentials": "hive://secret/gcp-collector-sa",
  "internal_domains": ["acme.com"],
  "sync_now": "onboard-1"
}
EOF
limacharlie hive set --hive-name cloudsec_provider --key ${ORG_CODE}-gcp \
  --oid $OID --input-file provider.json --enabled

# 3. Declare the crown jewels (nothing is sensitive without a policy).
cat > classification.json <<EOF
{
  "policy_type": "classification",
  "classification": {
    "data_stores": [
      {"name_contains": ["customer", "pii"], "classes": ["pii"]}
    ]
  }
}
EOF
limacharlie hive set --hive-name cloudsec_policy --key classification \
  --oid $OID --input-file classification.json --enabled

Multi-tenant policy push (recipe)

The same records applied to N organizations is the MSSP fleet-policy story:

for OID in $(cat tenant-oids.txt); do
  limacharlie hive set --hive-name cloudsec_policy --key classification \
    --oid "$OID" --input-file classification.json --enabled
done

Suppression rules (finding disposition policy)

A suppression-typed cloudsec_policy record dispositions matching findings automatically — the "accept this known risk in the sandbox for 90 days" mechanic. An operator's own disposition always wins, deleting a rule releases exactly its own findings on the next cycle, and criticals are never auto-suppressed unless a rule's max_severity says critical explicitly.

match takes four selectors — OR within a selector, AND across them: finding_class (the class enum), rule (exact rule ids), account (shell globs, including leading-! negation — "!prod-*" scopes to every account that is not production), and urn_prefix (resource-URN prefixes), with max_severity as the inclusive ceiling. The record is validated on write and rejected unless every rule has a unique non-empty name (it becomes the disposition actor, policy:<name>), at least one of those four match keys with no empty entries, a well-formed account glob, a max_severity from the severity enum, an effect.kind of accepted or false_positivemitigated is not a policy's call to make — and a non-empty effect.reason, since an unexplained auto-disposition is unauditable. Rules are ordered and first match wins.

{
  "policy_type": "suppression",
  "suppression": {
    "rules": [{
      "name": "sandbox-key-age",
      "match": {
        "rule": ["stale-user-managed-sa-key"],
        "account": ["proj-sandbox-*"],
        "max_severity": "high"
      },
      "effect": {
        "kind": "accepted",
        "reason": "sandbox accepted risk (SEC-123)",
        "ttl_days": 90
      }
    }]
  }
}

Saved queries

{
  "version": 1,
  "name": "Exposed VMs reaching sensitive data",
  "query": {"text": "MATCH (t:ComputeInstance {is_sensitive:true})<-[:can_reach]-(s:ComputeInstance) RETURN s, t"},
  "project": "rows",
  "tags": ["weekly"]
}

Save it as a cloudsec_query record and it appears in every teammate's Query Console and as a pinnable lens on the Attack Surface page's lens rail.

The schedule block is active: a record with schedule.scheduled: true is evaluated after each projection cycle — or on an optional cadence, one of interval (a duration of at least 5m) or cron (standard 5-field) — and emits cloud_query.match / cloud_query.resolved as anchors enter and leave its match-set. A scheduled query must be an enabled record, since a disabled one has to stay inert. Anchors already suppressed by the org's suppression policy do not emit unless the schedule sets emit_suppressed. The detection block is still reserved: it is shape-checked on write, so IaC written today survives the promote-to-detection phase, but nothing consumes it yet.

Findings ↔ Cases automation

Cloud findings emit lifecycle events into the organization's own event stream via the internally-provisioned cloudsec webhook adapter: cloud_finding.created (carries the full finding under finding), cloud_finding.closed ({finding_id, fingerprint, finding_class}), and cloud_finding.still_open (re-asserted at most once per day for open findings with a linked ticket, carrying that ticket in the payload). D&R rules match these like any event; the Cases extension actions close the loop. For richer automation the same stream also carries cloud_finding.updated (an open finding materially changed: a severity move in either direction, the subject becoming internet-reachable, or one of its CVEs entering CISA KEV) and the disposition verbs cloud_finding.resolved / .dismissed / .reopened / .assigned. Those last four cover both human triage and a suppression policy acting on its own: read actor to tell them apart (a user id versus policy:<rule name>), and branch on the payload's resolution rather than the verb, since an accepted risk and a fixed one both arrive as .resolved.

The console installs these three rules with one click (Settings → Cloud Security → Cases, an opt-in), or write them yourself:

Auto-case on high/critical findings (async, grouped, storm-safe — one case per rule category per window, and first-sync floods are summarized upstream):

detect:
  event: cloud_finding.created
  op: in
  path: event/finding/severity
  values: [CRITICAL, HIGH]
respond:
  - action: extension request
    extension name: ext-cases
    extension action: ingest_detection
    extension request:
      detect_id: "{{ .event.finding.fingerprint }}"
      cat: "cloudsec:{{ .event.finding.rule_id }}"
      source: cloudsec
      detect: "{{ .event.finding }}"

Resolve the case when the sweep confirms the fix:

detect:
  event: cloud_finding.closed
  op: exists
  path: event/fingerprint
respond:
  - action: extension request
    extension name: ext-cases
    extension action: update_case
    extension request:
      detect_id: "{{ .event.fingerprint }}"
      status: resolved
      note: "Finding closed: condition no longer detected by sweep"

Reopen a case that was closed while the cloud wasn't actually fixed:

detect:
  event: cloud_finding.still_open
  op: exists
  path: event/fingerprint
respond:
  - action: extension request
    extension name: ext-cases
    extension action: update_case
    extension request:
      detect_id: "{{ .event.fingerprint }}"
      reopen_if_closed: true
      note: "Linked cloud finding is still open  verified by latest sweep"

update_case resolves the case through the detection index (detect_id = the finding fingerprint), so the rules never need a case number; a finding with no linked case is a no-op. Cases never close findings — findings are detection truth and close when the sweep confirms the fix (or via operator/policy disposition).

Non-Cases shops: route the same cloud_finding.* events to Jira/ServiceNow via an Output on the cloudsec adapter's stream and key your tickets on fingerprint the same way.