Skip to content
300 linesCodeBlameRaw
1---
2title: Code scanning
3description: 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
6Code scanning reads the results of static analysis tools: any tool that
7writes [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html),
8the 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
19Code scanning is free on public repositories and part of the
20[Security and quality activation](/guides/security/pricing/) on private
21ones.
22
23## Set it up
24
25On a repository's Security page, open **Code scanning** and choose **Set
26up code scanning**. g1t opens a pull request, as you, adding
27`.g1t/workflows/code-scanning.yml`:
28
29```yaml
30name: Code scanning
31
32on:
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.
40permissions:
41 contents: read
42 security-events: write
43
44jobs:
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
152It looks at which languages the repository has, and scans each one with a
153scanner 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
162A language the repository does not have is skipped. Each scanner installs
163from its language's own registry (PyPI, the Go module proxy, npm or
164crates.io) when the job runs. Every pull request, every push to the
165default branch and a weekly run are scanned, and each language's results
166are uploaded on their own, under their own category, so an alert is fixed
167only by a later analysis of the same language. Merge the pull request to
168start. Setting it up takes the Maintain role, as other workflows do.
169
170To change what is scanned, edit the workflow: pass a scanner its own
171options or rules, or add another tool that writes SARIF with
172`/tmp/upload-sarif <file> <category>`. Give each tool, or each run of one
173tool, its own `category`.
174
175A pull request from a fork runs without the workspace's token, so its
176upload is refused and its results are not shown. Its workflow fails at the
177upload 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`
183in its [`permissions:`](/guides/actions/#the-jobs-token), as the workflow
184g1t 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
195The 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
198runs and 5,000 results are read from one upload; results past that are
199counted in the analysis's `dropped` and listed in `errors`.
200
201### How results become alerts
202
203Each result is fingerprinted, so the same problem found by the next
204analysis 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
214For 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
224Suppressed results (a tool's in-source suppressions) are not alerts.
225
226### Severity
227
228An alert's **severity** is its rule's **security severity** when the rule
229has a `security-severity` score (9.0 and up critical, 7.0 high, 4.0 medium,
230above 0 low); otherwise it comes from the result's level: `error` high,
231`warning` medium, `note` low.
232
233## On pull requests
234
235An upload for `refs/pull/<number>/head` is compared with the default
236branch:
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
255The check links to `g1t.sh/<owner>/<project>/security/pulls/<number>`,
256which lists what it found. To block merges on it, require **Code
257scanning** under **Settings → Branches** once it has reported on a pull
258request, as for any [required check](/guides/pull-requests/). The merge
259queue waits for it the same way.
260
261## Alerts
262
263`g1t.sh/<owner>/<project>/security/code-scanning` lists alerts, open
264first and worst first, with recent analyses below. Filter by state,
265severity and tool. Each alert's page has its rule's help, its location,
266the 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
277and help, and a definition of done: the tool no longer reports the rule
278there, and the tests still pass. g1t takes it as you, writes the fix in a
279pull request and lands it through your required checks (including Code
280scanning) and merge queue, like any agent's work. Asking again while the
281issue 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
297Over 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`.