@keynv/testers 0.1.0-rc.23
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/LICENSE +21 -0
- package/dist/http.d.ts +36 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +80 -0
- package/dist/http.js.map +1 -0
- package/dist/http.test.d.ts +2 -0
- package/dist/http.test.d.ts.map +1 -0
- package/dist/http.test.js +71 -0
- package/dist/http.test.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/mysql.d.ts +25 -0
- package/dist/mysql.d.ts.map +1 -0
- package/dist/mysql.js +60 -0
- package/dist/mysql.js.map +1 -0
- package/dist/postgres.d.ts +25 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +59 -0
- package/dist/postgres.js.map +1 -0
- package/dist/redis.d.ts +25 -0
- package/dist/redis.d.ts.map +1 -0
- package/dist/redis.js +57 -0
- package/dist/redis.js.map +1 -0
- package/dist/run.d.ts +16 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +50 -0
- package/dist/run.js.map +1 -0
- package/dist/run.test.d.ts +2 -0
- package/dist/run.test.d.ts.map +1 -0
- package/dist/run.test.js +69 -0
- package/dist/run.test.js.map +1 -0
- package/dist/sanitize.d.ts +14 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/sanitize.js +29 -0
- package/dist/sanitize.js.map +1 -0
- package/dist/sanitize.test.d.ts +2 -0
- package/dist/sanitize.test.d.ts.map +1 -0
- package/dist/sanitize.test.js +43 -0
- package/dist/sanitize.test.js.map +1 -0
- package/dist/ssh.d.ts +34 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +111 -0
- package/dist/ssh.js.map +1 -0
- package/dist/ssrf.d.ts +17 -0
- package/dist/ssrf.d.ts.map +1 -0
- package/dist/ssrf.js +185 -0
- package/dist/ssrf.js.map +1 -0
- package/dist/ssrf.test.d.ts +2 -0
- package/dist/ssrf.test.d.ts.map +1 -0
- package/dist/ssrf.test.js +104 -0
- package/dist/ssrf.test.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAShD,SAAS,WAAW,CAAI,OAAmB,EAAE,EAAU;IACrD,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,MAAM,CAAC,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,0BAA0B,EAAE,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACpF,CAAC,CAAC,KAAK,EAAE,CAAC;QACV,OAAO,CAAC,IAAI,CACV,CAAC,CAAC,EAAE,EAAE;YACJ,YAAY,CAAC,CAAC,CAAC,CAAC;YAChB,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,EACD,CAAC,GAAG,EAAE,EAAE;YACN,YAAY,CAAC,CAAC,CAAC,CAAC;YAChB,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,IAAa;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,OAAO;YACL,EAAE,EAAE,KAAK;YACT,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;YAC9B,KAAK,EAAE,mBAAmB,eAAe,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;SAC1D,CAAC;IACJ,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,kBAAkB,CAAC;IACvD,IAAI,MAAkB,CAAC;IACvB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAAC;IACpF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,GAAG;YACP,EAAE,EAAE,KAAK;YACT,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;YAC9B,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC;IACJ,CAAC;IACD,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,eAAe,CAAC,KAAe;IACtC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7F,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.test.d.ts","sourceRoot":"","sources":["../src/run.test.ts"],"names":[],"mappings":""}
|
package/dist/run.test.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { runTest } from './run.js';
|
|
4
|
+
const FakeTarget = z.object({ host: z.string().min(1) });
|
|
5
|
+
function fakeTester(behavior) {
|
|
6
|
+
return {
|
|
7
|
+
type: 'http',
|
|
8
|
+
schema: FakeTarget,
|
|
9
|
+
test: behavior,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe('runTest', () => {
|
|
13
|
+
it('returns the tester result on success', async () => {
|
|
14
|
+
const t = fakeTester(async () => ({ ok: true, latency_ms: 10 }));
|
|
15
|
+
const r = await runTest({
|
|
16
|
+
tester: t,
|
|
17
|
+
secret: { alias: '@x.dev.k', value: 'pw' },
|
|
18
|
+
target: { host: 'a' },
|
|
19
|
+
});
|
|
20
|
+
expect(r.ok).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
it('returns a sanitized error from the tester', async () => {
|
|
23
|
+
const t = fakeTester(async () => ({
|
|
24
|
+
ok: false,
|
|
25
|
+
latency_ms: 12,
|
|
26
|
+
error: 'auth failed for hunter2',
|
|
27
|
+
}));
|
|
28
|
+
const r = await runTest({
|
|
29
|
+
tester: t,
|
|
30
|
+
secret: { alias: '@x.dev.k', value: 'hunter2' },
|
|
31
|
+
target: { host: 'a' },
|
|
32
|
+
});
|
|
33
|
+
expect(r.ok).toBe(false);
|
|
34
|
+
expect(r.error).toBe('auth failed for <redacted>');
|
|
35
|
+
});
|
|
36
|
+
it('returns a validation error for a malformed target', async () => {
|
|
37
|
+
const t = fakeTester(async () => ({ ok: true, latency_ms: 0 }));
|
|
38
|
+
const r = await runTest({
|
|
39
|
+
tester: t,
|
|
40
|
+
secret: { alias: '@x.dev.k', value: 'p' },
|
|
41
|
+
target: { host: '' },
|
|
42
|
+
});
|
|
43
|
+
expect(r.ok).toBe(false);
|
|
44
|
+
expect(r.error).toMatch(/invalid target/);
|
|
45
|
+
});
|
|
46
|
+
it('enforces a hard timeout', async () => {
|
|
47
|
+
const t = fakeTester(() => new Promise(() => undefined));
|
|
48
|
+
const r = await runTest({
|
|
49
|
+
tester: t,
|
|
50
|
+
secret: { alias: '@x.dev.k', value: 'p' },
|
|
51
|
+
target: { host: 'a' },
|
|
52
|
+
timeoutMs: 50,
|
|
53
|
+
});
|
|
54
|
+
expect(r.ok).toBe(false);
|
|
55
|
+
expect(r.error).toMatch(/timed out/);
|
|
56
|
+
});
|
|
57
|
+
it('catches synchronous throws from the tester', async () => {
|
|
58
|
+
const t = fakeTester(async () => {
|
|
59
|
+
throw new Error('boom hunter2');
|
|
60
|
+
});
|
|
61
|
+
const r = await runTest({
|
|
62
|
+
tester: t,
|
|
63
|
+
secret: { alias: '@x.dev.k', value: 'hunter2' },
|
|
64
|
+
target: { host: 'a' },
|
|
65
|
+
});
|
|
66
|
+
expect(r.error).toBe('boom <redacted>');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=run.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.test.js","sourceRoot":"","sources":["../src/run.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ResolvedSecret, TestResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Final defense before returning a TestResult to the caller. Even
|
|
4
|
+
* with per-tester sanitization, driver errors sometimes embed
|
|
5
|
+
* connection strings or tokens. This pass:
|
|
6
|
+
*
|
|
7
|
+
* 1. Replaces the resolved value with `<redacted>` if it shows up
|
|
8
|
+
* literally.
|
|
9
|
+
* 2. Replaces any auxiliary fields (e.g., usernames passed by the
|
|
10
|
+
* caller as part of `secret.fields`) the same way.
|
|
11
|
+
* 3. Runs the redactor pattern bank over the error string.
|
|
12
|
+
*/
|
|
13
|
+
export declare function sanitizeResult(result: TestResult, secret: ResolvedSecret): TestResult;
|
|
14
|
+
//# sourceMappingURL=sanitize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.d.ts","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7D;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,UAAU,CAerF"}
|
package/dist/sanitize.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { redact } from '@keynv/redactor';
|
|
2
|
+
/**
|
|
3
|
+
* Final defense before returning a TestResult to the caller. Even
|
|
4
|
+
* with per-tester sanitization, driver errors sometimes embed
|
|
5
|
+
* connection strings or tokens. This pass:
|
|
6
|
+
*
|
|
7
|
+
* 1. Replaces the resolved value with `<redacted>` if it shows up
|
|
8
|
+
* literally.
|
|
9
|
+
* 2. Replaces any auxiliary fields (e.g., usernames passed by the
|
|
10
|
+
* caller as part of `secret.fields`) the same way.
|
|
11
|
+
* 3. Runs the redactor pattern bank over the error string.
|
|
12
|
+
*/
|
|
13
|
+
export function sanitizeResult(result, secret) {
|
|
14
|
+
if (!result.error)
|
|
15
|
+
return result;
|
|
16
|
+
let cleaned = result.error;
|
|
17
|
+
if (secret.value.length > 0) {
|
|
18
|
+
cleaned = cleaned.split(secret.value).join('<redacted>');
|
|
19
|
+
}
|
|
20
|
+
if (secret.fields) {
|
|
21
|
+
for (const v of Object.values(secret.fields)) {
|
|
22
|
+
if (v && v.length > 0)
|
|
23
|
+
cleaned = cleaned.split(v).join('<redacted>');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
cleaned = redact(cleaned).text;
|
|
27
|
+
return { ...result, error: cleaned };
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=sanitize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.js","sourceRoot":"","sources":["../src/sanitize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAGzC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,MAAkB,EAAE,MAAsB;IACvE,IAAI,CAAC,MAAM,CAAC,KAAK;QAAE,OAAO,MAAM,CAAC;IAEjC,IAAI,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC5B,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IAC3D,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QAClB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;gBAAE,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IACD,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC;IAE/B,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.test.d.ts","sourceRoot":"","sources":["../src/sanitize.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { sanitizeResult } from './sanitize.js';
|
|
3
|
+
describe('sanitizeResult', () => {
|
|
4
|
+
it('redacts the literal secret value when it appears in the error', () => {
|
|
5
|
+
const secret = { alias: '@x.dev.k', value: 'super-pass-123' };
|
|
6
|
+
const result = {
|
|
7
|
+
ok: false,
|
|
8
|
+
latency_ms: 12,
|
|
9
|
+
error: 'auth failed for super-pass-123',
|
|
10
|
+
};
|
|
11
|
+
expect(sanitizeResult(result, secret).error).toBe('auth failed for <redacted>');
|
|
12
|
+
});
|
|
13
|
+
it('redacts auxiliary fields too', () => {
|
|
14
|
+
const secret = {
|
|
15
|
+
alias: '@x.dev.k',
|
|
16
|
+
value: 'pass',
|
|
17
|
+
fields: { username: 'super-rare-user' },
|
|
18
|
+
};
|
|
19
|
+
const result = {
|
|
20
|
+
ok: false,
|
|
21
|
+
latency_ms: 12,
|
|
22
|
+
error: "no such user 'super-rare-user'",
|
|
23
|
+
};
|
|
24
|
+
expect(sanitizeResult(result, secret).error).toBe("no such user '<redacted>'");
|
|
25
|
+
});
|
|
26
|
+
it('runs the redactor pattern bank over the error', () => {
|
|
27
|
+
const secret = { alias: '@x.dev.k', value: 'irrelevant' };
|
|
28
|
+
const result = {
|
|
29
|
+
ok: false,
|
|
30
|
+
latency_ms: 12,
|
|
31
|
+
error: 'connection failed: postgres://app:hunter2@db.example.com/x',
|
|
32
|
+
};
|
|
33
|
+
const cleaned = sanitizeResult(result, secret).error ?? '';
|
|
34
|
+
expect(cleaned).toContain('<REDACTED:postgres-uri>');
|
|
35
|
+
expect(cleaned).not.toContain('hunter2');
|
|
36
|
+
});
|
|
37
|
+
it('passes ok=true results through unchanged', () => {
|
|
38
|
+
const secret = { alias: '@x.dev.k', value: 'p' };
|
|
39
|
+
const result = { ok: true, latency_ms: 24 };
|
|
40
|
+
expect(sanitizeResult(result, secret)).toEqual(result);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
//# sourceMappingURL=sanitize.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize.test.js","sourceRoot":"","sources":["../src/sanitize.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAG/C,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,MAAM,GAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;QAC9E,MAAM,MAAM,GAAe;YACzB,EAAE,EAAE,KAAK;YACT,UAAU,EAAE,EAAE;YACd,KAAK,EAAE,gCAAgC;SACxC,CAAC;QACF,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;IAClF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,MAAM,GAAmB;YAC7B,KAAK,EAAE,UAAU;YACjB,KAAK,EAAE,MAAM;YACb,MAAM,EAAE,EAAE,QAAQ,EAAE,iBAAiB,EAAE;SACxC,CAAC;QACF,MAAM,MAAM,GAAe;YACzB,EAAE,EAAE,KAAK;YACT,UAAU,EAAE,EAAE;YACd,KAAK,EAAE,gCAAgC;SACxC,CAAC;QACF,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;IACjF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,MAAM,GAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;QAC1E,MAAM,MAAM,GAAe;YACzB,EAAE,EAAE,KAAK;YACT,UAAU,EAAE,EAAE;YACd,KAAK,EAAE,4DAA4D;SACpE,CAAC;QACF,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC3D,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,yBAAyB,CAAC,CAAC;QACrD,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,MAAM,GAAmB,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QACjE,MAAM,MAAM,GAAe,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC;QACxD,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/ssh.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { Tester } from './types.js';
|
|
3
|
+
declare const Target: z.ZodObject<{
|
|
4
|
+
host: z.ZodString;
|
|
5
|
+
port: z.ZodDefault<z.ZodNumber>;
|
|
6
|
+
user: z.ZodString;
|
|
7
|
+
/**
|
|
8
|
+
* 'password' uses secret.value as a password; 'key' uses
|
|
9
|
+
* secret.value as a PEM-encoded private key.
|
|
10
|
+
*/
|
|
11
|
+
auth: z.ZodDefault<z.ZodEnum<["password", "key"]>>;
|
|
12
|
+
/**
|
|
13
|
+
* Expected SHA-256 fingerprint of the server host public key
|
|
14
|
+
* (base64, no padding). When set, the connection is rejected if
|
|
15
|
+
* the key does not match — preventing MITM attacks.
|
|
16
|
+
*/
|
|
17
|
+
host_key_sha256: z.ZodOptional<z.ZodString>;
|
|
18
|
+
}, "strip", z.ZodTypeAny, {
|
|
19
|
+
auth: "key" | "password";
|
|
20
|
+
user: string;
|
|
21
|
+
host: string;
|
|
22
|
+
port: number;
|
|
23
|
+
host_key_sha256?: string | undefined;
|
|
24
|
+
}, {
|
|
25
|
+
user: string;
|
|
26
|
+
host: string;
|
|
27
|
+
auth?: "key" | "password" | undefined;
|
|
28
|
+
port?: number | undefined;
|
|
29
|
+
host_key_sha256?: string | undefined;
|
|
30
|
+
}>;
|
|
31
|
+
type SshTarget = z.infer<typeof Target>;
|
|
32
|
+
export declare const sshTester: Tester<SshTarget>;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=ssh.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh.d.ts","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EAA8B,MAAM,EAAE,MAAM,YAAY,CAAC;AAErE,QAAA,MAAM,MAAM;;;;IAIV;;;OAGG;;IAEH;;;;OAIG;;;;;;;;;;;;;;EAEH,CAAC;AAEH,KAAK,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAExC,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,SAAS,CA6FvC,CAAC"}
|
package/dist/ssh.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { isBlockedHostResolved } from './ssrf.js';
|
|
4
|
+
const Target = z.object({
|
|
5
|
+
host: z.string().min(1),
|
|
6
|
+
port: z.coerce.number().int().min(1).max(65535).default(22),
|
|
7
|
+
user: z.string().min(1),
|
|
8
|
+
/**
|
|
9
|
+
* 'password' uses secret.value as a password; 'key' uses
|
|
10
|
+
* secret.value as a PEM-encoded private key.
|
|
11
|
+
*/
|
|
12
|
+
auth: z.enum(['password', 'key']).default('password'),
|
|
13
|
+
/**
|
|
14
|
+
* Expected SHA-256 fingerprint of the server host public key
|
|
15
|
+
* (base64, no padding). When set, the connection is rejected if
|
|
16
|
+
* the key does not match — preventing MITM attacks.
|
|
17
|
+
*/
|
|
18
|
+
host_key_sha256: z.string().min(1).optional(),
|
|
19
|
+
});
|
|
20
|
+
export const sshTester = {
|
|
21
|
+
type: 'ssh',
|
|
22
|
+
schema: Target,
|
|
23
|
+
async test(secret, target) {
|
|
24
|
+
if (await isBlockedHostResolved(target.host)) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
latency_ms: 0,
|
|
28
|
+
error: 'Target host is blocked (private/internal IP or metadata endpoint).',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
const { Client } = await import('ssh2');
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const client = new Client();
|
|
35
|
+
let settled = false;
|
|
36
|
+
const settle = (result) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
try {
|
|
41
|
+
client.end();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* ignore */
|
|
45
|
+
}
|
|
46
|
+
resolve(result);
|
|
47
|
+
};
|
|
48
|
+
client.once('ready', () => {
|
|
49
|
+
client.exec('true', (err, stream) => {
|
|
50
|
+
if (err) {
|
|
51
|
+
settle({ ok: false, latency_ms: Date.now() - start, error: err.message });
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
stream
|
|
55
|
+
.on('close', (code) => {
|
|
56
|
+
settle({
|
|
57
|
+
ok: code === 0,
|
|
58
|
+
latency_ms: Date.now() - start,
|
|
59
|
+
...(code === 0 ? {} : { error: `remote exit code ${code}` }),
|
|
60
|
+
});
|
|
61
|
+
})
|
|
62
|
+
.on('error', (e) => {
|
|
63
|
+
settle({ ok: false, latency_ms: Date.now() - start, error: e.message });
|
|
64
|
+
});
|
|
65
|
+
// We don't need stdout/stderr; just drain.
|
|
66
|
+
stream.resume();
|
|
67
|
+
stream.stderr?.resume();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
client.once('error', (err) => {
|
|
71
|
+
settle({ ok: false, latency_ms: Date.now() - start, error: err.message });
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
const connectOpts = {
|
|
75
|
+
host: target.host,
|
|
76
|
+
port: target.port,
|
|
77
|
+
username: target.user,
|
|
78
|
+
readyTimeout: 5000,
|
|
79
|
+
};
|
|
80
|
+
if (target.auth === 'password') {
|
|
81
|
+
connectOpts.password = secret.value;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
connectOpts.privateKey = secret.value;
|
|
85
|
+
}
|
|
86
|
+
if (target.host_key_sha256) {
|
|
87
|
+
const expected = target.host_key_sha256;
|
|
88
|
+
connectOpts.hostVerifier = (key) => {
|
|
89
|
+
if (typeof key === 'string') {
|
|
90
|
+
return (createHash('sha256').update(key, 'utf8').digest('base64').replace(/=+$/, '') ===
|
|
91
|
+
expected);
|
|
92
|
+
}
|
|
93
|
+
if (Buffer.isBuffer(key)) {
|
|
94
|
+
return (createHash('sha256').update(key).digest('base64').replace(/=+$/, '') === expected);
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
client.connect(connectOpts);
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
settle({
|
|
103
|
+
ok: false,
|
|
104
|
+
latency_ms: Date.now() - start,
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=ssh.js.map
|
package/dist/ssh.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssh.js","sourceRoot":"","sources":["../src/ssh.ts"],"names":[],"mappings":"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"}
|
package/dist/ssrf.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronous classification of a host *literal*. Returns true for blocked
|
|
3
|
+
* hostnames and for IP literals (IPv4 dotted/numeric, IPv6) that resolve to
|
|
4
|
+
* an internal range. Returns false for a plain hostname — use
|
|
5
|
+
* {@link isBlockedHostResolved} to also check what it resolves to.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isBlockedHost(host: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Like {@link isBlockedHost} but, for a hostname, also resolves it via DNS
|
|
10
|
+
* and blocks if ANY resolved address is internal. Async because it does I/O.
|
|
11
|
+
*/
|
|
12
|
+
export declare function isBlockedHostResolved(host: string): Promise<boolean>;
|
|
13
|
+
/** Synchronous URL guard (literal host only). */
|
|
14
|
+
export declare function isBlockedUrl(urlStr: string): boolean;
|
|
15
|
+
/** URL guard that also resolves the hostname via DNS. */
|
|
16
|
+
export declare function isBlockedUrlResolved(urlStr: string): Promise<boolean>;
|
|
17
|
+
//# sourceMappingURL=ssrf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssrf.d.ts","sourceRoot":"","sources":["../src/ssrf.ts"],"names":[],"mappings":"AA2FA;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAmBnD;AAMD;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa1E;AAED,iDAAiD;AACjD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQpD;AAED,yDAAyD;AACzD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAS3E"}
|
package/dist/ssrf.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF protection for testers. A tester runs with a real secret value and
|
|
3
|
+
* the user's connection target, so it must never be coaxed into hitting an
|
|
4
|
+
* internal/loopback/link-local/metadata address.
|
|
5
|
+
*
|
|
6
|
+
* Defense in depth:
|
|
7
|
+
* - Literal-IP classification covers IPv4 (incl. decimal/hex integer forms),
|
|
8
|
+
* IPv6 (loopback, unspecified, link-local fe80::/10, ULA fc00::/7,
|
|
9
|
+
* multicast, IPv4-mapped ::ffff:a.b.c.d), and the reserved/TEST-NET ranges.
|
|
10
|
+
* - {@link isBlockedHostResolved} additionally resolves a *hostname* via DNS
|
|
11
|
+
* and rejects it if ANY resolved address is internal. This closes the
|
|
12
|
+
* "public DNS name with an A record pointing at 127.0.0.1 / 169.254.169.254"
|
|
13
|
+
* bypass that a pure string blacklist cannot see.
|
|
14
|
+
*
|
|
15
|
+
* Residual: a determined attacker controlling DNS can still rebind between
|
|
16
|
+
* our check and the client library's own resolution (TOCTOU). Pinning the
|
|
17
|
+
* resolved IP into every client (pg/mysql/redis/ssh) without breaking TLS
|
|
18
|
+
* SNI is out of scope here; the resolved check removes the trivial bypass.
|
|
19
|
+
*/
|
|
20
|
+
import { lookup } from 'node:dns/promises';
|
|
21
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
22
|
+
'localhost',
|
|
23
|
+
'metadata.google.internal',
|
|
24
|
+
'metadata.internal',
|
|
25
|
+
'instance-data',
|
|
26
|
+
]);
|
|
27
|
+
function isLoopbackAllowed() {
|
|
28
|
+
return !!process.env['KEYNV_ALLOW_LOOPBACK_TESTERS'];
|
|
29
|
+
}
|
|
30
|
+
/** Strip brackets and an IPv6 zone id; lowercase. */
|
|
31
|
+
function normalizeHost(host) {
|
|
32
|
+
return host.trim().toLowerCase().replace(/^\[/, '').replace(/\]$/, '').replace(/%.*$/, '');
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* If `host` is a bare integer (2130706433) or hex (0x7f000001) IPv4, return
|
|
36
|
+
* its dotted-quad form; otherwise null. These numeric forms are accepted by
|
|
37
|
+
* many resolvers/clients and would otherwise slip past dotted-quad CIDRs.
|
|
38
|
+
*/
|
|
39
|
+
function normalizeNumericIPv4(host) {
|
|
40
|
+
let n = null;
|
|
41
|
+
if (/^\d+$/.test(host))
|
|
42
|
+
n = Number(host);
|
|
43
|
+
else if (/^0x[0-9a-f]+$/i.test(host))
|
|
44
|
+
n = Number.parseInt(host.slice(2), 16);
|
|
45
|
+
if (n === null || !Number.isInteger(n) || n < 0 || n > 0xffffffff)
|
|
46
|
+
return null;
|
|
47
|
+
return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.');
|
|
48
|
+
}
|
|
49
|
+
function isDottedIPv4(host) {
|
|
50
|
+
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host);
|
|
51
|
+
}
|
|
52
|
+
/** True if a dotted-quad IPv4 is private/loopback/link-local/reserved/metadata. */
|
|
53
|
+
function isPrivateIPv4(ip) {
|
|
54
|
+
const parts = ip.split('.').map((p) => Number(p));
|
|
55
|
+
if (parts.length !== 4 || parts.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const [a, b, c] = parts;
|
|
59
|
+
if (a === 0)
|
|
60
|
+
return true; // 0.0.0.0/8 "this network"
|
|
61
|
+
if (a === 10)
|
|
62
|
+
return true; // 10.0.0.0/8
|
|
63
|
+
if (a === 127)
|
|
64
|
+
return true; // loopback 127.0.0.0/8
|
|
65
|
+
if (a === 169 && b === 254)
|
|
66
|
+
return true; // link-local + cloud metadata 169.254.169.254
|
|
67
|
+
if (a === 172 && b >= 16 && b <= 31)
|
|
68
|
+
return true; // 172.16.0.0/12
|
|
69
|
+
if (a === 192 && b === 0 && (c === 0 || c === 2))
|
|
70
|
+
return true; // 192.0.0.0/24, TEST-NET-1
|
|
71
|
+
if (a === 192 && b === 168)
|
|
72
|
+
return true; // 192.168.0.0/16
|
|
73
|
+
if (a === 198 && (b === 18 || b === 19))
|
|
74
|
+
return true; // 198.18.0.0/15 benchmarking
|
|
75
|
+
if (a === 198 && b === 51 && c === 100)
|
|
76
|
+
return true; // TEST-NET-2
|
|
77
|
+
if (a === 203 && b === 0 && c === 113)
|
|
78
|
+
return true; // TEST-NET-3
|
|
79
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
80
|
+
return true; // CGNAT 100.64.0.0/10
|
|
81
|
+
if (a >= 224)
|
|
82
|
+
return true; // multicast 224/4 + reserved 240/4 + 255.255.255.255
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
/** True if an IPv6 literal is loopback/unspecified/link-local/ULA/multicast/mapped-internal. */
|
|
86
|
+
function isPrivateIPv6(ip) {
|
|
87
|
+
const s = normalizeHost(ip);
|
|
88
|
+
// IPv4-mapped / -embedded in dotted form: ::ffff:127.0.0.1, ::127.0.0.1
|
|
89
|
+
const mapped = s.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
|
|
90
|
+
if (s.startsWith('::') && mapped)
|
|
91
|
+
return isPrivateIPv4(mapped[1]);
|
|
92
|
+
if (s === '::1')
|
|
93
|
+
return true; // loopback
|
|
94
|
+
if (s === '::')
|
|
95
|
+
return true; // unspecified
|
|
96
|
+
if (/^fe[89ab]/.test(s))
|
|
97
|
+
return true; // link-local fe80::/10
|
|
98
|
+
if (/^f[cd]/.test(s))
|
|
99
|
+
return true; // unique local fc00::/7
|
|
100
|
+
if (/^ff/.test(s))
|
|
101
|
+
return true; // multicast ff00::/8
|
|
102
|
+
if (/^fec/.test(s))
|
|
103
|
+
return true; // deprecated site-local fec0::/10
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Synchronous classification of a host *literal*. Returns true for blocked
|
|
108
|
+
* hostnames and for IP literals (IPv4 dotted/numeric, IPv6) that resolve to
|
|
109
|
+
* an internal range. Returns false for a plain hostname — use
|
|
110
|
+
* {@link isBlockedHostResolved} to also check what it resolves to.
|
|
111
|
+
*/
|
|
112
|
+
export function isBlockedHost(host) {
|
|
113
|
+
let h = normalizeHost(host);
|
|
114
|
+
if (BLOCKED_HOSTNAMES.has(h)) {
|
|
115
|
+
if (isLoopbackAllowed() && h === 'localhost')
|
|
116
|
+
return false;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
const numeric = normalizeNumericIPv4(h);
|
|
120
|
+
if (numeric)
|
|
121
|
+
h = numeric;
|
|
122
|
+
if (isDottedIPv4(h)) {
|
|
123
|
+
const blocked = isPrivateIPv4(h);
|
|
124
|
+
if (blocked && isLoopbackAllowed() && /^127\./.test(h))
|
|
125
|
+
return false;
|
|
126
|
+
return blocked;
|
|
127
|
+
}
|
|
128
|
+
if (h.includes(':')) {
|
|
129
|
+
const blocked = isPrivateIPv6(h);
|
|
130
|
+
if (blocked && isLoopbackAllowed() && h === '::1')
|
|
131
|
+
return false;
|
|
132
|
+
return blocked;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function isIpLiteral(h) {
|
|
137
|
+
return isDottedIPv4(h) || h.includes(':') || normalizeNumericIPv4(h) !== null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Like {@link isBlockedHost} but, for a hostname, also resolves it via DNS
|
|
141
|
+
* and blocks if ANY resolved address is internal. Async because it does I/O.
|
|
142
|
+
*/
|
|
143
|
+
export async function isBlockedHostResolved(host) {
|
|
144
|
+
if (isBlockedHost(host))
|
|
145
|
+
return true;
|
|
146
|
+
const h = normalizeHost(host);
|
|
147
|
+
// Already an IP literal → isBlockedHost was authoritative.
|
|
148
|
+
if (isIpLiteral(h))
|
|
149
|
+
return false;
|
|
150
|
+
let addrs;
|
|
151
|
+
try {
|
|
152
|
+
addrs = await lookup(host, { all: true });
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Unresolvable: let the tester surface its own connection error.
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return addrs.some((a) => isBlockedHost(a.address));
|
|
159
|
+
}
|
|
160
|
+
/** Synchronous URL guard (literal host only). */
|
|
161
|
+
export function isBlockedUrl(urlStr) {
|
|
162
|
+
try {
|
|
163
|
+
const parsed = new URL(urlStr);
|
|
164
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
165
|
+
return true;
|
|
166
|
+
return isBlockedHost(parsed.hostname);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** URL guard that also resolves the hostname via DNS. */
|
|
173
|
+
export async function isBlockedUrlResolved(urlStr) {
|
|
174
|
+
let parsed;
|
|
175
|
+
try {
|
|
176
|
+
parsed = new URL(urlStr);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
182
|
+
return true;
|
|
183
|
+
return isBlockedHostResolved(parsed.hostname);
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=ssrf.js.map
|
package/dist/ssrf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../src/ssrf.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ssrf.test.d.ts","sourceRoot":"","sources":["../src/ssrf.test.ts"],"names":[],"mappings":""}
|