Automation & Infrastructure-as-Code¶
Everything Cloud Security does is scriptable: configuration is Hive records, the query/triage surface is the REST API and CLI, and findings flow through the standard event pipeline. This page collects the operator recipes.
Onboarding a tenant¶
# 1. Subscribe the org to the extension (billing/enable gate).
limacharlie extension subscribe --name ext-cloud-inventory --oid $OID
# 2. Store the collector credential as a secret (hive set reads
# record data from --input-file or piped stdin).
echo '{"secret": "<service-account-key-json>"}' | \
limacharlie hive set --hive-name secret --key gcp-collector-sa \
--oid $OID --enabled
# 3. Connect the provider.
cat > provider.json <<EOF
{
"provider_type": "gcp",
"gcp_scope": "organizations/123456789",
"credentials": "hive://secret/gcp-collector-sa",
"internal_domains": ["acme.com"]
}
EOF
limacharlie hive set --hive-name cloudsec_provider --key acme-gcp \
--oid $OID --input-file provider.json --enabled
# 4. 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¶
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; 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¶
Save a graph query as a cloudsec_query record and it appears in every
teammate's query console:
{
"version": 1,
"name": "Exposed VMs reaching sensitive data",
"query": {"text": "..."},
"tags": ["weekly"]
}
See Configuration for the full record shape.
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 (filter 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.
CLI --output csv is per-page
The CLI's global --output csv formats the rows the command returned —
one page. For a full-estate export use the ?format=csv server-side
walk above.
Findings ↔ Cases automation¶
Cloud findings emit lifecycle events into the organization's own event
stream (see the emission policy):
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.
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 and key your tickets on fingerprint the same
way.