mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-04 02:04:06 +00:00
feat(rolesanywhere): flag profiles with unscoped sessions (#12416)
This commit is contained in:
+762
@@ -0,0 +1,762 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest import mock
|
||||
|
||||
from prowler.providers.aws.services.rolesanywhere.rolesanywhere_service import Profile
|
||||
from tests.providers.aws.utils import (
|
||||
AWS_ACCOUNT_NUMBER,
|
||||
AWS_REGION_US_EAST_1,
|
||||
set_mocked_aws_provider,
|
||||
)
|
||||
|
||||
PROFILE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
PROFILE_NAME = "workload-profile"
|
||||
PROFILE_ARN = f"arn:aws:rolesanywhere:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:profile/{PROFILE_ID}"
|
||||
ADMIN_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/admin-role"
|
||||
READONLY_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/workload-role"
|
||||
UNKNOWN_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/cross-account-role"
|
||||
CUSTOM_ADMIN_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/custom-admin-role"
|
||||
INLINE_ADMIN_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/inline-admin-role"
|
||||
NAME_COLLISION_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/name-collision-role"
|
||||
UNRESOLVED_POLICY_ROLE_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/unresolved-policy-role"
|
||||
)
|
||||
ADMIN_UNRESOLVED_POLICY_ROLE_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/admin-unresolved-policy-role"
|
||||
)
|
||||
INVALID_POLICY_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/invalid-policy-role"
|
||||
DENY_OVERRIDE_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/deny-override-role"
|
||||
CONDITIONAL_DENY_ROLE_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/conditional-deny-role"
|
||||
)
|
||||
CONDITIONAL_ADMIN_ROLE_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/conditional-admin-role"
|
||||
)
|
||||
BOUNDED_ADMIN_ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/bounded-admin-role"
|
||||
UNRESOLVED_BOUNDARY_ROLE_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/unresolved-boundary-role"
|
||||
)
|
||||
ADMIN_BOUNDED_ROLE_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/admin-bounded-admin-role"
|
||||
)
|
||||
AWS_ADMIN_POLICY_ARN = "arn:aws:iam::aws:policy/AdministratorAccess"
|
||||
CUSTOMER_ADMIN_NAMED_POLICY_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/AdministratorAccess"
|
||||
)
|
||||
CUSTOM_ADMIN_POLICY_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/custom-admin"
|
||||
MANAGED_POLICY_ARN = "arn:aws:iam::aws:policy/ReadOnlyAccess"
|
||||
CUSTOMER_FULL_ACCESS_POLICY_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/full-access-session"
|
||||
)
|
||||
BOUNDARY_POLICY_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/scoped-boundary"
|
||||
UNRESOLVED_BOUNDARY_POLICY_ARN = (
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/unresolved-boundary"
|
||||
)
|
||||
UNRESOLVED_SESSION_POLICY_ARN = (
|
||||
"arn:aws:iam::aws:policy/job-function/SupportUser" # not in iam_client.policies
|
||||
)
|
||||
|
||||
SESSION_POLICY = (
|
||||
'{"Version":"2012-10-17","Statement":[{"Effect":"Allow",'
|
||||
'"Action":["s3:GetObject"],"Resource":["*"]}]}'
|
||||
)
|
||||
FULL_ACCESS_SESSION_POLICY = (
|
||||
'{"Version":"2012-10-17","Statement":[{"Effect":"Allow",'
|
||||
'"Action":"*","Resource":"*"}]}'
|
||||
)
|
||||
|
||||
FULL_ACCESS_DOCUMENT = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{"Effect": "Allow", "Action": "*", "Resource": "*"}],
|
||||
}
|
||||
READONLY_DOCUMENT = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{"Effect": "Allow", "Action": "s3:Get*", "Resource": "*"}],
|
||||
}
|
||||
DENY_ALL_DOCUMENT = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [{"Effect": "Deny", "Action": "*", "Resource": "*"}],
|
||||
}
|
||||
CONDITIONAL_ADMIN_DOCUMENT = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "*",
|
||||
"Resource": "*",
|
||||
"Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}},
|
||||
}
|
||||
],
|
||||
}
|
||||
CONDITIONAL_DENY_DOCUMENT = {
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Deny",
|
||||
"Action": "*",
|
||||
"Resource": "*",
|
||||
"Condition": {"StringNotEquals": {"aws:PrincipalTag/team": "security"}},
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _profile(
|
||||
*,
|
||||
enabled: bool = True,
|
||||
session_policy: str = "",
|
||||
managed_policy_arns=None,
|
||||
role_arns=None,
|
||||
):
|
||||
return Profile(
|
||||
arn=PROFILE_ARN,
|
||||
id=PROFILE_ID,
|
||||
name=PROFILE_NAME,
|
||||
region=AWS_REGION_US_EAST_1,
|
||||
enabled=enabled,
|
||||
role_arns=role_arns if role_arns is not None else [READONLY_ROLE_ARN],
|
||||
session_policy=session_policy,
|
||||
managed_policy_arns=managed_policy_arns or [],
|
||||
)
|
||||
|
||||
|
||||
def _role(arn, attached_policies=None, inline_policies=None, permissions_boundary=None):
|
||||
return SimpleNamespace(
|
||||
arn=arn,
|
||||
attached_policies=attached_policies or [],
|
||||
inline_policies=inline_policies or [],
|
||||
permissions_boundary=permissions_boundary,
|
||||
)
|
||||
|
||||
|
||||
def _iam_client():
|
||||
"""IAM client stub mirroring iam_service models: roles with attached/inline
|
||||
policies and a policies dict keyed by ARN (inline keyed {role_arn}:policy/{name}).
|
||||
"""
|
||||
roles = [
|
||||
_role(
|
||||
ADMIN_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "AdministratorAccess", "PolicyArn": AWS_ADMIN_POLICY_ARN}
|
||||
],
|
||||
),
|
||||
_role(
|
||||
READONLY_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "ReadOnlyAccess", "PolicyArn": MANAGED_POLICY_ARN}
|
||||
],
|
||||
),
|
||||
_role(
|
||||
CUSTOM_ADMIN_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "custom-admin", "PolicyArn": CUSTOM_ADMIN_POLICY_ARN}
|
||||
],
|
||||
),
|
||||
_role(INLINE_ADMIN_ROLE_ARN, inline_policies=["inline-admin"]),
|
||||
_role(
|
||||
NAME_COLLISION_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{
|
||||
"PolicyName": "AdministratorAccess",
|
||||
"PolicyArn": CUSTOMER_ADMIN_NAMED_POLICY_ARN,
|
||||
}
|
||||
],
|
||||
),
|
||||
_role(
|
||||
UNRESOLVED_POLICY_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{
|
||||
"PolicyName": "unresolved",
|
||||
"PolicyArn": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/unresolved",
|
||||
}
|
||||
],
|
||||
),
|
||||
# Proven admin attached policy combined with an unresolved one: the
|
||||
# unresolved document could contain a deny, so the outcome is unknown.
|
||||
_role(
|
||||
ADMIN_UNRESOLVED_POLICY_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{
|
||||
"PolicyName": "AdministratorAccess",
|
||||
"PolicyArn": AWS_ADMIN_POLICY_ARN,
|
||||
},
|
||||
{
|
||||
"PolicyName": "unresolved",
|
||||
"PolicyArn": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/unresolved",
|
||||
},
|
||||
],
|
||||
),
|
||||
# Attached policy resolves to a malformed (non-dict) document.
|
||||
_role(
|
||||
INVALID_POLICY_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{
|
||||
"PolicyName": "invalid",
|
||||
"PolicyArn": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/invalid",
|
||||
}
|
||||
],
|
||||
),
|
||||
# Allow *:* in the attached policy negated by an unconditional Deny *:*
|
||||
# in an inline policy: not effectively administrative.
|
||||
_role(
|
||||
DENY_OVERRIDE_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "custom-admin", "PolicyArn": CUSTOM_ADMIN_POLICY_ARN}
|
||||
],
|
||||
inline_policies=["deny-all"],
|
||||
),
|
||||
# Allow *:* in the attached policy plus a Condition-guarded Deny: the
|
||||
# deny may or may not apply, so the outcome is unknown.
|
||||
_role(
|
||||
CONDITIONAL_DENY_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "custom-admin", "PolicyArn": CUSTOM_ADMIN_POLICY_ARN}
|
||||
],
|
||||
inline_policies=["conditional-deny"],
|
||||
),
|
||||
# Allow *:* guarded by a Condition: not statically provable as admin.
|
||||
_role(
|
||||
CONDITIONAL_ADMIN_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{
|
||||
"PolicyName": "conditional-admin",
|
||||
"PolicyArn": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/conditional-admin",
|
||||
}
|
||||
],
|
||||
),
|
||||
# Administrative identity policies constrained by a restrictive
|
||||
# permissions boundary: not effectively administrative.
|
||||
_role(
|
||||
BOUNDED_ADMIN_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "AdministratorAccess", "PolicyArn": AWS_ADMIN_POLICY_ARN}
|
||||
],
|
||||
permissions_boundary={
|
||||
"PermissionsBoundaryType": "Policy",
|
||||
"PermissionsBoundaryArn": BOUNDARY_POLICY_ARN,
|
||||
},
|
||||
),
|
||||
# Boundary present but its document is not in the IAM inventory:
|
||||
# restrictions cannot be evaluated, so the role is not classified admin.
|
||||
_role(
|
||||
UNRESOLVED_BOUNDARY_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "AdministratorAccess", "PolicyArn": AWS_ADMIN_POLICY_ARN}
|
||||
],
|
||||
permissions_boundary={
|
||||
"PermissionsBoundaryType": "Policy",
|
||||
"PermissionsBoundaryArn": UNRESOLVED_BOUNDARY_POLICY_ARN,
|
||||
},
|
||||
),
|
||||
# AdministratorAccess as the boundary does not restrict anything.
|
||||
_role(
|
||||
ADMIN_BOUNDED_ROLE_ARN,
|
||||
attached_policies=[
|
||||
{"PolicyName": "AdministratorAccess", "PolicyArn": AWS_ADMIN_POLICY_ARN}
|
||||
],
|
||||
permissions_boundary={
|
||||
"PermissionsBoundaryType": "Policy",
|
||||
"PermissionsBoundaryArn": AWS_ADMIN_POLICY_ARN,
|
||||
},
|
||||
),
|
||||
]
|
||||
policies = {
|
||||
AWS_ADMIN_POLICY_ARN: SimpleNamespace(document=FULL_ACCESS_DOCUMENT),
|
||||
CUSTOM_ADMIN_POLICY_ARN: SimpleNamespace(document=FULL_ACCESS_DOCUMENT),
|
||||
MANAGED_POLICY_ARN: SimpleNamespace(document=READONLY_DOCUMENT),
|
||||
# Customer-managed policy that merely shares the AdministratorAccess name.
|
||||
CUSTOMER_ADMIN_NAMED_POLICY_ARN: SimpleNamespace(document=READONLY_DOCUMENT),
|
||||
f"{INLINE_ADMIN_ROLE_ARN}:policy/inline-admin": SimpleNamespace(
|
||||
document=FULL_ACCESS_DOCUMENT
|
||||
),
|
||||
f"{DENY_OVERRIDE_ROLE_ARN}:policy/deny-all": SimpleNamespace(
|
||||
document=DENY_ALL_DOCUMENT
|
||||
),
|
||||
f"{CONDITIONAL_DENY_ROLE_ARN}:policy/conditional-deny": SimpleNamespace(
|
||||
document=CONDITIONAL_DENY_DOCUMENT
|
||||
),
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/invalid": SimpleNamespace(
|
||||
document="invalid"
|
||||
),
|
||||
f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:policy/conditional-admin": SimpleNamespace(
|
||||
document=CONDITIONAL_ADMIN_DOCUMENT
|
||||
),
|
||||
BOUNDARY_POLICY_ARN: SimpleNamespace(document=READONLY_DOCUMENT),
|
||||
# Customer-managed session policy whose document grants *:*.
|
||||
CUSTOMER_FULL_ACCESS_POLICY_ARN: SimpleNamespace(document=FULL_ACCESS_DOCUMENT),
|
||||
}
|
||||
iam = mock.MagicMock()
|
||||
iam.roles = roles
|
||||
iam.policies = policies
|
||||
return iam
|
||||
|
||||
|
||||
def _build_client(profiles):
|
||||
ra_client = mock.MagicMock()
|
||||
ra_client.profiles = profiles
|
||||
return ra_client
|
||||
|
||||
|
||||
def _patched(ra_client):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
check_module = "prowler.providers.aws.services.rolesanywhere.rolesanywhere_profile_restricts_session_permissions.rolesanywhere_profile_restricts_session_permissions"
|
||||
return [
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=aws_provider,
|
||||
),
|
||||
mock.patch(f"{check_module}.rolesanywhere_client", new=ra_client),
|
||||
mock.patch(f"{check_module}.iam_client", new=_iam_client()),
|
||||
]
|
||||
|
||||
|
||||
def _enter(patches):
|
||||
from contextlib import ExitStack
|
||||
|
||||
stack = ExitStack()
|
||||
for p in patches:
|
||||
stack.enter_context(p)
|
||||
return stack
|
||||
|
||||
|
||||
def _run():
|
||||
from prowler.providers.aws.services.rolesanywhere.rolesanywhere_profile_restricts_session_permissions.rolesanywhere_profile_restricts_session_permissions import (
|
||||
rolesanywhere_profile_restricts_session_permissions,
|
||||
)
|
||||
|
||||
return rolesanywhere_profile_restricts_session_permissions().execute()
|
||||
|
||||
|
||||
class Test_rolesanywhere_profile_restricts_session_permissions:
|
||||
def test_no_profiles(self):
|
||||
with _enter(_patched(_build_client({}))):
|
||||
assert len(_run()) == 0
|
||||
|
||||
def test_unscoped_profile_with_admin_role_fails(self):
|
||||
with _enter(
|
||||
_patched(_build_client({PROFILE_ARN: _profile(role_arns=[ADMIN_ROLE_ARN])}))
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id == PROFILE_ID
|
||||
assert result[0].resource_arn == PROFILE_ARN
|
||||
assert result[0].region == AWS_REGION_US_EAST_1
|
||||
assert ADMIN_ROLE_ARN in result[0].status_extended
|
||||
assert "administrative" in result[0].status_extended
|
||||
|
||||
def test_unscoped_profile_with_custom_admin_policy_fails(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[CUSTOM_ADMIN_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert CUSTOM_ADMIN_ROLE_ARN in result[0].status_extended
|
||||
|
||||
def test_unscoped_profile_with_inline_admin_policy_fails(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[INLINE_ADMIN_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert INLINE_ADMIN_ROLE_ARN in result[0].status_extended
|
||||
|
||||
def test_mixed_roles_fail_lists_only_admin_role(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
role_arns=[READONLY_ROLE_ARN, ADMIN_ROLE_ARN]
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert ADMIN_ROLE_ARN in result[0].status_extended
|
||||
assert READONLY_ROLE_ARN not in result[0].status_extended
|
||||
|
||||
def test_customer_policy_named_administratoraccess_passes(self):
|
||||
# Name collision: customer-managed policy called AdministratorAccess
|
||||
# whose document is read-only must not flag the role as administrative.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[NAME_COLLISION_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_unresolved_attached_policy_is_manual(self):
|
||||
# Attached policy ARN missing from iam_client.policies: a missing
|
||||
# document is unknown, not proof that the role is unprivileged.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[UNRESOLVED_POLICY_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "could not be evaluated" in result[0].status_extended
|
||||
|
||||
def test_invalid_policy_document_is_manual(self):
|
||||
# A malformed policy document cannot prove anything about the role.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[INVALID_POLICY_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_allow_all_with_unresolved_policy_is_manual(self):
|
||||
# Proven admin policy plus an unresolved one: the unresolved document
|
||||
# could contain a deny, so the classification is unknown.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
role_arns=[ADMIN_UNRESOLVED_POLICY_ROLE_ARN]
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_unscoped_profile_with_least_privilege_role_passes(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client({PROFILE_ARN: _profile(role_arns=[READONLY_ROLE_ARN])})
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "defense-in-depth" in result[0].status_extended
|
||||
|
||||
def test_unscoped_profile_with_unknown_role_is_manual(self):
|
||||
# A referenced role missing from the IAM inventory is unknown, not
|
||||
# proof that no administrative role exists.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client({PROFILE_ARN: _profile(role_arns=[UNKNOWN_ROLE_ARN])})
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "could not be evaluated" in result[0].status_extended
|
||||
|
||||
def test_unscoped_profile_without_roles_passes(self):
|
||||
with _enter(_patched(_build_client({PROFILE_ARN: _profile(role_arns=[])}))):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_profile_with_session_policy_passes(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
session_policy=SESSION_POLICY, role_arns=[ADMIN_ROLE_ARN]
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "session policy" in result[0].status_extended
|
||||
|
||||
def test_full_access_session_policy_does_not_scope(self):
|
||||
# A sessionPolicy granting *:* does not restrict anything.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
session_policy=FULL_ACCESS_SESSION_POLICY,
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_profile_with_managed_policies_passes(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
managed_policy_arns=[MANAGED_POLICY_ARN],
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_admin_managed_session_policy_does_not_scope(self):
|
||||
# AdministratorAccess as the managed session policy restricts nothing.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
managed_policy_arns=[AWS_ADMIN_POLICY_ARN],
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_restrictive_inline_with_admin_managed_policy_fails(self):
|
||||
# The session-policy set is evaluated as a union: AdministratorAccess as
|
||||
# a managed session policy makes the boundary unrestricted even though
|
||||
# the inline session policy is restrictive.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
session_policy=SESSION_POLICY,
|
||||
managed_policy_arns=[AWS_ADMIN_POLICY_ARN],
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_full_access_inline_with_restrictive_managed_policy_fails(self):
|
||||
# Conversely, a *:* inline session policy leaves the union unrestricted
|
||||
# regardless of a restrictive managed session policy.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
session_policy=FULL_ACCESS_SESSION_POLICY,
|
||||
managed_policy_arns=[MANAGED_POLICY_ARN],
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_restrictive_inline_and_restrictive_managed_policy_passes(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
session_policy=SESSION_POLICY,
|
||||
managed_policy_arns=[MANAGED_POLICY_ARN],
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_customer_managed_full_access_session_policy_fails(self):
|
||||
# A customer-managed session policy whose document grants *:* must be
|
||||
# resolved through iam_client.policies and treated as unscoped.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
managed_policy_arns=[CUSTOMER_FULL_ACCESS_POLICY_ARN],
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_unresolved_managed_session_policy_is_manual(self):
|
||||
# A managed session policy whose document was not collected does not
|
||||
# prove that the session is restricted: the outcome is unknown.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
managed_policy_arns=[UNRESOLVED_SESSION_POLICY_ARN],
|
||||
role_arns=[ADMIN_ROLE_ARN],
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
assert "session scoping could not be evaluated" in result[0].status_extended
|
||||
|
||||
def test_invalid_inline_session_policy_is_manual(self):
|
||||
# An inline session policy that fails to parse does not prove that the
|
||||
# session is restricted: the outcome is unknown.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{
|
||||
PROFILE_ARN: _profile(
|
||||
session_policy="not-json", role_arns=[ADMIN_ROLE_ARN]
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_allow_all_with_cross_policy_deny_all_passes(self):
|
||||
# Allow *:* in an attached policy plus an unconditional Deny *:* in an
|
||||
# inline policy: the merged evaluation must not classify the role admin.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[DENY_OVERRIDE_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_conditional_admin_allow_is_manual(self):
|
||||
# An Allow *:* guarded by a Condition is not statically provable in
|
||||
# either direction: the classification is unknown.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[CONDITIONAL_ADMIN_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_allow_all_with_conditional_deny_is_manual(self):
|
||||
# Unconditional Allow *:* plus a Condition-guarded Deny: the deny may
|
||||
# or may not negate the grant, so the classification is unknown.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[CONDITIONAL_DENY_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_admin_role_with_restrictive_boundary_passes(self):
|
||||
# Admin identity policies intersected with a read-only permissions
|
||||
# boundary are not effectively administrative.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[BOUNDED_ADMIN_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_admin_role_with_unresolved_boundary_is_manual(self):
|
||||
# When the boundary document cannot be resolved the restrictions are
|
||||
# unknown: neither administrative nor safe can be proven.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[UNRESOLVED_BOUNDARY_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "MANUAL"
|
||||
|
||||
def test_admin_role_with_admin_boundary_fails(self):
|
||||
# An AdministratorAccess boundary restricts nothing: still admin.
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(role_arns=[ADMIN_BOUNDED_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert ADMIN_BOUNDED_ROLE_ARN in result[0].status_extended
|
||||
|
||||
def test_disabled_profile_passes(self):
|
||||
with _enter(
|
||||
_patched(
|
||||
_build_client(
|
||||
{PROFILE_ARN: _profile(enabled=False, role_arns=[ADMIN_ROLE_ARN])}
|
||||
)
|
||||
)
|
||||
):
|
||||
result = _run()
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "disabled" in result[0].status_extended
|
||||
@@ -4,6 +4,7 @@ import botocore
|
||||
from moto import mock_aws
|
||||
|
||||
from prowler.providers.aws.services.rolesanywhere.rolesanywhere_service import (
|
||||
Profile,
|
||||
RolesAnywhere,
|
||||
TrustAnchor,
|
||||
)
|
||||
@@ -16,6 +17,9 @@ from tests.providers.aws.utils import (
|
||||
TA_ID = "11111111-2222-3333-4444-555555555555"
|
||||
TA_ARN = f"arn:aws:rolesanywhere:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:trust-anchor/{TA_ID}"
|
||||
PCA_ARN = f"arn:aws:acm-pca:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:certificate-authority/abc"
|
||||
PROFILE_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
|
||||
PROFILE_ARN = f"arn:aws:rolesanywhere:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:profile/{PROFILE_ID}"
|
||||
ROLE_ARN = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/workload-role"
|
||||
|
||||
make_api_call = botocore.client.BaseClient._make_api_call
|
||||
|
||||
@@ -36,6 +40,21 @@ def mock_make_api_call(self, operation_name, kwarg):
|
||||
}
|
||||
]
|
||||
}
|
||||
if operation_name == "ListProfiles":
|
||||
return {
|
||||
"profiles": [
|
||||
{
|
||||
"profileArn": PROFILE_ARN,
|
||||
"profileId": PROFILE_ID,
|
||||
"name": "workload-profile",
|
||||
"enabled": True,
|
||||
"roleArns": [ROLE_ARN],
|
||||
"sessionPolicy": '{"Version":"2012-10-17","Statement":[]}',
|
||||
"durationSeconds": 3600,
|
||||
"acceptRoleSessionName": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
if operation_name == "ListTagsForResource":
|
||||
return {"tags": [{"key": "Environment", "value": "test"}]}
|
||||
return make_api_call(self, operation_name, kwarg)
|
||||
@@ -78,6 +97,25 @@ class Test_RolesAnywhere_Service:
|
||||
assert ta.region == AWS_REGION_US_EAST_1
|
||||
assert ta.tags == [{"key": "Environment", "value": "test"}]
|
||||
|
||||
@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call)
|
||||
@mock_aws
|
||||
def test_list_profiles(self):
|
||||
aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1])
|
||||
rolesanywhere = RolesAnywhere(aws_provider)
|
||||
assert len(rolesanywhere.profiles) == 1
|
||||
profile = rolesanywhere.profiles[PROFILE_ARN]
|
||||
assert isinstance(profile, Profile)
|
||||
assert profile.id == PROFILE_ID
|
||||
assert profile.name == "workload-profile"
|
||||
assert profile.enabled is True
|
||||
assert profile.role_arns == [ROLE_ARN]
|
||||
assert profile.session_policy == '{"Version":"2012-10-17","Statement":[]}'
|
||||
assert profile.managed_policy_arns == []
|
||||
assert profile.duration_seconds == 3600
|
||||
assert profile.accept_role_session_name is True
|
||||
assert profile.region == AWS_REGION_US_EAST_1
|
||||
assert profile.tags == [{"key": "Environment", "value": "test"}]
|
||||
|
||||
@patch(
|
||||
"botocore.client.BaseClient._make_api_call", new=mock_make_api_call_tags_failure
|
||||
)
|
||||
|
||||
@@ -490,6 +490,19 @@ def mock_api_projects_calls(client: MagicMock):
|
||||
}
|
||||
client.projects().serviceAccounts().list_next.return_value = None
|
||||
|
||||
# Workload Identity Federation pools/providers: return empty pages and stop
|
||||
# pagination so the discovery while-loops in the IAM service terminate.
|
||||
client.projects().locations().workloadIdentityPools().list().execute.return_value = {
|
||||
"workloadIdentityPools": []
|
||||
}
|
||||
client.projects().locations().workloadIdentityPools().list_next.return_value = None
|
||||
client.projects().locations().workloadIdentityPools().providers().list().execute.return_value = {
|
||||
"workloadIdentityPoolProviders": []
|
||||
}
|
||||
client.projects().locations().workloadIdentityPools().providers().list_next.return_value = (
|
||||
None
|
||||
)
|
||||
|
||||
def mock_list_service_accounts_keys(name):
|
||||
return_value = MagicMock()
|
||||
if (
|
||||
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
from unittest import mock
|
||||
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
GCP_PROJECT_ID,
|
||||
GCP_US_CENTER1_LOCATION,
|
||||
set_mocked_gcp_provider,
|
||||
)
|
||||
|
||||
CHECK_MODULE = "prowler.providers.gcp.services.iam.iam_workload_identity_pool_provider_attribute_condition.iam_workload_identity_pool_provider_attribute_condition"
|
||||
|
||||
|
||||
def _run(provider_kwargs):
|
||||
iam_client = mock.MagicMock()
|
||||
with (
|
||||
mock.patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(),
|
||||
),
|
||||
mock.patch(f"{CHECK_MODULE}.iam_client", new=iam_client),
|
||||
):
|
||||
from prowler.providers.gcp.services.iam.iam_service import (
|
||||
WorkloadIdentityPoolProvider,
|
||||
)
|
||||
from prowler.providers.gcp.services.iam.iam_workload_identity_pool_provider_attribute_condition.iam_workload_identity_pool_provider_attribute_condition import (
|
||||
iam_workload_identity_pool_provider_attribute_condition,
|
||||
)
|
||||
|
||||
providers = []
|
||||
for kwargs in provider_kwargs:
|
||||
provider_id = kwargs.get("provider_id", "my-provider")
|
||||
providers.append(
|
||||
WorkloadIdentityPoolProvider(
|
||||
name=(
|
||||
f"projects/{GCP_PROJECT_ID}/locations/global/"
|
||||
f"workloadIdentityPools/my-pool/providers/{provider_id}"
|
||||
),
|
||||
id=provider_id,
|
||||
pool_id="my-pool",
|
||||
pool_disabled=kwargs.get("pool_disabled", False),
|
||||
project_id=GCP_PROJECT_ID,
|
||||
state=kwargs.get("state", "ACTIVE"),
|
||||
disabled=kwargs.get("disabled", False),
|
||||
attribute_condition=kwargs.get("attribute_condition", ""),
|
||||
provider_type=kwargs.get("provider_type", "oidc"),
|
||||
issuer_uri=kwargs.get(
|
||||
"issuer_uri",
|
||||
"https://token.actions.githubusercontent.com",
|
||||
),
|
||||
display_name="My Provider",
|
||||
)
|
||||
)
|
||||
|
||||
iam_client.project_ids = [GCP_PROJECT_ID]
|
||||
iam_client.region = GCP_US_CENTER1_LOCATION
|
||||
iam_client.workload_identity_pool_providers = providers
|
||||
return iam_workload_identity_pool_provider_attribute_condition().execute()
|
||||
|
||||
|
||||
class Test_iam_workload_identity_pool_provider_attribute_condition:
|
||||
def test_no_providers(self):
|
||||
assert len(_run([])) == 0
|
||||
|
||||
def test_multi_tenant_issuer_without_attribute_condition_fails(self):
|
||||
result = _run(
|
||||
[
|
||||
{
|
||||
"attribute_condition": "",
|
||||
"issuer_uri": "https://token.actions.githubusercontent.com",
|
||||
}
|
||||
]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
assert result[0].resource_id.endswith("my-provider")
|
||||
assert result[0].location == "global"
|
||||
assert "multi-tenant issuer" in result[0].status_extended
|
||||
|
||||
def test_dedicated_issuer_without_attribute_condition_passes(self):
|
||||
result = _run(
|
||||
[
|
||||
{
|
||||
"attribute_condition": "",
|
||||
"issuer_uri": "https://oidc.eks.eu-west-1.amazonaws.com/id/ABC123",
|
||||
}
|
||||
]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "dedicated issuer" in result[0].status_extended
|
||||
|
||||
def test_non_oidc_provider_without_attribute_condition_passes(self):
|
||||
result = _run(
|
||||
[{"attribute_condition": "", "provider_type": "aws", "issuer_uri": ""}]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "not an OIDC provider" in result[0].status_extended
|
||||
|
||||
def test_multi_tenant_issuer_with_port_and_uppercase_fails(self):
|
||||
result = _run(
|
||||
[{"attribute_condition": "", "issuer_uri": "https://GitLab.com:443"}]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_multi_tenant_issuer_bare_host_fails(self):
|
||||
result = _run(
|
||||
[
|
||||
{
|
||||
"attribute_condition": "",
|
||||
"issuer_uri": "token.actions.githubusercontent.com",
|
||||
}
|
||||
]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "FAIL"
|
||||
|
||||
def test_multi_tenant_issuer_with_attribute_condition_passes(self):
|
||||
result = _run(
|
||||
[
|
||||
{
|
||||
"attribute_condition": "assertion.repository_owner == 'acme'",
|
||||
"issuer_uri": "https://token.actions.githubusercontent.com",
|
||||
}
|
||||
]
|
||||
)
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "enforces an attribute condition" in result[0].status_extended
|
||||
|
||||
def test_disabled_provider_passes(self):
|
||||
result = _run([{"disabled": True}])
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "not active" in result[0].status_extended
|
||||
|
||||
def test_non_active_provider_passes(self):
|
||||
result = _run([{"state": "DELETED"}])
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
|
||||
def test_active_provider_in_disabled_pool_passes(self):
|
||||
# The provider itself is ACTIVE and unconditioned on a multi-tenant
|
||||
# issuer, but its parent pool is disabled and cannot vend credentials.
|
||||
result = _run([{"pool_disabled": True}])
|
||||
assert len(result) == 1
|
||||
assert result[0].status == "PASS"
|
||||
assert "disabled pool" in result[0].status_extended
|
||||
@@ -0,0 +1,215 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from tests.providers.gcp.gcp_fixtures import (
|
||||
GCP_PROJECT_ID,
|
||||
mock_is_api_active,
|
||||
set_mocked_gcp_provider,
|
||||
)
|
||||
|
||||
PROJECT_A = GCP_PROJECT_ID
|
||||
PROJECT_B = "test-project-b"
|
||||
|
||||
|
||||
def _pool_name(project_id, pool_id="my-pool"):
|
||||
return f"projects/{project_id}/locations/global/workloadIdentityPools/{pool_id}"
|
||||
|
||||
|
||||
def _provider_payload(pool_name, provider_id="my-provider"):
|
||||
return {
|
||||
"name": f"{pool_name}/providers/{provider_id}",
|
||||
"state": "ACTIVE",
|
||||
"disabled": False,
|
||||
"attributeMapping": {"google.subject": "assertion.sub"},
|
||||
"oidc": {"issuerUri": "https://token.actions.githubusercontent.com"},
|
||||
"displayName": "gh",
|
||||
}
|
||||
|
||||
|
||||
def _empty_service_accounts(client):
|
||||
"""Stub the service-account calls used by the rest of the IAM __init__."""
|
||||
sa = client.projects.return_value.serviceAccounts.return_value
|
||||
sa.list.return_value.execute.return_value = {"accounts": []}
|
||||
sa.list_next.return_value = None
|
||||
|
||||
|
||||
def _wif_client(_GCPService, _service, _api_version, _credentials):
|
||||
"""Discovery client stub returning one pool with one provider."""
|
||||
client = MagicMock()
|
||||
|
||||
pool_name = _pool_name(GCP_PROJECT_ID)
|
||||
pools = (
|
||||
client.projects.return_value.locations.return_value.workloadIdentityPools.return_value
|
||||
)
|
||||
pools.list.return_value.execute.return_value = {
|
||||
"workloadIdentityPools": [{"name": pool_name, "state": "ACTIVE"}]
|
||||
}
|
||||
pools.list_next.return_value = None
|
||||
|
||||
providers = pools.providers.return_value
|
||||
providers.list.return_value.execute.return_value = {
|
||||
"workloadIdentityPoolProviders": [_provider_payload(pool_name)]
|
||||
}
|
||||
providers.list_next.return_value = None
|
||||
|
||||
_empty_service_accounts(client)
|
||||
return client
|
||||
|
||||
|
||||
def _disabled_pool_client(_GCPService, _service, _api_version, _credentials):
|
||||
"""Discovery client stub: a disabled pool containing an ACTIVE provider."""
|
||||
client = MagicMock()
|
||||
|
||||
pool_name = _pool_name(GCP_PROJECT_ID)
|
||||
pools = (
|
||||
client.projects.return_value.locations.return_value.workloadIdentityPools.return_value
|
||||
)
|
||||
pools.list.return_value.execute.return_value = {
|
||||
"workloadIdentityPools": [
|
||||
{"name": pool_name, "state": "ACTIVE", "disabled": True}
|
||||
]
|
||||
}
|
||||
pools.list_next.return_value = None
|
||||
|
||||
providers = pools.providers.return_value
|
||||
providers.list.return_value.execute.return_value = {
|
||||
"workloadIdentityPoolProviders": [_provider_payload(pool_name)]
|
||||
}
|
||||
providers.list_next.return_value = None
|
||||
|
||||
_empty_service_accounts(client)
|
||||
return client
|
||||
|
||||
|
||||
def _pool_list_failure_client(_GCPService, _service, _api_version, _credentials):
|
||||
"""Pool listing fails for PROJECT_A but succeeds for PROJECT_B."""
|
||||
client = MagicMock()
|
||||
pool_name_b = _pool_name(PROJECT_B)
|
||||
|
||||
pools = (
|
||||
client.projects.return_value.locations.return_value.workloadIdentityPools.return_value
|
||||
)
|
||||
|
||||
def pools_list(parent):
|
||||
request = MagicMock()
|
||||
if f"projects/{PROJECT_A}/" in parent:
|
||||
request.execute.side_effect = Exception("permission denied listing pools")
|
||||
else:
|
||||
request.execute.return_value = {
|
||||
"workloadIdentityPools": [{"name": pool_name_b, "state": "ACTIVE"}]
|
||||
}
|
||||
return request
|
||||
|
||||
pools.list.side_effect = pools_list
|
||||
pools.list_next.return_value = None
|
||||
|
||||
providers = pools.providers.return_value
|
||||
providers.list.return_value.execute.return_value = {
|
||||
"workloadIdentityPoolProviders": [_provider_payload(pool_name_b)]
|
||||
}
|
||||
providers.list_next.return_value = None
|
||||
|
||||
_empty_service_accounts(client)
|
||||
return client
|
||||
|
||||
|
||||
def _provider_list_failure_client(_GCPService, _service, _api_version, _credentials):
|
||||
"""Provider listing fails for pool-1 but succeeds for pool-2 in one project."""
|
||||
client = MagicMock()
|
||||
pool_1 = _pool_name(GCP_PROJECT_ID, "pool-1")
|
||||
pool_2 = _pool_name(GCP_PROJECT_ID, "pool-2")
|
||||
|
||||
pools = (
|
||||
client.projects.return_value.locations.return_value.workloadIdentityPools.return_value
|
||||
)
|
||||
pools.list.return_value.execute.return_value = {
|
||||
"workloadIdentityPools": [
|
||||
{"name": pool_1, "state": "ACTIVE"},
|
||||
{"name": pool_2, "state": "ACTIVE"},
|
||||
]
|
||||
}
|
||||
pools.list_next.return_value = None
|
||||
|
||||
providers = pools.providers.return_value
|
||||
|
||||
def providers_list(parent):
|
||||
request = MagicMock()
|
||||
if parent == pool_1:
|
||||
request.execute.side_effect = Exception(
|
||||
"permission denied listing providers"
|
||||
)
|
||||
else:
|
||||
request.execute.return_value = {
|
||||
"workloadIdentityPoolProviders": [
|
||||
_provider_payload(pool_2, provider_id="provider-2")
|
||||
]
|
||||
}
|
||||
return request
|
||||
|
||||
providers.list.side_effect = providers_list
|
||||
providers.list_next.return_value = None
|
||||
|
||||
_empty_service_accounts(client)
|
||||
return client
|
||||
|
||||
|
||||
def _run_service(client_factory, project_ids):
|
||||
with (
|
||||
patch(
|
||||
"prowler.providers.common.provider.Provider.get_global_provider",
|
||||
return_value=set_mocked_gcp_provider(project_ids=project_ids),
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__is_api_active__",
|
||||
new=mock_is_api_active,
|
||||
),
|
||||
patch(
|
||||
"prowler.providers.gcp.lib.service.service.GCPService.__generate_client__",
|
||||
new=client_factory,
|
||||
),
|
||||
):
|
||||
from prowler.providers.gcp.services.iam.iam_service import IAM
|
||||
|
||||
return IAM(set_mocked_gcp_provider(project_ids=project_ids))
|
||||
|
||||
|
||||
class TestIAMWorkloadIdentityService:
|
||||
def test_get_workload_identity_pool_providers(self):
|
||||
iam = _run_service(_wif_client, [GCP_PROJECT_ID])
|
||||
|
||||
assert len(iam.workload_identity_pool_providers) == 1
|
||||
provider = iam.workload_identity_pool_providers[0]
|
||||
assert provider.id == "my-provider"
|
||||
assert provider.pool_id == "my-pool"
|
||||
assert provider.project_id == GCP_PROJECT_ID
|
||||
assert provider.state == "ACTIVE"
|
||||
assert provider.disabled is False
|
||||
assert provider.pool_disabled is False
|
||||
assert provider.attribute_condition == ""
|
||||
assert provider.provider_type == "oidc"
|
||||
assert provider.issuer_uri == "https://token.actions.githubusercontent.com"
|
||||
|
||||
def test_disabled_pool_state_propagates_to_provider(self):
|
||||
iam = _run_service(_disabled_pool_client, [GCP_PROJECT_ID])
|
||||
|
||||
# The provider is ACTIVE, but its parent pool is disabled: the pool's
|
||||
# effective state must travel with the provider record.
|
||||
assert len(iam.workload_identity_pool_providers) == 1
|
||||
provider = iam.workload_identity_pool_providers[0]
|
||||
assert provider.state == "ACTIVE"
|
||||
assert provider.disabled is False
|
||||
assert provider.pool_disabled is True
|
||||
|
||||
def test_pool_list_failure_does_not_block_other_projects(self):
|
||||
iam = _run_service(_pool_list_failure_client, [PROJECT_A, PROJECT_B])
|
||||
|
||||
# PROJECT_A's pool listing failed, but PROJECT_B is still processed.
|
||||
assert len(iam.workload_identity_pool_providers) == 1
|
||||
assert iam.workload_identity_pool_providers[0].project_id == PROJECT_B
|
||||
|
||||
def test_provider_list_failure_only_skips_that_pool(self):
|
||||
iam = _run_service(_provider_list_failure_client, [GCP_PROJECT_ID])
|
||||
|
||||
# pool-1's provider listing failed, but pool-2's provider is still found.
|
||||
assert len(iam.workload_identity_pool_providers) == 1
|
||||
assert iam.workload_identity_pool_providers[0].pool_id == "pool-2"
|
||||
assert iam.workload_identity_pool_providers[0].id == "provider-2"
|
||||
Reference in New Issue
Block a user