tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Check working dir and tools
ls /app/ 2>&1; echo "---"; aws --version 2>&1; python3 --version 2>&1; which jq 2>&1
--- aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3 /usr/bin/jq [stdout] --- aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3 /usr/bin/jq
Verify localstack connectivity and creds
aws --endpoint-url=http://localstack:4566 sts get-caller-identity 2>&1; echo "---"; env | grep -i aws
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
AWS_DEFAULT_REGION=us-east-1
AWS_PAGER=
AWS_SECRET_ACCESS_KEY=test
AWS_ACCESS_KEY_ID=test
AWS_ENDPOINT_URL=http://localstack:4566
[stdout]
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
AWS_DEFAULT_REGION=us-east-1
AWS_PAGER=
AWS_SECRET_ACCESS_KEY=test
AWS_ACCESS_KEY_ID=test
AWS_ENDPOINT_URL=http://localstack:4566Create work directories
mkdir -p /app/scripts /app/policies /app/state && ls /app/
policies scripts state [stdout] policies scripts state
#!/usr/bin/env bash
set -euo pipefail
# ---------------------------------------------------------------------------
# Cross-account trust setup against LocalStack.
# Builds: orchestrator role (assumed by an IAM user), two tenant member roles
# whose trust policies require both a per-tenant ExternalId AND an aws:SourceArn
# pinned to the orchestrator role. ExternalIds live in Secrets Manager,
# encrypted by a customer-managed KMS key. SSM holds the ARNs the verifier
# needs.
# ---------------------------------------------------------------------------
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
STATE=/app/state
POL=/app/policies
mkdir -p "$STATE" "$POL"
echo "account=$ACCOUNT_ID region=$REGION"
# ---------------------------------------------------------------------------
# 1. IAM users , orchestrator caller + impersonator stand-in
# ---------------------------------------------------------------------------
for u in agent-runner attacker-runner; do
if ! aws iam get-user --user-name "$u" >/dev/null 2>&1; then
aws iam create-user --user-name "$u" >/dev/null
fi
done
AGENT_USER_ARN="arn:aws:iam::${ACCOUNT_ID}:user/agent-runner"
ATTACKER_USER_ARN="arn:aws:iam::${ACCOUNT_ID}:user/attacker-runner"
# ---------------------------------------------------------------------------
# 2. KMS customer-managed key + alias
# ---------------------------------------------------------------------------
KEY_POLICY_FILE="$POL/kms-key-policy.json"
cat > "$KEY_POLICY_FILE" <<JSON
{
"Version": "2012-10-17",
"Id": "cross-account-cmk-policy",
"Statement": [
{
"Sid": "EnableAccountRoot",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:root" },
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "arn:aws:kms:${REGION}:${ACCOUNT_ID}:key/*"
},
{
"Sid": "AllowSecretsManagerUse",
"Effect": "Allow",
"Principal": { "Service": "secretsmanager.amazonaws.com" },
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:${REGION}:${ACCOUNT_ID}:key/*"
},
{
"Sid": "AllowOrchestratorDecryptForSecrets",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::${ACCOUNT_ID}:role/orchestrator/OrchestratorRole" },
"Action": [ "kms:Decrypt", "kms:DescribeKey" ],
"Resource": "arn:aws:kms:${REGION}:${ACCOUNT_ID}:key/*"
}
]
}
JSON
# Reuse alias if already present
if aws kms describe-key --key-id alias/cross-account-cmk >/dev/null 2>&1; then
KEY_ID=$(aws kms describe-key --key-id alias/cross-account-cmk --query 'KeyMetadata.KeyId' --output text)
aws kms put-key-policy --key-id "$KEY_ID" --policy-name default --policy "file://$KEY_POLICY_FILE" >/dev/null
else
KEY_ID=$(aws kms create-key \
--description "cross-account secret encryption" \
--policy "file://$KEY_POLICY_FILE" \
--query 'KeyMetadata.KeyId' --output text)
aws kms create-alias --alias-name alias/cross-account-cmk --target-key-id "$KEY_ID" >/dev/null
fi
KEY_ARN="arn:aws:kms:${REGION}:${ACCOUNT_ID}:key/${KEY_ID}"
echo "kms key=$KEY_ARN"
# ---------------------------------------------------------------------------
# 3. OrchestratorRole , trusted by agent-runner only
# ---------------------------------------------------------------------------
ORCH_TRUST="$POL/orchestrator-trust.json"
cat > "$ORCH_TRUST" <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "${AGENT_USER_ARN}" },
"Action": "sts:AssumeRole"
}
]
}
JSON
if ! aws iam get-role --role-name OrchestratorRole >/dev/null 2>&1; then
aws iam create-role \
--role-name OrchestratorRole \
--path /orchestrator/ \
--assume-role-policy-document "file://$ORCH_TRUST" >/dev/null
else
aws iam update-assume-role-policy \
--role-name OrchestratorRole \
--policy-document "file://$ORCH_TRUST" >/dev/null
fi
ORCH_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/orchestrator/OrchestratorRole"
echo "orchestrator role=$ORCH_ROLE_ARN"
# ---------------------------------------------------------------------------
# 4. Per-tenant ExternalIds , random, distinct, >= 32 chars.
# Generate the values BEFORE the trust docs because trust references value.
# ---------------------------------------------------------------------------
gen_external_id() {
python3 -c "import secrets; print(secrets.token_urlsafe(40))"
}
EID_A=$(gen_external_id)
EID_B=$(gen_external_id)
# Sanity: distinct, >= 32
[ "$EID_A" != "$EID_B" ] || { echo "external ids collided"; exit 1; }
[ ${#EID_A} -ge 32 ] && [ ${#EID_B} -ge 32 ] || { echo "external id too short"; exit 1; }
echo "external ids generated: A=${#EID_A} chars, B=${#EID_B} chars, distinct=yes"
# ---------------------------------------------------------------------------
# 5. Secrets Manager , store each ExternalId, encrypted with the CMK
# ---------------------------------------------------------------------------
create_or_update_secret() {
local name=$1 value=$2
if aws secretsmanager describe-secret --secret-id "$name" >/dev/null 2>&1; then
aws secretsmanager put-secret-value \
--secret-id "$name" \
--secret-string "$value" >/dev/null
else
aws secretsmanager create-secret \
--name "$name" \
--kms-key-id "$KEY_ARN" \
--secret-string "$value" >/dev/null
fi
}
create_or_update_secret "harbor/cross-account/external-id-tenant-a" "$EID_A"
create_or_update_secret "harbor/cross-account/external-id-tenant-b" "$EID_B"
SECRET_A_ARN=$(aws secretsmanager describe-secret --secret-id "harbor/cross-account/external-id-tenant-a" --query ARN --output text)
SECRET_B_ARN=$(aws secretsmanager describe-secret --secret-id "harbor/cross-account/external-id-tenant-b" --query ARN --output text)
echo "secret A arn=$SECRET_A_ARN"
echo "secret B arn=$SECRET_B_ARN"
# ---------------------------------------------------------------------------
# 6. Resource policy on each secret , orchestrator role ARN as Principal.AWS
# ---------------------------------------------------------------------------
for arn_var in "harbor/cross-account/external-id-tenant-a" "harbor/cross-account/external-id-tenant-b"; do
POLICY_DOC=$(cat <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowOrchestratorRead",
"Effect": "Allow",
"Principal": { "AWS": "${ORCH_ROLE_ARN}" },
"Action": "secretsmanager:GetSecretValue",
"Resource": "*"
}
]
}
JSON
)
aws secretsmanager put-resource-policy \
--secret-id "$arn_var" \
--resource-policy "$POLICY_DOC" >/dev/null
done
# ---------------------------------------------------------------------------
# 7. Member role trust policies , ExternalId + aws:SourceArn, both required
# ---------------------------------------------------------------------------
build_member_trust() {
local eid=$1 out=$2
cat > "$out" <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "${ORCH_ROLE_ARN}" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": "${eid}",
"aws:SourceArn": "${ORCH_ROLE_ARN}"
}
}
}
]
}
JSON
}
TRUST_A="$POL/member-trust-tenant-a.json"
TRUST_B="$POL/member-trust-tenant-b.json"
build_member_trust "$EID_A" "$TRUST_A"
build_member_trust "$EID_B" "$TRUST_B"
create_or_update_role() {
local name=$1 trust=$2
if aws iam get-role --role-name "$name" >/dev/null 2>&1; then
aws iam update-assume-role-policy --role-name "$name" --policy-document "file://$trust" >/dev/null
else
aws iam create-role --role-name "$name" --path /member/ --assume-role-policy-document "file://$trust" >/dev/null
fi
}
create_or_update_role "MemberRole-Tenant-A" "$TRUST_A"
create_or_update_role "MemberRole-Tenant-B" "$TRUST_B"
MEMBER_A_ARN="arn:aws:iam::${ACCOUNT_ID}:role/member/MemberRole-Tenant-A"
MEMBER_B_ARN="arn:aws:iam::${ACCOUNT_ID}:role/member/MemberRole-Tenant-B"
echo "member A=$MEMBER_A_ARN"
echo "member B=$MEMBER_B_ARN"
# ---------------------------------------------------------------------------
# 8. S3 buckets , one per tenant
# ---------------------------------------------------------------------------
for b in tenant-a-bucket tenant-b-bucket; do
if ! aws s3api head-bucket --bucket "$b" >/dev/null 2>&1; then
aws s3api create-bucket --bucket "$b" >/dev/null
fi
done
# ---------------------------------------------------------------------------
# 9. Identity policies for member roles , scoped to OWN bucket only
# ---------------------------------------------------------------------------
build_member_identity() {
local bucket=$1 out=$2
cat > "$out" <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TenantBucketObjectRW",
"Effect": "Allow",
"Action": [ "s3:PutObject", "s3:GetObject" ],
"Resource": "arn:aws:s3:::${bucket}/*"
},
{
"Sid": "TenantBucketList",
"Effect": "Allow",
"Action": [ "s3:ListBucket" ],
"Resource": "arn:aws:s3:::${bucket}"
}
]
}
JSON
}
MEM_A_INLINE="$POL/member-a-inline.json"
MEM_B_INLINE="$POL/member-b-inline.json"
build_member_identity "tenant-a-bucket" "$MEM_A_INLINE"
build_member_identity "tenant-b-bucket" "$MEM_B_INLINE"
aws iam put-role-policy \
--role-name MemberRole-Tenant-A \
--policy-name TenantABucketAccess \
--policy-document "file://$MEM_A_INLINE" >/dev/null
aws iam put-role-policy \
--role-name MemberRole-Tenant-B \
--policy-name TenantBBucketAccess \
--policy-document "file://$MEM_B_INLINE" >/dev/null
# ---------------------------------------------------------------------------
# 10. Orchestrator identity policy , explicit member role ARNs and secret ARNs.
# ---------------------------------------------------------------------------
ORCH_INLINE="$POL/orchestrator-inline.json"
cat > "$ORCH_INLINE" <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeOnlyKnownMemberRoles",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"${MEMBER_A_ARN}",
"${MEMBER_B_ARN}"
]
},
{
"Sid": "GetOnlyKnownExternalIdSecrets",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": [
"${SECRET_A_ARN}",
"${SECRET_B_ARN}"
]
},
{
"Sid": "DecryptCmkForSecrets",
"Effect": "Allow",
"Action": [ "kms:Decrypt", "kms:DescribeKey" ],
"Resource": "${KEY_ARN}"
}
]
}
JSON
aws iam put-role-policy \
--role-name OrchestratorRole \
--policy-name OrchestratorBaseAccess \
--policy-document "file://$ORCH_INLINE" >/dev/null
# Allow the agent-runner user to call sts:AssumeRole on OrchestratorRole only.
AGENT_USER_INLINE="$POL/agent-runner-inline.json"
cat > "$AGENT_USER_INLINE" <<JSON
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "${ORCH_ROLE_ARN}"
}
]
}
JSON
aws iam put-user-policy \
--user-name agent-runner \
--policy-name AssumeOrchestrator \
--policy-document "file://$AGENT_USER_INLINE" >/dev/null
# ---------------------------------------------------------------------------
# 11. SSM pointers
# ---------------------------------------------------------------------------
put_ssm() {
aws ssm put-parameter --name "$1" --value "$2" --type String --overwrite >/dev/null
}
put_ssm "/harbor/orchestrator/role-arn" "$ORCH_ROLE_ARN"
put_ssm "/harbor/external-id-secret-arn-tenant-a" "$SECRET_A_ARN"
put_ssm "/harbor/external-id-secret-arn-tenant-b" "$SECRET_B_ARN"
put_ssm "/harbor/member-role-arn-tenant-a" "$MEMBER_A_ARN"
put_ssm "/harbor/member-role-arn-tenant-b" "$MEMBER_B_ARN"
# ---------------------------------------------------------------------------
# 12. Persist state for the happy-path runner
# ---------------------------------------------------------------------------
cat > "$STATE/state.env" <<ENV
ACCOUNT_ID=${ACCOUNT_ID}
REGION=${REGION}
ORCH_ROLE_ARN=${ORCH_ROLE_ARN}
MEMBER_A_ARN=${MEMBER_A_ARN}
MEMBER_B_ARN=${MEMBER_B_ARN}
SECRET_A_ARN=${SECRET_A_ARN}
SECRET_B_ARN=${SECRET_B_ARN}
KEY_ARN=${KEY_ARN}
ENV
# Access key for agent-runner (used by happy path)
if [ ! -f "$STATE/agent-runner.creds" ]; then
aws iam create-access-key --user-name agent-runner > "$STATE/agent-runner.creds.json"
AK=$(jq -r '.AccessKey.AccessKeyId' "$STATE/agent-runner.creds.json")
SK=$(jq -r '.AccessKey.SecretAccessKey' "$STATE/agent-runner.creds.json")
cat > "$STATE/agent-runner.creds" <<ENV
AGENT_AK=${AK}
AGENT_SK=${SK}
ENV
fi
echo "setup complete."
Run setup script
chmod +x /app/scripts/00_setup.sh && /app/scripts/00_setup.sh
account=000000000000 region=us-east-1 kms key=arn:aws:kms:us-east-1:000000000000:key/84bc366e-ab05-45bc-89c9-556f53857bb9 orchestrator role=arn:aws:iam::000000000000:role/orchestrator/OrchestratorRole external ids generated: A=54 chars, B=54 chars, distinct=yes secret A arn=arn:aws:secretsmanager:us-east-1:000000000000:secret:harbor/cross-account/external-id-tenant-a-WorCSX secret B arn=arn:aws:secretsmanager:us-east-1:000000000000:secret:harbor/cross-account/external-id-tenant-b-RNpert member A=arn:aws:iam::000000000000:role/member/MemberRole-Tenant-A member B=arn:aws:iam::000000000000:role/member/MemberRole-Tenant-B setup complete. [stdout] account=000000000000 region=us-east-1 kms key=arn:aws:kms:us-east-1:000000000000:key/84bc366e-ab05-45bc-89c9-556f53857bb9 orchestrator role=arn:aws:iam::000000000000:role/orchestrator/OrchestratorRole external ids generated: A=54 chars, B=54 chars, distinct=yes secret A arn=arn:aws:secretsmanager:us-east-1:000000000000:secret:harbor/cross-account/external-id-tenant-a-WorCSX secret B arn=arn:aws:secretsmanager:us-east-1:000000000000:secret:harbor/cross-account/external-id-tenant-b-RNpert member A=arn:aws:iam::000000000000:role/member/MemberRole-Tenant-A member B=arn:aws:iam::000000000000:role/member/MemberRole-Tenant-B setup complete.
#!/usr/bin/env bash
set -euo pipefail
# Happy path:
# agent-runner -> AssumeRole OrchestratorRole
# OrchestratorRole -> GetSecretValue (tenant-A external id)
# OrchestratorRole -> AssumeRole MemberRole-Tenant-A with --external-id
# MemberRole-Tenant-A -> PutObject in tenant-a-bucket
source /app/state/state.env
source /app/state/agent-runner.creds
unset_aws() { unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN; }
# Resolve everything via SSM the way the auditor expects
ORCH_ROLE_ARN=$(aws ssm get-parameter --name /harbor/orchestrator/role-arn --query Parameter.Value --output text)
SECRET_A_ARN=$(aws ssm get-parameter --name /harbor/external-id-secret-arn-tenant-a --query Parameter.Value --output text)
MEMBER_A_ARN=$(aws ssm get-parameter --name /harbor/member-role-arn-tenant-a --query Parameter.Value --output text)
echo "[ssm] orchestrator=$ORCH_ROLE_ARN"
echo "[ssm] secret-A =$SECRET_A_ARN"
echo "[ssm] member-A =$MEMBER_A_ARN"
# 1. Use agent-runner credentials to assume OrchestratorRole.
unset_aws
export AWS_ACCESS_KEY_ID="$AGENT_AK"
export AWS_SECRET_ACCESS_KEY="$AGENT_SK"
ORCH_CRED_JSON=$(aws sts assume-role \
--role-arn "$ORCH_ROLE_ARN" \
--role-session-name orchestrator-session)
unset_aws
export AWS_ACCESS_KEY_ID=$(echo "$ORCH_CRED_JSON" | jq -r .Credentials.AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo "$ORCH_CRED_JSON" | jq -r .Credentials.SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo "$ORCH_CRED_JSON" | jq -r .Credentials.SessionToken)
echo "[step1] assumed OrchestratorRole as $(aws sts get-caller-identity --query Arn --output text)"
# 2. Read tenant-A ExternalId from Secrets Manager.
EID_A=$(aws secretsmanager get-secret-value --secret-id "$SECRET_A_ARN" --query SecretString --output text)
echo "[step2] retrieved ExternalId for tenant-A (${#EID_A} chars)"
# 3. Assume MemberRole-Tenant-A using that ExternalId.
MEM_CRED_JSON=$(aws sts assume-role \
--role-arn "$MEMBER_A_ARN" \
--role-session-name member-a-session \
--external-id "$EID_A")
unset_aws
export AWS_ACCESS_KEY_ID=$(echo "$MEM_CRED_JSON" | jq -r .Credentials.AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo "$MEM_CRED_JSON" | jq -r .Credentials.SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo "$MEM_CRED_JSON" | jq -r .Credentials.SessionToken)
echo "[step3] assumed MemberRole-Tenant-A as $(aws sts get-caller-identity --query Arn --output text)"
# 4. Put an object into the tenant's bucket.
echo "hello from tenant A" > /tmp/tenant-a-payload.txt
aws s3api put-object \
--bucket tenant-a-bucket \
--key reports/$(date +%Y%m%d-%H%M%S)/payload.txt \
--body /tmp/tenant-a-payload.txt >/dev/null
echo "[step4] put object in tenant-a-bucket OK"
aws s3api list-objects-v2 --bucket tenant-a-bucket --query 'Contents[].Key' --output text
echo "happy path: PASS"
Run happy path
chmod +x /app/scripts/10_happy_path.sh && /app/scripts/10_happy_path.sh
[ssm] orchestrator=arn:aws:iam::000000000000:role/orchestrator/OrchestratorRole [ssm] secret-A =arn:aws:secretsmanager:us-east-1:000000000000:secret:harbor/cross-account/external-id-tenant-a-WorCSX [ssm] member-A =arn:aws:iam::000000000000:role/member/MemberRole-Tenant-A [step1] assumed OrchestratorRole as arn:aws:sts::000000000000:assumed-role/OrchestratorRole/orchestrator-session [step2] retrieved ExternalId for tenant-A (54 chars) [step3] assumed MemberRole-Tenant-A as arn:aws:sts::000000000000:assumed-role/MemberRole-Tenant-A/member-a-session [step4] put object in tenant-a-bucket OK reports/20260428-094658/payload.txt happy path: PASS [stdout] [ssm] orchestrator=arn:aws:iam::000000000000:role/orchestrator/OrchestratorRole [ssm] secret-A =arn:aws:secretsmanager:us-east-1:000000000000:secret:harbor/cross-account/external-id-tenant-a-WorCSX [ssm] member-A =arn:aws:iam::000000000000:role/member/MemberRole-Tenant-A [step1] assumed OrchestratorRole as arn:aws:sts::000000000000:assumed-role/OrchestratorRole/orchestrator-session [step2] retrieved ExternalId for tenant-A (54 chars) [step3] assumed MemberRole-Tenant-A as arn:aws:sts::000000000000:assumed-role/MemberRole-Tenant-A/member-a-session [step4] put object in tenant-a-bucket OK reports/20260428-094658/payload.txt happy path: PASS
#!/usr/bin/env python3
"""
Auditor-style verifier. Reads policy documents directly from IAM/Secrets Manager
and asserts SHAPE rather than runtime behavior, because LocalStack does not
enforce IAM at runtime.
Pass = all assertions hold. Any failure exits non-zero with a list of findings.
"""
from __future__ import annotations
import json
import sys
from typing import Any
import boto3
iam = boto3.client("iam")
sts = boto3.client("sts")
sec = boto3.client("secretsmanager")
ssm = boto3.client("ssm")
ACCOUNT = sts.get_caller_identity()["Account"]
findings: list[str] = []
checks: list[str] = []
def check(label: str, ok: bool, detail: str = "") -> None:
checks.append(label)
if ok:
print(f" PASS {label}")
else:
msg = f"FAIL {label}" + (f" :: {detail}" if detail else "")
print(f" {msg}")
findings.append(msg)
def as_list(v: Any) -> list:
if v is None:
return []
return v if isinstance(v, list) else [v]
def get_role(name: str) -> dict:
return iam.get_role(RoleName=name)["Role"]
def get_inline(name: str, policy: str) -> dict:
return iam.get_role_policy(RoleName=name, PolicyName=policy)["PolicyDocument"]
def get_secret_value(arn: str) -> str:
return sec.get_secret_value(SecretId=arn)["SecretString"]
def get_ssm(name: str) -> str:
return ssm.get_parameter(Name=name)["Parameter"]["Value"]
def policy_has_wildcard_resource_or_action(policy: dict, services: tuple[str, ...]) -> tuple[bool, str]:
for stmt in as_list(policy.get("Statement")):
if stmt.get("Effect") != "Allow":
continue
actions = as_list(stmt.get("Action"))
resources = as_list(stmt.get("Resource"))
for a in actions:
if a == "*":
return True, f"Action:* in stmt {stmt.get('Sid','?')}"
for svc in services:
if a == f"{svc}:*":
return True, f"Action:{svc}:* in stmt {stmt.get('Sid','?')}"
for r in resources:
if r == "*":
return True, f"Resource:* in stmt {stmt.get('Sid','?')}"
return False, ""
# ---------------------------------------------------------------------------
print("\n=== SSM pointers present ===")
expected_ssm = [
"/harbor/orchestrator/role-arn",
"/harbor/external-id-secret-arn-tenant-a",
"/harbor/external-id-secret-arn-tenant-b",
"/harbor/member-role-arn-tenant-a",
"/harbor/member-role-arn-tenant-b",
]
for n in expected_ssm:
try:
get_ssm(n)
check(f"SSM pointer exists: {n}", True)
except Exception as e:
check(f"SSM pointer exists: {n}", False, str(e))
ORCH_ARN = get_ssm("/harbor/orchestrator/role-arn")
SECRET_A_ARN = get_ssm("/harbor/external-id-secret-arn-tenant-a")
SECRET_B_ARN = get_ssm("/harbor/external-id-secret-arn-tenant-b")
MEMBER_A_ARN = get_ssm("/harbor/member-role-arn-tenant-a")
MEMBER_B_ARN = get_ssm("/harbor/member-role-arn-tenant-b")
# ---------------------------------------------------------------------------
print("\n=== Path-based enumeration ===")
orch_roles = iam.list_roles(PathPrefix="/orchestrator/")["Roles"]
member_roles = iam.list_roles(PathPrefix="/member/")["Roles"]
check("Exactly one role under /orchestrator/", len(orch_roles) == 1, f"found {len(orch_roles)}")
member_names = sorted(r["RoleName"] for r in member_roles)
check(
"Two member roles named correctly under /member/",
member_names == ["MemberRole-Tenant-A", "MemberRole-Tenant-B"],
f"found {member_names}",
)
# ---------------------------------------------------------------------------
print("\n=== Member role trust documents (the core defenses) ===")
EIDS = {
"MemberRole-Tenant-A": get_secret_value(SECRET_A_ARN),
"MemberRole-Tenant-B": get_secret_value(SECRET_B_ARN),
}
check(
"ExternalId values differ between tenants",
EIDS["MemberRole-Tenant-A"] != EIDS["MemberRole-Tenant-B"],
)
check("Tenant-A ExternalId >= 32 chars", len(EIDS["MemberRole-Tenant-A"]) >= 32)
check("Tenant-B ExternalId >= 32 chars", len(EIDS["MemberRole-Tenant-B"]) >= 32)
for tenant_role in ("MemberRole-Tenant-A", "MemberRole-Tenant-B"):
role = get_role(tenant_role)
trust = role["AssumeRolePolicyDocument"]
stmts = as_list(trust.get("Statement"))
check(f"{tenant_role}: trust has exactly one statement", len(stmts) == 1)
s = stmts[0]
# Action == sts:AssumeRole only
actions = as_list(s.get("Action"))
check(
f"{tenant_role}: Action is exactly ['sts:AssumeRole']",
actions == ["sts:AssumeRole"],
f"got {actions}",
)
# Principal must be the orchestrator ARN, not *
principal = s.get("Principal", {})
aws_p = as_list(principal.get("AWS"))
check(
f"{tenant_role}: Principal.AWS is orchestrator role arn (not '*')",
aws_p == [ORCH_ARN],
f"got {aws_p}",
)
check(
f"{tenant_role}: Principal does not contain wildcard",
"*" not in aws_p and principal != "*",
)
cond = s.get("Condition", {}) or {}
# ExternalId condition (StringEquals)
se = cond.get("StringEquals", {}) or {}
eid_in_trust = se.get("sts:ExternalId")
check(
f"{tenant_role}: Condition.StringEquals['sts:ExternalId'] present",
eid_in_trust is not None,
)
check(
f"{tenant_role}: trust ExternalId matches secret value",
eid_in_trust == EIDS[tenant_role],
)
# aws:SourceArn condition (ArnEquals/ArnLike or StringEquals , accept any)
src_seen = None
for op in ("ArnEquals", "ArnLike", "StringEquals"):
v = (cond.get(op, {}) or {}).get("aws:SourceArn")
if v:
src_seen = (op, v)
break
check(
f"{tenant_role}: Condition has aws:SourceArn (ArnEquals/ArnLike/StringEquals)",
src_seen is not None,
)
if src_seen:
check(
f"{tenant_role}: aws:SourceArn equals orchestrator role arn",
src_seen[1] == ORCH_ARN,
f"op={src_seen[0]} val={src_seen[1]}",
)
# ---------------------------------------------------------------------------
print("\n=== Member identity policies , bucket isolation ===")
expectations = {
"MemberRole-Tenant-A": ("tenant-a-bucket", "tenant-b-bucket"),
"MemberRole-Tenant-B": ("tenant-b-bucket", "tenant-a-bucket"),
}
for role_name, (own, other) in expectations.items():
pols = iam.list_role_policies(RoleName=role_name)["PolicyNames"]
check(f"{role_name}: at least one inline policy", len(pols) >= 1)
for pol_name in pols:
doc = get_inline(role_name, pol_name)
wild, why = policy_has_wildcard_resource_or_action(doc, ("s3", "kms"))
check(f"{role_name}/{pol_name}: no wildcard resource/action for s3 or kms", not wild, why)
all_resources: list[str] = []
for stmt in as_list(doc.get("Statement")):
all_resources.extend(as_list(stmt.get("Resource")))
own_arns = {f"arn:aws:s3:::{own}", f"arn:aws:s3:::{own}/*"}
check(
f"{role_name}/{pol_name}: references own bucket arn(s)",
any(r in own_arns for r in all_resources),
f"resources={all_resources}",
)
check(
f"{role_name}/{pol_name}: never references other tenant bucket",
not any(other in r for r in all_resources),
f"resources={all_resources}",
)
check(
f"{role_name}/{pol_name}: no Resource:'*'",
"*" not in all_resources,
)
# ---------------------------------------------------------------------------
print("\n=== Orchestrator identity policy , least privilege ===")
orch_policies = iam.list_role_policies(RoleName="OrchestratorRole")["PolicyNames"]
check("OrchestratorRole has at least one inline policy", len(orch_policies) >= 1)
all_assume_resources: list[str] = []
all_secret_resources: list[str] = []
for pol_name in orch_policies:
doc = get_inline("OrchestratorRole", pol_name)
wild, why = policy_has_wildcard_resource_or_action(doc, ("s3", "kms", "sts", "secretsmanager"))
check(f"OrchestratorRole/{pol_name}: no wildcard action/resource", not wild, why)
for stmt in as_list(doc.get("Statement")):
if stmt.get("Effect") != "Allow":
continue
actions = set(as_list(stmt.get("Action")))
if "sts:AssumeRole" in actions:
all_assume_resources.extend(as_list(stmt.get("Resource")))
if "secretsmanager:GetSecretValue" in actions:
all_secret_resources.extend(as_list(stmt.get("Resource")))
check(
"Orchestrator can AssumeRole on exactly the two member ARNs",
sorted(all_assume_resources) == sorted([MEMBER_A_ARN, MEMBER_B_ARN]),
f"got {sorted(all_assume_resources)}",
)
check("Orchestrator AssumeRole is not '*'", "*" not in all_assume_resources)
check(
"Orchestrator GetSecretValue scoped to the two ExternalId secret ARNs",
sorted(all_secret_resources) == sorted([SECRET_A_ARN, SECRET_B_ARN]),
f"got {sorted(all_secret_resources)}",
)
check("Orchestrator GetSecretValue is not '*'", "*" not in all_secret_resources)
# ---------------------------------------------------------------------------
print("\n=== Resource policy on each ExternalId secret ===")
for label, arn in (("tenant-a", SECRET_A_ARN), ("tenant-b", SECRET_B_ARN)):
rp = sec.get_resource_policy(SecretId=arn)
rp_doc = json.loads(rp["ResourcePolicy"])
found = False
for stmt in as_list(rp_doc.get("Statement")):
if stmt.get("Effect") != "Allow":
continue
principal = stmt.get("Principal", {}) or {}
aws_p = as_list(principal.get("AWS"))
actions = as_list(stmt.get("Action"))
if ORCH_ARN in aws_p and "secretsmanager:GetSecretValue" in actions:
found = True
if "*" in aws_p:
check(f"{label} secret resource policy: no wildcard principal", False)
check(
f"{label} secret resource policy: orchestrator role allowed GetSecretValue",
found,
)
# ---------------------------------------------------------------------------
print("\n=== KMS key policy , no wildcard for kms actions ===")
import re
key_id_match = re.search(r"key/([0-9a-f-]+)$", boto3.client("kms").describe_key(KeyId="alias/cross-account-cmk")["KeyMetadata"]["Arn"])
kms = boto3.client("kms")
key_arn = kms.describe_key(KeyId="alias/cross-account-cmk")["KeyMetadata"]["Arn"]
key_pol = json.loads(kms.get_key_policy(KeyId=key_arn, PolicyName="default")["Policy"])
key_wild_principal = False
for stmt in as_list(key_pol.get("Statement")):
if stmt.get("Effect") != "Allow":
continue
p = stmt.get("Principal", {})
if p == "*":
key_wild_principal = True
elif isinstance(p, dict):
if any(v == "*" for v in p.values() if isinstance(v, str)):
key_wild_principal = True
for v in p.values():
if isinstance(v, list) and "*" in v:
key_wild_principal = True
check("KMS key policy: no wildcard principal", not key_wild_principal)
# ---------------------------------------------------------------------------
print("\n=== Summary ===")
print(f"checks run : {len(checks)}")
print(f"failures : {len(findings)}")
if findings:
print("\nFINDINGS:")
for f in findings:
print(f" - {f}")
sys.exit(1)
print("ALL CHECKS PASSED")
Run policy-shape verifier
chmod +x /app/scripts/20_verify.py && python3 /app/scripts/20_verify.py
=== SSM pointers present === PASS SSM pointer exists: /harbor/orchestrator/role-arn PASS SSM pointer exists: /harbor/external-id-secret-arn-tenant-a PASS SSM pointer exists: /harbor/external-id-secret-arn-tenant-b PASS SSM pointer exists: /harbor/member-role-arn-tenant-a PASS SSM pointer exists: /harbor/member-role-arn-tenant-b === Path-based enumeration === PASS Exactly one role under /orchestrator/ PASS Two member roles named correctly under /member/ === Member role trust documents (the core defenses) === PASS ExternalId values differ between tenants PASS Tenant-A ExternalId >= 32 chars PASS Tenant-B ExternalId >= 32 chars PASS MemberRole-Tenant-A: trust has exactly one statement PASS MemberRole-Tenant-A: Action is exactly ['sts:AssumeRole'] PASS MemberRole-Tenant-A: Principal.AWS is orchestrator role arn (not '*') PASS MemberRole-Tenant-A: Principal does not contain wildcard PASS MemberRole-Tenant-A: Condition.StringEquals['sts:ExternalId'] present PASS MemberRole-Tenant-A: trust ExternalId matches secret value PASS MemberRole-Tenant-A: Condition has aws:SourceArn (ArnEquals/ArnLike/StringEquals) PASS MemberRole-Tenant-A: aws:SourceArn equals orchestrator role arn PASS MemberRole-Tenant-B: trust has exactly one statement PASS MemberRole-Tenant-B: Action is exactly ['sts:AssumeRole'] PASS MemberRole-Tenant-B: Principal.AWS is orchestrator role arn (not '*') PASS MemberRole-Tenant-B: Principal does not contain wildcard PASS MemberRole-Tenant-B: Condition.StringEquals['sts:ExternalId'] present PASS MemberRole-Tenant-B: trust ExternalId matches secret value PASS MemberRole-Tenant-B: Condition has aws:SourceArn (ArnEquals/ArnLike/StringEquals) PASS MemberRole-Tenant-B: aws:SourceArn equals orchestrator role arn === Member identity policies , bucket isolation === PASS MemberRole-Tenant-A: at least one inline policy PASS MemberRole-Tenant-A/TenantABucketAccess: no wildcard resource/action for s3 or kms PASS MemberRole-Tenant-A/TenantABucketAccess: references own bucket arn(s) PASS MemberRole-Tenant-A/TenantABucketAccess: never references other tenant bucket PASS MemberRole-Tenant-A/TenantABucketAccess: no Resource:'*' PASS MemberRole-Tenant-B: at least one inline policy PASS MemberRole-Tenant-B/TenantBBucketAccess: no wildcard resource/action for s3 or kms PASS MemberRole-Tenant-B/TenantBBucketAccess: references own bucket arn(s) PASS MemberRole-Tenant-B/TenantBBucketAccess: never references other tenant bucket PASS MemberRole-Tenant-B/TenantBBucketAccess: no Resource:'*' === Orchestrator identity policy , least privilege === PASS OrchestratorRole has at least one inline policy PASS OrchestratorRole/OrchestratorBaseAccess: no wildcard action/resource PASS Orchestrator can AssumeRole on exactly the two member ARNs PASS Orchestrator AssumeRole is not '*' PASS Orchestrator GetSecretValue scoped to the two ExternalId secret ARNs PASS Orchestrator GetSecretValue is not '*' === Resource policy on each ExternalId secret === PASS tenant-a secret resource policy: orchestrator role allowed GetSecretValue PASS tenant-b secret resource policy: orchestrator role allowed GetSecretValue === KMS key policy , no wildcard for kms actions === PASS KMS key policy: no wildcard principal === Summary === checks run : 45 failures : 0 ALL CHECKS PASSED [stdout] === SSM pointers present === PASS SSM pointer exists: /harbor/orchestrator/role-arn PASS SSM pointer exists: /harbor/external-id-secret-arn-tenant-a PASS SSM pointer exists: /harbor/external-id-secret-arn-tenant-b PASS SSM pointer exists: /harbor/member-role-arn-tenant-a PASS SSM pointer exists: /harbor/member-role-arn-tenant-b === Path-based enumeration === PASS Exactly one role under /orchestrator/ PASS Two member roles named correctly under /member/ === Member role trust documents (the core defenses) === PASS ExternalId values differ between tenants PASS Tenant-A ExternalId >= 32 chars PASS Tenant-B ExternalId >= 32 chars PASS MemberRole-Tenant-A: trust has exactly one statement PASS MemberRole-Tenant-A: Action is exactly ['sts:AssumeRole'] PASS MemberRole-Tenant-A: Principal.AWS is orchestrator role arn (not '*') PASS MemberRole-Tenant-A: Principal does not contain wildcard PASS MemberRole-Tenant-A: Condition.StringEquals['sts:ExternalId'] present PASS MemberRole-Tenant-A: trust ExternalId matches secret value PASS MemberRole-Tenant-A: Condition has aws:SourceArn (ArnEquals/ArnLike/StringEquals) PASS MemberRole-Tenant-A: aws:SourceArn equals orchestrator role arn PASS MemberRole-Tenant-B: trust has exactly one statement PASS MemberRole-Tenant-B: Action is exactly ['sts:AssumeRole'] PASS MemberRole-Tenant-B: Principal.AWS is orchestrator role arn (not '*') PASS MemberRole-Tenant-B: Principal does not contain wildcard PASS MemberRole-Tenant-B: Condition.StringEquals['sts:ExternalId'] present PASS MemberRole-Tenant-B: trust ExternalId matches secret value PASS MemberRole-Tenant-B: Condition has aws:SourceArn (ArnEquals/ArnLike/StringEquals) PASS MemberRole-Tenant-B: aws:SourceArn equals orchestrator role arn === Member identity policies , bucket isolation === PASS MemberRole-Tenant-A: at least one inline policy PASS MemberRole-Tenant-A/TenantABucketAccess: no wildcard resource/action for s3 or kms PASS MemberRole-Tenant-A/TenantABucketAccess: references own bucket arn(s) PASS MemberRole-Tenant-A/TenantABucketAccess: never references other tenant bucket PASS MemberRole-Tenant-A/TenantABucketAccess: no Resource:'*' PASS MemberRole-Tenant-B: at least one inline policy PASS MemberRole-Tenant-B/TenantBBucketAccess: no wildcard resource/action for s3 or kms PASS MemberRole-Tenant-B/TenantBBucketAccess: references own bucket arn(s) PASS MemberRole-Tenant-B/TenantBBucketAccess: never references other tenant bucket PASS MemberRole-Tenant-B/TenantBBucketAccess: no Resource:'*' === Orchestrator identity policy , least privilege === PASS OrchestratorRole has at least one inline policy PASS OrchestratorRole/OrchestratorBaseAccess: no wildcard action/resource PASS Orchestrator can AssumeRole on exactly the two member ARNs PASS Orchestrator AssumeRole is not '*' PASS Orchestrator GetSecretValue scoped to the two ExternalId secret ARNs PASS Orchestrator GetSecretValue is not '*' === Resource policy on each ExternalId secret === PASS tenant-a secret resource policy: orchestrator role allowed GetSecretValue PASS tenant-b secret resource policy: orchestrator role allowed GetSecretValue === KMS key policy , no wildcard for kms actions === PASS KMS key policy: no wildcard principal === Summary === checks run : 45 failures : 0 ALL CHECKS PASSED
/logs/agent/sessions/projects/-app/memory/MEMORY.md
File does not exist. Note: your current working directory is /app. [error] tool reported failure
--- name: cross-account trust hardening (SOC2 carryover) description: Why the cross-account assume-role chain in /app was rebuilt and what the audit shape requires type: project --- The `/app` LocalStack setup was rebuilt to satisfy a SOC2 carryover finding before the next sprint. Auditor flagged two attacks the prior trust chain didn't defend against: third-party impersonation of member-role ARNs and AWS-service confused-deputy. Both must be blocked at the same time on every member role. **Why:** SOC2 carryover at risk; auditor will re-check before next sprint cutoff. **How to apply:** Any change to member-role trust must keep BOTH conditions , `StringEquals['sts:ExternalId']` (per-tenant secret value) AND `aws:SourceArn` (= orchestrator role ARN). Removing either fails the audit. Principal must be the orchestrator role ARN, never `*`. Action must be exactly `sts:AssumeRole`. Each tenant's ExternalId is distinct and lives in Secrets Manager (KMS-CMK encrypted) with a resource policy naming the orchestrator role as `Principal.AWS` , identity-side scope is not enough on its own.
---
name: LocalStack /app layout for cross-account trust
description: Where the setup script, verifier, happy path, and SSM pointers live
type: reference
---
LocalStack endpoint: `http://localstack:4566` (already in `AWS_ENDPOINT_URL`). Creds `AWS_ACCESS_KEY_ID=test` / same secret, region `us-east-1`. IAM is mocked at runtime , verification must check policy DOCUMENT shape, not runtime denials.
- `/app/scripts/00_setup.sh` , builds users, KMS CMK, OrchestratorRole, two MemberRoles, two Secrets Manager secrets, S3 buckets, SSM pointers. Idempotent.
- `/app/scripts/10_happy_path.sh` , agent-runner → OrchestratorRole → GetSecretValue → AssumeRole MemberRole-Tenant-A with `--external-id` → PutObject in `tenant-a-bucket`.
- `/app/scripts/20_verify.py` , auditor-style shape checks (45 assertions). Reads SSM pointers under `/harbor/...` to find ARNs.
- `/app/policies/` , generated JSON for trust + inline policies.
- `/app/state/state.env`, `/app/state/agent-runner.creds` , derived ARNs and access keys consumed by happy-path runner.
SSM pointer names the verifier uses: `/harbor/orchestrator/role-arn`, `/harbor/external-id-secret-arn-tenant-{a,b}`, `/harbor/member-role-arn-tenant-{a,b}`.
- [Cross-account trust hardening (SOC2)](project_cross_account_trust.md) , both ExternalId AND aws:SourceArn must guard every member-role trust; resource policy on each ExternalId secret too - [LocalStack /app layout](reference_localstack_layout.md) , where setup, happy-path, and verifier scripts live; IAM is not enforced at runtime so checks are shape-based
No file changes, the agent left the workspace untouched.
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/Q12NZPayH58QNn9X-k3z0/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 20 items
../tests/test_state.py::test_01_orchestrator_role_exists_at_orchestrator_path PASSED [ 5%]
../tests/test_state.py::test_02_member_roles_exist_at_member_path PASSED [ 10%]
../tests/test_state.py::test_03_external_id_secrets_exist_with_min_length PASSED [ 15%]
../tests/test_state.py::test_04_both_tenant_buckets_exist PASSED [ 20%]
../tests/test_state.py::test_05_ssm_pointers_resolve PASSED [ 25%]
../tests/test_state.py::test_06_member_trusts_have_externalid_stringequals_matching_secret PASSED [ 30%]
../tests/test_state.py::test_07_member_trusts_have_sourcearn_equal_to_orchestrator_arn FAILED [ 35%]
../tests/test_state.py::test_08_member_trusts_action_is_assumerole_only PASSED [ 40%]
../tests/test_state.py::test_09_member_trusts_principal_is_orchestrator_not_wildcard PASSED [ 45%]
../tests/test_state.py::test_10_externalids_differ_per_tenant PASSED [ 50%]
../tests/test_state.py::test_11_orchestrator_identity_lists_member_arns_explicitly_no_wildcard PASSED [ 55%]
../tests/test_state.py::test_12_orchestrator_secret_get_scoped_to_two_secret_arns PASSED [ 60%]
../tests/test_state.py::test_13_orchestrator_kms_decrypt_scoped_to_cmk_arn PASSED [ 65%]
../tests/test_state.py::test_14_member_inline_policies_scope_to_own_tenant_bucket_only PASSED [ 70%]
../tests/test_state.py::test_15_secret_resource_policies_admit_orchestrator PASSED [ 75%]
../tests/test_state.py::test_16_kms_key_policy_has_account_root_statement FAILED [ 80%]
../tests/test_state.py::test_17_no_admin_managed_policies_attached_to_any_role PASSED [ 85%]
../tests/test_state.py::test_18_e2e_assume_with_externalid_then_put_object_succeeds PASSED [ 90%]
../tests/test_state.py::test_19_member_a_inline_does_not_reference_tenant_b_bucket PASSED [ 95%]
../tests/test_state.py::test_20_orchestrator_has_no_wildcard_assume_role_resource PASSED [100%]
=================================== FAILURES ===================================
________ test_07_member_trusts_have_sourcearn_equal_to_orchestrator_arn ________
def test_07_member_trusts_have_sourcearn_equal_to_orchestrator_arn():
"""Trust SourceArn equals orchestrator."""
src_a = _find_sourcearn(_stmts(_trust_doc(ROLE_MEMBER_A)))
src_b = _find_sourcearn(_stmts(_trust_doc(ROLE_MEMBER_B)))
> assert src_a == ORCH_ARN_EXPECTED, f"tenant-A trust SourceArn={src_a!r}, expected {ORCH_ARN_EXPECTED}"
E AssertionError: tenant-A trust SourceArn=None, expected arn:aws:iam::000000000000:role/orchestrator/OrchestratorRole
E assert None == 'arn:aws:iam::000000000000:role/orchestrator/OrchestratorRole'
/tests/test_state.py:234: AssertionError
______________ test_16_kms_key_policy_has_account_root_statement _______________
def test_16_kms_key_policy_has_account_root_statement():
"""KMS key policy has root statement."""
cmk_arn = _kms_key_arn()
kms = _client("kms")
pol = kms.get_key_policy(KeyId=cmk_arn, PolicyName="default")["Policy"]
doc = json.loads(pol)
found_root = False
for s in _stmts(doc):
p = s.get("Principal", {})
aws = p.get("AWS") if isinstance(p, dict) else None
arns = _as_list(aws)
if any(a.endswith(":root") for a in arns):
actions = _as_list(s.get("Action"))
if actions == ["kms:*"] or "kms:*" in actions:
found_root = True
> assert found_root, (
"KMS key policy missing the 'EnableIAMUserPermissions' root statement"
)
E AssertionError: KMS key policy missing the 'EnableIAMUserPermissions' root statement
E assert False
/tests/test_state.py:388: AssertionError
=============================== warnings summary ===============================
test_state.py: 56 warnings
/root/.cache/uv/archive-v0/Q12NZPayH58QNn9X-k3z0/lib/python3.12/site-packages/botocore/auth.py:424: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
datetime_now = datetime.datetime.utcnow()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED ../tests/test_state.py::test_01_orchestrator_role_exists_at_orchestrator_path
PASSED ../tests/test_state.py::test_02_member_roles_exist_at_member_path
PASSED ../tests/test_state.py::test_03_external_id_secrets_exist_with_min_length
PASSED ../tests/test_state.py::test_04_both_tenant_buckets_exist
PASSED ../tests/test_state.py::test_05_ssm_pointers_resolve
PASSED ../tests/test_state.py::test_06_member_trusts_have_externalid_stringequals_matching_secret
PASSED ../tests/test_state.py::test_08_member_trusts_action_is_assumerole_only
PASSED ../tests/test_state.py::test_09_member_trusts_principal_is_orchestrator_not_wildcard
PASSED ../tests/test_state.py::test_10_externalids_differ_per_tenant
PASSED ../tests/test_state.py::test_11_orchestrator_identity_lists_member_arns_explicitly_no_wildcard
PASSED ../tests/test_state.py::test_12_orchestrator_secret_get_scoped_to_two_secret_arns
PASSED ../tests/test_state.py::test_13_orchestrator_kms_decrypt_scoped_to_cmk_arn
PASSED ../tests/test_state.py::test_14_member_inline_policies_scope_to_own_tenant_bucket_only
PASSED ../tests/test_state.py::test_15_secret_resource_policies_admit_orchestrator
PASSED ../tests/test_state.py::test_17_no_admin_managed_policies_attached_to_any_role
PASSED ../tests/test_state.py::test_18_e2e_assume_with_externalid_then_put_object_succeeds
PASSED ../tests/test_state.py::test_19_member_a_inline_does_not_reference_tenant_b_bucket
PASSED ../tests/test_state.py::test_20_orchestrator_has_no_wildcard_assume_role_resource
FAILED ../tests/test_state.py::test_07_member_trusts_have_sourcearn_equal_to_orchestrator_arn
FAILED ../tests/test_state.py::test_16_kms_key_policy_has_account_root_statement
================== 2 failed, 18 passed, 56 warnings in 0.55s ===================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_b1d17cd6319044c2. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_b1d17cd6319044c2 · verifier authoritative; classifier explanatory.