Skip to content

Commit eb71c24

Browse files
author
Conduction Release Bot
committed
fix(ci): least-privilege workflow permissions + drop a dead string replace
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.
1 parent 68202d5 commit eb71c24

10 files changed

Lines changed: 133 additions & 9 deletions

.github/workflows/branch-protection.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,10 @@ on:
88

99
jobs:
1010
protect:
11+
# The callee's `check-branch` job is a single bash comparison of
12+
# `github.base_ref` against `github.head_ref` — no checkout, no network, no
13+
# API call. It needs nothing, so it gets nothing. `{}` is an explicit empty
14+
# grant, which is what satisfies actions/missing-workflow-permissions; an
15+
# ABSENT block means "whatever the repo default is", which is the finding.
16+
permissions: {}
1117
uses: ConductionNL/.github/.github/workflows/branch-protection.yml@main

.github/workflows/code-quality.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,36 @@ on:
8383
# be redder than a PR — that is the honest answer, not a regression.
8484
workflow_dispatch:
8585

86+
# Permission CEILING for the called quality pipeline, NOT a grant. Copied
87+
# verbatim from openconnector, where this exact block is live on `development`
88+
# with all ~30 quality jobs green — so it is a measured ceiling, not a guess.
89+
#
90+
# ⚠️ Most jobs in the callee declare NO permissions of their own (only
91+
# journeydoc-capture and features-extract do), which means they inherit THIS
92+
# ceiling exactly. Every entry below is load-bearing:
93+
#
94+
# contents: write journeydoc-capture and update-baseline `git push`
95+
# the regenerated screenshots / coverage baseline
96+
# actions: write journeydoc-capture re-dispatches downstream work
97+
# (a GITHUB_TOKEN push does not fire workflow_run)
98+
# issues / pull-requests: write
99+
# the Quality Report comment — `github.rest.issues
100+
# .createComment`, which 403s under contents+packages
101+
# read alone (quality.yml notes this at its own step)
102+
# packages: read pulling org images for the Nextcloud test matrix
103+
#
104+
# ⚠️ Do NOT "tighten" any of these to `read`. GitHub statically validates the
105+
# called workflow's DECLARED job permissions against this grant — including for
106+
# jobs an `if:` will skip — so capping one makes the whole call fail to START:
107+
# zero jobs, no annotations, and a Code Quality run that is absent rather than
108+
# red. That is quieter than the outage it would cause.
109+
permissions:
110+
contents: write
111+
actions: write
112+
issues: write
113+
pull-requests: write
114+
packages: read
115+
86116
jobs:
87117
quality:
88118
uses: ConductionNL/.github/.github/workflows/quality.yml@main

.github/workflows/documentation.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,20 @@ on:
88

99
jobs:
1010
deploy:
11+
# Permission CEILING for the called documentation workflow, not a grant.
12+
# The callee has three jobs and this block is their UNION, which is what the
13+
# token already resolves to today:
14+
# build contents: read
15+
# deploy contents: write (peaceiris/actions-gh-pages pushes to gh-pages)
16+
# image contents: read, packages: write (buildx push to GHCR)
17+
#
18+
# ⚠️ `packages: write` stays even though `build-image` is not set here, so
19+
# the `image` job never runs. GitHub validates the callee's DECLARED job
20+
# permissions against this ceiling before dispatch — including jobs an `if:`
21+
# will skip — so dropping it makes the whole call fail to start.
22+
permissions:
23+
contents: write
24+
packages: write
1125
uses: ConductionNL/.github/.github/workflows/documentation.yml@main
1226
with:
1327
cname: softwarecatalog.conduction.nl

.github/workflows/issue-triage.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ on:
1212

1313
jobs:
1414
triage:
15+
# Permission CEILING for the called workflow, not a grant. All three jobs in
16+
# the callee (triage-single, triage-decision, backlog-triage) already declare
17+
# exactly `issues: write` + `contents: read` for themselves, so this restates
18+
# the grant the token has today and changes nothing that runs. `issues: write`
19+
# is the label-and-assign step; the ProjectsV2 board step is disabled because
20+
# GITHUB_TOKEN cannot reach it at any permission level (ConductionNL/.github#432).
21+
permissions:
22+
issues: write
23+
contents: read
1524
uses: ConductionNL/.github/.github/workflows/issue-triage.yml@main
1625
with:
1726
app-name: softwarecatalog

.github/workflows/openspec-sync.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ on:
88

99
jobs:
1010
sync:
11+
# Permission CEILING for the called workflow, not a grant. The callee's
12+
# `sync` job already declares exactly `issues: write` + `contents: read`, so
13+
# this restates today's effective token. It creates/updates the OpenSpec
14+
# tracking issues; the board placement is disabled (GITHUB_TOKEN cannot do
15+
# ProjectsV2 — ConductionNL/.github#432).
16+
permissions:
17+
issues: write
18+
contents: read
1119
uses: ConductionNL/.github/.github/workflows/openspec-sync.yml@main
1220
with:
1321
app-name: softwarecatalog

.github/workflows/release-beta.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,19 @@ on:
77

