diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..2419b0c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,18 @@ +version: 2 +updates: + - package-ecosystem: bun + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + non-major-dependencies: + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 695ba87..a7b9c9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -47,20 +50,37 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for the schema-vs-migrations check. + persist-credentials: false - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + # Pin the version from the last successful upstream CI run (PR #70). + # The older 1.2.12 fails to resolve workspace packages in Vitest. + bun-version: "1.4.3" - name: Install dependencies run: bun install --frozen-lockfile + - name: Generate Fumadocs types + # Required by apps/web-ui's TypeScript sources; otherwise typecheck + # incorrectly fails on an absent generated @/.source module. + working-directory: apps/web-ui + run: bun -e 'import { postInstall } from "fumadocs-mdx/next"; await postInstall();' + - name: Prepare test database # migrate deploy, not db push: the HNSW pgvector indexes live in raw SQL # inside migrations, and db push silently skips them. run: bunx prisma migrate deploy --schema=./prisma/schema.prisma + - name: Validate migrations and vector indexes + run: bun run check:db + + - name: Typecheck + run: bun run typecheck + - name: Unit tests run: bun run test @@ -73,9 +93,10 @@ jobs: - name: Build run: bun run build - - name: Install Playwright browsers - run: bunx playwright install --with-deps chromium - + # E2E needs an independently verified CI web server. Installing a + # browser with no test execution wastes runner time. See the follow-up + # described in docs/dev/ci-security.md. + # # e2e is intentionally NOT run here yet. # # Playwright's webServer runs `bun run dev:all`, which starts four servers in @@ -98,8 +119,5 @@ jobs: path: apps/web-ui-e2e/playwright-report/ retention-days: 7 - # TODO: Add lint and typecheck scripts when ESLint / tsc targets are configured in Nx - # - name: Lint - # run: bun run lint - # - name: Type check - # run: bun run typecheck + # ESLint is not a CI gate yet: the repository does not currently have + # a working Nx-wide lint target/configuration. Enable it once defined. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9f3135e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,36 @@ +name: CodeQL SAST + +on: + pull_request: + branches: [main] + push: + branches: [main] + schedule: + - cron: "17 4 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: CodeQL (JavaScript/TypeScript) + runs-on: ubuntu-latest + timeout-minutes: 35 + permissions: + contents: read + actions: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: javascript-typescript + queries: security-extended + - name: Analyze + uses: github/codeql-action/analyze@v4 + with: + category: "/language:javascript-typescript" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 531c997..ee062fe 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -8,18 +8,15 @@ name: Build & push web-ui image # region), so a deploy job here would fail for reasons unrelated to the image. # Adding it later is a small diff once the infrastructure exists. +# Deploy only a successfully-tested main commit, never untrusted PR code. on: - push: + workflow_run: + workflows: ["CI"] + types: [completed] branches: [main] - paths: - - "apps/web-ui/**" - - "apps/sdk/**" - - "libs/**" - - "prisma/**" - - "package.json" - - "bun.lock" - - ".github/workflows/deploy.yml" - workflow_dispatch: + +permissions: + contents: read concurrency: group: image-${{ github.ref }} @@ -29,10 +26,50 @@ env: AWS_REGION: ${{ vars.AWS_REGION || 'ap-south-2' }} ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY || 'chatbot-web-ui' }} AWS_ROLE_ARN: ${{ vars.AWS_ROLE_ARN || 'arn:aws:iam::704554708141:role/agentfork-github-deploy' }} + SOURCE_SHA: ${{ github.event.workflow_run.head_sha }} jobs: + changes: + name: Detect image-related changes + if: >- + ${{ github.event.workflow_run.event == 'push' && + github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + outputs: + build: ${{ steps.paths.outputs.build }} + steps: + - name: Checkout tested commit + uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + fetch-depth: 2 + persist-credentials: false + - name: Compare with first parent + id: paths + shell: bash + run: | + set -euo pipefail + if git rev-parse HEAD^ >/dev/null 2>&1; then + git diff --name-only HEAD^ HEAD > /tmp/image-changes.txt + else + git ls-tree -r --name-only HEAD > /tmp/image-changes.txt + fi + build=false + while IFS= read -r file; do + case "$file" in + apps/web-ui/*|apps/sdk/*|libs/*|prisma/*|package.json|bun.lock|.github/workflows/deploy.yml) + build=true + break + ;; + esac + done < /tmp/image-changes.txt + echo "build=$build" >> "$GITHUB_OUTPUT" + echo "Image-related changes: $build" >> "$GITHUB_STEP_SUMMARY" + build-and-push: - name: Build & push image + name: Scan & push image + needs: [changes] + if: needs.changes.outputs.build == 'true' # Native arm64 runner. The image targets linux/arm64 (Fargate Graviton), and # this repo is public, so arm64 runners are free — building natively avoids # QEMU, which is slow enough on a Next.js build to hit the job timeout. @@ -42,8 +79,11 @@ jobs: id-token: write steps: - - name: Checkout + - name: Checkout tested commit uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha }} + persist-credentials: false - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 @@ -63,20 +103,21 @@ jobs: id: meta run: | IMAGE="${{ steps.ecr.outputs.registry }}/${ECR_REPOSITORY}" - SHORT_SHA="${GITHUB_SHA:0:12}" + SHORT_SHA="${SOURCE_SHA:0:12}" { echo "image=${IMAGE}:${SHORT_SHA}" echo "latest=${IMAGE}:latest" } >> "$GITHUB_OUTPUT" echo "Building ${IMAGE}:${SHORT_SHA}" - - name: Build and push + - name: Build local image for vulnerability scanning uses: docker/build-push-action@v6 with: context: . file: apps/web-ui/Dockerfile platforms: linux/arm64 - push: true + load: true + push: false # Tagged by commit SHA (immutable, traceable) plus :latest for convenience. tags: | ${{ steps.meta.outputs.image }} @@ -93,6 +134,26 @@ jobs: # silently bake a localhost URL into a production image's client bundle. NEXT_PUBLIC_MISSION_CONTROL_URL=${{ vars.NEXT_PUBLIC_MISSION_CONTROL_URL }} + - name: Scan local image before publication + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: image + image-ref: ${{ steps.meta.outputs.image }} + scanners: vuln + severity: HIGH,CRITICAL + ignore-unfixed: true + format: table + exit-code: "0" # Advisory until baseline findings are triaged. + + - name: Push scanned image + env: + IMAGE: ${{ steps.meta.outputs.image }} + LATEST: ${{ steps.meta.outputs.latest }} + run: | + set -euo pipefail + docker push "$IMAGE" + docker push "$LATEST" + - name: Summary run: | { diff --git a/.github/workflows/osv-scanner-pr.yml b/.github/workflows/osv-scanner-pr.yml new file mode 100644 index 0000000..6a18de8 --- /dev/null +++ b/.github/workflows/osv-scanner-pr.yml @@ -0,0 +1,14 @@ +name: OSV-Scanner PR + +on: + pull_request: + branches: [main] + +permissions: + actions: read + contents: read + security-events: write + +jobs: + scan: + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.7.0 diff --git a/.github/workflows/osv-scanner-scheduled.yml b/.github/workflows/osv-scanner-scheduled.yml new file mode 100644 index 0000000..ce272d1 --- /dev/null +++ b/.github/workflows/osv-scanner-scheduled.yml @@ -0,0 +1,15 @@ +name: OSV-Scanner full scan + +on: + schedule: + - cron: "11 6 * * 1" + workflow_dispatch: + +permissions: + actions: read + contents: read + security-events: write + +jobs: + scan: + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.7.0 diff --git a/.github/workflows/security-checks.yml b/.github/workflows/security-checks.yml new file mode 100644 index 0000000..c98230e --- /dev/null +++ b/.github/workflows/security-checks.yml @@ -0,0 +1,66 @@ +name: Security checks + +on: + pull_request: + branches: [main] + push: + branches: [main] + schedule: + - cron: "45 5 * * 1" + +permissions: + contents: read + +jobs: + secrets: + name: TruffleHog (verified secrets) + if: github.event_name != 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout full history for secret diff + uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: false + - name: Scan changed commits + uses: trufflesecurity/trufflehog@v3.92.5 + with: + extra_args: --results=verified + + workflow-audit: + name: Zizmor (GitHub Actions security) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Audit GitHub Actions workflows + uses: zizmorcore/zizmor-action@v0.6.4 + continue-on-error: true # Advisory until existing findings are triaged. + with: + advanced-security: false + min-severity: medium + + trivy: + name: Trivy (filesystem / IaC baseline) + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Scan dependencies and configuration + uses: aquasecurity/trivy-action@v0.36.0 + with: + scan-type: fs + scan-ref: . + scanners: vuln,misconfig + severity: HIGH,CRITICAL + ignore-unfixed: true + format: table + exit-code: "0" # Initial baseline report; do not hide historical findings. diff --git a/docs/dev/ci-security.md b/docs/dev/ci-security.md new file mode 100644 index 0000000..f8954bb --- /dev/null +++ b/docs/dev/ci-security.md @@ -0,0 +1,68 @@ +# CI and security gates + +This public-repository CI uses standard GitHub-hosted runners, free CodeQL +analysis on public sources, Dependabot, and open-source scanners. No GitHub Team +or Enterprise plan is required for these checks. + +**Cost boundary:** AWS ECR, external staging resources, large runners, and +artifact/cache usage beyond the free GitHub allowances can still cost money. +This change does not provision infrastructure; do not assume the AWS pipeline +itself is free. Private repositories have different GitHub entitlements. + +## Newly introduced controls + +| Workflow | Events | Behavior | +| --- | --- | --- | +| CI | PR, push to main | Pinned Bun 1.4.3 (last verified green upstream runner), real PostgreSQL/pgvector, migrations, index checks, Nx typecheck, unit tests, build | +| CodeQL SAST | PR, push to main, weekly | JS/TS security-extended queries; uploads findings | +| OSV PR | PR | Compares new dependency vulnerabilities with baseline | +| OSV full | Weekly, manual | Full dependency scan, including existing issues | +| TruffleHog | PR, push to main | Detects verified secrets in changed commits | +| Zizmor | PR, push to main, weekly | GitHub Actions security, advisory during baseline | +| Trivy filesystem | Weekly | High/critical dependency and IaC baseline, advisory | +| Dependabot | Weekly | Bun dependency and GitHub Actions update PRs | + +**ECR image publishing:** Deploy now starts only after a successful push CI on +main, checks relevant file changes, checks out the exact CI-tested commit, +builds a local image, scans it with Trivy, and then pushes to ECR using the +existing OIDC role. Image vulnerability findings are initially advisory +(exit code 0), not a hard security guarantee. The AWS role must never be +exposed to untrusted pull-request heads. + +- **Bun version metadata:** root package.json still declares Bun 1.2.12, + while the last green upstream CI used 1.4.3. Align this declaration + in a separate dependency/toolchain migration once validated. + +## Deliberate remaining work + +- **Playwright E2E:** Existing configuration starts four servers (dev:all) and + reproducibly times out. A reliable isolated web-only test server and + fixtures must precede enabling smoke/E2E in blocking CI. No browser is + installed pointlessly by CI in the meantime. +- **ESLint:** The repository has the package but lacks a working Nx lint target + and configuration. Establish these before creating a mandatory lint gate. +- **Coverage:** vitest/coverage-v8 is present; measure a package-by-package + baseline before introducing thresholds. +- **Security regressions:** Implement deterministic negative tests for + tenant isolation, cross-channel approval, missing identities, OAuth, + webhook replay, SSRF/MCP tools, idempotency and worker concurrency. +- **DAST and performance:** OWASP ZAP and k6 require an approved staging + target and safe test accounts. Never automatically target production. +- **Blocking security policy:** Triage existing findings before switching + Trivy/Zizmor to hard-fail; document time-limited accepted-risk exceptions. +- **Repo settings:** A maintainer should enable Dependabot alerts and require + the relevant CI/security checks via branch rulesets after validating them. + Versioned workflows cannot control those settings. +- **Fork PRs:** Validate CodeQL/SARIF and OSV behavior on fork-originating PRs. + Never use pull_request_target to run untrusted PR code. + +## Rollout + +1. Review the first complete CI/security workflow run on a fork PR. +2. Triage and fix historical issues instead of suppressing findings silently. +3. Gradually promote security scans from advisory to required. +4. Introduce isolated smoke E2E, lint, coverage and business security tests + as separately reviewed follow-ups. +5. Always retain maintainer review and human-only merge. + +These automated scans reduce risk; they do not prove absence of vulnerabilities.