Files
accounted/.github/workflows/swedish-compliance-review.yml
T
Jakob Wennberg 7dde8cac82 fix(security): resolve the CodeQL backlog, three fixes and three documented false positives (#1225)
Triage of all 9 CodeQL alerts surfaced on main by #1223. None were introduced by that PR.

Fixed: the compliance-review artifact now unpacks to runner.temp instead of over the trusted checkout (actions/artifact-poisoning, critical); MCP LIKE patterns escape backslash first, which was a real correctness bug returning wrong rows for any search containing a backslash (js/incomplete-sanitization, 2 sites); and the mcp-oauth consent form action is HTML-escaped (js/reflected-xss, not exploitable because WHATWG URL already percent-encodes " < >, but & is not in that encode set).

Dismissed as false positives with reasoning recorded at each site and in DECISIONS.md: sie-export escapeQuotes, where doubling backslashes would violate SIE 4B, corrupt files in conformant readers and skew #KSUMMA under BFL 7-year retention; hashApiKey, where SHA-256 is correct for a 256-bit CSPRNG token and changing it would invalidate every live gnubok_sk_ key; and the DuplicateBookingDialog href, which is a DB UUID behind a literal path prefix.

Regression tests cover both behavioural fixes, including the escape ordering.
2026-07-27 14:02:25 +02:00

127 lines
6.5 KiB
YAML

name: Swedish Accounting Compliance Review
# Stage 2 of the fork-safe compliance review (stage 1 is swedish-compliance-diff.yml).
#
# SECURITY: this is the privileged half: it has the AWS Bedrock secrets and a
# write token. It is triggered by `workflow_run` (NOT pull_request_target) and
# checks out ONLY the base repo, so it never executes fork PR code. The untrusted
# input (the PR diff) arrives as a downloaded artifact and is fed to the model
# as DATA: never run. This is the pattern GitHub recommends instead of
# `pull_request_target` + checking out the PR head.
on:
workflow_run:
# Must match the `name:` field in swedish-compliance-diff.yml exactly.
# A rename there silently stops this trigger from firing on all subsequent PRs.
workflows: ["Compliance diff"]
types: [completed]
permissions:
contents: read
actions: read # required to download the artifact from the triggering run
pull-requests: write # required to post the review comment
jobs:
review:
runs-on: ubuntu-latest
timeout-minutes: 10
# Only act on PR-triggered diffs that actually produced an artifact.
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
# Base repo only: the TRUSTED copy of the script and .claude/skills/.
# persist-credentials: false, no later step needs git push creds, so don't
# leave the token in .git/config for the steps that handle untrusted input.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 20
- name: Download diff artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: compliance-input
# Unpacked OUTSIDE the checkout, not into the workspace root.
#
# This artifact is built from a fork's PR head, so its contents are
# attacker-influenced. Extracted over the workspace, an entry named
# `scripts/swedish-compliance-review.mjs` would overwrite the trusted
# script this job is about to run, with the AWS secrets and a write
# token already in scope. Stage 1 only ever writes three fixed
# filenames, and its workflow definition comes from the base repo even
# for fork PRs, so that is not reachable today: this keeps it
# unreachable if stage 1 ever grows a filename derived from PR
# content. Flagged by CodeQL as actions/artifact-poisoning.
path: ${{ runner.temp }}/compliance-input
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve PR number
id: pr
run: |
set -euo pipefail
NUM=$(cat "$RUNNER_TEMP/compliance-input/pr-number.txt")
# Guard: pr-number.txt must be a plain integer (artifact is untrusted input).
if ! [[ "$NUM" =~ ^[0-9]+$ ]]; then
echo "Refusing to continue: pr-number.txt is not a number" >&2
exit 1
fi
echo "number=$NUM" >> "$GITHUB_OUTPUT"
- name: Install Anthropic Bedrock SDK
# Installed ONE DIRECTORY ABOVE the checkout on purpose. Running this
# inside the repo makes npm re-resolve the whole dependency tree from
# package.json (--no-package-lock throws away the pinned resolutions),
# and that re-resolution dies on an unrelated floating peer conflict
# (@hookform/resolvers -> valibot), which silently killed this review on
# every PR. Out of tree npm resolves this one package and nothing else,
# so an unrelated peer conflict can never take the compliance gate down
# again. Node still finds it: ESM bare specifiers walk up the parent
# directories' node_modules. NODE_PATH is not an alternative here, the
# ESM loader ignores it (CommonJS only).
# Version tracks the exact package.json pin: 0.32.0 broke Bedrock
# streaming in prod (DECISIONS.md 2026-07-08) and check:guards enforces
# 0.29.1 repo-wide, so this job must not be the one place running an
# unvetted build. Pinned exact + --ignore-scripts because this is the
# privileged job (write token in env): no floating @latest, and no
# dependency lifecycle scripts may execute here.
run: npm install --prefix "$GITHUB_WORKSPACE/.." --no-save --no-package-lock --ignore-scripts @anthropic-ai/bedrock-sdk@0.29.1
- name: Run compliance review
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION || 'eu-north-1' }}
REVIEW_MODEL: eu.anthropic.claude-sonnet-5
# Two-stage mode: read the diff from the artifact instead of git-diffing.
DIFF_FILE: ${{ runner.temp }}/compliance-input/diff.patch
FILES_FILE: ${{ runner.temp }}/compliance-input/files.txt
run: node scripts/swedish-compliance-review.mjs
- name: Assert review produced output
# This job once produced no compliance signal for 10 consecutive PR
# runs (the in-tree npm install died on an unrelated peer conflict, see
# the install step above) and nobody noticed: workflow_run-triggered
# jobs do not appear on the PR checks list, so a red or silently empty
# run is invisible from the PR. Fail loudly whenever the review script
# finishes without writing a non-empty review.md, so "no output" can
# never again pass as a green run.
run: |
set -euo pipefail
if [ ! -s review.md ]; then
echo "::error::Compliance review produced no output (review.md missing or empty)"
exit 1
fi
- name: Find previous compliance comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ steps.pr.outputs.number }}
comment-author: 'github-actions[bot]'
body-includes: '<!-- swedish-compliance-review-bot -->'
- name: Post or update PR comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5
with:
issue-number: ${{ steps.pr.outputs.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
body-path: review.md
edit-mode: replace