pm-ops 2026.8.31 → 2026.9.2
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 +48 -0
- package/dist/attestation.d.ts +177 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +593 -0
- package/dist/attestation.js.map +1 -0
- package/dist/index.d.ts +101 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/shell-scan.d.ts +438 -0
- package/dist/shell-scan.d.ts.map +1 -0
- package/dist/shell-scan.js +1516 -0
- package/dist/shell-scan.js.map +1 -0
- package/manifest.json +1 -1
- package/package.json +12 -3
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenises shell text into the commands it would actually run.
|
|
3
|
+
*
|
|
4
|
+
* A guard that decides "does any publish here omit `--provenance`" is only as
|
|
5
|
+
* good as its idea of what a command is. The previous scan answered that
|
|
6
|
+
* question with a regular expression: it blanked every quoted span so an
|
|
7
|
+
* advisory `echo "npm publish"` could not read as an invocation, then split the
|
|
8
|
+
* remainder on `&&`, `||`, `;` and a space-surrounded `|`.
|
|
9
|
+
*
|
|
10
|
+
* Both halves of that shortcut are wrong in the same direction -- they make the
|
|
11
|
+
* gate report a pass it has not earned:
|
|
12
|
+
*
|
|
13
|
+
* - Blanking quoted spans deletes the argument being audited. `npm publish
|
|
14
|
+
* "--provenance"` runs with an attestation but scans as one without, and the
|
|
15
|
+
* reverse case is worse: `eval "npm publish"` and `bash -c 'npm publish'` are
|
|
16
|
+
* real unattested publishes that vanish entirely, leaving a conventional
|
|
17
|
+
* attested sibling elsewhere in the file to carry the audit to green.
|
|
18
|
+
* - Splitting on three operators misses a backgrounding `&`, a pipe written
|
|
19
|
+
* without surrounding spaces (`true|npm publish`), and command substitution.
|
|
20
|
+
*
|
|
21
|
+
* So the text is tokenised the way a shell does it -- quotes resolved rather
|
|
22
|
+
* than erased, operators recognised as operators, `$(...)`, backticks, `eval`
|
|
23
|
+
* and `sh -c` payloads recursed into -- and each command records whether its
|
|
24
|
+
* words were quoted. Nothing downstream has to guess.
|
|
25
|
+
*
|
|
26
|
+
* This is deliberately not a shell. It resolves only provably literal scalar
|
|
27
|
+
* and array bindings, preserves unknown expansions, and removes redirections
|
|
28
|
+
* when locating command words; it does not expand globs or arithmetic. It
|
|
29
|
+
* exists to enumerate candidate command invocations for auditing, where
|
|
30
|
+
* missing one is a security failure and inventing one is merely noise.
|
|
31
|
+
*
|
|
32
|
+
* @packageDocumentation
|
|
33
|
+
*/
|
|
34
|
+
/** One word of a command, after quote resolution. */
|
|
35
|
+
export interface ShellToken {
|
|
36
|
+
/** The word's text with its quoting removed. */
|
|
37
|
+
value: string;
|
|
38
|
+
/** True when any part of the word came from inside quotes. */
|
|
39
|
+
quoted: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* True when the word's FIRST character came from inside quotes.
|
|
42
|
+
*
|
|
43
|
+
* `quoted` alone cannot tell an assignment apart from a literal that merely
|
|
44
|
+
* looks like one. `NPM_CONFIG_REGISTRY="https://example"` is a real
|
|
45
|
+
* assignment whose value happens to be quoted, while `"FOO=bar"` is a single
|
|
46
|
+
* quoted word that the shell does not treat as an assignment at all. Both set
|
|
47
|
+
* `quoted`; only the second starts inside quotes.
|
|
48
|
+
*/
|
|
49
|
+
startsQuoted: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** One simple command: the words it would run, in order. */
|
|
52
|
+
export type ShellCommand = ShellToken[];
|
|
53
|
+
/**
|
|
54
|
+
* Split shell text into the simple commands it contains.
|
|
55
|
+
*
|
|
56
|
+
* Command substitutions are scanned as well as the command containing them,
|
|
57
|
+
* because `VERSION=$(npm publish)` runs a publish however unusual that is, and a
|
|
58
|
+
* gate that only looked at the outer assignment would miss it.
|
|
59
|
+
*
|
|
60
|
+
* `eval`, `bash -c` and their siblings receive the same treatment one level
|
|
61
|
+
* deeper: their string argument is re-tokenised, so a publish smuggled through
|
|
62
|
+
* an interpreter is enumerated alongside a plain one. Recursion is bounded --
|
|
63
|
+
* shell text that nests evaluators more than a handful of levels deep is not
|
|
64
|
+
* something this repository writes, and an unbounded walk over hostile input is
|
|
65
|
+
* a denial of service rather than a stronger audit.
|
|
66
|
+
*
|
|
67
|
+
* @param text - Shell text, typically one file or one manifest script body.
|
|
68
|
+
* @param depth - Current evaluator recursion depth; callers pass nothing.
|
|
69
|
+
* @returns Every simple command found, outermost first.
|
|
70
|
+
*/
|
|
71
|
+
export declare function tokenizeCommands(text: string, depth?: number): ShellCommand[];
|
|
72
|
+
/**
|
|
73
|
+
* Name the program a command runs, or nothing when it runs none.
|
|
74
|
+
*
|
|
75
|
+
* Leading `NAME=value` assignments and wrapper words are skipped, and a path is
|
|
76
|
+
* reduced to its basename so `/usr/local/bin/npm publish` is recognised. The
|
|
77
|
+
* distinction this exists to draw is command *position*: `echo npm publish`
|
|
78
|
+
* prints three words and publishes nothing, while the previous scan searched
|
|
79
|
+
* the whole line for the word `npm` and counted it as an invocation.
|
|
80
|
+
*
|
|
81
|
+
* @param command - One simple command's tokens.
|
|
82
|
+
* @returns The program's basename, or undefined for an empty or assignment-only command.
|
|
83
|
+
*/
|
|
84
|
+
export declare function commandName(input: ShellCommand): string | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Enumerate every reading of a command that could name a program.
|
|
87
|
+
*
|
|
88
|
+
* `commandName` answers "what does this command run" and answers it once. That
|
|
89
|
+
* is right for reporting and wrong for auditing, because a wrapper's options
|
|
90
|
+
* are not all known: `sudo -u root npm publish` stops at `root`, since `-u`
|
|
91
|
+
* takes a value and nothing here knows that. Enumerating the value-taking
|
|
92
|
+
* options of every wrapper would be a list that silently goes stale, and each
|
|
93
|
+
* omission is a publish that disappears from the audit.
|
|
94
|
+
*
|
|
95
|
+
* So once a wrapper has been consumed, every later word is also offered as a
|
|
96
|
+
* possible program, with the words after it as its arguments. An auditor asking
|
|
97
|
+
* "does any publish here lack an attestation" then cannot miss one behind a
|
|
98
|
+
* wrapper option it has never heard of.
|
|
99
|
+
*
|
|
100
|
+
* The cost is noise, never a miss: `sudo -u npm publish` -- a user actually
|
|
101
|
+
* named `npm` -- is offered as a publish that no shell would run. For a gate
|
|
102
|
+
* whose failure mode is an unattested release, a spurious finding an operator
|
|
103
|
+
* dismisses is the cheaper error.
|
|
104
|
+
*
|
|
105
|
+
* A command with no wrapper yields exactly one reading, so ordinary commands
|
|
106
|
+
* are unaffected.
|
|
107
|
+
*
|
|
108
|
+
* @param command - One simple command's tokens.
|
|
109
|
+
* @returns Each candidate reading, the command's own first.
|
|
110
|
+
*/
|
|
111
|
+
export declare function commandCandidates(input: ShellCommand): ShellCommand[];
|
|
112
|
+
/**
|
|
113
|
+
* List a command's arguments -- everything after its program name.
|
|
114
|
+
*
|
|
115
|
+
* @param command - One simple command's tokens.
|
|
116
|
+
* @returns The argument tokens, in order.
|
|
117
|
+
*/
|
|
118
|
+
export declare function commandArguments(input: ShellCommand): ShellToken[];
|
|
119
|
+
/** A tracked file's path and contents. */
|
|
120
|
+
export interface SourceFile {
|
|
121
|
+
/** Repository-relative path. */
|
|
122
|
+
file: string;
|
|
123
|
+
/** File contents. */
|
|
124
|
+
text: string;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Collapse shell and YAML line continuations so one logical command is one string.
|
|
128
|
+
*
|
|
129
|
+
* A backslash at end of line joins the next line; without this every multi-line
|
|
130
|
+
* invocation looks like a set of fragments, none of which carries both the
|
|
131
|
+
* version input and the date flag.
|
|
132
|
+
*
|
|
133
|
+
* @param text - Raw file contents.
|
|
134
|
+
* @returns The same text with continuations joined.
|
|
135
|
+
*/
|
|
136
|
+
export declare function joinContinuations(text: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* Strip the YAML block indentation from `run:` block scalars.
|
|
139
|
+
*
|
|
140
|
+
* GitHub Actions takes a `run:` block's text, removes the indentation YAML
|
|
141
|
+
* gave it, and hands the result to bash — so the shell never sees the leading
|
|
142
|
+
* whitespace the raw workflow file carries. The scanner reads the raw file,
|
|
143
|
+
* and exactly one of its rules is whitespace-sensitive: a heredoc terminator
|
|
144
|
+
* is recognised only at the start of the line the shell sees. A terminator
|
|
145
|
+
* compared against a YAML-indented line therefore never matches, the heredoc
|
|
146
|
+
* swallows the rest of the file, every later assignment is payload, and a
|
|
147
|
+
* `$NPM publish` after the heredoc is omitted from the audit while an
|
|
148
|
+
* attested sibling elsewhere satisfies the non-vacuity guard — the scan
|
|
149
|
+
* reports clean over an unattested publish.
|
|
150
|
+
*
|
|
151
|
+
* Dedenting the block content restores the text bash actually receives. Every
|
|
152
|
+
* other rule in the scanner already tolerates leading whitespace
|
|
153
|
+
* (`STANDALONE_ASSIGNMENT` opens with `^[ \t]*`, control closers and function
|
|
154
|
+
* openers are matched against trimmed syntax, a comment starts after any
|
|
155
|
+
* separator or whitespace, and `bashArrays` anchors on a word boundary), so
|
|
156
|
+
* this function changes nothing else about what the scanner sees.
|
|
157
|
+
*
|
|
158
|
+
* Only `run:` blocks are dedented, because `run` is the key GitHub Actions
|
|
159
|
+
* executes; a block scalar under any other key is data no shell runs, and
|
|
160
|
+
* stripping its indentation would be rewriting prose. The block's indentation
|
|
161
|
+
* is learned from its first non-blank line, as YAML itself learns it, a line
|
|
162
|
+
* keeps any indentation beyond the block's own, and the block ends at the
|
|
163
|
+
* first non-blank line indented less — which is where YAML ends it too. A
|
|
164
|
+
* `run:`-shaped line inside another block's content is content, not a header,
|
|
165
|
+
* because the scanner walks the file once, forward, consuming each block
|
|
166
|
+
* before looking for the next.
|
|
167
|
+
*
|
|
168
|
+
* @param text - A workflow file's raw contents.
|
|
169
|
+
* @returns The same text with each `run:` block's content dedented.
|
|
170
|
+
*/
|
|
171
|
+
export declare function dedentRunBlocks(text: string): string;
|
|
172
|
+
/**
|
|
173
|
+
* Index bash array assignments so a shared options array can be expanded.
|
|
174
|
+
*
|
|
175
|
+
* The release workflows declare `common=( ... )` once and pass `"${common[@]}"`
|
|
176
|
+
* to each invocation, precisely so the invocations cannot drift. A scan that
|
|
177
|
+
* reads only the invocation line therefore sees none of the shared flags.
|
|
178
|
+
* Quoted or escaped closing parentheses are members, not array boundaries; an
|
|
179
|
+
* unsupported array shape is left unknown so verification fails closed.
|
|
180
|
+
*
|
|
181
|
+
* @param text - File contents with continuations already joined.
|
|
182
|
+
* @returns Array name mapped to the flag text it holds.
|
|
183
|
+
*/
|
|
184
|
+
export declare function bashArrays(text: string): Map<string, string>;
|
|
185
|
+
/**
|
|
186
|
+
* Read persistent literal assignments from one control-operator-delimited segment.
|
|
187
|
+
*
|
|
188
|
+
* The line segmenter has already separated `;`, `&&`, and `||`. Shell keywords
|
|
189
|
+
* and grouping operators can still precede an assignment in the remaining text:
|
|
190
|
+
* `then NPM=npm`, `do FLAG=--provenance`, and `( NPM=npm` are all assignment-only
|
|
191
|
+
* commands. Tokenising finds command position without an anchored text pattern.
|
|
192
|
+
*
|
|
193
|
+
* The ordinary line-opening parser remains the more permissive path for quoted
|
|
194
|
+
* and escaped literal values. A compound-position assignment is accepted only
|
|
195
|
+
* when its token was wholly unquoted and substitution-free; uncertainty is left
|
|
196
|
+
* unresolved so the auditor can refuse rather than invent a binding.
|
|
197
|
+
*
|
|
198
|
+
* @param segment - One segment returned by {@link segmentShellLine}.
|
|
199
|
+
* @returns Literal scalar bindings made by assignment-only commands.
|
|
200
|
+
*/
|
|
201
|
+
export declare function literalScalarAssignments(segment: string): Map<string, string>;
|
|
202
|
+
/**
|
|
203
|
+
* Report which lines of a file are heredoc *body* rather than shell source.
|
|
204
|
+
*
|
|
205
|
+
* A heredoc body is data on a command's stdin. It cannot bind a variable in the
|
|
206
|
+
* shell that reads it, so indexing `FLAG=--provenance` out of one invents a
|
|
207
|
+
* binding the shell never makes, and an unattested `npm publish $FLAG` then
|
|
208
|
+
* borrowed that flag and passed the attestation gate. Every heredoc spelling
|
|
209
|
+
* hid a binding the same way: the plain `<<EOF`, the space- and tab-separated
|
|
210
|
+
* `<< EOF`, the quoted `<<'EOF'` and `<<"EOF"`, the tab-stripping `<<-EOF`,
|
|
211
|
+
* and one written after another redirection (`cat > f <<EOF`).
|
|
212
|
+
*
|
|
213
|
+
* Only *binding* is suppressed here, never publish detection. A heredoc can
|
|
214
|
+
* carry a script that is written to a file and executed later, so an
|
|
215
|
+
* `npm publish` inside one remains a publish path this scan must report. Both
|
|
216
|
+
* halves of that split fail closed: an unindexed name leaves `$FLAG`
|
|
217
|
+
* unresolved, so the publish reads as unattested and the gate fails, and a
|
|
218
|
+
* publish written into a heredoc still has to carry the flag itself.
|
|
219
|
+
*
|
|
220
|
+
* Several heredocs may open on one line (`cat <<A <<B`); bash reads their
|
|
221
|
+
* bodies in the order the redirections appear, which is the order kept here. A
|
|
222
|
+
* body whose delimiter never arrives runs to the end of the file, exactly as
|
|
223
|
+
* the shell would read it.
|
|
224
|
+
*
|
|
225
|
+
* @param lines - The file's lines, with continuations already joined.
|
|
226
|
+
* @returns One flag per line, true where that line is heredoc body.
|
|
227
|
+
*/
|
|
228
|
+
export declare function heredocBodyLines(lines: string[]): boolean[];
|
|
229
|
+
/**
|
|
230
|
+
* Report body-data lines on which Bash performs parameter expansion.
|
|
231
|
+
*
|
|
232
|
+
* Quote removal still applies to every delimiter, but quoting any part of that
|
|
233
|
+
* delimiter suppresses expansion in its body. Terminator lines never expand.
|
|
234
|
+
*
|
|
235
|
+
* @param lines - The file's lines, with continuations already joined.
|
|
236
|
+
* @returns One flag per line, true only for expandable heredoc body data.
|
|
237
|
+
*/
|
|
238
|
+
export declare function heredocExpansionLines(lines: string[]): boolean[];
|
|
239
|
+
/**
|
|
240
|
+
* Parse the scalar names one command unsets, or nothing.
|
|
241
|
+
*
|
|
242
|
+
* `unset FLAG` after `FLAG=--provenance` leaves the shell passing no flag at
|
|
243
|
+
* all, so retaining the binding let an unattested publish borrow `--provenance`
|
|
244
|
+
* and pass the gate. Commands are read through the tokeniser rather than an
|
|
245
|
+
* anchored whole-line pattern, because `echo ready; unset FLAG` unsets exactly
|
|
246
|
+
* as effectively as a line holding nothing else.
|
|
247
|
+
*
|
|
248
|
+
* An unset the parent shell would not actually perform -- inside `$(...)`, or
|
|
249
|
+
* one the tokeniser surfaces from a nested body -- is honoured too, because the
|
|
250
|
+
* two directions of error are not symmetric. Dropping a binding can only make a
|
|
251
|
+
* later `$FLAG` unresolve, which reads as an unattested publish and *fails* the
|
|
252
|
+
* gate; keeping one the shell discarded is what lets an unattested publish
|
|
253
|
+
* pass. Erring toward the false alarm is the only direction that cannot ship an
|
|
254
|
+
* unattested artifact.
|
|
255
|
+
*
|
|
256
|
+
* Leading environment assignments and the `command unset` / `builtin unset`
|
|
257
|
+
* wrapper forms all reach the same builtin and therefore remove the same
|
|
258
|
+
* bindings.
|
|
259
|
+
*
|
|
260
|
+
* `-v` selects the variable namespace, which is what this map models. `-f`
|
|
261
|
+
* unsets a shell function and touches no variable, so such a command is left to
|
|
262
|
+
* bind nothing.
|
|
263
|
+
*
|
|
264
|
+
* @param command - One tokenised command.
|
|
265
|
+
* @returns The scalar names unset, empty when the command unsets none.
|
|
266
|
+
*/
|
|
267
|
+
export declare function unsetNames(command: ShellCommand): string[];
|
|
268
|
+
/**
|
|
269
|
+
* Split one shell line into segments separated by control operators.
|
|
270
|
+
*
|
|
271
|
+
* Quoting and backslash escaping are respected so that operators inside quotes
|
|
272
|
+
* or after a backslash do not split. Comments (`#` at a word boundary) absorb
|
|
273
|
+
* the rest of the line into the preceding segment. The returned array
|
|
274
|
+
* interleaves text segments with operator segments (`;`, `&`, `&&`, `|`, `||`).
|
|
275
|
+
*
|
|
276
|
+
* @param line - One line of shell source (continuations already joined).
|
|
277
|
+
* @returns Segments where control operators are separate entries.
|
|
278
|
+
*/
|
|
279
|
+
export declare function segmentShellLine(line: string): string[];
|
|
280
|
+
/**
|
|
281
|
+
* Net change in `case` nesting contributed by one line.
|
|
282
|
+
*
|
|
283
|
+
* Tracked separately from block depth because a `case` arm label ends in a bare
|
|
284
|
+
* `)` that is not a parenthesis close. Without knowing a `case` is open, that
|
|
285
|
+
* `)` decrements the depth counter and tags an assignment inside an untaken arm
|
|
286
|
+
* as file-scoped, which then attests a publish after `esac`.
|
|
287
|
+
*
|
|
288
|
+
* @param line - One already-continuation-joined source line.
|
|
289
|
+
* @returns +1 for each `case` opened, -1 for each `esac` closed.
|
|
290
|
+
*/
|
|
291
|
+
export declare function caseDepthChange(line: string): number;
|
|
292
|
+
/**
|
|
293
|
+
* Report whether a segment's `case` arm label belongs to an ENCLOSING `case`.
|
|
294
|
+
*
|
|
295
|
+
* A segment can both close one arm and open a nested block, and the two cases
|
|
296
|
+
* need opposite treatment. In `b) case "$Y" in` the label precedes the opener,
|
|
297
|
+
* so it starts a sibling of the previous arm and that arm's bindings must be
|
|
298
|
+
* cleared. In `case "$Y" in c) npm publish` the opener precedes the label, so
|
|
299
|
+
* the label is the FIRST arm of the block this very segment opens - it has no
|
|
300
|
+
* earlier sibling, and clearing there would discard a binding the enclosing arm
|
|
301
|
+
* legitimately made, refusing a publish the shell does attest.
|
|
302
|
+
*
|
|
303
|
+
* Position is what separates them, which is why this compares indices rather
|
|
304
|
+
* than asking the two questions independently.
|
|
305
|
+
*
|
|
306
|
+
* @param line - One shell segment.
|
|
307
|
+
* @returns True when the segment opens an arm of a `case` it did not itself open.
|
|
308
|
+
*/
|
|
309
|
+
export declare function startsEnclosingCaseArm(line: string): boolean;
|
|
310
|
+
/**
|
|
311
|
+
* Decide whether text starts a new `case` arm.
|
|
312
|
+
*
|
|
313
|
+
* An arm label ends at an unquoted `)` that does not close a parenthesis group.
|
|
314
|
+
* Treating an uncertain bare close as an arm boundary is deliberately
|
|
315
|
+
* conservative: it discards bindings from the preceding arm, which can only
|
|
316
|
+
* make later attestation fail closed.
|
|
317
|
+
*
|
|
318
|
+
* @param line - One already-continuation-joined source segment inside a case.
|
|
319
|
+
* @returns True when the segment contains a case-arm label terminator.
|
|
320
|
+
*/
|
|
321
|
+
export declare function startsCaseArm(line: string): boolean;
|
|
322
|
+
/**
|
|
323
|
+
* Net change in block nesting contributed by one line.
|
|
324
|
+
*
|
|
325
|
+
* Counts shell keywords and brace/parenthesis groups outside quotes. This is a
|
|
326
|
+
* nesting count, not a parse: it exists to tell "this assignment is inside
|
|
327
|
+
* something conditional" from "this assignment is the file's own scope", which
|
|
328
|
+
* is the distinction attestation depends on.
|
|
329
|
+
*
|
|
330
|
+
* @param line - One already-continuation-joined source line.
|
|
331
|
+
* @param insideCase - Whether a `case` is open, so a bare `)` is an arm label
|
|
332
|
+
* rather than a group close.
|
|
333
|
+
* @returns Positive when the line opens more blocks than it closes.
|
|
334
|
+
*/
|
|
335
|
+
export declare function blockDepthChange(line: string, insideCase?: boolean): number;
|
|
336
|
+
/**
|
|
337
|
+
* Index scalar assignments per line, so a command held in a variable can be
|
|
338
|
+
* audited against the bindings that stand where it actually runs.
|
|
339
|
+
*
|
|
340
|
+
* `CMD="npm publish"` followed by `$CMD` runs a publish that no scan of the
|
|
341
|
+
* invocation line can see, because the invocation line contains no publish. The
|
|
342
|
+
* assignment is where the command actually is. `NPM=npm` followed by
|
|
343
|
+
* `$NPM publish` hides one the same way, so unquoted values are indexed too.
|
|
344
|
+
*
|
|
345
|
+
* A name is taken only where a line OPENS with one assignment carrying a fully
|
|
346
|
+
* literal value and holds nothing else before its end or a `;`. `NPM=npm; cmd`
|
|
347
|
+
* therefore binds, because the semicolon ends the assignment and the shell keeps
|
|
348
|
+
* it afterwards, while `NPM=npm cmd` does not, because that binding lasts only
|
|
349
|
+
* for the command it precedes. Requiring the line to OPEN with the assignment is
|
|
350
|
+
* what keeps a `;` inside a comment from exposing one. That single rule keeps
|
|
351
|
+
* the scan from inventing
|
|
352
|
+
* bindings the shell never makes, each of which let an unattested publish
|
|
353
|
+
* borrow a flag and pass the gate:
|
|
354
|
+
*
|
|
355
|
+
* - `# FLAG=--provenance` is a comment, and a comment is not a line that is
|
|
356
|
+
* only an assignment.
|
|
357
|
+
* - `echo "config NPM=npm"` is a command with an argument, not an assignment.
|
|
358
|
+
* - `FLAG=--provenance some-command` binds only for that one command; the shell
|
|
359
|
+
* does not keep it afterwards, so neither does this map.
|
|
360
|
+
* - `$(FLAG=--provenance)` binds inside a subshell that the outer shell never
|
|
361
|
+
* sees.
|
|
362
|
+
* - `NPM=npm$SUFFIX` and `NPM=npm$(printf foo)` are not literal. The value must
|
|
363
|
+
* match to the end of the line, so a prefix is never mistaken for the whole
|
|
364
|
+
* value -- the mistake that let a scan analyse a different command from the
|
|
365
|
+
* one the shell runs.
|
|
366
|
+
*
|
|
367
|
+
* `export NPM=npm`, a trailing `# comment` and a CRLF line ending are all still
|
|
368
|
+
* assignments: refusing them left `$NPM` unresolved, and an attested publish
|
|
369
|
+
* elsewhere in the file then satisfied the non-vacuity guard, so being too
|
|
370
|
+
* strict here passes an unattested publish just as being too loose does.
|
|
371
|
+
*
|
|
372
|
+
* Escapes are honoured outside single quotes, so `NPM=npm\\ publish` is one word
|
|
373
|
+
* holding a command while `CMD='"'"'a\\b'"'"' keeps its backslash as the shell does.
|
|
374
|
+
* A value that still carries a substitution, backtick, quote or parenthesis
|
|
375
|
+
* after unescaping is refused: inlining `pkg_name="$(node -p …)"` injects an
|
|
376
|
+
* unbalanced parenthesis into an unrelated command, and the scan then reports
|
|
377
|
+
* invocations that are not there while losing the one that is -- a false
|
|
378
|
+
* verdict in both directions, which is worse than not resolving the variable.
|
|
379
|
+
* Shell metacharacters (`;`, `&`, `|`, `<`, `>`, `{`, `}`) are also refused
|
|
380
|
+
* after unescaping: an escaped `\;` would become `;`, and the tokeniser would
|
|
381
|
+
* split on it, so `FLAG=--provenance\;` would let an unattested publish borrow
|
|
382
|
+
* a flag the shell passes as a literal `--provenance;` argument.
|
|
383
|
+
*
|
|
384
|
+
* Two things the shell does that a line-at-a-time reading missed both let an
|
|
385
|
+
* unattested publish pass. A heredoc body is stdin data and binds nothing, yet
|
|
386
|
+
* `FLAG=--provenance` inside one was indexed as an assignment; see
|
|
387
|
+
* {@link heredocBodyLines}. And `unset FLAG` discards a binding, yet the map
|
|
388
|
+
* kept it, so a later `npm publish $FLAG` borrowed a flag the shell no longer
|
|
389
|
+
* passed; see the deletion below.
|
|
390
|
+
*
|
|
391
|
+
* Because `unset` makes a binding's lifetime positional, the result is one map
|
|
392
|
+
* per line rather than one per file. Reading every invocation against the
|
|
393
|
+
* file's end state would report an unattested publish for a flag that was
|
|
394
|
+
* genuinely set where the publish runs and unset only afterwards.
|
|
395
|
+
*
|
|
396
|
+
* @param text - File contents with continuations already joined.
|
|
397
|
+
* @returns One map per line: the bindings in effect once that line has run.
|
|
398
|
+
*/
|
|
399
|
+
export declare function shellScalarsByLine(text: string): Array<Map<string, string>>;
|
|
400
|
+
/**
|
|
401
|
+
* The scalar bindings standing at the end of a file.
|
|
402
|
+
*
|
|
403
|
+
* @param text - File contents with continuations already joined.
|
|
404
|
+
* @returns Variable name mapped to the literal text it holds.
|
|
405
|
+
*/
|
|
406
|
+
export declare function shellScalars(text: string): Map<string, string>;
|
|
407
|
+
/**
|
|
408
|
+
* Expand unescaped, non-single-quoted scalar references against known bindings.
|
|
409
|
+
*
|
|
410
|
+
* An unknown name is left in place for the same reason an unknown array is:
|
|
411
|
+
* erasing it would turn "not understood" into "carries no flags", which reads
|
|
412
|
+
* as a pass.
|
|
413
|
+
*
|
|
414
|
+
* @param line - One logical command.
|
|
415
|
+
* @param scalars - Scalar assignments from the same file.
|
|
416
|
+
* @returns The command with expandable known scalar references inlined.
|
|
417
|
+
*/
|
|
418
|
+
export declare function expandScalars(line: string, scalars: Map<string, string>): string;
|
|
419
|
+
/**
|
|
420
|
+
* Expand `"${name[@]}"` references against the file's array declarations.
|
|
421
|
+
*
|
|
422
|
+
* An unknown name is left untouched rather than erased: silently dropping it
|
|
423
|
+
* would turn "this scan does not understand the command" into "this command has
|
|
424
|
+
* no flags", which reads as a pass.
|
|
425
|
+
*
|
|
426
|
+
* @param line - One logical command.
|
|
427
|
+
* @param arrays - Array declarations from the same file.
|
|
428
|
+
* @returns The command with referenced array contents inlined.
|
|
429
|
+
*/
|
|
430
|
+
export declare function expandArrays(line: string, arrays: Map<string, string>): string;
|
|
431
|
+
/** The outcome of one verifier run. */
|
|
432
|
+
export interface VerifierResult {
|
|
433
|
+
/** Reasons the run failed; empty means it passed. */
|
|
434
|
+
failures: string[];
|
|
435
|
+
/** Lines describing what was checked, for the operator. */
|
|
436
|
+
notes: string[];
|
|
437
|
+
}
|
|
438
|
+
//# sourceMappingURL=shell-scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell-scan.d.ts","sourceRoot":"","sources":["../shell-scan.ts"],"names":[],"mappings":"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"}
|