pm-github 2026.11.1 → 2026.11.4
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 +14 -0
- package/README.md +12 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +368 -10
- package/dist/gate.js.map +1 -1
- package/dist/import-fidelity.d.ts.map +1 -1
- package/dist/import-fidelity.js +16 -6
- package/dist/import-fidelity.js.map +1 -1
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026.11.4 - 2026-11-04
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Make repeated gated imports byte-identical for closed items ([pm-github-repeat-noop](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-repeat-noop.toon))
|
|
8
|
+
- Reusable workflows fail after pm package install when callers pin zero-padded release tags like v2026.11.02 ([pm-github-4gak](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-4gak.toon))
|
|
9
|
+
- Allow public technical forms in the tracker privacy gate ([pm-github-01n8](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-01n8.toon))
|
|
10
|
+
- Resolve an unrelated unreachable commit identity reported by whole-store privacy checks ([pm-github-privacy-store-followup](https://github.com/unbraind/pm-github/blob/main/.agents/pm/issues/pm-github-privacy-store-followup.toon))
|
|
11
|
+
|
|
12
|
+
### Other
|
|
13
|
+
|
|
14
|
+
- Bound coverage aggregation memory and compare default-heap CI behavior ([pm-github-vhi6](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-vhi6.toon))
|
|
15
|
+
- Approve or amend the noreply identity on the normalize-release-tag-version branch commit ([pm-github-8mkc](https://github.com/unbraind/pm-github/blob/main/.agents/pm/tasks/pm-github-8mkc.toon))
|
|
16
|
+
|
|
3
17
|
## 2026.11.1 - 2026-11-01
|
|
4
18
|
|
|
5
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -792,9 +792,20 @@ pm github gate --allowlist .pm-github-gate-allowlist.json
|
|
|
792
792
|
It scans **only the proposed change** — the added lines and their filenames in the staged/working/untracked diff under the resolved pm tracker path (never a hardcoded `.agents/pm`; the SDK-resolved path wins), or an explicit `--diff` file — so pre-existing reviewed content is not re-litigated on every run, and removed content (which cannot publish anything new) is never scanned. A finding in a filename uses `field: "file_path"` and redacts a sensitive item id. It fails closed (non-zero, machine-readable report with `item_id` + `field` + `rule` + the sha256 `hash` of the matched content, **never the matched content itself**) on:
|
|
793
793
|
|
|
794
794
|
- **credentials** — GitHub (`ghp_/gho_/ghu_/ghs_/ghr_` and fine-grained `github_pat_`), npm, AWS access key ids, Slack (including app and cookie credentials), OpenAI, Anthropic tokens, generic `Authorization: Bearer` values, `-----BEGIN … PRIVATE KEY-----` blocks, and high-entropy assignments to secret-named identifiers (`token = "…"`, `api_key: …`, `accessToken: …`) whose value has high measured entropy. UUID-shaped credential values and values containing dates receive the same checks;
|
|
795
|
-
- **personal data** — email addresses other than no-reply identities (`@users.noreply.github.com`, `noreply@…`, `@noreply.…`), phone numbers in international (`+…`) or North-American (`(555) 123-4567`, `555-123-4567`) notation, and unformatted numbers under phone/tel/mobile contact labels;
|
|
795
|
+
- **personal data** — email addresses other than the reserved-domain fixtures described below and no-reply identities (`@users.noreply.github.com`, `noreply@…`, `@noreply.…`), phone numbers in international (`+…`) or North-American (`(555) 123-4567`, `555-123-4567`) notation, and unformatted numbers under phone/tel/mobile contact labels;
|
|
796
796
|
- **host paths** — absolute local filesystem paths (POSIX home/system directories and Windows drive paths with either separator), and named home-directory references.
|
|
797
797
|
|
|
798
|
+
Public technical forms pass without a hash allowlist:
|
|
799
|
+
|
|
800
|
+
- procfs interfaces: `/proc`, `/proc/`, `/proc/self/…`, `/proc/<placeholder>/…` (including the bare `root` entry directly after the `self` pid, but no path below it; a concrete numeric pid stays a refusal); sysfs interfaces under `/sys`;
|
|
801
|
+
- hosted runner workspaces: the suffix `runner/work/<seg>/<seg>/…` under either `/home` or `/Users`, Windows `D:/a/<seg>/<seg>/…` with either separator, and `/opt/hostedtoolcache/…` — including the runner's own `tmp` scratch directory inside the workspace;
|
|
802
|
+
- fixture emails on exactly `example.com`, `example.net`, `example.org`, bare `localhost`, or domains ending in `.example`, `.invalid`, `.test` or `.localhost`, compared case-insensitively against the whole domain;
|
|
803
|
+
- SSH transport using only the `git` account: `git@host:owner/repo.git` or `ssh://git@host/owner/repo.git` (including a numeric SSH port);
|
|
804
|
+
- placeholder account segments `example`, `<placeholder>`, `{placeholder}`, `$USER`, `${USER}` or `%USERNAME%` directly after `/home/`, `/Users/` or Windows `C:\Users\` (either separator, also mixed within one path).
|
|
805
|
+
- markup around a path: a closing tag that names one word (`</var>`, `</root>`, `</data>`) is not a path, and a path match ends before it, so `<var>/home/<placeholder>/x</var>` is judged as the path alone. A closing tag that carries a path, such as `</home/alice>`, is still matched.
|
|
806
|
+
|
|
807
|
+
Concrete home accounts, named tilde home paths (`~name/…`, also straight after any character other than a letter, a number, `.`, `_`, `~`, `-`, `/` or `\`, such as `]`, `)` or `;`; a bare `~name` token without a path is not matched), `/root`, every `/tmp` path and `/var/folders` remain refusals. So do runner paths outside that workspace shape, placeholders below a concrete account, dot-segment traversal, percent/backslash escape variants, embedded identity roots (including `/root`, `/tmp`, `/private`, `/Volumes` and the mount roots continuing into a permitted tail), any path below procfs's own `root` entry, and a second host path hiding behind any punctuation (a bracket, label colon, assignment equals, `[`, `(`, `|`, `@` and the rest) inside a permitted form. Real emails, `git` contact addresses without a transport path, other SSH account names on non-reserved domains, and look-alikes such as `notexample.com` or `example.com.evil.io` remain refused. The table-driven corpus and `node scripts/prove-gate-public-forms.ts` verify these boundaries; [measurement and mutation evidence](https://github.com/unbraind/pm-github/blob/main/docs/gate-public-forms-evidence.md) records the eight-repository read-only scan.
|
|
808
|
+
|
|
798
809
|
Unreadable input, malformed or truncated diffs, binary input, invalid UTF-8, Git failures, malformed allowlists, and scanner errors fail the gate. Untracked operational state in the tracker root (`locks/`, `extensions/`, `checkpoints/`) is excluded. Explicitly staged operational files remain in scope. Staged and unstaged changes are scanned separately, including staged content that the working copy subsequently removed.
|
|
799
810
|
|
|
800
811
|
**False positives** are allowlisted by *content hash*, not by pattern: put the sha256 from the finding into `.pm-github-gate-allowlist.json` at the repository root (or pass `--allowlist <file>`) with a written justification. An entry suppresses exactly the reviewed content, can never widen to a pattern, and a missing justification fails the gate:
|
package/dist/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../gate.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../gate.ts"],"names":[],"mappings":"AAwCA;;;;;;;GAOG;AACH,MAAM,WAAW,WAAW;IAC1B,8EAA8E;IAC9E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,mFAAmF;AACnF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,CAAC;AAE1C,8EAA8E;AAC9E,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,0FAA0F;IAC1F,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,CAAC;IAChC,sEAAsE;IACtE,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IAC1C,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AAED,mFAAmF;AACnF,UAAU,SAAS;IACjB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,wFAAwF;AACxF,UAAU,UAAU;IAClB,+DAA+D;IAC/D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oFAAoF;IACpF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,QAAQ,CAAC,UAAU,EAAE,SAAS,SAAS,EAAE,CAAC;CAC3C;AAED,mFAAmF;AACnF,MAAM,MAAM,MAAM,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/G;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,CAAC;CACtD;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,+EAA+E;IAC/E,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,kFAAkF;IAClF,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;KAAE,EAAE,CAAC;IACnH,sFAAsF;IACtF,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAOD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEvD;AAugBD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAU3D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CASxF;AAED,oEAAoE;AACpE,UAAU,OAAO;IACf,kCAAkC;IAClC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,kFAAkF;IAClF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AA+FD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,UAAQ,GAAG,OAAO,EAAE,CA6DhF;AASD,yEAAyE;AACzE,UAAU,QAAQ;IAChB,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,EAAE,CAOxE;AA2BD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAqG/D;AAoDD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAUnH;AAYD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,EACjB,YAAY,GAAE,mBAAwB,GACrC,UAAU,EAAE,CAqEd;AAMD,+DAA+D;AAC/D,UAAU,cAAc;IACtB,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,OAAO,EACjB,YAAY,GAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAyD,GAC5F,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CA6B7B;AAED,mFAAmF;AACnF,eAAO,MAAM,uBAAuB,mCAAmC,CAAC;AA2FxE;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,UAAU,CA2ElE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,EAAE,CAY7D"}
|
package/dist/gate.js
CHANGED
|
@@ -93,9 +93,330 @@ const CREDENTIAL_RULES = [
|
|
|
93
93
|
},
|
|
94
94
|
{ rule: "private-key-block", pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g },
|
|
95
95
|
];
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Public technical forms — the exact allowances shared by the personal-data
|
|
98
|
+
// and host-path rules. Each helper answers ONE narrow question; the confirms
|
|
99
|
+
// compose them so removing or broadening a single allowance is independently
|
|
100
|
+
// observable (see scripts/prove-gate-public-forms.ts).
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
/**
|
|
103
|
+
* Documentation-only email domains reserved by RFC 2606/6761 (plus the
|
|
104
|
+
* special-use `localhost` name): mail on them can never reach a real person,
|
|
105
|
+
* so an address is a fixture rather than personal data. Compared exactly and
|
|
106
|
+
* case-insensitively against the whole domain, so registrable look-alikes
|
|
107
|
+
* (`notexample.com`, `example.com.evil.io`) stay refusals.
|
|
108
|
+
*/
|
|
109
|
+
const RESERVED_EMAIL_DOMAINS = ["example.com", "example.net", "example.org", "localhost"];
|
|
110
|
+
/** Reserved top-level labels whose subdomains are also documentation-only. */
|
|
111
|
+
const RESERVED_EMAIL_DOMAIN_SUFFIXES = [".example", ".invalid", ".test", ".localhost"];
|
|
112
|
+
/**
|
|
113
|
+
* Whether an email domain (lowercased, the whole text after the at-sign) is
|
|
114
|
+
* documentation-only per RFC 2606/6761.
|
|
115
|
+
*
|
|
116
|
+
* @param domain - The complete lowercased domain after the at-sign.
|
|
117
|
+
* @returns True for `example.com`/`example.net`/`example.org`/`localhost`
|
|
118
|
+
* exactly, or any domain whose final label is `example`, `invalid`,
|
|
119
|
+
* `test` or `localhost`.
|
|
120
|
+
*/
|
|
121
|
+
function isReservedDocumentationDomain(domain) {
|
|
122
|
+
return RESERVED_EMAIL_DOMAINS.includes(domain) ||
|
|
123
|
+
RESERVED_EMAIL_DOMAIN_SUFFIXES.some((suffix) => domain.endsWith(suffix));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Home-root child segments that name no account: the documentation name, the
|
|
127
|
+
* shell/env placeholder users, and bracketed placeholders handled separately.
|
|
128
|
+
*/
|
|
129
|
+
const HOME_PLACEHOLDER_SEGMENTS = ["example", "$USER", "${USER}", "%USERNAME%"];
|
|
130
|
+
/**
|
|
131
|
+
* Whether a path segment is a bracketed placeholder such as `<user>` or
|
|
132
|
+
* `{user}`: matched brackets around a nonempty placeholder body.
|
|
133
|
+
*
|
|
134
|
+
* @param segment - One slash-separated path segment.
|
|
135
|
+
* @param open - The required opening bracket character.
|
|
136
|
+
* @param close - The required closing bracket character.
|
|
137
|
+
* @returns True when the segment is exactly `open + body + close`.
|
|
138
|
+
*/
|
|
139
|
+
function isBracketedPlaceholder(segment, open, close) {
|
|
140
|
+
return segment.length > open.length + close.length && segment.startsWith(open) && segment.endsWith(close) &&
|
|
141
|
+
/^[^<> {}\\/\s]+$/.test(segment.slice(open.length, segment.length - close.length));
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Whether a segment directly after a home root names no account.
|
|
145
|
+
*
|
|
146
|
+
* @param segment - The first path segment after `/home/`, `/Users/` or a
|
|
147
|
+
* Windows `Users` root.
|
|
148
|
+
* @returns True for `example`, `$USER`, `${USER}`, `%USERNAME%` and bracketed
|
|
149
|
+
* `<placeholder>`/`{placeholder}` segments.
|
|
150
|
+
*/
|
|
151
|
+
function isHomePlaceholderSegment(segment) {
|
|
152
|
+
return HOME_PLACEHOLDER_SEGMENTS.includes(segment) ||
|
|
153
|
+
isBracketedPlaceholder(segment, "<", ">") || isBracketedPlaceholder(segment, "{", "}");
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Whether one path segment can smuggle traversal or hidden content past a
|
|
157
|
+
* permitted shape.
|
|
158
|
+
*
|
|
159
|
+
* Exact `.`/`..` segments traverse out of the permitted root; percent signs
|
|
160
|
+
* can decode to them or hide other content outside the one Windows env
|
|
161
|
+
* placeholder; a backslash inside a POSIX path is content, never a separator,
|
|
162
|
+
* so it could re-split the path under a different reading and is refused.
|
|
163
|
+
*
|
|
164
|
+
* @param segment - One separator-split path segment.
|
|
165
|
+
* @param posix - Whether the path was split on forward slashes only.
|
|
166
|
+
* @returns True when the segment must keep the whole path a refusal.
|
|
167
|
+
*/
|
|
168
|
+
function isEscapePathSegment(segment, posix) {
|
|
169
|
+
if (segment === "." || segment === "..")
|
|
170
|
+
return true;
|
|
171
|
+
if (segment !== "%USERNAME%" && segment.includes("%"))
|
|
172
|
+
return true;
|
|
173
|
+
return posix && segment.includes("\\");
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Identity-bearing roots that must never appear inside a permitted tail: the
|
|
177
|
+
* two home roots, a Windows drive letter, and the account- or machine-bearing
|
|
178
|
+
* system roots (`tmp`, macOS `private`/`Volumes`, and the mount roots) that a
|
|
179
|
+
* real filesystem path continues into once a permitted prefix has passed.
|
|
180
|
+
* `root` is included because it names the root account's home tree everywhere
|
|
181
|
+
* except the one position procfs exposes it (see {@link isPublicPosixPath}).
|
|
182
|
+
*/
|
|
183
|
+
const EMBEDDED_IDENTITY_SEGMENTS = ["home", "Users", "tmp", "private", "Volumes", "mnt", "media", "srv", "root"];
|
|
184
|
+
/**
|
|
185
|
+
* Whether any segment of a permitted tail embeds a fresh identity root (a home
|
|
186
|
+
* root, an account-bearing system root, or a Windows drive), which would
|
|
187
|
+
* smuggle a concrete account past the allowed prefix (for example
|
|
188
|
+
* `…/x/home/alice` or `…/root/tmp/alice-notes`).
|
|
189
|
+
*
|
|
190
|
+
* @param segments - The segments after a shape's recognized prefix.
|
|
191
|
+
* @returns True when the tail must keep the whole path a refusal.
|
|
192
|
+
*/
|
|
193
|
+
function embedsIdentityRoot(segments) {
|
|
194
|
+
return segments.some((segment) => EMBEDDED_IDENTITY_SEGMENTS.includes(segment) || /^[A-Za-z]:$/.test(segment));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Runner-tail segments with the hosted runner's own `tmp` directory removed.
|
|
198
|
+
*
|
|
199
|
+
* The GitHub-hosted runner creates a real scratch `tmp` directory inside its
|
|
200
|
+
* workspace, so that one segment is runner infrastructure rather than an
|
|
201
|
+
* identity root there; every other identity segment keeps refusing the tail.
|
|
202
|
+
*
|
|
203
|
+
* @param segments - The segments after a runner workspace's recognized prefix.
|
|
204
|
+
* @returns The tail segments the embedded-identity check must judge.
|
|
205
|
+
*/
|
|
206
|
+
function runnerTailSegments(segments) {
|
|
207
|
+
return segments.filter((segment) => segment !== "tmp");
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Whether an absolute POSIX path is a public, non-identifying technical form.
|
|
211
|
+
*
|
|
212
|
+
* Admits exactly the procfs interface paths (`/proc` itself, `/proc/self/…`
|
|
213
|
+
* and `/proc/<placeholder>/…`; a concrete numeric pid names one host's process
|
|
214
|
+
* and stays a refusal), the GitHub-hosted runner
|
|
215
|
+
* workspaces (the fixed hosted account's `work/<seg>/<seg>/…` subtree under
|
|
216
|
+
* either home root), the hosted tool cache (`/opt/hostedtoolcache/…`) and
|
|
217
|
+
* placeholder home roots (`<placeholder>` children of either home root).
|
|
218
|
+
* Every other root — a concrete account, `/root`, `/tmp`, `/var` and friends —
|
|
219
|
+
* and any path with a traversal or encoding escape, or an identity root
|
|
220
|
+
* embedded in its tail, stays a refusal.
|
|
221
|
+
*
|
|
222
|
+
* @param matched - The complete matched path, starting at its leading slash.
|
|
223
|
+
* @returns True when the path carries no host or account identity.
|
|
224
|
+
*/
|
|
225
|
+
function isPublicPosixPath(matched) {
|
|
226
|
+
const segments = matched.slice(1).split("/");
|
|
227
|
+
const [root = "", first = ""] = segments;
|
|
228
|
+
if (segments.some((segment) => isEscapePathSegment(segment, true)))
|
|
229
|
+
return false;
|
|
230
|
+
if (root === "sys")
|
|
231
|
+
return !embedsIdentityRoot(segments.slice(1));
|
|
232
|
+
if (root === "proc") {
|
|
233
|
+
if (first === "" || first === "self" || isBracketedPlaceholder(first, "<", ">")) {
|
|
234
|
+
// `root` directly after the `self` pid directory is the real
|
|
235
|
+
// procfs root entry (the process's own root). It re-exposes the whole
|
|
236
|
+
// filesystem, so only the entry itself is public: anything after it
|
|
237
|
+
// names a host path. As a tail segment everywhere else it names the
|
|
238
|
+
// root account's tree.
|
|
239
|
+
const tail = segments.slice(2);
|
|
240
|
+
const exemptRootEntry = first === "self" && tail[0] === "root";
|
|
241
|
+
if (exemptRootEntry)
|
|
242
|
+
return tail.slice(1).every((segment) => segment === "");
|
|
243
|
+
return !embedsIdentityRoot(tail);
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
if (root === "opt")
|
|
248
|
+
return first === "hostedtoolcache" && !embedsIdentityRoot(segments.slice(2));
|
|
249
|
+
if (root !== "home" && root !== "Users")
|
|
250
|
+
return false;
|
|
251
|
+
if (isHomePlaceholderSegment(first))
|
|
252
|
+
return !embedsIdentityRoot(segments.slice(2));
|
|
253
|
+
return first === "runner" && segments[2] === "work" && (segments[3] ?? "") !== "" && (segments[4] ?? "") !== "" &&
|
|
254
|
+
!embedsIdentityRoot(runnerTailSegments(segments.slice(5)));
|
|
255
|
+
}
|
|
96
256
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
257
|
+
* Whether a Windows drive path is a public, non-identifying technical form.
|
|
258
|
+
*
|
|
259
|
+
* Admits exactly the GitHub-hosted runner workspaces (`D:\a\<seg>\<seg>\…`
|
|
260
|
+
* with either separator) and placeholder home roots
|
|
261
|
+
* (`C:\Users\<placeholder>\…`). Any other drive, root or account segment, and
|
|
262
|
+
* any path with a traversal or encoding escape, or an identity root embedded
|
|
263
|
+
* in its tail, stays a refusal.
|
|
264
|
+
*
|
|
265
|
+
* @param matched - The complete matched path, starting at its drive letter.
|
|
266
|
+
* @returns True when the path carries no host or account identity.
|
|
267
|
+
*/
|
|
268
|
+
function isPublicWindowsPath(matched) {
|
|
269
|
+
const [drive = "", ...segments] = matched.split(/[\\/]/);
|
|
270
|
+
const [root = "", first = ""] = segments;
|
|
271
|
+
if (segments.some((segment) => isEscapePathSegment(segment, false)))
|
|
272
|
+
return false;
|
|
273
|
+
if (drive.toLowerCase() === "c:") {
|
|
274
|
+
return root === "Users" && isHomePlaceholderSegment(first) && !embedsIdentityRoot(segments.slice(2));
|
|
275
|
+
}
|
|
276
|
+
return drive.toLowerCase() === "d:" && root === "a" && first !== "" && (segments[2] ?? "") !== "" &&
|
|
277
|
+
!embedsIdentityRoot(runnerTailSegments(segments.slice(3)));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Characters of an ordinary path segment name: letters (with their combining
|
|
281
|
+
* marks) and digits in any script, plus `.`, `_`, `~` and `-`. Each is judged
|
|
282
|
+
* as a whole code point, so an astral letter is a name character too. Every other character inside a matched
|
|
283
|
+
* host path is a delimiter that can end one path and begin the next — the two
|
|
284
|
+
* bracket pairs (which continue a path while they wrap a whole placeholder
|
|
285
|
+
* segment), the colon and equals of a label or assignment prefix (`cwd:`,
|
|
286
|
+
* `path=`), and any other punctuation (`[`, `(`, `|`, `@`, `#`, `?`, `&`, `*`,
|
|
287
|
+
* `!`, `+`, …) written straight into a permitted tail. Naming the segment
|
|
288
|
+
* characters rather than the delimiters keeps unlisted punctuation from hiding
|
|
289
|
+
* a second path; only a delimiter directly followed by a path start is judged.
|
|
290
|
+
*/
|
|
291
|
+
const PATH_NAME_CHARACTER = /[\p{L}\p{M}\p{N}._~-]/u;
|
|
292
|
+
/**
|
|
293
|
+
* Whether the text after a delimiter begins with a shape a host-path rule can
|
|
294
|
+
* start on: a POSIX root slash, a tilde username, a Windows drive, or a
|
|
295
|
+
* `file://` URL. Every other continuation is ordinary path content.
|
|
296
|
+
*
|
|
297
|
+
* @param text - The complete matched path.
|
|
298
|
+
* @param offset - The index directly after one delimiter.
|
|
299
|
+
* @returns Whether the delimiter can end one path and begin the next.
|
|
300
|
+
*/
|
|
301
|
+
function startsWithHostPathStart(text, offset) {
|
|
302
|
+
const first = text[offset];
|
|
303
|
+
return text.startsWith("file://", offset) || first === "/" || first === "~" || /^[A-Za-z]:[\\/]/.test(text.slice(offset, offset + 3));
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Whether the text after a delimiter is a host path a standalone scan would
|
|
307
|
+
* refuse.
|
|
308
|
+
*
|
|
309
|
+
* The sticky rule patterns match at one fixed offset (the start of `rest`, or
|
|
310
|
+
* the URL path of a `file://` prefix) and reproduce exactly the rule match
|
|
311
|
+
* that would begin there; the verdict then uses ONLY the non-recursive
|
|
312
|
+
* public-path predicates — never `rule.confirm`, whose re-entry into the
|
|
313
|
+
* delimiter check branches exponentially on adversarial bracket chains. That
|
|
314
|
+
* is sound because every delimiter of the match is examined on its own: a
|
|
315
|
+
* refused path always begins directly after some delimiter, so it is always
|
|
316
|
+
* judged at that delimiter without re-judging the suffixes around it.
|
|
317
|
+
*
|
|
318
|
+
* @param rest - The text following one delimiter.
|
|
319
|
+
* @returns True when the suffix is a host path the gate refuses standalone.
|
|
320
|
+
*/
|
|
321
|
+
function refusedHostPathAfterDelimiter(rest) {
|
|
322
|
+
const posix = STICKY_HOST_PATH_PATTERNS.get("absolute-host-path");
|
|
323
|
+
const windows = STICKY_HOST_PATH_PATTERNS.get("windows-host-path");
|
|
324
|
+
const tilde = STICKY_HOST_PATH_PATTERNS.get("home-username");
|
|
325
|
+
if (rest.startsWith("file://")) {
|
|
326
|
+
posix.lastIndex = "file://".length;
|
|
327
|
+
const hit = posix.exec(rest);
|
|
328
|
+
return hit !== null && !isPublicPosixPath(hit[0]);
|
|
329
|
+
}
|
|
330
|
+
if (rest[0] === "/") {
|
|
331
|
+
posix.lastIndex = 0;
|
|
332
|
+
const hit = posix.exec(rest);
|
|
333
|
+
return hit !== null && !isPublicPosixPath(hit[0]);
|
|
334
|
+
}
|
|
335
|
+
if (rest[0] === "~") {
|
|
336
|
+
tilde.lastIndex = 0;
|
|
337
|
+
return tilde.exec(rest) !== null;
|
|
338
|
+
}
|
|
339
|
+
windows.lastIndex = 0;
|
|
340
|
+
const hit = windows.exec(rest);
|
|
341
|
+
return hit !== null && !isPublicWindowsPath(hit[0]);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Most delimiter-started paths one match may have judged. Each judgment copies
|
|
345
|
+
* and checks the remaining suffix, so the bound keeps a match's cost linear in
|
|
346
|
+
* its length; a match with more delimiter-started paths fails closed.
|
|
347
|
+
*/
|
|
348
|
+
const MAX_DELIMITER_PATH_JUDGMENTS = 64;
|
|
349
|
+
/**
|
|
350
|
+
* Whether a delimiter inside a matched path begins a second host path that
|
|
351
|
+
* would be refused on its own.
|
|
352
|
+
*
|
|
353
|
+
* A whole placeholder segment (`<pid>`, `{user}`, `${USER}`, `%USERNAME%`)
|
|
354
|
+
* is judged in place, so its brackets, `$` and `%` never start a second path. A delimiter (any character
|
|
355
|
+
* outside {@link PATH_NAME_CHARACTER}) in any other segment — including the
|
|
356
|
+
* `:` or `=` of a label or assignment prefix — can end one path and begin the
|
|
357
|
+
* next (a root-account, temp, other-drive or
|
|
358
|
+
* tilde-username path written straight after it), so the text after it is
|
|
359
|
+
* judged with the public-path predicates, exactly as if it stood alone.
|
|
360
|
+
* The colon of a leading Windows drive prefix (`C:/…`) is the exception: it
|
|
361
|
+
* belongs to the matched path itself, which the Windows predicate judged whole.
|
|
362
|
+
*
|
|
363
|
+
* The scan is linear in the match length: one pass keeps the segment start,
|
|
364
|
+
* each segment is scanned forward once for its placeholder verdict, only
|
|
365
|
+
* delimiters directly followed by a possible path start are judged (at most
|
|
366
|
+
* {@link MAX_DELIMITER_PATH_JUDGMENTS}; more fail closed), and the sticky rule
|
|
367
|
+
* patterns are built once (see {@link STICKY_HOST_PATH_PATTERNS}) — never rebuilt per
|
|
368
|
+
* delimiter and rule, and never re-entered recursively.
|
|
369
|
+
*
|
|
370
|
+
* @param matched - The complete matched path.
|
|
371
|
+
* @returns True when a path behind a non-placeholder delimiter must refuse the
|
|
372
|
+
* whole match.
|
|
373
|
+
*/
|
|
374
|
+
function hidesHostPathBehindDelimiter(matched) {
|
|
375
|
+
let segmentStart = 0;
|
|
376
|
+
let judgedSegmentStart = -1;
|
|
377
|
+
let judgedPlaceholder = false;
|
|
378
|
+
let judgments = 0;
|
|
379
|
+
// A Windows match begins with its own drive prefix, and the Windows
|
|
380
|
+
// predicate has already judged that whole path, so the drive colon starts
|
|
381
|
+
// no second path. Judging the rest as a POSIX path would keep a mixed
|
|
382
|
+
// separator segment (`<user>\AppData`) whole and refuse a public form.
|
|
383
|
+
const scanStart = /^[A-Za-z]:[\\/]/.test(matched) ? 2 : 0;
|
|
384
|
+
for (let index = scanStart; index < matched.length; index++) {
|
|
385
|
+
const character = String.fromCodePoint(matched.codePointAt(index));
|
|
386
|
+
// Step over the low half of an astral character so it is judged once.
|
|
387
|
+
if (character.length === 2)
|
|
388
|
+
index++;
|
|
389
|
+
if (character === "/" || character === "\\") {
|
|
390
|
+
segmentStart = index + 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (PATH_NAME_CHARACTER.test(character))
|
|
394
|
+
continue;
|
|
395
|
+
if (segmentStart !== judgedSegmentStart) {
|
|
396
|
+
// Forward scan to this segment's end: each segment is scanned once, so
|
|
397
|
+
// the whole pass stays linear in the match length.
|
|
398
|
+
let segmentEnd = index;
|
|
399
|
+
while (segmentEnd < matched.length && matched[segmentEnd] !== "/" && matched[segmentEnd] !== "\\")
|
|
400
|
+
segmentEnd++;
|
|
401
|
+
judgedSegmentStart = segmentStart;
|
|
402
|
+
const segment = matched.slice(segmentStart, segmentEnd);
|
|
403
|
+
judgedPlaceholder = isHomePlaceholderSegment(segment);
|
|
404
|
+
}
|
|
405
|
+
if (judgedPlaceholder || !startsWithHostPathStart(matched, index + 1))
|
|
406
|
+
continue;
|
|
407
|
+
// Each judgment copies and checks the remaining suffix, so bound how many
|
|
408
|
+
// one match may request; a match needing more fails closed.
|
|
409
|
+
judgments += 1;
|
|
410
|
+
if (judgments > MAX_DELIMITER_PATH_JUDGMENTS)
|
|
411
|
+
return true;
|
|
412
|
+
if (refusedHostPathAfterDelimiter(matched.slice(index + 1)))
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
return false;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Personal-data signatures: an email that is not a no-reply or documentation
|
|
419
|
+
* address, and phone numbers in their two common written shapes.
|
|
99
420
|
*/
|
|
100
421
|
const PERSONAL_DATA_RULES = [
|
|
101
422
|
{
|
|
@@ -106,13 +427,19 @@ const PERSONAL_DATA_RULES = [
|
|
|
106
427
|
// contact address, even before a colon and a space, is still matched.
|
|
107
428
|
// The ssh:// form permits only the git service account with a URL path.
|
|
108
429
|
// The lookbehind keeps a match from starting inside a local part.
|
|
109
|
-
pattern: /(?<![A-Za-z0-9._%+-])(?!(?<=ssh:\/\/)git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?=\/|:\d+\/))(?!git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}:[A-Za-z0-9_~./-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]
|
|
430
|
+
pattern: /(?<![A-Za-z0-9._%+-])(?!(?<=ssh:\/\/)git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?=\/|:\d+\/))(?!git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}:[A-Za-z0-9_~./-])[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9.-][A-Za-z0-9-]*(?:\.[A-Za-z0-9-]*)*\.[A-Za-z]{2}[A-Za-z0-9-]*|[Ll][Oo][Cc][Aa][Ll][Hh][Oo][Ss][Tt])(?![A-Za-z0-9-]|\.[A-Za-z0-9])/g,
|
|
110
431
|
// GitHub, GitLab and most mail systems use a "noreply" domain or local part
|
|
111
432
|
// for automated identities; anything else in imported issue text is a real
|
|
112
|
-
// person's address and fails closed.
|
|
433
|
+
// person's address and fails closed. RFC 2606/6761 documentation domains
|
|
434
|
+
// (matched exactly after the at-sign) never deliver to a person, so fixture
|
|
435
|
+
// addresses on them are public examples rather than personal data; every
|
|
436
|
+
// registrable look-alike (`notexample.com`, `example.com.evil.io`) still
|
|
437
|
+
// fails closed.
|
|
113
438
|
confirm: (matched) => {
|
|
114
439
|
const [local, domain] = matched.toLowerCase().split("@");
|
|
115
|
-
|
|
440
|
+
if (/^(?:no-?reply)$/.test(local) || /(?:^|\.)no-?reply(?:\.|$)/.test(domain))
|
|
441
|
+
return false;
|
|
442
|
+
return !isReservedDocumentationDomain(domain);
|
|
116
443
|
},
|
|
117
444
|
},
|
|
118
445
|
{
|
|
@@ -143,7 +470,10 @@ const PERSONAL_DATA_RULES = [
|
|
|
143
470
|
* and home-directory usernames (`~user`, but never the anonymous `~/`), which
|
|
144
471
|
* would publish the machine and account that produced an import. The list is
|
|
145
472
|
* the POSIX/Windows set a developer workstation actually leaks, mirroring the
|
|
146
|
-
* repository's own object-store privacy gate.
|
|
473
|
+
* repository's own object-store privacy gate. Each path rule also carries a
|
|
474
|
+
* `confirm` that keeps exactly the public, non-identifying technical forms
|
|
475
|
+
* (see the public-forms helpers above) from failing; everything a real
|
|
476
|
+
* person's machine produces still fails closed.
|
|
147
477
|
*/
|
|
148
478
|
const HOST_PATH_RULES = [
|
|
149
479
|
{
|
|
@@ -155,22 +485,50 @@ const HOST_PATH_RULES = [
|
|
|
155
485
|
// not `/home`). The lead-in is a lookbehind so the match is the path
|
|
156
486
|
// itself; it also admits `file://` URLs and `label:` prefixes (`cwd:/home/…`),
|
|
157
487
|
// whose two-character minimum leaves drive letters to the Windows rule.
|
|
158
|
-
|
|
488
|
+
// Every opening or closing bracket is a lead-in too, so a path hidden
|
|
489
|
+
// directly behind a permitted form (an allowed root followed by `>` or
|
|
490
|
+
// `;`) is still matched on its own. Angle brackets are continuation
|
|
491
|
+
// characters (they carry placeholder segments such as `<pid>`), never a
|
|
492
|
+
// silent stop; the confirm then judges the complete path and refuses a
|
|
493
|
+
// second host path hiding behind any delimiter inside it. A closing tag
|
|
494
|
+
// that names one bare word (`</var>`, `</root>`) is markup, not a path:
|
|
495
|
+
// its slash never starts a match, and a match ends before it, so the tag
|
|
496
|
+
// can never pass as a path segment. `</home/alice>` is still matched.
|
|
497
|
+
pattern: /(?<=^|[\s"'`([=,{<>),;\]}]|[A-Za-z0-9_]{2}:|file:\/\/)(?!(?<=<)\/[A-Za-z]+>)\/(?:home|Users|root|tmp|var|etc|opt|srv|mnt|media|private|Volumes|usr|run|proc|sys|data|nix|scratch|workspace|builds|Library|System)(?=\/|$|[\s"'`<>),;\]}])(?:\/(?:(?!<\/[A-Za-z]+>)[^\s"'`),;\]])*)?/g,
|
|
498
|
+
confirm: (matched) => !isPublicPosixPath(matched) || hidesHostPathBehindDelimiter(matched),
|
|
159
499
|
},
|
|
160
500
|
{
|
|
161
501
|
rule: "windows-host-path",
|
|
162
502
|
// A drive letter at a token start, or as a `file:///C:/…` URL path.
|
|
163
|
-
|
|
503
|
+
// Angle brackets are continuation characters here too, so placeholder
|
|
504
|
+
// segments (`<placeholder>` after a Users root) are matched whole and
|
|
505
|
+
// judged by the confirm instead of truncating the match before them. A
|
|
506
|
+
// match ends before a bare closing tag (`</var>`), as in the POSIX rule.
|
|
507
|
+
pattern: /(?:(?<![A-Za-z0-9_:/])|(?<=file:\/\/\/))[A-Za-z]:[\\/](?:(?!<\/[A-Za-z]+>)[^\s"'`),;\]])+/g,
|
|
508
|
+
confirm: (matched) => !isPublicWindowsPath(matched) || hidesHostPathBehindDelimiter(matched),
|
|
164
509
|
},
|
|
165
510
|
{
|
|
166
511
|
rule: "home-username",
|
|
167
512
|
// A leading tilde followed by a name AND a path separator is a username
|
|
168
513
|
// leak (`~alice/report.txt`); `~/` (anonymous), `~~strikethrough~~`, and
|
|
169
514
|
// prose approximations (`~most users`) are excluded by the boundary and
|
|
170
|
-
// the required trailing slash.
|
|
171
|
-
|
|
515
|
+
// the required trailing slash. The boundary is any character outside the
|
|
516
|
+
// segment-name class and the path separators, the same set the delimiter
|
|
517
|
+
// scan uses, so a `]`, `)` or `;` that ends a host-path match cannot hide
|
|
518
|
+
// a tilde path written straight after it.
|
|
519
|
+
pattern: /(?:^|[^\p{L}\p{N}._~/\\-])~[A-Za-z][A-Za-z0-9._-]*\/[^\s"'`<>),;\]}]*/gu,
|
|
172
520
|
},
|
|
173
521
|
];
|
|
522
|
+
/**
|
|
523
|
+
* Sticky copies of the host-path rule patterns, built once per process and
|
|
524
|
+
* reused for every delimiter suffix. A sticky match at the start of a suffix
|
|
525
|
+
* (or at the URL path of a `file://` prefix) reproduces exactly the rule match
|
|
526
|
+
* that would begin there, so suffixes are judged without re-running the
|
|
527
|
+
* rules — and therefore without re-entering their confirms — recursively.
|
|
528
|
+
*/
|
|
529
|
+
const STICKY_HOST_PATH_PATTERNS = new Map(
|
|
530
|
+
// Keep every flag but the global one (the tilde rule needs `u`).
|
|
531
|
+
HOST_PATH_RULES.map((rule) => [rule.rule, new RegExp(rule.pattern.source, `${rule.pattern.flags.replace("g", "")}y`)]));
|
|
174
532
|
/** Identifier segments whose presence marks an assignment target as secret-bearing. */
|
|
175
533
|
const SECRET_IDENTIFIER_SEGMENTS = [
|
|
176
534
|
"token",
|