API Reference¶
All Cloud Security routes live under
https://api.limacharlie.io/v1/cloudsec/{oid}/… and appear in the public
OpenAPI spec at /openapi.
Authentication is the standard Authorization: Bearer <JWT> header.
Permissions & enable gate
Reads — and the read-only preview POSTs (query, simulate/resources,
simulate/findings, policy/suggest) — require cloudsec.get; every
other write requires cloudsec.set. Every route requires the
organization to be subscribed to ext-cloud-security — a 403 on any
route means subscribe first. The oid is always taken from the
authorized path. Provider records are not /cloudsec routes: their
CRUD goes through Hive (cloudsec_provider hive, gated by
cloudsec_provider.get/set/del).
Shared behaviors:
- Repeatable filters (
severity,finding_class,status,account, and the identity / Data Security / CAASM selectors listed per route below) are passed as repeated query keys:?severity=CRITICAL&severity=HIGH. OR within a key, AND across keys. A read filter takes at most 100 values per key and quietly drops the rest; a write body over its own cap is rejected outright rather than truncated, so a partial write is never silently reported as a whole one. Thesidandurnkeys on the/resolve/*routes are not filters but bulk lookup keys, with their own 500-per-request cap. - Boolean selectors are tri-state: an absent parameter means "not
filtered", which is not the same as passing
false. Omit it for no constraint; passfalseto pin the negative case. - Keyset pagination: pages carry
next_cursor; pass it back as?cursor=.limitis clamped to 1000. - CSV export: exactly four routes accept
?format=csvto stream the full filtered set as a CSV attachment —findings,inventory, andcompliance(GET), andquery(POST). The stream is a server-side keyset walk, capped at 100,000 rows, with an in-band#-comment truncation notice when the cap is hit.
Reads¶
| Route | Returns |
|---|---|
GET /findings |
{findings, next_cursor} — the risk-ranked worklist. Filters: severity[], finding_class[], status[], account[], reachable, kev, q, sort, order, cursor, limit. |
GET /findings/facets |
{facets} — cross-filtered facet counts under the same selectors. |
GET /findings/classes |
{classes} — the canonical finding_class enum (the 11 values), for building selectors. |
GET /findings/causes |
{causes, distinct} — findings grouped by their cause, the mutable object (a firewall rule, a role binding) whose single edit resolves all of them. Takes the full findings selectors, plus cause for the exact count of one cause and limit (default 20, max 200) for the top causes; distinct is the total number of causes matching the filter. |
GET /findings/{finding_id} |
{finding} — one finding in full. |
GET /attack-paths |
{paths} — marquee toxic-combination paths. Filters: severity[], account[], status[], q. |
GET /chokepoints |
{chokepoints, total_paths} — shared attack-path hops ranked by paths broken, including the principal-exposure metrics. |
GET /ciem/public-access |
{access, identity_kpis, principals} — public/external access to sensitive resources, the identity-posture headline counts, and the risk-ranked per-principal effective-access rollup (top 500). The two extra blocks are best-effort: a read failure omits the key rather than failing the call. |
GET /ciem/facets |
{facets} — identity facet counts. |
GET /ciem/identities |
{principals, next_cursor} — the same per-principal effective-access rows public-access carries, but server-filtered and keyset-paginated instead of a top-N. Takes the same selectors as /ciem/facets, so the rail's counts always describe the population in the list. Ranked by risk score by default, so a walk that spans a projector rebuild can move a row across the cursor — use it for browsing, and /inventory with type=Identity and the default urn sort for an exact export. |
GET /ciem/identity?urn= |
{identity} — the Identity 360 single-identity rollup for one principal URN: its grants, reachable sensitive resources, access levels, and escalation paths. |
GET /inventory |
{resources, next_cursor}. Filters: type, provider, account, region, q, account_unscoped (drop the account scoping and walk the whole estate), sort (urn, the default and the safe order for a full walk or export; risk with type=Identity; last_seen with type=ThirdPartyAsset), paging. With type=Identity, provider/source, account, and region become repeatable and the identity cross-filter applies: kind, criticality, risk_band, mfa (on/off/unknown), and the tri-state admin, external, public, disabled, crown_jewel, can_escalate, dormant_90d, with_sensitive. Pages served from the materialized view also carry served_from and data_as_of (the snapshot's build time) so a client can render "as of" instead of implying live. |
GET /inventory/facets |
Inventory facet counts by type/account/region. |
GET /data-security/facets |
{facets} — DSPM data-store rollup. |
GET /data-security/stores |
{stores, next_cursor} — the filtered, paginated data-store list, under the same selectors as data-security/facets: provider, account, region, store_kind, tier, and the tri-state sensitivity / exposure. Served from the materialized graph, so the list stays exact at any estate size. |
GET /resource?urn= |
{resource} — the canonical record for any URN (null when unknown). |
GET /graph/neighbors?urn= |
{graph: {nodes, edges}} — 1-hop expansion; limit default 200, cap 500; truncated flag. |
GET /topology |
{scopes, edges, available, generated_at} — the server-side estate aggregate behind the Topology diagram (scope nodes + the edges between them), with the unix time the aggregate was built. available is false only when the aggregate has not been materialized for this organization yet (the client should fall back to its own walk); a genuinely empty estate answers available: true with empty scopes and edges. |
GET /policy/vocabulary |
The classification-policy vocabulary, as a flat object: surfaces (the per-surface matcher-capability table), resource_types (data_stores, compute, identities), content_classes, providers, tiers, suggested_classes, and in_use — the org's in-use value histograms. |
GET /providers/manifest?type= |
{manifest} — the per-provider coverage manifest ("what you get"): the resource kinds, edges, and checks a given provider_type produces. Omit type to get {manifests}, the manifest for every supported provider. |
GET /queries |
{queries} — the named query pack (name, title, description, query). |
POST /query |
{rows} — run a graph query. Body: exactly one of named / text / query (DSL object), optional project. With project: "graph" the response also carries graph: {nodes, edges}, the subgraph induced over the matched URNs — or truncated: true in its place when the result spans more than 500 nodes (the rows are complete either way; only the drawing is capped). A shortest-path query answers {paths: [{anchor, target, hops, path}]} instead of rows, and its project: "graph" draws the whole kill chain, adding graph_partial when only some paths are drawn and graph_unavailable when none can be. |
GET /compliance |
{report} — per-control assessment. Params: framework (default cis-gcp) or assignment (overrides framework). |
GET /compliance/frameworks |
{frameworks} — id, name, version, control counts. |
GET /compliance/assignments |
{assignments} — scoped assignments with per-assignment scores. |
GET /overview |
Composed risk overview in one round-trip: score, severity_counts, open_count, reachable_open, reachable_criticals, top_paths, coverage, trend, recent_changes, the per-tenant usage metering block, and top_issue when there is one. Also carries estate_shape (cloud | identity | hybrid) and, for an identity or hybrid estate, an identity_summary posture headline — both best-effort and omitted on a read failure. Param: trend_days (default 30). |
GET /changes |
{changes} — created/closed feed, newest first. Param: limit (default 50, clamped to 1000). |
GET /risk-trend |
{trend} — score history, oldest first. Param: trend_days. |
GET /scan-status |
Collection run state. With provider set: {status} for that provider — ids are lowercase and the lookup is case-sensitive, so an unknown or miscased id reads as never-scanned. With provider omitted: {coverage, workload, status} — per-provider run state for every connected cloud provider, status as the first cloud provider's state for backward compatibility, and a reserved workload section. There is no default provider. |
GET /resolve/sensors?sid=… |
{resolved, unresolved} — sensor → cloud asset, bulk via repeated sid (server cap 500/request; keep URLs under ~8KB — the CLI/SDK chunks automatically). |
GET /resolve/assets?urn=… |
{resolved, unresolved} — cloud asset → sensors, bulk via repeated urn (same caps and chunking). |
GET /caasm/assets |
{resources, next_cursor} — the merged third-party asset inventory. Params: q, kind[], source[] (which observing tool reported the asset), the four repeatable device-posture filters posture_encryption, posture_screen_lock, posture_compromised, posture_managed, sort (urn default, or last_seen), paging. Pass an empty value to a posture filter to select the assets no source reported that fact for — unreported is not the same as compliant. Also carries served_from / data_as_of when the page came from the materialized merged view. |
GET /caasm/coverage |
{findings, next_cursor} — coverage-gap findings. Params: status[], severity[], q, sort, order, paging. |
GET /caasm/policy |
Resource-list shape: zero rows (no policy) or one row whose props is the policy. |
GET /free-tier |
{is_free_tier, sensor_quota, max_providers, enabled_providers} — the org's free-tier standing and the limits that apply, for surfacing an upgrade prompt before a limit is hit. Descriptive only: it is not a gate, the collector enforces the limits, and reads keep working after a trial expires. |
ciem/* and providers/* families
ciem/* has four members — public-access, facets, identities, and
identity (singular: one principal). providers/* has two — test
(POST, below) and manifest (GET, above). Creating, editing, and
deleting provider records is not a /cloudsec route: it goes through the
cloudsec_provider Hive.
Fleet (multi-org)¶
One route sits outside the {oid} path — the MSSP cross-tenant board:
| Route | Returns |
|---|---|
GET /cloudsec/fleet/overview |
{orgs, rollups, total_orgs, skipped, next_cursor} — risk posture rolled up across many organizations. skipped splits the tenants that could not be included into not_enabled (no ext-cloud-security subscription) and lookup_failed (a transient check failure, so one bad tenant never fails the whole call). Params: oids[] (repeat to select tenants), group (a saved fleet group), trend_days, cursor, limit (default 25, cap 100). Requires cloudsec.get on each tenant in scope — organizations you lack it on are simply left out, not reported as skipped. A single call may resolve to at most 500 organizations (past that, narrow with explicit oids or a smaller group), and the route carries its own quota of 120 calls per hour. |
Writes¶
All writes are POST with a JSON body and require cloudsec.set.
| Route | Body | Returns |
|---|---|---|
/findings/{finding_id}/status |
{resolution: {kind, reason, expires_at}} — kind is mitigated | accepted | false_positive, or open to clear the disposition and reopen. expires_at is unix seconds (meaningful with accepted). |
{ok} |
/findings/bulk/status |
{finding_ids: [...], resolution: {...}} — one resolution applied to many findings. Unlike the single-finding route, kind must be mitigated | accepted | false_positive — the bulk route does not accept open (reopen findings one at a time). At most 500 ids per call: an over-cap batch is rejected, not truncated, so split larger sets yourself rather than have a prefix dispositioned silently. |
{ok, updated} |
/findings/{finding_id}/owner |
{owner} — empty string clears. |
{ok} |
/findings/{finding_id}/ticket |
{ticket} — empty string clears. |
{ok} |
/chokepoints/dismiss |
{urn, reason?} |
{ok} |
/chokepoints/restore |
{urn} |
{ok} |
/caasm/policy |
{policy: {expect: [{label, capability, kinds}]}} — validated before storing. |
{ok} |
/caasm/ingest |
{source, records?: [...], record?: {...}, policy?: {...}} — source required; a single record is treated as a one-element batch. Body capped at 1 MiB; ingestion is idempotent. |
{result: {received, normalized, skipped, assets, created, updated, deleted, policy_set}} |
/providers/test |
{provider: <cloudsec_provider record>} — credential inline (ephemeral, never stored) or a hive://secret/<name> reference. |
{supported, report: {provider, ok, checks: [{id, name, required, ok, detail}]}}. A provider type with no preflight implemented answers {supported: false, report: null} rather than an error — treat it as "cannot verify", not "credential bad". |
Example — disposition a finding:
curl -X POST -H "Authorization: Bearer $JWT" -H "Content-Type: application/json" \
"https://api.limacharlie.io/v1/cloudsec/$OID/findings/fnd_0a1b.../status" \
-d '{"resolution": {"kind": "accepted", "reason": "SEC-123", "expires_at": 1767225600}}'
Policy authoring: Simulate & vocabulary¶
These POST routes back the console's policy-authoring aids (matcher
preview and value autocomplete). They never mutate state, so they require
only cloudsec.get. On the CLI they are limacharlie cloudsec simulate
resources / simulate findings, limacharlie cloudsec policy suggest,
and limacharlie cloudsec policy vocabulary.
| Route | Body | Returns |
|---|---|---|
POST /simulate/resources |
{rules, target, resource_types?, sample_limit?} — rules is a list of matcher rules, target scopes the walked surface (data_store, compute, identity, or any — the default). sample_limit defaults to 25, cap 100. |
{evaluated, matched, indeterminate, truncated, sample, indeterminate_sample} — preview a classification / coverage / exclusion matcher against stored inventory, with a bounded sample per bucket. |
POST /simulate/findings |
{match, sample_limit?} — a suppression matcher. An empty match is allowed: it matches everything up to the default severity ceiling, and showing that blast radius is the point of the preview. |
{evaluated, matched, partial, indeterminate, truncated, sample, partial_sample, indeterminate_sample} — preview it against currently-open findings. partial counts vulnerability rollups the rule would shrink (some affected resources accepted, the finding stays open); indeterminate counts rollups whose bounded target sample cannot decide. |
POST /policy/suggest |
{dimension, q, target, limit?} — dimension is name or account; q is required and at most 256 bytes (longer is a 400); target takes the same surface enum as simulate/resources; limit defaults to 20, cap 50. |
{values: [{value, count}], truncated, evaluated} — live matcher-value autocomplete drawn from the tenant estate. |
Both simulations are bounded walks: they stop at 50,000 rows or 15 seconds and
report truncated: true with the counts so far, so a preview on a large estate
never turns into an unbounded scan.
Pair these with GET /policy/vocabulary (the closed vocabularies and
per-surface capability table) and GET /findings/classes (the
finding_class enum) when building a classification or suppression policy.
Finding shape¶
The finding object (list, get, and the cloud_finding.created event all
carry the same shape) — key fields:
| Field | Meaning |
|---|---|
finding_id, fingerprint |
Stable identity of the condition (fnd_ + fingerprint prefix). |
rule_id, finding_class, classification |
What detected it and its class. The canonical enum is toxic_combination, public_exposure, ciem_risk, privilege_escalation, vulnerability, misconfig, malware, secret, scan_finding, coverage_gap, device_posture (malware, secret, and scan_finding are reserved for capabilities not yet enabled). workload_coverage_gap, the cloud-workload flavor of a coverage gap, can additionally appear on a finding even though it is not one of the 11 picker values. |
severity, lc_risk, risk_breakdown |
CRITICAL–INFO, the 0–1000 composite, and its explanation. |
title, resource_urn, resource_name, resource_type, account, region |
The affected resource. |
related_urns, path, path_kind |
Related resources; the hop list for path findings and the kind of path it represents. |
source_scope, target_scope |
For attack-path / toxic findings, the durable workload group at each end (GKE/EKS/AKS node pool, MIG, ASG, VMSS) — group_urn, group_kind, group_name (the human label), member_count (the group's full size), affected_count (the members actually on paths), and members, a sample of those affected members bounded at 20 — so the finding headlines the shared-fix group, not a churny ephemeral node. |
cause |
The mutable object that actually gets edited to fix the finding — kind, urn, name. The shared-fix grouping key behind GET /findings/causes. |
reachable, in_kev, vulns, epss, epss_percentile |
Exposure and exploit intelligence; vulns entries carry cve, package, package_version, fix_version, cvss_score. |
pivot_targets, target_count |
For rollup findings, a bounded sample (20) of the affected targets and the true total, so a rollup states its own blast radius without carrying every row. |
evidence, remediation |
The offending configuration and the fix. |
ciem_access |
For identity findings: role, identity kind, public/external/privileged flags, grant path, and the classified data-plane access level / capability (data_admin > data_write > data_read > metadata > none) the grant confers. A sixth value, unknown, is the unverified escape hatch: it ranks below none and deliberately fails every "at least" threshold, so handle it explicitly rather than by ordinal comparison. It still counts as potential data access for the "reaches sensitive data" gate, flagged unverified instead of silently dropped. |
ciem_escalation |
For privilege-escalation findings, the escalation detail: from_urn, admin_urn, and the ordered identity path. |
business_criticality, environment, tags |
Business context carried from the resource, for routing and prioritization. |
runtime_sids |
Sensors resolved onto the affected asset. |
status, resolution, resolved_by, resolution_expires_at, owner, ticket |
The triage overlay. |
first_seen, last_seen |
Lifecycle timestamps. |
Events¶
The platform emits lifecycle events into the organization's event stream. With
no emission policy record, finding
events are on and resource + operational events are off — an org gets
the finding feed without configuring anything, and opts in to the noisier
families. The policy also takes a severity_floor that drops finding events
below a given severity (the created/updated/closed feed only — the first-sync
summary still counts the whole estate).
Detection-truth lifecycle (from the projector):
cloud_finding.created— the full finding (includingruntime_sids) underevent/finding.cloud_finding.updated— an open finding materially changed (severity flip, vuln set). Payload{finding_id, fingerprint, finding_class, changed, old_severity, new_severity, finding}.cloud_finding.closed—{finding_id, fingerprint, finding_class}.cloud_finding.still_open— re-asserted at most once a day for open findings with a linked ticket (the case-sync verb); the payload carries theticketalongside the identity fields.
Disposition — flat payload
{finding_id, fingerprint, finding_class, actor, note?}, plus the finding's
post-disposition status and resolution on the operator-written ones:
cloud_finding.resolved,cloud_finding.dismissed,cloud_finding.reopened,cloud_finding.assigned.
Two things emit these verbs, and actor is what tells them apart: an operator
triage write carries the user, while a
suppression policy
applying automatically carries policy:<rule name> (and plain policy when a
rule stops matching and releases its findings). A broad first application is
capped per pass, so a very large policy rollout summarizes rather than emitting
one event per finding.
The verb alone is deliberately coarse: both a mitigated and an accepted
disposition emit cloud_finding.resolved, because minting a new verb would
break consumers filtering on a known set. Branch on resolution — not on
event_type — to tell a risk somebody signed off on carrying from one that was
actually fixed.
Summary & inventory:
cloudsec.sync_completed— emitted once on the first-ever (or rebuilt) projection in place of a per-findingcreatedflood; payload{total, by_class, by_severity}.cloud_resource.created,cloud_resource.updated,cloud_resource.deleted— inventory change events (off by default).
Scheduled queries: a cloudsec_query record with schedule.scheduled set
emits cloud_query.match once per new anchor entering its result set and
cloud_query.resolved once per anchor leaving — the same lifecycle symmetry as
findings — plus cloud_query.overbudget once on each transition into the
over-budget state (rather than a match flood).
Operational: cloudsec.sweep_failed reports a collection pass that
failed. Off by default; opt in with the emission policy's ops_events.
Every payload also carries event_type and oid.
See Automation & IaC for D&R rules that consume them.