tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Check working directory contents
ls /app && echo "---" && ls /app/.. 2>/dev/null | head -20
--- app bin boot dev etc home installed-agent lib lib64 logs media mnt opt proc root run sbin srv sys tmp [stdout] --- app bin boot dev etc home installed-agent lib lib64 logs media mnt opt proc root run sbin srv sys tmp
Check AWS CLI availability
which aws awslocal && awslocal --version 2>&1 | head -5
/usr/local/bin/aws /opt/venv/bin/awslocal aws-cli/2.34.38 Python/3.14.4 Linux/6.1.158 exe/x86_64.ubuntu.24 [stdout] /usr/local/bin/aws /opt/venv/bin/awslocal aws-cli/2.34.38 Python/3.14.4 Linux/6.1.158 exe/x86_64.ubuntu.24
Check LocalStack connection
awslocal sts get-caller-identity 2>&1; echo "---"; awslocal --version 2>&1; echo "---"; env | grep -iE "aws|localstack" | head
{
"UserId": "000000000000",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
aws-cli/2.34.38 Python/3.14.4 Linux/6.1.158 exe/x86_64.ubuntu.24
---
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": "000000000000",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
aws-cli/2.34.38 Python/3.14.4 Linux/6.1.158 exe/x86_64.ubuntu.24
---
AWS_DEFAULT_REGION=us-east-1
AWS_PAGER=
AWS_SECRET_ACCESS_KEY=test
AWS_ACCESS_KEY_ID=test
AWS_ENDPOINT_URL=http://localstack:4566{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Create working directories
mkdir -p /app/policies /app/lambda /app/build && ls /app
build lambda policies [stdout] build lambda policies
Create KMS CMK + alias
awslocal kms create-key --description "saas orders cmk" --key-usage ENCRYPT_DECRYPT --query 'KeyMetadata.KeyId' --output text > /app/build/kms_key_id.txt KEYID=$(cat /app/build/kms_key_id.txt) echo "KeyId=$KEYID" awslocal kms create-alias --alias-name alias/saas-orders-cmk --target-key-id "$KEYID" awslocal kms list-aliases --query "Aliases[?AliasName=='alias/saas-orders-cmk']"
KeyId=feb9ab20-856b-4948-ae66-ac042edfcfe3
[
{
"AliasName": "alias/saas-orders-cmk",
"AliasArn": "arn:aws:kms:us-east-1:000000000000:alias/saas-orders-cmk",
"TargetKeyId": "feb9ab20-856b-4948-ae66-ac042edfcfe3",
"CreationDate": "2026-04-29T18:24:18.204850+00:00"
}
]
[stdout]
KeyId=feb9ab20-856b-4948-ae66-ac042edfcfe3
[
{
"AliasName": "alias/saas-orders-cmk",
"AliasArn": "arn:aws:kms:us-east-1:000000000000:alias/saas-orders-cmk",
"TargetKeyId": "feb9ab20-856b-4948-ae66-ac042edfcfe3",
"CreationDate": "2026-04-29T18:24:18.204850+00:00"
}
]Create SaasOrders table with CMK
awslocal dynamodb create-table \ --table-name SaasOrders \ --attribute-definitions AttributeName=TenantID,AttributeType=S AttributeName=OrderID,AttributeType=S \ --key-schema AttributeName=TenantID,KeyType=HASH AttributeName=OrderID,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST \ --sse-specification "Enabled=true,SSEType=KMS,KMSMasterKeyId=alias/saas-orders-cmk" \ --query 'TableDescription.TableArn' --output text
arn:aws:dynamodb:us-east-1:000000000000:table/SaasOrders [stdout] arn:aws:dynamodb:us-east-1:000000000000:table/SaasOrders
Enable PITR
awslocal dynamodb update-continuous-backups \ --table-name SaasOrders \ --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true \ --query 'ContinuousBackupsDescription.PointInTimeRecoveryDescription.PointInTimeRecoveryStatus' --output text
ENABLED [stdout] ENABLED
Seed 6 items
for spec in "acme:O-1001:120.50:hunter2-acme-1" "acme:O-1002:75.00:hunter2-acme-2" "globex:O-2001:540.25:p4ssw0rd-globex-1" "globex:O-2002:9.99:p4ssw0rd-globex-2" "globex-eu:O-3001:300.00:eu-secret-1" "globex-eu:O-3002:42.42:eu-secret-2"; do
IFS=":" read -r T O A P <<< "$spec"
awslocal dynamodb put-item --table-name SaasOrders --item "{\"TenantID\":{\"S\":\"$T\"},\"OrderID\":{\"S\":\"$O\"},\"amount\":{\"N\":\"$A\"},\"password\":{\"S\":\"$P\"}}"
done
awslocal dynamodb scan --table-name SaasOrders --select COUNT --query 'Count'6 [stdout] 6
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
Create vendor exec role
awslocal iam create-role --role-name TenantTokenVendorRole \ --assume-role-policy-document file:///app/policies/vendor-trust.json \ --query 'Role.Arn' --output text > /app/build/vendor_role_arn.txt cat /app/build/vendor_role_arn.txt
arn:aws:iam::000000000000:role/TenantTokenVendorRole [stdout] arn:aws:iam::000000000000:role/TenantTokenVendorRole
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VendorAssumesWithTenantTag",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/TenantTokenVendorRole"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
],
"Condition": {
"Null": {
"aws:RequestTag/TenantID": "false"
},
"StringEquals": {
"aws:RequestTag/TenantID": [
"acme",
"globex",
"globex-eu"
]
}
}
}
]
}
Create TenantDataRole
awslocal iam create-role --role-name TenantDataRole \ --assume-role-policy-document file:///app/policies/data-role-trust.json \ --max-session-duration 3600 \ --query 'Role.Arn' --output text > /app/build/data_role_arn.txt cat /app/build/data_role_arn.txt
arn:aws:iam::000000000000:role/TenantDataRole [stdout] arn:aws:iam::000000000000:role/TenantDataRole
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TenantPartitionScopedReadWrite",
"Effect": "Allow",
"Action": [
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:BatchGetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:BatchWriteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/SaasOrders",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": [
"${aws:PrincipalTag/TenantID}"
],
"dynamodb:Attributes": [
"TenantID",
"OrderID",
"amount"
]
},
"StringEqualsIfExists": {
"dynamodb:Select": "SPECIFIC_ATTRIBUTES",
"dynamodb:ReturnValues": [
"NONE",
"UPDATED_OLD",
"UPDATED_NEW"
]
}
}
}
]
}
Attach data role identity policy
awslocal iam put-role-policy \ --role-name TenantDataRole \ --policy-name TenantPartitionAccess \ --policy-document file:///app/policies/data-role-identity.json echo "OK"
OK [stdout] OK
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeTenantDataRoleOnly",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::000000000000:role/TenantDataRole"
},
{
"Sid": "ScopedLogsToOwnLogGroup",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor:*"
]
},
{
"Sid": "CreateOwnLogGroup",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor"
}
]
}
Attach vendor inline policy
awslocal iam put-role-policy \ --role-name TenantTokenVendorRole \ --policy-name VendorAssumeAndLogs \ --policy-document file:///app/policies/vendor-inline.json echo "OK"
OK [stdout] OK
import json
import os
import re
import boto3
_TENANT_RE = re.compile(r"^[a-z][a-z0-9\-]{1,62}$")
_DATA_ROLE_ARN = os.environ["DATA_ROLE_ARN"]
_MAX_DURATION_SECONDS = 900
_sts = boto3.client("sts")
def _resp(status, body):
return {
"statusCode": status,
"headers": {"Content-Type": "application/json"},
"body": json.dumps(body),
}
def lambda_handler(event, context):
qs = (event or {}).get("queryStringParameters") or {}
tenant = qs.get("tenant", "")
if not tenant or not _TENANT_RE.match(tenant) or "--" in tenant:
return _resp(400, {"error": "invalid tenant"})
out = _sts.assume_role(
RoleArn=_DATA_ROLE_ARN,
RoleSessionName=f"tenant-{tenant}",
DurationSeconds=_MAX_DURATION_SECONDS,
Tags=[{"Key": "TenantID", "Value": tenant}],
TransitiveTagKeys=["TenantID"],
)
creds = out["Credentials"]
return _resp(
200,
{
"tenant": tenant,
"AccessKeyId": creds["AccessKeyId"],
"SecretAccessKey": creds["SecretAccessKey"],
"SessionToken": creds["SessionToken"],
"Expiration": creds["Expiration"].isoformat(),
},
)
Package lambda
cd /app/lambda && zip -q -r /app/build/lambda.zip handler.py && ls -la /app/build/lambda.zip
-rw-r--r-- 1 root root 760 Apr 29 18:25 /app/build/lambda.zip [stdout] -rw-r--r-- 1 root root 760 Apr 29 18:25 /app/build/lambda.zip
Create lambda function
awslocal logs create-log-group --log-group-name /aws/lambda/tenant-token-vendor 2>&1 || true
awslocal lambda create-function \
--function-name tenant-token-vendor \
--runtime python3.11 \
--role arn:aws:iam::000000000000:role/TenantTokenVendorRole \
--handler handler.lambda_handler \
--zip-file fileb:///app/build/lambda.zip \
--environment "Variables={DATA_ROLE_ARN=arn:aws:iam::000000000000:role/TenantDataRole}" \
--timeout 10 \
--query 'FunctionArn' --output text > /app/build/lambda_arn.txt
cat /app/build/lambda_arn.txtarn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor [stdout] arn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor
Smoke-test lambda directly
for i in 1 2 3 4 5; do
STATE=$(awslocal lambda get-function --function-name tenant-token-vendor --query 'Configuration.State' --output text 2>/dev/null)
if [ "$STATE" = "Active" ]; then echo "active"; break; fi
sleep 1
done
awslocal lambda invoke --function-name tenant-token-vendor \
--payload '{"queryStringParameters":{"tenant":"acme"}}' \
--cli-binary-format raw-in-base64-out /tmp/inv.json && cat /tmp/inv.jsonactive
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": "{\"tenant\": \"acme\", \"AccessKeyId\": \"LSIAQAAAAAAAFOEKC7QX\", \"SecretAccessKey\": \"AcWNyUjC6s3lK4iDhv1X2rERRj+ZiuSDO54Pe/wK\", \"SessionToken\": \"FQoGZXIvYXdzEZx2dVr2vtrEJW/Xcacl9gNdCkzUaXtYERPL2yFW=HmtVBm7LUU+X26pBDSVuGuZA2h8ZDlKDdBi7wA1rOmbqm1Gso2nmdTWm6syBh+Vi96YdjscQX5FbXEQfKwRzUdv9qVBSYA7vSAe7u7uWENwjAgUVJZGvGj/ORyDru1G1wOQnAk+IjiX=+Qavlt1ytF769ntTJ124ZQXKkEXvNlFMEX3tUmkGqjazx4ca/2jhKwmgZ4++xrTIVsoIn6LqJO9LkntuZWOcaUFykD1Js4n8z5hGjZspHQCP/Eb9WLxMsSwb/gZI6iOneNeA2D+ULYfEn/=45SRicTq/ddiBnFA/RBZ\", \"Expiration\": \"2026-04-29T18:41:13.761614+00:00\"}"}
[stdout]
active
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": "{\"tenant\": \"acme\", \"AccessKeyId\": \"LSIAQAAAAAAAFOEKC7QX\", \"SecretAccessKey\": \"AcWNyUjC6s3lK4iDhv1X2rERRj+ZiuSDO54Pe/wK\", \"SessionToken\": \"FQoGZXIvYXdzEZx2dVr2vtrEJW/Xcacl9gNdCkzUaXtYERPL2yFW=HmtVBm7LUU+X26pBDSVuGuZA2h8ZDlKDdBi7wA1rOmbqm1Gso2nmdTWm6syBh+Vi96YdjscQX5FbXEQfKwRzUdv9qVBSYA7vSAe7u7uWENwjAgUVJZGvGj/ORyDru1G1wOQnAk+IjiX=+Qavlt1ytF769ntTJ124ZQXKkEXvNlFMEX3tUmkGqjazx4ca/2jhKwmgZ4++xrTIVsoIn6LqJO9LkntuZWOcaUFykD1Js4n8z5hGjZspHQCP/Eb9WLxMsSwb/gZI6iOneNeA2D+ULYfEn/=45SRicTq/ddiBnFA/RBZ\", \"Expiration\": \"2026-04-29T18:41:13.761614+00:00\"}"}Validate isolation end-to-end
BODY=$(awslocal lambda invoke --function-name tenant-token-vendor \
--payload '{"queryStringParameters":{"tenant":"acme"}}' \
--cli-binary-format raw-in-base64-out /tmp/inv.json >/dev/null && cat /tmp/inv.json | python3 -c 'import sys,json; print(json.loads(sys.stdin.read())["body"])')
AK=$(echo "$BODY" | python3 -c 'import sys,json; print(json.loads(sys.stdin.read())["AccessKeyId"])')
SK=$(echo "$BODY" | python3 -c 'import sys,json; print(json.loads(sys.stdin.read())["SecretAccessKey"])')
ST=$(echo "$BODY" | python3 -c 'import sys,json; print(json.loads(sys.stdin.read())["SessionToken"])')
echo "--- own tenant query (acme): should succeed ---"
AWS_ACCESS_KEY_ID=$AK AWS_SECRET_ACCESS_KEY=$SK AWS_SESSION_TOKEN=$ST awslocal dynamodb query \
--table-name SaasOrders \
--key-condition-expression "TenantID = :t" \
--expression-attribute-values '{":t":{"S":"acme"}}' \
--projection-expression "TenantID,OrderID,amount" \
--select SPECIFIC_ATTRIBUTES 2>&1 | head -40
echo "--- cross-tenant query (globex): should fail ---"
AWS_ACCESS_KEY_ID=$AK AWS_SECRET_ACCESS_KEY=$SK AWS_SESSION_TOKEN=$ST awslocal dynamodb query \
--table-name SaasOrders \
--key-condition-expression "TenantID = :t" \
--expression-attribute-values '{":t":{"S":"globex"}}' \
--projection-expression "TenantID,OrderID,amount" \
--select SPECIFIC_ATTRIBUTES 2>&1 | head -10
echo "--- attempt to read password attr on own tenant: should fail ---"
AWS_ACCESS_KEY_ID=$AK AWS_SECRET_ACCESS_KEY=$SK AWS_SESSION_TOKEN=$ST awslocal dynamodb query \
--table-name SaasOrders \
--key-condition-expression "TenantID = :t" \
--expression-attribute-values '{":t":{"S":"acme"}}' \
--projection-expression "TenantID,OrderID,amount,#p" \
--expression-attribute-names '{"#p":"password"}' \
--select SPECIFIC_ATTRIBUTES 2>&1 | head -10--- own tenant query (acme): should succeed ---
{
"Items": [
{
"OrderID": {
"S": "O-1001"
},
"TenantID": {
"S": "acme"
},
"amount": {
"N": "120.5"
}
},
{
"OrderID": {
"S": "O-1002"
},
"TenantID": {
"S": "acme"
},
"amount": {
"N": "75"
}
}
],
"Count": 2,
"ScannedCount": 2,
"ConsumedCapacity": null
}
--- cross-tenant query (globex): should fail ---
{
"Items": [
{
"OrderID": {
"S": "O-2001"
},
"TenantID": {
"S": "globex"
},
"amount": {
--- attempt to read password attr on own tenant: should fail ---
{
"Items": [
{
"TenantID": {
"S": "acme"
},
"amount": {
"N": "120.5"
},
"password": {
[stdout]
--- own tenant query (acme): should succeed ---
{
"Items": [
{
"OrderID": {
"S": "O-1001"
},
"TenantID": {
"S": "acme"
},
"amount": {
"N": "120.5"
}
},
{
"OrderID": {
"S": "O-1002"
},
"TenantID": {
"S": "acme"
},
"amount": {
"N": "75"
}
}
],
"Count": 2,
"ScannedCount": 2,
"ConsumedCapacity": null
}
--- cross-tenant query (globex): should fail ---
{
"Items": [
{
"OrderID": {
"S": "O-2001"
},
"TenantID": {
"S": "globex"
},
"amount": {
--- attempt to read password attr on own tenant: should fail ---
{
"Items": [
{
"TenantID": {
"S": "acme"
},
"amount": {
"N": "120.5"
},
"password": {Check LocalStack IAM enforcement config
env | grep -iE "enforce|iam|localstack" 2>&1 | head; echo "---" curl -s http://localstack:4566/_localstack/info 2>&1 | head -40 echo "---" curl -s http://localstack:4566/_localstack/config 2>&1 | python3 -c 'import sys,json; d=json.loads(sys.stdin.read()); [print(k,v) for k,v in d.items() if "IAM" in k.upper() or "ENFORCE" in k.upper()]' 2>&1 | head -20
AWS_ENDPOINT_URL=http://localstack:4566
---
{"version": "2026.3.0:e28f8cfa4", "edition": "pro", "is_license_activated": true, "session_id": "458f20ac-f460-4a4c-96a5-06ab083aa6b2", "machine_id": "dkr_dc773c0a2fb6", "system": "Ubuntu 24.04.4 LTS,6.1.158,x86_64", "is_docker": true, "server_time_utc": "2026-04-29T18:26:42", "uptime": 232}---
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
[stdout]
AWS_ENDPOINT_URL=http://localstack:4566
---
{"version": "2026.3.0:e28f8cfa4", "edition": "pro", "is_license_activated": true, "session_id": "458f20ac-f460-4a4c-96a5-06ab083aa6b2", "machine_id": "dkr_dc773c0a2fb6", "system": "Ubuntu 24.04.4 LTS,6.1.158,x86_64", "is_docker": true, "server_time_utc": "2026-04-29T18:26:42", "uptime": 232}---
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)View LocalStack config raw
curl -s http://localstack:4566/_localstack/config 2>&1 | head -200
(Bash completed with no output)
Check LocalStack diagnose endpoint
curl -s -o /tmp/cfg.json -w "%{http_code}\n" http://localstack:4566/_localstack/config; ls -la /tmp/cfg.json; head -c 500 /tmp/cfg.json; echo
curl -s http://localstack:4566/_localstack/diagnose 2>&1 | python3 -c 'import sys,json; d=json.load(sys.stdin); print(json.dumps({k:v for k,v in d.get("config",{}).items() if "IAM" in k.upper() or "ENFORCE" in k.upper()}, indent=2))' 2>&1 | head -30404
-rw-r--r-- 1 root root 0 Apr 29 18:26 /tmp/cfg.json
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib/python3.12/json/__init__.py", line 293, in load
return loads(fp.read(),
^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
[stdout]
404
-rw-r--r-- 1 root root 0 Apr 29 18:26 /tmp/cfg.json
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/usr/lib/python3.12/json/__init__.py", line 293, in load
return loads(fp.read(),
^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)Diagnose endpoint raw
curl -sv http://localstack:4566/_localstack/diagnose 2>&1 | head -30
* Host localstack:4566 was resolved. * IPv6: (none) * IPv4: 172.18.0.2 * Trying 172.18.0.2:4566... * Connected to localstack (172.18.0.2) port 4566 > GET /_localstack/diagnose HTTP/1.1 > Host: localstack:4566 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 404 NOT FOUND < Server: TwistedWeb/25.5.0 < Date: Wed, 29 Apr 2026 18:26:51 GMT < Content-Type: text/plain; charset=utf-8 < x-localstack: true < Content-Length: 0 < * Connection #0 to host localstack left intact [stdout] * Host localstack:4566 was resolved. * IPv6: (none) * IPv4: 172.18.0.2 * Trying 172.18.0.2:4566... * Connected to localstack (172.18.0.2) port 4566 > GET /_localstack/diagnose HTTP/1.1 > Host: localstack:4566 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 404 NOT FOUND < Server: TwistedWeb/25.5.0 < Date: Wed, 29 Apr 2026 18:26:51 GMT < Content-Type: text/plain; charset=utf-8 < x-localstack: true < Content-Length: 0 < * Connection #0 to host localstack left intact
Check health endpoint
curl -s http://localstack:4566/_localstack/health | python3 -m json.tool 2>&1 | head -40 echo "---" # Try the IAM enforcement config flag curl -s http://localstack:4566/_localstack/init 2>&1 | head -20
{
"features": {
"persistence": "disabled"
},
"services": {
"acm": "disabled",
"apigateway": "available",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "running",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "disabled",
"firehose": "disabled",
"iam": "running",
"kinesis": "available",
"kms": "running",
"lambda": "running",
"logs": "running",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "running",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "available",
"ses": "disabled",
"sns": "disabled",
"sqs": "disabled",
"ssm": "available",
"stepfunctions": "disabled",
"sts": "running",
"support": "disabled",
"swf": "disabled",
"transcribe": "disabled",
---
{"completed": {"BOOT": true, "START": true, "READY": true, "SHUTDOWN": false}, "scripts": []}
[stdout]
{
"features": {
"persistence": "disabled"
},
"services": {
"acm": "disabled",
"apigateway": "available",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "running",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "disabled",
"firehose": "disabled",
"iam": "running",
"kinesis": "available",
"kms": "running",
"lambda": "running",
"logs": "running",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "running",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "available",
"ses": "disabled",
"sns": "disabled",
"sqs": "disabled",
"ssm": "available",
"stepfunctions": "disabled",
"sts": "running",
"support": "disabled",
"swf": "disabled",
"transcribe": "disabled",
---
{"completed": {"BOOT": true, "START": true, "READY": true, "SHUTDOWN": false}, "scripts": []}Probe IAM enforcement at sts layer
# /_localstack/health shows running services but not config flags # try the deprecated config endpoint with explicit secret token, or just probe with a known wrong key echo "--- probing IAM enforcement: AssumeRole with ZERO tags (should fail under enforcement) ---" awslocal sts assume-role --role-arn arn:aws:iam::000000000000:role/TenantDataRole --role-session-name probe 2>&1 | head -20 echo "---" # also try wrong-tag echo "--- probing AssumeRole with disallowed tag value (should fail under enforcement) ---" awslocal sts assume-role --role-arn arn:aws:iam::000000000000:role/TenantDataRole --role-session-name probe --tags Key=TenantID,Value=evil 2>&1 | head -10
--- probing IAM enforcement: AssumeRole with ZERO tags (should fail under enforcement) ---
{
"Credentials": {
"AccessKeyId": "LSIAQAAAAAAAPSCLCNJ3",
"SecretAccessKey": "OjY0tkgtfMvBUMmwnPj9LiJwSLLhVfn8CgsJwyDU",
"SessionToken": "FQoGZXIvYXdzErzAWlCh4lPffLr5iL+HDQm0zw3Ad18ffc3E61N3KCQAmyick4Pl80W8IfhGjAlnoVI+ffVJfStloj6eP7X=2LqjAxhF7+X0zWhncSSeGrNNCQYq82M64t4cRtJXnI+swQO5sDaklN24GZ=Vpep122vgUlt+kn6eDe3Lua0pwWYf7GARy/F8=8xEDZ9+w8v1qHfXmhYi1tGf6BbBTgnnlYwQxUSDL1vYYb41HgrSq//wxxc/=W+MwzXDI7Go1t68zI6jL7ezMEs4WpiKVtGF=egVjCQ8tUn/xtQJi2PmY1QwQwJVJitefU8vrMa0+V/hDSvnAHEEHYFP+HHfcwX4U0wi",
"Expiration": "2026-04-29T19:27:15.213157+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAQAAAAAAAAE3VKCILN:probe",
"Arn": "arn:aws:sts::000000000000:assumed-role/TenantDataRole/probe"
}
}
---
--- probing AssumeRole with disallowed tag value (should fail under enforcement) ---
{
"Credentials": {
"AccessKeyId": "LSIAQAAAAAAAAZCBEY74",
"SecretAccessKey": "swgykUw/foUf5P1K5FG5qAT0xw3lqz4ruHhEd24l",
"SessionToken": "FQoGZXIvYXdzECaAFXQKlqGu9Gs2cfSEYMmVPHZLIwxT/c7t6An7WYhRBg6m5oaEkajammy=/uDrjy562AO9=KFSchTczXScbSh9wCZRGmyZ1jejYiAobvmVeWrTniRkoUXP/MMSE3I6VjUg96xpL3G/c+gDv1TjuNgbkAprVFQD38rXH5vUJshN6INYF3r/4u2y8gKANP4geNUZsNWUVuZCoJfTcdKAA/1erYnU8/GGGkWYnEyrvm/zf0Q/+kWS6X92zj+0GMh72ldSKEXS1nHa9DD3kQEJI43B8rochMc19TtHacCmEgF3OcXU9=laH6=G478QnYTJJAPlod7aIwnXzZ6CPFYmU3n5",
"Expiration": "2026-04-29T19:27:16.279992+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAQAAAAAAAAE3VKCILN:probe",
"Arn": "arn:aws:sts::000000000000:assumed-role/TenantDataRole/probe"
[stdout]
--- probing IAM enforcement: AssumeRole with ZERO tags (should fail under enforcement) ---
{
"Credentials": {
"AccessKeyId": "LSIAQAAAAAAAPSCLCNJ3",
"SecretAccessKey": "OjY0tkgtfMvBUMmwnPj9LiJwSLLhVfn8CgsJwyDU",
"SessionToken": "FQoGZXIvYXdzErzAWlCh4lPffLr5iL+HDQm0zw3Ad18ffc3E61N3KCQAmyick4Pl80W8IfhGjAlnoVI+ffVJfStloj6eP7X=2LqjAxhF7+X0zWhncSSeGrNNCQYq82M64t4cRtJXnI+swQO5sDaklN24GZ=Vpep122vgUlt+kn6eDe3Lua0pwWYf7GARy/F8=8xEDZ9+w8v1qHfXmhYi1tGf6BbBTgnnlYwQxUSDL1vYYb41HgrSq//wxxc/=W+MwzXDI7Go1t68zI6jL7ezMEs4WpiKVtGF=egVjCQ8tUn/xtQJi2PmY1QwQwJVJitefU8vrMa0+V/hDSvnAHEEHYFP+HHfcwX4U0wi",
"Expiration": "2026-04-29T19:27:15.213157+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAQAAAAAAAAE3VKCILN:probe",
"Arn": "arn:aws:sts::000000000000:assumed-role/TenantDataRole/probe"
}
}
---
--- probing AssumeRole with disallowed tag value (should fail under enforcement) ---
{
"Credentials": {
"AccessKeyId": "LSIAQAAAAAAAAZCBEY74",
"SecretAccessKey": "swgykUw/foUf5P1K5FG5qAT0xw3lqz4ruHhEd24l",
"SessionToken": "FQoGZXIvYXdzECaAFXQKlqGu9Gs2cfSEYMmVPHZLIwxT/c7t6An7WYhRBg6m5oaEkajammy=/uDrjy562AO9=KFSchTczXScbSh9wCZRGmyZ1jejYiAobvmVeWrTniRkoUXP/MMSE3I6VjUg96xpL3G/c+gDv1TjuNgbkAprVFQD38rXH5vUJshN6INYF3r/4u2y8gKANP4geNUZsNWUVuZCoJfTcdKAA/1erYnU8/GGGkWYnEyrvm/zf0Q/+kWS6X92zj+0GMh72ldSKEXS1nHa9DD3kQEJI43B8rochMc19TtHacCmEgF3OcXU9=laH6=G478QnYTJJAPlod7aIwnXzZ6CPFYmU3n5",
"Expiration": "2026-04-29T19:27:16.279992+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "AROAQAAAAAAAAE3VKCILN:probe",
"Arn": "arn:aws:sts::000000000000:assumed-role/TenantDataRole/probe"Toggle ENFORCE_IAM via runtime config
# Try setting IAM enforcement via runtime config
for k in ENFORCE_IAM IAM_SOFT_MODE; do
echo "--- setting $k ---"
curl -s -X POST "http://localstack:4566/_localstack/config" -H "content-type: application/json" -d "{\"variable\":\"$k\",\"value\":\"1\"}" -w "\nhttp=%{http_code}\n"
done
echo "--- get ENFORCE_IAM ---"
curl -s "http://localstack:4566/_localstack/config?variable=ENFORCE_IAM" -w "\nhttp=%{http_code}\n"--- setting ENFORCE_IAM --- http=404 --- setting IAM_SOFT_MODE --- http=404 --- get ENFORCE_IAM --- http=404 [stdout] --- setting ENFORCE_IAM --- http=404 --- setting IAM_SOFT_MODE --- http=404 --- get ENFORCE_IAM --- http=404
Probe for docker access
# Check if there's an /init endpoint or any way to see envars on the localstack container
curl -s "http://localstack:4566/_localstack/usage" 2>&1 | head -5
echo "---"
# check if ENFORCE_IAM is exposed via init metadata
docker ps --format '{{.Names}}' 2>&1 | head -5
which docker 2>&1
ls /var/run/docker.sock 2>&1Exit code 2 --- /bin/bash: line 11: docker: command not found ls: cannot access '/var/run/docker.sock': No such file or directory [error] tool reported failure
Create REST api + /token GET + AWS_PROXY integration
API_ID=$(awslocal apigateway create-rest-api --name harbor-saas-api --query 'id' --output text)
echo "API_ID=$API_ID" | tee /app/build/api_id.txt
ROOT_ID=$(awslocal apigateway get-resources --rest-api-id "$API_ID" --query 'items[?path==`/`].id | [0]' --output text)
echo "ROOT_ID=$ROOT_ID"
TOKEN_ID=$(awslocal apigateway create-resource --rest-api-id "$API_ID" --parent-id "$ROOT_ID" --path-part token --query 'id' --output text)
echo "TOKEN_ID=$TOKEN_ID"
awslocal apigateway put-method --rest-api-id "$API_ID" --resource-id "$TOKEN_ID" --http-method GET --authorization-type NONE >/dev/null
LAMBDA_ARN=$(cat /app/build/lambda_arn.txt)
URI="arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/${LAMBDA_ARN}/invocations"
awslocal apigateway put-integration --rest-api-id "$API_ID" --resource-id "$TOKEN_ID" --http-method GET \
--type AWS_PROXY --integration-http-method POST --uri "$URI" >/dev/null
awslocal apigateway put-method-response --rest-api-id "$API_ID" --resource-id "$TOKEN_ID" --http-method GET --status-code 200 >/dev/null 2>&1 || true
echo "wired"API_ID=w8qu5x3qph ROOT_ID=j7vejwjliy TOKEN_ID=vxbcyphjd4 wired [stdout] API_ID=w8qu5x3qph ROOT_ID=j7vejwjliy TOKEN_ID=vxbcyphjd4 wired
Lambda permission, deploy stage, access logs
# Access logs target
awslocal logs create-log-group --log-group-name /aws/apigateway/harbor-saas-api 2>&1 || true
API_ID=$(grep -oP 'API_ID=\K\S+' /app/build/api_id.txt)
LAMBDA_ARN=$(cat /app/build/lambda_arn.txt)
# Lambda invoke permission with pinned SourceArn (any stage, this resource+method only)
SOURCE_ARN="arn:aws:execute-api:us-east-1:000000000000:${API_ID}/*/GET/token"
awslocal lambda add-permission \
--function-name tenant-token-vendor \
--statement-id apigw-invoke-harbor \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "$SOURCE_ARN" \
--query 'Statement' --output text | head -c 200
echo
echo "SOURCE_ARN=$SOURCE_ARN"
# Deploy stage
awslocal apigateway create-deployment --rest-api-id "$API_ID" --stage-name prod --query 'id' --output text > /app/build/deploy_id.txt
cat /app/build/deploy_id.txt
# Configure stage access logging
LOG_ARN="arn:aws:logs:us-east-1:000000000000:log-group:/aws/apigateway/harbor-saas-api"
awslocal apigateway update-stage --rest-api-id "$API_ID" --stage-name prod \
--patch-operations \
"op=replace,path=/accessLogSettings/destinationArn,value=$LOG_ARN" \
"op=replace,path=/accessLogSettings/format,value={\"requestId\":\"\$context.requestId\",\"ip\":\"\$context.identity.sourceIp\",\"path\":\"\$context.path\",\"status\":\"\$context.status\"}" \
--query 'accessLogSettings' --output json
# Re-deploy after stage change (per spec)
awslocal apigateway create-deployment --rest-api-id "$API_ID" --stage-name prod --query 'id' --output text{"Sid": "apigw-invoke-harbor", "Effect": "Allow", "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor", "Principal": {"Service": "apigate
SOURCE_ARN=arn:aws:execute-api:us-east-1:000000000000:w8qu5x3qph/*/GET/token
unmkpazmql
aws: [ERROR]: An error occurred (ParamValidation): Error parsing parameter '--patch-operations': Expected: '=', received: '"' for input:
op=replace,path=/accessLogSettings/format,value={"requestId":"$context.requestId","ip":"$context.identity.sourceIp","path":"$context.path","status":"$context.status"}
^
a7ehex7cbf
[stdout]
{"Sid": "apigw-invoke-harbor", "Effect": "Allow", "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor", "Principal": {"Service": "apigate
SOURCE_ARN=arn:aws:execute-api:us-east-1:000000000000:w8qu5x3qph/*/GET/token
unmkpazmql
aws: [ERROR]: An error occurred (ParamValidation): Error parsing parameter '--patch-operations': Expected: '=', received: '"' for input:
op=replace,path=/accessLogSettings/format,value={"requestId":"$context.requestId","ip":"$context.identity.sourceIp","path":"$context.path","status":"$context.status"}
^
a7ehex7cbfConfigure access logs via cli-input-json
API_ID=$(grep -oP 'API_ID=\K\S+' /app/build/api_id.txt)
LOG_ARN="arn:aws:logs:us-east-1:000000000000:log-group:/aws/apigateway/harbor-saas-api"
# Use shorthand by writing JSON-style format string without commas inside
FMT='{"requestId":"$context.requestId" "ip":"$context.identity.sourceIp" "status":"$context.status"}'
# Actually shorthand patch-operations doesn't tolerate `,` or `=` inside `value`. Use --cli-input-json instead.
cat > /tmp/access-log-patch.json <<EOF
{
"restApiId": "$API_ID",
"stageName": "prod",
"patchOperations": [
{"op": "replace", "path": "/accessLogSettings/destinationArn", "value": "$LOG_ARN"},
{"op": "replace", "path": "/accessLogSettings/format", "value": "{\"requestId\":\"\$context.requestId\",\"ip\":\"\$context.identity.sourceIp\",\"status\":\"\$context.status\",\"path\":\"\$context.path\"}"}
]
}
EOF
cat /tmp/access-log-patch.json
awslocal apigateway update-stage --cli-input-json file:///tmp/access-log-patch.json --query 'accessLogSettings'
echo "--- redeploy after stage change ---"
awslocal apigateway create-deployment --rest-api-id "$API_ID" --stage-name prod --query 'id' --output text{
"restApiId": "w8qu5x3qph",
"stageName": "prod",
"patchOperations": [
{"op": "replace", "path": "/accessLogSettings/destinationArn", "value": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/apigateway/harbor-saas-api"},
{"op": "replace", "path": "/accessLogSettings/format", "value": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"status\":\"$context.status\",\"path\":\"$context.path\"}"}
]
}
{
"format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"status\":\"$context.status\",\"path\":\"$context.path\"}",
"destinationArn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/apigateway/harbor-saas-api"
}
--- redeploy after stage change ---
8ibisr6o25
[stdout]
{
"restApiId": "w8qu5x3qph",
"stageName": "prod",
"patchOperations": [
{"op": "replace", "path": "/accessLogSettings/destinationArn", "value": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/apigateway/harbor-saas-api"},
{"op": "replace", "path": "/accessLogSettings/format", "value": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"status\":\"$context.status\",\"path\":\"$context.path\"}"}
]
}
{
"format": "{\"requestId\":\"$context.requestId\",\"ip\":\"$context.identity.sourceIp\",\"status\":\"$context.status\",\"path\":\"$context.path\"}",
"destinationArn": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/apigateway/harbor-saas-api"
}
--- redeploy after stage change ---
8ibisr6o25Smoke test the API endpoint
API_ID=$(grep -oP 'API_ID=\K\S+' /app/build/api_id.txt)
INVOKE_URL="http://localstack:4566/restapis/${API_ID}/prod/_user_request_/token"
echo "INVOKE_URL=$INVOKE_URL"
echo "--- GET /token?tenant=acme ---"
curl -s "${INVOKE_URL}?tenant=acme" | head -c 1500
echo
echo "--- GET /token?tenant=BAD!! ---"
curl -s -w "\nhttp=%{http_code}\n" "${INVOKE_URL}?tenant=BAD!!"
echo "--- GET /token?tenant=globex-eu ---"
curl -s "${INVOKE_URL}?tenant=globex-eu" | head -c 400
echoINVOKE_URL=http://localstack:4566/restapis/w8qu5x3qph/prod/_user_request_/token
--- GET /token?tenant=acme ---
{"tenant": "acme", "AccessKeyId": "LSIAQAAAAAAAOFXPNEWS", "SecretAccessKey": "XSpKZvEQtvadUx6MqGAeiGUWBCsBtadxfldVzSZx", "SessionToken": "FQoGZXIvYXdzE2bhu0pw4RKC4Ku7Qk9eTvBv/ZrFlKuuT8qPaNPd3tOUDOcFH7lrwxdWMFI4Tuq1jSkY5D4ZdIX+076rk4l0LEMoBpJ9ZPR38edlC7jitJ6tDcgEEwC/EBUrfjisefsQr5n6m6QB9TMKBQUlkXA9FP94mm7zACaWNexE9IanC1rvcfTCIGdl41yVvF4tfqtj1Ni+o1CY5o6siBqTL0DHtkg0y58Wqja6EinJZW98ZSLgsSd25Ut9t+bS+zpgQLeYRM1vrgwK7u+6w+6=RRckMQ7xD430KQaQ2/wKEV2LDyljB8d71y=Me32jT92jmCq1jYD5gasW=U6ifaE+irJreUov", "Expiration": "2026-04-29T18:43:48.261738+00:00"}
--- GET /token?tenant=BAD!! ---
{"error": "invalid tenant"}
http=400
--- GET /token?tenant=globex-eu ---
{"tenant": "globex-eu", "AccessKeyId": "LSIAQAAAAAAAHSZYMPLB", "SecretAccessKey": "1Qb8eES6TRQgG9LKz5jTO5U5EhzVjcmCxa/iIPFw", "SessionToken": "FQoGZXIvYXdzELJhm7Sp3fdt/UsSfZe6LmRWv1eXsdosUtKsFKRxohX3dlrl5YxDSWDvYB2ZR4+8C1B9BeNFW5cSqIxtOKJVRrLhayTMLNWokPO5Sjqmj11OYigYolQHYwbw/QxMGzXBMc0=1q4+yA4tn7+i6ubRkC+p/WpyI=pN9r/Jg9kSzg9bbsZdWQuqNtAPFrwiZA4MOKQPmpmfsqrfQCUbb3RgdQvVGEnEhNPk3Mbk/ODxxuFbJlb6AZKqD
[stdout]
INVOKE_URL=http://localstack:4566/restapis/w8qu5x3qph/prod/_user_request_/token
--- GET /token?tenant=acme ---
{"tenant": "acme", "AccessKeyId": "LSIAQAAAAAAAOFXPNEWS", "SecretAccessKey": "XSpKZvEQtvadUx6MqGAeiGUWBCsBtadxfldVzSZx", "SessionToken": "FQoGZXIvYXdzE2bhu0pw4RKC4Ku7Qk9eTvBv/ZrFlKuuT8qPaNPd3tOUDOcFH7lrwxdWMFI4Tuq1jSkY5D4ZdIX+076rk4l0LEMoBpJ9ZPR38edlC7jitJ6tDcgEEwC/EBUrfjisefsQr5n6m6QB9TMKBQUlkXA9FP94mm7zACaWNexE9IanC1rvcfTCIGdl41yVvF4tfqtj1Ni+o1CY5o6siBqTL0DHtkg0y58Wqja6EinJZW98ZSLgsSd25Ut9t+bS+zpgQLeYRM1vrgwK7u+6w+6=RRckMQ7xD430KQaQ2/wKEV2LDyljB8d71y=Me32jT92jmCq1jYD5gasW=U6ifaE+irJreUov", "Expiration": "2026-04-29T18:43:48.261738+00:00"}
--- GET /token?tenant=BAD!! ---
{"error": "invalid tenant"}
http=400
--- GET /token?tenant=globex-eu ---
{"tenant": "globex-eu", "AccessKeyId": "LSIAQAAAAAAAHSZYMPLB", "SecretAccessKey": "1Qb8eES6TRQgG9LKz5jTO5U5EhzVjcmCxa/iIPFw", "SessionToken": "FQoGZXIvYXdzELJhm7Sp3fdt/UsSfZe6LmRWv1eXsdosUtKsFKRxohX3dlrl5YxDSWDvYB2ZR4+8C1B9BeNFW5cSqIxtOKJVRrLhayTMLNWokPO5Sjqmj11OYigYolQHYwbw/QxMGzXBMc0=1q4+yA4tn7+i6ubRkC+p/WpyI=pN9r/Jg9kSzg9bbsZdWQuqNtAPFrwiZA4MOKQPmpmfsqrfQCUbb3RgdQvVGEnEhNPk3Mbk/ODxxuFbJlb6AZKqDWrite SSM pointers
API_ID=$(grep -oP 'API_ID=\K\S+' /app/build/api_id.txt)
DATA_ROLE_ARN=$(cat /app/build/data_role_arn.txt)
VENDOR_ROLE_ARN=$(cat /app/build/vendor_role_arn.txt)
LAMBDA_ARN=$(cat /app/build/lambda_arn.txt)
INVOKE_URL="http://localstack:4566/restapis/${API_ID}/prod/_user_request_/token"
for kv in \
"/harbor/saas/table-name=SaasOrders" \
"/harbor/saas/role-arn=$DATA_ROLE_ARN" \
"/harbor/saas/vendor-role-arn=$VENDOR_ROLE_ARN" \
"/harbor/saas/lambda-arn=$LAMBDA_ARN" \
"/harbor/saas/api-id=$API_ID" \
"/harbor/saas/api-url=$INVOKE_URL"; do
K="${kv%%=*}"; V="${kv#*=}"
awslocal ssm put-parameter --name "$K" --value "$V" --type String --overwrite --query 'Version' --output text
done
echo "--- list ---"
awslocal ssm get-parameters-by-path --path /harbor/saas/ --query 'Parameters[].[Name,Value]' --output table1 1 1 1 1 1 --- list --- ---------------------------------------------------------------------------------------------------------- | GetParametersByPath | +-------------------------------+------------------------------------------------------------------------+ | /harbor/saas/table-name | SaasOrders | | /harbor/saas/role-arn | arn:aws:iam::000000000000:role/TenantDataRole | | /harbor/saas/vendor-role-arn | arn:aws:iam::000000000000:role/TenantTokenVendorRole | | /harbor/saas/lambda-arn | arn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor | | /harbor/saas/api-id | w8qu5x3qph | | /harbor/saas/api-url | http://localstack:4566/restapis/w8qu5x3qph/prod/_user_request_/token | +-------------------------------+------------------------------------------------------------------------+ [stdout] 1 1 1 1 1 1 --- list --- ---------------------------------------------------------------------------------------------------------- | GetParametersByPath | +-------------------------------+------------------------------------------------------------------------+ | /harbor/saas/table-name | SaasOrders | | /harbor/saas/role-arn | arn:aws:iam::000000000000:role/TenantDataRole | | /harbor/saas/vendor-role-arn | arn:aws:iam::000000000000:role/TenantTokenVendorRole | | /harbor/saas/lambda-arn | arn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor | | /harbor/saas/api-id | w8qu5x3qph | | /harbor/saas/api-url | http://localstack:4566/restapis/w8qu5x3qph/prod/_user_request_/token | +-------------------------------+------------------------------------------------------------------------+
Read back policies + config for shape audit
echo "=== TenantDataRole TRUST POLICY ==="
awslocal iam get-role --role-name TenantDataRole --query 'Role.AssumeRolePolicyDocument' --output json
echo
echo "=== TenantDataRole IDENTITY POLICY ==="
awslocal iam get-role-policy --role-name TenantDataRole --policy-name TenantPartitionAccess --query 'PolicyDocument' --output json
echo
echo "=== TenantTokenVendorRole INLINE POLICY ==="
awslocal iam get-role-policy --role-name TenantTokenVendorRole --policy-name VendorAssumeAndLogs --query 'PolicyDocument' --output json
echo
echo "=== TenantTokenVendorRole ATTACHED MANAGED ==="
awslocal iam list-attached-role-policies --role-name TenantTokenVendorRole --query 'AttachedPolicies' --output json
echo
echo "=== Lambda env (DATA_ROLE_ARN should be set, no creds in env) ==="
awslocal lambda get-function-configuration --function-name tenant-token-vendor --query '{Env:Environment.Variables,Role:Role,Runtime:Runtime}' --output json
echo
echo "=== Lambda invoke permission ==="
awslocal lambda get-policy --function-name tenant-token-vendor --query 'Policy' --output text | python3 -m json.tool
echo
echo "=== DDB encryption + PITR ==="
awslocal dynamodb describe-table --table-name SaasOrders --query 'Table.SSEDescription' --output json
awslocal dynamodb describe-continuous-backups --table-name SaasOrders --query 'ContinuousBackupsDescription' --output json=== TenantDataRole TRUST POLICY ===
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VendorAssumesWithTenantTag",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/TenantTokenVendorRole"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
],
"Condition": {
"Null": {
"aws:RequestTag/TenantID": "false"
},
"StringEquals": {
"aws:RequestTag/TenantID": [
"acme",
"globex",
"globex-eu"
]
}
}
}
]
}
=== TenantDataRole IDENTITY POLICY ===
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TenantPartitionScopedReadWrite",
"Effect": "Allow",
"Action": [
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:BatchGetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:BatchWriteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/SaasOrders",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": [
"${aws:PrincipalTag/TenantID}"
],
"dynamodb:Attributes": [
"TenantID",
"OrderID",
"amount"
]
},
"StringEqualsIfExists": {
"dynamodb:Select": "SPECIFIC_ATTRIBUTES",
"dynamodb:ReturnValues": [
"NONE",
"UPDATED_OLD",
"UPDATED_NEW"
]
}
}
}
]
}
=== TenantTokenVendorRole INLINE POLICY ===
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeTenantDataRoleOnly",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::000000000000:role/TenantDataRole"
},
{
"Sid": "ScopedLogsToOwnLogGroup",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor:*"
]
},
{
"Sid": "CreateOwnLogGroup",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor"
}
]
}
=== TenantTokenVendorRole ATTACHED MANAGED ===
[]
=== Lambda env (DATA_ROLE_ARN should be set, no creds in env) ===
{
"Env": {
"DATA_ROLE_ARN": "arn:aws:iam::000000000000:role/TenantDataRole"
},
"Role": "arn:aws:iam::000000000000:role/TenantTokenVendorRole",
"Runtime": "python3.11"
}
=== Lambda invoke permission ===
{
"Version": "2012-10-17",
"Id": "default",
"Statement": [
{
"Sid": "apigw-invoke-harbor",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Condition": {
"ArnLike": {
"AWS:SourceArn": "arn:aws:execute-api:us-east-1:000000000000:w8qu5x3qph/*/GET/token"
}
}
}
]
}
=== DDB encryption + PITR ===
{
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": "arn:aws:kms:us-east-1:000000000000:key/alias/saas-orders-cmk"
}
{
"ContinuousBackupsStatus": "ENABLED",
"PointInTimeRecoveryDescription": {
"PointInTimeRecoveryStatus": "ENABLED"
}
}
[stdout]
=== TenantDataRole TRUST POLICY ===
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VendorAssumesWithTenantTag",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::000000000000:role/TenantTokenVendorRole"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
],
"Condition": {
"Null": {
"aws:RequestTag/TenantID": "false"
},
"StringEquals": {
"aws:RequestTag/TenantID": [
"acme",
"globex",
"globex-eu"
]
}
}
}
]
}
=== TenantDataRole IDENTITY POLICY ===
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TenantPartitionScopedReadWrite",
"Effect": "Allow",
"Action": [
"dynamodb:Query",
"dynamodb:GetItem",
"dynamodb:BatchGetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:BatchWriteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/SaasOrders",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": [
"${aws:PrincipalTag/TenantID}"
],
"dynamodb:Attributes": [
"TenantID",
"OrderID",
"amount"
]
},
"StringEqualsIfExists": {
"dynamodb:Select": "SPECIFIC_ATTRIBUTES",
"dynamodb:ReturnValues": [
"NONE",
"UPDATED_OLD",
"UPDATED_NEW"
]
}
}
}
]
}
=== TenantTokenVendorRole INLINE POLICY ===
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AssumeTenantDataRoleOnly",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::000000000000:role/TenantDataRole"
},
{
"Sid": "ScopedLogsToOwnLogGroup",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor:*"
]
},
{
"Sid": "CreateOwnLogGroup",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/tenant-token-vendor"
}
]
}
=== TenantTokenVendorRole ATTACHED MANAGED ===
[]
=== Lambda env (DATA_ROLE_ARN should be set, no creds in env) ===
{
"Env": {
"DATA_ROLE_ARN": "arn:aws:iam::000000000000:role/TenantDataRole"
},
"Role": "arn:aws:iam::000000000000:role/TenantTokenVendorRole",
"Runtime": "python3.11"
}
=== Lambda invoke permission ===
{
"Version": "2012-10-17",
"Id": "default",
"Statement": [
{
"Sid": "apigw-invoke-harbor",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-east-1:000000000000:function:tenant-token-vendor",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Condition": {
"ArnLike": {
"AWS:SourceArn": "arn:aws:execute-api:us-east-1:000000000000:w8qu5x3qph/*/GET/token"
}
}
}
]
}
=== DDB encryption + PITR ===
{
"Status": "ENABLED",
"SSEType": "KMS",
"KMSMasterKeyArn": "arn:aws:kms:us-east-1:000000000000:key/alias/saas-orders-cmk"
}
{
"ContinuousBackupsStatus": "ENABLED",
"PointInTimeRecoveryDescription": {
"PointInTimeRecoveryStatus": "ENABLED"
}
}Final round-trip + log scan
API_URL=$(awslocal ssm get-parameter --name /harbor/saas/api-url --query 'Parameter.Value' --output text)
echo "=== GET /token?tenant=acme via SSM-resolved URL ==="
curl -s "${API_URL}?tenant=acme" | python3 -m json.tool
echo
echo "=== bad tenant 'BAD' (uppercase) ==="
curl -s -w "http=%{http_code}\n" "${API_URL}?tenant=BAD"
echo
echo "=== bad tenant '../etc/passwd' ==="
curl -s -w "http=%{http_code}\n" "${API_URL}?tenant=../etc/passwd"
echo
echo "=== missing tenant ==="
curl -s -w "http=%{http_code}\n" "${API_URL}"
echo
echo "=== all 6 ssm params ==="
awslocal ssm get-parameters-by-path --path /harbor/saas/ --query 'Parameters[].Name' --output json
echo
echo "=== confirm CloudWatch logs do not contain credential fields ==="
sleep 2
LATEST=$(awslocal logs describe-log-streams --log-group-name /aws/lambda/tenant-token-vendor --order-by LastEventTime --descending --max-items 1 --query 'logStreams[0].logStreamName' --output text 2>/dev/null)
if [ -n "$LATEST" ] && [ "$LATEST" != "None" ]; then
awslocal logs get-log-events --log-group-name /aws/lambda/tenant-token-vendor --log-stream-name "$LATEST" --query 'events[].message' --output text | grep -E "AccessKeyId|SecretAccessKey|SessionToken" || echo "no creds in logs ✓"
else
echo "no log streams (lambda runtime did not write any) , nothing to leak ✓"
fi=== GET /token?tenant=acme via SSM-resolved URL ===
{
"tenant": "acme",
"AccessKeyId": "LSIAQAAAAAAAMQ45GDYW",
"SecretAccessKey": "hj2IyFqevUMziM+kQXGKGgiSzsHg4UqDcOpf7YEc",
"SessionToken": "FQoGZXIvYXdzEhsqfLaBtewXLcbr/=ohyhjXO/MWf71oD9R34YO9EEJWr3ab381ZbIX8FLFxZmC/YgUBUeHcg8kBSjav4AFtvCA0j90JMorpQ54ybBM0SRTf5LUMpVq6s5LRXJwQ7HDZYio/Ss88E1X+FDiQfAsRo10GQnjSHIb9Tr4dXN6NZbMuAnGYSjjhKBsXN63Mk4u8F4Bs/t77bS+0PlAqgg3CZM6elso1ba5UrfQsshsIwb26V7pwMQVokF0FEmuRxzbenC4ketpq53Zf3QKseRKfPFxT40inQY7nVCUjYUtHm1o6Ogp6e5LPOLkJh0=7IB1mvNmXn0Nx4a3xCZO9UdFUew3a",
"Expiration": "2026-04-29T18:45:05.187104+00:00"
}
=== bad tenant 'BAD' (uppercase) ===
{"error": "invalid tenant"}http=400
=== bad tenant '../etc/passwd' ===
{"error": "invalid tenant"}http=400
=== missing tenant ===
{"error": "invalid tenant"}http=400
=== all 6 ssm params ===
[
"/harbor/saas/table-name",
"/harbor/saas/role-arn",
"/harbor/saas/vendor-role-arn",
"/harbor/saas/lambda-arn",
"/harbor/saas/api-id",
"/harbor/saas/api-url"
]
=== confirm CloudWatch logs do not contain credential fields ===
no log streams (lambda runtime did not write any) , nothing to leak ✓
[stdout]
=== GET /token?tenant=acme via SSM-resolved URL ===
{
"tenant": "acme",
"AccessKeyId": "LSIAQAAAAAAAMQ45GDYW",
"SecretAccessKey": "hj2IyFqevUMziM+kQXGKGgiSzsHg4UqDcOpf7YEc",
"SessionToken": "FQoGZXIvYXdzEhsqfLaBtewXLcbr/=ohyhjXO/MWf71oD9R34YO9EEJWr3ab381ZbIX8FLFxZmC/YgUBUeHcg8kBSjav4AFtvCA0j90JMorpQ54ybBM0SRTf5LUMpVq6s5LRXJwQ7HDZYio/Ss88E1X+FDiQfAsRo10GQnjSHIb9Tr4dXN6NZbMuAnGYSjjhKBsXN63Mk4u8F4Bs/t77bS+0PlAqgg3CZM6elso1ba5UrfQsshsIwb26V7pwMQVokF0FEmuRxzbenC4ketpq53Zf3QKseRKfPFxT40inQY7nVCUjYUtHm1o6Ogp6e5LPOLkJh0=7IB1mvNmXn0Nx4a3xCZO9UdFUew3a",
"Expiration": "2026-04-29T18:45:05.187104+00:00"
}
=== bad tenant 'BAD' (uppercase) ===
{"error": "invalid tenant"}http=400
=== bad tenant '../etc/passwd' ===
{"error": "invalid tenant"}http=400
=== missing tenant ===
{"error": "invalid tenant"}http=400
=== all 6 ssm params ===
[
"/harbor/saas/table-name",
"/harbor/saas/role-arn",
"/harbor/saas/vendor-role-arn",
"/harbor/saas/lambda-arn",
"/harbor/saas/api-id",
"/harbor/saas/api-url"
]
=== confirm CloudWatch logs do not contain credential fields ===
no log streams (lambda runtime did not write any) , nothing to leak ✓No file changes, the agent left the workspace untouched.
downloading uv 0.9.7 x86_64-unknown-linux-gnu
Downloading pygments (1.2MiB)
Downloading botocore (12.7MiB)
Downloading pygments
Downloading botocore
Installed 13 packages in 78ms
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/TBNJzlQw1bLbzfWbx4PWT/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 35 items
../tests/test_outputs.py::test_01_table_exists_with_pk_and_sk PASSED [ 2%]
../tests/test_outputs.py::test_02_seed_items_two_per_tenant PASSED [ 5%]
../tests/test_outputs.py::test_03_lambda_exists_python311 PASSED [ 8%]
../tests/test_outputs.py::test_04_data_and_vendor_roles_exist PASSED [ 11%]
../tests/test_outputs.py::test_05_apigw_route_token_exists_and_ssm_pointers_resolve PASSED [ 14%]
../tests/test_outputs.py::test_06_data_role_trust_includes_sts_tagsession PASSED [ 17%]
../tests/test_outputs.py::test_07_data_role_trust_principal_is_vendor_role_arn PASSED [ 20%]
../tests/test_outputs.py::test_08_data_role_trust_request_tag_allowlist PASSED [ 22%]
../tests/test_outputs.py::test_09_data_role_inline_uses_principal_tag_substitution_literal PASSED [ 25%]
../tests/test_outputs.py::test_10_data_role_inline_uses_forallvalues_stringequals PASSED [ 28%]
../tests/test_outputs.py::test_11_data_role_inline_resource_is_table_arn_not_wildcard PASSED [ 31%]
../tests/test_outputs.py::test_12_data_role_inline_actions_scoped_no_scan_no_wildcard PASSED [ 34%]
../tests/test_outputs.py::test_13_lambda_source_uses_transitive_tag_keys_and_tags PASSED [ 37%]
../tests/test_outputs.py::test_14_lambda_source_validates_tenant_input_regex PASSED [ 40%]
../tests/test_outputs.py::test_15_lambda_source_duration_seconds_le_900 FAILED [ 42%]
../tests/test_outputs.py::test_16_vendor_role_can_only_assume_data_role PASSED [ 45%]
../tests/test_outputs.py::test_17_no_admin_managed_policies_attached PASSED [ 48%]
../tests/test_outputs.py::test_18_e2e_get_token_for_acme_returns_creds_and_query_works PASSED [ 51%]
../tests/test_outputs.py::test_19_evaluator_admits_acme_blocks_globex_with_session_tag PASSED [ 54%]
../tests/test_outputs.py::test_20_invalid_tenant_input_rejected PASSED [ 57%]
../tests/test_outputs.py::test_21_third_tenant_globex_eu_seeded_two_items PASSED [ 60%]
../tests/test_outputs.py::test_22_data_role_trust_action_set_has_no_extras PASSED [ 62%]
../tests/test_outputs.py::test_23_data_role_trust_requires_tenantid_tag_present PASSED [ 65%]
../tests/test_outputs.py::test_24_data_role_inline_uses_dynamodb_attributes_column_scope PASSED [ 68%]
../tests/test_outputs.py::test_25_data_role_inline_does_not_use_leadingkey_singular_typo PASSED [ 71%]
../tests/test_outputs.py::test_26_vendor_role_has_no_aws_managed_basic_execution PASSED [ 74%]
../tests/test_outputs.py::test_27_vendor_role_inline_no_wildcard_actions PASSED [ 77%]
../tests/test_outputs.py::test_28_lambda_runtime_python311_memory_le_256 PASSED [ 80%]
../tests/test_outputs.py::test_29_lambda_source_transitive_tag_keys_is_tenantid_only PASSED [ 82%]
../tests/test_outputs.py::test_30_apigw_access_log_group_exists PASSED [ 85%]
../tests/test_outputs.py::test_31_lambda_permission_sourcearn_pinned_to_api PASSED [ 88%]
../tests/test_outputs.py::test_32_ddb_table_uses_customer_managed_cmk_sse PASSED [ 91%]
../tests/test_outputs.py::test_33_lambda_handler_does_not_log_credentials PASSED [ 94%]
../tests/test_outputs.py::test_34_doc_evaluator_blocks_tenant_substitution_attacks PASSED [ 97%]
../tests/test_outputs.py::test_35_globex_eu_token_works_e2e PASSED [100%]
=================================== FAILURES ===================================
________________ test_15_lambda_source_duration_seconds_le_900 _________________
def test_15_lambda_source_duration_seconds_le_900():
"""Duration is at most 900s."""
src = _lambda_source()
matches = re.findall(r"DurationSeconds\s*[=:]\s*(\d+)", src)
> assert matches, "lambda source does not set DurationSeconds"
E AssertionError: lambda source does not set DurationSeconds
E assert []
/tests/test_outputs.py:388: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 60 warnings
/root/.cache/uv/archive-v0/TBNJzlQw1bLbzfWbx4PWT/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_outputs.py::test_01_table_exists_with_pk_and_sk
PASSED ../tests/test_outputs.py::test_02_seed_items_two_per_tenant
PASSED ../tests/test_outputs.py::test_03_lambda_exists_python311
PASSED ../tests/test_outputs.py::test_04_data_and_vendor_roles_exist
PASSED ../tests/test_outputs.py::test_05_apigw_route_token_exists_and_ssm_pointers_resolve
PASSED ../tests/test_outputs.py::test_06_data_role_trust_includes_sts_tagsession
PASSED ../tests/test_outputs.py::test_07_data_role_trust_principal_is_vendor_role_arn
PASSED ../tests/test_outputs.py::test_08_data_role_trust_request_tag_allowlist
PASSED ../tests/test_outputs.py::test_09_data_role_inline_uses_principal_tag_substitution_literal
PASSED ../tests/test_outputs.py::test_10_data_role_inline_uses_forallvalues_stringequals
PASSED ../tests/test_outputs.py::test_11_data_role_inline_resource_is_table_arn_not_wildcard
PASSED ../tests/test_outputs.py::test_12_data_role_inline_actions_scoped_no_scan_no_wildcard
PASSED ../tests/test_outputs.py::test_13_lambda_source_uses_transitive_tag_keys_and_tags
PASSED ../tests/test_outputs.py::test_14_lambda_source_validates_tenant_input_regex
PASSED ../tests/test_outputs.py::test_16_vendor_role_can_only_assume_data_role
PASSED ../tests/test_outputs.py::test_17_no_admin_managed_policies_attached
PASSED ../tests/test_outputs.py::test_18_e2e_get_token_for_acme_returns_creds_and_query_works
PASSED ../tests/test_outputs.py::test_19_evaluator_admits_acme_blocks_globex_with_session_tag
PASSED ../tests/test_outputs.py::test_20_invalid_tenant_input_rejected
PASSED ../tests/test_outputs.py::test_21_third_tenant_globex_eu_seeded_two_items
PASSED ../tests/test_outputs.py::test_22_data_role_trust_action_set_has_no_extras
PASSED ../tests/test_outputs.py::test_23_data_role_trust_requires_tenantid_tag_present
PASSED ../tests/test_outputs.py::test_24_data_role_inline_uses_dynamodb_attributes_column_scope
PASSED ../tests/test_outputs.py::test_25_data_role_inline_does_not_use_leadingkey_singular_typo
PASSED ../tests/test_outputs.py::test_26_vendor_role_has_no_aws_managed_basic_execution
PASSED ../tests/test_outputs.py::test_27_vendor_role_inline_no_wildcard_actions
PASSED ../tests/test_outputs.py::test_28_lambda_runtime_python311_memory_le_256
PASSED ../tests/test_outputs.py::test_29_lambda_source_transitive_tag_keys_is_tenantid_only
PASSED ../tests/test_outputs.py::test_30_apigw_access_log_group_exists
PASSED ../tests/test_outputs.py::test_31_lambda_permission_sourcearn_pinned_to_api
PASSED ../tests/test_outputs.py::test_32_ddb_table_uses_customer_managed_cmk_sse
PASSED ../tests/test_outputs.py::test_33_lambda_handler_does_not_log_credentials
PASSED ../tests/test_outputs.py::test_34_doc_evaluator_blocks_tenant_substitution_attacks
PASSED ../tests/test_outputs.py::test_35_globex_eu_token_works_e2e
FAILED ../tests/test_outputs.py::test_15_lambda_source_duration_seconds_le_900
================== 1 failed, 34 passed, 60 warnings in 6.93s ===================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_ed5aa0d0f92b46ed. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_ed5aa0d0f92b46ed · verifier authoritative; classifier explanatory.