diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index 6b4360a0..5890bd4c 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -1,5 +1,8 @@ name: "BoardGameTracker CodeQL Config" +queries: + - uses: security-extended + query-filters: - exclude: id: cs/log-forging diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b83e4324..ca6e71c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,35 +4,36 @@ on: pull_request: branches: - master - paths-ignore: - - "docs/**" - - ".github/workflows/docs.yml" -env: - REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.io' }} - IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'uping/boardgametracker' }} +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: - version: - name: Calculate Version + test-and-analyze: + name: Test and SonarCloud Analysis runs-on: ubuntu-latest + env: + SONAR_SCANNER_VERSION: "11.2.1" + SONAR_PROJECT_KEY: mregni_BoardGameTracker + SONAR_ORGANIZATION: uping permissions: contents: read - outputs: - version: ${{ steps.versioning.outputs.version }} + pull-requests: write + checks: write steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Git Semantic Version - uses: PaulHatch/semantic-version@v6.0.3 + uses: PaulHatch/semantic-version@502226b17e3e242e4befd0a45690fcbb27dd1373 # v6.0.3 id: versioning with: enable_prerelease_mode: true @@ -41,50 +42,31 @@ jobs: version_format: "${major}.${minor}.${patch}-beta" debug: true - test-and-analyze: - name: Test and SonarCloud Analysis - runs-on: ubuntu-latest - needs: [version] - permissions: - contents: read - pull-requests: write - checks: write - steps: - - name: Harden runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - name: Set up JDK 17 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: java-version: 17 distribution: 'zulu' - name: Setup dotnet v8 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: dotnet-version: "8.x" - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "20.x" cache: 'pnpm' cache-dependency-path: boardgametracker.client/pnpm-lock.yaml - name: Cache SonarCloud packages - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar @@ -92,17 +74,16 @@ jobs: - name: Cache SonarCloud scanner id: cache-sonar-scanner - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ./.sonar/scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner + key: ${{ runner.os }}-sonar-scanner-${{ env.SONAR_SCANNER_VERSION }} - name: Install SonarCloud scanner if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' run: | mkdir -p ./.sonar/scanner - dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner --version "$SONAR_SCANNER_VERSION" - name: Install .NET dependencies run: dotnet restore ./BoardGameTracker.sln @@ -111,23 +92,20 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} - SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} + VERSION: ${{ steps.versioning.outputs.version }} run: | ./.sonar/scanner/dotnet-sonarscanner begin \ - /k:"${{ env.SONAR_PROJECT_KEY }}" \ - /o:"${{ env.SONAR_ORGANIZATION }}" \ + /k:"$SONAR_PROJECT_KEY" \ + /o:"$SONAR_ORGANIZATION" \ /d:sonar.host.url="https://sonarcloud.io" \ - /d:sonar.token="${{ env.SONAR_TOKEN }}" \ - /v:"${{ needs.version.outputs.version }}" \ + /d:sonar.token="$SONAR_TOKEN" \ + /v:"$VERSION" \ /d:sonar.cs.opencover.reportsPaths="TestResults/**/coverage.opencover.xml" \ /d:sonar.cs.vstest.reportsPaths="TestResults/*.trx" \ /d:sonar.javascript.lcov.reportPaths="coverage/lcov.info" \ /d:sonar.testExecutionReportPaths="boardgametracker.client/coverage/sonar-report.xml" \ /d:sonar.exclusions="**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/TestResults/**,**/obj/**,**/bin/**,**/Migrations/**,**/routeTree.gen.ts" \ /d:sonar.coverage.exclusions="**/BoardGameTracker.Host/**/*.cs,**/BoardGameTracker.Core/Datastore/**/*.cs,**/ViewModels/**/*.cs,**/Entities/**/*.cs,**/routeTree.gen.ts,**/tailwind.config.js,**/node_modules/**" \ - /d:sonar.qualitygate.wait=true \ - /d:sonar.qualitygate.timeout=300 \ /d:sonar.issue.ignore.multicriteria=e1 \ /d:sonar.issue.ignore.multicriteria.e1.ruleKey=githubactions:S7637 \ /d:sonar.issue.ignore.multicriteria.e1.resourceKey="**/.github/**" @@ -175,7 +153,7 @@ jobs: sed -i "s|path=\"src/|path=\"$(pwd)/src/|g" coverage/sonar-report.xml - name: Publish .NET test results - uses: EnricoMi/publish-unit-test-result-action@v2 + uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2 if: always() with: files: TestResults/**/*.trx @@ -186,11 +164,11 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ env.SONAR_TOKEN }}" + run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" if: env.SONAR_TOKEN != '' - name: Upload test results - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results path: | @@ -200,7 +178,7 @@ jobs: if: success() || failure() - name: Code Coverage Report - uses: danielpalme/ReportGenerator-GitHub-Action@v5.5.10 + uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10 with: reports: "TestResults/**/coverage.opencover.xml;boardgametracker.client/coverage/lcov.info" targetdir: "coveragereport" @@ -213,7 +191,7 @@ jobs: run: cat coveragereport/SummaryGithub.md >> $GITHUB_STEP_SUMMARY - name: Add Coverage PR Comment - uses: marocchino/sticky-pull-request-comment@v3 + uses: marocchino/sticky-pull-request-comment@5770ad5eb8f42dd2c4f34da00c94c5381e49af88 # v3 with: recreate: true path: coveragereport/SummaryGithub.md diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b6ce42dc..cf95ee79 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,6 +8,10 @@ on: schedule: - cron: '15 7 * * 4' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze (${{ matrix.language }}) @@ -31,21 +35,21 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} config-file: ./.github/codeql/codeql-config.yml - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 with: category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 51ac069b..281ba3e9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -24,20 +24,20 @@ jobs: working-directory: docs steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: docs/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: 22 cache: pnpm @@ -45,7 +45,7 @@ jobs: - name: Setup Pages id: pages - uses: actions/configure-pages@v6 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6 - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts @@ -56,7 +56,7 @@ jobs: VITE_BASE_PATH: ${{ steps.pages.outputs.base_path }}/ - name: Upload artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 with: path: docs/dist @@ -71,10 +71,10 @@ jobs: needs: build steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5 diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7c7e0f5d..5912b780 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -3,7 +3,11 @@ name: PR Quality on: pull_request: branches: [master] - types: [opened, synchronize, reopened, edited, labeled, unlabeled] + types: [opened, synchronize, reopened, edited] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: labeler: @@ -14,15 +18,15 @@ jobs: pull-requests: write steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Apply file-based labels - uses: actions/labeler@v6 + uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6 with: repo-token: ${{ secrets.GITHUB_TOKEN }} configuration-path: .github/labeler.yml @@ -30,15 +34,14 @@ jobs: - name: Remove old size labels env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | - PR_NUMBER=${{ github.event.pull_request.number }} - REPO=${{ github.repository }} for LABEL in "size/XS" "size/S" "size/M" "size/L" "size/XL"; do - gh api "repos/$REPO/issues/$PR_NUMBER/labels/$LABEL" -X DELETE 2>/dev/null || true + gh api "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/labels/$LABEL" -X DELETE 2>/dev/null || true done - name: Label PR by size - uses: codelytv/pr-size-labeler@v1 + uses: codelytv/pr-size-labeler@095a41fca88b8764fd9e008ad269bcdb82bb38b9 # v1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} xs_label: "size/XS" @@ -61,12 +64,12 @@ jobs: permissions: {} steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Validate PR title - uses: amannn/action-semantic-pull-request@v6 + uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.github/workflows/publish-container.yml b/.github/workflows/publish-container.yml index 5529d97a..dfe7ac3c 100644 --- a/.github/workflows/publish-container.yml +++ b/.github/workflows/publish-container.yml @@ -5,8 +5,13 @@ on: branches: - master paths-ignore: + - "**/*.md" - "docs/**" - ".github/workflows/docs.yml" + - ".github/images/**" + - "LICENSE" + - "crowdin.yml" + - ".vscode/**" workflow_dispatch: inputs: stable_release: @@ -15,6 +20,10 @@ on: default: false type: boolean +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }} + cancel-in-progress: false + env: REGISTRY: ${{ vars.DOCKER_REGISTRY || 'docker.io' }} IMAGE_NAME: ${{ vars.DOCKER_IMAGE_NAME || 'uping/boardgametracker' }} @@ -31,17 +40,17 @@ jobs: is_stable: ${{ env.IS_STABLE }} steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Git Semantic Version - uses: PaulHatch/semantic-version@v6.0.3 + uses: PaulHatch/semantic-version@502226b17e3e242e4befd0a45690fcbb27dd1373 # v6.0.3 id: versioning with: major_pattern: "(MAJOR)" @@ -56,44 +65,48 @@ jobs: name: Test and SonarCloud Analysis runs-on: ubuntu-latest needs: [version] + env: + SONAR_SCANNER_VERSION: "11.2.1" + SONAR_PROJECT_KEY: mregni_BoardGameTracker + SONAR_ORGANIZATION: uping permissions: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Set up JDK 17 - uses: actions/setup-java@v5 + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5 with: java-version: 17 distribution: 'zulu' - name: Setup dotnet v8 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: dotnet-version: "8.x" - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "20.x" cache: 'pnpm' cache-dependency-path: boardgametracker.client/pnpm-lock.yaml - name: Cache SonarCloud packages - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ~/.sonar/cache key: ${{ runner.os }}-sonar @@ -101,17 +114,16 @@ jobs: - name: Cache SonarCloud scanner id: cache-sonar-scanner - uses: actions/cache@v5 + uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5 with: path: ./.sonar/scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner + key: ${{ runner.os }}-sonar-scanner-${{ env.SONAR_SCANNER_VERSION }} - name: Install SonarCloud scanner if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' run: | mkdir -p ./.sonar/scanner - dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner + dotnet tool update dotnet-sonarscanner --tool-path ./.sonar/scanner --version "$SONAR_SCANNER_VERSION" - name: Install .NET dependencies run: dotnet restore ./BoardGameTracker.sln @@ -121,22 +133,19 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - SONAR_PROJECT_KEY: ${{ secrets.SONAR_PROJECT_KEY }} - SONAR_ORGANIZATION: ${{ secrets.SONAR_ORGANIZATION }} + VERSION: ${{ needs.version.outputs.version }} run: | ./.sonar/scanner/dotnet-sonarscanner begin \ - /k:"${{ env.SONAR_PROJECT_KEY }}" \ - /o:"${{ env.SONAR_ORGANIZATION }}" \ + /k:"$SONAR_PROJECT_KEY" \ + /o:"$SONAR_ORGANIZATION" \ /d:sonar.host.url="https://sonarcloud.io" \ - /d:sonar.token="${{ env.SONAR_TOKEN }}" \ - /v:"${{ needs.version.outputs.version }}" \ + /d:sonar.token="$SONAR_TOKEN" \ + /v:"$VERSION" \ /d:sonar.cs.opencover.reportsPaths="TestResults/**/coverage.opencover.xml" \ /d:sonar.cs.vstest.reportsPaths="TestResults/*.trx" \ /d:sonar.javascript.lcov.reportPaths="boardgametracker.client/coverage/lcov.info" \ /d:sonar.exclusions="**/node_modules/**,**/dist/**,**/build/**,**/coverage/**,**/TestResults/**,**/obj/**,**/bin/**,**/Migrations/**,**/routeTree.gen.ts" \ /d:sonar.coverage.exclusions="**/BoardGameTracker.Host/**/*.cs,**/BoardGameTracker.Core/Datastore/**/*.cs,**/ViewModels/**/*.cs,**/Entities/**/*.cs,**/routeTree.gen.ts,**/tailwind.config.js,**/node_modules/**" \ - /d:sonar.qualitygate.wait=true \ - /d:sonar.qualitygate.timeout=300 \ /d:sonar.issue.ignore.multicriteria=e1 \ /d:sonar.issue.ignore.multicriteria.e1.ruleKey=githubactions:S7637 \ /d:sonar.issue.ignore.multicriteria.e1.resourceKey="**/.github/**" @@ -176,14 +185,13 @@ jobs: - name: End SonarCloud analysis if: env.SONAR_TOKEN != '' - continue-on-error: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="${{ env.SONAR_TOKEN }}" + run: ./.sonar/scanner/dotnet-sonarscanner end /d:sonar.token="$SONAR_TOKEN" - name: Upload test results - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results path: | @@ -193,7 +201,7 @@ jobs: if: success() || failure() - name: Code Coverage Report - uses: danielpalme/ReportGenerator-GitHub-Action@v5.5.10 + uses: danielpalme/ReportGenerator-GitHub-Action@049f7ec958c672fd31d5cc1cb01622dc8d2e23ab # v5.5.10 with: reports: "TestResults/**/coverage.opencover.xml;boardgametracker.client/coverage/lcov.info" targetdir: "coveragereport" @@ -217,35 +225,37 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json - name: Update package.json version + env: + VERSION: ${{ needs.version.outputs.version }} run: | cd boardgametracker.client - pnpm version ${{ needs.version.outputs.version }} --no-git-tag-version --allow-same-version + pnpm version "$VERSION" --no-git-tag-version --allow-same-version - name: Set up QEMU - uses: docker/setup-qemu-action@v4 + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4 with: platforms: all - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Login to Docker Hub - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -253,7 +263,7 @@ jobs: - name: Extract metadata for Docker (beta) id: meta-beta if: env.IS_STABLE != 'true' - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: ${{ env.IMAGE_NAME }} tags: | @@ -263,7 +273,7 @@ jobs: - name: Extract metadata for Docker (stable) id: meta-stable if: env.IS_STABLE == 'true' - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: ${{ env.IMAGE_NAME }} tags: | @@ -273,7 +283,7 @@ jobs: type=raw,value=latest - name: Build and Push Multi-Architecture Images - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: ./Dockerfile @@ -301,18 +311,18 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Build local image for scanning - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: ./Dockerfile @@ -324,7 +334,7 @@ jobs: VERSION=${{ needs.version.outputs.version }} - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'sarif' @@ -333,13 +343,13 @@ jobs: timeout: '10m' - name: Upload Trivy results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 if: always() with: sarif_file: 'trivy-results.sarif' - name: Run Trivy vulnerability scanner (fail on critical) - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'table' @@ -348,14 +358,14 @@ jobs: timeout: '10m' - name: Generate SBOM - uses: aquasecurity/trivy-action@master + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: ${{ env.IMAGE_NAME }}:scan format: 'cyclonedx' output: 'sbom.json' - name: Upload SBOM artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: sbom-${{ needs.version.outputs.version }} path: sbom.json @@ -370,19 +380,19 @@ jobs: contents: write steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Generate Release Notes + env: + VERSION: ${{ needs.version.outputs.version }} run: | - VERSION=${{ needs.version.outputs.version }} - # Get previous tag PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") @@ -414,7 +424,7 @@ jobs: EOF - name: Create Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: tag_name: v${{ needs.version.outputs.version }} name: v${{ needs.version.outputs.version }} @@ -426,10 +436,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create major and major.minor tags + env: + VERSION: ${{ needs.version.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - VERSION=${{ needs.version.outputs.version }} MAJOR=$(echo "$VERSION" | cut -d. -f1) MINOR=$(echo "$VERSION" | cut -d. -f2) git tag -fa "v${MAJOR}" -m "Release v${VERSION}" @@ -444,15 +455,16 @@ jobs: permissions: {} steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Build Summary + env: + VERSION: ${{ needs.version.outputs.version }} + IS_STABLE_RELEASE: ${{ needs.version.outputs.is_stable }} run: | - VERSION=${{ needs.version.outputs.version }} - - if [ "${{ needs.version.outputs.is_stable }}" = "true" ]; then + if [ "$IS_STABLE_RELEASE" = "true" ]; then cat >> $GITHUB_STEP_SUMMARY << EOF # Release v${VERSION} Published @@ -464,7 +476,7 @@ jobs: ## Links - [Docker Hub](https://hub.docker.com/r/uping/boardgametracker) - - [Release Notes](https://github.com/${{ github.repository }}/releases/tag/v${VERSION}) + - [Release Notes](https://github.com/${GITHUB_REPOSITORY}/releases/tag/v${VERSION}) EOF else cat >> $GITHUB_STEP_SUMMARY << EOF diff --git a/.github/workflows/security-full-scan.yml b/.github/workflows/security-full-scan.yml index f1ba0665..557facd4 100644 --- a/.github/workflows/security-full-scan.yml +++ b/.github/workflows/security-full-scan.yml @@ -6,9 +6,12 @@ on: - cron: "0 1 * * 1-5" workflow_dispatch: +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + env: REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} RESOURCE_GROUP: bgt-security-scan-${{ github.run_id }} AZURE_LOCATION: westeurope @@ -19,23 +22,28 @@ jobs: timeout-minutes: 90 permissions: contents: read + packages: write id-token: write security-events: write actions: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Compute image name + id: image + run: echo "name=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Login to GHCR - uses: docker/login-action@v4 + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -43,12 +51,13 @@ jobs: - name: Build and push image id: build - uses: docker/build-push-action@v7 + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 with: context: . file: ./Dockerfile push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:security-scan-${{ github.run_id }} + provenance: false + tags: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:security-scan-${{ github.run_id }} cache-from: type=gha build-args: | VERSION=0.0.0-security @@ -64,7 +73,7 @@ jobs: echo "jwt_secret=$JWT_SECRET" >> "$GITHUB_OUTPUT" - name: Azure OIDC Login - uses: azure/login@v3 + uses: azure/login@f5d393ae46f8fde4be8b75f32e3fc50e654ad0ca # v3 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} @@ -73,20 +82,27 @@ jobs: - name: Create resource group run: | az group create \ - --name "${{ env.RESOURCE_GROUP }}" \ - --location "${{ env.AZURE_LOCATION }}" \ - --tags purpose=security-scan run_id=${{ github.run_id }} + --name "$RESOURCE_GROUP" \ + --location "$AZURE_LOCATION" \ + --tags name=boardgametracker purpose=security-scan run_id="$GITHUB_RUN_ID" - name: Deploy Bicep template id: deploy + env: + IMAGE_REF: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} + SEC_DB_PASSWORD: ${{ steps.secrets.outputs.db_password }} + SEC_JWT_SECRET: ${{ steps.secrets.outputs.jwt_secret }} + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | RESULT=$(az deployment group create \ - --resource-group "${{ env.RESOURCE_GROUP }}" \ + --resource-group "$RESOURCE_GROUP" \ --template-file infra/security-scan.bicep \ --parameters \ - imageName="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" \ - dbPassword="${{ steps.secrets.outputs.db_password }}" \ - jwtSecret="${{ steps.secrets.outputs.jwt_secret }}" \ + imageName="$IMAGE_REF" \ + dbPassword="$SEC_DB_PASSWORD" \ + jwtSecret="$SEC_JWT_SECRET" \ + registryUsername="$GITHUB_ACTOR" \ + registryPassword="$GHCR_TOKEN" \ --query "properties.outputs" \ --output json) @@ -96,21 +112,25 @@ jobs: echo "Application URL: $APP_URL" - name: Wait for application to become healthy + env: + APP_URL: ${{ steps.deploy.outputs.app_url }} run: | - APP_URL="${{ steps.deploy.outputs.app_url }}" + set -euo pipefail + echo "Polling $APP_URL/api/settings ..." - for i in $(seq 1 120); do - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "$APP_URL/api/settings" 2>/dev/null || echo "000") + for i in $(seq 1 60); do + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" --connect-timeout 5 --max-time 10 "$APP_URL/api/settings" 2>/dev/null || echo "000") + echo "Attempt $i: HTTP $HTTP_CODE" if [ "$HTTP_CODE" = "200" ]; then - echo "Application is healthy after ${i}s" + echo "Application is healthy" break fi - if [ "$i" -eq 120 ]; then - echo "::error::Application did not become healthy within 120s (last HTTP $HTTP_CODE)" + if [ "$i" -eq 60 ]; then + echo "::error::Application did not become healthy (last HTTP $HTTP_CODE)" exit 1 fi - sleep 1 + sleep 5 done - name: Authenticate and extract JWT @@ -118,30 +138,59 @@ jobs: env: ZAP_TEST_USERNAME: ${{ secrets.ZAP_TEST_USERNAME }} ZAP_TEST_PASSWORD: ${{ secrets.ZAP_TEST_PASSWORD }} + APP_URL: ${{ steps.deploy.outputs.app_url }} run: | - APP_URL="${{ steps.deploy.outputs.app_url }}" + set -euo pipefail + + PAYLOAD=$(jq -n \ + --arg u "$ZAP_TEST_USERNAME" \ + --arg p "$ZAP_TEST_PASSWORD" \ + '{username: $u, password: $p}') RESPONSE=$(curl -sf -X POST "$APP_URL/api/auth/login" \ -H "Content-Type: application/json" \ - -d "{\"username\": \"${{ env.ZAP_TEST_USERNAME }}\", \"password\": \"${{ env.ZAP_TEST_PASSWORD }}\"}") + --data-binary "$PAYLOAD") - TOKEN=$(echo "$RESPONSE" | jq -r '.accessToken') + TOKEN=$(printf '%s' "$RESPONSE" | jq -r '.accessToken // empty') - if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + if [ -z "$TOKEN" ]; then echo "::error::Failed to extract JWT from login response" exit 1 fi + if ! printf '%s' "$TOKEN" | grep -Eq '^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$'; then + echo "::error::Login response did not contain a well-formed JWT" + exit 1 + fi + echo "::add-mask::$TOKEN" echo "token=$TOKEN" >> "$GITHUB_OUTPUT" echo "Authentication successful" - - name: Run ZAP Full Scan - uses: zaproxy/action-full-scan@v0.13.0 + - name: Validate OpenAPI spec is served + env: + APP_URL: ${{ steps.deploy.outputs.app_url }} + run: | + set -euo pipefail + + curl -sf "$APP_URL/swagger/v1/swagger.json" -o /tmp/swagger.json + + ENDPOINT_COUNT=$(jq '[.paths | keys[]] | length' /tmp/swagger.json) + echo "OpenAPI spec contains $ENDPOINT_COUNT endpoint paths" + + if [ "$ENDPOINT_COUNT" -lt 10 ]; then + echo "::error::OpenAPI spec has suspiciously few endpoints ($ENDPOINT_COUNT)" + exit 1 + fi + + - name: Run ZAP API Scan + uses: zaproxy/action-api-scan@5158fe4d9d8fcc75ea204db81317cce7f9e5453d # v0.10.0 with: - target: ${{ steps.deploy.outputs.app_url }} + target: "${{ steps.deploy.outputs.app_url }}/swagger/v1/swagger.json" + format: openapi docker_name: "ghcr.io/zaproxy/zaproxy:stable" allow_issue_writing: false + fail_action: "false" cmd_options: >- -z "-config replacer.full_list(0).description=AuthHeader -config replacer.full_list(0).enabled=true @@ -152,30 +201,133 @@ jobs: -config replacer.full_list(0).initiators=" rules_file_name: ".zap/rules.tsv" - - name: Upload SARIF to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4 - if: always() - with: - sarif_file: report_sarif.json - category: zap-full-scan - - name: Upload scan reports - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 if: always() with: - name: zap-full-scan-reports-${{ github.run_id }} + name: zap-api-scan-reports-${{ github.run_id }} path: | report_html.html report_json.json report_md.md - report_sarif.json retention-days: 90 + - name: Collect Azure diagnostics + if: failure() + run: | + az extension add --name containerapp --upgrade || true + + echo "=== bgt-api-sec status ===" + az containerapp show -g "$RESOURCE_GROUP" -n bgt-api-sec \ + --query "{provisioning: properties.provisioningState, running: properties.runningStatus}" -o json || true + az containerapp revision list -g "$RESOURCE_GROUP" -n bgt-api-sec -o table || true + + echo "=== bgt-api-sec system log ===" + az containerapp logs show -g "$RESOURCE_GROUP" -n bgt-api-sec --type system --tail 100 || true + + echo "=== bgt-api-sec console log ===" + az containerapp logs show -g "$RESOURCE_GROUP" -n bgt-api-sec --type console --tail 100 || true + + echo "=== postgres sidecar console log ===" + az containerapp logs show -g "$RESOURCE_GROUP" -n bgt-api-sec --container postgres --type console --tail 50 || true + - name: Delete Azure resource group if: always() run: | az group delete \ - --name "${{ env.RESOURCE_GROUP }}" \ + --name "$RESOURCE_GROUP" \ --yes \ --no-wait echo "Resource group deletion initiated (async)" + + docker-scout: + name: Docker Scout CVE Scan + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + security-events: write + steps: + - name: Harden runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 + + - name: Login to Docker Hub + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build image for scanning + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7 + with: + context: . + file: ./Dockerfile + push: false + load: true + tags: bgt:scout-scan + cache-from: type=gha + build-args: | + VERSION=0.0.0-security + + - name: Docker Scout CVE scan + uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1 + with: + command: cves + image: local://bgt:scout-scan + only-severities: critical,high + sarif-file: scout-results.sarif + exit-code: true + only-fixed: true + + - name: Upload Scout SARIF + uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + if: always() + with: + sarif_file: scout-results.sarif + category: docker-scout + + full-dependency-audit: + name: Full Frontend Dependency Audit + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - name: Harden runner + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + + - name: Setup pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + with: + package_json_file: boardgametracker.client/package.json + + - name: Setup Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: "20.x" + cache: "pnpm" + cache-dependency-path: boardgametracker.client/pnpm-lock.yaml + + - name: Install dependencies + run: | + cd boardgametracker.client + pnpm install --frozen-lockfile --ignore-scripts + + - name: Audit all dependencies (informational) + continue-on-error: true + run: | + cd boardgametracker.client + pnpm audit --audit-level=moderate diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index d89a47b8..dab7aa08 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -4,6 +4,10 @@ on: pull_request: branches: [master] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: dependency-scan-backend: name: Backend Dependency Scan @@ -12,15 +16,15 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Setup dotnet v8 - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 with: dotnet-version: "8.x" @@ -29,13 +33,20 @@ jobs: - name: Check for vulnerable packages run: | - dotnet list ./BoardGameTracker.sln package --vulnerable --include-transitive 2>&1 | tee vulnerable.txt + set -uo pipefail + + dotnet list ./BoardGameTracker.sln package --vulnerable --include-transitive 2>&1 | tee vulnerable.txt || true if grep -q "has the following vulnerable packages" vulnerable.txt; then echo "::error::Vulnerable NuGet packages detected" exit 1 fi + if ! grep -q "has no vulnerable packages" vulnerable.txt; then + echo "::error::dotnet list package produced no vulnerability verdict" + exit 1 + fi + echo "No vulnerable packages found." dependency-scan-frontend: @@ -45,20 +56,20 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 - name: Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: package_json_file: boardgametracker.client/package.json - name: Setup Node.js - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: "20.x" cache: "pnpm" @@ -72,7 +83,7 @@ jobs: - name: Audit for vulnerabilities run: | cd boardgametracker.client - pnpm audit --audit-level=moderate + pnpm audit --audit-level=high --prod secret-detection: name: Secret Detection @@ -81,187 +92,16 @@ jobs: contents: read steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - name: Checkout (full history) - uses: actions/checkout@v6 + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 with: fetch-depth: 0 - name: Run Gitleaks - uses: gitleaks/gitleaks-action@v2 + uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - docker-scout: - name: Docker Scout CVE Scan - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - security-events: write - steps: - - name: Harden runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Login to Docker Hub - uses: docker/login-action@v4 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build image for scanning - uses: docker/build-push-action@v7 - with: - context: . - file: ./Dockerfile - push: false - load: true - tags: bgt:scout-scan - cache-from: type=gha - build-args: | - VERSION=0.0.0-security - - - name: Docker Scout CVE scan - uses: docker/scout-action@v1 - with: - command: cves - image: local://bgt:scout-scan - only-severities: critical,high - sarif-file: scout-results.sarif - exit-code: true - only-fixed: true - - - name: Upload Scout SARIF - uses: github/codeql-action/upload-sarif@v4 - if: always() - with: - sarif_file: scout-results.sarif - category: docker-scout - - zap-baseline: - name: ZAP Baseline Scan - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - pull-requests: write - steps: - - name: Harden runner - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Build application image - uses: docker/build-push-action@v7 - with: - context: . - file: ./Dockerfile - push: false - load: true - tags: bgt:security-scan - cache-from: type=gha - build-args: | - VERSION=0.0.0-security - - - name: Generate ephemeral secrets - id: secrets - run: | - DB_PASSWORD=$(openssl rand -base64 24) - JWT_SECRET=$(openssl rand -base64 48) - echo "::add-mask::$DB_PASSWORD" - echo "::add-mask::$JWT_SECRET" - echo "db_password=$DB_PASSWORD" >> "$GITHUB_OUTPUT" - echo "jwt_secret=$JWT_SECRET" >> "$GITHUB_OUTPUT" - - - name: Start application stack - env: - SEC_DB_PASSWORD: ${{ steps.secrets.outputs.db_password }} - SEC_JWT_SECRET: ${{ steps.secrets.outputs.jwt_secret }} - run: | - docker compose -f infra/docker-compose.security.yml up -d - echo "Waiting for application to become healthy..." - for i in $(seq 1 60); do - if curl -sf http://localhost:5444/api/health > /dev/null 2>&1; then - echo "Application is healthy after ${i}s" - break - fi - if [ "$i" -eq 60 ]; then - echo "::error::Application failed to become healthy within 60s" - docker compose -f infra/docker-compose.security.yml logs - exit 1 - fi - sleep 1 - done - - - name: Authenticate and extract JWT - id: auth - env: - ZAP_TEST_USERNAME: ${{ secrets.ZAP_TEST_USERNAME }} - ZAP_TEST_PASSWORD: ${{ secrets.ZAP_TEST_PASSWORD }} - run: | - RESPONSE=$(curl -sf -X POST http://localhost:5444/api/auth/login \ - -H "Content-Type: application/json" \ - -d "{\"username\": \"${{ env.ZAP_TEST_USERNAME }}\", \"password\": \"${{ env.ZAP_TEST_PASSWORD }}\"}") - - TOKEN=$(echo "$RESPONSE" | jq -r '.accessToken') - - if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then - echo "::error::Failed to extract JWT from login response" - echo "Response: $RESPONSE" - exit 1 - fi - - echo "::add-mask::$TOKEN" - echo "token=$TOKEN" >> "$GITHUB_OUTPUT" - - - name: Import OpenAPI spec into ZAP and run baseline - run: | - # Download the OpenAPI spec so ZAP knows all 60+ endpoints - curl -sf http://localhost:5444/swagger/v1/swagger.json -o /tmp/swagger.json - - ENDPOINT_COUNT=$(jq '[.paths | keys[] ] | length' /tmp/swagger.json) - echo "OpenAPI spec contains $ENDPOINT_COUNT endpoint paths" - - if [ "$ENDPOINT_COUNT" -lt 10 ]; then - echo "::error::OpenAPI spec has suspiciously few endpoints ($ENDPOINT_COUNT)" - exit 1 - fi - - - name: Run ZAP API Scan - uses: zaproxy/action-api-scan@v0.10.0 - with: - target: "http://localhost:5444/swagger/v1/swagger.json" - format: openapi - docker_name: "ghcr.io/zaproxy/zaproxy:stable" - allow_issue_writing: true - fail_action: "false" - cmd_options: >- - -z "-config replacer.full_list(0).description=AuthHeader - -config replacer.full_list(0).enabled=true - -config replacer.full_list(0).matchtype=REQ_HEADER - -config replacer.full_list(0).matchstr=Authorization - -config replacer.full_list(0).regex=false - -config replacer.full_list(0).replacement=Bearer\ ${{ steps.auth.outputs.token }} - -config replacer.full_list(0).initiators=" - rules_file_name: ".zap/rules.tsv" - - - name: Tear down application stack - if: always() - run: docker compose -f infra/docker-compose.security.yml down -v diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 98b8f73d..c38e6213 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit @@ -84,7 +84,7 @@ jobs: --force - name: Process stale issues and PRs - uses: actions/stale@v10 + uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10 with: stale-issue-label: stale stale-issue-message: > diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 39cd2036..e5be72a5 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -16,11 +16,11 @@ jobs: steps: - name: Harden runner - uses: step-security/harden-runner@v2 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2 with: egress-policy: audit - - uses: actions/first-interaction@v3 + - uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3 with: repo_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/infra/docker-compose.security.yml b/infra/docker-compose.security.yml deleted file mode 100644 index 761e1277..00000000 --- a/infra/docker-compose.security.yml +++ /dev/null @@ -1,38 +0,0 @@ -services: - boardgametracker: - image: bgt:security-scan - depends_on: - db: - condition: service_healthy - ports: - - "5444:5444" - environment: - - ASPNETCORE_ENVIRONMENT=production - - ASPNETCORE_URLS=http://*:5444 - - DB_HOST=db - - DB_USER=secuser - - DB_PASSWORD=${SEC_DB_PASSWORD:-securityscanpassword} - - DB_NAME=boardgametracker - - DB_PORT=5432 - - JWT_SECRET=${SEC_JWT_SECRET:-ci-security-scan-jwt-secret-not-for-production} - - AUTH_ENABLED=true - - TZ=UTC - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5444/api/health"] - interval: 5s - timeout: 5s - retries: 12 - start_period: 15s - - db: - image: postgres:16 - environment: - - POSTGRES_DB=boardgametracker - - POSTGRES_USER=secuser - - POSTGRES_PASSWORD=${SEC_DB_PASSWORD:-securityscanpassword} - healthcheck: - test: ["CMD-SHELL", "pg_isready -U secuser -d boardgametracker"] - interval: 5s - timeout: 5s - retries: 10 - start_period: 5s diff --git a/infra/security-scan.bicep b/infra/security-scan.bicep index 63c38632..0352b97e 100644 --- a/infra/security-scan.bicep +++ b/infra/security-scan.bicep @@ -9,26 +9,38 @@ param dbPassword string @secure() param jwtSecret string +@description('Username for pulling the image from ghcr.io') +param registryUsername string + +@description('Token for pulling the image from ghcr.io') +@secure() +param registryPassword string + @description('Azure region for all resources') param location string = resourceGroup().location @description('Unique suffix for resource names') param suffix string = uniqueString(resourceGroup().id) +var tags = { + name: 'boardgametracker' +} + resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { name: 'law-bgt-sec-${suffix}' location: location + tags: tags properties: { sku: { name: 'PerGB2018' } - retentionInDays: 7 } } resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { name: 'cae-bgt-sec-${suffix}' location: location + tags: tags properties: { appLogsConfiguration: { destination: 'log-analytics' @@ -40,20 +52,30 @@ resource containerAppEnv 'Microsoft.App/managedEnvironments@2024-03-01' = { } } -resource postgresApp 'Microsoft.App/containerApps@2024-03-01' = { - name: 'postgres-bgt-sec' +resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { + name: 'bgt-api-sec' location: location + tags: tags properties: { managedEnvironmentId: containerAppEnv.id configuration: { secrets: [ { name: 'db-password', value: dbPassword } + { name: 'jwt-secret', value: jwtSecret } + { name: 'ghcr-token', value: registryPassword } + ] + registries: [ + { + server: 'ghcr.io' + username: registryUsername + passwordSecretRef: 'ghcr-token' + } ] ingress: { - external: false - targetPort: 5432 - transport: 'tcp' - exposedPort: 5432 + external: true + targetPort: 5444 + transport: 'http' + allowInsecure: false } } template: { @@ -70,45 +92,7 @@ resource postgresApp 'Microsoft.App/containerApps@2024-03-01' = { { name: 'POSTGRES_USER', value: 'bgtuser' } { name: 'POSTGRES_PASSWORD', secretRef: 'db-password' } ] - probes: [ - { - type: 'Readiness' - tcpSocket: { - port: 5432 - } - initialDelaySeconds: 5 - periodSeconds: 10 - } - ] } - ] - scale: { - minReplicas: 1 - maxReplicas: 1 - } - } - } -} - -resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { - name: 'bgt-api-sec' - location: location - properties: { - managedEnvironmentId: containerAppEnv.id - configuration: { - secrets: [ - { name: 'db-password', value: dbPassword } - { name: 'jwt-secret', value: jwtSecret } - ] - ingress: { - external: true - targetPort: 5444 - transport: 'http' - allowInsecure: false - } - } - template: { - containers: [ { name: 'bgt-api' image: imageName @@ -119,7 +103,7 @@ resource bgtApp 'Microsoft.App/containerApps@2024-03-01' = { env: [ { name: 'ASPNETCORE_ENVIRONMENT', value: 'production' } { name: 'ASPNETCORE_URLS', value: 'http://*:5444' } - { name: 'DB_HOST', value: postgresApp.properties.configuration.ingress.fqdn } + { name: 'DB_HOST', value: 'localhost' } { name: 'DB_PORT', value: '5432' } { name: 'DB_USER', value: 'bgtuser' } { name: 'DB_PASSWORD', secretRef: 'db-password' } diff --git a/renovate.json b/renovate.json index 0e9d86d6..010457e7 100644 --- a/renovate.json +++ b/renovate.json @@ -5,7 +5,8 @@ "config:recommended", ":dependencyDashboard", ":semanticCommits", - "group:allNonMajor" + "group:allNonMajor", + "helpers:pinGitHubActionDigests" ], "timezone": "Europe/Brussels", "schedule": [