SARIF Integration

Push the SARIF output of any code-scanning tool (CodeQL, Semgrep, Snyk, Checkmarx, Trivy, Bandit, GitLab SAST, OWASP ZAP) at SecureCodingHub. Every unique CWE in the run becomes an assignment on the commit author's account — usually within seconds of the merge.

Why CI is the right trigger

Generic "every developer takes the same OWASP Top 10 course" rollouts have low completion and lower retention. The CI integration flips the model: training is assigned specifically to the developer who introduced an issue, specifically on the vulnerability class they need, with a 14-day deadline tied to the merge that exposed it. That's what auditors call just-in-time secure-coding training.

How it works end-to-end

1

Your SAST tool produces a SARIF 2.1.0 file as part of a CI job.

2

A CI step POSTs the file to /api/public/v1/sarif with commit metadata as request headers.

3

SecureCodingHub parses every result, extracts its CWE tags, and looks each one up in our internal CWE-to-topic map.

4

For each unique mapped topic, an assignment is created on the user whose email matches X-Commit-Author-Email, with a 14-day deadline.

5

A sarif.ingested webhook fires with the same payload the synchronous response returns.

The endpoint

POST /api/public/v1/sarif
Authorization: Bearer scs_live_…
Content-Type: application/json
X-Source: github-codeql
X-Repo: acme/payments-api
X-Branch: main
X-Commit-Sha: 9c2f4b8e0c1d…
X-Commit-Author-Email: jane.smith@acme.com

<SARIF 2.1.0 JSON body, up to 10 MB>

Required scope: sarif:ingest.

Request headers

HeaderRequiredMeaning
X-SourceFree-form tool identifier — e.g. github-codeql, snyk, semgrep-ci. Recorded for audit.
X-RepoRepository slug (owner/name). Recorded for audit.
X-BranchBranch name. Recorded for audit.
X-Commit-SharecommendedCommit hash. Used together with X-Repo for 24-hour idempotency.
X-Commit-Author-EmailrecommendedEmail of the commit author. Resolved to a SecureCodingHub user; assignments are attached to them. If the email doesn't match a user, the run still ingests but no assignments are created.

Response

{
  "ingestionId": "7e4c9d09-86ca-4125-bbd8-7fae54c8f654",
  "findingsCount": 2,
  "uniqueCwes": ["CWE-79", "CWE-89"],
  "unmappedCwes": [],
  "assignmentsCreated": 12,
  "notes": []
}
  • findingsCount — total number of result entries parsed.
  • uniqueCwes — set of CWEs mentioned across all findings.
  • unmappedCwes — CWEs we could not map to a topic. Send to support if you want coverage added.
  • assignmentsCreated — count of new assignment records (existing duplicates are skipped).
  • notes — non-fatal warnings (e.g. invalid commit author email format).

Idempotency

Re-ingesting the same SARIF file is safe. The tuple (organization, repo, commit_sha) is the idempotency key — a duplicate inside a 24-hour window returns 200 with the original ingestionId and assignmentsCreated: 0. This means CI workflows that re-run on the same commit (rebase, manual rerun, push to PR) do not pile up duplicate assignments.

GitHub Actions example

Drop this step in a workflow after your CodeQL or Semgrep job has produced results.sarif:

- name: Send SARIF to SecureCodingHub
  if: always()
  run: |
    curl -sS -f -X POST \
      -H "Authorization: Bearer $SCH_API_KEY" \
      -H "Content-Type: application/json" \
      -H "X-Source: github-codeql" \
      -H "X-Repo: ${{ github.repository }}" \
      -H "X-Branch: ${{ github.ref_name }}" \
      -H "X-Commit-Sha: ${{ github.sha }}" \
      -H "X-Commit-Author-Email: ${{ github.event.head_commit.author.email }}" \
      --data-binary @results.sarif \
      https://api.limeplate.com/api/public/v1/sarif
  env:
    SCH_API_KEY: ${{ secrets.SCH_API_KEY }}

Store the key in repository or organization secrets. The if: always() guard ensures findings are sent even if the security job marked the build as failed — which is when you most want training assignments to fire.

GitLab CI example

send_sarif_to_sch:
  stage: post-security
  image: curlimages/curl:latest
  needs: ["sast"]
  script:
    - >
      curl -sS -f -X POST
      -H "Authorization: Bearer $SCH_API_KEY"
      -H "Content-Type: application/json"
      -H "X-Source: gitlab-sast"
      -H "X-Repo: $CI_PROJECT_PATH"
      -H "X-Branch: $CI_COMMIT_BRANCH"
      -H "X-Commit-Sha: $CI_COMMIT_SHA"
      -H "X-Commit-Author-Email: $GITLAB_USER_EMAIL"
      --data-binary @gl-sast-report.json
      https://api.limeplate.com/api/public/v1/sarif
  rules:
    - if: $CI_COMMIT_BRANCH

The GitLab SAST report is SARIF-compatible — no conversion needed.

Semgrep example

semgrep ci --sarif --output results.sarif
curl -sS -f -X POST \
  -H "Authorization: Bearer $SCH_API_KEY" \
  -H "X-Source: semgrep" \
  -H "X-Repo: $REPO" \
  -H "X-Commit-Sha: $COMMIT" \
  -H "X-Commit-Author-Email: $AUTHOR_EMAIL" \
  --data-binary @results.sarif \
  https://api.limeplate.com/api/public/v1/sarif

CWE coverage

SecureCodingHub currently maps the OWASP Top 10, OWASP API Security Top 10, OWASP Mobile Top 10, and the CWE Top 25 to training topics. Findings tagged with CWE IDs outside that set appear in the unmappedCwes list of the response. Common formats we accept on the SARIF tag side:

  • external/cwe/cwe-89 (CodeQL standard)
  • cwe-89
  • CWE-89
  • cwe:89

The tags are read from both result.properties.tags and the corresponding tool.driver.rules[].properties.tags.

Body size cap

The SARIF endpoint accepts payloads up to 10 MB uncompressed. Most real-world SAST reports for a single repository fall well under this. If you have an enterprise-scale monorepo report that exceeds the cap, split by tool driver (one SARIF run per driver) and POST each part — they will be idempotent under the same commit SHA.

Failure modes

SymptomLikely cause
200 with assignmentsCreated: 0The commit author email isn't a user in your organization, or all CWEs were already assigned.
200 with unmappedCwes non-emptyFindings reference CWEs that have no training topic yet. Forward the list to support.
400 empty_bodyCurl was missing --data-binary @file.sarif.
413 body_too_largeSARIF exceeds 10 MB. Split by driver.
200 with a notes entry starting idempotency:The same (repo, commit) was already ingested within 24 h. The response is otherwise identical to a fresh ingestion with assignmentsCreated: 0 — safe to ignore.