Two ways to get source into Offense: install the GitHub App and pick one repository, or upload an archive yourself. Both feed the same scan pipeline.

What a code scan is

A scan is a source inspection, not a live assessment. The runner opens a disposable Boat sandbox and runs OSV-Scanner for known dependency vulnerabilities, Gitleaks for committed secrets, and Semgrep for code patterns. The result is findings, a coverage report, a report URL, and ordinary evidence and budget handling.

Repository code and install scripts are never executed. Git submodules and LFS objects are not downloaded.

The GitHub App

Open /app/connections and choose Connect GitHub. That installs the Offense GitHub App on an organization or personal account and then lets you pick one repository. Offense never stores a token: it signs a short-lived RS256 JWT with the App private key and mints installation tokens per request.

What is stored is the installation ID, the account login and type, and the chosen repository with its resolved commit. The App private key never leaves the Worker, and installation tokens are never persisted, rendered, or sent into a sandbox. A cross-site-but-random state cookie binds the callback to the workspace and the pending connection row.

Run sandbox scan resolves the saved ref to a commit, downloads a bounded archive (20 MiB by default, MAX_SCAN_ARCHIVE_BYTES), and dispatches to /scans. Redirects are restricted to GitHub's archive host and the installation token is not forwarded.

App permissions

Permission Needed for
Contents: Read reading the archive
Metadata: Read basic repository facts
Contents: Write opening remediation pull requests
Pull requests: Write opening remediation pull requests
Checks: Write posting pull-request check runs

Set the Setup URL to https://app.offense.sh/app/connections/github/callback (matching PUBLIC_ORIGIN). For pull-request checks set the webhook URL to https://app.offense.sh/webhooks/github subscribed to Pull request events. Required Worker secrets: GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_PRIVATE_KEY (PKCS#1 or PKCS#8 PEM), and GITHUB_APP_WEBHOOK_SECRET for the check.

Run web/db/migrations/0027-github-app-connection.sql and 0034-github-remediation.sql before deploying.

Uploading an archive yourself

POST /api/v1/scans takes the raw .tar.gz as the request body, not as multipart form data:

tar -czf src.tgz -C ./my-app .
curl -sX POST https://app.offense.sh/api/v1/scans \
  -H "Authorization: Bearer $OFFENSE_API_KEY" \
  -H 'content-type: application/gzip' \
  -H 'x-offense-repo: acme/app' \
  -H 'x-offense-commit: 0f97314' \
  --data-binary @src.tgz

Returns 201 (or 200 on an idempotent retry):

{ "scan": { "…assessment fields…" }, "reportUrl": "https://app.offense.sh/app/assessments/…" }
Status error Meaning
400 archive_required body was empty
413 archive_too_large over limitBytes, which is included
402 monthly_plan_required first scan needs a plan; adds checkoutUrl
402 insufficient_credits balance too low
502 scan_dispatch_failed runner refused the job

x-offense-repo and x-offense-commit are optional labels. GET /api/v1/scans/:id returns { scan, findings, reportUrl } with the scans:read scope, and 404 not_found unless the assessment is a code scan in your workspace.

Pre-merge checks in CI

POST /api/v1/checks runs the same policy as the GitHub webhook without GitHub. It needs the checks:create scope.

Pass a change set directly:

{ "files": [{ "filename": "src/app.js", "patch": "@@ …" }], "advisories": [] }

Or point at a pull request on the connected repository with { "repo": "acme/app", "number": 42 }. With files supplied the analysis is returned inline. With a pull request number Offense fetches it using the installed App, then posts one check run and one comment, updated in place.

The policy:

  • scans added lines for secrets,
  • diffs dependency manifests,
  • blocks new critical or high risk,
  • warns on dependency changes.

github_not_connected (409) means no App installation is saved; github_not_configured (503) means the App secrets are missing from the deployment.

Remediation pull requests

POST /api/v1/assessments/:id/findings/:findingId/remediation with findings:remediate opens one draft pull request for a confirmed finding. You supply the fix files; the App commits them on a new branch and opens the pull request. The App never merges and never writes the base branch. A missing write scope is reported as its own error rather than a generic failure, and the finding shows the pull request link once it exists.

When a remediation pull request is merged, Offense retests the findings it opened through the same fresh-sandbox path as a manual retest and marks the remediation merged. An unmerged close does nothing. Nothing is auto-merged.

Not implemented

  • Automatic scans on push.
  • Manifest-driven scanning.
  • Submodule and LFS fetching.