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.

Publish a Semgrep report →

Related