From d5dc982b753cbe87e092e52b759ec685b29e86f1 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Apr 2026 19:53:47 +0100 Subject: [PATCH 01/66] feat(pr-summary): Security summary comment in PRs --- .github/workflows/security-summary.yml | 80 ++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/security-summary.yml diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml new file mode 100644 index 00000000..6eb72f42 --- /dev/null +++ b/.github/workflows/security-summary.yml @@ -0,0 +1,80 @@ +--- +name: Security Summary + +on: + workflow_run: + workflows: ["Container", "Code"] + types: [completed] + branches-ignore: ["main"] + +jobs: + comment: + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + permissions: + security-events: read + pull-requests: write + actions: read + + steps: + - name: Get PR number + id: pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RUN_ID: ${{ github.event.workflow_run.id }} + REPO: ${{ github.repository }} + run: | + pr=$(gh api repos/$REPO/actions/runs/$RUN_ID/pull_requests --jq '.[0].number') + echo "number=$pr" >> "$GITHUB_OUTPUT" + ref=$(gh api repos/$REPO/actions/runs/$RUN_ID/pull_requests --jq '.[0].head.ref') + echo "ref=$ref" >> "$GITHUB_OUTPUT" + + - name: Fetch findings and post comment + if: steps.pr.outputs.number != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ steps.pr.outputs.number }} + REF: ${{ steps.pr.outputs.ref }} + run: | + declare -A tool_counts + tools=("Grype" "Trivy" "docker-scout" "Semgrep" "CodeQL") + labels=("Grype" "Trivy" "Scout" "Semgrep" "CodeQL") + + page=1 + while true; do + batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=$REF&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") + count=$(echo "$batch" | jq 'length') + [[ "$count" -eq 0 ]] && break + for tool in "${tools[@]}"; do + n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') + tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) + done + [[ "$count" -lt 100 ]] && break + (( page++ )) + done + + total=0 + rows="" + for i in "${!tools[@]}"; do + tool="${tools[$i]}" + label="${labels[$i]}" + c=${tool_counts[$tool]:-0} + total=$(( total + c )) + rows+="| $label | $c |"$'\n' + done + + body="## 🔒 Security Findings Summary + + | Tool | Open Findings | + |------|--------------| + ${rows}| **Total** | **$total** | + + > Findings filtered to this PR's branch. View details in the [Security tab](https://github.com/$REPO/security/code-scanning?q=is%3Aopen+pr%3A$PR)." + + # Delete previous summary comment if exists + existing=$(gh api "repos/$REPO/issues/$PR/comments" \ + --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) + [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" + + gh pr comment "$PR" --repo "$REPO" --body "$body" From 4513a0d6d22a860eea11089fc02e9421709e4113 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Apr 2026 20:18:05 +0100 Subject: [PATCH 02/66] fix(triggers): Remove branch ignore main --- .github/workflows/security-summary.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 6eb72f42..ab8d80fa 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -5,7 +5,6 @@ on: workflow_run: workflows: ["Container", "Code"] types: [completed] - branches-ignore: ["main"] jobs: comment: From 750989d9e67c093af1638ef2a515e8d6363af064 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sat, 11 Apr 2026 20:30:15 +0100 Subject: [PATCH 03/66] fix(summary): Update security comment summary PR logic --- .github/workflows/security-summary.yml | 48 ++++++++++++++++---------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index ab8d80fa..b34cf9fd 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -2,39 +2,50 @@ name: Security Summary on: - workflow_run: - workflows: ["Container", "Code"] - types: [completed] + pull_request: + branches: ["main"] + types: [opened, synchronize, reopened] jobs: comment: - if: github.event.workflow_run.event == 'pull_request' runs-on: ubuntu-latest permissions: security-events: read pull-requests: write actions: read + checks: read steps: - - name: Get PR number - id: pr + - name: Wait for security checks env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - RUN_ID: ${{ github.event.workflow_run.id }} REPO: ${{ github.repository }} + SHA: ${{ github.event.pull_request.head.sha }} run: | - pr=$(gh api repos/$REPO/actions/runs/$RUN_ID/pull_requests --jq '.[0].number') - echo "number=$pr" >> "$GITHUB_OUTPUT" - ref=$(gh api repos/$REPO/actions/runs/$RUN_ID/pull_requests --jq '.[0].head.ref') - echo "ref=$ref" >> "$GITHUB_OUTPUT" + checks=("Container" "Code") + timeout=1800 + elapsed=0 + while true; do + all_done=true + for check in "${checks[@]}"; do + statuses=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ + --jq "[.check_runs[] | select(.name | startswith(\"$check\") or contains(\"$check\"))] | map(.status) | unique | .[]" 2>/dev/null) + for s in $statuses; do + [[ "$s" != "completed" ]] && all_done=false && break 2 + done + done + $all_done && break + [[ $elapsed -ge $timeout ]] && echo "Timed out waiting for checks" && exit 1 + sleep 30 + elapsed=$(( elapsed + 30 )) + done - name: Fetch findings and post comment - if: steps.pr.outputs.number != '' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} - PR: ${{ steps.pr.outputs.number }} - REF: ${{ steps.pr.outputs.ref }} + PR: ${{ github.event.pull_request.number }} + REF: ${{ github.event.pull_request.head.ref }} run: | declare -A tool_counts tools=("Grype" "Trivy" "docker-scout" "Semgrep" "CodeQL") @@ -65,13 +76,12 @@ jobs: body="## 🔒 Security Findings Summary - | Tool | Open Findings | - |------|--------------| - ${rows}| **Total** | **$total** | +| Tool | Open Findings | +|------|--------------| +${rows}| **Total** | **$total** | - > Findings filtered to this PR's branch. View details in the [Security tab](https://github.com/$REPO/security/code-scanning?q=is%3Aopen+pr%3A$PR)." +> View details in the [Security tab](https://github.com/$REPO/security/code-scanning?q=is%3Aopen+pr%3A$PR)." - # Delete previous summary comment if exists existing=$(gh api "repos/$REPO/issues/$PR/comments" \ --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" From a2b124ddbe9f07a1239d7f1aefefaaadc3ffd629 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 08:39:23 +0100 Subject: [PATCH 04/66] fix(linter-findings): update gha yaml to fix linter findings --- .github/workflows/security-summary.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index b34cf9fd..cf4641f5 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -74,13 +74,11 @@ jobs: rows+="| $label | $c |"$'\n' done - body="## 🔒 Security Findings Summary - -| Tool | Open Findings | -|------|--------------| -${rows}| **Total** | **$total** | - -> View details in the [Security tab](https://github.com/$REPO/security/code-scanning?q=is%3Aopen+pr%3A$PR)." + header="## 🔒 Security Findings Summary" + sep=$'\n' + table="| Tool | Open Findings |${sep}|------|--------------|${sep}${rows}| **Total** | **$total** |" + footer="> View details in the [Security tab](https://github.com/$REPO/security/code-scanning?q=is%3Aopen+pr%3A$PR)." + body="${header}${sep}${sep}${table}${sep}${sep}${footer}" existing=$(gh api "repos/$REPO/issues/$PR/comments" \ --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) From 8e2ea2c3d23cd5c95715c43a49f02df0dd54079d Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 08:58:19 +0100 Subject: [PATCH 05/66] test(dockerfile): Downgrading alpine version to check if summary shows a vulnerability --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 5a0f2761..c63d0302 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build stage environment using alpine python Image -FROM alpine:3.23 AS build-env +FROM alpine:3.22 AS build-env # Set build directory WORKDIR /build From a9de700f7c0cc68df16414685d563eec4325384d Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 09:00:43 +0100 Subject: [PATCH 06/66] fix(workflow): Change trigger logic for security summary action --- .github/workflows/security-summary.yml | 34 ++++---------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index cf4641f5..c2c28b01 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -2,9 +2,9 @@ name: Security Summary on: - pull_request: - branches: ["main"] - types: [opened, synchronize, reopened] + workflow_run: + workflows: ["Code", "Container"] + types: [completed] jobs: comment: @@ -16,36 +16,12 @@ jobs: checks: read steps: - - name: Wait for security checks - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - SHA: ${{ github.event.pull_request.head.sha }} - run: | - checks=("Container" "Code") - timeout=1800 - elapsed=0 - while true; do - all_done=true - for check in "${checks[@]}"; do - statuses=$(gh api "repos/$REPO/commits/$SHA/check-runs" \ - --jq "[.check_runs[] | select(.name | startswith(\"$check\") or contains(\"$check\"))] | map(.status) | unique | .[]" 2>/dev/null) - for s in $statuses; do - [[ "$s" != "completed" ]] && all_done=false && break 2 - done - done - $all_done && break - [[ $elapsed -ge $timeout ]] && echo "Timed out waiting for checks" && exit 1 - sleep 30 - elapsed=$(( elapsed + 30 )) - done - - name: Fetch findings and post comment env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} - PR: ${{ github.event.pull_request.number }} - REF: ${{ github.event.pull_request.head.ref }} + PR: ${{ github.event.workflow_run.pull_requests[0].number }} + REF: ${{ github.event.workflow_run.head_branch }} run: | declare -A tool_counts tools=("Grype" "Trivy" "docker-scout" "Semgrep" "CodeQL") From fd554c7aa42fab32c2ebae5e239817b30308049c Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 09:04:11 +0100 Subject: [PATCH 07/66] test(semgrep): Revert dockerfile change, deliberate semgrep issue used isntead to test summary --- Dockerfile | 2 +- app/templates/index.html | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index c63d0302..5a0f2761 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Build stage environment using alpine python Image -FROM alpine:3.22 AS build-env +FROM alpine:3.23 AS build-env # Set build directory WORKDIR /build diff --git a/app/templates/index.html b/app/templates/index.html index db44e900..44998974 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -8,8 +8,6 @@ type="image/png" href="https://{{ cdn }}/favicon-96x96.png" sizes="96x96" - integrity="sha384-dnkgadw7QrrNMHmdxZW+FZtG1u69j4jZN+Rc1aPzMe8rYAfhbtAyh6p8NuI+7CEH" - crossorigin="anonymous" /> Date: Sun, 12 Apr 2026 09:29:30 +0100 Subject: [PATCH 08/66] feat(summary): add print statement in runner --- .github/workflows/security-summary.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index c2c28b01..2741d541 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -50,10 +50,17 @@ jobs: rows+="| $label | $c |"$'\n' done + echo "=== Security Findings ===" + for i in "${!tools[@]}"; do + echo "${labels[$i]}: ${tool_counts[${tools[$i]}]:-0}" + done + echo "Total: $total" + echo "=========================" + header="## 🔒 Security Findings Summary" sep=$'\n' table="| Tool | Open Findings |${sep}|------|--------------|${sep}${rows}| **Total** | **$total** |" - footer="> View details in the [Security tab](https://github.com/$REPO/security/code-scanning?q=is%3Aopen+pr%3A$PR)." + footer="> View details in the [Security tab](https://github.com/jackseceng/LinkShort/security/code-scanning?query=is%3Aopen+pr%3A$PR+)." body="${header}${sep}${sep}${table}${sep}${sep}${footer}" existing=$(gh api "repos/$REPO/issues/$PR/comments" \ From 604fc553f4d78908b71a2c434e1a555f737b1983 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 09:40:37 +0100 Subject: [PATCH 09/66] fix(summary): Update logic of all workflow to better trigger summary comment workflow --- .github/workflows/codeql.yml | 2 +- .github/workflows/container.yml | 1 + .github/workflows/conventional-commits.yml | 1 + .github/workflows/lint.yml | 2 +- .github/workflows/pr-checks.yml | 27 ++++++++++++++++++++++ .github/workflows/security-summary.yml | 2 +- .github/workflows/semgrep.yml | 1 + 7 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/pr-checks.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 488c496c..02d3199c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,7 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] - + workflow_call: permissions: read-all diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 44ef8c1b..112f2cc1 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -6,6 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + workflow_call: env: REGISTRY: docker.io diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 5c283fab..52902fb2 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -4,6 +4,7 @@ name: Commits on: pull_request: branches: [ "main" ] + workflow_call: jobs: standardise: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ae03bf80..b2b79eac 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] - + workflow_call: permissions: read-all diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 00000000..929c0a82 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,27 @@ +--- +name: PR Checks + +on: + pull_request: + branches: ["main"] + types: [opened, synchronize, reopened] + +jobs: + code: + uses: ./.github/workflows/codeql.yml + + semgrep: + uses: ./.github/workflows/semgrep.yml + secrets: inherit + + container: + uses: ./.github/workflows/container.yml + secrets: inherit + + commits: + uses: ./.github/workflows/conventional-commits.yml + secrets: inherit + + lint: + uses: ./.github/workflows/lint.yml + secrets: inherit diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 2741d541..7f91012e 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -3,7 +3,7 @@ name: Security Summary on: workflow_run: - workflows: ["Code", "Container"] + workflows: ["PR Checks"] types: [completed] jobs: diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index de6ca661..582c095d 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -7,6 +7,7 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ "main" ] + workflow_call: jobs: semgrep: From 9200bd715f976a8e5adc576be8027c8f7c5be6b4 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:04:42 +0100 Subject: [PATCH 10/66] fix(summary): remove non-security checks from workflow dependency --- .github/workflows/pr-checks.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 929c0a82..c71f9f68 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -17,11 +17,3 @@ jobs: container: uses: ./.github/workflows/container.yml secrets: inherit - - commits: - uses: ./.github/workflows/conventional-commits.yml - secrets: inherit - - lint: - uses: ./.github/workflows/lint.yml - secrets: inherit From a74642478163676a9a8db1c0ea43489349ab0a89 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:19:14 +0100 Subject: [PATCH 11/66] fix(summary): Adjust permissions and ubuntu version --- .github/workflows/pr-checks.yml | 10 ++++++++++ .github/workflows/security-summary.yml | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index c71f9f68..55a40c48 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -6,6 +6,16 @@ on: branches: ["main"] types: [opened, synchronize, reopened] +permissions: + contents: read + actions: read + checks: read + packages: write + security-events: write + pull-requests: write + statuses: write + id-token: write + jobs: code: uses: ./.github/workflows/codeql.yml diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 7f91012e..485fe892 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -8,7 +8,7 @@ on: jobs: comment: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 permissions: security-events: read pull-requests: write From 3f6151d6674fff3e196e6f2d3b131f86c56260ad Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:44:12 +0100 Subject: [PATCH 12/66] fix(codeql): update permissions for PR checks --- .github/workflows/codeql.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 02d3199c..004d873f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -20,6 +20,14 @@ jobs: packages: read actions: read contents: read + artifact-metadata: read + attestations: read + deployments: read + discussions: read + issues: read + models: read + pages: read + repository-projects: read strategy: fail-fast: false From 957d11b263be3419b66c137a4df856db7948119c Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:45:37 +0100 Subject: [PATCH 13/66] fix(codeql): update permissions --- .github/workflows/codeql.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 004d873f..bedee70d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,9 +8,6 @@ on: branches: [ "main" ] workflow_call: -permissions: - read-all - jobs: analyze: name: Analyze (${{ matrix.language }}) From 7496f06ab6055ce4463529e726cad25d434e98bc Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:46:30 +0100 Subject: [PATCH 14/66] fix(codeql): update permissions --- .github/workflows/codeql.yml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bedee70d..09a3b882 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,23 +8,24 @@ on: branches: [ "main" ] workflow_call: +permissions: + security-events: write + packages: read + actions: read + contents: read + artifact-metadata: read + attestations: read + deployments: read + discussions: read + issues: read + models: read + pages: read + repository-projects: read + jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-24.04' }} - permissions: - security-events: write - packages: read - actions: read - contents: read - artifact-metadata: read - attestations: read - deployments: read - discussions: read - issues: read - models: read - pages: read - repository-projects: read strategy: fail-fast: false From 810b2932cd5bcd135698d76086799b75f5fba96a Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:47:28 +0100 Subject: [PATCH 15/66] fix(pr-checks): update permissions --- .github/workflows/codeql.yml | 18 ++++++------------ .github/workflows/pr-checks.yml | 18 +++++++++++------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 09a3b882..02d3199c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -9,23 +9,17 @@ on: workflow_call: permissions: - security-events: write - packages: read - actions: read - contents: read - artifact-metadata: read - attestations: read - deployments: read - discussions: read - issues: read - models: read - pages: read - repository-projects: read + read-all jobs: analyze: name: Analyze (${{ matrix.language }}) runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-24.04' }} + permissions: + security-events: write + packages: read + actions: read + contents: read strategy: fail-fast: false diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 55a40c48..21e40bcf 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -7,14 +7,18 @@ on: types: [opened, synchronize, reopened] permissions: - contents: read - actions: read - checks: read - packages: write security-events: write - pull-requests: write - statuses: write - id-token: write + packages: read + actions: read + contents: read + artifact-metadata: read + attestations: read + deployments: read + discussions: read + issues: read + models: read + pages: read + repository-projects: read jobs: code: From 73cfaf710fb5bf02384efde6cf76dbaefc1046b4 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:48:16 +0100 Subject: [PATCH 16/66] fix(pr-checks): update permissions --- .github/workflows/pr-checks.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 21e40bcf..ee358bf1 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -19,6 +19,10 @@ permissions: models: read pages: read repository-projects: read + checks: read + pull-requests: read + statuses: read + id-token: read jobs: code: From 386842f00a2f16863e83ad19d7c515962a60ce4f Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:49:41 +0100 Subject: [PATCH 17/66] fix(pr-checks): update permissions --- .github/workflows/pr-checks.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ee358bf1..52d364ad 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -22,7 +22,6 @@ permissions: checks: read pull-requests: read statuses: read - id-token: read jobs: code: From 8229b12aaec6bbbebd92bd7e23b789f80b3ab797 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:50:37 +0100 Subject: [PATCH 18/66] fix(pr-checks): update permissions --- .github/workflows/pr-checks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 52d364ad..362eb732 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -15,6 +15,7 @@ permissions: attestations: read deployments: read discussions: read + id-token: read issues: read models: read pages: read From 1e6ea7af43169f8dcc296bba6ac8efcf785cf52d Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:52:09 +0100 Subject: [PATCH 19/66] fix(pr-checks): update permissions --- .github/workflows/pr-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 362eb732..4bc8db96 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -15,7 +15,7 @@ permissions: attestations: read deployments: read discussions: read - id-token: read + id-token: write issues: read models: read pages: read From 23db8c2ffea0d5a9a09373d69ed37a189f449118 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:52:55 +0100 Subject: [PATCH 20/66] fix(pr-checks): update permissions --- .github/workflows/pr-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 4bc8db96..74cb559a 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -8,7 +8,7 @@ on: permissions: security-events: write - packages: read + packages: write actions: read contents: read artifact-metadata: read From aea8fba2f7b59bd6dfd266edefb6fa4eace2a535 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 12:53:43 +0100 Subject: [PATCH 21/66] fix(pr-checks): update permissions --- .github/workflows/pr-checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 74cb559a..5ef31a0b 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -21,7 +21,7 @@ permissions: pages: read repository-projects: read checks: read - pull-requests: read + pull-requests: write statuses: read jobs: From 5ef644f88ea0670d5b782c0fbec5b301ab8e997d Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:13:16 +0100 Subject: [PATCH 22/66] ci(summary): Change summary trigger to review being ready instead of CI completion --- .github/workflows/conventional-commits.yml | 2 +- .github/workflows/pr-checks.yml | 37 ---------------------- .github/workflows/security-summary.yml | 10 +++--- 3 files changed, 6 insertions(+), 43 deletions(-) delete mode 100644 .github/workflows/pr-checks.yml diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 52902fb2..6008919d 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -18,4 +18,4 @@ jobs: uses: webiny/action-conventional-commits@faccb24fc2550dd15c0390d944379d2d8ed9690e # v1.3.1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - allowed-commit-types: "feat,fix,chore,docs,refactor,build,ci" + allowed-commit-types: "feat,fix,chore,docs,refactor,build,ci,style,perf,test" diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml deleted file mode 100644 index 5ef31a0b..00000000 --- a/.github/workflows/pr-checks.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: PR Checks - -on: - pull_request: - branches: ["main"] - types: [opened, synchronize, reopened] - -permissions: - security-events: write - packages: write - actions: read - contents: read - artifact-metadata: read - attestations: read - deployments: read - discussions: read - id-token: write - issues: read - models: read - pages: read - repository-projects: read - checks: read - pull-requests: write - statuses: read - -jobs: - code: - uses: ./.github/workflows/codeql.yml - - semgrep: - uses: ./.github/workflows/semgrep.yml - secrets: inherit - - container: - uses: ./.github/workflows/container.yml - secrets: inherit diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 485fe892..946024ac 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -2,9 +2,9 @@ name: Security Summary on: - workflow_run: - workflows: ["PR Checks"] - types: [completed] + pull_request: + branches: ["main"] + types: [ready_for_review] jobs: comment: @@ -20,8 +20,8 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} - PR: ${{ github.event.workflow_run.pull_requests[0].number }} - REF: ${{ github.event.workflow_run.head_branch }} + PR: ${{ github.event.pull_request.number }} + REF: ${{ github.event.pull_request.head.ref }} run: | declare -A tool_counts tools=("Grype" "Trivy" "docker-scout" "Semgrep" "CodeQL") From 99092555e238e2e58250158260555f5aa8e9b8a4 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:20:34 +0100 Subject: [PATCH 23/66] ci(summary): update url used to get security findings --- .github/workflows/security-summary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 946024ac..0bb41c64 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -29,7 +29,7 @@ jobs: page=1 while true; do - batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=$REF&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") + batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") count=$(echo "$batch" | jq 'length') [[ "$count" -eq 0 ]] && break for tool in "${tools[@]}"; do From 477028acea7b59f66a0a6395719f8ce3b9d27961 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:23:33 +0100 Subject: [PATCH 24/66] ci(container-scans): Reduce scope to just container related changes --- .github/workflows/container.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 112f2cc1..1966bfdc 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -4,8 +4,20 @@ name: Container on: push: branches: [ "main" ] + paths: + - 'Dockerfile' + - 'docker-compose.yaml' + - '.dockerignore' + - 'app/**' + - '!app/static/**' pull_request: branches: [ "main" ] + paths: + - 'Dockerfile' + - 'docker-compose.yaml' + - '.dockerignore' + - 'app/**' + - '!app/static/**' workflow_call: env: From d7a32b19df42a714db5cf2c1dd8b5b69c326f837 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:25:49 +0100 Subject: [PATCH 25/66] ci(yaml-files): Simplify GHA trigger conditions --- .github/workflows/codeql.yml | 1 - .github/workflows/container.yml | 13 ------------- .github/workflows/conventional-commits.yml | 1 - .github/workflows/lint.yml | 1 - .github/workflows/semgrep.yml | 2 -- 5 files changed, 18 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 02d3199c..5f17903d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,7 +6,6 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] - workflow_call: permissions: read-all diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 1966bfdc..44ef8c1b 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -4,21 +4,8 @@ name: Container on: push: branches: [ "main" ] - paths: - - 'Dockerfile' - - 'docker-compose.yaml' - - '.dockerignore' - - 'app/**' - - '!app/static/**' pull_request: branches: [ "main" ] - paths: - - 'Dockerfile' - - 'docker-compose.yaml' - - '.dockerignore' - - 'app/**' - - '!app/static/**' - workflow_call: env: REGISTRY: docker.io diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml index 6008919d..0b7d9580 100644 --- a/.github/workflows/conventional-commits.yml +++ b/.github/workflows/conventional-commits.yml @@ -4,7 +4,6 @@ name: Commits on: pull_request: branches: [ "main" ] - workflow_call: jobs: standardise: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b2b79eac..8913e7da 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,6 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] - workflow_call: permissions: read-all diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 582c095d..b0093252 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -5,9 +5,7 @@ on: push: branches: [ "main" ] pull_request: - # The branches below must be a subset of the branches above branches: [ "main" ] - workflow_call: jobs: semgrep: From f200472123fc9e7b166b9ee6d5fb4cf1c6e42cf6 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:27:45 +0100 Subject: [PATCH 26/66] ci(summary): Add additional logic to scrape security alerts --- .github/workflows/security-summary.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 0bb41c64..d8e7e6c0 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -27,6 +27,10 @@ jobs: tools=("Grype" "Trivy" "docker-scout" "Semgrep" "CodeQL") labels=("Grype" "Trivy" "Scout" "Semgrep" "CodeQL") + echo "=== Raw alerts (no ref filter) ===" + gh api "repos/$REPO/code-scanning/alerts?state=open&per_page=10" 2>/dev/null | jq '[.[] | {tool: .tool.name, ref: .most_recent_instance.ref, state: .state}]' + echo "===================================" + page=1 while true; do batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") From 7a5da10b91498366cf54ae9f5c6cd82343cc7b10 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:29:01 +0100 Subject: [PATCH 27/66] ci(summary): add print statements for debugging --- .github/workflows/security-summary.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index d8e7e6c0..45dc05bb 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -31,6 +31,11 @@ jobs: gh api "repos/$REPO/code-scanning/alerts?state=open&per_page=10" 2>/dev/null | jq '[.[] | {tool: .tool.name, ref: .most_recent_instance.ref, state: .state}]' echo "===================================" + echo "=== Querying ref: refs/heads/$REF ===" + echo "=== Full first page response ===" + gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=1" 2>&1 | jq '.' + echo "================================" + page=1 while true; do batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") From 9c0e8fd73f13acb96cd6df87a1293616a739eb77 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:29:10 +0100 Subject: [PATCH 28/66] ci(summary): add print statements to debug --- .github/workflows/security-summary.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 45dc05bb..4d2b4add 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -38,7 +38,9 @@ jobs: page=1 while true; do - batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") + batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=$page" 2>&1 || echo "[]") + echo "=== Page $page: $count alerts, batch sample ===" + echo "$batch" | jq '[.[] | {tool: .tool.name, ref: .most_recent_instance.ref}] | .[0:3]' 2>/dev/null || echo "$batch" count=$(echo "$batch" | jq 'length') [[ "$count" -eq 0 ]] && break for tool in "${tools[@]}"; do From d8db3a90322e85e5127b536a380fa4830ad9aa8a Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:39:33 +0100 Subject: [PATCH 29/66] ci(summary): update PR check logic to show introduced vulnerabilities in PR --- .github/workflows/security-summary.yml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 4d2b4add..83c62668 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -21,26 +21,14 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPO: ${{ github.repository }} PR: ${{ github.event.pull_request.number }} - REF: ${{ github.event.pull_request.head.ref }} run: | declare -A tool_counts - tools=("Grype" "Trivy" "docker-scout" "Semgrep" "CodeQL") + tools=("Grype" "Trivy" "docker-scout" "Semgrep PRO" "CodeQL") labels=("Grype" "Trivy" "Scout" "Semgrep" "CodeQL") - echo "=== Raw alerts (no ref filter) ===" - gh api "repos/$REPO/code-scanning/alerts?state=open&per_page=10" 2>/dev/null | jq '[.[] | {tool: .tool.name, ref: .most_recent_instance.ref, state: .state}]' - echo "===================================" - - echo "=== Querying ref: refs/heads/$REF ===" - echo "=== Full first page response ===" - gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=1" 2>&1 | jq '.' - echo "================================" - page=1 while true; do - batch=$(gh api "repos/$REPO/code-scanning/alerts?ref=refs/heads/$REF&state=open&per_page=100&page=$page" 2>&1 || echo "[]") - echo "=== Page $page: $count alerts, batch sample ===" - echo "$batch" | jq '[.[] | {tool: .tool.name, ref: .most_recent_instance.ref}] | .[0:3]' 2>/dev/null || echo "$batch" + batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") count=$(echo "$batch" | jq 'length') [[ "$count" -eq 0 ]] && break for tool in "${tools[@]}"; do From 1a9de987ffc91e5a1c56977d5e698bd6182c2b4b Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:42:48 +0100 Subject: [PATCH 30/66] ci(summary): add vulnerabiltiy description to comment --- .github/workflows/security-summary.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 83c62668..d6008004 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -26,11 +26,13 @@ jobs: tools=("Grype" "Trivy" "docker-scout" "Semgrep PRO" "CodeQL") labels=("Grype" "Trivy" "Scout" "Semgrep" "CodeQL") + all_alerts="[]" page=1 while true; do batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") count=$(echo "$batch" | jq 'length') [[ "$count" -eq 0 ]] && break + all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') for tool in "${tools[@]}"; do n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) @@ -49,18 +51,15 @@ jobs: rows+="| $label | $c |"$'\n' done - echo "=== Security Findings ===" - for i in "${!tools[@]}"; do - echo "${labels[$i]}: ${tool_counts[${tools[$i]}]:-0}" - done - echo "Total: $total" - echo "=========================" + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line) | \(.tool.name) | \(.rule.severity) | \(.most_recent_instance.message.text) |"') - header="## 🔒 Security Findings Summary" sep=$'\n' + header="## 🔒 Security Findings Summary" table="| Tool | Open Findings |${sep}|------|--------------|${sep}${rows}| **Total** | **$total** |" + findings_table="| File | Tool | Severity | Description |${sep}|------|------|----------|-------------|${sep}${details}" + dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" footer="> View details in the [Security tab](https://github.com/jackseceng/LinkShort/security/code-scanning?query=is%3Aopen+pr%3A$PR+)." - body="${header}${sep}${sep}${table}${sep}${sep}${footer}" + body="${header}${sep}${sep}${table}${sep}${sep}${dropdown}${sep}${sep}${footer}" existing=$(gh api "repos/$REPO/issues/$PR/comments" \ --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) From de25937e8e133ea25673ef0f22c89672587e072d Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:47:52 +0100 Subject: [PATCH 31/66] test(VULNERABILITY): Adding a DELIBERATLEY vulnerable docker image for testing Will change it back after testing is done --- Dockerfile | 92 +++++------------------------------------------------- 1 file changed, 7 insertions(+), 85 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5a0f2761..7c1b04cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,87 +1,9 @@ -# Stage 1: Build stage environment using alpine python Image -FROM alpine:3.23 AS build-env +# TEMPORARY: vulnerable test image - do not merge +FROM ubuntu:14.04 -# Set build directory -WORKDIR /build +RUN apt-get update && apt-get install -y \ + busybox \ + openssl \ + curl -# Copy application files -COPY . . - -# Build Python 3.15.0a6 and apk dependencies from source -RUN set -e; \ - apk add --no-cache \ - build-base=0.5-r3 \ - cmake=4.1.3-r0 \ - coreutils=9.8-r1 \ - libffi-dev=3.5.2-r0 \ - openssl-dev=3.5.6-r0 \ - zlib-dev=1.3.2-r0 \ - bzip2-dev=1.0.8-r6 \ - xz-dev=5.8.2-r0 \ - wget=1.25.0-r2; \ - wget --progress=dot:giga https://www.python.org/ftp/python/3.15.0/Python-3.15.0a6.tgz; \ - tar -xzf Python-3.15.0a6.tgz; \ - ./Python-3.15.0a6/configure --prefix=/usr/local --enable-shared --with-ensurepip=install; \ - make -j"$(nproc)"; \ - make install; \ - ln -s /usr/local/bin/python3.15 /usr/local/bin/python; - -# Install python dependencies into a target directory -RUN set -e; \ - pip3.15 install --no-cache-dir --upgrade 'pip==26.0'; \ - PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip3.15 install --no-cache-dir -r requirements.txt --target /packages; \ - mkdir -p /tmp && chmod 1777 /tmp && chown 1001:1001 /tmp; - - -# Stage 2: Runtime Stage using scratch Image -FROM scratch - -# Copy necessary system libraries and interpreter from build-env -COPY --from=build-env /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 -COPY --from=build-env /lib/libc.musl-x86_64.so.1 /lib/libc.musl-x86_64.so.1 -COPY --from=build-env /usr/local/lib/libpython3.15.so.1.0 /usr/local/lib/libpython3.15.so.1.0 -COPY --from=build-env /usr/local/lib/python3.15 /usr/local/lib/python3.15 -COPY --from=build-env /usr/lib/libssl.so.3 /usr/lib/libssl.so.3 -COPY --from=build-env /usr/lib/libcrypto.so.3 /usr/lib/libcrypto.so.3 -COPY --from=build-env /usr/lib/libz.so.1 /usr/lib/libz.so.1 -COPY --from=build-env /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 -COPY --from=build-env /usr/lib/libffi.so.8 /usr/lib/libffi.so.8 - -# Copy Python installation -COPY --from=build-env /usr/local/bin/python /usr/local/bin/python - -# Copy CA certificates needed for SSL verification -COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt - -# Copy installed packages -COPY --from=build-env /packages /packages - -# Copy application code -COPY --from=build-env /build/app /app - -# Copy tmp directory (required for Python/gunicorn) -COPY --from=build-env /tmp /tmp - -# Set required environment variables for Python -ENV PYTHONUNBUFFERED=1 \ - PYTHONIOENCODING=UTF-8 \ - PYTHONPATH=/packages \ - SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ - TMPDIR=/tmp \ - HOME=/tmp - -# Set runtime directory -WORKDIR /app - -# Expose the application port -EXPOSE 80 - -# Set up container healthcheck -HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \ - CMD ["/usr/local/bin/python", "healthcheck/healthcheck.py"] - -# Switch to non-privileged user -USER 1001:1001 - -# Define the command to run the application -CMD ["/usr/local/bin/python", "gunicorn_cfg.py"] +CMD ["busybox", "sh"] From 71854c529b2bc382b48187a1878d234faa66c2f0 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 13:52:40 +0100 Subject: [PATCH 32/66] ci(summary): Cache findings for large amounts of vulnerabilities --- .github/workflows/security-summary.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index d6008004..a12a2408 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -51,7 +51,7 @@ jobs: rows+="| $label | $c |"$'\n' done - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line) | \(.tool.name) | \(.rule.severity) | \(.most_recent_instance.message.text) |"') + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line) | \(.tool.name) | \(.rule.severity) | \(.most_recent_instance.message.text | .[0:150]) |"') sep=$'\n' header="## 🔒 Security Findings Summary" @@ -65,4 +65,5 @@ jobs: --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" - gh pr comment "$PR" --repo "$REPO" --body "$body" + echo "$body" > /tmp/comment-body.md + gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md From 44250e058e477a967412d3f9c80130f40481268f Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:00:44 +0100 Subject: [PATCH 33/66] ci(trivy-test): update contianer config for trivy to test older versions --- .github/workflows/container.yml | 2 +- .github/workflows/security-summary.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 44ef8c1b..9a1bb157 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -101,7 +101,7 @@ jobs: with: image-ref: '${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }}' scan-type: 'image' - ignore-unfixed: true + ignore-unfixed: false severity: 'CRITICAL,HIGH,MEDIUM' format: 'sarif' output: 'results.sarif' diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index a12a2408..ed60571c 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -23,7 +23,7 @@ jobs: PR: ${{ github.event.pull_request.number }} run: | declare -A tool_counts - tools=("Grype" "Trivy" "docker-scout" "Semgrep PRO" "CodeQL") + tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") labels=("Grype" "Trivy" "Scout" "Semgrep" "CodeQL") all_alerts="[]" From 4760313c30558d0280ec5b5f943149c6f2902fc4 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:05:17 +0100 Subject: [PATCH 34/66] test(VULNERABILITY): new vulnerable ubuntu version for testing --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7c1b04cc..c1e9f568 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # TEMPORARY: vulnerable test image - do not merge -FROM ubuntu:14.04 +FROM ubuntu:20.04 RUN apt-get update && apt-get install -y \ busybox \ From 7ba90c3dd10d5da5ebc3184edaa95b4cb86cbea7 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:10:11 +0100 Subject: [PATCH 35/66] ci(summary): fix table formatting --- .github/workflows/security-summary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index ed60571c..9ffdab88 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -51,7 +51,7 @@ jobs: rows+="| $label | $c |"$'\n' done - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path):\(.most_recent_instance.location.start_line) | \(.tool.name) | \(.rule.severity) | \(.most_recent_instance.message.text | .[0:150]) |"') + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.tool.name) | \(.rule.severity) | \(.most_recent_instance.message.text | gsub("\n"; " ") | gsub("\\s+"; " ") | .[0:150]) |"') sep=$'\n' header="## 🔒 Security Findings Summary" From 4646ab7fa01340a6045b08c5a0595e11e03ebefd Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:17:49 +0100 Subject: [PATCH 36/66] ci(summary): update messaging --- .github/workflows/security-summary.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 9ffdab88..03ea6038 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -67,3 +67,5 @@ jobs: echo "$body" > /tmp/comment-body.md gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md + + [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. All findings must be fixed or justified before this PR can be merged. See https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 From ea399debd0e4d95b307661ffb070b8d23b1cc692 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:21:52 +0100 Subject: [PATCH 37/66] ci(summary): update messaging format --- .github/workflows/security-summary.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 03ea6038..7910784b 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -58,7 +58,7 @@ jobs: table="| Tool | Open Findings |${sep}|------|--------------|${sep}${rows}| **Total** | **$total** |" findings_table="| File | Tool | Severity | Description |${sep}|------|------|----------|-------------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" - footer="> View details in the [Security tab](https://github.com/jackseceng/LinkShort/security/code-scanning?query=is%3Aopen+pr%3A$PR+)." + footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs") for details." body="${header}${sep}${sep}${table}${sep}${sep}${dropdown}${sep}${sep}${footer}" existing=$(gh api "repos/$REPO/issues/$PR/comments" \ @@ -68,4 +68,4 @@ jobs: echo "$body" > /tmp/comment-body.md gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md - [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. All findings must be fixed or justified before this PR can be merged. See https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 + [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 From 871e50ac3738aedc6174dc2f0d7d6f2bd27c798b Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:24:00 +0100 Subject: [PATCH 38/66] fix(summary): fix syntax error --- .github/workflows/security-summary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 7910784b..71266ebd 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -58,7 +58,7 @@ jobs: table="| Tool | Open Findings |${sep}|------|--------------|${sep}${rows}| **Total** | **$total** |" findings_table="| File | Tool | Severity | Description |${sep}|------|------|----------|-------------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" - footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs") for details." + footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." body="${header}${sep}${sep}${table}${sep}${sep}${dropdown}${sep}${sep}${footer}" existing=$(gh api "repos/$REPO/issues/$PR/comments" \ From ed05d1c412d9f1bbf6bb411035aeb914698173b3 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:39:36 +0100 Subject: [PATCH 39/66] ci(summary): update the source and formatting of the detail table --- .github/workflows/security-summary.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 71266ebd..09e5f3ee 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -51,12 +51,12 @@ jobs: rows+="| $label | $c |"$'\n' done - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.tool.name) | \(.rule.severity) | \(.most_recent_instance.message.text | gsub("\n"; " ") | gsub("\\s+"; " ") | .[0:150]) |"') + details=$(echo "$all_alerts" | jq -r '.[] | "| [\(.rule.id)](\(.html_url)) | \(.tool.name) | \(.rule.security_severity_level) | \(.rule.description) |"') sep=$'\n' header="## 🔒 Security Findings Summary" table="| Tool | Open Findings |${sep}|------|--------------|${sep}${rows}| **Total** | **$total** |" - findings_table="| File | Tool | Severity | Description |${sep}|------|------|----------|-------------|${sep}${details}" + findings_table="| CVE / Rule | Tool | Severity | Description |${sep}|-----------|------|----------|-------------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." body="${header}${sep}${sep}${table}${sep}${sep}${dropdown}${sep}${sep}${footer}" From def467a9eb233db2a6c4726d294de7afe812da1e Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:45:04 +0100 Subject: [PATCH 40/66] ci(summary): update format --- .github/workflows/security-summary.yml | 27 ++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 09e5f3ee..7d33208e 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -24,7 +24,6 @@ jobs: run: | declare -A tool_counts tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") - labels=("Grype" "Trivy" "Scout" "Semgrep" "CodeQL") all_alerts="[]" page=1 @@ -42,24 +41,36 @@ jobs: done total=0 - rows="" for i in "${!tools[@]}"; do tool="${tools[$i]}" - label="${labels[$i]}" c=${tool_counts[$tool]:-0} total=$(( total + c )) - rows+="| $label | $c |"$'\n' done - details=$(echo "$all_alerts" | jq -r '.[] | "| [\(.rule.id)](\(.html_url)) | \(.tool.name) | \(.rule.security_severity_level) | \(.rule.description) |"') + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | [\(.rule.id)](\(.html_url)) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') + + severity_order=("critical" "high" "error" "medium" "warning" "low" "note") + highest="none" + while IFS= read -r sev; do + for s in "${severity_order[@]}"; do + if [[ "${sev,,}" == "$s" ]]; then + for ranked in "${severity_order[@]}"; do + [[ "$ranked" == "$highest" ]] && break + [[ "$ranked" == "$s" ]] && highest="$s" && break + done + break + fi + done + done < <(echo "$all_alerts" | jq -r '.[] | (.rule.security_severity_level // .rule.severity)') sep=$'\n' header="## 🔒 Security Findings Summary" - table="| Tool | Open Findings |${sep}|------|--------------|${sep}${rows}| **Total** | **$total** |" - findings_table="| CVE / Rule | Tool | Severity | Description |${sep}|-----------|------|----------|-------------|${sep}${details}" + summary="**Total findings:** $total | **Highest severity:** $highest" + findings_table="| File | CVE / Rule | Description | Severity | Tool |${sep}|------|-----------|-------------|----------|------|${sep}${details}" + dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." - body="${header}${sep}${sep}${table}${sep}${sep}${dropdown}${sep}${sep}${footer}" + body="${header}${sep}${sep}${summary}${sep}${sep}${dropdown}${sep}${sep}${footer}" existing=$(gh api "repos/$REPO/issues/$PR/comments" \ --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) From 13031860ad2c950dc969be8e26c04c5d30f4628f Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:47:32 +0100 Subject: [PATCH 41/66] ci(summary): adding markdown format to summary --- .github/workflows/security-summary.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 7d33208e..14f5340d 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -63,9 +63,16 @@ jobs: done done < <(echo "$all_alerts" | jq -r '.[] | (.rule.security_severity_level // .rule.severity)') + case "$highest" in + critical) alert_type="CAUTION" ;; + high|error) alert_type="WARNING" ;; + medium) alert_type="IMPORTANT" ;; + *) alert_type="NOTE" ;; + esac + sep=$'\n' header="## 🔒 Security Findings Summary" - summary="**Total findings:** $total | **Highest severity:** $highest" + summary="> [!${alert_type}]${sep}> **Total findings:** $total | **Highest severity:** $highest" findings_table="| File | CVE / Rule | Description | Severity | Tool |${sep}|------|-----------|-------------|----------|------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" From 7e2439fd02472f3b4443b8055eff23344ee7ce29 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:50:31 +0100 Subject: [PATCH 42/66] ci(summary): update messaging --- .github/workflows/security-summary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 14f5340d..17ba74cf 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -72,7 +72,7 @@ jobs: sep=$'\n' header="## 🔒 Security Findings Summary" - summary="> [!${alert_type}]${sep}> **Total findings:** $total | **Highest severity:** $highest" + summary="> [!${alert_type}]${sep}> **Total findings:** $total which are at most $highest severity." findings_table="| File | CVE / Rule | Description | Severity | Tool |${sep}|------|-----------|-------------|----------|------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" From 53f3da16fe769b7b70980bd1b2589323492fda75 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:51:50 +0100 Subject: [PATCH 43/66] ci(summary): update messaging --- .github/workflows/security-summary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 17ba74cf..d42339ba 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -72,7 +72,7 @@ jobs: sep=$'\n' header="## 🔒 Security Findings Summary" - summary="> [!${alert_type}]${sep}> **Total findings:** $total which are at most $highest severity." + summary="> [!${alert_type}]${sep}> **$total findings, some of which are $highest severity." findings_table="| File | CVE / Rule | Description | Severity | Tool |${sep}|------|-----------|-------------|----------|------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" From b17d18d312dc8addf9f197c8623849cee1ef9ea1 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:52:39 +0100 Subject: [PATCH 44/66] ci(summary): update messaging --- .github/workflows/security-summary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index d42339ba..3efbad73 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -72,7 +72,7 @@ jobs: sep=$'\n' header="## 🔒 Security Findings Summary" - summary="> [!${alert_type}]${sep}> **$total findings, some of which are $highest severity." + summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." findings_table="| File | CVE / Rule | Description | Severity | Tool |${sep}|------|-----------|-------------|----------|------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" From f6e13d44d283e678547aa333290046f5ff8dc935 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:55:06 +0100 Subject: [PATCH 45/66] ci(summary): update table format --- .github/workflows/security-summary.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 3efbad73..695589a8 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -47,7 +47,7 @@ jobs: total=$(( total + c )) done - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | [\(.rule.id)](\(.html_url)) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | [\(.rule.id)](\(.html_url)) | \(.tool.name) |"' | grep -v '^$') severity_order=("critical" "high" "error" "medium" "warning" "low" "note") highest="none" @@ -73,7 +73,7 @@ jobs: sep=$'\n' header="## 🔒 Security Findings Summary" summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." - findings_table="| File | CVE / Rule | Description | Severity | Tool |${sep}|------|-----------|-------------|----------|------|${sep}${details}" + findings_table="| File | Description | Severity | CVE / Rule | Tool |${sep}|------|-------------|----------|-----------|------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." From 925c69abc6d7554e6519cc65f017c5c664e4b353 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:57:50 +0100 Subject: [PATCH 46/66] ci(summary): remove cve links --- .github/workflows/security-summary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 695589a8..07849050 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -47,7 +47,7 @@ jobs: total=$(( total + c )) done - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | [\(.rule.id)](\(.html_url)) | \(.tool.name) |"' | grep -v '^$') + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.rule.id) | \(.tool.name) |"' | grep -v '^$') severity_order=("critical" "high" "error" "medium" "warning" "low" "note") highest="none" From 6f3350684135e63b75c0ec31aff7d0c007eca220 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 14:59:23 +0100 Subject: [PATCH 47/66] ci(summary): remove cve column --- .github/workflows/security-summary.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml index 07849050..32e3f4f0 100644 --- a/.github/workflows/security-summary.yml +++ b/.github/workflows/security-summary.yml @@ -47,7 +47,7 @@ jobs: total=$(( total + c )) done - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.rule.id) | \(.tool.name) |"' | grep -v '^$') + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') severity_order=("critical" "high" "error" "medium" "warning" "low" "note") highest="none" @@ -73,7 +73,7 @@ jobs: sep=$'\n' header="## 🔒 Security Findings Summary" summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." - findings_table="| File | Description | Severity | CVE / Rule | Tool |${sep}|------|-------------|----------|-----------|------|${sep}${details}" + findings_table="| File | Description | Severity | Tool |${sep}|------|-------------|----------|------|${sep}${details}" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." From 7587c6222c1971fb74b67a0378efd174e448aee6 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 15:03:14 +0100 Subject: [PATCH 48/66] fix(dockerfile): revert to clean version --- Dockerfile | 92 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1e9f568..5a0f2761 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,87 @@ -# TEMPORARY: vulnerable test image - do not merge -FROM ubuntu:20.04 +# Stage 1: Build stage environment using alpine python Image +FROM alpine:3.23 AS build-env -RUN apt-get update && apt-get install -y \ - busybox \ - openssl \ - curl +# Set build directory +WORKDIR /build -CMD ["busybox", "sh"] +# Copy application files +COPY . . + +# Build Python 3.15.0a6 and apk dependencies from source +RUN set -e; \ + apk add --no-cache \ + build-base=0.5-r3 \ + cmake=4.1.3-r0 \ + coreutils=9.8-r1 \ + libffi-dev=3.5.2-r0 \ + openssl-dev=3.5.6-r0 \ + zlib-dev=1.3.2-r0 \ + bzip2-dev=1.0.8-r6 \ + xz-dev=5.8.2-r0 \ + wget=1.25.0-r2; \ + wget --progress=dot:giga https://www.python.org/ftp/python/3.15.0/Python-3.15.0a6.tgz; \ + tar -xzf Python-3.15.0a6.tgz; \ + ./Python-3.15.0a6/configure --prefix=/usr/local --enable-shared --with-ensurepip=install; \ + make -j"$(nproc)"; \ + make install; \ + ln -s /usr/local/bin/python3.15 /usr/local/bin/python; + +# Install python dependencies into a target directory +RUN set -e; \ + pip3.15 install --no-cache-dir --upgrade 'pip==26.0'; \ + PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 pip3.15 install --no-cache-dir -r requirements.txt --target /packages; \ + mkdir -p /tmp && chmod 1777 /tmp && chown 1001:1001 /tmp; + + +# Stage 2: Runtime Stage using scratch Image +FROM scratch + +# Copy necessary system libraries and interpreter from build-env +COPY --from=build-env /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=build-env /lib/libc.musl-x86_64.so.1 /lib/libc.musl-x86_64.so.1 +COPY --from=build-env /usr/local/lib/libpython3.15.so.1.0 /usr/local/lib/libpython3.15.so.1.0 +COPY --from=build-env /usr/local/lib/python3.15 /usr/local/lib/python3.15 +COPY --from=build-env /usr/lib/libssl.so.3 /usr/lib/libssl.so.3 +COPY --from=build-env /usr/lib/libcrypto.so.3 /usr/lib/libcrypto.so.3 +COPY --from=build-env /usr/lib/libz.so.1 /usr/lib/libz.so.1 +COPY --from=build-env /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=build-env /usr/lib/libffi.so.8 /usr/lib/libffi.so.8 + +# Copy Python installation +COPY --from=build-env /usr/local/bin/python /usr/local/bin/python + +# Copy CA certificates needed for SSL verification +COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +# Copy installed packages +COPY --from=build-env /packages /packages + +# Copy application code +COPY --from=build-env /build/app /app + +# Copy tmp directory (required for Python/gunicorn) +COPY --from=build-env /tmp /tmp + +# Set required environment variables for Python +ENV PYTHONUNBUFFERED=1 \ + PYTHONIOENCODING=UTF-8 \ + PYTHONPATH=/packages \ + SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \ + TMPDIR=/tmp \ + HOME=/tmp + +# Set runtime directory +WORKDIR /app + +# Expose the application port +EXPOSE 80 + +# Set up container healthcheck +HEALTHCHECK --interval=30s --timeout=3s --start-period=30s --retries=3 \ + CMD ["/usr/local/bin/python", "healthcheck/healthcheck.py"] + +# Switch to non-privileged user +USER 1001:1001 + +# Define the command to run the application +CMD ["/usr/local/bin/python", "gunicorn_cfg.py"] From 9acc860120968af3baa7dbd78659562965eb296d Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 19:02:12 +0100 Subject: [PATCH 49/66] ci(gha-workflows): update triggers to activate on PR being ready for review --- .github/workflows/container.yml | 1 + .github/workflows/lint.yml | 1 + .github/workflows/semgrep.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 9a1bb157..b7b08571 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -6,6 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + types: [ready_for_review] env: REGISTRY: docker.io diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8913e7da..f0a01a2b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,6 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + types: [ready_for_review] permissions: read-all diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index b0093252..f7cb0fa7 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -6,6 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + types: [ready_for_review] jobs: semgrep: From 82d53a4adad4a756572ef17c939345bc3bd9ae52 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Sun, 12 Apr 2026 19:02:18 +0100 Subject: [PATCH 50/66] ci(codeql): update trigger logic --- .github/workflows/codeql.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5f17903d..3145e68a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,6 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + types: [ready_for_review] permissions: read-all From 25cc5c6732ef0975c88263872cd599c042d34e31 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Mon, 13 Apr 2026 20:25:24 +0100 Subject: [PATCH 51/66] fix(index): fix integrity issue found by semgrep --- app/templates/index.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/templates/index.html b/app/templates/index.html index 44998974..db44e900 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -8,6 +8,8 @@ type="image/png" href="https://{{ cdn }}/favicon-96x96.png" sizes="96x96" + integrity="sha384-dnkgadw7QrrNMHmdxZW+FZtG1u69j4jZN+Rc1aPzMe8rYAfhbtAyh6p8NuI+7CEH" + crossorigin="anonymous" /> Date: Wed, 15 Apr 2026 18:56:22 +0100 Subject: [PATCH 52/66] ci(workflows): re-factor all workflows to use dependant summary step instead of ready for review trigger This significantly reworks the github actions on this repository --- .github/workflows/codeql.yml | 54 ------ .github/workflows/container.yml | 125 ------------- .github/workflows/pr-security.yml | 244 +++++++++++++++++++++++++ .github/workflows/security-summary.yml | 89 --------- .github/workflows/semgrep.yml | 33 ---- 5 files changed, 244 insertions(+), 301 deletions(-) delete mode 100644 .github/workflows/codeql.yml delete mode 100644 .github/workflows/container.yml create mode 100644 .github/workflows/pr-security.yml delete mode 100644 .github/workflows/security-summary.yml delete mode 100644 .github/workflows/semgrep.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index 3145e68a..00000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: Code - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - types: [ready_for_review] - -permissions: - read-all - -jobs: - analyze: - name: Analyze (${{ matrix.language }}) - runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-24.04' }} - permissions: - security-events: write - packages: read - actions: read - contents: read - - strategy: - fail-fast: false - matrix: - include: - - language: python - build-mode: none - - language: javascript-typescript - build-mode: none - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - if: matrix.build-mode == 'manual' - shell: bash - run: | - echo 'If you are using a "manual" build mode for one or more of the' \ - 'languages you are analyzing, replace this with the commands to build' \ - 'your code, for example:' - echo ' make bootstrap' - echo ' make release' - exit 1 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml deleted file mode 100644 index b7b08571..00000000 --- a/.github/workflows/container.yml +++ /dev/null @@ -1,125 +0,0 @@ ---- -name: Container - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - types: [ready_for_review] - -env: - REGISTRY: docker.io - REPOSITORY: jackseceng - IMAGE_NAME: linkshort - -jobs: - build: - name: Build and Push - runs-on: ubuntu-24.04 - permissions: - contents: read - packages: write - id-token: write # Required for provenance - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - - name: Build and Push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 - with: - context: . - push: true - tags: | - ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event_name == 'pull_request' && github.event.number || 'latest' }} - provenance: true - sbom: true - - scan: - needs: build - name: ${{ matrix.scanner.name }} Scan - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - scanner: - - name: Grype - id: grype - - name: Trivy - id: trivy - - name: Scout - id: scout - - permissions: - contents: read - security-events: write - pull-requests: write # Required for Docker Scout to write comments - - steps: - - name: 'Get image tag' - id: get_tag - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "tag=${{ github.event.number }}" >> "$GITHUB_OUTPUT" - else - echo "tag=latest" >> "$GITHUB_OUTPUT" - fi - - - name: Login to Docker Hub - if: matrix.scanner.id == 'scout' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Run Grype Scan - if: matrix.scanner.id == 'grype' - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 - with: - image: ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} - fail-build: false - severity-cutoff: medium - only-fixed: true - output-format: sarif - output-file: 'results.sarif' - - - name: Run Trivy Scan - if: matrix.scanner.id == 'trivy' - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 - with: - image-ref: '${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }}' - scan-type: 'image' - ignore-unfixed: false - severity: 'CRITICAL,HIGH,MEDIUM' - format: 'sarif' - output: 'results.sarif' - - - name: Run Docker Scout Scan - if: matrix.scanner.id == 'scout' - uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4 - with: - command: cves - image: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} - only-severities: critical,high,medium - only-fixed: true - sarif-file: 'results.sarif' - write-comment: true - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload SARIF results to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - sarif_file: 'results.sarif' \ No newline at end of file diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml new file mode 100644 index 00000000..7df50a77 --- /dev/null +++ b/.github/workflows/pr-security.yml @@ -0,0 +1,244 @@ +--- +name: PR Security + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + types: [ready_for_review] + +env: + REGISTRY: docker.io + REPOSITORY: jackseceng + IMAGE_NAME: linkshort + +jobs: + semgrep: + name: Analyze (semgrep) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + container: + image: semgrep/semgrep + if: github.actor != 'dependabot[bot]' + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - run: semgrep ci --sarif --output semgrep.sarif + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + sarif_file: semgrep.sarif + if: always() + + codeql: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-24.04 + permissions: + security-events: write + packages: read + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + - language: javascript-typescript + build-mode: none + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Initialize CodeQL + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + category: "/language:${{matrix.language}}" + + container: + name: Build and Push + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Login to Docker Hub + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Build and Push + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + push: true + tags: | + ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event_name == 'pull_request' && github.event.number || 'latest' }} + provenance: true + sbom: true + + container-scan: + needs: container + name: ${{ matrix.scanner.name }} Scan + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + scanner: + - name: Grype + id: grype + - name: Trivy + id: trivy + - name: Scout + id: scout + permissions: + contents: read + security-events: write + pull-requests: write + steps: + - name: Get image tag + id: get_tag + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "tag=${{ github.event.number }}" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + - name: Login to Docker Hub + if: matrix.scanner.id == 'scout' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Run Grype Scan + if: matrix.scanner.id == 'grype' + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + with: + image: ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + fail-build: false + severity-cutoff: medium + only-fixed: true + output-format: sarif + output-file: results.sarif + - name: Run Trivy Scan + if: matrix.scanner.id == 'trivy' + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + scan-type: image + ignore-unfixed: false + severity: CRITICAL,HIGH,MEDIUM + format: sarif + output: results.sarif + - name: Run Docker Scout Scan + if: matrix.scanner.id == 'scout' + uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4 + with: + command: cves + image: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + only-severities: critical,high,medium + only-fixed: true + sarif-file: results.sarif + write-comment: true + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + sarif_file: results.sarif + + comment: + needs: [semgrep, codeql, container-scan] + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + permissions: + security-events: read + pull-requests: write + actions: read + checks: read + steps: + - name: Fetch findings and post comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + run: | + declare -A tool_counts + tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") + + all_alerts="[]" + page=1 + while true; do + batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") + count=$(echo "$batch" | jq 'length') + [[ "$count" -eq 0 ]] && break + all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') + for tool in "${tools[@]}"; do + n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') + tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) + done + [[ "$count" -lt 100 ]] && break + (( page++ )) + done + + total=0 + for i in "${!tools[@]}"; do + tool="${tools[$i]}" + c=${tool_counts[$tool]:-0} + total=$(( total + c )) + done + + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') + + severity_order=("critical" "high" "error" "medium" "warning" "low" "note") + highest="none" + while IFS= read -r sev; do + for s in "${severity_order[@]}"; do + if [[ "${sev,,}" == "$s" ]]; then + for ranked in "${severity_order[@]}"; do + [[ "$ranked" == "$highest" ]] && break + [[ "$ranked" == "$s" ]] && highest="$s" && break + done + break + fi + done + done < <(echo "$all_alerts" | jq -r '.[] | (.rule.security_severity_level // .rule.severity)') + + case "$highest" in + critical) alert_type="CAUTION" ;; + high|error) alert_type="WARNING" ;; + medium) alert_type="IMPORTANT" ;; + *) alert_type="NOTE" ;; + esac + + sep=$'\n' + header="## 🔒 Security Findings Summary" + summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." + findings_table="| File | Description | Severity | Tool |${sep}|------|-------------|----------|------|${sep}${details}" + dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" + footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." + body="${header}${sep}${sep}${summary}${sep}${sep}${dropdown}${sep}${sep}${footer}" + + existing=$(gh api "repos/$REPO/issues/$PR/comments" \ + --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) + [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" + + echo "$body" > /tmp/comment-body.md + gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md + + [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 diff --git a/.github/workflows/security-summary.yml b/.github/workflows/security-summary.yml deleted file mode 100644 index 32e3f4f0..00000000 --- a/.github/workflows/security-summary.yml +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: Security Summary - -on: - pull_request: - branches: ["main"] - types: [ready_for_review] - -jobs: - comment: - runs-on: ubuntu-24.04 - permissions: - security-events: read - pull-requests: write - actions: read - checks: read - - steps: - - name: Fetch findings and post comment - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR: ${{ github.event.pull_request.number }} - run: | - declare -A tool_counts - tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") - - all_alerts="[]" - page=1 - while true; do - batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") - count=$(echo "$batch" | jq 'length') - [[ "$count" -eq 0 ]] && break - all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') - for tool in "${tools[@]}"; do - n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') - tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) - done - [[ "$count" -lt 100 ]] && break - (( page++ )) - done - - total=0 - for i in "${!tools[@]}"; do - tool="${tools[$i]}" - c=${tool_counts[$tool]:-0} - total=$(( total + c )) - done - - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') - - severity_order=("critical" "high" "error" "medium" "warning" "low" "note") - highest="none" - while IFS= read -r sev; do - for s in "${severity_order[@]}"; do - if [[ "${sev,,}" == "$s" ]]; then - for ranked in "${severity_order[@]}"; do - [[ "$ranked" == "$highest" ]] && break - [[ "$ranked" == "$s" ]] && highest="$s" && break - done - break - fi - done - done < <(echo "$all_alerts" | jq -r '.[] | (.rule.security_severity_level // .rule.severity)') - - case "$highest" in - critical) alert_type="CAUTION" ;; - high|error) alert_type="WARNING" ;; - medium) alert_type="IMPORTANT" ;; - *) alert_type="NOTE" ;; - esac - - sep=$'\n' - header="## 🔒 Security Findings Summary" - summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." - findings_table="| File | Description | Severity | Tool |${sep}|------|-------------|----------|------|${sep}${details}" - dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" - dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" - footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." - body="${header}${sep}${sep}${summary}${sep}${sep}${dropdown}${sep}${sep}${footer}" - - existing=$(gh api "repos/$REPO/issues/$PR/comments" \ - --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) - [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" - - echo "$body" > /tmp/comment-body.md - gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md - - [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml deleted file mode 100644 index f7cb0fa7..00000000 --- a/.github/workflows/semgrep.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -name: Code - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - types: [ready_for_review] - -jobs: - semgrep: - name: Analyze (semgrep) - runs-on: ubuntu-latest - permissions: - contents: read # for actions/checkout to fetch code - security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status - env: - SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - container: - image: semgrep/semgrep - if: (github.actor != 'dependabot[bot]') - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - run: semgrep ci --sarif --output semgrep.sarif - - # Upload SARIF file generated in previous step - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - sarif_file: semgrep.sarif - if: always() From 66cc48243bfc36ffd1a318e79497354db94452d8 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:09:34 +0100 Subject: [PATCH 53/66] ci(security): Adjust triggers and made security results only focus on latest commit --- .github/workflows/pr-security.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml index 7df50a77..efaf06f8 100644 --- a/.github/workflows/pr-security.yml +++ b/.github/workflows/pr-security.yml @@ -1,12 +1,11 @@ --- -name: PR Security +name: Security on: push: branches: ["main"] pull_request: branches: ["main"] - types: [ready_for_review] env: REGISTRY: docker.io @@ -180,6 +179,7 @@ jobs: run: | declare -A tool_counts tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") + HEAD_SHA="${{ github.event.pull_request.head.sha }}" all_alerts="[]" page=1 @@ -187,6 +187,7 @@ jobs: batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") count=$(echo "$batch" | jq 'length') [[ "$count" -eq 0 ]] && break + batch=$(echo "$batch" | jq --arg sha "$HEAD_SHA" '[.[] | select(.most_recent_instance.commit_sha == $sha)]') all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') for tool in "${tools[@]}"; do n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') From a04015c0abb155592e492fd9911ef43b3c5a257f Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:28:06 +0100 Subject: [PATCH 54/66] fix(comment): add || true guards to prevent premature exit under bash -e Non-critical gh api calls were causing the script to exit immediately under bash -e before any alert fetching occurred, resulting in a false exit code 1 with only ~2s runtime. --- .github/workflows/pr-security.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml index efaf06f8..108cbad4 100644 --- a/.github/workflows/pr-security.yml +++ b/.github/workflows/pr-security.yml @@ -236,10 +236,10 @@ jobs: body="${header}${sep}${sep}${summary}${sep}${sep}${dropdown}${sep}${sep}${footer}" existing=$(gh api "repos/$REPO/issues/$PR/comments" \ - --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1) - [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" + --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1 || true) + [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" || true echo "$body" > /tmp/comment-body.md - gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md + gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md || true [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 From 8d16cb77e714d537fbb83a3c8261c5f397a0fe73 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:33:44 +0100 Subject: [PATCH 55/66] ci(container): migrate build to Blacksmith runner and actions Replace ubuntu-24.04 runner with blacksmith-2vcpu-ubuntu-24.04 and swap docker/{setup-buildx,build-push}-action for Blacksmith equivalents. Drop setup-qemu-action as Blacksmith provides native ARM hardware. --- .github/workflows/pr-security.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml index 108cbad4..94e0eb10 100644 --- a/.github/workflows/pr-security.yml +++ b/.github/workflows/pr-security.yml @@ -65,7 +65,7 @@ jobs: container: name: Build and Push - runs-on: ubuntu-24.04 + runs-on: blacksmith-2vcpu-ubuntu-24.04 permissions: contents: read packages: write @@ -78,12 +78,10 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: useblacksmith/setup-buildx-action@10b575630ff313e39b815381ae867989bf547e22 # v1 - name: Build and Push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2.1.0 with: context: . push: true From d8f6f6b355bda9da9ff9f9ffb79414f7408940b6 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:34:10 +0100 Subject: [PATCH 56/66] ci(lint): remove ready for review trigger --- .github/workflows/lint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f0a01a2b..8913e7da 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,6 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] - types: [ready_for_review] permissions: read-all From 1fc0fb44940b00d1dad15dfa65c7d98908beef27 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:38:29 +0100 Subject: [PATCH 57/66] ci(lint): revert removal of ready for review trigger --- .github/workflows/lint.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8913e7da..f0a01a2b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,6 +6,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + types: [ready_for_review] permissions: read-all From 66e5e124fc17503b1567f63444997f3d2785dae2 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:38:38 +0100 Subject: [PATCH 58/66] ci(lint): re-apply removal of ready for review trigger --- .github/workflows/lint.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f0a01a2b..8913e7da 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,7 +6,6 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] - types: [ready_for_review] permissions: read-all From 8cf220a95f4891ca5f26d580c6a43cc6719e2fe4 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:38:38 +0100 Subject: [PATCH 59/66] ci(container): revert migration to Blacksmith runner and actions --- .github/workflows/pr-security.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml index 94e0eb10..108cbad4 100644 --- a/.github/workflows/pr-security.yml +++ b/.github/workflows/pr-security.yml @@ -65,7 +65,7 @@ jobs: container: name: Build and Push - runs-on: blacksmith-2vcpu-ubuntu-24.04 + runs-on: ubuntu-24.04 permissions: contents: read packages: write @@ -78,10 +78,12 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - name: Set up Docker Buildx - uses: useblacksmith/setup-buildx-action@10b575630ff313e39b815381ae867989bf547e22 # v1 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - name: Build and Push - uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2.1.0 + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 with: context: . push: true From f94105c955933e24e99bc3fdcd659a17250eea72 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:39:23 +0100 Subject: [PATCH 60/66] ci(security): rename gha file --- .github/workflows/pr-security.yml | 245 ------------------------------ 1 file changed, 245 deletions(-) delete mode 100644 .github/workflows/pr-security.yml diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml deleted file mode 100644 index 108cbad4..00000000 --- a/.github/workflows/pr-security.yml +++ /dev/null @@ -1,245 +0,0 @@ ---- -name: Security - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -env: - REGISTRY: docker.io - REPOSITORY: jackseceng - IMAGE_NAME: linkshort - -jobs: - semgrep: - name: Analyze (semgrep) - runs-on: ubuntu-latest - permissions: - contents: read - security-events: write - actions: read - env: - SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} - container: - image: semgrep/semgrep - if: github.actor != 'dependabot[bot]' - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - run: semgrep ci --sarif --output semgrep.sarif - - name: Upload SARIF file - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - sarif_file: semgrep.sarif - if: always() - - codeql: - name: Analyze (${{ matrix.language }}) - runs-on: ubuntu-24.04 - permissions: - security-events: write - packages: read - actions: read - contents: read - strategy: - fail-fast: false - matrix: - include: - - language: python - build-mode: none - - language: javascript-typescript - build-mode: none - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Initialize CodeQL - uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - languages: ${{ matrix.language }} - build-mode: ${{ matrix.build-mode }} - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - category: "/language:${{matrix.language}}" - - container: - name: Build and Push - runs-on: ubuntu-24.04 - permissions: - contents: read - packages: write - id-token: write - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Login to Docker Hub - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Set up QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 - - name: Build and Push - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 - with: - context: . - push: true - tags: | - ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event_name == 'pull_request' && github.event.number || 'latest' }} - provenance: true - sbom: true - - container-scan: - needs: container - name: ${{ matrix.scanner.name }} Scan - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - scanner: - - name: Grype - id: grype - - name: Trivy - id: trivy - - name: Scout - id: scout - permissions: - contents: read - security-events: write - pull-requests: write - steps: - - name: Get image tag - id: get_tag - run: | - if [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "tag=${{ github.event.number }}" >> "$GITHUB_OUTPUT" - else - echo "tag=latest" >> "$GITHUB_OUTPUT" - fi - - name: Login to Docker Hub - if: matrix.scanner.id == 'scout' - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Run Grype Scan - if: matrix.scanner.id == 'grype' - uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 - with: - image: ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} - fail-build: false - severity-cutoff: medium - only-fixed: true - output-format: sarif - output-file: results.sarif - - name: Run Trivy Scan - if: matrix.scanner.id == 'trivy' - uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 - with: - image-ref: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} - scan-type: image - ignore-unfixed: false - severity: CRITICAL,HIGH,MEDIUM - format: sarif - output: results.sarif - - name: Run Docker Scout Scan - if: matrix.scanner.id == 'scout' - uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4 - with: - command: cves - image: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} - only-severities: critical,high,medium - only-fixed: true - sarif-file: results.sarif - write-comment: true - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Upload SARIF results - uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 - with: - sarif_file: results.sarif - - comment: - needs: [semgrep, codeql, container-scan] - if: github.event_name == 'pull_request' - runs-on: ubuntu-24.04 - permissions: - security-events: read - pull-requests: write - actions: read - checks: read - steps: - - name: Fetch findings and post comment - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - PR: ${{ github.event.pull_request.number }} - run: | - declare -A tool_counts - tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") - HEAD_SHA="${{ github.event.pull_request.head.sha }}" - - all_alerts="[]" - page=1 - while true; do - batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") - count=$(echo "$batch" | jq 'length') - [[ "$count" -eq 0 ]] && break - batch=$(echo "$batch" | jq --arg sha "$HEAD_SHA" '[.[] | select(.most_recent_instance.commit_sha == $sha)]') - all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') - for tool in "${tools[@]}"; do - n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') - tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) - done - [[ "$count" -lt 100 ]] && break - (( page++ )) - done - - total=0 - for i in "${!tools[@]}"; do - tool="${tools[$i]}" - c=${tool_counts[$tool]:-0} - total=$(( total + c )) - done - - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') - - severity_order=("critical" "high" "error" "medium" "warning" "low" "note") - highest="none" - while IFS= read -r sev; do - for s in "${severity_order[@]}"; do - if [[ "${sev,,}" == "$s" ]]; then - for ranked in "${severity_order[@]}"; do - [[ "$ranked" == "$highest" ]] && break - [[ "$ranked" == "$s" ]] && highest="$s" && break - done - break - fi - done - done < <(echo "$all_alerts" | jq -r '.[] | (.rule.security_severity_level // .rule.severity)') - - case "$highest" in - critical) alert_type="CAUTION" ;; - high|error) alert_type="WARNING" ;; - medium) alert_type="IMPORTANT" ;; - *) alert_type="NOTE" ;; - esac - - sep=$'\n' - header="## 🔒 Security Findings Summary" - summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." - findings_table="| File | Description | Severity | Tool |${sep}|------|-------------|----------|------|${sep}${details}" - dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" - footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." - body="${header}${sep}${sep}${summary}${sep}${sep}${dropdown}${sep}${sep}${footer}" - - existing=$(gh api "repos/$REPO/issues/$PR/comments" \ - --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1 || true) - [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" || true - - echo "$body" > /tmp/comment-body.md - gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md || true - - [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 From 5eb56d38c6c3854bdfcfc6d264f933d71da2a434 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:39:48 +0100 Subject: [PATCH 61/66] ci(security): rename security file --- .github/workflows/security.yml | 245 +++++++++++++++++++++++++++++++++ 1 file changed, 245 insertions(+) create mode 100644 .github/workflows/security.yml diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 00000000..108cbad4 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,245 @@ +--- +name: Security + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +env: + REGISTRY: docker.io + REPOSITORY: jackseceng + IMAGE_NAME: linkshort + +jobs: + semgrep: + name: Analyze (semgrep) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + container: + image: semgrep/semgrep + if: github.actor != 'dependabot[bot]' + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - run: semgrep ci --sarif --output semgrep.sarif + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + sarif_file: semgrep.sarif + if: always() + + codeql: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-24.04 + permissions: + security-events: write + packages: read + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none + - language: javascript-typescript + build-mode: none + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Initialize CodeQL + uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + category: "/language:${{matrix.language}}" + + container: + name: Build and Push + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Login to Docker Hub + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up QEMU + uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + - name: Build and Push + uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0 + with: + context: . + push: true + tags: | + ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event_name == 'pull_request' && github.event.number || 'latest' }} + provenance: true + sbom: true + + container-scan: + needs: container + name: ${{ matrix.scanner.name }} Scan + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + scanner: + - name: Grype + id: grype + - name: Trivy + id: trivy + - name: Scout + id: scout + permissions: + contents: read + security-events: write + pull-requests: write + steps: + - name: Get image tag + id: get_tag + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + echo "tag=${{ github.event.number }}" >> "$GITHUB_OUTPUT" + else + echo "tag=latest" >> "$GITHUB_OUTPUT" + fi + - name: Login to Docker Hub + if: matrix.scanner.id == 'scout' + uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Run Grype Scan + if: matrix.scanner.id == 'grype' + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + with: + image: ${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + fail-build: false + severity-cutoff: medium + only-fixed: true + output-format: sarif + output-file: results.sarif + - name: Run Trivy Scan + if: matrix.scanner.id == 'trivy' + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 + with: + image-ref: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + scan-type: image + ignore-unfixed: false + severity: CRITICAL,HIGH,MEDIUM + format: sarif + output: results.sarif + - name: Run Docker Scout Scan + if: matrix.scanner.id == 'scout' + uses: docker/scout-action@bacf462e8d090c09660de30a6ccc718035f961e3 # v1.20.4 + with: + command: cves + image: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ steps.get_tag.outputs.tag }} + only-severities: critical,high,medium + only-fixed: true + sarif-file: results.sarif + write-comment: true + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1 + with: + sarif_file: results.sarif + + comment: + needs: [semgrep, codeql, container-scan] + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + permissions: + security-events: read + pull-requests: write + actions: read + checks: read + steps: + - name: Fetch findings and post comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ github.event.pull_request.number }} + run: | + declare -A tool_counts + tools=("Grype" "Trivy" "Docker Scout" "Semgrep PRO" "CodeQL") + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + + all_alerts="[]" + page=1 + while true; do + batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") + count=$(echo "$batch" | jq 'length') + [[ "$count" -eq 0 ]] && break + batch=$(echo "$batch" | jq --arg sha "$HEAD_SHA" '[.[] | select(.most_recent_instance.commit_sha == $sha)]') + all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') + for tool in "${tools[@]}"; do + n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') + tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) + done + [[ "$count" -lt 100 ]] && break + (( page++ )) + done + + total=0 + for i in "${!tools[@]}"; do + tool="${tools[$i]}" + c=${tool_counts[$tool]:-0} + total=$(( total + c )) + done + + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') + + severity_order=("critical" "high" "error" "medium" "warning" "low" "note") + highest="none" + while IFS= read -r sev; do + for s in "${severity_order[@]}"; do + if [[ "${sev,,}" == "$s" ]]; then + for ranked in "${severity_order[@]}"; do + [[ "$ranked" == "$highest" ]] && break + [[ "$ranked" == "$s" ]] && highest="$s" && break + done + break + fi + done + done < <(echo "$all_alerts" | jq -r '.[] | (.rule.security_severity_level // .rule.severity)') + + case "$highest" in + critical) alert_type="CAUTION" ;; + high|error) alert_type="WARNING" ;; + medium) alert_type="IMPORTANT" ;; + *) alert_type="NOTE" ;; + esac + + sep=$'\n' + header="## 🔒 Security Findings Summary" + summary="> [!${alert_type}]${sep}> **$total** findings, some of which are **$highest** severity." + findings_table="| File | Description | Severity | Tool |${sep}|------|-------------|----------|------|${sep}${details}" + dropdown="
View all findings${sep}${sep}${findings_table}${sep}${sep}
" + footer="> Any findings must be fixed or justified, see [repository PR guidance](https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs) for details." + body="${header}${sep}${sep}${summary}${sep}${sep}${dropdown}${sep}${sep}${footer}" + + existing=$(gh api "repos/$REPO/issues/$PR/comments" \ + --jq '.[] | select(.body | startswith("## 🔒 Security Findings Summary")) | .id' | head -1 || true) + [[ -n "$existing" ]] && gh api -X DELETE "repos/$REPO/issues/comments/$existing" || true + + echo "$body" > /tmp/comment-body.md + gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md || true + + [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 From 20d01f9fd21c1858234ecc9db48f2cee52a5cefe Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 19:57:06 +0100 Subject: [PATCH 62/66] fix(comment): add || true guards to grep and arithmetic expansion to prevent premature exit under bash -e --- .github/workflows/security.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 108cbad4..b3505cd2 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -194,7 +194,7 @@ jobs: tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) done [[ "$count" -lt 100 ]] && break - (( page++ )) + (( page++ )) || true done total=0 @@ -204,7 +204,7 @@ jobs: total=$(( total + c )) done - details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$') + details=$(echo "$all_alerts" | jq -r '.[] | "| \(.most_recent_instance.location.path | ltrimstr("linkshort/") | gsub("//"; "/")):\(.most_recent_instance.location.start_line) | \(.rule.description) | \(.rule.security_severity_level // .rule.severity) | \(.tool.name) |"' | grep -v '^$' || true) severity_order=("critical" "high" "error" "medium" "warning" "low" "note") highest="none" From d4b6e4af5fd3eceaa7414df8edaac258e10e02aa Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 20:03:58 +0100 Subject: [PATCH 63/66] ci(security): skip scans when irrelevant files change, always run comment step --- .github/workflows/security.yml | 38 +++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index b3505cd2..3820fb14 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -13,7 +13,34 @@ env: IMAGE_NAME: linkshort jobs: + changes: + name: Detect Changes + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ steps.filter.outputs.code }} + container: ${{ steps.filter.outputs.container }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: filter + with: + filters: | + code: + - 'app/**' + - 'requirements.txt' + - '.github/workflows/security.yml' + container: + - 'Dockerfile' + - 'docker-compose.yaml' + - '.dockerignore' + - 'requirements.txt' + - '.github/workflows/security.yml' + semgrep: + needs: changes name: Analyze (semgrep) runs-on: ubuntu-latest permissions: @@ -24,7 +51,7 @@ jobs: SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} container: image: semgrep/semgrep - if: github.actor != 'dependabot[bot]' + if: github.actor != 'dependabot[bot]' && needs.changes.outputs.code == 'true' steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - run: semgrep ci --sarif --output semgrep.sarif @@ -35,8 +62,10 @@ jobs: if: always() codeql: + needs: changes name: Analyze (${{ matrix.language }}) runs-on: ubuntu-24.04 + if: needs.changes.outputs.code == 'true' permissions: security-events: write packages: read @@ -64,8 +93,10 @@ jobs: category: "/language:${{matrix.language}}" container: + needs: changes name: Build and Push runs-on: ubuntu-24.04 + if: needs.changes.outputs.container == 'true' permissions: contents: read packages: write @@ -93,7 +124,8 @@ jobs: sbom: true container-scan: - needs: container + needs: [changes, container] + if: needs.changes.outputs.container == 'true' name: ${{ matrix.scanner.name }} Scan runs-on: ubuntu-latest strategy: @@ -163,7 +195,7 @@ jobs: comment: needs: [semgrep, codeql, container-scan] - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' && always() runs-on: ubuntu-24.04 permissions: security-events: read From e580f665f70dd57d500d788bd80d76b5c77a9567 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 20:23:59 +0100 Subject: [PATCH 64/66] fix(comment): only fail job when scans ran, remove per-commit SHA filter from alert query --- .github/workflows/security.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 3820fb14..4a789a8f 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -22,6 +22,7 @@ jobs: outputs: code: ${{ steps.filter.outputs.code }} container: ${{ steps.filter.outputs.container }} + scans_ran: ${{ steps.filter.outputs.code == 'true' || steps.filter.outputs.container == 'true' }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 @@ -194,7 +195,7 @@ jobs: sarif_file: results.sarif comment: - needs: [semgrep, codeql, container-scan] + needs: [changes, semgrep, codeql, container-scan] if: github.event_name == 'pull_request' && always() runs-on: ubuntu-24.04 permissions: @@ -219,7 +220,6 @@ jobs: batch=$(gh api "repos/$REPO/code-scanning/alerts?pr=$PR&state=open&per_page=100&page=$page" 2>/dev/null || echo "[]") count=$(echo "$batch" | jq 'length') [[ "$count" -eq 0 ]] && break - batch=$(echo "$batch" | jq --arg sha "$HEAD_SHA" '[.[] | select(.most_recent_instance.commit_sha == $sha)]') all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') for tool in "${tools[@]}"; do n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') @@ -274,4 +274,5 @@ jobs: echo "$body" > /tmp/comment-body.md gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md || true - [[ "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 + scans_ran="${{ needs.changes.outputs.scans_ran }}" + [[ "$scans_ran" == "true" && "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 From 7d09ff3bcae985d058a681f06b24215e904f1674 Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 20:36:33 +0100 Subject: [PATCH 65/66] fix(comment): count only HEAD SHA alerts for fail condition, show all open alerts in comment --- .github/workflows/security.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 4a789a8f..7bddc733 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -221,8 +221,9 @@ jobs: count=$(echo "$batch" | jq 'length') [[ "$count" -eq 0 ]] && break all_alerts=$(echo "$all_alerts $batch" | jq -s 'add') + head_batch=$(echo "$batch" | jq --arg sha "$HEAD_SHA" '[.[] | select(.most_recent_instance.commit_sha == $sha)]') for tool in "${tools[@]}"; do - n=$(echo "$batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') + n=$(echo "$head_batch" | jq --arg t "$tool" '[.[] | select(.tool.name | ascii_downcase | contains($t | ascii_downcase))] | length') tool_counts[$tool]=$(( ${tool_counts[$tool]:-0} + n )) done [[ "$count" -lt 100 ]] && break From 2249faff19c235a83922a4f09665db5ad5cbb53e Mon Sep 17 00:00:00 2001 From: Jack Edwards Date: Wed, 15 Apr 2026 20:51:39 +0100 Subject: [PATCH 66/66] fix(comment): use if block instead of bare [[ ]] && chain to avoid bash -e exit on false condition --- .github/workflows/security.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 7bddc733..3a9a9e40 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -276,4 +276,7 @@ jobs: gh pr comment "$PR" --repo "$REPO" --body-file /tmp/comment-body.md || true scans_ran="${{ needs.changes.outputs.scans_ran }}" - [[ "$scans_ran" == "true" && "$total" -gt 0 ]] && echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" && exit 1 + if [[ "$scans_ran" == "true" && "$total" -gt 0 ]]; then + echo "::error::$total open security finding(s) found. Any findings must be fixed or justified, see repository PR guidance for details https://github.com/jackseceng/LinkShort?tab=contributing-ov-file#making-prs" + exit 1 + fi