Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset. | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import { type BrokeredSecret, fill, misdirectedNote, newlyMisdirected, scrub, scrubHeaders, scrubbable, standInsIn } from "./broker.ts"; | |
| 5 | ||
| 6 | const NPM: BrokeredSecret = { | |
| 7 | standIn: `g1tsec_${"a".repeat(48)}`, | |
| 8 | name: "NPM_TOKEN", | |
| 9 | value: "npm_realvalue123", | |
| 10 | hosts: ["registry.npmjs.org"], | |
| 11 | }; | |
| 12 | const API: BrokeredSecret = { | |
| 13 | standIn: `g1tsec_${"b".repeat(48)}`, | |
| 14 | name: "API_KEY", | |
| 15 | value: "key with spaces&=", | |
| 16 | hosts: ["*.example.com"], | |
| 17 | }; | |
| 18 | const secrets = [NPM, API]; | |
| 19 | ||
| 20 | const basic = (text: string) => `Basic ${btoa(text)}`; | |
| 21 | ||
| 22 | test("stand-ins are found in the URL, the headers and Basic credentials", () => { | |
| 23 | assert.deepEqual(standInsIn(`https://x.example.com/?key=${API.standIn}`, new Headers()), [API.standIn]); | |
| 24 | assert.deepEqual(standInsIn("https://registry.npmjs.org/", new Headers({ authorization: `Bearer ${NPM.standIn}` })), [NPM.standIn]); | |
| 25 | assert.deepEqual(standInsIn("https://registry.npmjs.org/", new Headers({ authorization: basic(`me:${NPM.standIn}`) })), [NPM.standIn]); | |
| 26 | assert.deepEqual(standInsIn("https://registry.npmjs.org/", new Headers({ authorization: "Bearer g1tsec_short" })), []); | |
| 27 | }); | |
| 28 | ||
| 29 | test("a stand-in in a header is filled in for its host", () => { | |
| 30 | const headers = new Headers({ authorization: `Bearer ${NPM.standIn}`, "x-other": "kept" }); | |
| 31 | const out = fill("https://registry.npmjs.org/pkg", headers, secrets); | |
| 32 | assert.equal(out.headers.get("authorization"), "Bearer npm_realvalue123"); | |
| 33 | assert.equal(out.headers.get("x-other"), "kept"); | |
| 34 | assert.deepEqual(out.filled, [NPM]); | |
| 35 | assert.deepEqual(out.misdirected, []); | |
| 36 | }); | |
| 37 | ||
| 38 | test("custom headers and token schemes are filled in as well", () => { | |
| 39 | const headers = new Headers({ authorization: `token ${API.standIn}`, "x-api-key": API.standIn }); | |
| 40 | const out = fill("https://api.example.com/v1", headers, secrets); | |
| 41 | assert.equal(out.headers.get("authorization"), `token ${API.value}`); | |
| 42 | assert.equal(out.headers.get("x-api-key"), API.value); | |
| 43 | }); | |
| 44 | ||
| 45 | test("Basic credentials are decoded, filled in and encoded again", () => { | |
| 46 | const headers = new Headers({ authorization: basic(`me:${NPM.standIn}`) }); | |
| 47 | const out = fill("https://registry.npmjs.org/", headers, secrets); | |
| 48 | assert.equal(out.headers.get("authorization"), basic("me:npm_realvalue123")); | |
| 49 | }); | |
| 50 | ||
| 51 | test("a stand-in in the query string is filled in, encoded", () => { | |
| 52 | const out = fill(`https://api.example.com/v1?key=${API.standIn}&q=1`, new Headers(), secrets); | |
| 53 | assert.equal(new URL(out.url).searchParams.get("key"), API.value); | |
| 54 | assert.equal(new URL(out.url).searchParams.get("q"), "1"); | |
| 55 | assert.ok(!out.url.includes(API.standIn)); | |
| 56 | }); | |
| 57 | ||
| 58 | test("a stand-in sent to another host is left as it is and reported", () => { | |
| 59 | const headers = new Headers({ authorization: `Bearer ${NPM.standIn}` }); | |
| 60 | const out = fill("https://evil.com/steal", headers, secrets); | |
| 61 | assert.equal(out.headers.get("authorization"), `Bearer ${NPM.standIn}`); | |
| 62 | assert.deepEqual(out.filled, []); | |
| 63 | assert.deepEqual(out.misdirected, [NPM]); | |
| 64 | // Not even a host that only looks like it. | |
| 65 | assert.deepEqual(fill("https://registry.npmjs.org.evil.com/", headers, secrets).filled, []); | |
| 66 | assert.deepEqual(fill("https://example.com/", new Headers({ "x-api-key": API.standIn }), secrets).filled, []); | |
| 67 | }); | |
| 68 | ||
| A brokered secret is filled in only over HTTPS. A job's request to one of the secret's hosts over plain HTTP would have carried the real value across the internet readable by anyone on the way; it now goes with the stand-in, and the job's log notes it once, as for a host the secret is not for. The secrets guide says so. | 69 | test("a value is filled in only over HTTPS, even for its own host", () => { |
| 70 | const headers = new Headers({ authorization: `Bearer ${NPM.standIn}` }); | |
| 71 | const plain = fill("http://registry.npmjs.org/pkg", headers, secrets); | |
| 72 | assert.equal(plain.headers.get("authorization"), `Bearer ${NPM.standIn}`); | |
| 73 | assert.deepEqual(plain.filled, []); | |
| 74 | assert.deepEqual(plain.misdirected, [NPM]); | |
| 75 | }); | |
| 76 | ||
| Merge brokered secrets: a secret a job uses only to call an API can be tied to the hosts it is for, and the job gets a stand-in in its place, which g1t fills in on the way out to those hosts and nowhere else, so the value never enters the sandbox; and a request to an allowed host that fails upstream is retried when it can be, or answered with a 502 that says why, rather than reset. | 77 | test("a stand-in the job was not given is left alone, and not reported", () => { |
| 78 | const stranger = `g1tsec_${"c".repeat(48)}`; | |
| 79 | const out = fill("https://registry.npmjs.org/", new Headers({ authorization: `Bearer ${stranger}` }), secrets); | |
| 80 | assert.equal(out.headers.get("authorization"), `Bearer ${stranger}`); | |
| 81 | assert.deepEqual(out.filled, []); | |
| 82 | assert.deepEqual(out.misdirected, []); | |
| 83 | }); | |
| 84 | ||
| 85 | test("a value echoed back is swapped for its stand-in again", () => { | |
| 86 | const headers = scrubHeaders(new Headers({ "x-echo": "Bearer npm_realvalue123", "content-type": "application/json" }), [NPM]); | |
| 87 | assert.equal(headers.get("x-echo"), `Bearer ${NPM.standIn}`); | |
| 88 | assert.equal(scrub('{"token":"npm_realvalue123"}', [NPM]), `{"token":"${NPM.standIn}"}`); | |
| 89 | }); | |
| 90 | ||
| 91 | test("only small, plain, uncompressed answers have their bodies rewritten", () => { | |
| 92 | assert.equal(scrubbable(new Headers({ "content-type": "application/json", "content-length": "120" })), true); | |
| 93 | assert.equal(scrubbable(new Headers({ "content-type": "text/plain; charset=utf-8", "content-length": "10" })), true); | |
| 94 | assert.equal(scrubbable(new Headers({ "content-type": "application/vnd.api+json", "content-length": "10" })), true); | |
| 95 | assert.equal(scrubbable(new Headers({ "content-type": "application/octet-stream", "content-length": "10" })), false); | |
| 96 | assert.equal(scrubbable(new Headers({ "content-type": "application/json", "content-length": "10", "content-encoding": "gzip" })), false); | |
| 97 | assert.equal(scrubbable(new Headers({ "content-type": "application/json" })), false); | |
| 98 | assert.equal(scrubbable(new Headers({ "content-type": "application/json", "content-length": String(10 * 1024 * 1024) })), false); | |
| 99 | }); | |
| 100 | ||
| 101 | test("a misdirected secret is noted once per host", () => { | |
| 102 | const first = newlyMisdirected([], "NPM_TOKEN", "Evil.com:443"); | |
| 103 | assert.ok(first); | |
| 104 | assert.equal(first.note, misdirectedNote("NPM_TOKEN", "evil.com")); | |
| 105 | assert.match(first.note, /^##\[warning\]NPM_TOKEN was sent to evil\.com, which it is not for/); | |
| 106 | assert.equal(newlyMisdirected(first.seen, "NPM_TOKEN", "evil.com"), null); | |
| 107 | assert.ok(newlyMisdirected(first.seen, "NPM_TOKEN", "other.com")); | |
| 108 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.