Knowledge / Technical Archive / ARC-003
Four failures to a green Semgrep: fixing the SAST job in my CI
The Semgrep check failed on every run. What each failure meant, how I fixed it, and what I would read first next time.
I added Semgrep to this site's CI to run static analysis (SAST) on every push to main. It failed on every run. This is what each failure was, in the order they appeared, and what finally made the check green.
I worked through it with an AI coding assistant. The repository is private and the assistant could not read the CI logs, so I sent it each error as a screenshot and every fix below was first reproduced on my machine.
Where it started
The workflow runs semgrep scan with rulesets for the OWASP Top Ten, the CWE Top 25, security audit, Next.js, React, TypeScript and Dockerfiles. It uses --error, so the job fails when Semgrep finds anything, and it was meant to hand the results to GitHub's Security tab as a SARIF file.
Failure 1: the scan itself found 8 things
I could not read the log, so I installed Semgrep in a throwaway virtual environment and ran the same command. It found 8 issues, all with WARNING severity:
- 7 uses of a mutable tag in GitHub Actions, in both workflows (
actions/checkout@v4,docker/login-action@v3and the like). A tag can be moved by the action's owner, so the code that runs in my pipeline could change without any change in my repository. Semgrep's rule isgithub-actions-mutable-action-tag. - 1 dynamic
urllibcall in the script that generates the brand SVGs (dynamic-urllib-use-detected).
The fix for the actions: pin each one to the full commit SHA that its tag pointed at, and keep the version in a comment. I read the SHAs from the official repositories with git ls-remote, so the code that runs is the same as before, only now it cannot change silently.
The fix for the script took three tries. I first replaced the URL with a dictionary of fixed URLs. Semgrep still flagged it, because a value looked up by a variable counts as dynamic. I then wrote the URLs as literals, and it was flagged again: the rule objects to any non-literal argument, and the destination path was one. The honest fix was to remove the download from the script. It now expects the two font files in a folder, and the README shows the curl commands to get them. A script that draws a logo does not need network code.
Failure 2: Invalid SARIF
With the findings gone, the job still failed, now with Invalid SARIF. JSON syntax error ... Unexpected end of JSON input. The scan had passed. The step that uploads the results was receiving a broken file.
I reproduced it locally. The workflow combined --sarif --output semgrep.sarif --text, and that combination leaves the file empty. The fix was a single flag, --sarif-output=semgrep.sarif, which writes a valid SARIF file and keeps the readable report in the log. While I was there I moved the upload action to v4, because the warning on the run said v3 would be retired in December 2026.
Failure 3: Resource not accessible by integration
Next the upload step failed with this message, pointing to the "get a workflow run" API. In a private repository the upload action reads the workflow run, and that needs the actions: read permission, which my workflow did not declare. GitHub's documentation lists it for private repositories. I added it.
Failure 4: Code scanning is not enabled for this repository
This was the real wall. Sending SARIF to the Security tab is code scanning, and GitHub's troubleshooting page for this error says that private repositories need GitHub Code Security enabled. I had not enabled it, so no change to the workflow could make that upload work.
The options were to enable the feature, to make the repository public, or to stop uploading to the Security tab. I did not check what enabling it costs, and I do not want to make the repository public. I chose the third. The workflow now saves the SARIF file as a run artifact with actions/upload-artifact, pinned by SHA, kept for 30 days, and the step fails if the file is missing. Since nothing is uploaded to code scanning any more, I also removed the actions: read and security-events: write permissions I had just added. The job keeps only contents: read. The pipeline article on this site said the findings went to the Security tab, so I corrected it in both languages.
One more pin
The job runs in the semgrep/semgrep:latest container, which is the same kind of mutable reference I had just removed from the actions. I pinned it by digest. Before committing, I pulled that exact image with Podman and ran the workflow's command inside it: Semgrep 1.182.1, no findings, exit code 0.
One detail I noticed and have not investigated: inside that image Semgrep ran 323 rules, and the copy I had installed with pip ran 1,517. The CI uses the image, so the check on the site covers the smaller set.
Result
Both workflows are green: the Semgrep job passes and the image still builds and deploys.
What I would do differently
- Read the private-repository requirements first. Failures 3 and 4 are both documented. I fixed them one at a time, and I would have saved two rounds by reading GitHub's page on uploading SARIF before the first upload.
- Reproduce before guessing. Running the exact command locally, and later inside the exact image, turned each vague red mark into a specific cause.
- Do not trick a rule to make it quiet. The
urllibfinding was a reminder that the cleaner fix was less code, not a cleverer way of writing it.
What it still does not do
- The check does not block anything. It runs after the push, so a commit with a finding still reaches
mainand only turns the run red. Making it a required check is still planned. - Pinned versions do not update themselves. The actions and the Semgrep image are frozen until I change them by hand, and I have not set up Dependabot.
- The SARIF file is only an artifact. I have to open the run and download it to read the findings in a viewer.
- A clean result from 323 rules is not proof that the code is secure. It is one layer, and the pipeline article lists what is still missing: scanning of the image and the dependencies, a software bill of materials and dynamic testing.