Share a Semgrep report
Semgrep is fast, the rules are good, and the output is a JSON file. Or a SARIF file. Either way it is a machine artifact, and the humans who have to decide what to do about a finding get a terminal dump pasted into a ticket.
PR annotations cover the changed-lines case well. They cover nothing else: the full-repo backlog scan, the new ruleset somebody is evaluating, the report a customer's security questionnaire asked for. Those need something a person can read at an address.
Convert to HTML
semgrep ci --sarif --output semgrep.sarif
pip install sarif-tools
sarif html semgrep.sarif --output semgrep.html
SARIF is the right intermediate here — it is the format the rest of the security tooling ecosystem already speaks, so the converter you set up for Semgrep also handles CodeQL, Trivy and anything else that emits it.
The result is a single self-contained file.
Publish it
curl -fsS -X PATCH "https://commareports.com/api/v1/reports/$REPORT_ID" \
-H "Authorization: Bearer $COMMA_API_TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --rawfile html semgrep.html \
--arg title "Semgrep — $(git rev-parse --short HEAD)" \
'{title: $title, html: $html}')"
One call, no assets. Use a scoped token
(reports:write) from CI secrets, and PATCH a saved report id so one URL
accumulates a revision per scan instead of a new orphan link every run.
semgrep ci exits non-zero when it finds blocking issues, so make the
publish unconditional — if: always() in GitHub Actions,
after_script in GitLab, post { always { … } } in Jenkins. See
publishing from CI.
Triage, on the finding
This is the part a SARIF file structurally cannot do. Static analysis findings are a queue of decisions, and the decisions are what get lost:
- False positive, because the sink is already sanitised two frames up.
- True but accepted, with a reason and an owner.
- Real, and here is the ticket.
Published to a URL, each of those is a thread anchored to the finding,
surviving across scans. The next person who sees rule
javascript.express.security.audit.xss fire on the same file finds the
previous decision attached to it rather than re-deriving it. See
commenting on HTML.
Revision diffs answer the trend question honestly: two scans, one diff, which findings actually cleared — rather than a count that moved because someone changed the ruleset.
Treat it as sensitive
The report names file paths and usually quotes vulnerable source. Access is per report: private, your team, anyone signed in at your domain, or anyone with the link, with view / comment / edit rights. Default to team or private, and use a link-only URL deliberately when you actually mean to hand it to an auditor. See sharing & access control.
Report HTML renders inside a sandboxed iframe (allow-scripts, no
allow-same-origin), so a finding that quotes an XSS payload cannot reach
anyone's session while you are reading about it. See the
security model.
Limits
- Entry HTML: 5 MB. A first full-repo scan can blow past that — publish by severity, or scope the scan to the paths under review. Assets: 25 MB per file, 250 MB and 500 files total.
- Scripts run, sandboxed — no same-origin access.
- 60 requests/minute per token.
Try it
Comma is free — unlimited reports, unlimited commenters, unlimited revision history.