Skip to content

g1t/apps/docs/src/content/docs/guides/security.mdx

371 lines16,431 bytesCodeBlame
1---
2title: Security
3description: How g1t keeps secrets out of your repositories, finds vulnerable dependencies, and opens the pull requests that fix them.
4---
5
6import Aside from '../../../components/Aside.astro';
7
8Every project has a **Security** page at `g1t.sh/<owner>/<project>/security`.
9It shows what g1t has found, and what is being done about each alert:
10
11- **Secrets.** A push that adds a key or a token is refused before it lands,
12 and each repository's history is scanned once, in the background.
13- **Dependencies.** Every package your lockfiles resolve is checked against
14 the [OSV](https://osv.dev) database of known vulnerabilities. For each
15 vulnerable package that has a fix, g1t opens a pull request that raises it
16 to the fixed version, and the pull request lands through your branch's
17 required checks and merge queue.
18
19Alerts are the workspace's to fix, so the Security page needs a
20[role](/guides/access-and-roles/) on the repository, whether the project
21is public or private:
22
23| | Needs |
24| --- | --- |
25| See alerts, **Re-scan now** | Write |
26| **Dismiss** or **Reopen** a dependency alert | Write |
27| **Security updates** on or off | Maintain |
28| **Dismiss** or **Reopen** a secret alert | Admin |
29
30Someone with Read or Triage is told the page needs Write. The workspace's
31own page, `g1t.sh/<owner>/-/security`, lists the open alerts of every
32project the member can see them on, most severe first.
33
34## The overview
35
36The top of the page counts open alerts by severity: critical, high,
37medium, low and unrated. The counts are:
38
39- each open vulnerability, by its severity, and
40- each secret in the repository's history that looks real, as critical.
41
42A secret that a push carried but that never landed (push blocked) is not
43counted, and neither is a [likely test value](#likely-test-values). A
44dismissed alert is not counted. Below the counts:
45
46- **Re-scan now** reads the dependencies again at once and scans the
47 history again from the start.
48- **Security updates** turns [security updates](#security-updates) on or off
49 for the project. It is on unless someone turns it off.
50
51The **Secrets** and **Dependencies** tabs list each alert. Filter either
52tab by state:
53
54| State | Meaning |
55| --- | --- |
56| Open | Needs attention. |
57| Dismissed | Someone dismissed it with a reason. |
58| Fixed | The secret was revoked, or the vulnerable version is no longer resolved by any lockfile. |
59
60## Secret scanning
61
62g1t looks for credentials whose format their issuer made recognisable, so a
63match is nearly always a real secret or a fake made to look like one:
64
65| What | What it looks like |
66| --- | --- |
67| AWS access keys | `AKIA`, `ASIA`, `ABIA` or `ACCA` and 16 more characters |
68| AWS secret access keys | 40 characters on a line that names an AWS secret |
69| GitHub tokens | `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, `github_pat_` |
70| GitLab tokens | `glpat-`, `gloas-`, `glrt-`, `glptt-`, `gldt-` |
71| Stripe live keys | `sk_live_`, `rk_live_` (test keys are left alone) |
72| Slack tokens | `xoxb-`, `xoxp-`, `xoxa-`, `xoxr-`, `xoxs-`, `xoxe-` |
73| Slack webhooks | `https://hooks.slack.com/services/` addresses |
74| Google API keys | `AIza` and 35 more characters |
75| Anthropic API keys | `sk-ant-` |
76| OpenAI API keys | `sk-proj-`, `sk-svcacct-`, `sk-admin-`, and older `sk-` keys |
77| Private keys | A PEM `BEGIN … PRIVATE KEY` header followed by the key |
78| Service-role JWTs | A JWT whose claims carry `service_role` |
79| npm tokens | `npm_` |
80| g1t tokens | `g1t_` and 40 hex characters |
81| SendGrid keys | `SG.` and two dotted parts |
82
83Placeholders made of a handful of characters, such as `ghp_xxxxxxxx…`, are
84not reported. Lockfiles, and files under `node_modules/` and `vendor/`, are
85not scanned.
86
87g1t never stores a secret it finds. It keeps a fingerprint, so it can
88recognise the same secret again, and a short preview, such as `AKIA…`, so
89you can recognise it.
90
91### Likely test values
92
93g1t judges each value it finds by the value alone, never by the file it is
94in: a key under `tests/` is as real as one under `src/` if it was ever
95issued. A value is a **likely test value** when it:
96
97- is a key its issuer publishes as an example, such as AWS's documented
98 example keys;
99- says it is an example: it contains `example`, `sample`, `dummy`, `fake`,
100 `placeholder`, `changeme`, `notreal`, `redacted` or `xxxxx`;
101- counts up for six or more characters in a row, like `abcdef` or `123456`;
102- repeats one character five or more times in a row;
103- is a short piece repeated; or
104- has too little randomness to be a real key.
105
106A likely test value is still listed, in its own group on the **Secrets**
107tab, with the reason. It never stops a push and is never counted as
108critical. Private keys are judged by the words only.
109
110### Push protection
111
112When a push over HTTPS adds a secret, g1t refuses the whole push and nothing
113is stored. Only the lines the push adds are checked, so a secret that is
114already in the repository does not block every later push to the same file.
115This applies to every push, including the pushes g1t and its agents make to
116pull requests.
117
118Very large pushes are scanned after they land, not before. Such a push is
119too large to read in full before it is stored, so it goes through, and g1t
120then scans every commit it added, on whichever branch, in the
121background. A secret found there is an open alert, as if it had been found
122in history, and the workspace's owners get an email when one looks real.
123To have a large push checked before it lands, push it in parts, as
124[Size limits](/guides/git/#size-limits) shows.
125
126Git lists every secret it found, by file and line, with a link to allow
127each one:
128
129```text
130$ git push
131remote: g1t found a secret in this push, so nothing was pushed.
132remote:
133remote: config/prod.env:3 an AWS access key (commit 4807077)
134remote:
135remote: Take the secret out of the commit that adds it (git commit --amend, or
136remote: git rebase -i for an older commit), rotate it if it was ever real, and
137remote: push again.
138remote:
139remote: If it is not a real secret, such as a test fixture:
140remote: - add g1t:allow-secret in a comment on its line, or
141remote: - allow it once at https://g1t.sh/acme/rocket/security?tab=secrets&finding=sec_…
142remote: Allowing is recorded with your name, then the same push goes through.
143To https://g1t.sh/acme/rocket.git
144 ! [remote rejected] main -> main (secret found: config/prod.env:3 has an AWS access key)
145```
146
147To fix a real secret:
148
1491. Remove it from the commit that adds it: `git commit --amend` for the last
150 commit, `git rebase -i` for an older one.
1512. Rotate it with whoever issued it. Once a secret has been on any machine
152 but yours, treat it as known.
1533. Push again.
154
155### Letting a secret through
156
157A fake key that does not look made up is still reported, on purpose: it
158looks exactly like a real one. Two ways to let it through:
159
160- **Mark the line.** Put `g1t:allow-secret` anywhere on the line, usually in
161 a comment. The line is never reported, in any push or in history.
162
163 ```ts
164 const FIXTURE_KEY = "AKIA…"; // g1t:allow-secret
165 ```
166
167- **Dismiss the alert.** Open the link in git's message (or the alert on the
168 **Secrets** tab), choose **Dismiss** with the reason **False positive** or
169 **Used in tests**, and push again, unchanged: that secret no longer stops
170 a push to this project.
171
172Dismissing a secret alert needs the Admin [role](/guides/access-and-roles/)
173on the repository. Someone without it sees the same message and asks
174someone who has it.
175
176### Secrets in history
177
178The first time g1t sees a repository (when it is created, on its next push
179to the default branch, or when its Security page is first opened) it scans
180the default branch's history in the background, a page of commits at a
181time, comparing each commit with its first parent. The page shows how far
182it has got. Scanning is metered to the workspace like other usage, and it
183pauses if the workspace reaches its spending limit.
184
185A secret found in history is open: it is in the repository, and anyone
186who could clone it may have it. Rotate it, then dismiss it as **Revoked**.
187Removing it from the code is not enough, since it stays in history.
188
189### Dismissing a secret alert
190
191Choose **Dismiss**, pick a reason, and add a comment if you like (up to 500
192characters):
193
194| Reason | API value | What happens |
195| --- | --- | --- |
196| False positive | `false_positive` | Dismissed. Pushes carrying the secret go through. |
197| Used in tests | `used_in_tests` | Dismissed. Pushes carrying the secret go through. |
198| Revoked | `revoked` | Fixed. The secret was real and has been revoked or rotated. |
199| Won't fix | `wont_fix` | Dismissed. Pushes carrying the secret go through. |
200
201Who dismissed it, when, the reason and the comment are kept in the alert's
202activity log. **Reopen** puts the alert back to open; a secret that never
203landed goes back to blocking pushes.
204
205## Dependencies
206
207g1t reads these lockfiles on the default branch, up to four directories
208deep, skipping `node_modules`, `vendor`, `target`, `dist` and `build`:
209
210| Ecosystem | Files |
211| --- | --- |
212| npm | `package-lock.json`, `pnpm-lock.yaml`, `yarn.lock` |
213| Cargo | `Cargo.lock` |
214| Go | `go.mod` (or `go.sum` where there is no `go.mod`) |
215| Python | `poetry.lock`, and pinned lines (`name==1.2.3`) in `requirements.txt` |
216
217It reads them on every push to the default branch and again every day, and
218asks OSV about every package at the exact version locked. Each advisory
219found is listed with its id (its GHSA id when it has one), severity, the
220version that fixes it, and the lockfile that resolves the vulnerable
221version. A vulnerability that a later scan no longer finds is marked fixed.
222
223### Dismissing a dependency alert
224
225Dismissing a dependency alert needs the Write role. Pick a reason, and add
226a comment if you like (up to 500 characters):
227
228| Reason | API value |
229| --- | --- |
230| A fix has already been started | `fix_started` |
231| No bandwidth to fix this | `no_bandwidth` |
232| Risk is tolerable to this project | `tolerable_risk` |
233| This alert is inaccurate or incorrect | `inaccurate` |
234| Vulnerable code is not actually used | `not_used` |
235
236A dismissed vulnerability that a later scan finds again stays dismissed
237until someone reopens it. The activity log keeps who dismissed it, when,
238the reason and the comment.
239
240### When there is no fix yet
241
242When no patched version has been published, the alert links the advisory
243and nothing is opened for it. You can dismiss it as **Risk is tolerable to
244this project**. g1t checks your dependencies again every day, fetching
245OSV's record for each advisory without a fix again, and opens a security
246update as soon as a fix is published.
247
248## Security updates
249
250With **Security updates** on, g1t opens a pull request to upgrade each
251vulnerable dependency that has a fix. It lands through your branch's
252required checks.
253
2541. g1t starts a sandbox, which raises the package to the fixed version in
255 each lockfile that resolves it, using the ecosystem's own tool.
2562. It commits as `g1t <g1t@users.noreply.g1t.sh>` and pushes a branch named
257 `g1t/security/<package>-<version>`.
2583. It opens a pull request, authored by `g1t`, that names the advisories it
259 fixes, the old and new versions, and the lockfiles it changed.
2604. The pull request merges through the branch's
261 [required status checks](/guides/pull-requests/#required-status-checks)
262 and the [merge queue](/guides/merge-queue/), like any other. When it
263 merges, the next scan finds the vulnerability fixed.
264
265Each alert shows its update's status and links the pull request:
266
267| Status | Meaning |
268| --- | --- |
269| Making the change | The sandbox is raising the version. |
270| Open | The pull request is open. |
271| Merged | The pull request merged. |
272| Closed | The pull request was closed without merging. |
273| Superseded | A newer security update for the same package replaced it, or the package is no longer vulnerable. |
274| Needs code changes | Raising the version was not enough; an agent is working on it. |
275| Failed | The update could not be made. |
276
277A newer security update for the same package closes the older pull request
278as superseded. So does the package no longer being vulnerable.
279
280How each lockfile is changed:
281
282| Lockfile | How the version is raised |
283| --- | --- |
284| `package-lock.json` | `npm install --package-lock-only --ignore-scripts` for a direct dependency, keeping its range style; for an indirect one, `npm update`, then an `overrides` entry if that is not enough |
285| `pnpm-lock.yaml` | `pnpm update --lockfile-only`, then `pnpm.overrides` if needed |
286| `yarn.lock` | `yarn up` (Yarn 2 and later) or `yarn upgrade` (Yarn 1), then `resolutions` if needed |
287| `Cargo.lock` | `cargo update -p <package> --precise <version>` |
288| `go.mod`, `go.sum` | `go get <module>@<version>`, then `go mod tidy` |
289| `poetry.lock` | `poetry add <package>@^<version> --lock`, or `poetry update --lock` for an indirect one |
290| `requirements.txt` | The `==` pin is rewritten |
291
292Install scripts never run. A lockfile the sandbox cannot change this way,
293such as a `requirements.txt` that pins with `--hash`, is handled as
294[when code has to change](#when-code-has-to-change): if no branch has been
295pushed 45 minutes after the update started, g1t gets an issue for it.
296
297### When code has to change
298
299Sometimes raising the version is not enough: the bump fails, or the pull
300request's required checks fail because code must change. Then g1t opens an
301issue and assigns it to [g1t](/guides/working-with-g1t/). That session
302shows as **started by g1t**. This is the only time an agent is involved in
303a security update.
304
305Turn **Security updates** off on the Security page to stop g1t opening
306these pull requests for a project. Alerts are still listed.
307
308## Version updates
309
310`.g1t/dependencies.yml` on the default branch says which dependencies to
311keep current, how often, how to group them, and which to leave alone.
312
313<Aside type="note" title="Coming soon">
314g1t reads and checks this file today and shows what it found on the
315Security page; it does not open version update pull requests yet.
316</Aside>
317
318```yaml
319version: 1
320updates:
321 - ecosystem: npm
322 directory: /web
323 schedule:
324 interval: daily
325 open-pull-requests-limit: 3
326 groups:
327 lint:
328 patterns: ["eslint*", "@typescript-eslint/*"]
329 ignore:
330 - dependency: react
331 versions: [">=19"]
332 - dependency: left-pad
333 - ecosystem: cargo
334```
335
336The file has `version: 1` and `updates`, a list of up to 50 entries. Each
337entry takes:
338
339| Key | Value | Default |
340| --- | --- | --- |
341| `ecosystem` | `npm`, `cargo`, `go` or `pip`. Required. | |
342| `directory` | Where the lockfile is, from the repository's root. It must stay inside the repository. | `/` |
343| `schedule.interval` | `daily`, `weekly` or `monthly`. | `weekly` |
344| `open-pull-requests-limit` | How many version update pull requests can be open at once, from 0 to 20. | `5` |
345| `groups` | Group names, each with `patterns`: a list of package names, where `*` matches anything. Matching packages are updated together. | |
346| `ignore` | A list of entries, each with a `dependency` pattern and an optional `versions` list of requirements, such as `>=19`. With no `versions`, the dependency is never updated. | |
347
348The same `ecosystem` and `directory` listed twice, an unknown key and an
349unknown value are errors. The Security page shows the error, naming the
350entry it is in.
351
352## The g1t identity
353
354Pull requests, issues and merges that g1t makes itself, such as security
355updates, are authored by `g1t`, shown with the pixel 1 avatar. `g1t` is not
356an account: no one can sign in to it, and the name cannot be registered.
357
358## API and MCP
359
360| Route | What it does | Scope |
361| --- | --- | --- |
362| `GET /repos/{owner}/{name}/security/alerts` | Lists alerts. Filter with `state` (`open`, `dismissed`, `fixed`) and `kind`. | `repo:read` |
363| `POST /repos/{owner}/{name}/security/alerts/{id}/dismiss` | Dismisses an alert, with `reason` and an optional `comment`. | `repo:admin` |
364| `POST /repos/{owner}/{name}/security/alerts/{id}/reopen` | Reopens a dismissed alert. | `repo:admin` |
365
366`reason` is one of the API values in the tables above. Over MCP, the
367[`repository` tool](/reference/mcp/#repository) has the actions
368`security_alerts`, `dismiss_alert` and `reopen_alert`. The token's scope
369does not replace the role: dismissing still needs Admin for a secret and
370Write for a dependency. See the [API reference](/reference/api/) for the
371fields of each.