Skip to content

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

Private Beta

Cloud Security is currently in Private Beta. Features, APIs, and configuration formats described here may change before general availability. Contact us if you would like access.

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 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, attack-paths (with the same filter selectors), chokepoints (incl. the principal-exposure metrics), ciem/public-access, inventory (+inventory/facets), compliance (+compliance/frameworks), overview (incl. the per-tenant usage metering block), risk-trend, changes, scan-status, query (the graph DSL), and graph/neighbors.

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 cloud/IdP connection what to collect (GCP / AWS / Azure / Okta / Google Workspace)
cloudsec_policy many, typed by policy_type classification (crown jewels), coverage (EDR expectation), scanning (agentless YARA), emission (event feed), exclusions (resource escape hatch), suppression (finding disposition rules)
cloudsec_query one per saved query org-shared saved graph queries (the Query Console library)

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",
  "scope": "organizations/123456789",
  "secret": {"secret_name": "hive://secret/gcp-collector-sa"},
  "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.

{
  "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 Explore lens. The schedule and detection blocks are accepted (so IaC written today survives the scheduled-query phase) but inert.

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). D&R rules match these like any event; the Cases extension actions close the loop.

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.