Skip to content

Pin Semgrep CI image by digest and scope security-events to the upload job (CWE-829) - #31

Open
07souravkunda wants to merge 3 commits into
masterfrom
locsec/WI-7132e51e
Open

Pin Semgrep CI image by digest and scope security-events to the upload job (CWE-829)#31
07souravkunda wants to merge 3 commits into
masterfrom
locsec/WI-7132e51e

Conversation

@07souravkunda

@07souravkunda 07souravkunda commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

What

CI supply-chain hardening for the Semgrep workflow (CWE-829 — Inclusion of Functionality from Untrusted Control Sphere).

  1. Pin returntocorp/semgrep by immutable digest instead of the 1.166.0 tag.
  2. Move the SARIF upload into its own job, so security-events: write is no longer held by the job that runs the third-party container.
  3. Add a Dependabot config for github-actions so the action SHA pins don't go stale.

Why

.github/workflows/Semgrep.yml runs a third-party container image on a 0 6 * * * cron (plus every push/PR to master), in a job that has the repository checked out and holds security-events: write.

  • A version tag is still mutable. 1.166.0 was a real improvement over :latest, but Docker Hub tags can be re-pointed at new content upstream, and the next scheduled run would pull and execute it with no PR gate. A digest cannot be re-pointed — updating it now requires a reviewable commit, which restores the audit trail.
  • The container did not need security-events: write. Only the SARIF upload does. As written, a compromised image could forge or suppress entries in the repository's code-scanning dashboard. Splitting the jobs means the third-party code runs with contents: read and nothing else.

Changes

.github/workflows/Semgrep.yml

-      image: returntocorp/semgrep:1.166.0
+      image: returntocorp/semgrep@sha256:c180f0c93a17b420c0af5006214a29d3c747c5459c732b740191adf657dd0068

The digest is the multi-arch OCI index that returntocorp/semgrep:1.166.0 resolves to today (pushed 2026-06-11) — confirmed independently against registry-1.docker.io (Docker-Content-Digest) and the Docker Hub v2 tag API. Same image, so no behavioural change; pinning the index rather than a per-arch manifest keeps the ref architecture-agnostic. The human-readable version and the command to refresh the pin are recorded in a comment next to it.

Job layout:

Job Runs third-party code Permissions
semgrep (unchanged name, so any required check keeps matching) yes — the Semgrep container contents: read
upload-sarif (new) no — first-party actions only contents: read, security-events: write

The SARIF crosses between them as a workflow artifact. Both the artifact upload and the new job use if: always(), because semgrep ci exits non-zero whenever it has blocking findings — without that, the SARIF would stop reaching the dashboard on exactly the runs that matter.