88
jobs:
99
release:
10+
# Permission CEILING for the called release workflow, not a grant. The
11+
# callee's `release` job already declares `contents: write` for itself, so
12+
# this restates today's effective token and changes nothing that runs.
13+
# `contents: write` cuts the tag and uploads the release asset
14+
# (ncipollo/release-action, svenstaro/upload-release-action); the App Store
15+
# publish authenticates with NEXTCLOUD_APPSTORE_TOKEN, not GITHUB_TOKEN, so
16+
# it needs nothing here.
17+
#
18+
# ⚠️ Not `contents: read`. A caller block CAPS the callee — GitHub validates
19+
# the callee's declared job permissions against it — so a lower ceiling makes
20+
# the call fail to START (zero jobs) rather than run with less.
21+
permissions:
22+
contents: write
1023
uses: ConductionNL/.github/.github/workflows/release-beta.yml@main
1124
with:
1225
app-name: softwarecatalog

.github/workflows/release-development.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ concurrency:
2929

3030
jobs:
3131
release:
32+
# Permission CEILING for the called release workflow, not a grant. The
33+
# callee's `release` job already declares `contents: write` for itself, so
34+
# this restates today's effective token and changes nothing that runs.
35+
# `contents: write` cuts the tag and publishes the prerelease + .tar.gz that
36+
# the App Versions app installs from. This channel never touches the App
37+
# Store, so nothing else is needed.
38+
#
39+
# ⚠️ Not `contents: read`. A caller block CAPS the callee, so a lower ceiling
40+
# makes the call fail to START rather than run with less.
41+
permissions:
42+
contents: write
3243
uses: ConductionNL/.github/.github/workflows/release-beta.yml@main
3344
with:
3445
app-name: softwarecatalog

.github/workflows/release-stable.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ on:
77

88
jobs:
99
release:
10+
# Permission CEILING for the called release workflow, not a grant. The
11+
# callee's `release` job already declares `contents: write` for itself, so
12+
# this restates today's effective token and changes nothing that runs.
13+
# `contents: write` cuts the tag and uploads the release asset; the App Store
14+
# publish authenticates with NEXTCLOUD_APPSTORE_TOKEN, not GITHUB_TOKEN.
15+
#
16+
# ⚠️ Not `contents: read`. A caller block CAPS the callee, so a lower ceiling
17+
# makes the call fail to START rather than run with less.
18+
permissions:
19+
contents: write
1020
uses: ConductionNL/.github/.github/workflows/release-stable.yml@main
1121
with:
1222
app-name: softwarecatalog

.github/workflows/sync-to-beta.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,15 @@ on:
77

88
jobs:
99
sync:
10+
# Permission CEILING for the called workflow, not a grant. The callee's
11+
# `create-pr` job already declares exactly these two, so this restates
12+
# today's effective token. It runs `gh pr list` / `gh pr create` to keep the
13+
# standing development -> beta release PR open, which needs
14+
# `pull-requests: write`; `contents: write` is the checkout it pushes from.
15+
#
16+
# ⚠️ `contents: read` alone would leave the PR step 403-ing at runtime — a
17+
# red release-sync that looks like a merge conflict.
18+
permissions:
19+
contents: write
20+
pull-requests: write
1021
uses: ConductionNL/.github/.github/workflows/sync-to-beta.yml@main

src/views/Dashboard.vue

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -480,20 +480,32 @@ export default {
480480
/**
481481
* Format date for display
482482
*
483+
* Renders as `17/08/2026, 08:33`. `toLocaleDateString` is correct here
484+
* even though the result carries a time: explicit `hour`/`minute`
485+
* options are honoured (ECMA-402 ToDateTimeOptions only supplies
486+
* date-part DEFAULTS when none are given), so this is not the
487+
* "toLocaleDateString silently drops the time" trap it resembles.
488+
*
489+
* A trailing `.replace(',', ',')` was removed here. It replaced the
490+
* comma with itself — a no-op, flagged as js/identity-replacement.
491+
* It was born in that identical form (5c33f0b, "Working on the detail
492+
* pages"), so no working behaviour was ever lost and no intent is
493+
* recorded anywhere to recover. Deleting it is byte-for-byte
494+
* output-preserving, verified against the string above; guessing at
495+
* `.replace(',', '')` would have invented a UI change nothing asked for.
496+
*
483497
* @param {Date} date - Date to format
484498
* @return {string} Formatted date string
485499
* @spec openspec/specs/fe-shell-navigation/spec.md
486500
*/
487501
formatDate(date) {
488-
return date
489-
.toLocaleDateString('en-GB', {
490-
day: '2-digit',
491-
month: '2-digit',
492-
year: 'numeric',
493-
hour: '2-digit',
494-
minute: '2-digit',
495-
})
496-
.replace(',', ',')
502+
return date.toLocaleDateString('en-GB', {
503+
day: '2-digit',
504+
month: '2-digit',
505+
year: 'numeric',
506+
hour: '2-digit',
507+
minute: '2-digit',
508+
})
497509
},
498510
499511
/**

0 commit comments

Comments
 (0)