| 1 | --- |
| 2 | title: Code scanning |
| 3 | description: Static analysis results uploaded as SARIF become alerts, pull request comments and a check that can block merges; g1t fixes what they find. |
| 4 | --- |
| 5 | |
| 6 | Code scanning reads the results of static analysis tools: any tool that |
| 7 | writes [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html), |
| 8 | the standard format for them. You run the tool, in a |
| 9 | [workflow](/guides/actions/) or anywhere else, and upload its results. |
| 10 | |
| 11 | - On the **default branch**, each problem is an **alert**, kept across |
| 12 | analyses by its fingerprint, and **fixed** when a later analysis no |
| 13 | longer reports it. |
| 14 | - On a **pull request**, the results that are new to it, on the lines it |
| 15 | changes, become **review comments** and the **Code scanning** check, |
| 16 | which fails at the threshold you choose. Require it in branch protection |
| 17 | and it blocks merges, for people and agents alike. |
| 18 | |
| 19 | Code scanning is free on public repositories and part of the |
| 20 | [Security and quality activation](/guides/security/pricing/) on private |
| 21 | ones. |
| 22 | |
| 23 | ## Set it up |
| 24 | |
| 25 | On a repository's Security page, open **Code scanning** and choose **Set |
| 26 | up code scanning**. g1t opens a pull request, as you, adding |
| 27 | `.g1t/workflows/code-scanning.yml`: |
| 28 | |
| 29 | ```yaml |
| 30 | name: Code scanning |
| 31 | |
| 32 | on: |
| 33 | push: |
| 34 | branches: ["main"] |
| 35 | pull_request: |
| 36 | schedule: |
| 37 | - cron: "27 4 * * 1" |
| 38 | |
| 39 | # The job's token reads the code and uploads the results. |
| 40 | permissions: |
| 41 | contents: read |
| 42 | security-events: write |
| 43 | |
| 44 | jobs: |
| 45 | scan: |
| 46 | name: Code scanning |
| 47 | runs-on: ubuntu-latest |
| 48 | timeout-minutes: 30 |
| 49 | env: |
| 50 | G1T_TOKEN: ${{ secrets.G1T_TOKEN }} |
| 51 | steps: |
| 52 | - uses: actions/checkout@v4 |
| 53 | |
| 54 | - name: Find the languages to scan |
| 55 | id: languages |
| 56 | run: | |
| 57 | found() { [ -n "$(git ls-files -- "$@" | head -n 1)" ]; } |
| 58 | if found '*.py'; then echo "python=true" >> "$GITHUB_OUTPUT"; fi |
| 59 | if found 'go.mod' '*/go.mod'; then echo "go=true" >> "$GITHUB_OUTPUT"; fi |
| 60 | if found '*.js' '*.jsx' '*.mjs' '*.cjs' '*.ts' '*.tsx' '*.mts' '*.cts'; then echo "javascript=true" >> "$GITHUB_OUTPUT"; fi |
| 61 | if found 'Cargo.toml' '*/Cargo.toml'; then echo "rust=true" >> "$GITHUB_OUTPUT"; fi |
| 62 | mkdir -p /tmp/sarif |
| 63 | # Uploads one SARIF file: upload-sarif <file> <category> [<directory its paths are relative to>] |
| 64 | cat > /tmp/upload-sarif <<'SCRIPT' |
| 65 | #!/bin/sh |
| 66 | set -eu |
| 67 | file="$1"; category="$2"; dir="${3:-.}" |
| 68 | if [ "$dir" != "." ]; then |
| 69 | jq --arg prefix "$dir/" '(.runs[]?.results[]?.locations[]?.physicalLocation.artifactLocation |
| 70 | | select(.uri != null and (.uri | test("^(/|[A-Za-z][A-Za-z0-9+.-]*:)") | not)) | .uri) |= $prefix + .' \ |
| 71 | "$file" > "$file.tmp" && mv "$file.tmp" "$file" |
| 72 | fi |
| 73 | ref="$GITHUB_REF" |
| 74 | sha="$GITHUB_SHA" |
| 75 | if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then |
| 76 | ref="refs/pull/$(jq -r .number "$GITHUB_EVENT_PATH")/head" |
| 77 | sha="$(jq -r '.pull_request.head.sha // env.GITHUB_SHA' "$GITHUB_EVENT_PATH")" |
| 78 | fi |
| 79 | gzip -c "$file" | base64 -w0 > "$file.b64" |
| 80 | jq -n --arg sha "$sha" --arg ref "$ref" --arg checkout "file://$GITHUB_WORKSPACE" --arg category "$category" --rawfile sarif "$file.b64" \ |
| 81 | '{commit_sha: $sha, ref: $ref, sarif: $sarif, checkout_uri: $checkout, category: $category}' > "$file.json" |
| 82 | curl --fail-with-body -sS -X POST "$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/code-scanning/sarifs" \ |
| 83 | -H "Authorization: Bearer $G1T_TOKEN" -H "Content-Type: application/json" --data @"$file.json" |
| 84 | echo |
| 85 | SCRIPT |
| 86 | chmod +x /tmp/upload-sarif |
| 87 | |
| 88 | - name: Python (Bandit) |
| 89 | if: steps.languages.outputs.python == 'true' |
| 90 | run: | |
| 91 | python3 -m venv /tmp/bandit && /tmp/bandit/bin/pip install --quiet "bandit[sarif]" |
| 92 | /tmp/bandit/bin/bandit --recursive . --exclude ./.git,./node_modules,./.venv,./venv \ |
| 93 | --format sarif --output /tmp/sarif/python.sarif --exit-zero --quiet |
| 94 | /tmp/upload-sarif /tmp/sarif/python.sarif python |
| 95 | |
| 96 | - name: Go (gosec) |
| 97 | if: steps.languages.outputs.go == 'true' |
| 98 | run: | |
| 99 | go install github.com/securego/gosec/v2/cmd/gosec@latest |
| 100 | gosec="$(go env GOPATH)/bin/gosec" |
| 101 | # Each module on its own, its results under its own category. |
| 102 | for dir in $(git ls-files -- 'go.mod' '*/go.mod' | xargs -n1 dirname); do |
| 103 | out="/tmp/sarif/go-$(echo "$dir" | tr '/.' '__').sarif" |
| 104 | (cd "$dir" && "$gosec" -quiet -no-fail -fmt sarif -out "$out" ./...) |
| 105 | if [ "$dir" = "." ]; then category="go"; else category="go:$dir"; fi |
| 106 | /tmp/upload-sarif "$out" "$category" "$dir" |
| 107 | done |
| 108 | |
| 109 | - name: JavaScript and TypeScript (ESLint) |
| 110 | if: steps.languages.outputs.javascript == 'true' |
| 111 | run: | |
| 112 | mkdir -p /tmp/eslint |
| 113 | npm install --prefix /tmp/eslint --no-audit --no-fund --silent \ |
| 114 | eslint@9 eslint-plugin-security typescript-eslint typescript @microsoft/eslint-formatter-sarif |
| 115 | cat > /tmp/eslint/eslint.config.mjs <<'CONFIG' |
| 116 | import security from "eslint-plugin-security"; |
| 117 | import tseslint from "typescript-eslint"; |
| 118 | |
| 119 | const files = ["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"]; |
| 120 | |
| 121 | export default [ |
| 122 | { ignores: ["**/node_modules/", "**/dist/", "**/build/", "**/coverage/", "**/vendor/", "**/*.min.js"] }, |
| 123 | { files, languageOptions: { parser: tseslint.parser, parserOptions: { ecmaFeatures: { jsx: true } } } }, |
| 124 | { ...security.configs.recommended, files }, |
| 125 | ]; |
| 126 | CONFIG |
| 127 | formatter="$(node -p "require.resolve('@microsoft/eslint-formatter-sarif', { paths: ['/tmp/eslint'] })")" |
| 128 | # 1 is findings; 2 is ESLint failing to run. |
| 129 | /tmp/eslint/node_modules/.bin/eslint --config /tmp/eslint/eslint.config.mjs --no-warn-ignored \ |
| 130 | --format "$formatter" --output-file /tmp/sarif/javascript.sarif . || [ $? -eq 1 ] |
| 131 | /tmp/upload-sarif /tmp/sarif/javascript.sarif javascript |
| 132 | |
| 133 | - name: Rust (Clippy) |
| 134 | if: steps.languages.outputs.rust == 'true' |
| 135 | run: | |
| 136 | cargo install --locked --quiet clippy-sarif |
| 137 | # The workspace at the top, or else each outermost crate. |
| 138 | if [ -f Cargo.toml ]; then |
| 139 | roots="." |
| 140 | else |
| 141 | roots="$(git ls-files -- '*/Cargo.toml' | xargs -n1 dirname | sort | awk 'NR == 1 || index($0 "/", last "/") != 1 { print; last = $0 }')" |
| 142 | fi |
| 143 | for dir in $roots; do |
| 144 | out="/tmp/sarif/rust-$(echo "$dir" | tr '/.' '__').sarif" |
| 145 | (cd "$dir" && cargo clippy --all-targets --message-format=json > /tmp/clippy.json) || true |
| 146 | clippy-sarif < /tmp/clippy.json > "$out" |
| 147 | if [ "$dir" = "." ]; then category="rust"; else category="rust:$dir"; fi |
| 148 | /tmp/upload-sarif "$out" "$category" "$dir" |
| 149 | done |
| 150 | ``` |
| 151 | |
| 152 | It looks at which languages the repository has, and scans each one with a |
| 153 | scanner made for it: |
| 154 | |
| 155 | | Language | Scanner | Category | |
| 156 | | --- | --- | --- | |
| 157 | | Python | [Bandit](https://bandit.readthedocs.io) | `python` | |
| 158 | | Go | [gosec](https://securego.io), each module on its own | `go`, or `go:<directory>` for a module below the top | |
| 159 | | JavaScript and TypeScript | [ESLint](https://eslint.org) with [eslint-plugin-security](https://www.npmjs.com/package/eslint-plugin-security), written as SARIF by `@microsoft/eslint-formatter-sarif` | `javascript` | |
| 160 | | Rust | [Clippy](https://doc.rust-lang.org/clippy/), written as SARIF by `clippy-sarif` | `rust`, or `rust:<directory>` for a crate below the top | |
| 161 | |
| 162 | A language the repository does not have is skipped. Each scanner installs |
| 163 | from its language's own registry (PyPI, the Go module proxy, npm or |
| 164 | crates.io) when the job runs. Every pull request, every push to the |
| 165 | default branch and a weekly run are scanned, and each language's results |
| 166 | are uploaded on their own, under their own category, so an alert is fixed |
| 167 | only by a later analysis of the same language. Merge the pull request to |
| 168 | start. Setting it up takes the Maintain role, as other workflows do. |
| 169 | |
| 170 | To change what is scanned, edit the workflow: pass a scanner its own |
| 171 | options or rules, or add another tool that writes SARIF with |
| 172 | `/tmp/upload-sarif <file> <category>`. Give each tool, or each run of one |
| 173 | tool, its own `category`. |
| 174 | |
| 175 | A pull request from a fork runs without the workspace's token, so its |
| 176 | upload is refused and its results are not shown. Its workflow fails at the |
| 177 | upload step. |
| 178 | |
| 179 | ## Uploading SARIF |
| 180 | |
| 181 | `POST /repos/{owner}/{name}/code-scanning/sarifs`, with a token that has |
| 182 | `security:write` (a workflow's `G1T_TOKEN` does, with `security-events: write` |
| 183 | in its [`permissions:`](/guides/actions/#the-jobs-token), as the workflow |
| 184 | g1t writes has): |
| 185 | |
| 186 | | Field | | |
| 187 | | --- | --- | |
| 188 | | `commit_sha` | The full hash of the commit analysed. | |
| 189 | | `ref` | `refs/heads/<branch>`, or `refs/pull/<number>/head` (or `/merge`) for a pull request. | |
| 190 | | `sarif` | The SARIF file gzipped, then base64-encoded: `gzip -c results.sarif \| base64 -w0`. At most 10 MB encoded and 40 MB unzipped. | |
| 191 | | `tool_name` | Optional: another name for the tool, when the file has one run. | |
| 192 | | `category` | Optional: which analysis this is. Default: the run's `automationDetails.id` up to its last `/`, or the tool's name. | |
| 193 | | `checkout_uri` | Optional: where the files were checked out (`file:///home/runner/work/repo`), so absolute paths become repository paths. | |
| 194 | |
| 195 | The upload is read at once. The answer's `processing_status` is |
| 196 | `complete` or `failed`, with `errors` saying why; `GET |
| 197 | /repos/{owner}/{name}/code-scanning/sarifs/{id}` returns it again. Up to 20 |
| 198 | runs and 5,000 results are read from one upload; results past that are |
| 199 | counted in the analysis's `dropped` and listed in `errors`. |
| 200 | |
| 201 | ### How results become alerts |
| 202 | |
| 203 | Each result is fingerprinted, so the same problem found by the next |
| 204 | analysis is the same alert: |
| 205 | |
| 206 | - A tool's own fingerprint (`partialFingerprints`, such as |
| 207 | `primaryLocationLineHash`) is used when it gives one: it survives the |
| 208 | line moving. |
| 209 | - Otherwise, the rule, the file and the code the result points at (its |
| 210 | snippet, or its message), so editing elsewhere in the file does not make |
| 211 | it a new alert. Identical results in one file are told apart by their |
| 212 | order. |
| 213 | |
| 214 | For each tool and category, an analysis of the default branch: |
| 215 | |
| 216 | | The result | The alert | |
| 217 | | --- | --- | |
| 218 | | Not seen before | Opens, numbered in the repository | |
| 219 | | Seen before | Refreshed: its message, severity and location | |
| 220 | | Fixed before, found again | Opens again | |
| 221 | | Dismissed, found again | Stays dismissed | |
| 222 | | Not reported any more | Fixed | |
| 223 | |
| 224 | Suppressed results (a tool's in-source suppressions) are not alerts. |
| 225 | |
| 226 | ### Severity |
| 227 | |
| 228 | An alert's **severity** is its rule's **security severity** when the rule |
| 229 | has a `security-severity` score (9.0 and up critical, 7.0 high, 4.0 medium, |
| 230 | above 0 low); otherwise it comes from the result's level: `error` high, |
| 231 | `warning` medium, `note` low. |
| 232 | |
| 233 | ## On pull requests |
| 234 | |
| 235 | An upload for `refs/pull/<number>/head` is compared with the default |
| 236 | branch: |
| 237 | |
| 238 | - A result whose fingerprint is open on the default branch is not the pull |
| 239 | request's own. |
| 240 | - A result new to the pull request, on a line it adds, is left as a |
| 241 | **review comment** on that line, once. |
| 242 | - The **Code scanning** check is set on the pull request's head commit: |
| 243 | it fails when a new result on a changed line reaches the threshold in the |
| 244 | repository's **Security settings**: |
| 245 | |
| 246 | | Code scanning results | Fails on | |
| 247 | | --- | --- | |
| 248 | | Never | Nothing | |
| 249 | | Errors only | Results the tool calls errors | |
| 250 | | Critical, and errors | Errors, and security results of critical severity | |
| 251 | | High or higher, and errors (the default) | Errors, and security results of high or critical severity | |
| 252 | | Medium or higher, and errors | Errors, and security results of medium severity or worse | |
| 253 | | Any security result, and errors | Errors, and any security result | |
| 254 | |
| 255 | The check links to `g1t.sh/<owner>/<project>/security/pulls/<number>`, |
| 256 | which lists what it found. To block merges on it, require **Code |
| 257 | scanning** under **Settings → Branches** once it has reported on a pull |
| 258 | request, as for any [required check](/guides/pull-requests/). The merge |
| 259 | queue waits for it the same way. |
| 260 | |
| 261 | ## Alerts |
| 262 | |
| 263 | `g1t.sh/<owner>/<project>/security/code-scanning` lists alerts, open |
| 264 | first and worst first, with recent analyses below. Filter by state, |
| 265 | severity and tool. Each alert's page has its rule's help, its location, |
| 266 | the analyses that reported it and its actions: |
| 267 | |
| 268 | | Action | Needs | What happens | |
| 269 | | --- | --- | --- | |
| 270 | | **Dismiss** | Write | With a reason: **False positive** (`false_positive`), **Won't fix** (`wont_fix`) or **Used in tests** (`used_in_tests`), and an optional comment. | |
| 271 | | **Reopen** | Write | A dismissed alert opens again. | |
| 272 | | **Fix with g1t** | Write, and agents allowed to run | Opens an issue assigned to g1t. | |
| 273 | |
| 274 | ### Fix with g1t |
| 275 | |
| 276 | **Fix with g1t** opens an issue with the alert's rule, message, location |
| 277 | and help, and a definition of done: the tool no longer reports the rule |
| 278 | there, and the tests still pass. g1t takes it as you, writes the fix in a |
| 279 | pull request and lands it through your required checks (including Code |
| 280 | scanning) and merge queue, like any agent's work. Asking again while the |
| 281 | issue is open goes to it. The run is charged as |
| 282 | [agent usage](/guides/usage-and-billing/). |
| 283 | |
| 284 | ## API and MCP |
| 285 | |
| 286 | | Route | What it does | Scope | |
| 287 | | --- | --- | --- | |
| 288 | | `GET /repos/{owner}/{name}/code-scanning/alerts` | Lists alerts; filter with `state`, `severity`, `tool`, `rule_id`. | `security:read` | |
| 289 | | `GET /workspaces/{workspace}/code-scanning/alerts` | The same across a workspace. | `security:read` | |
| 290 | | `GET /repos/{owner}/{name}/code-scanning/alerts/{number}` | One alert, with its activity and analyses. | `security:read` | |
| 291 | | `PATCH /repos/{owner}/{name}/code-scanning/alerts/{number}` | `state` `dismissed` with `dismissed_reason` and `dismissed_comment`, or `open`. | `security:write` | |
| 292 | | `GET /repos/{owner}/{name}/code-scanning/analyses` | Analyses, newest first. | `security:read` | |
| 293 | | `POST /repos/{owner}/{name}/code-scanning/sarifs` | Uploads SARIF. | `security:write` | |
| 294 | | `GET /repos/{owner}/{name}/code-scanning/sarifs/{id}` | One upload. | `security:read` | |
| 295 | | `POST /repos/{owner}/{name}/security/alerts/{id}/fix` | Fix with g1t, for a code scanning, vulnerability or secret alert. | `security:write`, with `issues:write` and `agents:run` | |
| 296 | |
| 297 | Over MCP, the [`security` tool](/reference/mcp/#security) has |
| 298 | `code_alerts`, `code_alert`, `update_code_alert`, `analyses`, |
| 299 | `upload_sarif`, `sarif_upload` and `fix`. Webhooks: |
| 300 | `code_scanning_alert.created`, `.fixed`, `.dismissed` and `.reopened`. |