dsh-plugin-inspector 0.4.0 → 0.5.0
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/lib/checks/tier-a.js +86 -3
- package/lib/checks/tier-c.js +92 -20
- package/lib/files.js +17 -0
- package/lib/knowledge.js +32 -0
- package/lib/types/checks/tier-c.d.ts +9 -8
- package/lib/types/files.d.ts +10 -0
- package/lib/types/knowledge.d.ts +30 -0
- package/package.json +1 -1
package/lib/checks/tier-a.js
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
* @module dsh-plugin-inspector/checks/tier-a
|
|
10
10
|
*/
|
|
11
11
|
import { isJsExpr } from "../cordis-yaml.js";
|
|
12
|
-
import { boundedJson, lineColumn, normalizePackagePath, snippet } from "../files.js";
|
|
12
|
+
import { boundedJson, isNativeSource, lineColumn, normalizePackagePath, snippet } from "../files.js";
|
|
13
13
|
import { scanInjection } from "../injection.js";
|
|
14
|
-
import { CORE_ROWS, HARNESS_BUNDLE_PACKAGES, INSTALL_LIFECYCLE_SCRIPTS,
|
|
14
|
+
import { CORE_ROWS, GYP_COMMAND_KEYS, HARNESS_BUNDLE_PACKAGES, INSTALL_LIFECYCLE_SCRIPTS, MCP_CLIENT_PACKAGE, NATIVE_BUILD_FILE, SECURITY_ROW_IDS, SECURITY_SEAM_KEYS, SEAM_KEYS, SKILL_FILESYSTEM_ROW, SKILL_ROOT_CONFIG_KEYS, matchingLifecycleSignals, } from "../knowledge.js";
|
|
15
15
|
import { declaredPackages } from "../manifest.js";
|
|
16
16
|
/**
|
|
17
17
|
* Checks that read a Cordis patch row. None of them may produce a finding
|
|
@@ -424,7 +424,7 @@ function checkManifest(input) {
|
|
|
424
424
|
for (const name of lifecycle) {
|
|
425
425
|
/* v8 ignore next -- `name` came from filtering the same object's own keys. */
|
|
426
426
|
const command = manifest.scripts[name] ?? '';
|
|
427
|
-
const signals =
|
|
427
|
+
const signals = matchingLifecycleSignals(command);
|
|
428
428
|
findings.push(tierA({
|
|
429
429
|
checkId: 'A1',
|
|
430
430
|
name: 'install-lifecycle-script',
|
|
@@ -548,6 +548,88 @@ function checkManifest(input) {
|
|
|
548
548
|
}
|
|
549
549
|
return findings;
|
|
550
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Whether the package ships anything a native build would compile.
|
|
553
|
+
*
|
|
554
|
+
* Skipped files count: a `.cc` the reader passed over for its size is still a
|
|
555
|
+
* source in the tarball, and claiming a package has none because the analyzer
|
|
556
|
+
* declined to read one would be wrong in the direction that raises a finding.
|
|
557
|
+
* @param input - the decoded package.
|
|
558
|
+
* @returns true when C-family source is present.
|
|
559
|
+
*/
|
|
560
|
+
function shipsNativeSource(input) {
|
|
561
|
+
const paths = [...input.source.files.keys(), ...input.source.skipped.map(entry => entry.path)];
|
|
562
|
+
return paths.some(isNativeSource);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* A24 — a native build declaration, which is an install-time execution point
|
|
566
|
+
* that appears in no entry the manifest declares.
|
|
567
|
+
*
|
|
568
|
+
* Tier A because the decidable half is the whole finding: the file is at the
|
|
569
|
+
* package root or it is not, and npm's default install command for a package
|
|
570
|
+
* that ships one and declares no `install` or `preinstall` script is
|
|
571
|
+
* `node-gyp rebuild`. Nothing has to be inferred about the code to know that a
|
|
572
|
+
* build runs, which is the same standard A1 and A22 are read at — a field npm
|
|
573
|
+
* itself must read literally in order to act on it.
|
|
574
|
+
*
|
|
575
|
+
* **The file is not parsed and never evaluated.** GYP is Python-ish, not JSON:
|
|
576
|
+
* single-quoted strings, `#` comments, trailing commas, and `conditions` whose
|
|
577
|
+
* first element is a Python expression written as a string. There is no
|
|
578
|
+
* maintained JavaScript parser for it — `node-gyp` shells out to Python — so
|
|
579
|
+
* parsing it here would mean hand-rolling one for an attacker-controlled file,
|
|
580
|
+
* and evaluating a condition is the one thing this tool may never do. It also
|
|
581
|
+
* would not change the verdict: what distinguishes a build declaration from a
|
|
582
|
+
* build step is the presence of an `actions`, `rules` or `postbuilds` key and
|
|
583
|
+
* the shape of the command line under it, and both are literal text in the file
|
|
584
|
+
* either way. The severity is therefore keyed on a key match plus the same
|
|
585
|
+
* command signals A1 grades a lifecycle script by.
|
|
586
|
+
* @param input - the decoded package.
|
|
587
|
+
* @returns the finding, or none when the package ships no `binding.gyp`.
|
|
588
|
+
*/
|
|
589
|
+
function checkNativeBuild(input) {
|
|
590
|
+
const text = input.source.files.get(NATIVE_BUILD_FILE);
|
|
591
|
+
if (text === undefined)
|
|
592
|
+
return [];
|
|
593
|
+
const runsCommands = GYP_COMMAND_KEYS.test(text);
|
|
594
|
+
const signals = runsCommands ? matchingLifecycleSignals(text) : [];
|
|
595
|
+
const empty = shipsNativeSource(input) ? '' : ' The package ships no C or C++ source, so there is nothing here for '
|
|
596
|
+
+ 'a compiler to build and the build step is the only effect the file has.';
|
|
597
|
+
const first = signals[0];
|
|
598
|
+
let at = 0;
|
|
599
|
+
if (first !== undefined) {
|
|
600
|
+
/* v8 ignore next -- `first` is in the list because it matched this same text, so `exec` finds it again. */
|
|
601
|
+
at = first.pattern.exec(text)?.index ?? 0;
|
|
602
|
+
}
|
|
603
|
+
return [tierA({
|
|
604
|
+
checkId: 'A24',
|
|
605
|
+
name: 'native-build-declaration',
|
|
606
|
+
subject: NATIVE_BUILD_FILE,
|
|
607
|
+
severity: signals.length === 0 ? 'medium' : 'high',
|
|
608
|
+
title: signals.length === 0
|
|
609
|
+
? 'Ships `binding.gyp`, which npm turns into an install-time build'
|
|
610
|
+
: 'Ships a `binding.gyp` whose build steps run commands rather than a compiler',
|
|
611
|
+
detail: 'A package that ships this file and declares no `install` or `preinstall` script gets `node-gyp rebuild` '
|
|
612
|
+
+ 'as its install command by default, and `node-gyp` evaluates the file to decide what that build does. The '
|
|
613
|
+
+ 'declaration is in none of the entry points a reader checks: not `main`, not `bin`, not `exports`, and not '
|
|
614
|
+
+ '`scripts`. It runs under the same gate as A1 — pnpm and npm block a dependency\'s build until the package is '
|
|
615
|
+
+ 'named in `allowBuilds` — but reaching that gate takes no key in `package.json` at all, which is why an '
|
|
616
|
+
+ 'ecosystem where install hooks are off by default is one where this path is worth reading.'
|
|
617
|
+
+ (signals.length === 0
|
|
618
|
+
? ' This file declares no `actions`, `rules` or `postbuilds` step whose command line does anything a compile '
|
|
619
|
+
+ 'does not, so what it describes is a build.'
|
|
620
|
+
: ` It declares a build step whose command ${signals.map(signal => signal.meaning).join(', and ')}.`)
|
|
621
|
+
+ empty
|
|
622
|
+
+ ' The file was read as text, never parsed and never evaluated — GYP is Python-ish syntax whose conditions are '
|
|
623
|
+
+ 'Python expressions. Reading it that way is enough to decide that a build runs, which is this finding. It is '
|
|
624
|
+
+ 'not enough to decide what the build does, so the grade above reads the command line the way A1 reads a '
|
|
625
|
+
+ 'lifecycle script\'s.',
|
|
626
|
+
evidence: {
|
|
627
|
+
file: NATIVE_BUILD_FILE,
|
|
628
|
+
path: lineColumn(text, at),
|
|
629
|
+
snippet: snippet(text.slice(at, at + 400)),
|
|
630
|
+
},
|
|
631
|
+
})];
|
|
632
|
+
}
|
|
551
633
|
/** A12 — shipped markdown that reaches the model when it is discovered. */
|
|
552
634
|
function checkModelVisibleText(input) {
|
|
553
635
|
if (input.modelVisibleFiles.length === 0)
|
|
@@ -652,6 +734,7 @@ function checkInjectionText(input) {
|
|
|
652
734
|
export function runTierA(input) {
|
|
653
735
|
const findings = [
|
|
654
736
|
...checkManifest(input),
|
|
737
|
+
...checkNativeBuild(input),
|
|
655
738
|
...checkDisabledRows(input),
|
|
656
739
|
...checkOverriddenRows(input),
|
|
657
740
|
...checkExpressions(input),
|
package/lib/checks/tier-c.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import ts from 'typescript';
|
|
18
18
|
import { lineColumn, snippet } from "../files.js";
|
|
19
|
+
import { MAX_EXAMPLES } from "../model.js";
|
|
19
20
|
/** A line longer than this is not written by hand. */
|
|
20
21
|
const MINIFIED_LINE_LENGTH = 500;
|
|
21
22
|
/** Below this many bytes, a low line count says nothing. */
|
|
@@ -35,14 +36,26 @@ const NAMED_TARGET_CALLEES = new Set([
|
|
|
35
36
|
function tierC(finding) {
|
|
36
37
|
return { ...finding, tier: 'C', confidence: 'moderate', examples: [finding.evidence], occurrences: 1 };
|
|
37
38
|
}
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Parse every shipped source file once.
|
|
41
|
+
* @param input - the decoded package.
|
|
42
|
+
* @returns one entry per source file, in `sourceFiles` order.
|
|
43
|
+
*/
|
|
44
|
+
function parseSources(input) {
|
|
45
|
+
const files = [];
|
|
41
46
|
for (const path of input.sourceFiles) {
|
|
42
47
|
const text = input.source.files.get(path);
|
|
43
48
|
/* v8 ignore next -- `sourceFiles` is filtered from `source.files`'s own keys, so the lookup always hits. */
|
|
44
49
|
if (text === undefined)
|
|
45
50
|
continue;
|
|
51
|
+
files.push({ path, text, node: ts.createSourceFile(path, text, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS) });
|
|
52
|
+
}
|
|
53
|
+
return files;
|
|
54
|
+
}
|
|
55
|
+
/** C1 — source that is not written to be read. */
|
|
56
|
+
function checkMinification(files) {
|
|
57
|
+
const findings = [];
|
|
58
|
+
for (const { path, text } of files) {
|
|
46
59
|
const lines = text.split('\n');
|
|
47
60
|
const longest = lines.reduce((max, line) => Math.max(max, line.length), 0);
|
|
48
61
|
const dense = text.length >= MINIFICATION_SIZE_FLOOR && lines.length < 5;
|
|
@@ -73,14 +86,9 @@ function checkMinification(input) {
|
|
|
73
86
|
return findings;
|
|
74
87
|
}
|
|
75
88
|
/** C2 — names the analyzer cannot resolve without running the code. */
|
|
76
|
-
function checkDynamicDispatch(
|
|
89
|
+
function checkDynamicDispatch(files) {
|
|
77
90
|
const findings = [];
|
|
78
|
-
for (const path of
|
|
79
|
-
const text = input.source.files.get(path);
|
|
80
|
-
/* v8 ignore next -- `sourceFiles` is filtered from `source.files`'s own keys, so the lookup always hits. */
|
|
81
|
-
if (text === undefined)
|
|
82
|
-
continue;
|
|
83
|
-
const source = ts.createSourceFile(path, text, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
|
|
91
|
+
for (const { path, text, node: source } of files) {
|
|
84
92
|
const report = (node, what) => {
|
|
85
93
|
findings.push(tierC({
|
|
86
94
|
checkId: 'C2',
|
|
@@ -194,18 +202,80 @@ function literalOf(node) {
|
|
|
194
202
|
return node.text;
|
|
195
203
|
return null;
|
|
196
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* C8 — identifiers spelled with Unicode escapes.
|
|
207
|
+
*
|
|
208
|
+
* An escaped spelling and a plain one are the same identifier: the escape is
|
|
209
|
+
* resolved in the scanner, before any binding, so the two are the same program
|
|
210
|
+
* and only a reader sees a difference. That is the whole technique — the file
|
|
211
|
+
* says one thing to a person and another to the engine.
|
|
212
|
+
*
|
|
213
|
+
* It says the same thing to this tool as to the engine. `ts.createSourceFile`
|
|
214
|
+
* hands back `node.text === 'fetch'` for the escaped form, so every Tier B
|
|
215
|
+
* check that matches a name matches the escaped spelling too. That is measured
|
|
216
|
+
* rather than assumed — `tests/unit/detection.spec.ts` runs escaped spellings
|
|
217
|
+
* through B6, B7, B9 and B12 — which is why this finding sits with C3 in
|
|
218
|
+
* {@link NON_DEGRADING_CHECKS} rather than making every Tier B negative
|
|
219
|
+
* unreliable.
|
|
220
|
+
* @param files - the parsed source files.
|
|
221
|
+
* @returns one finding for the package, or none.
|
|
222
|
+
*/
|
|
223
|
+
function checkEscapedIdentifiers(files) {
|
|
224
|
+
const sites = [];
|
|
225
|
+
for (const { path, text, node: source } of files) {
|
|
226
|
+
const visit = (node) => {
|
|
227
|
+
if (ts.isIdentifier(node)) {
|
|
228
|
+
const start = node.getStart(source);
|
|
229
|
+
const raw = text.slice(start, node.end);
|
|
230
|
+
// An identifier token holds a backslash only as part of a `\uXXXX` or
|
|
231
|
+
// `\u{X}` escape; nothing else in the grammar puts one there.
|
|
232
|
+
if (raw.includes('\\')) {
|
|
233
|
+
sites.push({ name: node.text, evidence: { file: path, path: lineColumn(text, start), snippet: snippet(raw) } });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
ts.forEachChild(node, visit);
|
|
237
|
+
};
|
|
238
|
+
ts.forEachChild(source, visit);
|
|
239
|
+
}
|
|
240
|
+
const first = sites[0];
|
|
241
|
+
if (first === undefined)
|
|
242
|
+
return [];
|
|
243
|
+
const names = [...new Set(sites.map(site => site.name))].sort();
|
|
244
|
+
return [{
|
|
245
|
+
...tierC({
|
|
246
|
+
checkId: 'C8',
|
|
247
|
+
name: 'escaped-identifier',
|
|
248
|
+
subject: 'escaped-identifier',
|
|
249
|
+
severity: 'medium',
|
|
250
|
+
title: 'Writes identifier names as Unicode escapes',
|
|
251
|
+
detail: `The escapes resolve to ${names.map(name => `\`${name}\``).join(', ')}. JavaScript resolves an `
|
|
252
|
+
+ 'identifier escape in the scanner, so the escaped and the plain spelling are the same program and no '
|
|
253
|
+
+ 'behavior distinguishes them — the difference is only visible to whoever reads the file. Nothing writes a '
|
|
254
|
+
+ 'name this way by accident, and a published package has no build reason to.'
|
|
255
|
+
+ ' This does not weaken the rest of the report: the parser resolves the escape before any check sees the '
|
|
256
|
+
+ 'name, so a `\\u`-escaped `fetch` is still reported as network egress and an escaped `process.env` read is '
|
|
257
|
+
+ 'still reported as a credential read. What the escape defeats is the reading, not the detection.',
|
|
258
|
+
evidence: first.evidence,
|
|
259
|
+
bypass: 'concealing the name a way this check is not about — a computed member or a string assembled at '
|
|
260
|
+
+ 'runtime, which is C2',
|
|
261
|
+
}),
|
|
262
|
+
examples: sites.slice(0, MAX_EXAMPLES).map(site => site.evidence),
|
|
263
|
+
occurrences: sites.length,
|
|
264
|
+
}];
|
|
265
|
+
}
|
|
197
266
|
/**
|
|
198
267
|
* Tier C checks that do **not** make a Tier B negative unreliable.
|
|
199
268
|
*
|
|
200
|
-
* Every other check here says the analyzer could not read something. C3
|
|
201
|
-
* the opposite: the bytes were read exactly as written and exactly as
|
|
202
|
-
* run — what cannot be checked is whether they match the repository
|
|
203
|
-
* claims to have produced them.
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
269
|
+
* Every other check here says the analyzer could not read something. C3 and C8
|
|
270
|
+
* say the opposite. C3: the bytes were read exactly as written and exactly as
|
|
271
|
+
* they will run — what cannot be checked is whether they match the repository
|
|
272
|
+
* that claims to have produced them. Treating that as an unreadable package
|
|
273
|
+
* marks every ordinary published tarball `degraded`, because shipping built
|
|
274
|
+
* output and no source is what publishing a package *is*. C8: the escape is
|
|
275
|
+
* resolved by the parser before any check reads the name, so the analysis of an
|
|
276
|
+
* escaped identifier is exactly as good as the analysis of a plain one.
|
|
207
277
|
*/
|
|
208
|
-
export const NON_DEGRADING_CHECKS = new Set(['C3']);
|
|
278
|
+
export const NON_DEGRADING_CHECKS = new Set(['C3', 'C8']);
|
|
209
279
|
/** C3, C6 — shipped build output with nothing to compare it against. */
|
|
210
280
|
function checkSourcelessBuild(input) {
|
|
211
281
|
const built = input.sourceFiles.filter(path => /^(?:lib|dist|build|out)\//.test(path));
|
|
@@ -308,9 +378,11 @@ function checkPatchAliases(input) {
|
|
|
308
378
|
* @returns findings, unordered.
|
|
309
379
|
*/
|
|
310
380
|
export function runTierC(input) {
|
|
381
|
+
const files = parseSources(input);
|
|
311
382
|
return [
|
|
312
|
-
...checkMinification(
|
|
313
|
-
...checkDynamicDispatch(
|
|
383
|
+
...checkMinification(files),
|
|
384
|
+
...checkDynamicDispatch(files),
|
|
385
|
+
...checkEscapedIdentifiers(files),
|
|
314
386
|
...checkSourcelessBuild(input),
|
|
315
387
|
...checkUnreadableFiles(input),
|
|
316
388
|
...checkPatchWalkLimit(input),
|
package/lib/files.js
CHANGED
|
@@ -20,6 +20,23 @@ export function isSourceFile(path) {
|
|
|
20
20
|
return false;
|
|
21
21
|
return SOURCE_EXTENSIONS.some(extension => path.endsWith(extension));
|
|
22
22
|
}
|
|
23
|
+
/** Extensions a `binding.gyp` target compiles. */
|
|
24
|
+
const NATIVE_SOURCE_EXTENSIONS = [
|
|
25
|
+
'.c', '.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp', '.hxx', '.m', '.mm', '.s', '.asm',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Whether a path is C-family source a native build would compile.
|
|
29
|
+
*
|
|
30
|
+
* Used to answer one question about a package that ships a `binding.gyp`: is
|
|
31
|
+
* there anything in it to build. A gyp with no compilable source is a build
|
|
32
|
+
* declaration whose only effect is that a build runs.
|
|
33
|
+
* @param path - package-relative POSIX path.
|
|
34
|
+
* @returns true when the file is C-family source or a header.
|
|
35
|
+
*/
|
|
36
|
+
export function isNativeSource(path) {
|
|
37
|
+
const lower = path.toLowerCase();
|
|
38
|
+
return NATIVE_SOURCE_EXTENSIONS.some(extension => lower.endsWith(extension));
|
|
39
|
+
}
|
|
23
40
|
/**
|
|
24
41
|
* Whether a path is markdown that can reach the model verbatim.
|
|
25
42
|
*
|
package/lib/knowledge.js
CHANGED
|
@@ -329,6 +329,38 @@ export const LIFECYCLE_SIGNALS = [
|
|
|
329
329
|
meaning: 'decodes an encoded payload, which is how a command hides what it runs',
|
|
330
330
|
},
|
|
331
331
|
];
|
|
332
|
+
/**
|
|
333
|
+
* Every lifecycle signal a command line matches.
|
|
334
|
+
*
|
|
335
|
+
* One entry point rather than the filter written out twice, because the table
|
|
336
|
+
* now grades two different things — a `package.json` lifecycle command (A1) and
|
|
337
|
+
* a `binding.gyp` build step (A24) — and a rule added to it has to reach both.
|
|
338
|
+
* @param command - the command line, or the text that holds one.
|
|
339
|
+
* @returns the matching signals, in table order.
|
|
340
|
+
*/
|
|
341
|
+
export function matchingLifecycleSignals(command) {
|
|
342
|
+
return LIFECYCLE_SIGNALS.filter(signal => signal.pattern.test(command));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The file `node-gyp` reads, at the package root and nowhere else.
|
|
346
|
+
*
|
|
347
|
+
* npm and pnpm treat its presence as a declaration: a package that ships one
|
|
348
|
+
* and declares no `install` or `preinstall` script gets `node-gyp rebuild` as
|
|
349
|
+
* its install command. That default appears in no field of `package.json`.
|
|
350
|
+
*/
|
|
351
|
+
export const NATIVE_BUILD_FILE = 'binding.gyp';
|
|
352
|
+
/**
|
|
353
|
+
* GYP keys that carry a command line rather than a list of sources to compile.
|
|
354
|
+
*
|
|
355
|
+
* `actions` and `rules` run a program during the build; `postbuilds` runs one
|
|
356
|
+
* after it. A target that only lists `sources`, `include_dirs` and `libraries`
|
|
357
|
+
* compiles code the package shipped and runs nothing else.
|
|
358
|
+
*
|
|
359
|
+
* Matched against the whole file, so a block nested inside a `conditions` arm
|
|
360
|
+
* counts the same as a top-level one — which is the point, because a condition
|
|
361
|
+
* is where a build step goes to be read past.
|
|
362
|
+
*/
|
|
363
|
+
export const GYP_COMMAND_KEYS = /['"](?:actions?|rules?|postbuilds)['"]\s*:/;
|
|
332
364
|
/** Entry fields the loader never interpolates: a `!!js` node here is inert data. */
|
|
333
365
|
export const STATIC_ENTRY_FIELDS = [
|
|
334
366
|
'id', 'name', 'group', 'inject', 'intercept', 'isolate',
|
|
@@ -14,18 +14,19 @@
|
|
|
14
14
|
* from claiming nothing was found.
|
|
15
15
|
* @module dsh-plugin-inspector/checks/tier-c
|
|
16
16
|
*/
|
|
17
|
-
import type
|
|
17
|
+
import { type Finding } from '../model.ts';
|
|
18
18
|
import type { CheckInput } from './input.ts';
|
|
19
19
|
/**
|
|
20
20
|
* Tier C checks that do **not** make a Tier B negative unreliable.
|
|
21
21
|
*
|
|
22
|
-
* Every other check here says the analyzer could not read something. C3
|
|
23
|
-
* the opposite: the bytes were read exactly as written and exactly as
|
|
24
|
-
* run — what cannot be checked is whether they match the repository
|
|
25
|
-
* claims to have produced them.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
22
|
+
* Every other check here says the analyzer could not read something. C3 and C8
|
|
23
|
+
* say the opposite. C3: the bytes were read exactly as written and exactly as
|
|
24
|
+
* they will run — what cannot be checked is whether they match the repository
|
|
25
|
+
* that claims to have produced them. Treating that as an unreadable package
|
|
26
|
+
* marks every ordinary published tarball `degraded`, because shipping built
|
|
27
|
+
* output and no source is what publishing a package *is*. C8: the escape is
|
|
28
|
+
* resolved by the parser before any check reads the name, so the analysis of an
|
|
29
|
+
* escaped identifier is exactly as good as the analysis of a plain one.
|
|
29
30
|
*/
|
|
30
31
|
export declare const NON_DEGRADING_CHECKS: ReadonlySet<string>;
|
|
31
32
|
/**
|
package/lib/types/files.d.ts
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
* @returns true when the file should be parsed.
|
|
11
11
|
*/
|
|
12
12
|
export declare function isSourceFile(path: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Whether a path is C-family source a native build would compile.
|
|
15
|
+
*
|
|
16
|
+
* Used to answer one question about a package that ships a `binding.gyp`: is
|
|
17
|
+
* there anything in it to build. A gyp with no compilable source is a build
|
|
18
|
+
* declaration whose only effect is that a build runs.
|
|
19
|
+
* @param path - package-relative POSIX path.
|
|
20
|
+
* @returns true when the file is C-family source or a header.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isNativeSource(path: string): boolean;
|
|
13
23
|
/**
|
|
14
24
|
* Whether a path is markdown that can reach the model verbatim.
|
|
15
25
|
*
|
package/lib/types/knowledge.d.ts
CHANGED
|
@@ -134,6 +134,36 @@ export interface LifecycleSignal {
|
|
|
134
134
|
* that shape is deliberately not a signal here.
|
|
135
135
|
*/
|
|
136
136
|
export declare const LIFECYCLE_SIGNALS: readonly LifecycleSignal[];
|
|
137
|
+
/**
|
|
138
|
+
* Every lifecycle signal a command line matches.
|
|
139
|
+
*
|
|
140
|
+
* One entry point rather than the filter written out twice, because the table
|
|
141
|
+
* now grades two different things — a `package.json` lifecycle command (A1) and
|
|
142
|
+
* a `binding.gyp` build step (A24) — and a rule added to it has to reach both.
|
|
143
|
+
* @param command - the command line, or the text that holds one.
|
|
144
|
+
* @returns the matching signals, in table order.
|
|
145
|
+
*/
|
|
146
|
+
export declare function matchingLifecycleSignals(command: string): LifecycleSignal[];
|
|
147
|
+
/**
|
|
148
|
+
* The file `node-gyp` reads, at the package root and nowhere else.
|
|
149
|
+
*
|
|
150
|
+
* npm and pnpm treat its presence as a declaration: a package that ships one
|
|
151
|
+
* and declares no `install` or `preinstall` script gets `node-gyp rebuild` as
|
|
152
|
+
* its install command. That default appears in no field of `package.json`.
|
|
153
|
+
*/
|
|
154
|
+
export declare const NATIVE_BUILD_FILE = "binding.gyp";
|
|
155
|
+
/**
|
|
156
|
+
* GYP keys that carry a command line rather than a list of sources to compile.
|
|
157
|
+
*
|
|
158
|
+
* `actions` and `rules` run a program during the build; `postbuilds` runs one
|
|
159
|
+
* after it. A target that only lists `sources`, `include_dirs` and `libraries`
|
|
160
|
+
* compiles code the package shipped and runs nothing else.
|
|
161
|
+
*
|
|
162
|
+
* Matched against the whole file, so a block nested inside a `conditions` arm
|
|
163
|
+
* counts the same as a top-level one — which is the point, because a condition
|
|
164
|
+
* is where a build step goes to be read past.
|
|
165
|
+
*/
|
|
166
|
+
export declare const GYP_COMMAND_KEYS: RegExp;
|
|
137
167
|
/** Entry fields the loader never interpolates: a `!!js` node here is inert data. */
|
|
138
168
|
export declare const STATIC_ENTRY_FIELDS: readonly string[];
|
|
139
169
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-inspector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Know what a DeepSeek Harness plugin does before you install it — static pre-install analysis of a plugin directory or tarball",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ivan Tyshchenko",
|