From 37cb6d26ba3af4440557c5e903d2d3bf000c7b48 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 14 Aug 2026 02:17:02 +0000 Subject: [PATCH 1/2] feat(threatcrush-scan): ask for no write scope when nothing writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SAG closed their request on, among other things, "an externally maintained CLI ... together with PR and security-reporting permissions". The permission block was static and requested `pull-requests: write` and `security-events: write` unconditionally, including in the configuration where both the upload and the comment are switched off. A workflow that asks for a write scope it will not use cannot call itself least privilege, which is awkward for one whose request body argues about supply-chain hygiene. commentOnPr joins uploadSarif as a switch, and extraPermissions carries the lines beneath `contents: read` — computed from the two rather than set by hand, so the block cannot drift out of step with what the workflow actually does. With both false the rendered workflow requests `contents: read` and nothing else, and findings arrive in the job summary and the SARIF artifact. Verified by parsing the rendered output: default {"contents":"read","pull-requests":"write","security-events":"write"} least privilege {"contents":"read"} Pack to 1.5.0. Signed-off-by: Anthony Ettinger Co-Authored-By: Claude Opus 5 --- .../threatcrush-scan/sh1pt.actionpack.yaml | 26 +++++++++++++++++++ .../actions/threatcrush-scan/workflow.yml | 17 +++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml index 097a0c91..25c4202e 100644 --- a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml +++ b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml @@ -59,6 +59,32 @@ inputs: but a confusing one to debug. Read it from `npm view dist.integrity`. Empty skips verification, for a consumer pointing the spec at something they build themselves. + commentOnPr: + type: string + default: 'true' + enum: + - 'true' + - 'false' + description: >- + Post the report as a pull request comment. Requires + `pull-requests: write`. + + Set this and uploadSarif both to 'false' and the workflow requests + `contents: read` and nothing else — findings arrive in the job summary + and the SARIF artifact instead. That is the configuration for a + repository that wants the scan without granting a third-party CLI any + write scope, which is a substantial part of what reviewers decline on. + extraPermissions: + type: string + default: " pull-requests: write\n security-events: write" + description: >- + The permission lines added beneath `contents: read`, computed from + uploadSarif and commentOnPr rather than set by hand. Two spaces of + indentation per line; empty when neither output is enabled. + + An input rather than a fixed block because a workflow that asks for a + write scope it will not use cannot argue it is least-privilege, and the + two scopes here only exist to serve features a consumer can switch off. failOn: type: string default: '' diff --git a/packages/actions/threatcrush-scan/workflow.yml b/packages/actions/threatcrush-scan/workflow.yml index 7be0b18e..9f42be03 100644 --- a/packages/actions/threatcrush-scan/workflow.yml +++ b/packages/actions/threatcrush-scan/workflow.yml @@ -3,10 +3,18 @@ name: threatcrush security scan on: pull_request: +# Only what the enabled outputs actually need. Both write scopes exist to +# serve an optional feature — the Security tab upload and the PR comment — and +# were requested unconditionally even when both were switched off. +# +# With uploadSarif and commentOnPr both false this reads `contents: read` and +# nothing else, and the findings arrive in the job summary and the artifact. +# SAG declined partly on "an externally maintained CLI ... together with PR and +# security-reporting permissions"; a scanner that asks for write scopes it is +# not going to use has no answer to that, and now it does not have to ask. permissions: contents: read - pull-requests: write - security-events: write +{{extraPermissions}} jobs: scan: @@ -305,7 +313,10 @@ jobs: # get a writable token: that event runs with repository secrets in scope # against a checkout of untrusted contributor code. - name: Comment on PR - if: always() && github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]' + if: >- + always() && '{{commentOnPr}}' == 'true' + && github.event.pull_request.head.repo.full_name == github.repository + && github.actor != 'dependabot[bot]' continue-on-error: true uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7 with: From 658c0f847561ebb0468dcf9424aa659b8849d531 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 14 Aug 2026 02:17:17 +0000 Subject: [PATCH 2/2] chore(threatcrush-scan): bump the pack to 1.5.0 The previous commit said 1.5.0 and left the manifest at 1.4.0. Signed-off-by: Anthony Ettinger Co-Authored-By: Claude Opus 5 --- packages/actions/threatcrush-scan/sh1pt.actionpack.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml index 25c4202e..06d5eb21 100644 --- a/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml +++ b/packages/actions/threatcrush-scan/sh1pt.actionpack.yaml @@ -5,7 +5,7 @@ description: >- Scans pull requests for hardcoded credentials, injection, SSRF, unsafe deserialisation and dependency tampering, and uploads SARIF to the Security tab. -version: 1.4.0 +version: 1.5.0 publisher: profullstack visibility: public license: MIT