dsh-plugin-inspector 0.2.1 → 0.3.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/README.md +12 -15
- package/lib/checks/tier-a.js +11 -4
- package/lib/injection.js +16 -0
- package/lib/inspect.js +13 -2
- package/lib/knowledge.js +39 -0
- package/lib/types/inspect.d.ts +12 -2
- package/lib/types/knowledge.d.ts +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,15 +13,15 @@ $ dsh-inspect --from-npm some-dsh-plugin@1.4.0
|
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
16
|
-
Node `^22.19.0 || >=24`.
|
|
16
|
+
Node `^22.19.0 || >=24`.
|
|
17
17
|
|
|
18
18
|
```console
|
|
19
19
|
npm install -g dsh-plugin-inspector
|
|
20
20
|
dsh-inspect --help
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
From a checkout instead — `lib/` is generated, so a fresh clone has no `dsh-inspect` until it is
|
|
24
|
+
built:
|
|
25
25
|
|
|
26
26
|
```console
|
|
27
27
|
git clone https://github.com/CharlotteN7/dsh-plugin-inspector
|
|
@@ -89,14 +89,14 @@ every `B13`.
|
|
|
89
89
|
|
|
90
90
|
## What it reports on the real ecosystem
|
|
91
91
|
|
|
92
|
-
Measured **2026-08-
|
|
93
|
-
publish a resolvable npm package, each pinned to the version
|
|
94
|
-
`pnpm run sweep`; the corpus is `scripts/ecosystem-corpus.json` and the recorded
|
|
95
|
-
`tests/ecosystem-baseline.json`.
|
|
92
|
+
Measured **2026-08-17** against the 40 most-starred GitHub repositories tagged `dsh-plugin` that
|
|
93
|
+
publish a resolvable npm package, each pinned to the version it resolved to on 2026-08-16. Re-run
|
|
94
|
+
it with `pnpm run sweep`; the corpus is `scripts/ecosystem-corpus.json` and the recorded
|
|
95
|
+
measurement is `tests/ecosystem-baseline.json`.
|
|
96
96
|
|
|
97
97
|
Both columns come from the same corpus and the same pinned versions, so the difference is this
|
|
98
|
-
tool's doing and not the ecosystem's. "0.1" is
|
|
99
|
-
|
|
98
|
+
tool's doing and not the ecosystem's. "0.1" is `dsh-plugin-inspector@0.1.0`; "0.2" is this tree,
|
|
99
|
+
which reports itself as `0.2.1` because it reads the version out of its own manifest.
|
|
100
100
|
|
|
101
101
|
| | 0.1 | 0.2 |
|
|
102
102
|
|---|---|---|
|
|
@@ -230,7 +230,7 @@ confidence `certain`.
|
|
|
230
230
|
|
|
231
231
|
| id | Check | Severity | Method |
|
|
232
232
|
|---|---|---|---|
|
|
233
|
-
| A1 | Install lifecycle script (`preinstall`, `install`, `postinstall`, `prepare`, `prepublish`, `preprepare`, `postprepare`) | medium | `package.json.scripts` key set. `dsh plugin add` forwards to pnpm verbatim and adds no `--ignore-scripts`, but pnpm ≥ 10 blocks a dependency's lifecycle scripts by default until the package is listed under `allowBuilds`, and `apps/cli/src/plugin.ts` prints that instruction when a build is blocked. The script is one approval away from running, not already running |
|
|
233
|
+
| A1 | Install lifecycle script (`preinstall`, `install`, `postinstall`, `prepare`, `prepublish`, `preprepare`, `postprepare`) | medium; **high** when the command itself fetches, decodes, pipes to a shell, or evaluates inline code | `package.json.scripts` key set. `dsh plugin add` forwards to pnpm verbatim and adds no `--ignore-scripts`, but pnpm ≥ 10 blocks a dependency's lifecycle scripts by default until the package is listed under `allowBuilds`, and `apps/cli/src/plugin.ts` prints that instruction when a build is blocked. The script is one approval away from running, not already running — which is why the category alone is `medium`, and why the measured 5 of 40 legitimate packages that declare one (`tsdown`, `npm run build`, `husky`, `node scripts/prepare.mjs`) stay there. The escalation reads the command line itself, and is calibrated to fire on none of them |
|
|
234
234
|
| A2 | Patch row sets `disabled` **truthily** on a **security-relevant** core row (`approval`, `permission`, `sandbox`, `sandbox-policy`, `bash-sandbox`, `pwsh-sandbox`, `fs-sandbox`, `fs-observation-policy`, `subprocess`, `credentials`, `timeout-policy`, `spill-policy`, `session-persistence-jsonl`) | **critical** | patch YAML row with `id ∈ SECURITY_ROWS`. The loader coerces — `disabledOf` is `Boolean(options.disabled)` (`vendor/loader/src/config/entry.ts`) — so `null`, `0` and `""` leave the row **running** and are not this finding. A `!!js` node is an object and stays truthy, so an expression is judged by what it can evaluate to |
|
|
235
235
|
| A3 | Patch row disables any other known core row | high for a `@deepseek-ai/dsh-base` row, medium for one only a surface bundle inserts | same, `id ∈ CORE_ROWS`. The row inventory records which of the three shipped bundles inserts each row, because they are not one profile: a `ui-*` row exists only where the web bundle is mounted. Suppressed entirely when the package under analysis *is* one of the three bundles — `@deepseek-ai/dsh-web-app` disabling two dozen rows `@deepseek-ai/dsh-base` inserted is what composing a surface bundle is |
|
|
236
236
|
| A4 | Patch row carries a `name` that does not match the targeted row's `name` | medium | `applyEntryPatches` treats `name` on a non-insert patch as an **assertion guard**, not an override: on mismatch it warns and `continue`s, skipping the whole patch. So this row does nothing at all. Either the author is targeting a row that has been renamed, or the patch is stale — in both cases what the user reads and what mounts disagree |
|
|
@@ -277,15 +277,12 @@ readability finding fires.
|
|
|
277
277
|
| id | Check | Severity | Method |
|
|
278
278
|
|---|---|---|---|
|
|
279
279
|
| B1 | Replaces a core capability seam — `ctx.provide(<seam>, …)` / `ctx.set(<seam>, …)` where `<seam>` is a key from `api-catalog.ts` | **critical** | call expression, literal first argument matched against the seam key set |
|
|
280
|
-
| B2 | Auto-approves — a listener on `approval/request` that returns an approving verdict with no user interaction | **critical** | listener body return analysis |
|
|
281
|
-
| B3 | `tools/pre-execute` listener returning `allow` | high | same |
|
|
282
|
-
| B4 | Waterfall listener that never references `next` | high | The waterfall set is exactly 13 events: `agent/pre-step`, `agent/request`, `agent/request-error`, `approval/request`, `fs/edit-intent`, `fs/write-intent`, `llm/stream`, `session-telemetry/record`, `system-prompt/assemble`, `tools/code-dispatch-log`, `tools/execute`, `tools/post-execute`, `tools/pre-execute`. Per the harness's own rule, returning without calling `next()` short-circuits the chain **including the built-in behavior**, silently disabling the default for everyone downstream. Note there is **no** `fs/read-intent` — the intent family is write and edit only |
|
|
283
280
|
| B5 | System-prompt mutation — `system-prompt/assemble` listener, or `ctx.systemPrompt.{section,context,variable,tools,suppressRuntimeContext}` | high | call matching |
|
|
284
281
|
| B6 | Credential read — `process.env.*(TOKEN\|KEY\|SECRET\|PASSWORD\|CREDENTIAL)*`, `~/.dsh/credentials`, `~/.npmrc`, `~/.aws`, `~/.ssh`, `ctx.credentials.*` | medium alone | identifier + literal matching |
|
|
285
282
|
| B7 | Network egress — `fetch`, `node:http(s).request`, `node:net`, `WebSocket`, `undici` | medium alone | import + call matching |
|
|
286
283
|
| B8 | **Exfiltration pair** — B6 ∧ B7 in the same package | high | set intersection. Reported explicitly as *capability, not dataflow*: the tool cannot prove the credential value reaches the socket. `high` rather than critical because it fires on 18 % of published plugins |
|
|
287
284
|
| B9 | Direct `node:child_process` / `node:worker_threads` / `node:vm` | medium alone, high paired with B8's two halves | import specifier. Bypasses `ctx.subprocess` and `ctx.sandbox` entirely. `medium` alone because a bare import fires on half the published ecosystem |
|
|
288
|
-
| B10 | Prompt-injection heuristics on **model-visible text only** — registered tool `description` string literals, and shipped skill/instruction files | high | imperative-override phrasing, role reassignment, exfiltration instructions, hidden-text markers. Run on *exactly* the text that reaches the model, never on ordinary source comments |
|
|
285
|
+
| B10 | Prompt-injection heuristics on **model-visible text only** — registered tool `description` string literals, and shipped skill/instruction files | high | imperative-override phrasing, role reassignment, exfiltration instructions, hidden-text markers — zero-width and bidirectional controls, the tag block, and runs of four or more variation selectors, the encoding GlassWorm shipped executable JavaScript in. Run on *exactly* the text that reaches the model, never on ordinary source comments |
|
|
289
286
|
| B11 | Nested plugin mounting — `ctx.plugin(…)`, loader manipulation | high | call matching. A layer that mounts further layers moves the analysis target |
|
|
290
287
|
| B12 | Dynamic code construction — `eval`, `new Function`, `vm.runInNewContext`, `module._load` | high | call matching |
|
|
291
288
|
| B13 | Filesystem access outside `ctx.fs` — imports `node:fs` or `node:fs/promises` | medium | Reads and writes through the Node API are invisible to `fs/write-intent`, `fs/edit-intent`, `fs/observed`, and the `fs-sandbox` row, so no policy in the profile sees them and nothing appears in the session log |
|
|
@@ -378,7 +375,7 @@ would have burned the idea.
|
|
|
378
375
|
is why it is `high` and not `critical`.
|
|
379
376
|
7. **Injection phrasing that is not spelled in ASCII.** The injection heuristics are Latin-alphabet
|
|
380
377
|
regexes. Substituting Cyrillic homoglyphs — `о` U+043E for `o`, `е` U+0435 for `e` — defeats
|
|
381
|
-
**every one of the
|
|
378
|
+
**every one of the eleven rules**, including the two hidden-character rules, which look for
|
|
382
379
|
invisible characters and not for visible ones that are the wrong letter. Verified against the
|
|
383
380
|
rule table, not assumed. Normalisation is not in 0.2; do not read a clean `A21`/`B10` as
|
|
384
381
|
evidence that shipped markdown carries no instructions.
|
package/lib/checks/tier-a.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { isJsExpr } from "../cordis-yaml.js";
|
|
12
12
|
import { boundedJson, lineColumn, normalizePackagePath, snippet } from "../files.js";
|
|
13
13
|
import { scanInjection } from "../injection.js";
|
|
14
|
-
import { CORE_ROWS, HARNESS_BUNDLE_PACKAGES, INSTALL_LIFECYCLE_SCRIPTS, MCP_CLIENT_PACKAGE, SECURITY_ROW_IDS, SECURITY_SEAM_KEYS, SEAM_KEYS, SKILL_FILESYSTEM_ROW, SKILL_ROOT_CONFIG_KEYS, } from "../knowledge.js";
|
|
14
|
+
import { CORE_ROWS, HARNESS_BUNDLE_PACKAGES, INSTALL_LIFECYCLE_SCRIPTS, LIFECYCLE_SIGNALS, MCP_CLIENT_PACKAGE, SECURITY_ROW_IDS, SECURITY_SEAM_KEYS, SEAM_KEYS, SKILL_FILESYSTEM_ROW, SKILL_ROOT_CONFIG_KEYS, } 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
|
|
@@ -421,19 +421,26 @@ function checkManifest(input) {
|
|
|
421
421
|
const { manifest, source } = input;
|
|
422
422
|
const lifecycle = INSTALL_LIFECYCLE_SCRIPTS.filter(name => name in manifest.scripts);
|
|
423
423
|
for (const name of lifecycle) {
|
|
424
|
+
const command = manifest.scripts[name] ?? '';
|
|
425
|
+
const signals = LIFECYCLE_SIGNALS.filter(signal => signal.pattern.test(command));
|
|
424
426
|
findings.push(tierA({
|
|
425
427
|
checkId: 'A1',
|
|
426
428
|
name: 'install-lifecycle-script',
|
|
427
429
|
subject: name,
|
|
428
|
-
severity: 'medium',
|
|
430
|
+
severity: signals.length === 0 ? 'medium' : 'high',
|
|
429
431
|
title: `Declares a \`${name}\` script, which runs at install time once allowed`,
|
|
430
432
|
detail: 'This command would run at the user\'s uid as part of `dsh plugin add`, before the user has read a '
|
|
431
433
|
+ 'line of the package. Two things stand between it and execution, and neither is this package\'s doing: '
|
|
432
434
|
+ '`dsh plugin add` forwards its arguments to pnpm verbatim and adds no --ignore-scripts, but pnpm ≥10 '
|
|
433
435
|
+ 'blocks dependency lifecycle scripts by default until the exact package is listed under `allowBuilds` in '
|
|
434
436
|
+ 'the profile\'s pnpm-workspace.yaml — and the harness prints that instruction itself when a build is '
|
|
435
|
-
+ 'blocked (apps/cli/src/plugin.ts). Approving the prompt runs this command.'
|
|
436
|
-
|
|
437
|
+
+ 'blocked (apps/cli/src/plugin.ts). Approving the prompt runs this command.'
|
|
438
|
+
+ (signals.length === 0
|
|
439
|
+
? ''
|
|
440
|
+
: ` The command ${signals.map(signal => signal.meaning).join(', and ')}. A build hook runs something `
|
|
441
|
+
+ 'this package shipped and this one does not, which is the shape 21.2 % of malicious npm packages '
|
|
442
|
+
+ 'take: the whole attack inside `package.json`, with no module to read.'),
|
|
443
|
+
evidence: { file: 'package.json', path: `scripts.${name}`, snippet: snippet(command) },
|
|
437
444
|
}));
|
|
438
445
|
}
|
|
439
446
|
for (const command of manifest.binNames) {
|
package/lib/injection.js
CHANGED
|
@@ -62,6 +62,22 @@ export const INJECTION_RULES = [
|
|
|
62
62
|
pattern: /[\u200b-\u200f\u202a-\u202e\u2060-\u2064\ufeff]|[\u{E0000}-\u{E007F}]/u,
|
|
63
63
|
meaning: 'contains zero-width or bidirectional-control characters, which change what a human reader sees but not what the model reads',
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
// Keyed on a run of four, not on a single selector. One selector is
|
|
67
|
+
// ordinary: U+FE0F and U+FE0E pick the emoji or text presentation of the
|
|
68
|
+
// character before them, and the U+E01xx plane carries the Ideographic
|
|
69
|
+
// Variation Sequences that CJK text uses, so firing on one would fire on
|
|
70
|
+
// every document with an emoji in it. Nothing standardised puts four in a
|
|
71
|
+
// row: a variation selector modifies the single character it follows, so a
|
|
72
|
+
// second one has nothing to modify. GlassWorm's five waves encoded
|
|
73
|
+
// executable JavaScript one byte per selector, which makes any real payload
|
|
74
|
+
// an unbroken run of tens to thousands. Four is far below that and above
|
|
75
|
+
// the doubled selectors that copy-paste through an editor produces.
|
|
76
|
+
id: 'variation-selector-payload',
|
|
77
|
+
pattern: /[\uFE00-\uFE0F\u{E0100}-\u{E01EF}]{4,}/u,
|
|
78
|
+
meaning: 'contains a run of variation selectors, which occupy no width in any editor, terminal or diff '
|
|
79
|
+
+ 'view and can carry an arbitrary encoded payload one byte per selector',
|
|
80
|
+
},
|
|
65
81
|
{
|
|
66
82
|
id: 'hidden-html-instruction',
|
|
67
83
|
pattern: /<!--[^]{0,400}?\b(?:you (?:must|should|are)|instruction|assistant|ignore)\b[^]{0,400}?-->/i,
|
package/lib/inspect.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* `cordis-yaml.ts`, and its result is discarded without being called.
|
|
10
10
|
* @module dsh-plugin-inspector/inspect
|
|
11
11
|
*/
|
|
12
|
+
import { readFileSync } from 'node:fs';
|
|
12
13
|
import { EXPRESSION_CLASSES, PatchParseError, parsePatchDocument, } from "./cordis-yaml.js";
|
|
13
14
|
import { isCordisConfigFile, isModelVisibleText, isSourceFile, normalizePackagePath } from "./files.js";
|
|
14
15
|
import { HARNESS_REFERENCE } from "./knowledge.js";
|
|
@@ -18,8 +19,18 @@ import { loadSource } from "./source.js";
|
|
|
18
19
|
import { runTierA } from "./checks/tier-a.js";
|
|
19
20
|
import { runTierB } from "./checks/tier-b.js";
|
|
20
21
|
import { NON_DEGRADING_CHECKS, runTierC } from "./checks/tier-c.js";
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* This tool's own version, reported in the JSON document, by `--version`, and
|
|
24
|
+
* in the recorded ecosystem measurement.
|
|
25
|
+
*
|
|
26
|
+
* Read from this package's own `package.json` rather than written down a second
|
|
27
|
+
* time. A constant is a copy that only a release checklist keeps honest, and it
|
|
28
|
+
* stopped being honest for two releases: every report claimed `0.1.0` while the
|
|
29
|
+
* published package was `0.2.1`. The manifest sits one directory above this
|
|
30
|
+
* module in the source tree, in `lib/` after a build, and in the published
|
|
31
|
+
* tarball, so the same relative path resolves in all three.
|
|
32
|
+
*/
|
|
33
|
+
export const TOOL_VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
23
34
|
/** This tool's package name, reported in the JSON document. */
|
|
24
35
|
export const TOOL_NAME = 'dsh-plugin-inspector';
|
|
25
36
|
/**
|
package/lib/knowledge.js
CHANGED
|
@@ -291,6 +291,45 @@ export const SKILL_ROOT_CONFIG_KEYS = ['customSkillDirs', 'bundledSkillDir'];
|
|
|
291
291
|
export const INSTALL_LIFECYCLE_SCRIPTS = [
|
|
292
292
|
'preinstall', 'install', 'postinstall', 'prepare', 'prepublish', 'preprepare', 'postprepare',
|
|
293
293
|
];
|
|
294
|
+
/**
|
|
295
|
+
* Command shapes that make an install lifecycle script the attack rather than
|
|
296
|
+
* the build.
|
|
297
|
+
*
|
|
298
|
+
* The head-to-head measurement on 6,420 malicious and 7,288 benign npm packages
|
|
299
|
+
* (ASE 2026) puts 72.21 % of malicious packages on a lifecycle hook and 21.2 %
|
|
300
|
+
* with the whole attack inside `package.json` scripts — no shipped module at
|
|
301
|
+
* all. That second number is what this table is for: it is the case where the
|
|
302
|
+
* command line itself fetches, decodes, or evaluates, and there is nothing else
|
|
303
|
+
* to read.
|
|
304
|
+
*
|
|
305
|
+
* Each pattern is chosen against the measured false-positive side rather than
|
|
306
|
+
* against the idea of a build script. The five packages in the pinned corpus
|
|
307
|
+
* that declare a hook run `tsdown`, `npm run build`, `husky`, and
|
|
308
|
+
* `node scripts/prepare.mjs`; running a shipped file is what a build hook is, so
|
|
309
|
+
* that shape is deliberately not a signal here.
|
|
310
|
+
*/
|
|
311
|
+
export const LIFECYCLE_SIGNALS = [
|
|
312
|
+
{
|
|
313
|
+
id: 'fetches-remote',
|
|
314
|
+
pattern: /\b(?:curl|wget|Invoke-WebRequest|iwr)\b/i,
|
|
315
|
+
meaning: 'fetches a remote resource at install time, so what runs is not what was published',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'pipes-to-shell',
|
|
319
|
+
pattern: /\|\s*(?:sudo\s+)?(?:ba|z|k)?sh\b/,
|
|
320
|
+
meaning: 'pipes its input straight into a shell',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 'evaluates-inline-code',
|
|
324
|
+
pattern: /\b(?:node|deno|bun|ruby|perl)\s+(?:-\S+\s+)*--?e(?:val)?\b|\bpython3?\s+(?:-\S+\s+)*-c\b/,
|
|
325
|
+
meaning: 'evaluates code written on the command line, which no published file records',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: 'decodes-payload',
|
|
329
|
+
pattern: /\bbase64\s+(?:-d|-D|--decode)\b|\batob\s*\(|\bBuffer\.from\([^)]*base64/,
|
|
330
|
+
meaning: 'decodes an encoded payload, which is how a command hides what it runs',
|
|
331
|
+
},
|
|
332
|
+
];
|
|
294
333
|
/** Entry fields the loader never interpolates: a `!!js` node here is inert data. */
|
|
295
334
|
export const STATIC_ENTRY_FIELDS = [
|
|
296
335
|
'id', 'name', 'group', 'inject', 'intercept', 'isolate',
|
package/lib/types/inspect.d.ts
CHANGED
|
@@ -11,8 +11,18 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type RegistryProvenance, type Report, type Severity } from './model.ts';
|
|
13
13
|
import { type PluginSource } from './source.ts';
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* This tool's own version, reported in the JSON document, by `--version`, and
|
|
16
|
+
* in the recorded ecosystem measurement.
|
|
17
|
+
*
|
|
18
|
+
* Read from this package's own `package.json` rather than written down a second
|
|
19
|
+
* time. A constant is a copy that only a release checklist keeps honest, and it
|
|
20
|
+
* stopped being honest for two releases: every report claimed `0.1.0` while the
|
|
21
|
+
* published package was `0.2.1`. The manifest sits one directory above this
|
|
22
|
+
* module in the source tree, in `lib/` after a build, and in the published
|
|
23
|
+
* tarball, so the same relative path resolves in all three.
|
|
24
|
+
*/
|
|
25
|
+
export declare const TOOL_VERSION: string;
|
|
16
26
|
/** This tool's package name, reported in the JSON document. */
|
|
17
27
|
export declare const TOOL_NAME = "dsh-plugin-inspector";
|
|
18
28
|
/**
|
package/lib/types/knowledge.d.ts
CHANGED
|
@@ -110,6 +110,31 @@ export declare const SKILL_ROOT_CONFIG_KEYS: readonly string[];
|
|
|
110
110
|
* only needs one of these to run code before the user has read a line of it.
|
|
111
111
|
*/
|
|
112
112
|
export declare const INSTALL_LIFECYCLE_SCRIPTS: readonly string[];
|
|
113
|
+
/** One thing a lifecycle command can do that a build never needs to. */
|
|
114
|
+
export interface LifecycleSignal {
|
|
115
|
+
readonly id: string;
|
|
116
|
+
readonly pattern: RegExp;
|
|
117
|
+
/** What the match means, phrased for a report. */
|
|
118
|
+
readonly meaning: string;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Command shapes that make an install lifecycle script the attack rather than
|
|
122
|
+
* the build.
|
|
123
|
+
*
|
|
124
|
+
* The head-to-head measurement on 6,420 malicious and 7,288 benign npm packages
|
|
125
|
+
* (ASE 2026) puts 72.21 % of malicious packages on a lifecycle hook and 21.2 %
|
|
126
|
+
* with the whole attack inside `package.json` scripts — no shipped module at
|
|
127
|
+
* all. That second number is what this table is for: it is the case where the
|
|
128
|
+
* command line itself fetches, decodes, or evaluates, and there is nothing else
|
|
129
|
+
* to read.
|
|
130
|
+
*
|
|
131
|
+
* Each pattern is chosen against the measured false-positive side rather than
|
|
132
|
+
* against the idea of a build script. The five packages in the pinned corpus
|
|
133
|
+
* that declare a hook run `tsdown`, `npm run build`, `husky`, and
|
|
134
|
+
* `node scripts/prepare.mjs`; running a shipped file is what a build hook is, so
|
|
135
|
+
* that shape is deliberately not a signal here.
|
|
136
|
+
*/
|
|
137
|
+
export declare const LIFECYCLE_SIGNALS: readonly LifecycleSignal[];
|
|
113
138
|
/** Entry fields the loader never interpolates: a `!!js` node here is inert data. */
|
|
114
139
|
export declare const STATIC_ENTRY_FIELDS: readonly string[];
|
|
115
140
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-inspector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|