Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions doc/cli/cloud-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,41 @@ Before the scan starts, the CLI refuses a document the scanner would not accept:

**Scanner version.** The default image is pinned to scanner v0.16.0. A `--image` or `--binary` running `sast` must be v0.16.0 or newer, because older scanners reject the rule-set flags. That failure is a usage error (exit 2), and the CLI's error message names the version you need. A scan without `sast` passes no rule-set flag, so it still runs on older scanners.

### IaC provenance selectors (CS-02)

When server-side provenance queries are enabled, finding list, facets, causes and
CSV export accept repeatable `--iac-attribution` values (`attributed`, `ambiguous`,
`none`, `unknown`). Findings and inventory list/facets/export accept
`--has-iac-origin` or `--no-has-iac-origin`. Omit both for no origin constraint.
The negative selector means no **recorded** origin evidence, not proof that a
resource has no IaC. Unknown, partial and stale evidence never establish safety.

```bash
limacharlie cloudsec finding list --iac-attribution unknown --no-has-iac-origin
limacharlie cloudsec inventory list --has-iac-origin
limacharlie cloudsec export findings --iac-attribution ambiguous
```

SDK equivalents use keyword arguments `iac_attribution=["unknown"]` and
`has_iac_origin=False`; explicit false is preserved on the wire. Malformed values
are rejected before HTTP. Selectors remain scoped to the bound organization.
Facets exclude their own selector. Resource detail may include bounded
`iac_origin` and `iac_origin_partial` evidence even for a clean resource. Source
permalinks are optional and commit-bound; missing revision metadata is unknown,
not a link to HEAD. These read-only clients do not fetch source URLs.

Rollout requires the compatible gateway and graph reader before use; a disabled
provenance server rejects the new selectors. Revert clients independently or omit
the selectors; no schema rollback or feature enablement is performed by this CLI.
Program: maximelb/claude-config#137, epic maximelb/claude-config#134.

New-selector requests require an exact `applied_iac_filters` receipt in JSON.
CSV responses carry a bounded first comment line with a base64url JSON receipt;
the SDK validates and removes it before returning CSV. Older or partly upgraded
servers that do not acknowledge the requested selectors raise an error instead
of presenting an unfiltered result. Selector-free calls remain unchanged.


## Build provenance

`limacharlie cloudsec code provenance push -f provenance.json` sends an LC
Expand Down
39 changes: 29 additions & 10 deletions limacharlie/commands/cloudsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -1216,12 +1216,21 @@ def _selector_with_empty(values, include_empty: bool) -> list[str] | None:
return out or None


def _iac_origin_option(f):
return click.option("--has-iac-origin/--no-has-iac-origin", default=None,
help="Filter recorded IaC origin evidence. No recorded evidence does not prove no IaC exists; omit for no constraint.")(f)


