Bump Flask 3.1.1→3.1.3: fix GHSA-68rp-wp8r-4726 (Vary: Cookie) #89
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| # Minimal permissions by default | |
| permissions: | |
| contents: read | |
| jobs: | |
| go-build-and-test: | |
| name: Go Build & Test | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| matrix: | |
| service: [airlock, registry, tool-firewall, gpu-integrity-watch, mcp-firewall, policy-engine, runtime-attestor, integrity-monitor, incident-recorder] | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version: "1.25" | |
| cache-dependency-path: services/${{ matrix.service }}/go.sum | |
| - name: Build | |
| working-directory: services/${{ matrix.service }} | |
| run: CGO_ENABLED=0 go build -ldflags="-s -w" -o /dev/null . | |
| - name: Test | |
| working-directory: services/${{ matrix.service }} | |
| run: go test -v -race -count=1 ./... | |
| - name: Vet | |
| working-directory: services/${{ matrix.service }} | |
| run: go vet ./... | |
| python-test: | |
| name: Python Test & Lint | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install dependencies (pinned) | |
| run: pip install -r requirements-ci.txt | |
| - name: Lint (syntax check) | |
| run: | | |
| python -m py_compile services/ui/ui/app.py | |
| python -m py_compile services/diffusion-worker/app.py | |
| python -m py_compile services/common/audit_chain.py | |
| python -m py_compile services/common/auth.py | |
| python -m py_compile services/common/mlock_helper.py | |
| python -m py_compile services/agent/agent/app.py | |
| python -m py_compile services/agent/agent/models.py | |
| python -m py_compile services/agent/agent/policy.py | |
| python -m py_compile services/agent/agent/planner.py | |
| python -m py_compile services/agent/agent/executor.py | |
| python -m py_compile services/agent/agent/storage.py | |
| python -m py_compile services/agent/agent/capabilities.py | |
| python -m py_compile services/agent/agent/sandbox.py | |
| - name: Ruff lint | |
| run: ruff check services/ tests/ --select E,F,W --ignore E501,E402 | |
| - name: Bandit security scan | |
| run: | | |
| # Fail on high-severity + high-confidence findings. | |
| # Medium/low findings are reported as warnings. | |
| bandit -r services/ -ll --skip B101,B404,B603 -f json -o /tmp/bandit.json || true | |
| python3 -c " | |
| import json, sys | |
| with open('/tmp/bandit.json') as f: | |
| data = json.load(f) | |
| high = [r for r in data.get('results', []) | |
| if r['issue_severity'] == 'HIGH' and r['issue_confidence'] == 'HIGH'] | |
| for r in data.get('results', []): | |
| sev = r['issue_severity'] | |
| msg = f\"{r['filename']}:{r['line_number']}: [{sev}] {r['issue_text']}\" | |
| if sev == 'HIGH': | |
| print(f'::error ::{msg}') | |
| else: | |
| print(f'::warning ::{msg}') | |
| if high: | |
| print(f'FAIL: {len(high)} high-severity/high-confidence finding(s)') | |
| sys.exit(1) | |
| print('OK: no high-severity/high-confidence findings') | |
| " | |
| - name: Mypy type check (security-sensitive services) | |
| run: | | |
| mypy --ignore-missing-imports \ | |
| services/common/ \ | |
| services/agent/agent/ \ | |
| services/quarantine/quarantine/ \ | |
| services/ui/ui/ | |
| - name: Test (unit + integration) | |
| env: | |
| PYTHONPATH: services | |
| run: python -m pytest tests/ -v --ignore=tests/test_adversarial.py --ignore=tests/test_m5_acceptance.py -x | |
| - name: Test (adversarial + acceptance) | |
| env: | |
| PYTHONPATH: services | |
| run: python -m pytest tests/test_adversarial.py tests/test_m5_acceptance.py -v --tb=short | |
| shellcheck: | |
| name: Shell Script Lint | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Lint shell scripts | |
| run: | | |
| shellcheck -s bash \ | |
| files/system/usr/libexec/secure-ai/*.sh \ | |
| files/scripts/build-services.sh \ | |
| files/scripts/generate-mok.sh \ | |
| files/scripts/first-boot-check.sh \ | |
| files/scripts/verify-release.sh | |
| policy-validate: | |
| name: Validate YAML configs | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install pyyaml | |
| run: pip install pyyaml | |
| - name: Validate YAML files | |
| run: | | |
| python -c " | |
| import yaml, sys, glob | |
| errors = 0 | |
| for pattern in ['files/system/etc/secure-ai/**/*.yaml', 'recipes/*.yml']: | |
| for f in glob.glob(pattern, recursive=True): | |
| try: | |
| with open(f) as fh: | |
| yaml.safe_load(fh) | |
| print(f'OK: {f}') | |
| except Exception as e: | |
| print(f'FAIL: {f}: {e}') | |
| errors += 1 | |
| sys.exit(errors) | |
| " | |
| check-pins: | |
| name: Verify action pins | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - run: bash .github/scripts/check-action-pins.sh | |
| supply-chain-verify: | |
| name: Supply Chain & SBOM Verification | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version: "1.25" | |
| - name: Install Syft (SBOM generator) | |
| run: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin | |
| - name: Install cosign (signing & attestation) | |
| run: | | |
| COSIGN_VERSION="v2.4.3" | |
| curl -sSfL "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \ | |
| -o /usr/local/bin/cosign | |
| chmod +x /usr/local/bin/cosign | |
| - name: Verify SBOM generation (Go services) | |
| run: | | |
| echo "=== SBOM generation verification ===" | |
| for svc in airlock registry tool-firewall gpu-integrity-watch mcp-firewall \ | |
| policy-engine runtime-attestor integrity-monitor incident-recorder; do | |
| echo "--- ${svc} ---" | |
| syft dir:services/${svc} -o cyclonedx-json=/dev/null | |
| echo "OK: ${svc} SBOM generated" | |
| done | |
| - name: Verify SBOM generation (Python services) | |
| run: | | |
| for svc in agent ui quarantine common diffusion-worker search-mediator; do | |
| if [ -d "services/${svc}" ]; then | |
| syft dir:services/${svc} -o cyclonedx-json=/dev/null | |
| echo "OK: ${svc} SBOM generated" | |
| fi | |
| done | |
| - name: Verify cosign is functional | |
| run: | | |
| cosign version | |
| echo "OK: cosign available for signing and attestation" | |
| - name: Verify release workflow has provenance steps | |
| run: | | |
| echo "=== Checking release.yml provenance pipeline ===" | |
| # Verify release workflow exists and contains required supply-chain steps | |
| test -f .github/workflows/release.yml || { echo "FAIL: release.yml missing"; exit 1; } | |
| for keyword in "sbom-action" "attest-build-provenance" "cosign" "cyclonedx" "SHA256SUMS"; do | |
| grep -q "${keyword}" .github/workflows/release.yml || \ | |
| { echo "FAIL: release.yml missing '${keyword}'"; exit 1; } | |
| echo "OK: release.yml contains '${keyword}'" | |
| done | |
| # Verify build workflow has SBOM attestation | |
| test -f .github/workflows/build.yml || { echo "FAIL: build.yml missing"; exit 1; } | |
| for keyword in "sbom-action" "cosign attest" "cyclonedx"; do | |
| grep -q "${keyword}" .github/workflows/build.yml || \ | |
| { echo "FAIL: build.yml missing '${keyword}'"; exit 1; } | |
| echo "OK: build.yml contains '${keyword}'" | |
| done | |
| echo "=== Supply chain verification passed ===" | |
| security-regression: | |
| name: Security Regression Tests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version: "1.25" | |
| - name: Install Python dependencies | |
| run: pip install -r requirements-ci.txt | |
| - name: Run adversarial Python tests | |
| run: python -m pytest tests/test_adversarial.py -v --tb=short | |
| - name: Run MCP firewall adversarial tests | |
| working-directory: services/mcp-firewall | |
| run: go test -v -race -run TestAdversarial ./... | |
| - name: Run policy-engine adversarial tests | |
| working-directory: services/policy-engine | |
| run: go test -v -race -run TestAdversarial ./... | |
| - name: Run incident-recorder recovery tests | |
| working-directory: services/incident-recorder | |
| run: go test -v -race -run "TestRecovery|TestEscalation|TestForensic|TestLatched" ./... | |
| test-count-check: | |
| name: Test Count Drift Check | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version: "1.25" | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Python dependencies | |
| run: pip install -r requirements-ci.txt | |
| - name: Check test counts for drift | |
| run: bash .github/scripts/check-test-counts.sh | |
| dependency-audit: | |
| name: Dependency Vulnerability Audit | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 | |
| with: | |
| go-version: "1.25" | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: Install govulncheck | |
| run: go install golang.org/x/vuln/cmd/govulncheck@latest | |
| - name: Go vulnerability scan (enforced) | |
| run: | | |
| echo "=== Go Dependency Vulnerability Scan ===" | |
| VULN_ERRORS=0 | |
| for svc in airlock registry tool-firewall gpu-integrity-watch mcp-firewall \ | |
| policy-engine runtime-attestor integrity-monitor incident-recorder; do | |
| echo "--- ${svc} ---" | |
| cd "services/${svc}" | |
| if ! govulncheck ./... 2>&1; then | |
| VULN_ERRORS=$((VULN_ERRORS + 1)) | |
| echo "::error::${svc}: govulncheck found vulnerabilities" | |
| fi | |
| cd ../.. | |
| done | |
| # Check waivers | |
| WAIVED=$(python3 -c " | |
| import json, datetime | |
| with open('.github/vuln-waivers.json') as f: | |
| data = json.load(f) | |
| today = datetime.date.today().isoformat() | |
| active = [w for w in data.get('go', []) if w.get('expires', '') >= today] | |
| print(len(active)) | |
| ") | |
| EFFECTIVE=$((VULN_ERRORS - WAIVED)) | |
| if [ "$EFFECTIVE" -gt 0 ]; then | |
| echo "FAIL: $VULN_ERRORS service(s) have vulnerabilities ($WAIVED waived, $EFFECTIVE unwaived)" | |
| echo "To waive a reviewed finding, add it to .github/vuln-waivers.json" | |
| exit 1 | |
| fi | |
| echo "OK: Go vulnerability scan passed ($WAIVED waiver(s) active)" | |
| - name: Python dependency audit (enforced) | |
| run: | | |
| pip install -r requirements-ci.txt | |
| echo "=== Python Dependency Audit ===" | |
| # Run pip-audit, capture output | |
| pip-audit --strict --desc -f json -o /tmp/pip-audit.json 2>/dev/null || true | |
| python3 -c " | |
| import json, sys, datetime | |
| # Load audit results | |
| try: | |
| with open('/tmp/pip-audit.json') as f: | |
| data = json.load(f) | |
| except (FileNotFoundError, json.JSONDecodeError): | |
| print('OK: pip-audit produced no findings') | |
| sys.exit(0) | |
| vulns = data if isinstance(data, list) else data.get('dependencies', []) | |
| findings = [d for d in vulns if d.get('vulns')] | |
| if not findings: | |
| print('OK: no Python dependency vulnerabilities') | |
| sys.exit(0) | |
| # Load waivers | |
| with open('.github/vuln-waivers.json') as f: | |
| waivers = json.load(f) | |
| today = datetime.date.today().isoformat() | |
| waived_ids = {w['id'] for w in waivers.get('python', []) if w.get('expires', '') >= today} | |
| unwaived = 0 | |
| for dep in findings: | |
| for v in dep.get('vulns', []): | |
| vid = v.get('id', '') | |
| if vid in waived_ids: | |
| print(f'WAIVED: {dep[\"name\"]} {vid}') | |
| else: | |
| print(f'::error::{dep[\"name\"]}: {vid} — {v.get(\"description\", \"\")}') | |
| unwaived += 1 | |
| if unwaived > 0: | |
| print(f'FAIL: {unwaived} unwaived Python vulnerability finding(s)') | |
| print('To waive a reviewed finding, add it to .github/vuln-waivers.json') | |
| sys.exit(1) | |
| print(f'OK: all Python findings waived ({len(waived_ids)} waiver(s) active)') | |
| " | |
| docs-validation: | |
| name: Documentation Validation | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Check for broken internal links | |
| run: | | |
| echo "=== Checking internal doc links ===" | |
| ERRORS=0 | |
| # Find all markdown links to local files | |
| for md in $(find docs/ README.md CONTRIBUTING.md SECURITY.md -name '*.md' 2>/dev/null); do | |
| # Extract relative links (not URLs, not anchors) | |
| grep -oP '\[([^\]]*)\]\((?!https?://|#)([^)]+)\)' "$md" 2>/dev/null | \ | |
| grep -oP '\(([^)]+)\)' | tr -d '()' | while read -r link; do | |
| # Strip anchor fragments | |
| target="${link%%#*}" | |
| [ -z "$target" ] && continue | |
| # Resolve relative to file's directory | |
| dir=$(dirname "$md") | |
| resolved="${dir}/${target}" | |
| if [ ! -f "$resolved" ] && [ ! -d "$resolved" ]; then | |
| echo "BROKEN: ${md} -> ${link} (resolved: ${resolved})" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done | |
| done | |
| if [ "$ERRORS" -gt 0 ]; then | |
| echo "FAIL: ${ERRORS} broken internal links found" | |
| exit 1 | |
| fi | |
| echo "OK: All internal doc links valid" | |
| - name: Verify required docs exist | |
| run: | | |
| echo "=== Checking required documentation ===" | |
| REQUIRED_DOCS=( | |
| "docs/threat-model.md" | |
| "docs/architecture.md" | |
| "docs/api.md" | |
| "docs/security-status.md" | |
| "docs/production-operations.md" | |
| "docs/production-readiness-checklist.md" | |
| "docs/slos.md" | |
| "docs/release-policy.md" | |
| "docs/support-lifecycle.md" | |
| "docs/test-counts.json" | |
| "docs/install/bare-metal.md" | |
| "SECURITY.md" | |
| "CONTRIBUTING.md" | |
| "LICENSE" | |
| ) | |
| ERRORS=0 | |
| for doc in "${REQUIRED_DOCS[@]}"; do | |
| if [ -f "$doc" ]; then | |
| echo "OK: $doc" | |
| else | |
| echo "MISSING: $doc" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done | |
| if [ "$ERRORS" -gt 0 ]; then | |
| echo "FAIL: ${ERRORS} required document(s) missing" | |
| exit 1 | |
| fi | |
| echo "All required documents present" | |
| - name: Validate test-counts.json format | |
| run: | | |
| python3 -c " | |
| import json, sys | |
| with open('docs/test-counts.json') as f: | |
| data = json.load(f) | |
| required = ['generated', 'go', 'go_total', 'python_total', 'grand_total'] | |
| for key in required: | |
| if key not in data: | |
| print(f'FAIL: test-counts.json missing key: {key}') | |
| sys.exit(1) | |
| if not isinstance(data['go'], dict): | |
| print('FAIL: go field must be a dict of service -> count') | |
| sys.exit(1) | |
| print(f'OK: test-counts.json valid (total: {data[\"grand_total\"]} tests)') | |
| " |