@avi2dg/checks 0.30.0 → 0.32.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 +17 -0
- package/README.md +5 -2
- package/dist/readability/index.js +94 -1
- package/docs/configs/commit-messages.md +5 -0
- package/docs/configs/typescript-rules.md +10 -0
- package/docs/design.md +38 -4
- package/docs/gates/checks-commit-identity.md +3 -0
- package/docs/gates/checks-lint-coverage.md +9 -8
- package/docs/gates/checks-lint.md +3 -2
- 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-unused.md +7 -6
- package/oxlintrc.json +7 -0
- package/package.json +22 -3
- package/src/complexity/exports.ts +2 -1
- package/src/complexity/knip.ts +4 -5
- package/src/complexity/unused.ts +4 -3
- package/src/core/gates.ts +7 -4
- package/src/delivery/commit-identity.ts +18 -2
- 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 +195 -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 +79 -0
- package/src/quality/lint-coverage.sh +11 -6
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
kind: reference
|
|
3
|
+
audience: consumers
|
|
4
|
+
---
|
|
5
|
+
# checks-release-tag
|
|
6
|
+
|
|
7
|
+
`checks-release-tag` tags a landed release commit with its version and dispatches the release workflow on the tag.
|
|
8
|
+
|
|
9
|
+
## What it checks
|
|
10
|
+
|
|
11
|
+
It reads the subject of `HEAD`, and does nothing unless the subject is `chore: release <version>`, with or without the ` (#N)` a squash merge adds.
|
|
12
|
+
It refuses a release commit whose `package.json` holds another version.
|
|
13
|
+
It tags `HEAD` as `v<version>` and pushes the tag to `origin`.
|
|
14
|
+
A tag the workflow token pushes starts no `push` workflow, so it then dispatches the workflow its argument names on the tag.
|
|
15
|
+
It does nothing when `v<version>` already tags `HEAD`, so a rerun after a release never publishes it twice.
|
|
16
|
+
When the dispatch fails after the push, it names `gh workflow run <workflow> --ref v<version>`.
|
|
17
|
+
A rerun then finds the tag and does nothing, so that command is what dispatches the release workflow by hand.
|
|
18
|
+
It refuses when `v<version>` already tags another commit.
|
|
19
|
+
Before it pushes the tag, it runs `bun run build` and refuses when the build rewrites a committed file.
|
|
20
|
+
That happens when a release pull request merged behind `main`, so its `CHANGELOG.md` lacks the commits `main` gained.
|
|
21
|
+
The refusal says to open a `chore: cancel the unpublished <version>` pull request that returns `package.json` to the last tag's version and commits what `bun run build` then writes to `CHANGELOG.md`.
|
|
22
|
+
The build reads the returned version as a revert, so it drops the unpublished section, and the next `daily-release` run cuts the release again with every change since the last tag.
|
|
23
|
+
|
|
24
|
+
## What it reads
|
|
25
|
+
|
|
26
|
+
It reads the subject of `HEAD` and `package.json` from the checkout, and the tag from `origin` with `git ls-remote`.
|
|
27
|
+
The build it runs reads the whole history, so the checkout fetches all of it.
|
|
28
|
+
It pushes the tag with the credentials the checkout holds.
|
|
29
|
+
It calls the GitHub API through `gh api`, which takes the repository from the checkout's remote and the token from `GH_TOKEN`.
|
|
30
|
+
The token needs `contents: write` and `actions: write`.
|
|
31
|
+
|
|
32
|
+
## Arguments
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
checks-release-tag <workflow>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The argument names the release workflow file under `.github/workflows/`, such as `release.yml`.
|
|
39
|
+
That workflow triggers on `workflow_dispatch` and keeps its own guards, as [checks-release-notes](checks-release-notes.md) shows.
|
|
40
|
+
|
|
41
|
+
## Exit codes
|
|
42
|
+
|
|
43
|
+
| Code | When |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| 0 | `HEAD` is no release commit, its tag already tags it, or the tag is pushed and the release workflow dispatched |
|
|
46
|
+
| 2 | the arguments do not parse, `package.json` disagrees with the subject, the tag tags another commit, the build fails or rewrites a committed file, or the push or a GitHub API call fails |
|
|
47
|
+
|
|
48
|
+
## Sample output
|
|
49
|
+
|
|
50
|
+
A run that tags the release prints the build's own output, then one line:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
release-tag: tagged 3f2a9c81d0b4 as v0.4.0, and dispatched release.yml on it
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## When it runs
|
|
57
|
+
|
|
58
|
+
The `tag` job of the daily release workflow runs it on each push to `main` whose head commit reads as a release, as [checks-release-pr](checks-release-pr.md#running-it-in-ci) shows.
|
|
59
|
+
|
|
60
|
+
## Related topics
|
|
61
|
+
|
|
62
|
+
- [checks-release-pr](checks-release-pr.md)
|
|
63
|
+
- [checks-release-notes](checks-release-notes.md)
|
|
@@ -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)
|
|
@@ -4,14 +4,14 @@ audience: consumers
|
|
|
4
4
|
---
|
|
5
5
|
# checks-unused
|
|
6
6
|
|
|
7
|
-
`checks-unused` is the gate that refuses a TypeScript file no entry point reaches.
|
|
7
|
+
`checks-unused` is the gate that refuses a TypeScript or Astro file no entry point reaches.
|
|
8
8
|
|
|
9
9
|
## What it checks
|
|
10
10
|
|
|
11
|
-
It runs Knip with the repository's own configuration and names each `.ts` or `.
|
|
11
|
+
It runs Knip with the repository's own configuration and names each `.ts`, `.tsx` or `.astro` file no entry reaches.
|
|
12
12
|
It reads only the files issue type, so an unused export or dependency never fails it.
|
|
13
13
|
It asks Knip for that issue type itself, so a configuration that narrows `include`, excludes files or turns the files rule off still has its files judged.
|
|
14
|
-
It fails when the repository tracks no
|
|
14
|
+
It fails when the repository tracks no `.ts`, `.tsx` or `.astro` file, since an empty scan would pass without judging anything.
|
|
15
15
|
It fails when the repository holds no Knip configuration, since Knip's default entries cannot tell a dead file from an entry point.
|
|
16
16
|
|
|
17
17
|
## What it reads
|
|
@@ -27,6 +27,7 @@ export default { ...base, entry: ["src/index.ts", "tests/**/*.test.ts"] };
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
The base in `knip-base.json` reports only unreferenced files.
|
|
30
|
+
In a repository that depends on `astro`, Knip reads `.astro` imports, so a file only an `.astro` entry imports counts as used and an `.astro` file no entry reaches is named.
|
|
30
31
|
The gate resolves the Knip binary from the installed kit, so a consumer installs nothing beyond the kit.
|
|
31
32
|
|
|
32
33
|
## Arguments
|
|
@@ -39,7 +40,7 @@ It takes none.
|
|
|
39
40
|
| --- | --- |
|
|
40
41
|
| 0 | no tracked file is unreferenced |
|
|
41
42
|
| 1 | a tracked file is unreferenced or the repository holds no Knip configuration |
|
|
42
|
-
| 2 | the repository tracks no TypeScript source, Knip cannot run, or its configuration does not parse |
|
|
43
|
+
| 2 | the repository tracks no TypeScript or Astro source, Knip cannot run, or its configuration does not parse |
|
|
43
44
|
|
|
44
45
|
## Sample output
|
|
45
46
|
|
|
@@ -51,12 +52,12 @@ unused: 1 unreferenced file(s):
|
|
|
51
52
|
A passing run counts the files it judged:
|
|
52
53
|
|
|
53
54
|
```
|
|
54
|
-
unused: no unreferenced files among 110 tracked .ts/.tsx file(s)
|
|
55
|
+
unused: no unreferenced files among 110 tracked .ts/.tsx/.astro file(s)
|
|
55
56
|
```
|
|
56
57
|
|
|
57
58
|
## When it runs
|
|
58
59
|
|
|
59
|
-
`checks-lint` runs it when the repository tracks a `.ts` or `.
|
|
60
|
+
`checks-lint` runs it when the repository tracks a `.ts`, `.tsx` or `.astro` file.
|
|
60
61
|
Such a repository names its entries in a Knip configuration.
|
|
61
62
|
|
|
62
63
|
## Related topics
|
package/oxlintrc.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avi2dg/checks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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",
|
|
@@ -60,6 +67,7 @@
|
|
|
60
67
|
"src/dependencies/advisories.ts",
|
|
61
68
|
"src/dependencies/advisory-rules.ts",
|
|
62
69
|
"src/dependencies/osv-scanner.ts",
|
|
70
|
+
"src/dependencies/pinned-binary.ts",
|
|
63
71
|
"src/docs/doc-agents.ts",
|
|
64
72
|
"src/docs/doc-outline.ts",
|
|
65
73
|
"src/docs/doc-rules.ts",
|
|
@@ -93,6 +101,8 @@
|
|
|
93
101
|
"checks-changelog": "src/delivery/changelog-write.ts",
|
|
94
102
|
"checks-release-notes": "src/delivery/release-notes.ts",
|
|
95
103
|
"checks-release-report": "src/delivery/release-report.ts",
|
|
104
|
+
"checks-release-pr": "src/delivery/release-pr.ts",
|
|
105
|
+
"checks-release-tag": "src/delivery/release-tag.ts",
|
|
96
106
|
"checks-lint-coverage": "src/quality/lint-coverage.sh",
|
|
97
107
|
"checks-test-layout": "src/testing/test-layout.ts",
|
|
98
108
|
"checks-test": "src/testing/test.ts",
|
|
@@ -110,14 +120,17 @@
|
|
|
110
120
|
"checks-quarantine-clock": "src/testing/quarantine-clock.ts",
|
|
111
121
|
"checks-docs": "src/docs/docs.ts",
|
|
112
122
|
"checks-vendor": "src/dependencies/vendor.ts",
|
|
113
|
-
"checks-advisories": "src/dependencies/advisories.ts"
|
|
123
|
+
"checks-advisories": "src/dependencies/advisories.ts",
|
|
124
|
+
"checks-secrets": "src/delivery/secrets.ts"
|
|
114
125
|
},
|
|
115
126
|
"scripts": {
|
|
116
127
|
"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",
|
|
117
128
|
"build": "bun build src/quality/effect-channel/index.ts --outdir dist/effect-channel --target node --format esm && bun build src/complexity/readability/index.ts --outdir dist/readability --target node --format esm && bun build src/quality/data-shape/index.ts --outdir dist/data-shape --target node --format esm && bun scripts/doc-templates-write.ts && bun src/delivery/changelog-write.ts && bun scripts/doc-blocks-write.ts",
|
|
118
129
|
"lint": "oxlint --type-aware && bun src/core/lint.ts && depcruise --config .dependency-cruiser.cjs .",
|
|
119
130
|
"typecheck": "tsc --noEmit && effect-tsgo diagnostics --project tsconfig.json --format text --strict",
|
|
120
|
-
"test": "bun src/testing/test.ts"
|
|
131
|
+
"test": "bun src/testing/test.ts",
|
|
132
|
+
"mutate": "bunx stryker run",
|
|
133
|
+
"mutate:incremental": "bunx stryker run --incremental"
|
|
121
134
|
},
|
|
122
135
|
"peerDependencies": {
|
|
123
136
|
"@swc/core": "1.16.2",
|
|
@@ -131,7 +144,9 @@
|
|
|
131
144
|
},
|
|
132
145
|
"devDependencies": {
|
|
133
146
|
"@effect/tsgo": "0.45.0",
|
|
147
|
+
"@hughescr/stryker-bun-runner": "1.4.0",
|
|
134
148
|
"@oxlint/plugins": "1.83.0",
|
|
149
|
+
"@stryker-mutator/core": "10.0.0",
|
|
135
150
|
"@swc/core": "1.16.2",
|
|
136
151
|
"@types/bun": "^1.3.0",
|
|
137
152
|
"dependency-cruiser": "18.4.0",
|
|
@@ -148,5 +163,9 @@
|
|
|
148
163
|
"@effect/platform-node-shared": "4.0.0-rc.115",
|
|
149
164
|
"@total-typescript/ts-reset": "0.6.1",
|
|
150
165
|
"knip": "6.38.0"
|
|
166
|
+
},
|
|
167
|
+
"overrides": {
|
|
168
|
+
"qs": "6.16.0",
|
|
169
|
+
"smol-toml": "1.9.0"
|
|
151
170
|
}
|
|
152
171
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
3
|
+
import { TYPESCRIPT_SOURCE } from "../core/gates.ts";
|
|
3
4
|
import { checkoutFiles, commitOf, git, pathsAt, rangeEnds, refArgs } from "../core/git.ts";
|
|
4
5
|
import { runMain } from "../core/main.ts";
|
|
5
6
|
import { knipReport, scanTree } from "./knip.ts";
|
|
@@ -127,7 +128,7 @@ export function report(head: Baseline, { unlisted, added, stale }: Drift): strin
|
|
|
127
128
|
: `${NAME}: ${head.length} unused export(s) in ${BASELINE_FILE}, and no new ones`;
|
|
128
129
|
}
|
|
129
130
|
|
|
130
|
-
const scan = scanTree(NAME, INCLUDED).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
|
|
131
|
+
const scan = scanTree(NAME, INCLUDED, TYPESCRIPT_SOURCE).pipe(Effect.mapError((cause) => new ExportsError({ message: cause.message })));
|
|
131
132
|
|
|
132
133
|
// Knip loads the configuration's imports, such as the kit's knip-base.json, from a node_modules the checkout lacks.
|
|
133
134
|
const unusedAt = Effect.fn("unusedAt")(
|
package/src/complexity/knip.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Console, Effect, FileSystem, Path, Schema } from "effect";
|
|
2
|
+
import type { TrackedContent } from "../core/gates.ts";
|
|
2
3
|
import { collect, git } from "../core/git.ts";
|
|
3
4
|
|
|
4
5
|
class KnipError extends Schema.TaggedError<KnipError>()("KnipError", {
|
|
@@ -16,8 +17,6 @@ const CONFIGS = [
|
|
|
16
17
|
"knip.config.ts",
|
|
17
18
|
] as const;
|
|
18
19
|
|
|
19
|
-
const TYPESCRIPT = ["*.ts", "*.tsx"];
|
|
20
|
-
|
|
21
20
|
const hasKnipConfig = Effect.fn("hasKnipConfig")(function* (root: string) {
|
|
22
21
|
const fs = yield* FileSystem.FileSystem;
|
|
23
22
|
const path = yield* Path.Path;
|
|
@@ -55,13 +54,13 @@ export const knipReport = Effect.fn("knipReport")(function* (root: string, args:
|
|
|
55
54
|
return { kind: "reported", stdout: run.stdout } as const;
|
|
56
55
|
});
|
|
57
56
|
|
|
58
|
-
export const scanTree = Effect.fn("scanTree")(function* (name: string, args: readonly string[]) {
|
|
57
|
+
export const scanTree = Effect.fn("scanTree")(function* (name: string, args: readonly string[], source: TrackedContent) {
|
|
59
58
|
const root = (yield* git(["rev-parse", "--show-toplevel"])).trim();
|
|
60
|
-
const tracked = (yield* git(["ls-files", "--", ...
|
|
59
|
+
const tracked = (yield* git(["ls-files", "--", ...source.pathspecs], root))
|
|
61
60
|
.split("\n")
|
|
62
61
|
.map((line) => line.trim())
|
|
63
62
|
.filter((line) => line !== "");
|
|
64
|
-
if (tracked.length === 0) return yield* new KnipError({ message:
|
|
63
|
+
if (tracked.length === 0) return yield* new KnipError({ message: `no tracked ${source.content} to scan` });
|
|
65
64
|
const reported = yield* knipReport(root, args);
|
|
66
65
|
if (reported.kind === "unconfigured") {
|
|
67
66
|
yield* Console.log(`${name}: no knip configuration names entry files, so add one extending the kit's knip-base.json`);
|
package/src/complexity/unused.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Console, Effect, Schema } from "effect";
|
|
3
|
+
import { LINTED_SOURCE } from "../core/gates.ts";
|
|
3
4
|
import { runMain } from "../core/main.ts";
|
|
4
5
|
import { scanTree } from "./knip.ts";
|
|
5
6
|
|
|
6
7
|
export type Scan = { readonly tracked: number; readonly files: readonly string[] };
|
|
7
8
|
|
|
8
9
|
const NAME = "unused";
|
|
9
|
-
const JUDGED =
|
|
10
|
+
const JUDGED = /[.]tsx?$|[.]astro$/;
|
|
10
11
|
|
|
11
12
|
class UnusedError extends Schema.TaggedError<UnusedError>()("UnusedError", {
|
|
12
13
|
message: Schema.String,
|
|
@@ -31,12 +32,12 @@ export const filesOf = Effect.fn("filesOf")(function* (stdout: string) {
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
export function report({ tracked, files }: Scan): string {
|
|
34
|
-
if (files.length === 0) return `${NAME}: no unreferenced files among ${tracked} tracked .ts/.tsx file(s)`;
|
|
35
|
+
if (files.length === 0) return `${NAME}: no unreferenced files among ${tracked} tracked .ts/.tsx/.astro file(s)`;
|
|
35
36
|
return [`${NAME}: ${files.length} unreferenced file(s):`, ...files.map((file) => ` ${file}`)].join("\n");
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
const unused = Effect.gen(function* () {
|
|
39
|
-
const { tracked, reported } = yield* scanTree(NAME, ["--files"]).pipe(
|
|
40
|
+
const { tracked, reported } = yield* scanTree(NAME, ["--files"], LINTED_SOURCE).pipe(
|
|
40
41
|
Effect.mapError((cause) => new UnusedError({ message: cause.message })),
|
|
41
42
|
);
|
|
42
43
|
if (reported.kind === "unconfigured") return false;
|
package/src/core/gates.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type Program = {
|
|
|
3
3
|
readonly script: string;
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
-
type TrackedContent = {
|
|
6
|
+
export type TrackedContent = {
|
|
7
7
|
readonly pathspecs: readonly string[];
|
|
8
8
|
readonly content: string;
|
|
9
9
|
};
|
|
@@ -30,20 +30,23 @@ export const TEST_ENTRY_POINT: Program = { bin: "checks-test", script: "testing/
|
|
|
30
30
|
|
|
31
31
|
export const DEFAULT_BRANCH = "main";
|
|
32
32
|
|
|
33
|
-
const TYPESCRIPT_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx"], content: "TypeScript source" };
|
|
33
|
+
export const TYPESCRIPT_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx"], content: "TypeScript source" };
|
|
34
|
+
|
|
35
|
+
export const LINTED_SOURCE: TrackedContent = { pathspecs: ["*.ts", "*.tsx", "*.astro"], content: "TypeScript or Astro source" };
|
|
34
36
|
|
|
35
37
|
const BUN_LOCKFILE: TrackedContent = { pathspecs: ["bun.lock"], content: "a bun lockfile" };
|
|
36
38
|
|
|
37
39
|
export const KIT_GATES = [
|
|
38
|
-
{ bin: "checks-lint-coverage", vector: "quality", file: "lint-coverage.sh", reads: "tree", appliesTo:
|
|
40
|
+
{ bin: "checks-lint-coverage", vector: "quality", file: "lint-coverage.sh", reads: "tree", appliesTo: LINTED_SOURCE },
|
|
39
41
|
{ bin: "checks-test-layout", vector: "testing", file: "test-layout.ts", reads: "tree", appliesTo: TYPESCRIPT_SOURCE },
|
|
40
42
|
{ bin: "checks-commit-identity", vector: "delivery", file: "commit-identity.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
41
43
|
{ bin: "checks-comment-gate", vector: "quality", file: "comment-gate.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
42
44
|
{ bin: "checks-suppressions-ratchet", vector: "complexity", file: "suppressions-ratchet.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
43
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 },
|
|
44
47
|
{ bin: "checks-docs", vector: "docs", file: "docs.ts", reads: "range", alsoReads: "every agent file at the head commit", appliesTo: EVERY_REPOSITORY },
|
|
45
48
|
{ bin: "checks-repetition", vector: "complexity", file: "repetition.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
46
|
-
{ bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo:
|
|
49
|
+
{ bin: "checks-unused", vector: "complexity", file: "unused.ts", reads: "tree", appliesTo: LINTED_SOURCE },
|
|
47
50
|
{ bin: "checks-exports", vector: "complexity", file: "exports.ts", reads: "range", appliesTo: TYPESCRIPT_SOURCE },
|
|
48
51
|
{ bin: "checks-quarantine-clock", vector: "testing", file: "quarantine-clock.ts", reads: "range", appliesTo: EVERY_REPOSITORY },
|
|
49
52
|
{ bin: "checks-advisories", vector: "dependencies", file: "advisories.ts", reads: "range", appliesTo: BUN_LOCKFILE },
|
|
@@ -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
|
}
|
|
@@ -152,4 +168,4 @@ const check = Effect.gen(function* () {
|
|
|
152
168
|
return true;
|
|
153
169
|
});
|
|
154
170
|
|
|
155
|
-
runMain("commit-identity", check);
|
|
171
|
+
if (import.meta.main) runMain("commit-identity", check);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { ChildProcessSpawner } from "effect/unstable/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)$''']
|