Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bootstraps/optional/idle-shutdown/install-idle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,9 @@ JSON

WAKE_INLINE=$(cat <<JSON
{"Version":"2012-10-17","Statement":[
{"Effect":"Allow","Action":["ec2:StartInstances","ec2:DescribeInstanceStatus"],
{"Effect":"Allow","Action":["ec2:StartInstances"],
"Resource":"arn:aws:ec2:${REGION}:${ACCT_ID}:instance/${INSTANCE_ID}"},
{"Effect":"Allow","Action":["ec2:DescribeInstanceStatus"],"Resource":"*"},
{"Effect":"Allow","Action":["ssm:GetParameter","ssm:PutParameter","ssm:DeleteParameter"],"Resource":[
"arn:aws:ssm:${REGION}:${ACCT_ID}:parameter/openclaw/wake-token",
"arn:aws:ssm:${REGION}:${ACCT_ID}:parameter/openclaw/wake-config/*"
Expand Down
96 changes: 96 additions & 0 deletions tests/test-idle-wake-policy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# tests/test-idle-wake-policy.sh — Contract test for the wake Lambda IAM policy
# in bootstraps/optional/idle-shutdown/install-idle.sh.
#
# EC2 Describe* actions do not support resource-level permissions. They match
# only "Resource": "*". This test reads the WAKE_INLINE policy from the script
# and makes sure that:
# - the policy is valid JSON after variable substitution
# - each statement with an ec2:Describe* action has "Resource": "*"
# - ec2:StartInstances stays limited to the one instance ARN
#
# No AWS required. Run standalone or via CI (discovered by pack-tests.yml).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Trigger this contract test on bootstrap changes

Although the new test is discovered when the workflow runs, .github/workflows/pack-tests.yml lines 6-20 do not include bootstraps/** in either path filter. A future PR that changes only bootstraps/optional/idle-shutdown/install-idle.sh therefore will not start this workflow, so the contract test cannot catch the policy regression it was added to prevent; add the relevant bootstrap path to both workflow filters.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct: pack-tests.yml does not have bootstraps/** in its path filters, so a change to install-idle.sh alone does not start this test. I did not change the workflow, because changes to CI configuration are a maintainer decision and not part of a fix for #129. In d767a4b I added a note to the test header about this. Recommendation for a maintainer: add bootstraps/** to both the push and pull_request path filters of pack-tests.yml (tests/test-idle-check.sh has the same gap for idle-check.py).

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
INSTALL="$REPO_ROOT/bootstraps/optional/idle-shutdown/install-idle.sh"

if [[ ! -f "$INSTALL" ]]; then
echo "FAIL: install-idle.sh not found at $INSTALL" >&2
exit 1
fi

RED='\033[0;31m'; GREEN='\033[0;32m'; BOLD='\033[1m'; NC='\033[0m'
PASS=0; FAIL=0

pass() { printf "${GREEN} \xE2\x9C\x93${NC} %s\n" "$1"; PASS=$((PASS + 1)); }
fail() { printf "${RED} \xE2\x9C\x97${NC} %s\n" "$1"; FAIL=$((FAIL + 1)); }

printf "${BOLD}idle-shutdown wake Lambda IAM policy${NC}\n"

# Get the heredoc body between 'WAKE_INLINE=$(cat <<JSON' and the closing 'JSON'.
POLICY="$(awk '/^WAKE_INLINE=\$\(cat <<JSON$/{f=1; next} f && /^JSON$/{exit} f' "$INSTALL")"

if [[ -z "$POLICY" ]]; then
fail "WAKE_INLINE heredoc found in install-idle.sh"
printf "\n%d passed, %d failed\n" "$PASS" "$FAIL"
exit 1
fi
pass "WAKE_INLINE heredoc found in install-idle.sh"

# Check the policy with Python. Each output line is "PASS <msg>" or "FAIL <msg>".
RESULTS="$(POLICY="$POLICY" python3 - <<'PY'
import json, os

region, acct, inst = "us-east-1", "123456789012", "i-0123456789abcdef0"
text = (os.environ["POLICY"]
.replace("${REGION}", region)
.replace("${ACCT_ID}", acct)
.replace("${INSTANCE_ID}", inst))
instance_arn = f"arn:aws:ec2:{region}:{acct}:instance/{inst}"

def out(ok, msg):
print(("PASS " if ok else "FAIL ") + msg)

try:
doc = json.loads(text)
except ValueError as e:
out(False, f"policy is valid JSON ({e})")
raise SystemExit(0)
out(True, "policy is valid JSON")

def as_list(v):
return v if isinstance(v, list) else [v]

stmts = as_list(doc.get("Statement", []))
describe_seen = False
start_seen = False
for s in stmts:
actions = as_list(s.get("Action", []))
resources = as_list(s.get("Resource", []))
describes = [a for a in actions if a.startswith("ec2:Describe")]
if describes:
describe_seen = True
out(resources == ["*"],
f"{', '.join(describes)} uses Resource \"*\" (got {resources})")
if "ec2:StartInstances" in actions:
start_seen = True
out(resources == [instance_arn],
f"ec2:StartInstances is limited to the instance ARN (got {resources})")

out(describe_seen, "policy allows ec2:DescribeInstanceStatus")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check for DescribeInstanceStatus specifically

This assertion labels any ec2:Describe* action as proof that ec2:DescribeInstanceStatus is allowed because describe_seen is set from the prefix match above. If the required action is later deleted or replaced with another describe action such as ec2:DescribeInstances, the test still passes even though wake-lambda/index.mjs calls DescribeInstanceStatusCommand and wake requests will fail; track exact membership of ec2:DescribeInstanceStatus instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d767a4b. The test now sets the "allows ec2:DescribeInstanceStatus" result only when that exact action is in a statement. I checked it with a mutation: when I change the action to ec2:DescribeInstances, the test fails (exit 1) on that check. It also still fails on main and passes with the fix.

out(start_seen, "policy allows ec2:StartInstances")
PY
)"

while IFS= read -r line; do
[[ -z "$line" ]] && continue
case "$line" in
PASS\ *) pass "${line#PASS }" ;;
*) fail "${line#FAIL }" ;;
esac
done <<< "$RESULTS"

printf "\n%d passed, %d failed\n" "$PASS" "$FAIL"
[[ "$FAIL" -eq 0 ]]
Loading