turbot/steampipe-mod-gcp-perimeter

Control: Compute resource policy IAM policies should only grant access to trusted principals

Description

This control checks whether Compute resource 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.compute_resource_policy_shared_access

Snapshot and share results via Turbot Pipes:

powerpipe login
powerpipe control run gcp_perimeter.control.compute_resource_policy_shared_access --share

Steampipe 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_compute_resource_policy,
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_compute_resource_policy 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

ArgsNameDefaultDescriptionVariable
$1trusted_users
["user1@example.com","user2@example.com"]
A list of trusted Google Account emails.
$2trusted_groups
["admins@googlegroups.com","developers@googlegroups.com"]
A list of trusted Google Groups.
$3trusted_domains
["trusted-company.com","trusted-partner.com"]
A list of trusted Google Workspace domains.
$4trusted_service_accounts
["app-sa@project-id.iam.gserviceaccount.com"]
A list of trusted service accounts.

Tags