From eb71c247374ecccdcbf27fb4202efdfc38e83898 Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Mon, 17 Aug 2026 06:46:43 +0200 Subject: [PATCH 1/2] fix(ci): least-privilege workflow permissions + drop a dead string replace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clears all 10 open CodeQL alerts on `development`: 9 workflow-hardening findings and 1 dead no-op in production code. Neither category is a vulnerability that was exploitable, and the check-run title ("7 new alerts including 3 high severity security vulnerabilities") overstates both. All three "high severity" alerts are `js/insecure-randomness` in Playwright fixtures; they are handled by dismissal, not by this commit. 1. Nine `actions/missing-workflow-permissions`, all MEDIUM, all in `.github/workflows/`. An absent `permissions:` block means the job runs with the repository default rather than a stated grant. Eight of the nine only CALL a reusable workflow in ConductionNL/.github, so the block restates what the callee's own job already declares and the effective token is unchanged: release-beta / release-development / release-stable contents: write sync-to-beta contents: write + pull-requests: write issue-triage issues: write + contents: read openspec-sync issues: write + contents: read documentation contents: write + packages: write (UNION of the callee's build / deploy / image jobs) branch-protection {} — the callee is one bash string comparison with no checkout, no network and no API call code-quality is the exception and the only risky one. Most jobs in the shared quality pipeline declare no permissions of their own, so they inherit the caller ceiling exactly. The block used is copied verbatim from openconnector, where it is live on `development` with ~30 quality jobs green — a measured ceiling, not a guess. A caller block is a CEILING, not a grant: GitHub validates the callee's declared job permissions against it, including for jobs an `if:` will skip, so tightening one to `read` makes the call fail to START with zero jobs. 2. One `js/identity-replacement` (MEDIUM) at src/views/Dashboard.vue:496 — `.replace(',', ',')`, replacing the comma with itself. It was born in that identical form in 5c33f0b ("Working on the detail pages"), so it never worked and no intent is recorded to recover. Deleting it is output-preserving: `formatDate` still returns `17/08/2026, 08:33`, verified against the actual string. Guessing at `.replace(',', '')` would have invented a UI change nothing asked for. Checked and ruled out while here: `toLocaleDateString` with explicit `hour`/`minute` options DOES emit the time (ECMA-402 supplies date-part defaults only when none are given), so this was not the "the time is silently missing" bug it resembles. Measured, not assumed. Verified: all 11 workflows parse, and a job-level sweep reports 0 jobs without a block, against 9 before the change — the same 9 CodeQL names. --- .github/workflows/branch-protection.yml | 6 +++++ .github/workflows/code-quality.yml | 30 +++++++++++++++++++++++ .github/workflows/documentation.yml | 14 +++++++++++ .github/workflows/issue-triage.yml | 9 +++++++ .github/workflows/openspec-sync.yml | 8 ++++++ .github/workflows/release-beta.yml | 13 ++++++++++ .github/workflows/release-development.yml | 11 +++++++++ .github/workflows/release-stable.yml | 10 ++++++++ .github/workflows/sync-to-beta.yml | 11 +++++++++ src/views/Dashboard.vue | 30 ++++++++++++++++------- 10 files changed, 133 insertions(+), 9 deletions(-) diff --git a/.github/workflows/branch-protection.yml b/.github/workflows/branch-protection.yml index 666fc767..d3e40cad 100644 --- a/.github/workflows/branch-protection.yml +++ b/.github/workflows/branch-protection.yml @@ -8,4 +8,10 @@ on: jobs: protect: + # The callee's `check-branch` job is a single bash comparison of + # `github.base_ref` against `github.head_ref` — no checkout, no network, no + # API call. It needs nothing, so it gets nothing. `{}` is an explicit empty + # grant, which is what satisfies actions/missing-workflow-permissions; an + # ABSENT block means "whatever the repo default is", which is the finding. + permissions: {} uses: ConductionNL/.github/.github/workflows/branch-protection.yml@main diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 878d0952..20937988 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -83,6 +83,36 @@ on: # be redder than a PR — that is the honest answer, not a regression. workflow_dispatch: +# Permission CEILING for the called quality pipeline, NOT a grant. Copied +# verbatim from openconnector, where this exact block is live on `development` +# with all ~30 quality jobs green — so it is a measured ceiling, not a guess. +# +# ⚠️ Most jobs in the callee declare NO permissions of their own (only +# journeydoc-capture and features-extract do), which means they inherit THIS +# ceiling exactly. Every entry below is load-bearing: +# +# contents: write journeydoc-capture and update-baseline `git push` +# the regenerated screenshots / coverage baseline +# actions: write journeydoc-capture re-dispatches downstream work +# (a GITHUB_TOKEN push does not fire workflow_run) +# issues / pull-requests: write +# the Quality Report comment — `github.rest.issues +# .createComment`, which 403s under contents+packages +# read alone (quality.yml notes this at its own step) +# packages: read pulling org images for the Nextcloud test matrix +# +# ⚠️ Do NOT "tighten" any of these to `read`. GitHub statically validates the +# called workflow's DECLARED job permissions against this grant — including for +# jobs an `if:` will skip — so capping one makes the whole call fail to START: +# zero jobs, no annotations, and a Code Quality run that is absent rather than +# red. That is quieter than the outage it would cause. +permissions: + contents: write + actions: write + issues: write + pull-requests: write + packages: read + jobs: quality: uses: ConductionNL/.github/.github/workflows/quality.yml@main diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 5b3aa456..41a559a4 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -8,6 +8,20 @@ on: jobs: deploy: + # Permission CEILING for the called documentation workflow, not a grant. + # The callee has three jobs and this block is their UNION, which is what the + # token already resolves to today: + # build contents: read + # deploy contents: write (peaceiris/actions-gh-pages pushes to gh-pages) + # image contents: read, packages: write (buildx push to GHCR) + # + # ⚠️ `packages: write` stays even though `build-image` is not set here, so + # the `image` job never runs. GitHub validates the callee's DECLARED job + # permissions against this ceiling before dispatch — including jobs an `if:` + # will skip — so dropping it makes the whole call fail to start. + permissions: + contents: write + packages: write uses: ConductionNL/.github/.github/workflows/documentation.yml@main with: cname: softwarecatalog.conduction.nl diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index e2603654..ac9a743c 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -12,6 +12,15 @@ on: jobs: triage: + # Permission CEILING for the called workflow, not a grant. All three jobs in + # the callee (triage-single, triage-decision, backlog-triage) already declare + # exactly `issues: write` + `contents: read` for themselves, so this restates + # the grant the token has today and changes nothing that runs. `issues: write` + # is the label-and-assign step; the ProjectsV2 board step is disabled because + # GITHUB_TOKEN cannot reach it at any permission level (ConductionNL/.github#432). + permissions: + issues: write + contents: read uses: ConductionNL/.github/.github/workflows/issue-triage.yml@main with: app-name: softwarecatalog diff --git a/.github/workflows/openspec-sync.yml b/.github/workflows/openspec-sync.yml index bf06b8e8..541e3221 100644 --- a/.github/workflows/openspec-sync.yml +++ b/.github/workflows/openspec-sync.yml @@ -8,6 +8,14 @@ on: jobs: sync: + # Permission CEILING for the called workflow, not a grant. The callee's + # `sync` job already declares exactly `issues: write` + `contents: read`, so + # this restates today's effective token. It creates/updates the OpenSpec + # tracking issues; the board placement is disabled (GITHUB_TOKEN cannot do + # ProjectsV2 — ConductionNL/.github#432). + permissions: + issues: write + contents: read uses: ConductionNL/.github/.github/workflows/openspec-sync.yml@main with: app-name: softwarecatalog diff --git a/.github/workflows/release-beta.yml b/.github/workflows/release-beta.yml index 23a2ab0c..711c3124 100644 --- a/.github/workflows/release-beta.yml +++ b/.github/workflows/release-beta.yml @@ -7,6 +7,19 @@ on: jobs: release: + # Permission CEILING for the called release workflow, not a grant. The + # callee's `release` job already declares `contents: write` for itself, so + # this restates today's effective token and changes nothing that runs. + # `contents: write` cuts the tag and uploads the release asset + # (ncipollo/release-action, svenstaro/upload-release-action); the App Store + # publish authenticates with NEXTCLOUD_APPSTORE_TOKEN, not GITHUB_TOKEN, so + # it needs nothing here. + # + # ⚠️ Not `contents: read`. A caller block CAPS the callee — GitHub validates + # the callee's declared job permissions against it — so a lower ceiling makes + # the call fail to START (zero jobs) rather than run with less. + permissions: + contents: write uses: ConductionNL/.github/.github/workflows/release-beta.yml@main with: app-name: softwarecatalog diff --git a/.github/workflows/release-development.yml b/.github/workflows/release-development.yml index 9bcf84b1..0f295026 100644 --- a/.github/workflows/release-development.yml +++ b/.github/workflows/release-development.yml @@ -29,6 +29,17 @@ concurrency: jobs: release: + # Permission CEILING for the called release workflow, not a grant. The + # callee's `release` job already declares `contents: write` for itself, so + # this restates today's effective token and changes nothing that runs. + # `contents: write` cuts the tag and publishes the prerelease + .tar.gz that + # the App Versions app installs from. This channel never touches the App + # Store, so nothing else is needed. + # + # ⚠️ Not `contents: read`. A caller block CAPS the callee, so a lower ceiling + # makes the call fail to START rather than run with less. + permissions: + contents: write uses: ConductionNL/.github/.github/workflows/release-beta.yml@main with: app-name: softwarecatalog diff --git a/.github/workflows/release-stable.yml b/.github/workflows/release-stable.yml index 34a2b6b6..4cdbad92 100644 --- a/.github/workflows/release-stable.yml +++ b/.github/workflows/release-stable.yml @@ -7,6 +7,16 @@ on: jobs: release: + # Permission CEILING for the called release workflow, not a grant. The + # callee's `release` job already declares `contents: write` for itself, so + # this restates today's effective token and changes nothing that runs. + # `contents: write` cuts the tag and uploads the release asset; the App Store + # publish authenticates with NEXTCLOUD_APPSTORE_TOKEN, not GITHUB_TOKEN. + # + # ⚠️ Not `contents: read`. A caller block CAPS the callee, so a lower ceiling + # makes the call fail to START rather than run with less. + permissions: + contents: write uses: ConductionNL/.github/.github/workflows/release-stable.yml@main with: app-name: softwarecatalog diff --git a/.github/workflows/sync-to-beta.yml b/.github/workflows/sync-to-beta.yml index 7d2dacae..c21f6f0d 100644 --- a/.github/workflows/sync-to-beta.yml +++ b/.github/workflows/sync-to-beta.yml @@ -7,4 +7,15 @@ on: jobs: sync: + # Permission CEILING for the called workflow, not a grant. The callee's + # `create-pr` job already declares exactly these two, so this restates + # today's effective token. It runs `gh pr list` / `gh pr create` to keep the + # standing development -> beta release PR open, which needs + # `pull-requests: write`; `contents: write` is the checkout it pushes from. + # + # ⚠️ `contents: read` alone would leave the PR step 403-ing at runtime — a + # red release-sync that looks like a merge conflict. + permissions: + contents: write + pull-requests: write uses: ConductionNL/.github/.github/workflows/sync-to-beta.yml@main diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue index 8eda9c5a..e43c0ad8 100644 --- a/src/views/Dashboard.vue +++ b/src/views/Dashboard.vue @@ -480,20 +480,32 @@ export default { /** * Format date for display * + * Renders as `17/08/2026, 08:33`. `toLocaleDateString` is correct here + * even though the result carries a time: explicit `hour`/`minute` + * options are honoured (ECMA-402 ToDateTimeOptions only supplies + * date-part DEFAULTS when none are given), so this is not the + * "toLocaleDateString silently drops the time" trap it resembles. + * + * A trailing `.replace(',', ',')` was removed here. It replaced the + * comma with itself — a no-op, flagged as js/identity-replacement. + * It was born in that identical form (5c33f0b, "Working on the detail + * pages"), so no working behaviour was ever lost and no intent is + * recorded anywhere to recover. Deleting it is byte-for-byte + * output-preserving, verified against the string above; guessing at + * `.replace(',', '')` would have invented a UI change nothing asked for. + * * @param {Date} date - Date to format * @return {string} Formatted date string * @spec openspec/specs/fe-shell-navigation/spec.md */ formatDate(date) { - return date - .toLocaleDateString('en-GB', { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }) - .replace(',', ',') + return date.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) }, /** From a99ee1995b5d3c03919b86eb2c9b2eff73bae07e Mon Sep 17 00:00:00 2001 From: Conduction Release Bot Date: Mon, 17 Aug 2026 06:55:48 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(ci):=20correct=20the=20documentation.ym?= =?UTF-8?q?l=20permissions=20rationale=20=E2=80=94=20build-image=20default?= =?UTF-8?q?s=20to=20true?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment claimed `packages: write` was needed only because GitHub statically validates a callee's declared job permissions, and that the `image` job "never runs" here. That is wrong on the second half. `build-image` in ConductionNL/.github/.github/workflows/documentation.yml is `type: boolean, default: true`, and none of the callers pass it. So the `image` job DOES run on a push to `documentation`, and it really does `docker buildx` push to GHCR. `packages: write` is load-bearing at RUNTIME, not merely statically — dropping it would 403 that push. Comment only; the permissions block itself is unchanged and was already correct. --- .github/workflows/documentation.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 41a559a4..1de66271 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -15,10 +15,15 @@ jobs: # deploy contents: write (peaceiris/actions-gh-pages pushes to gh-pages) # image contents: read, packages: write (buildx push to GHCR) # - # ⚠️ `packages: write` stays even though `build-image` is not set here, so - # the `image` job never runs. GitHub validates the callee's DECLARED job - # permissions against this ceiling before dispatch — including jobs an `if:` - # will skip — so dropping it makes the whole call fail to start. + # ⚠️ `packages: write` is load-bearing at RUNTIME, not merely statically: + # the callee's `build-image` input DEFAULTS TO TRUE, so the `image` job + # really does run on a push to `documentation` and really does push to + # GHCR. Dropping it 403s that push. + # + # It would be required even if that job were disabled, because GitHub + # validates the callee's DECLARED job permissions against this ceiling + # before dispatch — including for jobs an `if:` will skip — so a too-low + # ceiling makes the whole call fail to start rather than run with less. permissions: contents: write packages: write