@avi2dg/checks 0.31.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +6 -3
- package/dist/readability/index.js +18 -9
- package/docs/configs/commit-messages.md +5 -0
- package/docs/configs/typescript-rules.md +1 -1
- package/docs/design.md +42 -4
- package/docs/gates/checks-commit-identity.md +3 -0
- package/docs/gates/checks-docs.md +9 -3
- package/docs/gates/checks-lint.md +1 -1
- package/docs/gates/checks-mutation.md +2 -1
- package/docs/gates/checks-release-notes.md +10 -1
- package/docs/gates/checks-release-pr.md +185 -0
- package/docs/gates/checks-release-report.md +4 -2
- package/docs/gates/checks-release-tag.md +63 -0
- package/docs/gates/checks-secrets.md +93 -0
- package/docs/gates/checks-vendor.md +3 -3
- package/package.json +23 -6
- package/src/core/gates.ts +1 -0
- package/src/core/git.ts +1 -1
- package/src/core/lint.ts +1 -1
- package/src/delivery/commit-identity.ts +17 -1
- package/src/delivery/github.ts +38 -0
- package/src/delivery/gitleaks.toml +113 -0
- package/src/delivery/gitleaks.ts +111 -0
- package/src/delivery/release-pr.ts +196 -0
- package/src/delivery/release-report.ts +3 -30
- package/src/delivery/release-tag.ts +64 -0
- package/src/delivery/release.ts +79 -0
- package/src/delivery/secrets.ts +86 -0
- package/src/dependencies/osv-scanner.ts +3 -45
- package/src/dependencies/pinned-binary.ts +80 -0
- package/src/docs/doc-agents.ts +22 -20
- package/src/docs/doc-names.ts +1 -1
- package/src/docs/doc-references.ts +14 -7
- package/src/docs/docs.ts +15 -2
- package/src/docs/prose-matchers.ts +1 -1
- package/src/docs/sentence-length.ts +106 -0
- package/src/testing/flake.ts +1 -1
- package/src/testing/mutation.ts +1 -1
- package/src/testing/test.ts +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-secrets
|
|
6
|
+
|
|
7
|
+
`checks-secrets` is the gate that fails a range whose commits add a secret, such as an API token, a private key, a VPN key or a proxy link that carries its credential.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It scans the lines each commit in the range adds, in every file the commit adds or modifies, with gitleaks.
|
|
12
|
+
In a merge commit it scans the lines the merge's own resolution adds, such as a key written while resolving a conflict, and not what the merge brings in from the other parent.
|
|
13
|
+
It exits 2 when a commit it scans is an octopus merge, one with three or more parents, because git gives no resolution diff for one.
|
|
14
|
+
It fails on each secret the scan finds, and names its file, its line, the rule that matched and the commit that added it.
|
|
15
|
+
A secret one commit adds and a later commit in the range removes still fails, because the first commit still holds it.
|
|
16
|
+
It guards new changes only, and never scans a commit before the range, so a secret already in the history takes no part.
|
|
17
|
+
|
|
18
|
+
It runs gitleaks' default rules, which know the API keys and tokens of common services and private keys in PEM form, and eight rules of the kit's own that report a secret:
|
|
19
|
+
|
|
20
|
+
| Rule | What it matches | What passes |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `wireguard-key` | a WireGuard or AmneziaWG private or pre-shared key value, 44 base64 characters, under a name that ends in `PrivateKey`, `PresharedKey` or `psk`, such as a real key after `PrivateKey =`, `"privateKey":`, `wg_private_key:` or `client_psk:` | a placeholder such as `<private-key>` or `${WG_PRIVATE_KEY}` |
|
|
23
|
+
| `wireguard-key-file` | a line that holds only a WireGuard or AmneziaWG key, 44 base64 characters, in a file whose name holds `priv`, `preshared`, `pre_shared`, `pre-shared` or `psk`, such as the `privatekey` file `wg genkey \| tee privatekey` writes, whatever else the name holds | a bare key in a file with any other name, such as `publickey` or `peer.pub`, and a placeholder such as `<private-key>` |
|
|
24
|
+
| `wireguard-dot-key-file` | a line that holds only a WireGuard or AmneziaWG key in a file whose name ends in `.key`, such as `wg0.key` | a file whose name ends in `pub.key` or `public.key`, such as `server-public.key`, unless `wireguard-key-file` matches its name |
|
|
25
|
+
| `proxy-userinfo-link` | a `vless://`, `vmess://`, `ss://`, `trojan://`, `hysteria://`, `hysteria2://`, `hy2://`, `tuic://`, `socks://`, `socks5://`, `socks5h://`, `wireguard://` or `wg://` link whose credential before an `@` has a Shannon entropy above 2.5 bits per character, such as a real password before the `@` of a `trojan://` link or a real private key before the `@` of a `wireguard://` link, and a template reference with real text around it, such as a literal user before `:${PASS}` or a non-empty default in `${PASS:-…}` | a link with no credential such as `socks5://127.0.0.1:1080`, a low-entropy credential such as `pw@` or `user@`, a placeholder credential such as `vless://<uuid>@<host>`, a credential that is only template references, such as `{password}`, `$PASS`, `{{ password }}` or `%(password)s` naming a variable that starts with a letter or underscore, or any `${…}` interpolation except a shell default such as `${PASS:-value}`, such as `${pass}`, `${pass:-}`, `${pass:?message}` or `${encodeURIComponent(pass)}`, including a user part such as `$USER:$PASS@`, and a link to `example.com`, `example.net`, `example.org`, a host under `.example`, `.invalid` or `.test`, or `localhost` |
|
|
26
|
+
| `proxy-base64-link` | a `vmess://`, `ss://` or `ssr://` link whose payload is 16 or more base64 characters with a Shannon entropy above 4.2 bits per character, whatever follows it | a placeholder such as `vmess://<base64-config>`, a link to a host and port such as `ss://vpn.home.net:8388`, and a payload an `@` follows, which `proxy-userinfo-link` judges |
|
|
27
|
+
| `proxy-query-credential-first` | the first `auth`, `auth_str`, `obfsParam`, `obfs-password`, `password` or `pk` query value of one of the links `proxy-userinfo-link` names or an `ssr://` link, whose Shannon entropy is above 2.5 bits per character, such as a real password in the `auth` of a `hysteria://` link or a real private key in the `pk` of a `wg://` link, whatever parameters come before or after it | a low-entropy value such as `auth=pw`, a placeholder value such as `auth=<password>`, a value that is only template references as `proxy-userinfo-link` describes, such as `auth={auth}`, `auth=${HY_AUTH:?set HY_AUTH}` or `auth=${encodeURIComponent("a b")}`, and a link to one of the example hosts above |
|
|
28
|
+
| `proxy-query-credential-last` | the last of these query values in such a link, judged the same way, so a real value after a low-entropy or template one still fails | what passes `proxy-query-credential-first` |
|
|
29
|
+
| `proxy-subscription-url` | 16 or more token characters with a Shannon entropy above 3.0 bits per character after the `sub`, `subs`, `subscribe`, `subscription` or `link` path segment of an `http` or `https` URL, or in any `token`, `key`, `uuid` or `id` query value on a line that holds such a URL, each judged on its own | a `token`, `key`, `uuid` or `id` value on a line with no such URL, and a URL to one of the example hosts above |
|
|
30
|
+
|
|
31
|
+
A ninth rule, `proxy-subscription-path`, reports nothing: it finds the URL `proxy-subscription-url` needs on the same line.
|
|
32
|
+
|
|
33
|
+
These rules do not catch:
|
|
34
|
+
|
|
35
|
+
- a real query value between two others of these parameters in one link whose values are low-entropy or template references, such as a real `auth` between an `obfsParam=pw` or `obfsParam=${OBFS}` and a `password=pw`
|
|
36
|
+
- a real `auth`, `auth_str`, `obfsParam`, `obfs-password`, `password` or `pk` query value of an `http` or `https` URL, or of a scheme the table does not name, because the kit's query rules judge share links only, and gitleaks' default rules catch such a value only now and then
|
|
37
|
+
- a credential that begins with `${` and is not a shell default such as `${PASS:-value}`, `${PASS-value}`, `${PASS:=value}` or `${PASS:+value}`, because it reads as an interpolation, even when it holds a fallback such as `${auth ?? "value"}`
|
|
38
|
+
|
|
39
|
+
No setting accepts a finding.
|
|
40
|
+
It ignores a repository's `.gitleaks.toml`, its `.gitleaksignore` and every `gitleaks:allow` comment.
|
|
41
|
+
It skips the paths gitleaks' default config skips, such as lockfiles, images and `node_modules/`.
|
|
42
|
+
|
|
43
|
+
## What it reads
|
|
44
|
+
|
|
45
|
+
It reads the commits of the range from git, not the working tree.
|
|
46
|
+
It needs git 2.36 or newer, which diffs a merge against the merge git would make on its own, and it exits 2 on an older git.
|
|
47
|
+
It reads its rules from `src/delivery/gitleaks.toml` in the installed package.
|
|
48
|
+
|
|
49
|
+
It runs gitleaks 8.30.1, pinned by the SHA-256 of each platform's archive and of the binary inside it.
|
|
50
|
+
On first use it downloads the archive from the scanner's GitHub release and unpacks the binary into `~/.cache/avi2dg-checks/gitleaks/8.30.1/`.
|
|
51
|
+
It checks the binary's SHA-256 again on every run and exits 2 on a copy that differs.
|
|
52
|
+
Builds are pinned for macOS and Linux, each on x64 and arm64.
|
|
53
|
+
On any other platform it exits 2, and no setting runs a scanner other than the pinned build.
|
|
54
|
+
A cold cache downloads about 8 MB.
|
|
55
|
+
|
|
56
|
+
## Arguments
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
checks-secrets <base-ref> <head-ref>
|
|
60
|
+
checks-secrets <ref>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
With two arguments it scans each commit from the merge base of the two refs to the head.
|
|
64
|
+
With one it scans that commit alone.
|
|
65
|
+
|
|
66
|
+
## Exit codes
|
|
67
|
+
|
|
68
|
+
| Code | When |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| 0 | no commit in the range adds a secret |
|
|
71
|
+
| 1 | a commit in the range adds a secret |
|
|
72
|
+
| 2 | a ref does not resolve, or a commit it scans is an octopus merge, or no verified scanner is at hand, or git is older than 2.36, or gitleaks fails |
|
|
73
|
+
|
|
74
|
+
## Sample output
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
secrets: the range adds 3 secret(s); put a placeholder such as <private-key> in its place in the commit that added it, and rotate any secret that left this machine:
|
|
78
|
+
vpn/links.txt:1 proxy-userinfo-link in e19e45c2: A proxy share link that carries its credential before the @, such as vless://, trojan://, socks5:// or wireguard://
|
|
79
|
+
vpn/wg0.conf:2 wireguard-key in e19e45c2: A WireGuard or AmneziaWG private or pre-shared key
|
|
80
|
+
vpn/wg0.conf:7 wireguard-key in e19e45c2: A WireGuard or AmneziaWG private or pre-shared key
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The report never prints a secret.
|
|
84
|
+
|
|
85
|
+
## When it runs
|
|
86
|
+
|
|
87
|
+
`checks-lint` runs it over each pull request's range in every repository, as [checks-lint](checks-lint.md) says.
|
|
88
|
+
A local `bun run lint` runs it over the commits the branch adds, so it fails before a push.
|
|
89
|
+
|
|
90
|
+
## Related topics
|
|
91
|
+
|
|
92
|
+
- [checks-lint](checks-lint.md)
|
|
93
|
+
- [Why it is shaped this way](../design.md)
|
|
@@ -60,19 +60,19 @@ With no arguments it pins nothing.
|
|
|
60
60
|
## Sample output
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
checks-vendor: cloned effect@4.0.0
|
|
63
|
+
checks-vendor: cloned effect@4.0.0 from https://github.com/Effect-TS/effect.git and linked repos/effect
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
A fresh fetch reports the tag it cloned and the link it made.
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
checks-vendor: repos/effect still holds effect@4.0.0
|
|
69
|
+
checks-vendor: repos/effect still holds effect@4.0.0, verified against its recorded commit
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
A later run reports the link it kept.
|
|
73
73
|
|
|
74
74
|
```
|
|
75
|
-
checks-vendor: found 1 path writable by its owner, starting with /home/runner/.cache/avi2dg-checks/repos/github.com/Effect-TS/effect/effect@4.0.0
|
|
75
|
+
checks-vendor: found 1 path writable by its owner, starting with /home/runner/.cache/avi2dg-checks/repos/github.com/Effect-TS/effect/effect@4.0.0, and froze the tree again, so repos/effect still holds effect@4.0.0, verified against its recorded commit
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
A run that found an owner write bit reports how many paths carried one and the first of them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Deterministic checks shared across a set of TypeScript repositories",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"src/delivery/changelog-write.ts",
|
|
30
30
|
"src/delivery/release-notes.ts",
|
|
31
31
|
"src/delivery/release-report.ts",
|
|
32
|
+
"src/delivery/release.ts",
|
|
33
|
+
"src/delivery/release-pr.ts",
|
|
34
|
+
"src/delivery/release-tag.ts",
|
|
35
|
+
"src/delivery/github.ts",
|
|
32
36
|
"src/testing/test-layout.ts",
|
|
33
37
|
"src/testing/test.ts",
|
|
34
38
|
"src/testing/test-skips.ts",
|
|
@@ -43,6 +47,9 @@
|
|
|
43
47
|
"src/testing/subsumed-tests.ts",
|
|
44
48
|
"src/delivery/ci-wiring.ts",
|
|
45
49
|
"src/delivery/shell-command.ts",
|
|
50
|
+
"src/delivery/secrets.ts",
|
|
51
|
+
"src/delivery/gitleaks.ts",
|
|
52
|
+
"src/delivery/gitleaks.toml",
|
|
46
53
|
"src/quality/comment-matchers.ts",
|
|
47
54
|
"src/docs/prose-matchers.ts",
|
|
48
55
|
"src/quality/comments.ts",
|
|
@@ -53,6 +60,7 @@
|
|
|
53
60
|
"src/complexity/exports.ts",
|
|
54
61
|
"src/complexity/knip.ts",
|
|
55
62
|
"src/testing/quarantine-clock.ts",
|
|
63
|
+
"src/docs/sentence-length.ts",
|
|
56
64
|
"src/docs/docs.ts",
|
|
57
65
|
"src/dependencies/vendor.ts",
|
|
58
66
|
"src/dependencies/vendor-args.ts",
|
|
@@ -60,6 +68,7 @@
|
|
|
60
68
|
"src/dependencies/advisories.ts",
|
|
61
69
|
"src/dependencies/advisory-rules.ts",
|
|
62
70
|
"src/dependencies/osv-scanner.ts",
|
|
71
|
+
"src/dependencies/pinned-binary.ts",
|
|
63
72
|
"src/docs/doc-agents.ts",
|
|
64
73
|
"src/docs/doc-outline.ts",
|
|
65
74
|
"src/docs/doc-rules.ts",
|
|
@@ -93,6 +102,8 @@
|
|
|
93
102
|
"checks-changelog": "src/delivery/changelog-write.ts",
|
|
94
103
|
"checks-release-notes": "src/delivery/release-notes.ts",
|
|
95
104
|
"checks-release-report": "src/delivery/release-report.ts",
|
|
105
|
+
"checks-release-pr": "src/delivery/release-pr.ts",
|
|
106
|
+
"checks-release-tag": "src/delivery/release-tag.ts",
|
|
96
107
|
"checks-lint-coverage": "src/quality/lint-coverage.sh",
|
|
97
108
|
"checks-test-layout": "src/testing/test-layout.ts",
|
|
98
109
|
"checks-test": "src/testing/test.ts",
|
|
@@ -110,7 +121,8 @@
|
|
|
110
121
|
"checks-quarantine-clock": "src/testing/quarantine-clock.ts",
|
|
111
122
|
"checks-docs": "src/docs/docs.ts",
|
|
112
123
|
"checks-vendor": "src/dependencies/vendor.ts",
|
|
113
|
-
"checks-advisories": "src/dependencies/advisories.ts"
|
|
124
|
+
"checks-advisories": "src/dependencies/advisories.ts",
|
|
125
|
+
"checks-secrets": "src/delivery/secrets.ts"
|
|
114
126
|
},
|
|
115
127
|
"scripts": {
|
|
116
128
|
"prepare": "bun src/dependencies/vendor.ts --library effect --package effect --repository https://github.com/Effect-TS/effect.git --tag 'effect@{version}' --path packages/effect/package.json",
|
|
@@ -125,7 +137,7 @@
|
|
|
125
137
|
"@swc/core": "1.16.2",
|
|
126
138
|
"dependency-cruiser": "18.4.0",
|
|
127
139
|
"@effect/tsgo": "0.45.0",
|
|
128
|
-
"effect": "4.0.0
|
|
140
|
+
"effect": "4.0.0",
|
|
129
141
|
"jscpd": "5.3.2",
|
|
130
142
|
"oxlint": "1.83.0",
|
|
131
143
|
"oxlint-tsgolint": "7.0.2002",
|
|
@@ -138,8 +150,9 @@
|
|
|
138
150
|
"@stryker-mutator/core": "10.0.0",
|
|
139
151
|
"@swc/core": "1.16.2",
|
|
140
152
|
"@types/bun": "^1.3.0",
|
|
153
|
+
"@types/commonmark": "0.27.10",
|
|
141
154
|
"dependency-cruiser": "18.4.0",
|
|
142
|
-
"effect": "4.0.0
|
|
155
|
+
"effect": "4.0.0",
|
|
143
156
|
"jscpd": "5.3.2",
|
|
144
157
|
"oxlint": "1.83.0",
|
|
145
158
|
"oxlint-tsgolint": "7.0.2002",
|
|
@@ -148,13 +161,17 @@
|
|
|
148
161
|
"dependencies": {
|
|
149
162
|
"@commitlint/cli": "21.2.3",
|
|
150
163
|
"@commitlint/config-conventional": "21.2.3",
|
|
151
|
-
"@effect/platform-bun": "4.0.0
|
|
152
|
-
"@effect/platform-node-shared": "4.0.0
|
|
164
|
+
"@effect/platform-bun": "4.0.0",
|
|
165
|
+
"@effect/platform-node-shared": "4.0.0",
|
|
153
166
|
"@total-typescript/ts-reset": "0.6.1",
|
|
167
|
+
"commonmark": "0.31.2",
|
|
154
168
|
"knip": "6.38.0"
|
|
155
169
|
},
|
|
156
170
|
"overrides": {
|
|
157
171
|
"qs": "6.16.0",
|
|
158
172
|
"smol-toml": "1.9.0"
|
|
173
|
+
},
|
|
174
|
+
"patchedDependencies": {
|
|
175
|
+
"@hughescr/stryker-bun-runner@1.4.0": "patches/@hughescr%2Fstryker-bun-runner@1.4.0.patch"
|
|
159
176
|
}
|
|
160
177
|
}
|
package/src/core/gates.ts
CHANGED
|
@@ -43,6 +43,7 @@ export const KIT_GATES = [
|
|
|
43
43
|
{ bin: "checks-comment-gate", vector: "quality", file: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
44
44
|
{ bin: "checks-suppressions-ratchet", vector: "complexity", file: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
45
45
|
{ bin: "checks-ci-wiring", vector: "delivery", file: "ci-wiring.ts", reads: "tree", appliesTo: EVERY_REPOSITORY },
|
|
46
|
+
{ bin: "checks-secrets", vector: "delivery", file: "secrets.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
46
47
|
{ bin: "checks-docs", vector: "docs", file: "docs.ts", reads: "range", alsoReads: "every agent file at the head commit", appliesTo: EVERY_REPOSITORY },
|
|
47
48
|
{ bin: "checks-repetition", vector: "complexity", file: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
48
49
|
{ bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: LINTED_SOURCE },
|
package/src/core/git.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Config, Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
2
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
2
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
3
3
|
import { DEFAULT_BRANCH } from "./gates.ts";
|
|
4
4
|
import { Usage } from "./main.ts";
|
|
5
5
|
|
package/src/core/lint.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Config, Console, Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
3
|
-
import { ChildProcess, ChildProcessSpawner } from "effect/
|
|
3
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/process";
|
|
4
4
|
import { EVERY_REPOSITORY, KIT_GATES, type KitGate } from "./gates.ts";
|
|
5
5
|
import { defaultBranch, git } from "./git.ts";
|
|
6
6
|
import { runMain, Usage } from "./main.ts";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { releasedVersionOf } from "./release.ts";
|
|
3
4
|
import { git, refArgs } from "../core/git.ts";
|
|
4
5
|
import { runMain } from "../core/main.ts";
|
|
5
6
|
|
|
@@ -20,6 +21,17 @@ const DEFAULT_AUTHORS: readonly Identity[] = [{ name: "avi2d", email: "avi2dg@gm
|
|
|
20
21
|
// GitHub writes the squash commit, so it commits what the owner authored and never authors.
|
|
21
22
|
const SQUASH_COMMITTER: Identity = { name: "GitHub", email: "noreply@github.com" };
|
|
22
23
|
|
|
24
|
+
// checks-release-pr commits through the workflow token, which GitHub attributes to its Actions bot.
|
|
25
|
+
// GitHub may also name that bot as co-author when it squashes the pull request the bot opened.
|
|
26
|
+
const RELEASE_AUTHOR: Identity = { name: "github-actions[bot]", email: "41898282+github-actions[bot]@users.noreply.github.com" };
|
|
27
|
+
|
|
28
|
+
const CO_AUTHOR = /^co-authored-by:\s*(.*?)\s*<([^<>]*)>\s*$/i;
|
|
29
|
+
|
|
30
|
+
function namesReleaseAuthor(trailer: string): boolean {
|
|
31
|
+
const [, name, email = ""] = CO_AUTHOR.exec(trailer) ?? [];
|
|
32
|
+
return name !== undefined && allows([RELEASE_AUTHOR], { name, email });
|
|
33
|
+
}
|
|
34
|
+
|
|
23
35
|
// git's own trailer parser, so only the trailer block counts and prose never does.
|
|
24
36
|
const CO_AUTHORED_BY_FORMAT = "%(trailers:key=Co-authored-by)";
|
|
25
37
|
|
|
@@ -113,13 +125,16 @@ function allows(allowed: readonly Identity[], identity: Identity): boolean {
|
|
|
113
125
|
|
|
114
126
|
function inspect(commit: Commit, allowed: readonly Identity[]): Offence | undefined {
|
|
115
127
|
const reasons: string[] = [];
|
|
116
|
-
|
|
128
|
+
const release = releasedVersionOf(commit.subject) !== undefined;
|
|
129
|
+
const authors = release ? [...allowed, RELEASE_AUTHOR] : allowed;
|
|
130
|
+
if (!allows(authors, commit.author)) {
|
|
117
131
|
reasons.push(`author ${render(commit.author)}`);
|
|
118
132
|
}
|
|
119
133
|
if (!allows([...allowed, SQUASH_COMMITTER], commit.committer)) {
|
|
120
134
|
reasons.push(`committer ${render(commit.committer)}`);
|
|
121
135
|
}
|
|
122
136
|
for (const trailer of commit.coAuthoredBy) {
|
|
137
|
+
if (release && namesReleaseAuthor(trailer)) continue;
|
|
123
138
|
reasons.push(`trailer ${trailer}`);
|
|
124
139
|
}
|
|
125
140
|
return reasons.length === 0 ? undefined : { commit, reasons };
|
|
@@ -144,6 +159,7 @@ const check = Effect.gen(function* () {
|
|
|
144
159
|
}
|
|
145
160
|
lines.push(` allowed: ${allowed.map(render).join(", ")}`);
|
|
146
161
|
lines.push(` allowed as committer only: ${render(SQUASH_COMMITTER)}`);
|
|
162
|
+
lines.push(` allowed as release author only: ${render(RELEASE_AUTHOR)}`);
|
|
147
163
|
yield* Console.error(lines.join("\n"));
|
|
148
164
|
return false;
|
|
149
165
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ChildProcessSpawner } from "effect/process";
|
|
3
|
+
import { collect } from "../core/git.ts";
|
|
4
|
+
|
|
5
|
+
export class GitHubFailure extends Schema.TaggedError<GitHubFailure>()("GitHubFailure", {
|
|
6
|
+
message: Schema.String,
|
|
7
|
+
}) {}
|
|
8
|
+
|
|
9
|
+
type Method = "GET" | "POST" | "PATCH";
|
|
10
|
+
|
|
11
|
+
type Decode<A> = (text: string) => Effect.Effect<A, Schema.SchemaError>;
|
|
12
|
+
|
|
13
|
+
// gh fills {owner} and {repo} from the checkout's remote, so a workflow names no repository.
|
|
14
|
+
export const REPOSITORY = "repos/{owner}/{repo}";
|
|
15
|
+
|
|
16
|
+
const gitHub = Effect.fn("gitHub")(function* (method: Method, path: string, body?: unknown) {
|
|
17
|
+
const failed = (reason: string): GitHubFailure => new GitHubFailure({ message: `gh api ${method} ${path}: ${reason.trim()}` });
|
|
18
|
+
const input = body === undefined ? [] : ["--input", "-"];
|
|
19
|
+
const feed = body === undefined ? {} : { input: JSON.stringify(body) };
|
|
20
|
+
const { stdout, stderr, exitCode } = yield* collect("gh", ["api", "--method", method, path, ...input], undefined, feed).pipe(
|
|
21
|
+
Effect.mapError((cause) => failed(cause.message)),
|
|
22
|
+
);
|
|
23
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) return yield* failed(stderr);
|
|
24
|
+
return stdout;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export function gitHubJson<A>(decode: Decode<A>, method: Method, path: string, body?: unknown): Effect.Effect<A, GitHubFailure, ChildProcessSpawner.ChildProcessSpawner> {
|
|
28
|
+
return gitHub(method, path, body).pipe(
|
|
29
|
+
Effect.flatMap((text) =>
|
|
30
|
+
decode(text).pipe(Effect.mapError((cause) => new GitHubFailure({ message: `gh api ${method} ${path} answered ${cause.message}` }))),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// A dispatch is the one run a workflow token starts at once: GitHub starts none for the token's push, and holds its pull request's runs for approval.
|
|
36
|
+
export const dispatch = Effect.fn("dispatch")(function* (workflow: string, ref: string) {
|
|
37
|
+
yield* gitHub("POST", `${REPOSITORY}/actions/workflows/${workflow}/dispatches`, { ref });
|
|
38
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
title = "checks-secrets"
|
|
2
|
+
|
|
3
|
+
[extend]
|
|
4
|
+
useDefault = true
|
|
5
|
+
|
|
6
|
+
[[rules]]
|
|
7
|
+
id = "wireguard-key"
|
|
8
|
+
description = "A WireGuard or AmneziaWG private or pre-shared key"
|
|
9
|
+
regex = '''(?i)(?:private[_-]?key|pre[_-]?shared[_-]?key|psk)["']?[ \t]*[:=][ \t]*["']?([A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=)'''
|
|
10
|
+
secretGroup = 1
|
|
11
|
+
entropy = 3.0
|
|
12
|
+
keywords = ["private", "preshared", "pre_shared", "pre-shared", "psk"]
|
|
13
|
+
|
|
14
|
+
[[rules]]
|
|
15
|
+
id = "wireguard-key-file"
|
|
16
|
+
description = "A WireGuard or AmneziaWG key alone on a line of a file whose name says private, priv, pre-shared or psk"
|
|
17
|
+
path = '''(?i)(?:^|/)[^/]*(?:priv|pre[_-]?shared|psk)[^/]*$'''
|
|
18
|
+
regex = '''(?m)^[ \t]*([A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=)[ \t]*\r?$'''
|
|
19
|
+
secretGroup = 1
|
|
20
|
+
entropy = 3.0
|
|
21
|
+
|
|
22
|
+
[[rules]]
|
|
23
|
+
id = "wireguard-dot-key-file"
|
|
24
|
+
description = "A WireGuard or AmneziaWG key alone on a line of a .key file not named for a public key"
|
|
25
|
+
path = '''(?i)\.key$'''
|
|
26
|
+
regex = '''(?m)^[ \t]*([A-Za-z0-9+/]{42}[AEIMQUYcgkosw048]=)[ \t]*\r?$'''
|
|
27
|
+
secretGroup = 1
|
|
28
|
+
entropy = 3.0
|
|
29
|
+
|
|
30
|
+
[[rules.allowlists]]
|
|
31
|
+
description = "A .key file named for a public key"
|
|
32
|
+
paths = ['''(?i)pub(?:lic)?\.key$''']
|
|
33
|
+
|
|
34
|
+
[[rules]]
|
|
35
|
+
id = "proxy-userinfo-link"
|
|
36
|
+
description = "A proxy share link that carries its credential before the @, such as vless://, trojan://, socks5:// or wireguard://"
|
|
37
|
+
regex = '''(?i)\b(?:vless|vmess|ss|trojan|hysteria2?|hy2|tuic|socks5?h?|wireguard|wg)://([^\s"'`<>@?#]+)@[^\s"'`<>]+'''
|
|
38
|
+
secretGroup = 1
|
|
39
|
+
entropy = 2.5
|
|
40
|
+
keywords = ["vless://", "vmess://", "ss://", "trojan://", "hysteria://", "hysteria2://", "hy2://", "tuic://", "socks://", "socksh://", "socks5://", "socks5h://", "wireguard://", "wg://"]
|
|
41
|
+
|
|
42
|
+
[[rules.allowlists]]
|
|
43
|
+
description = "A link to a reserved example or test host"
|
|
44
|
+
regexTarget = "match"
|
|
45
|
+
regexes = ['''^(?i)[a-z0-9]+://(?:[^\s"'`@?#]*@)?(?:[\w-]+\.)*(?:example\.(?:com|net|org)|[\w-]+\.(?:example|invalid|test)|localhost)(?::\d+)?(?:[/?#]|$)''']
|
|
46
|
+
|
|
47
|
+
[[rules]]
|
|
48
|
+
id = "proxy-base64-link"
|
|
49
|
+
description = "A proxy share link whose whole payload is base64, such as vmess://, ss:// or ssr://"
|
|
50
|
+
regex = '''(?i)\b(?:vmess|ssr?)://([A-Za-z0-9+/_-]{16,}={0,2})@?'''
|
|
51
|
+
secretGroup = 1
|
|
52
|
+
entropy = 4.2
|
|
53
|
+
keywords = ["vmess://", "ss://", "ssr://"]
|
|
54
|
+
|
|
55
|
+
[[rules.allowlists]]
|
|
56
|
+
description = "A payload an @ follows, which proxy-userinfo-link judges"
|
|
57
|
+
regexTarget = "match"
|
|
58
|
+
regexes = ['''@$''']
|
|
59
|
+
|
|
60
|
+
[[rules]]
|
|
61
|
+
id = "proxy-query-credential-first"
|
|
62
|
+
description = "The first auth, auth_str, obfsParam, obfs-password, password or pk query parameter of a proxy share link, such as hysteria://host?auth="
|
|
63
|
+
regex = '''(?i)\b(?:vless|vmess|ssr?|trojan|hysteria2?|hy2|tuic|socks5?h?|wireguard|wg)://[^\s"'`?#]*\?(?:[^\s"'`#]*?&)??(?:auth|auth_str|obfsParam|obfs-password|password|pk)=([^\s"'`<>&#]+)'''
|
|
64
|
+
secretGroup = 1
|
|
65
|
+
entropy = 2.5
|
|
66
|
+
keywords = ["vless://", "vmess://", "ss://", "ssr://", "trojan://", "hysteria://", "hysteria2://", "hy2://", "tuic://", "socks://", "socksh://", "socks5://", "socks5h://", "wireguard://", "wg://"]
|
|
67
|
+
|
|
68
|
+
[[rules.allowlists]]
|
|
69
|
+
description = "A link to a reserved example or test host"
|
|
70
|
+
regexTarget = "match"
|
|
71
|
+
regexes = ['''^(?i)[a-z0-9]+://(?:[^\s"'`@?#]*@)?(?:[\w-]+\.)*(?:example\.(?:com|net|org)|[\w-]+\.(?:example|invalid|test)|localhost)(?::\d+)?(?:[/?#]|$)''']
|
|
72
|
+
|
|
73
|
+
[[rules]]
|
|
74
|
+
id = "proxy-query-credential-last"
|
|
75
|
+
description = "The last auth, auth_str, obfsParam, obfs-password, password or pk query parameter of a proxy share link, such as hysteria://host?auth="
|
|
76
|
+
regex = '''(?i)\b(?:vless|vmess|ssr?|trojan|hysteria2?|hy2|tuic|socks5?h?|wireguard|wg)://[^\s"'`?#]*\?(?:[^\s"'`#]*&)?(?:auth|auth_str|obfsParam|obfs-password|password|pk)=([^\s"'`<>&#]+)'''
|
|
77
|
+
secretGroup = 1
|
|
78
|
+
entropy = 2.5
|
|
79
|
+
keywords = ["vless://", "vmess://", "ss://", "ssr://", "trojan://", "hysteria://", "hysteria2://", "hy2://", "tuic://", "socks://", "socksh://", "socks5://", "socks5h://", "wireguard://", "wg://"]
|
|
80
|
+
|
|
81
|
+
[[rules.allowlists]]
|
|
82
|
+
description = "A link to a reserved example or test host"
|
|
83
|
+
regexTarget = "match"
|
|
84
|
+
regexes = ['''^(?i)[a-z0-9]+://(?:[^\s"'`@?#]*@)?(?:[\w-]+\.)*(?:example\.(?:com|net|org)|[\w-]+\.(?:example|invalid|test)|localhost)(?::\d+)?(?:[/?#]|$)''']
|
|
85
|
+
|
|
86
|
+
[[rules]]
|
|
87
|
+
id = "proxy-subscription-url"
|
|
88
|
+
description = "A proxy subscription URL whose path or query carries its token"
|
|
89
|
+
regex = '''(?i)(?:\bhttps?://[^\s"'`<>/]+/(?:[^\s"'`<>?]*/)?(?:sub|subs|subscribe|subscription|link)/|[?&](?:token|key|uuid|id)=)([A-Za-z0-9_-]{16,})'''
|
|
90
|
+
secretGroup = 1
|
|
91
|
+
entropy = 3.0
|
|
92
|
+
keywords = ["sub", "link"]
|
|
93
|
+
|
|
94
|
+
[[rules.required]]
|
|
95
|
+
id = "proxy-subscription-path"
|
|
96
|
+
withinLines = 0
|
|
97
|
+
|
|
98
|
+
[[rules]]
|
|
99
|
+
id = "proxy-subscription-path"
|
|
100
|
+
description = "An http or https URL whose last path segment before its token or query is sub, subs, subscribe, subscription or link"
|
|
101
|
+
regex = '''(?i)\bhttps?://[^\s"'`<>/]+/(?:[^\s"'`<>?]*/)?(?:sub|subs|subscribe|subscription|link)[/?]'''
|
|
102
|
+
keywords = ["sub", "link"]
|
|
103
|
+
skipReport = true
|
|
104
|
+
|
|
105
|
+
[[rules.allowlists]]
|
|
106
|
+
description = "A link to a reserved example or test host"
|
|
107
|
+
regexTarget = "match"
|
|
108
|
+
regexes = ['''^(?i)https?://(?:[\w-]+\.)*(?:example\.(?:com|net|org)|[\w-]+\.(?:example|invalid|test)|localhost)(?:[:/?#]|$)''']
|
|
109
|
+
|
|
110
|
+
[[allowlists]]
|
|
111
|
+
description = "A link credential that is only template references, such as {password}, ${pass}, ${encodeURIComponent(pass)}, ${pass:-}, ${pass:?message}, $PASS, {{ password }} or %(password)s, with a user part that is one too; a last ${...} may be cut short, and one that gives a default or alternative value, such as ${pass:-value}, is not a reference"
|
|
112
|
+
targetRules = ["proxy-userinfo-link", "proxy-query-credential-first", "proxy-query-credential-last"]
|
|
113
|
+
regexes = ['''^(?:(?:\$\{(?:[^A-Za-z_}][^}]*|[A-Za-z_][\w.]*(?:[^\w.:=+}-][^}]*|:(?:[^-=+}][^}]*)?|:?[-=+])?)?\}|\$[A-Za-z_]\w*|\{\{\s*[A-Za-z_][\w.]*\s*\}\}|\{[A-Za-z_][\w.]*\}|%\([A-Za-z_][\w.]*\)s):)?(?:\$\{(?:[^A-Za-z_}][^}]*|[A-Za-z_][\w.]*(?:[^\w.:=+}-][^}]*|:(?:[^-=+}][^}]*)?|:?[-=+])?)?\}?|\$[A-Za-z_]\w*|\{\{\s*[A-Za-z_][\w.]*\s*\}\}|\{[A-Za-z_][\w.]*\}|%\([A-Za-z_][\w.]*\)s)$''']
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Effect, Option, Path, Schema } from "effect";
|
|
2
|
+
import { collect } from "../core/git.ts";
|
|
3
|
+
import { installPinned } from "../dependencies/pinned-binary.ts";
|
|
4
|
+
|
|
5
|
+
const GITLEAKS_VERSION = "8.30.1";
|
|
6
|
+
const RELEASES = `https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}`;
|
|
7
|
+
const MEMBER = "gitleaks";
|
|
8
|
+
|
|
9
|
+
type Build = { readonly archive: string; readonly sha256: string; readonly binarySha256: string };
|
|
10
|
+
|
|
11
|
+
// The archive's SHA-256 as gitleaks_<version>_checksums.txt of the release lists it, and the SHA-256 of the binary inside.
|
|
12
|
+
const BUILDS: Readonly<Record<string, Build>> = {
|
|
13
|
+
"darwin-arm64": {
|
|
14
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_darwin_arm64.tar.gz`,
|
|
15
|
+
sha256: "b40ab0ae55c505963e365f271a8d3846efbc170aa17f2607f13df610a9aeb6a5",
|
|
16
|
+
binarySha256: "ba52fb1bfabbcde42f032afad3d6e0b19dff8ed105229a16e7caa338bbc0e84f",
|
|
17
|
+
},
|
|
18
|
+
"darwin-x64": {
|
|
19
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_darwin_x64.tar.gz`,
|
|
20
|
+
sha256: "dfe101a4db2255fc85120ac7f3d25e4342c3c20cf749f2c20a18081af1952709",
|
|
21
|
+
binarySha256: "cee01fea7173f1b779dff188e1c26ecbcb4027d394acc573b23aaf0be260e291",
|
|
22
|
+
},
|
|
23
|
+
"linux-arm64": {
|
|
24
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_linux_arm64.tar.gz`,
|
|
25
|
+
sha256: "e4a487ee7ccd7d3a7f7ec08657610aa3606637dab924210b3aee62570fb4b080",
|
|
26
|
+
binarySha256: "00e91bbe655bd7c47753e8cfe61cb76ea1a5d7e7702fe161ee40102b46b3823b",
|
|
27
|
+
},
|
|
28
|
+
"linux-x64": {
|
|
29
|
+
archive: `gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz`,
|
|
30
|
+
sha256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb",
|
|
31
|
+
binarySha256: "88f91962aa2f93ac6ab281d553b9e125f5197bbbce38f9f2437f7299c32e5509",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
class GitleaksError extends Schema.TaggedError<GitleaksError>()("GitleaksError", {
|
|
36
|
+
message: Schema.String,
|
|
37
|
+
}) {}
|
|
38
|
+
|
|
39
|
+
export function gitleaksBuildFor(platform: string, arch: string): Option.Option<Build> {
|
|
40
|
+
return Option.fromNullishOr(BUILDS[`${platform}-${arch}`]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const pinnedGitleaks = Effect.fn("pinnedGitleaks")(function* (cache: string) {
|
|
44
|
+
const build = gitleaksBuildFor(process.platform, process.arch);
|
|
45
|
+
if (Option.isNone(build)) {
|
|
46
|
+
return yield* new GitleaksError({ message: `no pinned gitleaks build runs on ${process.platform}-${process.arch}` });
|
|
47
|
+
}
|
|
48
|
+
const path = yield* Path.Path;
|
|
49
|
+
const { archive, sha256, binarySha256 } = build.value;
|
|
50
|
+
return yield* installPinned(
|
|
51
|
+
{ kind: "tar.gz", url: `${RELEASES}/${archive}`, sha256, member: MEMBER, memberSha256: binarySha256 },
|
|
52
|
+
path.join(cache, "gitleaks", GITLEAKS_VERSION, MEMBER),
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const Leak = Schema.Struct({
|
|
57
|
+
RuleID: Schema.String,
|
|
58
|
+
Description: Schema.String,
|
|
59
|
+
File: Schema.String,
|
|
60
|
+
StartLine: Schema.Int,
|
|
61
|
+
Commit: Schema.String,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
export type Leak = typeof Leak.Type;
|
|
65
|
+
|
|
66
|
+
const decodeGitleaksReport = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(Leak)));
|
|
67
|
+
|
|
68
|
+
export type Scan = {
|
|
69
|
+
readonly binary: string;
|
|
70
|
+
readonly gitDir: string;
|
|
71
|
+
readonly logOptions: string;
|
|
72
|
+
readonly config: string;
|
|
73
|
+
readonly ignoreDir: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// gitleaks exits 1 both on a leak and on a fatal error, so it exits 0 on a leak here and the report alone tells them apart.
|
|
77
|
+
// It exits 0 with an empty report when git log fails, and logs only errors here, so any line it logs fails the scan.
|
|
78
|
+
// It reads a .gitleaksignore at its source whatever --gitleaks-ignore-path says, so the source is the git directory, where no tracked file lands.
|
|
79
|
+
export const scanCommits = Effect.fn("scanCommits")(function* ({ binary, gitDir, logOptions, config, ignoreDir }: Scan) {
|
|
80
|
+
const args = [
|
|
81
|
+
"git",
|
|
82
|
+
gitDir,
|
|
83
|
+
"--log-opts",
|
|
84
|
+
logOptions,
|
|
85
|
+
"--config",
|
|
86
|
+
config,
|
|
87
|
+
"--gitleaks-ignore-path",
|
|
88
|
+
ignoreDir,
|
|
89
|
+
"--ignore-gitleaks-allow",
|
|
90
|
+
"--redact",
|
|
91
|
+
"--no-banner",
|
|
92
|
+
"--no-color",
|
|
93
|
+
"--log-level",
|
|
94
|
+
"error",
|
|
95
|
+
"--exit-code",
|
|
96
|
+
"0",
|
|
97
|
+
"--report-format",
|
|
98
|
+
"json",
|
|
99
|
+
"--report-path",
|
|
100
|
+
"-",
|
|
101
|
+
];
|
|
102
|
+
const { stdout, stderr, exitCode } = yield* collect(binary, args, ignoreDir).pipe(
|
|
103
|
+
Effect.mapError((cause) => new GitleaksError({ message: `cannot run ${binary}: ${cause.message}` })),
|
|
104
|
+
);
|
|
105
|
+
if (exitCode !== 0 || stderr.trim() !== "") {
|
|
106
|
+
return yield* new GitleaksError({ message: `${binary} failed: ${stderr.trim() || `exit code ${exitCode}`}` });
|
|
107
|
+
}
|
|
108
|
+
return yield* decodeGitleaksReport(stdout).pipe(
|
|
109
|
+
Effect.mapError((cause) => new GitleaksError({ message: `cannot read the report gitleaks wrote: ${cause.message}` })),
|
|
110
|
+
);
|
|
111
|
+
});
|