@@ -97,20 +97,71 @@ jobs:
9797 pip install pip-audit bandit
9898
9999 - name : Run pip-audit (dependency vulnerabilities)
100+ id : pip_audit
100101 working-directory : backend
101102 run : |
102103 # Audit declared requirements directly; avoid installing heavy runtime deps (e.g. manim)
103104 # that require OS libraries just to perform vulnerability checks.
105+ set +e
104106 if grep -qE "^[a-zA-Z]" requirements.txt 2>/dev/null; then
105- pip-audit -r requirements.txt || echo "::warning::Security vulnerabilities found in dependencies"
107+ pip-audit -r requirements.txt -f json -o pip-audit-report.json
108+ STATUS=$?
106109 else
107110 echo "No external dependencies to audit"
111+ printf '{"message":"No external dependencies to audit"}\n' > pip-audit-report.json
112+ STATUS=0
113+ fi
114+ set -e
115+ echo "status=$STATUS" >> "$GITHUB_OUTPUT"
116+ if [ "$STATUS" -ne 0 ]; then
117+ echo "::error::pip-audit detected vulnerabilities"
108118 fi
109119
110120 - name : Run Bandit (security linter)
121+ id : bandit_scan
111122 working-directory : backend
112123 run : |
113- bandit -r . -x ./tests -ll -ii --format github || echo "::warning::Potential security issues found"
124+ set +e
125+ bandit -r . -x ./tests -ll -ii -f json -o bandit-report.json
126+ STATUS=$?
127+ set -e
128+ echo "status=$STATUS" >> "$GITHUB_OUTPUT"
129+ if [ "$STATUS" -ne 0 ]; then
130+ echo "::error::Bandit detected security findings"
131+ fi
132+
133+ - name : Upload backend security reports
134+ uses : actions/upload-artifact@v4
135+ if : always()
136+ with :
137+ name : backend-security-reports
138+ path : |
139+ backend/pip-audit-report.json
140+ backend/bandit-report.json
141+ retention-days : 7
142+
143+ - name : Enforce security gate
144+ run : |
145+ if [ "${{ steps.pip_audit.outputs.status }}" != "0" ]; then
146+ exit 1
147+ fi
148+ if [ "${{ steps.bandit_scan.outputs.status }}" != "0" ]; then
149+ exit 1
150+ fi
151+
152+ secret-scan :
153+ name : Secret Scan (Gitleaks)
154+ runs-on : ubuntu-latest
155+ steps :
156+ - name : Checkout
157+ uses : actions/checkout@v4
158+ with :
159+ ref : ${{ env.DEPLOY_REF }}
160+
161+ - name : Run gitleaks
162+ uses : gitleaks/gitleaks-action@v2
163+ env :
164+ GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
114165
115166 test :
116167 name : Unit Tests
@@ -346,7 +397,7 @@ jobs:
346397
347398 quality-gate :
348399 name : Quality Gate
349- needs : [lint, security, test, validate-sam, validate-routes]
400+ needs : [lint, security, secret-scan, test, validate-sam, validate-routes]
350401 runs-on : ubuntu-latest
351402 steps :
352403 - name : All checks passed
@@ -357,6 +408,7 @@ jobs:
357408 echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
358409 echo "| Lint & Code Quality | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
359410 echo "| Security Scan | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
411+ echo "| Secret Scan | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
360412 echo "| Unit Tests | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
361413 echo "| SAM Validation | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
362414 echo "| Route Validation | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
@@ -375,6 +427,10 @@ jobs:
375427 FIREBASE_PROJECT_ID : ${{ vars.BACKEND_FIREBASE_PROJECT_ID || 'fpai-cbcb2' }}
376428 OPENAI_MODEL : ${{ vars.BACKEND_OPENAI_MODEL || 'gpt-5.2' }}
377429 API_KEYS_SECRET_ID : ${{ vars.BACKEND_API_KEYS_SECRET_ID }}
430+ ALLOWED_ORIGINS : ${{ vars.BACKEND_ALLOWED_ORIGINS || 'http://localhost:3000,http://localhost:5173,https://fpai.io,https://d1gyb6wodlq2q2.cloudfront.net' }}
431+ ASSETS_ALLOWED_ORIGINS : ${{ vars.BACKEND_ASSETS_ALLOWED_ORIGINS || 'https://fpai.io,http://localhost:3000' }}
432+ SHARE_PUBLIC_BASE_URL : ${{ vars.BACKEND_SHARE_PUBLIC_BASE_URL || 'https://fpai.io' }}
433+ REQUIRE_BRAVE_API_KEY : ${{ vars.BACKEND_REQUIRE_BRAVE_API_KEY || 'true' }}
378434 steps :
379435 - name : Checkout
380436 uses : actions/checkout@v4
@@ -418,7 +474,9 @@ jobs:
418474 OPENAI_API_KEY : ${{ secrets.OPENAI_API_KEY }}
419475 GEMINI_API_KEY : ${{ secrets.GEMINI_API_KEY }}
420476 GOOGLE_API_KEY : ${{ secrets.GOOGLE_API_KEY }}
477+ BRAVE_API_KEY : ${{ secrets.BRAVE_API_KEY }}
421478 FIREBASE_SERVICE_ACCOUNT_JSON : ${{ secrets.FIREBASE_SERVICE_ACCOUNT_JSON }}
479+ REQUIRE_BRAVE_API_KEY : ${{ env.REQUIRE_BRAVE_API_KEY }}
422480 run : |
423481 if [ -z "$OPENAI_API_KEY" ]; then
424482 echo "::error::Missing GitHub secret OPENAI_API_KEY"
@@ -433,6 +491,10 @@ jobs:
433491 echo "::error::Missing GitHub secret FIREBASE_SERVICE_ACCOUNT_JSON"
434492 exit 1
435493 fi
494+ if [ "${REQUIRE_BRAVE_API_KEY}" != "false" ] && [ -z "$BRAVE_API_KEY" ]; then
495+ echo "::error::Missing GitHub secret BRAVE_API_KEY"
496+ exit 1
497+ fi
436498
437499 if [ "${{ env.ENVIRONMENT }}" = "prod" ]; then
438500 DEFAULT_SECRET_ID="fpai/api-keys"
@@ -457,8 +519,9 @@ jobs:
457519 UPDATED_SECRET=$(printf '%s' "$CURRENT_SECRET" \
458520 | jq --arg openai_key "$OPENAI_API_KEY" \
459521 --arg gemini_key "$EFFECTIVE_GEMINI_API_KEY" \
522+ --arg brave_key "$BRAVE_API_KEY" \
460523 --arg firebase_sa "$FIREBASE_SERVICE_ACCOUNT_JSON" \
461- '.OPENAI_API_KEY = $openai_key | .GEMINI_API_KEY = $gemini_key | .FIREBASE_SERVICE_ACCOUNT_JSON = $firebase_sa')
524+ '.OPENAI_API_KEY = $openai_key | .GEMINI_API_KEY = $gemini_key | .BRAVE_API_KEY = $brave_key | . FIREBASE_SERVICE_ACCOUNT_JSON = $firebase_sa')
462525
463526 if [ "$SECRET_EXISTS" = "true" ]; then
464527 aws secretsmanager put-secret-value \
@@ -470,7 +533,32 @@ jobs:
470533 --secret-string "$UPDATED_SECRET"
471534 fi
472535
473- echo "✅ Updated OPENAI_API_KEY + GEMINI_API_KEY + FIREBASE_SERVICE_ACCOUNT_JSON in Secrets Manager ($SECRET_ID)"
536+ echo "✅ Updated OPENAI_API_KEY + GEMINI_API_KEY + BRAVE_API_KEY + FIREBASE_SERVICE_ACCOUNT_JSON in Secrets Manager ($SECRET_ID)"
537+
538+ - name : Validate required Secrets Manager fields
539+ env :
540+ REQUIRE_BRAVE_API_KEY : ${{ env.REQUIRE_BRAVE_API_KEY }}
541+ run : |
542+ if [ "${{ env.ENVIRONMENT }}" = "prod" ]; then
543+ DEFAULT_SECRET_ID="fpai/api-keys"
544+ else
545+ DEFAULT_SECRET_ID="fpai/api-keys-${{ env.ENVIRONMENT }}"
546+ fi
547+ SECRET_ID="${API_KEYS_SECRET_ID:-$DEFAULT_SECRET_ID}"
548+ SECRET_JSON=$(aws secretsmanager get-secret-value \
549+ --secret-id "$SECRET_ID" \
550+ --query SecretString \
551+ --output text)
552+
553+ echo "$SECRET_JSON" | jq -e '
554+ ((.OPENAI_API_KEY // "") | length > 0) and
555+ ((.GEMINI_API_KEY // "") | length > 0) and
556+ ((.FIREBASE_SERVICE_ACCOUNT_JSON // "") | length > 0)
557+ ' >/dev/null
558+
559+ if [ "${REQUIRE_BRAVE_API_KEY}" != "false" ]; then
560+ echo "$SECRET_JSON" | jq -e '(.BRAVE_API_KEY // "") | length > 0' >/dev/null
561+ fi
474562
475563 - name : SAM Build
476564 working-directory : backend
@@ -486,6 +574,9 @@ jobs:
486574 fi
487575
488576 SECRET_ID="${API_KEYS_SECRET_ID:-$DEFAULT_SECRET_ID}"
577+ ALLOWED_ORIGINS_CLEAN=$(echo "${ALLOWED_ORIGINS}" | tr -d '[:space:]')
578+ ASSETS_ALLOWED_ORIGINS_CLEAN=$(echo "${ASSETS_ALLOWED_ORIGINS}" | tr -d '[:space:]')
579+ SHARE_PUBLIC_BASE_URL_CLEAN=$(echo "${SHARE_PUBLIC_BASE_URL}" | tr -d '[:space:]')
489580
490581 if [ "${{ env.ENVIRONMENT }}" = "prod" ]; then
491582 BETA_GATE_ENABLED=true
@@ -500,7 +591,7 @@ jobs:
500591 --resolve-s3 \
501592 --resolve-image-repos \
502593 --capabilities CAPABILITY_IAM \
503- --parameter-overrides "Environment=${{ env.ENVIRONMENT }} OpenAIModel=${OPENAI_MODEL} FirebaseProjectId=${FIREBASE_PROJECT_ID} BetaGateEnabled=${BETA_GATE_ENABLED} EnableJobEventStreaming=true ApiKeysSecretId=${SECRET_ID}"
594+ --parameter-overrides "Environment=${{ env.ENVIRONMENT }} OpenAIModel=${OPENAI_MODEL} FirebaseProjectId=${FIREBASE_PROJECT_ID} BetaGateEnabled=${BETA_GATE_ENABLED} EnableJobEventStreaming=true ApiKeysSecretId=${SECRET_ID} AllowedOrigins=${ALLOWED_ORIGINS_CLEAN} AssetsAllowedOrigins=${ASSETS_ALLOWED_ORIGINS_CLEAN} SharePublicBaseUrl=${SHARE_PUBLIC_BASE_URL_CLEAN} "
504595
505596 - name : Get API Endpoint
506597 id : get-endpoint
0 commit comments