.github/dependabot.yml (new) — weekly github-actions updates, with a 7-day cooldown so a version isn't proposed the day it's published. Note that Dependabot cannot bump a workflow container: digest: its docker ecosystem only parses Dockerfiles, Kubernetes manifests and Helm values (dependabot-core#5819 is still open), and github-actions only covers uses: refs. That's why the manual refresh command is in the workflow comment — adopting Renovate, which does support workflow container digests, would automate it.

Testing

The workflow is exercised by this PR itself — the pull_request trigger runs the modified file, so the runs on this PR are the test.

Run Head semgrep/ci upload-sarif (new) Overall
31599734585 1fc44b3 ❌ exit 1 — 1 finding (see below) failure
31599955720 8b08eb9 ✅ 0 findings success

Digest pin confirmed from the runner log — docker pull returntocorp/semgrep@sha256:c180f0c9…semgrep 1.166.0 on python 3.12.13, i.e. the digest resolves to exactly the version the tag named.

The permission split is confirmed by the first run, which is the more informative one: the semgrep job failed and the SARIF still reached the dashboard, because the artifact upload and the new job both carry if: always(). That preserves the one behaviour that mattered about the old upload step.

The first run's finding was in this PR's own new file, and it was a fair catch. Semgrep's p/default flagged package_managers.dependabot.dependabot-missing-cooldown on .github/dependabot.yml — a Dependabot with no cooldown proposes a version the instant it's published, which is the same window a hijacked release exploits. Fixed in 8b08eb9; the re-run is clean.

Also checked locally before pushing: both files parse as YAML; every third-party ref is immutable (digest or full commit SHA, no @vN remaining); and asserted programmatically that no job both runs the container and holds security-events: write. The image digest was cross-checked against two independent sources (registry Docker-Content-Digest header and the Docker Hub v2 tag API).

One thing the green here does not mean: on pull_request, semgrep ci is diff-aware and scanned only the 2 changed files. master's full scan still exits 1 with 5 pre-existing findings (e.g. run 31569942332) — untouched by this PR.

Not in this PR

A composer.lock was considered — the repo has none, so composer install resolves phpunit/phpunit: 4.6.* and its transitives fresh against Packagist on every clone. It turns out a lockfile cannot be committed here without making things worse. Composer 2.9 refuses to resolve that constraint at all under its default block-insecure audit, and forcing it (--no-security-blocking) produces a lock that pins 4 known advisories across 2 packages:

Committing that would require an audit.ignore for a high-severity CVE and would pin the vulnerability into the dependency graph. The advisory is in any case already visible without a lockfile — Dependabot alert #1 on the default branch is exactly GHSA-vvj3-c3rp-c85p, detected from composer.json. The real fix is to move off PHPUnit 4.6 — which means migrating tests/LocalTest.php off the PHPUnit_Framework_TestCase API — and that belongs in its own PR, separate from CI pinning. Nothing here ships to consumers: both the image and PHPUnit are CI/dev-only (Composer never installs a library's require-dev for downstream consumers).

Worth noting for whoever picks that up: no CI job currently runs composer install at all. .travis.yml still references it, but Travis is no longer wired to this repo — recent commits report no Travis status contexts, only semgrep/ci and Analyze (actions).

…ainer job

The Semgrep workflow runs a third-party container on a daily cron with
`security-events: write` and the repository checked out. Two hardening changes
(CWE-829, Inclusion of Functionality from Untrusted Control Sphere):

1. Pin `returntocorp/semgrep` by immutable digest instead of the `1.166.0` tag.
   A version tag on Docker Hub is still mutable — it can be re-pointed at new
   content upstream, and the next scheduled run would execute unreviewed code
   with no PR gate. The digest is the same image that tag resolves to today
   (multi-arch index, pushed 2026-06-11), so this is a no-op for behaviour.

2. Move the SARIF upload into its own job. `security-events: write` is now held
   only by a job that runs no third-party code; the container job keeps
   `contents: read`. If the image is ever compromised it can no longer forge
   entries in the code-scanning dashboard. The SARIF crosses between jobs as a
   workflow artifact, uploaded with `if: always()` because `semgrep ci` exits
   non-zero when it has blocking findings.

Also adds a Dependabot config for `github-actions` so the action SHA pins do not
go stale. Dependabot cannot bump a workflow `container:` digest
(dependabot/dependabot-core#5819), so the refresh command for the image pin is
recorded next to it.

Ref: https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@07souravkunda 07souravkunda self-assigned this Aug 12, 2026
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Comment thread .github/dependabot.yml Fixed
Flagged by this repo's own Semgrep scan on the previous commit
(package_managers.dependabot.dependabot-missing-cooldown): without a cooldown,
Dependabot proposes a version the moment it is published, which is exactly the
window a hijacked release exploits. Compromised releases are typically yanked
within a few days, so wait 7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@07souravkunda 07souravkunda left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security-fix review — 2 blocking, 1 nit. Both blockers are tracker-side; the diff itself is clean.

The change does what it claims and I verified it rather than taking it on trust:

  • The digest is right. returntocorp/semgrep:1.166.0 resolves to sha256:c180f0c9… — confirmed independently against registry-1.docker.io's Docker-Content-Digest, matching the PR's claim. Same image, so the "no behavioural change" line holds.
  • It was actually exercised, not just parsed. Run 31599955720 (8b08eb9): conclusion=success, both jobs green. Run 31599734585 is the better evidence — the container job failed and the SARIF still uploaded, which is the one behaviour the old if: always() step was protecting.
  • The permission split is real. semgrep holds contents: read only; security-events: write now lives on a job that runs no third-party code. That is the third remediation bullet on the ticket, delivered rather than deferred.
  • Scope is tight. Two files, both under .github/, no drive-by version bumps, no PHP touched. Nothing here reaches the published Packagist package. The repo is public and carries no internal tracker id in the title, body, commits or diff.
  • The residuals are honest. The composer.lock leg genuinely cannot be fixed as the ticket words it — I re-ran the reasoning and Composer 2.9's block-insecure audit does refuse phpunit/phpunit: 4.6.*, and forcing a lock pins a high CVE. Likewise, Dependabot really cannot bump a workflow container: digest (dependabot-core#5819), and the PR says so instead of claiming the bullet. Both are the right call, stated plainly.
  • Also worth noting: the first run's Semgrep finding was in this PR's own new file, and it was fixed rather than waved through. And the file now ends with a newline, which master's did not.

What blocks, and neither is in the code:

  1. The two component findings this chain closes are both still New Item with no routing comment — including the one whose remediation is this diff. A human opening either ticket sees no sign a fix exists.
  2. The completion comment on the parent ticket is accurate but unstructured — no 🔎 Summary, so a non-security reader has to parse CVSS vectors to learn what the weakness was.

Details inline. No code change is needed for either; the diff is good as it stands.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] The two component findings this change closes are still unrouted on the tracker — including the one whose remediation this file is.

The change itself is correct and I could verify every claim behind it (see the review summary). The gap is on the tracker side: the finding that asks for the digest pin on this exact file is still sitting in New Item with no record that a fix exists, and its sibling is in the same state. Both are still assigned to a different engineer, so from their side nothing has happened.

Evidence

  • Chain-breaker finding (digest pin on .github/workflows/Semgrep.yml) — status New Item, assignee unchanged, only comment is a June ETA note from security. Nothing links it to this PR.
  • Sibling finding (composer.lock) — status New Item, no comment. The fix write-up argues at length that it cannot be fixed as worded (Composer 2.9 refuses phpunit/phpunit: 4.6.* under block-insecure; forcing a lock pins CVE-2026-24765 high + 3 symfony/yaml lows). That reasoning is sound and I re-checked it — but it lives only in the parent's comment and in this PR body. The ticket a human will actually triage says nothing.
  • The parent's completion comment does say "Once merged, LOC-6931 and LOC-6925 can both close; LOC-6927 needs the separate PHPUnit decision" — but a sentence on the parent is not a routing record on the siblings. Whoever picks either one up next sees an untouched ticket.

Fix

  • On the chain-breaker finding: a completion comment naming this PR as the fix ("covered by the chain-breaker fix in PR Pin Semgrep CI image by digest and scope security-events to the upload job (CWE-829) #31"), so it closes with the parent.
  • On the composer.lock finding: its own wont-do / dispute comment carrying the audit evidence already gathered (composer-audit-if-locked.txt, composer.lock.rejected-evidence) and the follow-up ask (migrate tests/LocalTest.php off the PHPUnit 4.6 API), so it can be closed as Won't Do rather than left open with no explanation.

No change to this diff is needed for either.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — both siblings are routed on the tracker now, not just referenced from the parent.

Chain-breaker (LOC-6925 · F-009) — assigned to the operator, moved to Dev in Progress, labelled locsec-fixed, and given a full completion comment naming this PR as its remediation ("Covered by the chain-breaker fix in PR #31"). It carries its own assessment rather than a pointer: its own CVSS mismatch (7.5 stated vs 8.0 recomputed from its own vector), the reachability evidence (the three consecutive scheduled runs that executed the mutable ref), the digest provenance, and the run ids that prove the fix. It closes with the parent.

Sibling (LOC-6927 · F-011) — assigned, moved to Dev in Progress, labelled locsec-wontfix, and given its own won't-do assessment (no-fix-available, because the prescribed remediation is what's unavailable). It carries the audit evidence in full: the exact composer 2.9 block-insecure failure text, the 4 advisories a forced lock would pin (phpunit 4.6.10 → CVE-2026-24765 HIGH, symfony/yaml v3.4.47 → three lows, plus the abandoned mock-objects package), the pointer to composer.lock.rejected-evidence / composer-audit-if-locked.txt, and the follow-up with owner and auto-resolve condition. Recommended close as Won't Do — proposed, not accepted; that call stays with a human.

