mirror of
https://github.com/LearningCircuit/local-deep-research.git
synced 2026-06-16 03:51:07 +03:00
Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.19.0 to 2.19.1.
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](8d3c67de8e...a5ad31d6a1)
---
updated-dependencies:
- dependency-name: step-security/harden-runner
dependency-version: 2.19.1
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
195 lines
7.0 KiB
YAML
195 lines
7.0 KiB
YAML
name: Security Headers Validation
|
|
|
|
on:
|
|
workflow_call: # Called by release-gate.yml
|
|
schedule:
|
|
- cron: '0 3 * * *' # Daily scan for early detection
|
|
workflow_dispatch: # Manual trigger for debugging/verification
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
# NOTE: No concurrency block here. When called via workflow_call from
|
|
# release-gate.yml, the caller's workflow name is used for the concurrency
|
|
# group, which can cause this job to be cancelled if another release gate
|
|
# run starts on the same ref. The parent workflow manages concurrency.
|
|
|
|
jobs:
|
|
validate-headers:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 15
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:13@sha256:4689940c683801b4ab839ab3b0a0a3555a5fe425371422310944e89eca7d8068 # v13
|
|
env:
|
|
POSTGRES_USER: ldr_test
|
|
POSTGRES_PASSWORD: ${{ github.run_id }}_test_pwd
|
|
POSTGRES_DB: test_db
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
ports:
|
|
- 5432:5432
|
|
|
|
steps:
|
|
- name: Harden the runner (Audit all outbound calls)
|
|
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2.19.1
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- name: Set up Python 3.12
|
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
|
with:
|
|
python-version: '3.12'
|
|
|
|
- name: Cache pip packages
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: ~/.cache/pip
|
|
key: ${{ runner.os }}-pip-headers-${{ hashFiles('**/pyproject.toml') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-pip-headers-
|
|
${{ runner.os }}-pip-
|
|
|
|
- name: Set up PDM
|
|
uses: pdm-project/setup-pdm@973541a5febeafcfdadf8a51211435be6ecfd90f # v4.5
|
|
with:
|
|
python-version: '3.12'
|
|
|
|
- name: Install system dependencies
|
|
run: |
|
|
sudo apt-get update
|
|
sudo apt-get install -y libsqlcipher-dev build-essential
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
pdm install --dev
|
|
|
|
- name: Set up test environment
|
|
run: |
|
|
mkdir -p data
|
|
mkdir -p research_outputs
|
|
cp -r src/local_deep_research/defaults/.env.template .env.test
|
|
|
|
- name: Start Flask application in background
|
|
run: |
|
|
# Use FLASK_DEBUG=0 (FLASK_ENV is deprecated)
|
|
export FLASK_DEBUG=0
|
|
export DATABASE_URL=postgresql://ldr_test:${{ github.run_id }}_test_pwd@localhost:5432/test_db
|
|
pdm run python -m local_deep_research.web.app &
|
|
echo $! > flask.pid
|
|
|
|
- name: Wait for application to be ready
|
|
run: |
|
|
timeout 60 bash -c 'until curl -s --max-time 5 http://localhost:5000/ > /dev/null; do sleep 2; done'
|
|
|
|
- name: Validate security headers
|
|
run: |
|
|
echo "=== Testing Security Headers ==="
|
|
echo ""
|
|
|
|
# Capture headers once for efficiency (with timeout)
|
|
HEADERS=$(curl -sI --max-time 10 http://localhost:5000/)
|
|
|
|
echo "Full response headers:"
|
|
echo "$HEADERS"
|
|
echo ""
|
|
|
|
# Check for required security headers and validate values
|
|
ERRORS=()
|
|
|
|
# X-Frame-Options - must be SAMEORIGIN or DENY
|
|
XFO=$(echo "$HEADERS" | grep -i "^X-Frame-Options:" | cut -d':' -f2- | tr -d '\r' | xargs)
|
|
if [[ -z "$XFO" ]]; then
|
|
ERRORS+=("X-Frame-Options: MISSING")
|
|
elif [[ "$XFO" != "SAMEORIGIN" && "$XFO" != "DENY" ]]; then
|
|
ERRORS+=("X-Frame-Options: Invalid value '$XFO' (expected SAMEORIGIN or DENY)")
|
|
else
|
|
echo "✅ X-Frame-Options: $XFO"
|
|
fi
|
|
|
|
# X-Content-Type-Options - must be nosniff
|
|
XCTO=$(echo "$HEADERS" | grep -i "^X-Content-Type-Options:" | cut -d':' -f2- | tr -d '\r' | xargs)
|
|
if [[ -z "$XCTO" ]]; then
|
|
ERRORS+=("X-Content-Type-Options: MISSING")
|
|
elif [[ "$XCTO" != "nosniff" ]]; then
|
|
ERRORS+=("X-Content-Type-Options: Invalid value '$XCTO' (expected nosniff)")
|
|
else
|
|
echo "✅ X-Content-Type-Options: $XCTO"
|
|
fi
|
|
|
|
# Referrer-Policy - must not be unsafe
|
|
RP=$(echo "$HEADERS" | grep -i "^Referrer-Policy:" | cut -d':' -f2- | tr -d '\r' | xargs)
|
|
if [[ -z "$RP" ]]; then
|
|
ERRORS+=("Referrer-Policy: MISSING")
|
|
elif [[ "$RP" == "unsafe-url" || "$RP" == "no-referrer-when-downgrade" ]]; then
|
|
ERRORS+=("Referrer-Policy: Insecure value '$RP'")
|
|
else
|
|
echo "✅ Referrer-Policy: $RP"
|
|
fi
|
|
|
|
# Permissions-Policy - must restrict dangerous features
|
|
PP=$(echo "$HEADERS" | grep -i "^Permissions-Policy:" | cut -d':' -f2- | tr -d '\r' | xargs)
|
|
if [[ -z "$PP" ]]; then
|
|
ERRORS+=("Permissions-Policy: MISSING")
|
|
elif [[ "$PP" != *"geolocation=()"* ]]; then
|
|
ERRORS+=("Permissions-Policy: Should restrict geolocation")
|
|
else
|
|
echo "✅ Permissions-Policy: $PP"
|
|
fi
|
|
|
|
# Content-Security-Policy - must have default-src and not be overly permissive
|
|
CSP=$(echo "$HEADERS" | grep -i "^Content-Security-Policy:" | cut -d':' -f2- | tr -d '\r' | xargs)
|
|
if [[ -z "$CSP" ]]; then
|
|
ERRORS+=("Content-Security-Policy: MISSING")
|
|
elif [[ "$CSP" == *"default-src *"* || "$CSP" == *"default-src 'unsafe-inline' 'unsafe-eval'"* ]]; then
|
|
ERRORS+=("Content-Security-Policy: Overly permissive policy detected")
|
|
elif [[ "$CSP" != *"default-src"* ]]; then
|
|
ERRORS+=("Content-Security-Policy: Missing default-src directive")
|
|
else
|
|
echo "✅ Content-Security-Policy: Present with default-src"
|
|
fi
|
|
|
|
# Report results
|
|
echo ""
|
|
if [ ${#ERRORS[@]} -eq 0 ]; then
|
|
echo "✅ All required security headers are present and valid"
|
|
else
|
|
echo "❌ Security header issues found:"
|
|
for error in "${ERRORS[@]}"; do
|
|
echo " - $error"
|
|
done
|
|
exit 1
|
|
fi
|
|
|
|
- name: Test API endpoint headers
|
|
run: |
|
|
echo "=== Testing Security Headers on API Endpoints ==="
|
|
echo ""
|
|
|
|
# Test health endpoint (with timeout)
|
|
API_HEADERS=$(curl -sI --max-time 10 http://localhost:5000/api/health)
|
|
|
|
if echo "$API_HEADERS" | grep -iq "^X-Frame-Options:"; then
|
|
echo "✅ Security headers present on API endpoints"
|
|
else
|
|
echo "❌ Security headers missing on API endpoints"
|
|
echo "$API_HEADERS"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Cleanup
|
|
if: always()
|
|
run: |
|
|
if [ -f flask.pid ]; then
|
|
kill "$(cat flask.pid)" 2>/dev/null || true
|
|
rm -f flask.pid
|
|
fi
|