Control: Storage bucket IAM policies should only grant access to trusted principals
Description
This control checks whether Cloud Storage bucket IAM policies grant access to untrusted users, groups, domains, or service accounts.
Usage
Run the control in your terminal:
powerpipe control run gcp_perimeter.control.storage_bucket_policy_shared_accessSnapshot and share results via Turbot Pipes:
powerpipe loginpowerpipe control run gcp_perimeter.control.storage_bucket_policy_shared_access --shareSteampipe Tables
SQL
    with policy_analysis as (      select        name as resource_id,        -- Count all members        count(*) as total_members,        -- Count project-level members        count(*) filter (where member like 'project%') as project_members,        -- Count trusted members (excluding project-level)        count(*) filter (where           member not like 'project%' and (            (member like 'user:%' and split_part(member, 'user:', 2) = any(($1)::text[]))            or            (member like 'group:%' and split_part(member, 'group:', 2) = any(($2)::text[]))            or            (member like 'domain:%' and split_part(member, 'domain:', 2) = any(($3)::text[]))            or            (member like 'serviceAccount:%' and split_part(member, 'serviceAccount:', 2) = any(($4)::text[]))          )        ) as trusted_members,        -- Collect untrusted members for alarm messages        array_agg(distinct member) filter (where           member not like 'project%' and not (          (member like 'user:%' and split_part(member, 'user:', 2) = any(($1)::text[]))            or            (member like 'group:%' and split_part(member, 'group:', 2) = any(($2)::text[]))            or            (member like 'domain:%' and split_part(member, 'domain:', 2) = any(($3)::text[]))            or            (member like 'serviceAccount:%' and split_part(member, 'serviceAccount:', 2) = any(($4)::text[]))          )        ) as untrusted_members      from        gcp_storage_bucket,        jsonb_array_elements(iam_policy -> 'bindings') as binding,        jsonb_array_elements_text(binding -> 'members') as member      group by        name    )    select      r.name as resource,      case        -- SKIP: When no members exist        when (r.iam_policy -> 'bindings') is null or jsonb_array_length(r.iam_policy -> 'bindings') = 0 then 'skip'        -- INFO: When only project-level roles are assigned        when p.total_members = p.project_members then 'info'        -- OK: When all non-project members are trusted        when p.untrusted_members is null and (p.trusted_members > 0 or p.project_members > 0) then 'ok'        -- ALARM: When there are untrusted members        else 'alarm'      end as status,      case        when (r.iam_policy -> 'bindings') is null or jsonb_array_length(r.iam_policy -> 'bindings') = 0 then title || ' has no IAM policy members.'        when p.total_members = p.project_members then title || ' only has project-level role assignments (' || p.project_members || ' members).'        when p.untrusted_members is null and (p.trusted_members > 0 or p.project_members > 0) then title || ' policy only grants access to trusted principals (' || coalesce(p.trusted_members, 0) || ' trusted + ' || coalesce(p.project_members, 0) || ' project-level).'        else title || ' policy contains ' || coalesce(array_length(p.untrusted_members, 1), 0) || ' untrusted member(s): ' || array_to_string(p.untrusted_members, ', ')      end as reason,      r.project,      r.location    from      gcp_storage_bucket as r      left join policy_analysis as p on p.resource_id = r.name    where      -- Only check resources where we have access to IAM policy      r.iam_policy is not null;
Params
| Args | Name | Default | Description | Variable | 
|---|---|---|---|---|
| $1 | trusted_users |  | A list of trusted Google Account emails. | |
| $2 | trusted_groups |  | A list of trusted Google Groups. | |
| $3 | trusted_domains |  | A list of trusted Google Workspace domains. | |
| $4 | trusted_service_accounts |  | A list of trusted service accounts. |