Three corrections to that ticket went in with it, since a human triaging it would otherwise inherit them: its score disagrees with its own vector (5.9 vs 6.5); its data-flow step 3 rests on the dead Travis composer install; and its Impact overstates blast radius as "all consumers of this library" when Composer never installs a library's require-dev downstream. I also named the root cause the report missed — .gitignore:3 ignores composer.lock deliberately, so any real fix has to change that line.

Also recorded the won't-do as a row in .claude/knowledge/workspace/false-positives.md (Table 2), per the dispute-skill requirement, so the next agent that meets this constraint doesn't re-derive it.

You were right that a sentence on the parent isn't a routing record. Agreed and fixed — no change to this diff.

# Digest below == returntocorp/semgrep:1.166.0 (multi-arch index, pushed 2026-06-11).
# To refresh the pin (and update this comment):
# docker manifest inspect returntocorp/semgrep:<version> -v | grep -m1 Digest
image: returntocorp/semgrep@sha256:c180f0c93a17b420c0af5006214a29d3c747c5459c732b740191adf657dd0068

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] The completion comment posted on the parent ticket skips the plain-English summary and is free-form prose instead of the standard structure.

(Anchored here because this line is the change the comment describes — the finding itself is tracker-side, not in this diff.)

Every factual claim in it checks out. I re-derived all three CVSS scores from their own vectors (8.7 / 8.0 / 6.5 against the tickets' 8.4 / 7.5 / 5.9), so the flagged mismatch is real and not a v3.1-vs-v4.0 artifact of the same advisory; the digest confirms independently against registry-1.docker.io; and the dead-Travis claim holds (zero status contexts on master's head, composer install present at .travis.yml:9 but nothing runs it). The content is accurate — the shape is the problem.

