Skip to content

Fix deploy CI secret scan and security gate blockers #11

Fix deploy CI secret scan and security gate blockers

Fix deploy CI secret scan and security gate blockers #11

name: Deploy FPAI Admin Backend to AWS Lambda
on:
push:
branches: ["main", "prod"]
paths:
- "backend-admin/**"
- ".github/workflows/deploy-admin-backend-aws.yml"
workflow_dispatch:
permissions:
id-token: write
contents: read
concurrency:
group: deploy-fpai-admin-backend-${{ github.ref_name == 'prod' && 'prod' || 'dev' }}
cancel-in-progress: true
env:
AWS_REGION: us-east-1
AWS_ROLE_TO_ASSUME: arn:aws:iam::675177356722:role/GitHubActionsDeployer
DEPLOY_REF: ${{ github.ref_name }}
ENVIRONMENT: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }}
jobs:
lint:
name: Lint Admin Backend
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install linting tools
run: |
python -m pip install --upgrade pip
pip install ruff
- name: Lint Python files
working-directory: backend-admin
run: |
ruff check . --output-format=github || true
ruff check . --select=E,F,W --exit-zero
- name: Validate Python syntax
working-directory: backend-admin
run: python -m py_compile admin_handler.py
test:
name: Admin Backend Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install pytest boto3
- name: Run admin tests
working-directory: backend-admin
run: |
export PYTHONPATH="$PWD"
pytest -v tests/test_admin_handler.py tests/test_admin_template_security.py
validate-sam:
name: Validate Admin SAM Template
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Install SAM CLI
uses: aws-actions/setup-sam@v2
with:
use-installer: true
- name: Validate template
working-directory: backend-admin
run: |
sam validate --lint
echo "✅ Admin SAM template is valid"
validate-routes:
name: Validate Admin Routes
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Validate route definitions
working-directory: backend-admin
run: |
python << 'EOF'
import ast
import re
import sys
with open("admin_handler.py", "r", encoding="utf-8") as f:
content = f.read()
tree = ast.parse(content)
route_literals = re.findall(r"path\s*==\s*['\"]([^'\"]+)['\"]", content)
expected_routes = [
"/admin/overview",
"/admin/jobs",
"/admin/db",
"/admin/activity",
"/admin/errors",
"/admin/chats/recent",
]
missing_routes = [r for r in expected_routes if r not in route_literals]
if missing_routes:
print(f"❌ Missing admin routes in handler: {missing_routes}")
sys.exit(1)
if "/admin/chats/{job_id}" not in content:
print("❌ Missing /admin/chats/{job_id} route handling")
sys.exit(1)
functions = {n.name for n in ast.walk(tree) if isinstance(n, ast.FunctionDef)}
expected_functions = {
"authenticate_admin",
"handle_admin_overview",
"handle_admin_jobs",
"handle_admin_db",
"handle_admin_activity",
"handle_admin_errors",
"handle_admin_chats_recent",
"handle_admin_chat_detail",
"handler",
}
missing_functions = sorted(expected_functions - functions)
if missing_functions:
print(f"❌ Missing admin functions: {missing_functions}")
sys.exit(1)
print("✅ Admin route validation complete")
EOF
secret-scan:
name: Secret Scan (Gitleaks)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
fetch-depth: 0
- name: Install gitleaks CLI
run: |
curl -sSfL https://raw.githubusercontent.com/gitleaks/gitleaks/master/install.sh \
| sh -s -- -b /usr/local/bin v8.24.2
- name: Run gitleaks
run: |
gitleaks detect --source . --redact --report-format sarif --report-path gitleaks-report.sarif
- name: Upload gitleaks report
uses: actions/upload-artifact@v4
if: always()
with:
name: admin-backend-gitleaks-report
path: gitleaks-report.sarif
retention-days: 7
quality-gate:
name: Quality Gate
needs: [lint, test, validate-sam, validate-routes, secret-scan]
runs-on: ubuntu-latest
steps:
- name: All checks passed
run: |
echo "## ✅ All Admin Backend Checks Passed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Secret scan: passed" >> $GITHUB_STEP_SUMMARY
echo "Proceeding to deployment..." >> $GITHUB_STEP_SUMMARY
deploy:
needs: quality-gate
runs-on: ubuntu-latest
environment: ${{ github.ref_name == 'prod' && 'prod' || 'dev' }}
env:
ADMIN_USER_POOL_ARN: ${{ vars.ADMIN_USER_POOL_ARN }}
ADMIN_ALLOWED_ORIGIN: ${{ vars.ADMIN_ALLOWED_ORIGIN || 'https://admin.fpai.io' }}
ADMIN_ALLOWED_ORIGINS: ${{ vars.ADMIN_ALLOWED_ORIGINS || vars.ADMIN_ALLOWED_ORIGIN || 'https://admin.fpai.io' }}
ADMIN_EMAIL_ALLOWLIST: ${{ vars.ADMIN_EMAIL_ALLOWLIST }}
ADMIN_GROUP_ALLOWLIST: ${{ vars.ADMIN_GROUP_ALLOWLIST }}
BACKEND_STACK_NAME: ${{ vars.BACKEND_STACK_NAME }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ env.DEPLOY_REF }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install SAM CLI
uses: aws-actions/setup-sam@v2
with:
use-installer: true
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ env.AWS_ROLE_TO_ASSUME }}
- name: Validate required admin config
run: |
if [ -z "${ADMIN_USER_POOL_ARN}" ]; then
echo "::error::Missing ADMIN_USER_POOL_ARN GitHub environment variable"
exit 1
fi
if [ -z "${ADMIN_EMAIL_ALLOWLIST}" ]; then
echo "::error::Missing ADMIN_EMAIL_ALLOWLIST GitHub environment variable"
exit 1
fi
- name: Who am I (debug)
run: aws sts get-caller-identity
- name: Resolve backend resource names
id: resolve
run: |
set -euo pipefail
STACK_NAME="${BACKEND_STACK_NAME:-fpai-backend-${{ env.ENVIRONMENT }}}"
echo "Using backend stack: $STACK_NAME"
get_resource_id() {
aws cloudformation describe-stack-resource \
--stack-name "$STACK_NAME" \
--logical-resource-id "$1" \
--query 'StackResourceDetail.PhysicalResourceId' \
--output text
}
SESSIONS_TABLE_NAME="$(get_resource_id SessionsTable)"
SESSION_ITEMS_TABLE_NAME="$(get_resource_id SessionItemsTable)"
JOBS_TABLE_NAME="$(get_resource_id JobsTable)"
JOB_EVENTS_TABLE_NAME="$(get_resource_id JobEventsTable)"
USERS_TABLE_NAME="$(get_resource_id UsersTable)"
APP_FUNCTION_NAME="$(aws cloudformation describe-stacks \
--stack-name "$STACK_NAME" \
--query 'Stacks[0].Outputs[?OutputKey==`FunctionName`].OutputValue' \
--output text)"
if [ -z "$APP_FUNCTION_NAME" ] || [ "$APP_FUNCTION_NAME" = "None" ]; then
echo "::error::Could not resolve FunctionName output from backend stack $STACK_NAME"
exit 1
fi
echo "sessions_table_name=$SESSIONS_TABLE_NAME" >> "$GITHUB_OUTPUT"
echo "session_items_table_name=$SESSION_ITEMS_TABLE_NAME" >> "$GITHUB_OUTPUT"
echo "jobs_table_name=$JOBS_TABLE_NAME" >> "$GITHUB_OUTPUT"
echo "job_events_table_name=$JOB_EVENTS_TABLE_NAME" >> "$GITHUB_OUTPUT"
echo "users_table_name=$USERS_TABLE_NAME" >> "$GITHUB_OUTPUT"
echo "app_function_name=$APP_FUNCTION_NAME" >> "$GITHUB_OUTPUT"
echo "app_log_group=/aws/lambda/$APP_FUNCTION_NAME" >> "$GITHUB_OUTPUT"
- name: SAM Build
working-directory: backend-admin
run: sam build --cached --parallel
- name: SAM Deploy
working-directory: backend-admin
run: |
ADMIN_ALLOWED_ORIGIN_CLEAN=$(echo "${ADMIN_ALLOWED_ORIGIN}" | tr -d '[:space:]')
ADMIN_ALLOWED_ORIGINS_CLEAN=$(echo "${ADMIN_ALLOWED_ORIGINS}" | tr -d '[:space:]')
ADMIN_EMAIL_ALLOWLIST_CLEAN=$(echo "${ADMIN_EMAIL_ALLOWLIST}" | tr -d '[:space:]')
ADMIN_GROUP_ALLOWLIST_CLEAN=$(echo "${ADMIN_GROUP_ALLOWLIST}" | tr -d '[:space:]')
sam deploy \
--config-env ${{ env.ENVIRONMENT }} \
--no-confirm-changeset \
--no-fail-on-empty-changeset \
--resolve-s3 \
--capabilities CAPABILITY_IAM \
--parameter-overrides "Environment=${{ env.ENVIRONMENT }} AdminUserPoolArn=${ADMIN_USER_POOL_ARN} AdminAllowedOrigin=${ADMIN_ALLOWED_ORIGIN_CLEAN} AdminAllowedOrigins=${ADMIN_ALLOWED_ORIGINS_CLEAN} AdminEmailAllowlist=${ADMIN_EMAIL_ALLOWLIST_CLEAN} AdminGroupAllowlist=${ADMIN_GROUP_ALLOWLIST_CLEAN} SessionsTableName=${{ steps.resolve.outputs.sessions_table_name }} SessionItemsTableName=${{ steps.resolve.outputs.session_items_table_name }} JobsTableName=${{ steps.resolve.outputs.jobs_table_name }} JobEventsTableName=${{ steps.resolve.outputs.job_events_table_name }} UsersTableName=${{ steps.resolve.outputs.users_table_name }} AppFunctionName=${{ steps.resolve.outputs.app_function_name }} AppLogGroup=${{ steps.resolve.outputs.app_log_group }}"
- name: Get Admin API Endpoint
id: get-endpoint
run: |
ENDPOINT=$(aws cloudformation describe-stacks \
--stack-name fpai-admin-backend-${{ env.ENVIRONMENT }} \
--query 'Stacks[0].Outputs[?OutputKey==`AdminApiEndpoint`].OutputValue' \
--output text)
echo "endpoint=$ENDPOINT" >> "$GITHUB_OUTPUT"
echo "Admin API Endpoint: $ENDPOINT"
- name: Health Check (unauthorized expected)
run: |
ENDPOINT="${{ steps.get-endpoint.outputs.endpoint }}"
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$ENDPOINT/admin/overview" || echo "000")
if [ "$STATUS" = "401" ] || [ "$STATUS" = "403" ]; then
echo "✅ Admin API is reachable (status: $STATUS)"
exit 0
fi
echo "❌ Admin API returned unexpected status: $STATUS"
exit 1
notify:
needs: deploy
runs-on: ubuntu-latest
if: always()
steps:
- name: Deployment Summary
run: |
echo "## Admin Backend Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Environment | ${{ env.ENVIRONMENT }} |" >> $GITHUB_STEP_SUMMARY
echo "| Status | ${{ needs.deploy.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Triggered by | ${{ github.actor }} |" >> $GITHUB_STEP_SUMMARY
echo "| Commit | ${{ github.sha }} |" >> $GITHUB_STEP_SUMMARY