redac---n 0.0.3 → 0.0.5
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/README.md +11 -10
- package/dist/calls.d.ts +8 -0
- package/dist/calls.js +60 -3
- package/dist/checksums.js +4 -3
- package/dist/commands.d.ts +5 -0
- package/dist/commands.js +93 -14
- package/dist/context.d.ts +5 -2
- package/dist/context.js +36 -9
- package/dist/encoded.js +13 -6
- package/dist/environment.d.ts +5 -3
- package/dist/environment.js +38 -14
- package/dist/formats.js +15 -0
- package/dist/index.js +10 -4
- package/dist/json.js +29 -5
- package/dist/known.d.ts +7 -7
- package/dist/known.js +33 -25
- package/dist/named.d.ts +6 -0
- package/dist/named.js +421 -125
- package/dist/names.d.ts +5 -1
- package/dist/names.js +143 -18
- package/dist/normalize.d.ts +4 -1
- package/dist/normalize.js +37 -11
- package/dist/pem.d.ts +2 -0
- package/dist/pem.js +12 -0
- package/dist/policy.js +12 -5
- package/dist/public.js +35 -13
- package/dist/secretfile.d.ts +5 -0
- package/dist/secretfile.js +49 -20
- package/dist/shell.d.ts +12 -3
- package/dist/shell.js +51 -7
- package/dist/spans.d.ts +4 -3
- package/dist/spans.js +79 -8
- package/dist/sql.d.ts +12 -8
- package/dist/sql.js +94 -16
- package/dist/stream.d.ts +2 -1
- package/dist/stream.js +15 -3
- package/dist/structure.js +210 -61
- package/dist/syntax.d.ts +5 -2
- package/dist/syntax.js +7 -4
- package/dist/transcript.js +116 -28
- package/dist/unnamed.js +128 -20
- package/dist/urls.d.ts +8 -3
- package/dist/urls.js +16 -7
- package/dist/values.d.ts +2 -1
- package/dist/values.js +70 -25
- package/docs/holdout.svg +8 -8
- package/docs/layers.md +1 -1
- package/package.json +1 -1
package/dist/secretfile.d.ts
CHANGED
|
@@ -16,6 +16,11 @@ export declare function findInSecretFile(text: string, { part, path }?: {
|
|
|
16
16
|
* `--output text` rows of AWS's.
|
|
17
17
|
*/
|
|
18
18
|
export declare function findPrintedSecret(text: string): Finding[];
|
|
19
|
+
/**
|
|
20
|
+
* Whether a line is a JSON document whole: an object, an array, or a string that holds one (output encoded once more,
|
|
21
|
+
* `--query SecretString`'s `"{\"password\": …}"`). A string that holds anything else is a value (jsonString).
|
|
22
|
+
*/
|
|
23
|
+
export declare function isJsonDocument(line: string): boolean;
|
|
19
24
|
/**
|
|
20
25
|
* Where the first prompt line inside a command's output starts (a later command), or its length if none. (Not a `!`
|
|
21
26
|
* line: in a command's output that is a value or an option file's directive, not a notebook's command.)
|
package/dist/secretfile.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* output under a prompt in a pasted session; and the places a secret file's content sits inside other text (a
|
|
5
5
|
* diff's hunks, a heredoc that writes one, `echo … > ~/.vault_pass`).
|
|
6
6
|
*/
|
|
7
|
-
import { commandsRun, holdsOnlySecrets, isConfigFileName, isSecretFile, printsSecret, printsSecretFile, } from "./commands.js";
|
|
7
|
+
import { commandsRun, holdsOnlySecrets, isConfigFileName, isSecretFile, pipes, printsSecret, printsSecretFile, } from "./commands.js";
|
|
8
8
|
import { allRedirects, LineIndex, programName, PROMPT, simpleCommand, wordSpan } from "./shell.js";
|
|
9
9
|
import { naturalness } from "./charlm.js";
|
|
10
10
|
import { isPlaceholder } from "./public.js";
|
|
@@ -53,6 +53,11 @@ const MIN_UNNAMED_TOKEN = 24;
|
|
|
53
53
|
function isBareVendorWord(name) {
|
|
54
54
|
return /^[A-Z][A-Z0-9]{2,30}$/.test(name);
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* What a setting names in numbers and lower-case words: a platform (`linux/amd64`, `linux/arm64/v8`), a size
|
|
58
|
+
* (`1920x1080`), a port and its protocol (`8080/tcp`, `5000-5010/udp`).
|
|
59
|
+
*/
|
|
60
|
+
const PLATFORM_OR_MEASURE = /^[a-z][a-z0-9_.-]{0,30}(?:\/[a-z0-9][a-z0-9_.-]{0,30}){1,3}$|^\d+x\d+$|^\d+(?:-\d+)?\/(?:tcp|udp|sctp)$/;
|
|
56
61
|
/** A host's name, and its port: `db.internal:5432`, `abc123def456.ngrok.io`. */
|
|
57
62
|
const HOST = /^[\w-]+(?:\.[a-z][\w-]*)+(?::\d+)?$/;
|
|
58
63
|
/**
|
|
@@ -65,6 +70,7 @@ function isSettingShaped(v) {
|
|
|
65
70
|
isPasswordlessUrl(v) ||
|
|
66
71
|
/^[\w.+-]+@[\w-]+(?:\.[\w-]+)+$/.test(v) ||
|
|
67
72
|
/^v?\d+(?:[.:-]\d+)*$/.test(v) ||
|
|
73
|
+
PLATFORM_OR_MEASURE.test(v) ||
|
|
68
74
|
HOST.test(v) ||
|
|
69
75
|
isWordySetting(v) ||
|
|
70
76
|
isIdentifierShaped(v) ||
|
|
@@ -168,9 +174,10 @@ function isOneValueLine(line) {
|
|
|
168
174
|
const nameShaped = named !== undefined && /^(?:[a-z_][a-z0-9_.-]*|[A-Z_][A-Z0-9_.-]*|[a-z]+(?:[A-Z][a-z]+)+)$/.test(named);
|
|
169
175
|
if (line.length > 1000 || /[=:]\s|:\/\//.test(line) || nameShaped)
|
|
170
176
|
return false;
|
|
171
|
-
// One token in brackets is the value, brackets and all: `{Kd8pQ2zX9mL}`.
|
|
172
|
-
|
|
173
|
-
|
|
177
|
+
// One token in brackets is the value, brackets and all: `{Kd8pQ2zX9mL}`. (Not an empty document, `{}`, `[]`, nor an
|
|
178
|
+
// INI file's section, `[default]`, `[core]`: its name reads as a setting's.)
|
|
179
|
+
if (/^(?:\[[^\s\]]+\]|\{[^\s}]+\}|<[^\s>]+>)$/.test(line) && !/[:,=]/.test(line))
|
|
180
|
+
return !(line.startsWith("[") && isSettingShaped(line.slice(1, -1)));
|
|
174
181
|
// (A netrc entry is read by its keywords, spacedKeywords: `machine github.com login x password ${GITHUB_TOKEN}`.)
|
|
175
182
|
const structured = /^#[ \t!#]|^\[[^\]]*\]$|^\{[\s\S]*\}$|^<[\s\S]*>$|^["{[][\s\S]*[:,]|^(?:machine[ \t]+\S+|default)[ \t]+(?:login|password|account)[ \t]/.test(line);
|
|
176
183
|
// (Nor a keyword and a reference: `requirepass ${REDIS_PASSWORD}`, its value read elsewhere.)
|
|
@@ -245,7 +252,10 @@ const TABLE_WORD = /^(?:true|false|null|n\/a|-+|values?|data|type|description)$/
|
|
|
245
252
|
function tableRows(text, out) {
|
|
246
253
|
for (const m of text.matchAll(/^(?:[ \t]*[│|┃║][ \t]*|[ \t]*)([\w.\-/@]{1,200})(?:[ \t]{2,}|[ \t]*[│|┃║][ \t]*)([^\s│|┃║](?:[^│|┃║\r\n]{0,4000}?[^\s│|┃║])?)[ \t]*(?:[│|┃║][^\r\n]*)?\r?$/dgm)) {
|
|
247
254
|
const [, name, value] = m;
|
|
248
|
-
|
|
255
|
+
// (A value that starts with `=` or a `:` is a padded assignment's, `db_password = "…"` in a tfvars file: the
|
|
256
|
+
// assignments' reader reads it whole.)
|
|
257
|
+
if (/^(?:=|:(?!\/\/))/.test(value) ||
|
|
258
|
+
!isCredentialName(name) ||
|
|
249
259
|
isNotSecret(value) ||
|
|
250
260
|
isPlaceholder(value) ||
|
|
251
261
|
TABLE_WORD.test(value) ||
|
|
@@ -300,9 +310,9 @@ function cookiesTxt(text, out) {
|
|
|
300
310
|
out.push({ start: at(m, 2)[0], end: at(m, 2)[1], kind: "cookie", rule: "file:cookies", confidence: 0.95 });
|
|
301
311
|
}
|
|
302
312
|
}
|
|
303
|
-
/** htpasswd: `user:<hash>`, a crypt hash's `$id$` (`$apr1$…`, `$2y$…`) or `{SSHA}
|
|
313
|
+
/** htpasswd: `user:<hash>`, a crypt hash's `$id$` (`$apr1$…`, `$2y$…`) or LDAP's `{SSHA}…`. (Not `admin:$ADMIN_PASSWORD`.) */
|
|
304
314
|
function htpasswd(text, out) {
|
|
305
|
-
for (const m of text.matchAll(/^[\w.@-]{1,100}:(\$[\w.+=-]{1,30}\$[\w$./+=-]{8,300}|\{
|
|
315
|
+
for (const m of text.matchAll(/^[\w.@-]{1,100}:(\$[\w.+=-]{1,30}\$[\w$./+=-]{8,300}|\{[A-Z][A-Z\d-]{1,20}\}[\w+/=.$-]{10,300})[ \t]*$/dgm))
|
|
306
316
|
out.push({ start: at(m, 1)[0], end: at(m, 1)[1], kind: "password hash", rule: "file:hash", confidence: 1 });
|
|
307
317
|
}
|
|
308
318
|
/**
|
|
@@ -431,13 +441,19 @@ function firstLine(text) {
|
|
|
431
441
|
export function findPrintedSecret(text) {
|
|
432
442
|
const out = [];
|
|
433
443
|
const add = (start, end) => out.push({ start, end, kind: "credential", rule: "command:prints-secret", confidence: 1 });
|
|
434
|
-
// `--output text` rows: SSM's value follows its type,
|
|
435
|
-
|
|
444
|
+
// `--output text` rows: SSM's value follows its type (a `PARAMETER` row, or `get-parameters`' `PARAMETERS`), Secrets
|
|
445
|
+
// Manager's is the fourth field.
|
|
446
|
+
for (const m of text.matchAll(/^(?:PARAMETERS?\t[^\n]*?\t(?:SecureString|String|StringList)\t([^\t\n]+)\t\d+|arn:aws:secretsmanager:[^\t\n]+\t[^\t\n]*\t[^\t\n]*\t([^\t\n]+)(?:\t[^\n]*)?)\r?$/dgm)) {
|
|
436
447
|
const [start, end] = at(m, m[1] !== undefined ? 1 : 2);
|
|
437
448
|
add(start, end);
|
|
438
449
|
}
|
|
439
450
|
if (out.length)
|
|
440
451
|
return out;
|
|
452
|
+
// Output in an env file's lines (`heroku config:get -s`, `netlify env:list --plain`, a pulled `.env` read back): read
|
|
453
|
+
// as that file, each value by its name, so a `NEXT_PUBLIC_…` setting stays.
|
|
454
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
455
|
+
if (lines.length && lines.every((l) => /^[ \t]*(?:export[ \t]+)?[A-Za-z_][\w.]*=/.test(l)))
|
|
456
|
+
return findInSecretFile(text, {});
|
|
441
457
|
// (A path is where a credential is, not one: `GOOGLE_APPLICATION_CREDENTIALS=/workspace/secrets/sa.json`.)
|
|
442
458
|
const notSecret = (line) => isInert(line) ||
|
|
443
459
|
isPath(line.replace(/^[A-Za-z_][\w.-]*=/, "")) ||
|
|
@@ -451,13 +467,23 @@ export function findPrintedSecret(text) {
|
|
|
451
467
|
const quote = firstValue === first.value ? 0 : 1;
|
|
452
468
|
if (firstValue && !notSecret(firstValue) && !/^[{[]$/.test(firstValue) && readsAsValue(firstValue))
|
|
453
469
|
add(first.start + quote, first.start + first.value.length - quote);
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
470
|
+
// (Up to a blank line: what follows one is no longer the command's output, `Perfect!` from whoever ran it. A markup
|
|
471
|
+
// tag's closer ends the output too, `</tool_result>`, on its own line or after the last one, `honeybear</output>`; an
|
|
472
|
+
// opener, `<output>`, is the harness's wrapper, not a value.)
|
|
473
|
+
const blank = /\n[ \t]*\r?\n/.exec(text.slice(firstEnd));
|
|
474
|
+
const rest = text.slice(firstEnd, blank ? firstEnd + blank.index : text.length);
|
|
475
|
+
for (const m of rest.matchAll(/^[ \t]*(\S{6,})[ \t]*\r?$/dgm)) {
|
|
476
|
+
const closed = /<\/[A-Za-z][\w:.-]*>$/.exec(m[1]);
|
|
477
|
+
const token = closed ? m[1].slice(0, closed.index) : m[1];
|
|
478
|
+
if (/^<[A-Za-z][\w:.-]*>$/.test(token))
|
|
459
479
|
continue;
|
|
460
|
-
|
|
480
|
+
const value = jsonString(token) ?? token;
|
|
481
|
+
const q = value === token ? 0 : 1;
|
|
482
|
+
// (A message's tokens are not values; `[`/`{` start a JSON document's lines.)
|
|
483
|
+
if (value.length >= 6 && !isMessageToken(value) && !/^[{[]/.test(value) && !notSecret(value))
|
|
484
|
+
add(firstEnd + at(m, 1)[0] + q, firstEnd + at(m, 1)[0] + token.length - q);
|
|
485
|
+
if (closed)
|
|
486
|
+
break;
|
|
461
487
|
}
|
|
462
488
|
return out;
|
|
463
489
|
}
|
|
@@ -491,7 +517,7 @@ function readsAsValue(line) {
|
|
|
491
517
|
* Whether a line is a JSON document whole: an object, an array, or a string that holds one (output encoded once more,
|
|
492
518
|
* `--query SecretString`'s `"{\"password\": …}"`). A string that holds anything else is a value (jsonString).
|
|
493
519
|
*/
|
|
494
|
-
function isJsonDocument(line) {
|
|
520
|
+
export function isJsonDocument(line) {
|
|
495
521
|
if (!/^(?:[{[].*[}\]]|".*")$/.test(line))
|
|
496
522
|
return false;
|
|
497
523
|
try {
|
|
@@ -677,7 +703,7 @@ function writtenToSecretFile(commands) {
|
|
|
677
703
|
for (let k = 0; k < commands.length; k++) {
|
|
678
704
|
const c = commands[k];
|
|
679
705
|
pipeline.push(c);
|
|
680
|
-
if ((c
|
|
706
|
+
if (pipes(c) && commands[k + 1]?.host === c.host)
|
|
681
707
|
continue;
|
|
682
708
|
// Where it writes: a redirection's file (`>`, `>>`, `>|`, `&>`, `>&`), its groups' included, and `tee`'s.
|
|
683
709
|
// (A descriptor, `2>&1`, `>&-`, is no secret file's name.)
|
|
@@ -706,9 +732,12 @@ function writtenToSecretFile(commands) {
|
|
|
706
732
|
if (r.op === "<<<" && r.target)
|
|
707
733
|
sections.push({ ...wordSpan(r.target), part });
|
|
708
734
|
}
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
735
|
+
// (Each of the echoes that start it, a group's several: `{ echo A=1; echo B=2; } | tee .env`.)
|
|
736
|
+
for (const p of pipeline) {
|
|
737
|
+
const first = simpleCommand(p.words);
|
|
738
|
+
const program = programName(first[0]);
|
|
739
|
+
if (program !== "echo" && program !== "printf")
|
|
740
|
+
break;
|
|
712
741
|
const args = first.slice(1).filter((w) => !/^-[neE]+$/.test(w.text));
|
|
713
742
|
const operands = program === "printf" && args.length > 1 ? args.slice(1) : args;
|
|
714
743
|
for (const w of operands)
|
package/dist/shell.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface Heredoc {
|
|
|
45
45
|
export interface ShellGroup {
|
|
46
46
|
kind: string;
|
|
47
47
|
redirects: ShellRedirect[];
|
|
48
|
+
/** The operator after the group: `|` pipes what every command in it prints (`{ echo A=1; echo B=2; } | tee .env`). */
|
|
49
|
+
then?: ShellOperator;
|
|
48
50
|
}
|
|
49
51
|
type ShellOperator = "|" | "|&" | "||" | "&&" | ";" | "&" | "\n" | "";
|
|
50
52
|
export interface ShellCommand {
|
|
@@ -110,13 +112,20 @@ export declare function wordSpan(word: ShellWord, from?: number): {
|
|
|
110
112
|
/** The command line an argument vector stands for, each word quoted as a shell needs it: `["bash", "-lc", "cat .env"]` → `bash -lc 'cat .env'`. */
|
|
111
113
|
export declare function shellLine(argv: string[]): string;
|
|
112
114
|
/**
|
|
113
|
-
* A terminal prompt at the start of a line, before a command
|
|
114
|
-
*
|
|
115
|
+
* A terminal prompt at the start of a line, before a command. Group 1 is the command. A POSIX shell's, after a
|
|
116
|
+
* virtualenv's or conda's `(venv) `: `$ `, `user@host:~/app$ `, `root@3f2a9c1d:/app# `, RHEL's `[ec2-user@ip-10-0-1-5
|
|
117
|
+
* app]$ `, macOS zsh's `user@host app % `, a bare shell's `bash-5.1$ ` and `sh-4.2# `, busybox's `/app # `. PowerShell's
|
|
118
|
+
* `PS C:\> ` and `PS> `, cmd's `C:\app>` (no blank after it), a notebook's `In [3]: !` or a cell's bare `!` before a
|
|
119
|
+
* command. (Not root's bare `# `: a comment or a Markdown heading is written so.)
|
|
115
120
|
*/
|
|
116
121
|
export declare const PROMPT: RegExp;
|
|
117
122
|
/** Whether a word names a wrapper, a command that runs the command after it. */
|
|
118
123
|
export declare function isWrapper(word: string): boolean;
|
|
119
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* A program's name without its directory and Windows' `.exe`: `/usr/bin/grep` → `grep`, `.\\steamcmd.exe` → `steamcmd`.
|
|
126
|
+
* PowerShell's and cmd's commands in the case the readers know them by, as both shells take any case (`get-content`,
|
|
127
|
+
* `TYPE`, `Select-string`).
|
|
128
|
+
*/
|
|
120
129
|
export declare function programName(word: ShellWord | undefined): string;
|
|
121
130
|
/**
|
|
122
131
|
* A simple command's words from the program it runs: past assignments (`FOO=1`), reserved words (`!`, `then`, `do`)
|
package/dist/shell.js
CHANGED
|
@@ -408,6 +408,7 @@ export function shellCommands(text, offset = 0, { backslash = true, oneLine = fa
|
|
|
408
408
|
if (closed && !words.length) {
|
|
409
409
|
// The redirections and the operator after a group's closer are the group's.
|
|
410
410
|
closed.group.redirects.push(...redirects);
|
|
411
|
+
closed.group.then = then;
|
|
411
412
|
const last = out.at(-1);
|
|
412
413
|
if (last && out.length > closed.from)
|
|
413
414
|
last.then = then;
|
|
@@ -608,7 +609,10 @@ export function wordSpan(word, from = 0) {
|
|
|
608
609
|
let start = word.map[from];
|
|
609
610
|
let end = word.ends.at(-1);
|
|
610
611
|
const raw = (k) => word.raw[k - word.start];
|
|
611
|
-
|
|
612
|
+
// (A span that starts and ends in the same quotes, or outside any, keeps the word's quotes about it as they were: the
|
|
613
|
+
// `'\''` a single-quoted password writes its quote with, `'admin:pa'\''ss'`, leaves the closing quote out.)
|
|
614
|
+
if (word.raw.slice(start - word.start, end - word.start) !== word.text.slice(from) &&
|
|
615
|
+
quoteAt(word.raw, start - word.start) !== quoteAt(word.raw, end - word.start)) {
|
|
612
616
|
while (start > word.start && /["'$]/.test(raw(start - 1)))
|
|
613
617
|
start--;
|
|
614
618
|
while (end < word.end && /["']/.test(raw(end)))
|
|
@@ -616,15 +620,30 @@ export function wordSpan(word, from = 0) {
|
|
|
616
620
|
}
|
|
617
621
|
return { start, end };
|
|
618
622
|
}
|
|
623
|
+
/** The quote a shell word's raw text is inside at `at`: `'`, `"`, or "" outside quotes. */
|
|
624
|
+
function quoteAt(raw, at) {
|
|
625
|
+
let quote = "";
|
|
626
|
+
for (let k = 0; k < at; k++) {
|
|
627
|
+
const c = raw[k];
|
|
628
|
+
if (c === "\\" && quote !== "'")
|
|
629
|
+
k++;
|
|
630
|
+
else if (quote ? c === quote : c === "'" || c === '"')
|
|
631
|
+
quote = quote ? "" : c;
|
|
632
|
+
}
|
|
633
|
+
return quote;
|
|
634
|
+
}
|
|
619
635
|
/** The command line an argument vector stands for, each word quoted as a shell needs it: `["bash", "-lc", "cat .env"]` → `bash -lc 'cat .env'`. */
|
|
620
636
|
export function shellLine(argv) {
|
|
621
637
|
return argv.map((w) => (/^[\w@%+=:,./-]+$/.test(w) ? w : `'${w.replace(/'/g, "'\\''")}'`)).join(" ");
|
|
622
638
|
}
|
|
623
639
|
/**
|
|
624
|
-
* A terminal prompt at the start of a line, before a command
|
|
625
|
-
*
|
|
640
|
+
* A terminal prompt at the start of a line, before a command. Group 1 is the command. A POSIX shell's, after a
|
|
641
|
+
* virtualenv's or conda's `(venv) `: `$ `, `user@host:~/app$ `, `root@3f2a9c1d:/app# `, RHEL's `[ec2-user@ip-10-0-1-5
|
|
642
|
+
* app]$ `, macOS zsh's `user@host app % `, a bare shell's `bash-5.1$ ` and `sh-4.2# `, busybox's `/app # `. PowerShell's
|
|
643
|
+
* `PS C:\> ` and `PS> `, cmd's `C:\app>` (no blank after it), a notebook's `In [3]: !` or a cell's bare `!` before a
|
|
644
|
+
* command. (Not root's bare `# `: a comment or a Markdown heading is written so.)
|
|
626
645
|
*/
|
|
627
|
-
export const PROMPT = /^[ \t]*(?:\$ |
|
|
646
|
+
export const PROMPT = /^[ \t]*(?:(?:\([\w.-]{1,40}\)[ \t]+)?(?:\$ |[\w.-]+@[\w.-]+:[^\n$#]{0,200}[$#] |\[[\w.-]+@[\w.-]+(?:[ \t][^\]\n]{0,200})?\][$#] |[\w.-]+@[\w.-]+[ \t]\S{1,200}[ \t][%$#] |[a-z]+-\d+(?:\.\d+)*[$#] |[~/][\w./~-]{0,200}[ \t]#[ \t])|In \[\d*\]:[ \t]*!|PS(?: [^>\n]{0,200})?> |[A-Za-z]:\\[^<>|"\n]{0,200}>|!(?=[a-z]))[ \t]*(\S[^\n]{0,2000})/;
|
|
628
647
|
/** Words before a simple command's own (`!` negates it); `then`, `do`, `else` open a group's commands. */
|
|
629
648
|
const RESERVED = new Set(["!", "if", "then", "else", "elif", "do", "while", "until"]);
|
|
630
649
|
/**
|
|
@@ -637,7 +656,8 @@ const WRAPPERS = new Map([
|
|
|
637
656
|
{ values: "ugCDhprtTUR", long: /^--(?:user|group|chdir|host|prompt|role|type|close-from|other-user|chroot)$/ },
|
|
638
657
|
],
|
|
639
658
|
["doas", { values: "uC" }],
|
|
640
|
-
|
|
659
|
+
// (`runuser -u app cmd`; with no `-u` it is su's twin, `runuser -l app -c 'cmd'`, and its `-l` is `--login`.)
|
|
660
|
+
["runuser", { values: "ug", long: /^--(?:user|group)$/ }],
|
|
641
661
|
["env", { values: "uCS", long: /^--(?:unset|chdir|split-string)$/ }],
|
|
642
662
|
["timeout", { values: "sk", long: /^--(?:signal|kill-after)$/, operands: 1 }],
|
|
643
663
|
["nice", { values: "n", long: /^--adjustment$/ }],
|
|
@@ -661,10 +681,34 @@ const WRAPPERS = new Map([
|
|
|
661
681
|
export function isWrapper(word) {
|
|
662
682
|
return WRAPPERS.has(word);
|
|
663
683
|
}
|
|
664
|
-
/**
|
|
684
|
+
/**
|
|
685
|
+
* A program's name without its directory and Windows' `.exe`: `/usr/bin/grep` → `grep`, `.\\steamcmd.exe` → `steamcmd`.
|
|
686
|
+
* PowerShell's and cmd's commands in the case the readers know them by, as both shells take any case (`get-content`,
|
|
687
|
+
* `TYPE`, `Select-string`).
|
|
688
|
+
*/
|
|
665
689
|
export function programName(word) {
|
|
666
|
-
|
|
690
|
+
const name = (word?.text ?? "").replace(/^.*[/\\]/, "").replace(/\.exe$/i, "");
|
|
691
|
+
return WINDOWS_COMMANDS.get(name.toLowerCase()) ?? name;
|
|
667
692
|
}
|
|
693
|
+
/** PowerShell's cmdlets and aliases and cmd's builtins that the readers name, by their names in lower case. */
|
|
694
|
+
const WINDOWS_COMMANDS = new Map([
|
|
695
|
+
"Get-Content",
|
|
696
|
+
"Select-String",
|
|
697
|
+
"Write-Output",
|
|
698
|
+
"Write-Host",
|
|
699
|
+
"Get-Item",
|
|
700
|
+
"Get-ChildItem",
|
|
701
|
+
"Get-Secret",
|
|
702
|
+
"gc",
|
|
703
|
+
"sls",
|
|
704
|
+
"gci",
|
|
705
|
+
"gi",
|
|
706
|
+
"type",
|
|
707
|
+
"findstr",
|
|
708
|
+
"pwsh",
|
|
709
|
+
"powershell",
|
|
710
|
+
"cmd",
|
|
711
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
668
712
|
/**
|
|
669
713
|
* A simple command's words from the program it runs: past assignments (`FOO=1`), reserved words (`!`, `then`, `do`)
|
|
670
714
|
* and wrappers with their options and operands (`sudo -iu app`, `env -i`, `timeout 5`, `nice -n 10`, `xargs -I{}`).
|
package/dist/spans.d.ts
CHANGED
|
@@ -24,9 +24,10 @@ export declare function isLearnable(f: Finding, value: string, minConfidence?: n
|
|
|
24
24
|
* The secret a finding covers, as the program reads it, so one secret written two ways gets one tag and is learned
|
|
25
25
|
* once: its recorded value (a known value whichever way it was written, a literal split in pieces joined, a JSON
|
|
26
26
|
* escape decoded); or the text it spans, read through a URL's percent-encoding (a userinfo's `Harbor2291%21x`, a
|
|
27
|
-
* query's `%26`, `+` a blank), a double-quoted string's escapes (`"Ab\"cd9"`, `\u0041
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* query's `%26`, `+` a blank), a double-quoted string's escapes (JSON's, `"Ab\"cd9"`, `\u0041`, or else a shell's,
|
|
28
|
+
* `"Ab\$cd9"`), a single-quoted one's quote written inside it (YAML's and SQL's `'Ab''cd9'`, a shell's `'Ab'\''cd9'`,
|
|
29
|
+
* JavaScript's and Python's `'Ab\'cd9'`), or a shell's backslashes outside quotes (`Harbor2291\!x`, `Ab\ cd9`).
|
|
30
|
+
* (A single-quoted string's other backslashes stay: a shell's has no escapes.)
|
|
30
31
|
*/
|
|
31
32
|
export declare function secretValue(text: string, f: {
|
|
32
33
|
start: number;
|
package/dist/spans.js
CHANGED
|
@@ -59,9 +59,10 @@ export function isLearnable(f, value, minConfidence = 0.9) {
|
|
|
59
59
|
* The secret a finding covers, as the program reads it, so one secret written two ways gets one tag and is learned
|
|
60
60
|
* once: its recorded value (a known value whichever way it was written, a literal split in pieces joined, a JSON
|
|
61
61
|
* escape decoded); or the text it spans, read through a URL's percent-encoding (a userinfo's `Harbor2291%21x`, a
|
|
62
|
-
* query's `%26`, `+` a blank), a double-quoted string's escapes (`"Ab\"cd9"`, `\u0041
|
|
63
|
-
*
|
|
64
|
-
*
|
|
62
|
+
* query's `%26`, `+` a blank), a double-quoted string's escapes (JSON's, `"Ab\"cd9"`, `\u0041`, or else a shell's,
|
|
63
|
+
* `"Ab\$cd9"`), a single-quoted one's quote written inside it (YAML's and SQL's `'Ab''cd9'`, a shell's `'Ab'\''cd9'`,
|
|
64
|
+
* JavaScript's and Python's `'Ab\'cd9'`), or a shell's backslashes outside quotes (`Harbor2291\!x`, `Ab\ cd9`).
|
|
65
|
+
* (A single-quoted string's other backslashes stay: a shell's has no escapes.)
|
|
65
66
|
*/
|
|
66
67
|
export function secretValue(text, f) {
|
|
67
68
|
if (f.value !== undefined)
|
|
@@ -81,10 +82,19 @@ export function secretValue(text, f) {
|
|
|
81
82
|
try {
|
|
82
83
|
return JSON.parse(`"${raw}"`);
|
|
83
84
|
}
|
|
84
|
-
catch { } // not JSON's escapes: the text as it is
|
|
85
|
+
catch { } // not JSON's escapes: a shell's, or the text as it is
|
|
86
|
+
return raw.replace(/\\([$`"\\])/g, "$1");
|
|
85
87
|
}
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
+
if (/'$/.test(before) && raw.includes("'")) {
|
|
89
|
+
// (`\'` is no shell's, whose single quotes hold no escapes: a language's that has them, `\\` included.)
|
|
90
|
+
if (/'\\''/.test(raw))
|
|
91
|
+
return raw.replace(/'\\''/g, "'");
|
|
92
|
+
if (/\\'/.test(raw))
|
|
93
|
+
return raw.replace(/\\(['\\])/g, "$1");
|
|
94
|
+
return raw.replace(/''/g, "'");
|
|
95
|
+
}
|
|
96
|
+
if (/\\[^\w\n]/.test(raw) && !/["'`]$/.test(before))
|
|
97
|
+
return raw.replace(/\\([^\w\n])/g, "$1");
|
|
88
98
|
return raw;
|
|
89
99
|
}
|
|
90
100
|
/** The escape sequence covering position `i` (after its backslash), as [backslash, end), if `i` is inside one. */
|
|
@@ -154,8 +164,28 @@ function preferNarrower(findings) {
|
|
|
154
164
|
return true;
|
|
155
165
|
});
|
|
156
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* A token's character, where a format that stops before it may have been cut and its tail is still secret: a
|
|
169
|
+
* percent-encoded one too (a session token's `%2B` in a URL); a dot only inside a token (`hvs.…`), not a sentence's
|
|
170
|
+
* full stop.
|
|
171
|
+
*/
|
|
172
|
+
const TOKEN_CHAR = /^(?:[\w+/=~-]|\.\w|%[0-9A-Fa-f])/;
|
|
173
|
+
/**
|
|
174
|
+
* Whether `f`, starting with or inside a format's token `token`, runs past it only by reading on into what follows it
|
|
175
|
+
* (`ghp_…;git push`, `ghp_…&q=foo`, `ghp_…#top`): an unquoted value read to the next blank. The format's regex bounds
|
|
176
|
+
* the token, so the rest is the text after it, not more of the secret. (A finding that starts before the token, a
|
|
177
|
+
* quote or a prefix, or a token that stops before a token character, may have been cut: those widen.)
|
|
178
|
+
*/
|
|
179
|
+
function runsPastToken(text, token, f) {
|
|
180
|
+
return (text !== undefined &&
|
|
181
|
+
layerOf(token.rule) === "format" &&
|
|
182
|
+
token.confidence > f.confidence &&
|
|
183
|
+
f.start >= token.start &&
|
|
184
|
+
f.end > token.end &&
|
|
185
|
+
!TOKEN_CHAR.test(text.slice(token.end, token.end + 3)));
|
|
186
|
+
}
|
|
157
187
|
/** Overlapping findings as one span each, every field but the span the most confident one's. */
|
|
158
|
-
function union(findings) {
|
|
188
|
+
function union(findings, text) {
|
|
159
189
|
const sorted = [...findings].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
160
190
|
const out = [];
|
|
161
191
|
for (const f of sorted) {
|
|
@@ -166,6 +196,12 @@ function union(findings) {
|
|
|
166
196
|
out.push({ ...f });
|
|
167
197
|
continue;
|
|
168
198
|
}
|
|
199
|
+
if (runsPastToken(text, last, f))
|
|
200
|
+
continue;
|
|
201
|
+
if (runsPastToken(text, f, last)) {
|
|
202
|
+
out[out.length - 1] = { ...f };
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
169
205
|
const end = Math.max(last.end, f.end);
|
|
170
206
|
const winner = f.confidence > last.confidence ? f : last;
|
|
171
207
|
// (A value goes with its own span only: `last` may start earlier or `f` end later than the winner.)
|
|
@@ -223,11 +259,46 @@ export function trimmedToValue(text, f) {
|
|
|
223
259
|
}
|
|
224
260
|
if (end - start > 1 && /[,;]/.test(text[end - 1]) && /^[ \t]+[A-Za-z_][\w.-]*=/.test(text.slice(end, end + 80)))
|
|
225
261
|
end--;
|
|
262
|
+
else if (layerOf(f.rule) === "named" && !/["'`]/.test(q ?? ""))
|
|
263
|
+
end = start + nextSetting(text.slice(start, end));
|
|
226
264
|
return start === f.start && end === f.end ? f : { ...f, start, end };
|
|
227
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Settings that end an unquoted value read to the next blank: `;`, `,` or `&`, a name of a word's shape (lower case,
|
|
268
|
+
* upper case or camel case, at least two letters), `=` and a plain value, to the end (`;username=bob`,
|
|
269
|
+
* `,DB_HOST=db,DB_PORT=5432`, `&page=2&sort=desc`). A password's own punctuation seldom reads that way to its end
|
|
270
|
+
* (`86)68/C{,C=wyhJH9`, `Nakp&nDiqWJ-K=nW`).
|
|
271
|
+
*/
|
|
272
|
+
const SETTINGS_AFTER = /[;,&](?=[\w.-]*[A-Za-z][\w.-]*[A-Za-z])(?:[a-z][a-z0-9]*(?:[_.-][a-z0-9]+)*|[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z]+(?:[A-Z][a-z0-9]+)+)=[\w.:/@+-]*(?=[;,&]|$)/y;
|
|
273
|
+
/**
|
|
274
|
+
* Where an unquoted value read to the next blank ends at the settings after it of a list it is in, with no blank
|
|
275
|
+
* between (`password=Kx9…;username=bob`, `A=…,B=db`): the value's length up to there, or all of it. Not in a value that
|
|
276
|
+
* is itself a list of settings or a URL (an `=` or `?` before), whose own separators these are (`DSN=host=db;password=…`),
|
|
277
|
+
* and not after a value too short to be one.
|
|
278
|
+
*/
|
|
279
|
+
const readsAsName = (setting) => naturalness(setting
|
|
280
|
+
.slice(1, setting.indexOf("="))
|
|
281
|
+
.replace(/[_.-]+/g, " ")
|
|
282
|
+
.toLowerCase()) > 0;
|
|
283
|
+
function nextSetting(value) {
|
|
284
|
+
const list = value.search(/=+[^=;,&]|\?|:\/\//);
|
|
285
|
+
const separators = /[;,&]/g;
|
|
286
|
+
for (let m; (m = separators.exec(value)) && (list < 0 || m.index < list);) {
|
|
287
|
+
if (m.index < 6)
|
|
288
|
+
continue;
|
|
289
|
+
let at = m.index;
|
|
290
|
+
SETTINGS_AFTER.lastIndex = at;
|
|
291
|
+
// (A setting's name reads as words, `username`, `DB_HOST`; a random password's `;jAnr=` or `;uKj=` doesn't.)
|
|
292
|
+
for (let s; at < value.length && (s = SETTINGS_AFTER.exec(value)) && readsAsName(s[0]);)
|
|
293
|
+
at = SETTINGS_AFTER.lastIndex;
|
|
294
|
+
if (at === value.length)
|
|
295
|
+
return m.index;
|
|
296
|
+
}
|
|
297
|
+
return value.length;
|
|
298
|
+
}
|
|
228
299
|
/** Findings merged into one span each, sorted; with `text`, kept off escape sequences. (The inputs aren't changed.) */
|
|
229
300
|
export function merge(findings, text) {
|
|
230
|
-
return union(preferNarrower(text ? findings.map((f) => onEscapes(text, f)) : findings));
|
|
301
|
+
return union(preferNarrower(text ? findings.map((f) => onEscapes(text, f)) : findings), text);
|
|
231
302
|
}
|
|
232
303
|
/** The first of `sorted` (merged, sorted by start) that ends after `start`, by binary search. */
|
|
233
304
|
function firstEndingAfter(sorted, start) {
|
package/dist/sql.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL
|
|
3
|
-
* every rule that asks what a value in a row is:
|
|
4
|
-
* the
|
|
5
|
-
*
|
|
2
|
+
* SQL rows read into cells: each value of an `INSERT … VALUES` statement, or of a `COPY … FROM stdin` block as
|
|
3
|
+
* pg_dump writes its data, under its table and column. One reader for every rule that asks what a value in a row is:
|
|
4
|
+
* the name layer hides a credential column's values (named.ts sqlRows), the random layer keeps the other columns'
|
|
5
|
+
* IDs and data (context.ts, `sqlCell`). A statement without a column list (`INSERT INTO t VALUES …`, as mysqldump
|
|
6
|
+
* writes by default) names no column, so it isn't read.
|
|
6
7
|
*/
|
|
7
8
|
import { type Argument } from "./calls.ts";
|
|
8
9
|
export interface SqlCell {
|
|
@@ -10,11 +11,14 @@ export interface SqlCell {
|
|
|
10
11
|
table: string;
|
|
11
12
|
/** The column, without quotes: `password` in `"password"`, `` `password` ``, `[password]`. */
|
|
12
13
|
column: string;
|
|
13
|
-
/** The value as an argument of the row:
|
|
14
|
+
/** The value as an argument of the row: the string it gives the column is its `literal` (sqlLiteral). */
|
|
14
15
|
value: Argument;
|
|
16
|
+
/** The row's columns and values, the cell's among them: what a key/value table's value is named by. */
|
|
17
|
+
columns: readonly string[];
|
|
18
|
+
row: readonly Argument[];
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
|
-
* The cells of every `INSERT` in `text`, row by row (up to 10,000 rows a statement); with `wants`, only of
|
|
18
|
-
* statements whose table and columns it accepts (so a dump's other tables cost no reading).
|
|
21
|
+
* The cells of every `INSERT` and `COPY` in `text`, row by row (up to 10,000 rows a statement); with `wants`, only of
|
|
22
|
+
* the statements whose table and columns it accepts (so a dump's other tables cost no reading).
|
|
19
23
|
*/
|
|
20
|
-
export declare function
|
|
24
|
+
export declare function sqlCells(text: string, wants?: (table: string, columns: string[]) => boolean): Generator<SqlCell>;
|
package/dist/sql.js
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL
|
|
3
|
-
* every rule that asks what a value in a row is:
|
|
4
|
-
* the
|
|
5
|
-
*
|
|
2
|
+
* SQL rows read into cells: each value of an `INSERT … VALUES` statement, or of a `COPY … FROM stdin` block as
|
|
3
|
+
* pg_dump writes its data, under its table and column. One reader for every rule that asks what a value in a row is:
|
|
4
|
+
* the name layer hides a credential column's values (named.ts sqlRows), the random layer keeps the other columns'
|
|
5
|
+
* IDs and data (context.ts, `sqlCell`). A statement without a column list (`INSERT INTO t VALUES …`, as mysqldump
|
|
6
|
+
* writes by default) names no column, so it isn't read.
|
|
6
7
|
*/
|
|
7
|
-
import { callArguments, closingBracket } from "./calls.js";
|
|
8
|
+
import { callArguments, closingBracket, literalAt } from "./calls.js";
|
|
8
9
|
import { delimitedCells } from "./syntax.js";
|
|
9
10
|
/** An identifier as SQL dialects quote it: `"name"`, `` `name` ``, `[name]`. */
|
|
10
11
|
const unquote = (name) => name.trim().replace(/^["`[]|["`\]]$/g, "");
|
|
12
|
+
/** A table's name as any dialect quotes it: `"my table"`, `` `app`.`users` ``, `[dbo].[users]`. */
|
|
13
|
+
const TABLE = String.raw `((?:"[^"\n]{1,100}"|\x60[^\x60\n]{1,100}\x60|\[[^\]\n]{1,100}\]|[\w.]){1,200})`;
|
|
11
14
|
/**
|
|
12
15
|
* `INSERT [INTO] <table> (<columns>) VALUES`, and the dialects' other forms (`INSERT IGNORE`, `INSERT OR REPLACE`,
|
|
13
|
-
* `REPLACE INTO`)
|
|
16
|
+
* `REPLACE INTO`).
|
|
14
17
|
*/
|
|
15
|
-
const INSERT =
|
|
18
|
+
const INSERT = new RegExp(String.raw `\b(?:INSERT(?:\s+IGNORE|\s+OR\s+(?:REPLACE|IGNORE|ABORT|FAIL|ROLLBACK))?|REPLACE)\s+(?:INTO\s+)?${TABLE}\s*\(([^()]{1,2000})\)\s*VALUES\s*`, "gi");
|
|
19
|
+
/** `COPY <table> (<columns>) FROM stdin;` and its line break: pg_dump's data, one tab-separated row a line after it. */
|
|
20
|
+
const COPY = new RegExp(String.raw `(?:^|\n)COPY\s+${TABLE}\s*\(([^()]{1,2000})\)\s+FROM\s+stdin\b[^;\n]{0,200};[ \t]*\r?\n`, "gi");
|
|
16
21
|
/** What may stand between two rows: blanks, comments and one comma. */
|
|
17
22
|
const BETWEEN_ROWS = /(?:\s|--[^\n]*|\/\*[\s\S]{0,2000}?\*\/)*,?(?:\s|--[^\n]*|\/\*[\s\S]{0,2000}?\*\/)*/y;
|
|
18
23
|
/**
|
|
19
|
-
* The cells of every `INSERT` in `text`, row by row (up to 10,000 rows a statement); with `wants`, only of
|
|
20
|
-
* statements whose table and columns it accepts (so a dump's other tables cost no reading).
|
|
24
|
+
* The cells of every `INSERT` and `COPY` in `text`, row by row (up to 10,000 rows a statement); with `wants`, only of
|
|
25
|
+
* the statements whose table and columns it accepts (so a dump's other tables cost no reading).
|
|
21
26
|
*/
|
|
22
|
-
export function*
|
|
23
|
-
if (
|
|
24
|
-
|
|
27
|
+
export function* sqlCells(text, wants) {
|
|
28
|
+
if (/\b(?:INSERT|REPLACE)\b/i.test(text))
|
|
29
|
+
yield* insertCells(text, wants);
|
|
30
|
+
if (/\bCOPY\b/i.test(text))
|
|
31
|
+
yield* copyCells(text, wants);
|
|
32
|
+
}
|
|
33
|
+
function* insertCells(text, wants) {
|
|
25
34
|
for (const m of text.matchAll(INSERT)) {
|
|
26
35
|
const table = unquote(m[1].split(".").at(-1));
|
|
27
36
|
const columns = delimitedCells(m[2], ",").map((c) => unquote(c.text));
|
|
@@ -33,8 +42,10 @@ export function* insertCells(text, wants) {
|
|
|
33
42
|
if (text[at] === "(" && closingBracket(text, at + 1) < 0) {
|
|
34
43
|
const end = text.indexOf("\n", at);
|
|
35
44
|
const line = text.slice(m.index, end < 0 ? undefined : end);
|
|
36
|
-
for (const cell of rowCells(line, at - m.index, table, columns))
|
|
37
|
-
|
|
45
|
+
for (const cell of rowCells(line, at - m.index, table, columns)) {
|
|
46
|
+
const row = cell.row.map((a) => shifted(a, m.index));
|
|
47
|
+
yield { ...cell, value: row[cell.row.indexOf(cell.value)], row };
|
|
48
|
+
}
|
|
38
49
|
}
|
|
39
50
|
else
|
|
40
51
|
yield* rowCells(text, at, table, columns);
|
|
@@ -44,17 +55,84 @@ export function* insertCells(text, wants) {
|
|
|
44
55
|
function* rowCells(text, at, table, columns) {
|
|
45
56
|
for (let row = 0; row < 10_000 && text[at] === "("; row++) {
|
|
46
57
|
// (A row cut short, a dump's `head`, ends the rows. callArguments reads to the bracket closingBracket finds.)
|
|
47
|
-
const values = callArguments(text, at + 1);
|
|
58
|
+
const values = callArguments(text, at + 1)?.map((value) => withLiteral(text, value));
|
|
48
59
|
if (!values)
|
|
49
60
|
break;
|
|
50
61
|
const close = closingBracket(text, at + 1);
|
|
51
62
|
for (const [k, value] of values.entries())
|
|
52
63
|
if (columns[k])
|
|
53
|
-
yield { table, column: columns[k], value };
|
|
64
|
+
yield { table, column: columns[k], value, columns, row: values };
|
|
54
65
|
BETWEEN_ROWS.lastIndex = close + 1;
|
|
55
66
|
at = close + 1 + BETWEEN_ROWS.exec(text)[0].length;
|
|
56
67
|
}
|
|
57
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* The cells of `COPY … FROM stdin` blocks: tab-separated rows up to the `\.` line, each value as written (a `\N` is
|
|
71
|
+
* NULL, no value). (Up to 10,000 rows a block, as for an INSERT's.)
|
|
72
|
+
*/
|
|
73
|
+
function* copyCells(text, wants) {
|
|
74
|
+
for (const m of text.matchAll(COPY)) {
|
|
75
|
+
const table = unquote(m[1].split(".").at(-1));
|
|
76
|
+
const columns = delimitedCells(m[2], ",").map((c) => unquote(c.text));
|
|
77
|
+
if (wants && !wants(table, columns))
|
|
78
|
+
continue;
|
|
79
|
+
let at = m.index + m[0].length;
|
|
80
|
+
for (let n = 0; n < 10_000 && at < text.length; n++) {
|
|
81
|
+
const stop = text.indexOf("\n", at);
|
|
82
|
+
const end = stop < 0 ? text.length : stop;
|
|
83
|
+
const line = text.slice(at, end).replace(/\r$/, "");
|
|
84
|
+
if (line === "\\." || !line.includes("\t"))
|
|
85
|
+
break;
|
|
86
|
+
const row = [];
|
|
87
|
+
let start = at;
|
|
88
|
+
for (const value of line.split("\t")) {
|
|
89
|
+
const span = { value, start, end: start + value.length };
|
|
90
|
+
row.push({
|
|
91
|
+
text: value,
|
|
92
|
+
start,
|
|
93
|
+
end: span.end,
|
|
94
|
+
...(value === "\\N" ? {} : { literal: { ...span, quote: "" } }),
|
|
95
|
+
});
|
|
96
|
+
start = span.end + 1;
|
|
97
|
+
}
|
|
98
|
+
for (const [k, value] of row.entries())
|
|
99
|
+
if (columns[k])
|
|
100
|
+
yield { table, column: columns[k], value, columns, row };
|
|
101
|
+
at = end + 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The string a row's value gives its column, read through SQL's own ways of writing one: a prefix (`N'…'`, `E'…'`,
|
|
107
|
+
* calls.ts reads those), dollar quoting (`$$…$$`, `$body$…$body$`), a cast (`'…'::text`) and the functions a value is
|
|
108
|
+
* stored through (`crypt('…', gen_salt('bf'))`, `MD5('…')`, `encode(digest('…', 'sha256'), 'hex')`), which are given it
|
|
109
|
+
* first. Undefined for any other expression, and for a number.
|
|
110
|
+
*/
|
|
111
|
+
function sqlLiteral(text, value, depth = 0) {
|
|
112
|
+
if (value.literal)
|
|
113
|
+
return value.literal;
|
|
114
|
+
const literal = literalAt(text, value.start);
|
|
115
|
+
if (literal && literal.end < value.end) {
|
|
116
|
+
const rest = text.slice(literal.end + 1, value.end);
|
|
117
|
+
return CAST_TAIL.test(rest) ? literal : undefined;
|
|
118
|
+
}
|
|
119
|
+
// (A function's first argument, read the same way, not too deep to stay cheap: the functions a value is stored
|
|
120
|
+
// through, `encode(digest('…', 'sha256'), 'hex')`, are given it first.)
|
|
121
|
+
const call = /^[A-Za-z_][\w.]{0,60}[ \t]*\(/.exec(value.text);
|
|
122
|
+
if (!call ||
|
|
123
|
+
depth >= 3 ||
|
|
124
|
+
!CAST_TAIL.test(text.slice(closingBracket(text, value.start + call[0].length) + 1, value.end)))
|
|
125
|
+
return undefined;
|
|
126
|
+
const first = callArguments(text, value.start + call[0].length)?.[0];
|
|
127
|
+
return first && sqlLiteral(text, first, depth + 1);
|
|
128
|
+
}
|
|
129
|
+
/** What may follow a string and leave it the value: Postgres' casts, `::text`, `::character varying(255)`, `::bytea[]`. */
|
|
130
|
+
const CAST_TAIL = /^(?:[ \t]*::[ \t]*[A-Za-z_][\w ."]{0,60}?(?:\([\d, ]{1,12}\))?(?:\[\])?)*[ \t]*$/;
|
|
131
|
+
/** A row's value with the string it gives its column as its literal (sqlLiteral). */
|
|
132
|
+
function withLiteral(text, value) {
|
|
133
|
+
const literal = sqlLiteral(text, value);
|
|
134
|
+
return literal && !value.literal ? { ...value, literal } : value;
|
|
135
|
+
}
|
|
58
136
|
/** An argument read from a piece of a text, placed where the piece starts in it. */
|
|
59
137
|
function shifted(a, by) {
|
|
60
138
|
return {
|