Evidence — the posted comment opens straight into ### Vulnerability assessment with a paragraph of CVSS vectors. The required shape is a 🔎 Summary heading first (one TL;DR line a non-security reader understands, then Attacker / Path / Impact / Fix bullets), followed by the bold-label bullet sections — Assessment, Fix, Testing, Decision. What is posted has no Summary at all, no Decision heading, and runs as narrative paragraphs rather than labelled bullets.

Concretely, the reader gets no one-liner telling them: anyone who could push to the returntocorp Docker Hub account could have re-pointed the 1.166.0 tag, and the next 6am cron would have run their code inside a runner holding security-events: write. That sentence is the whole ticket for a non-security triager, and it is the one thing missing.

Fix — edit the comment in place (same commentId), prepending the 🔎 Summary section and reflowing the existing sections into the bold-label bullet skeleton. The locsec-fix-done JSON marker block stays as-is; nothing under it needs to change.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — edited in place on the same commentId (2283185), so the history stays single-threaded rather than a correction stacked underneath.

The comment now opens with 🔎 Summary: a one-line TL;DR followed by Attacker / Path / Impact / Fix bullets, then Assessment / Fix / Testing / Residuals / Decision as bold-label bullet lists. I used your sentence more or less verbatim as the TL;DR — anyone who could push to the returntocorp Docker Hub account could have re-pointed the 1.166.0 tag, and the next 6am cron would have run their code inside a runner holding security-events: write — because it is a better opening line than what I had. Attacker is stated as a different principal (an outside publisher, so a real trust boundary), which was the other thing a triager couldn't tell from the old prose.

One deviation from your instruction, flagged rather than done quietly: I did not keep the marker block as-is — I shrank it. It was ~4 KB of JSON that re-encoded the whole assessment/fix/testing prose. The skill's § Marker section makes that a hard size guard, not a preference: only disposition, operator and pr_url are machine-read, and a ~15 KB ADF body has tripped the 300s addCommentToJiraIssue timeout before. Since the reflow into Summary + bullets adds visible text, keeping the fat marker pushed toward that ceiling for no benefit, so it is now the minimal {"schema":"locsec/v1","disposition":...,"operator":...,"pr_url":...}. Nothing machine-read was dropped, and every fact that was only in the JSON is now in a visible section. Say the word if you'd rather I restore the long form.