def _finding_filter_options(f):
"""The findings worklist filter selectors (shared by list/facets).

Click stacks decorators bottom-up, so the option that should show
first in --help is applied last.
"""
f = _iac_origin_option(f)
f = click.option("--iac-attribution", "iac_attributions", multiple=True,
type=click.Choice(["attributed", "ambiguous", "none", "unknown"]),
help="IaC attribution verdict; repeat up to four times. Missing evidence is unknown, never safe.")(f)
f = click.option(
"-q", "--search", "q", default=None,
help="Substring search over the findings.",
Expand Down Expand Up @@ -1347,6 +1356,7 @@ def _finding_filter_options(f):

def _inventory_filter_options(f):
"""The inventory filter selectors (shared by list/export)."""
f = _iac_origin_option(f)
f = click.option(
"--account-empty", "account_empty", is_flag=True, default=False,
help="Select only resources that have no cloud account.",
Expand Down Expand Up @@ -3111,7 +3121,7 @@ def finding_group() -> None:
@_sort_options
@_paging_options
@pass_context
def finding_list(ctx, severities, finding_classes, statuses, accounts, repos,
def finding_list(ctx, has_iac_origin, iac_attributions, severities, finding_classes, statuses, accounts, repos,
image_urns, fix_states, exploit_bands, grains,
source, owners, unassigned, sla_states, reachable, kev, q,
cause, sort, order, cursor, limit) -> None:
Expand All @@ -3128,6 +3138,8 @@ def finding_list(ctx, severities, finding_classes, statuses, accounts, repos,
"""
cs = _get_cloudsec(ctx)
_output(ctx, cs.list_findings(
has_iac_origin=has_iac_origin,
iac_attribution=list(iac_attributions) or None,
severity=list(severities) or None,
finding_class=list(finding_classes) or None,
status=list(statuses) or None,
Expand Down Expand Up @@ -3165,7 +3177,7 @@ def finding_list(ctx, severities, finding_classes, statuses, accounts, repos,
"slots with any --owner values, so past ~50 combined a "
"pin can still be dropped.")
@pass_context
def finding_facets(ctx, severities, finding_classes, statuses, accounts, repos,
def finding_facets(ctx, has_iac_origin, iac_attributions, severities, finding_classes, statuses, accounts, repos,
image_urns, fix_states, exploit_bands, grains,
source, owners, unassigned, sla_states, reachable, kev, q,
cause, owner_pins) -> None:
Expand All @@ -3178,6 +3190,8 @@ def finding_facets(ctx, severities, finding_classes, statuses, accounts, repos,
"""
cs = _get_cloudsec(ctx)
_output(ctx, cs.get_finding_facets(
has_iac_origin=has_iac_origin,
iac_attribution=list(iac_attributions) or None,
severity=list(severities) or None,
finding_class=list(finding_classes) or None,
status=list(statuses) or None,
Expand Down Expand Up @@ -3208,7 +3222,7 @@ def finding_facets(ctx, severities, finding_classes, statuses, accounts, repos,
help="Rollup size (default 20, cap 200). Not a page size — the "
"rollup is not paginated; 'distinct' reports the tail.")
@pass_context
def finding_causes(ctx, severities, finding_classes, statuses, accounts, repos,
def finding_causes(ctx, has_iac_origin, iac_attributions, severities, finding_classes, statuses, accounts, repos,
image_urns, fix_states, exploit_bands, grains,
source, owners, unassigned, sla_states, reachable, kev, q,
cause, limit) -> None:
Expand All @@ -3222,6 +3236,8 @@ def finding_causes(ctx, severities, finding_classes, statuses, accounts, repos,
cs = _get_cloudsec(ctx)
_output(ctx, cs.list_finding_causes(
cause=cause,
has_iac_origin=has_iac_origin,
iac_attribution=list(iac_attributions) or None,
severity=list(severities) or None,
finding_class=list(finding_classes) or None,
status=list(statuses) or None,
Expand Down Expand Up @@ -3503,7 +3519,7 @@ def inventory_group() -> None:
@_inventory_filter_options
@_paging_options
@pass_context
def inventory_list(ctx, resource_type, provider, account, region, q,
def inventory_list(ctx, has_iac_origin, resource_type, provider, account, region, q,
all_accounts, account_empty, cursor, limit) -> None:
"""List the cloud resource inventory.

Expand All @@ -3518,24 +3534,25 @@ def inventory_list(ctx, resource_type, provider, account, region, q,
cs = _get_cloudsec(ctx)
_output(ctx, cs.list_inventory(
resource_type=resource_type, provider=provider, account=account,
region=region, q=q,
region=region, q=q, has_iac_origin=has_iac_origin,
account_empty=_inventory_account_empty(
account, all_accounts, account_empty),
cursor=cursor, limit=limit,
))


@inventory_group.command("facets")
@_iac_origin_option
@pass_context
def inventory_facets(ctx) -> None:
def inventory_facets(ctx, has_iac_origin) -> None:
"""Inventory facet counts by type/account/region.

\b
Example:
limacharlie cloudsec inventory facets
"""
cs = _get_cloudsec(ctx)
_output(ctx, cs.get_inventory_facets())
_output(ctx, cs.get_inventory_facets(has_iac_origin=has_iac_origin))


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -4556,7 +4573,7 @@ def export_group() -> None:
@_sort_options
@_export_output_option
@pass_context
def export_findings(ctx, severities, finding_classes, statuses, accounts, repos,
def export_findings(ctx, has_iac_origin, iac_attributions, severities, finding_classes, statuses, accounts, repos,
image_urns, fix_states, exploit_bands, grains,
source, owners, unassigned, sla_states, reachable, kev, q,
cause, sort, order, output_path) -> None:
Expand All @@ -4570,6 +4587,8 @@ def export_findings(ctx, severities, finding_classes, statuses, accounts, repos,
"""
cs = _get_cloudsec(ctx)
_emit_csv(ctx, cs.export_findings_csv(
has_iac_origin=has_iac_origin,
iac_attribution=list(iac_attributions) or None,
severity=list(severities) or None,
finding_class=list(finding_classes) or None,
status=list(statuses) or None,
Expand All @@ -4595,7 +4614,7 @@ def export_findings(ctx, severities, finding_classes, statuses, accounts, repos,
@_inventory_filter_options
@_export_output_option
@pass_context
def export_inventory(ctx, resource_type, provider, account, region, q,
def export_inventory(ctx, has_iac_origin, resource_type, provider, account, region, q,
all_accounts, account_empty, output_path) -> None:
"""Export the (filtered) cloud resource inventory as CSV.

Expand All @@ -4607,7 +4626,7 @@ def export_inventory(ctx, resource_type, provider, account, region, q,
cs = _get_cloudsec(ctx)
_emit_csv(ctx, cs.export_inventory_csv(
resource_type=resource_type, provider=provider, account=account,
region=region, q=q,
region=region, q=q, has_iac_origin=has_iac_origin,
account_empty=_inventory_account_empty(
account, all_accounts, account_empty),
), output_path)
Expand Down
Loading
Loading