While reflowing I also folded in what changed this round — the Also affects bullet pointing at LOC-6927, and a Residuals section covering both the composer.lock constraint and the Dependabot-can't-bump-container-digests limitation — so the parent comment matches the tracker state after the sibling routing above.

And thank you for re-deriving all three CVSS scores independently rather than taking mine; that mismatch claim is load-bearing for the ticket and it's worth more having been checked twice.

security-events: write # for github/codeql-action/upload-sarif to upload SARIF results

steps:
- name: Download SARIF artifact

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] When the semgrep job dies before writing the SARIF, this job now turns red too, with a misleading reason.

The interesting failure mode — semgrep ci exits 1 on blocking findings — is handled correctly, and run 31599734585 proves it: the container job failed and the SARIF still reached the dashboard through this job. That is the case that mattered and it works.

The uncovered case is a failure upstream of semgrep ci: a container-pull failure (registry outage, or the pinned digest going away), or actions/checkout failing. Then no step runs, Upload SARIF as a workflow artifact never fires, and if-no-files-found: error is moot because there is no artifact at all. upload-sarif still starts (if: always()), Download SARIF artifact errors on a missing artifact, and the run shows two red jobs where the pre-split workflow showed one. The second one points at artifact download rather than at the actual cause.

Not a correctness problem — nothing is silently lost, and the dashboard behaviour is unchanged. Purely about not sending someone debugging a registry outage to a download step.

If you want it tighter, either gate the job on the artifact-producing step having run, or let the download tolerate absence:

      - name: Download SARIF artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: semgrep-sarif
        continue-on-error: true

with the upload step then guarded on the file existing. Equally fine to leave as-is and let the container job's own red tell the story.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken — 233e0b0. You offered "equally fine to leave as-is", but the case you describe is real and I'd rather not ship a workflow that points a debugger at the wrong step during a registry outage.

Went with your second suggestion, both halves of it:

      - name: Download SARIF artifact
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          name: semgrep-sarif
        continue-on-error: true

      - name: Upload SARIF file for GitHub Advanced Security Dashboard
        uses: github/codeql-action/upload-sarif@6c089f53dd51dc3fc7e599c3cb5356453a52ca9e # v2.20.0
        with:
          sarif_file: semgrep.sarif
        if: hashFiles('semgrep.sarif') != ''

The guard on the upload step is what makes continue-on-error safe rather than just quieter — without it the upload would run against a nonexistent file and fail anyway, one step later. hashFiles() returns an empty string for a missing path, and this job does no checkout, so the file exists only if the download actually produced it.

I preferred this over gating the job on a needs.semgrep.outputs.* flag: job outputs from a failed job are propagated, so it would work, but it means threading a step output through a job output to express something the file's presence already states directly.

Net effect is what you asked for — an upstream failure now shows exactly one red job, the container job, which is the one that can explain itself. The case that mattered is unchanged: semgrep ci exiting 1 on blocking findings still writes the SARIF, uploads it, and reaches the dashboard, which is what run 31599734585 demonstrated.

No new CI evidence for the new path, and I'd rather say so than imply otherwise: forcing a container-pull failure would mean pushing a deliberately bogus digest, and I didn't think a broken commit in the PR history was worth it. The reasoning above is the argument; the normal path stays covered by the runs already on the PR.

Review nit: if the container job dies before `semgrep ci` runs — a container-pull
failure, or checkout failing — no SARIF is written, and the upload job then went
red on "artifact not found", pointing a debugger at the download step instead of
at the real upstream cause. Two reds where the pre-split workflow showed one.

Tolerate the missing artifact and skip the upload when there is no file. The
normal case is unaffected: `semgrep ci` exiting 1 on blocking findings still
writes the SARIF, still uploads it, and still reaches the dashboard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@07souravkunda 07souravkunda left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review (round 2) — both blockers resolved. No blocking findings; 1 nit, non-gating.

I re-read the diff from scratch at 233e0b04 rather than diffing against my last read, and re-checked each prior finding against the tracker rather than assuming the fixer's word for it.

Round-1 blocker 1 — chain siblings unrouted: resolved.

  • The chain-breaker finding now carries a full completion comment naming this PR ("Covered by the chain-breaker fix in PR #31"), moved to Dev in Progress, labelled locsec-fixed, reassigned.
  • The composer.lock finding now carries its own won't-do assessment — the composer evidence, the corrected data flow, a named follow-up (migrate off PHPUnit 4.6, then commit the lock and drop it from .gitignore), and an explicit "I am proposing this, not accepting the risk: that call is yours." That is the right posture; it recommends rather than decides.

Round-1 blocker 2 — missing plain-English summary: resolved. The parent's comment was edited in place on the same commentId (created 18:45, updated 20:13), so there is no duplicate. It now opens with the 🔎 Summary and the Attacker / Path / Impact / Fix walkthrough, and the added Also affects bullet states up front that the second leg is not fixed — the residual is now visible in the first paragraph a triager reads, not buried.

Claims in the new comments, spot-checked against origin rather than taken on trust:

  • Semgrep.yml:30 for the image on master — correct at line 30 (the ticket's :31 was from the older e563485).
  • Dependabot alert #1GHSA-vvj3-c3rp-c85p, high, phpunit/phpunit, from composer.json, open. Matches exactly.
  • The "proven live" cron claim — run 31569942332 is event: schedule, 2026-08-12 06:25Z, on master's head. Confirmed.
  • "Binding frozen since 2016" — last commit touching lib/ is 2016-09-19.
  • The CVSS recomputations (8.7 / 8.0 / 6.5) I re-derived independently last round; unchanged and correct.

Round-1 nit — noisy second failure: fixed in 233e0b04, and verified. Run 31607926461: both jobs green, and this time I checked step level — the upload step ran with conclusion success, not skipped, so hashFiles() resolved as intended and the SARIF landed (Semgrep OSS is present on the new head). The commit is scoped to exactly that change and nothing else.

Gates re-run on the new head: scope still two files under .github/, no drive-by edits, digest still resolves correctly at the registry, no internal tracker id in the title, body, commit messages or diff, and no regression test — correct for a config pin.

One nit inline on the trade the fix makes. It doesn't gate; leaving it as-is is defensible. Nothing here needs another round from my side — this is ready for a human to approve and merge.

with:
sarif_file: semgrep.sarif
if: always() No newline at end of file
if: hashFiles('semgrep.sarif') != ''

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] This does what I asked for, and it works — but it's worth naming the trade it makes, since I'm the one who suggested it.

Verified on the new head: run 31607926461Download SARIF artifact success, this step success (not skipped), and Semgrep OSS shows up as a check on 233e0b04, so the SARIF genuinely reached the dashboard. hashFiles() resolves against GITHUB_WORKSPACE and download-artifact lands there by default, so the path lines up. The implicit success() that GitHub ANDs into any non-status if also stays true, because continue-on-error leaves the job status untouched.

The trade: continue-on-error + a file-existence gate can't tell "no SARIF was ever produced" (benign — the container job already went red for the real reason) apart from "the SARIF existed and was uploaded, but the download failed" (a transient artifact-service or network failure). The first is what we wanted to stop being noisy. The second used to be a red job; now it's a green run with nothing delivered to the dashboard, and no signal anywhere.

I've reasoned that path rather than exercised it — the PR runs only cover the happy path, and I wouldn't ask you to force an artifact-download failure to prove it.

Low stakes either way: the next push or the 6am cron re-uploads, so a missed upload self-heals within a day, and code scanning is not the enforcement gate here. Leaving it as-is is a defensible call. If you'd rather keep the loud signal for the case that deserves it, gating on the producing step is the discriminator — e.g. give the semgrep ci step an id and pass its outcome through a job output, then let a genuine download failure stay red when that step actually ran.

@07souravkunda
07souravkunda marked this pull request as ready for review August 12, 2026 15:37
@07souravkunda
07souravkunda requested a review from a team as a code owner August 12, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants