@claude-flow/cli 3.56.3 → 3.56.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/.claude/helpers/auto-memory-hook.mjs +13 -8
- package/.claude/helpers/helpers.manifest.json +5 -5
- package/.claude/helpers/hook-handler.cjs +19 -14
- package/.claude/helpers/learning-service.mjs +5 -2
- package/.claude/helpers/session.cjs +48 -12
- package/.claude/helpers/statusline.cjs +14 -4
- package/catalog-manifest.json +2 -2
- package/dist/src/commands/doctor.js +7 -13
- package/dist/src/commands/hooks.js +3 -6
- package/dist/src/commands/init.js +3 -10
- package/dist/src/commands/metaharness.js +9 -14
- package/dist/src/commands/plugins.js +81 -8
- package/dist/src/commands/security-code-patterns.d.ts +40 -0
- package/dist/src/commands/security-code-patterns.js +192 -0
- package/dist/src/commands/security.js +8 -11
- package/dist/src/commands/status.js +8 -10
- package/dist/src/init/executor.d.ts +1 -1
- package/dist/src/init/executor.js +25 -16
- package/dist/src/init/helper-commands.d.ts +18 -0
- package/dist/src/init/helper-commands.js +142 -0
- package/dist/src/init/helpers-generator.js +128 -29
- package/dist/src/init/settings-generator.js +36 -103
- package/dist/src/mcp-tools/metaharness-tools.js +7 -10
- package/dist/src/mcp-tools/task-tools.js +24 -3
- package/dist/src/mcp-tools/testgen-tools.js +8 -12
- package/dist/src/mcp-tools/transfer-tools.js +9 -8
- package/dist/src/mcp-tools/workflow-tools.js +10 -4
- package/dist/src/memory/graph-edge-writer.js +33 -11
- package/dist/src/memory/memory-initializer.js +2 -19
- package/dist/src/memory/shared-sqlite.d.ts +6 -0
- package/dist/src/memory/shared-sqlite.js +35 -7
- package/dist/src/missions/schemas.d.ts +125 -125
- package/dist/src/mods/command-registry/schema.d.ts +199 -199
- package/dist/src/plugins/manager.d.ts +3 -1
- package/dist/src/plugins/manager.js +31 -10
- package/dist/src/plugins/own-install.d.ts +15 -0
- package/dist/src/plugins/own-install.js +51 -0
- package/dist/src/plugins/store/discovery.d.ts +26 -2
- package/dist/src/plugins/store/discovery.js +108 -81
- package/dist/src/plugins/store/index.d.ts +1 -1
- package/dist/src/plugins/store/index.js +1 -1
- package/dist/src/plugins/store/types.d.ts +5 -0
- package/dist/src/plugins/trust-policy.d.ts +13 -0
- package/dist/src/plugins/trust-policy.js +29 -0
- package/dist/src/ruvector/graph-analyzer.js +18 -6
- package/dist/src/transfer/ipfs/client.js +3 -1
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts +1 -0
- package/node_modules/@claude-flow/mcp/dist/resource-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/resource-registry.js +10 -4
- package/node_modules/@claude-flow/mcp/dist/resource-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/sampling.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/sampling.js +13 -6
- package/node_modules/@claude-flow/mcp/dist/sampling.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.js +10 -1
- package/node_modules/@claude-flow/mcp/dist/task-manager.js.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts +1 -0
- package/node_modules/@claude-flow/mcp/dist/tool-registry.d.ts.map +1 -1
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js +17 -11
- package/node_modules/@claude-flow/mcp/dist/tool-registry.js.map +1 -1
- package/node_modules/@claude-flow/mcp/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/index.d.ts +1 -1
- package/node_modules/@claude-flow/security/dist/index.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/index.js +1 -1
- package/node_modules/@claude-flow/security/dist/index.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts +9 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js +15 -2
- package/node_modules/@claude-flow/security/dist/plugins/integrity-verifier.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.json +5 -5
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { output } from '../output.js';
|
|
9
9
|
import { createPluginDiscoveryService, searchPlugins, getPluginSearchSuggestions, getFeaturedPlugins, getOfficialPlugins, } from '../plugins/store/index.js';
|
|
10
10
|
import { getPluginManager } from '../plugins/manager.js';
|
|
11
|
-
import { DEFAULT_PLUGIN_PERMISSIONS } from '../plugins/trust-policy.js';
|
|
11
|
+
import { DEFAULT_PLUGIN_PERMISSIONS, isStrictPluginMode } from '../plugins/trust-policy.js';
|
|
12
12
|
import { getBulkRatings } from '../services/registry-api.js';
|
|
13
13
|
// List subcommand - Now uses IPFS-based registry
|
|
14
14
|
const listCommand = {
|
|
@@ -92,6 +92,9 @@ const listCommand = {
|
|
|
92
92
|
spinner.stop(output.warning(`Signed plugin registry unavailable or unverified; showing the built-in list ` +
|
|
93
93
|
`(${result.registry.totalPlugins} plugins)`));
|
|
94
94
|
}
|
|
95
|
+
else if (result.verified !== true) {
|
|
96
|
+
spinner.stop(output.warning(`Registry discovered but not verified: ${result.registry.totalPlugins} plugins available`));
|
|
97
|
+
}
|
|
95
98
|
else {
|
|
96
99
|
spinner.succeed(`Registry discovered: ${result.registry.totalPlugins} plugins available`);
|
|
97
100
|
}
|
|
@@ -108,6 +111,9 @@ const listCommand = {
|
|
|
108
111
|
if (official) {
|
|
109
112
|
plugins = getOfficialPlugins(result.registry);
|
|
110
113
|
title = 'Official Plugins';
|
|
114
|
+
if (result.verified !== true) {
|
|
115
|
+
output.writeln(output.warning('No plugin is shown as official: only a verified registry can say which are.'));
|
|
116
|
+
}
|
|
111
117
|
}
|
|
112
118
|
else if (featured) {
|
|
113
119
|
plugins = getFeaturedPlugins(result.registry);
|
|
@@ -174,9 +180,13 @@ const listCommand = {
|
|
|
174
180
|
}
|
|
175
181
|
output.writeln(output.dim(`Source: ${result.source}${result.fromCache ? ' (cached)' : ''}`));
|
|
176
182
|
if (result.demo) {
|
|
177
|
-
output.writeln(output.warning('Not from the signed registry: download counts
|
|
183
|
+
output.writeln(output.warning('Not from the signed registry: download counts and ratings are the ' +
|
|
178
184
|
"CLI's built-in defaults, not verified registry data."));
|
|
179
185
|
}
|
|
186
|
+
if (result.verified !== true) {
|
|
187
|
+
output.writeln(output.warning(`Registry not verified (${result.unverifiedReason ?? 'unknown reason'}): every entry is ` +
|
|
188
|
+
'shown as Unverified and installs get no registry trust.'));
|
|
189
|
+
}
|
|
180
190
|
if (result.cid) {
|
|
181
191
|
output.writeln(output.dim(`Registry CID: ${result.cid.slice(0, 30)}...`));
|
|
182
192
|
}
|
|
@@ -208,6 +218,15 @@ const installCommand = {
|
|
|
208
218
|
default: true,
|
|
209
219
|
},
|
|
210
220
|
{ name: 'trust', type: 'boolean', description: 'Register hooks and commands even if the plugin is untrusted or declares elevated permissions', default: false },
|
|
221
|
+
{
|
|
222
|
+
name: 'allow-unverified',
|
|
223
|
+
type: 'boolean',
|
|
224
|
+
description: 'With CLAUDE_FLOW_STRICT_PLUGINS=true, install from npm even though the plugin registry is not verified. ' +
|
|
225
|
+
'The plugin still gets no registry trust. Strict mode gates on the registry, not the package: while the ' +
|
|
226
|
+
'registry is unverified every npm install is refused, listed or not; once it verifies, a package it does ' +
|
|
227
|
+
'not list installs without this flag, but also without registry trust.',
|
|
228
|
+
default: false,
|
|
229
|
+
},
|
|
211
230
|
{ name: 'registry', short: 'r', type: 'string', description: 'Registry to use' },
|
|
212
231
|
],
|
|
213
232
|
examples: [
|
|
@@ -221,6 +240,7 @@ const installCommand = {
|
|
|
221
240
|
const registryName = ctx.flags.registry;
|
|
222
241
|
const verify = ctx.flags.verify !== false;
|
|
223
242
|
const trust = ctx.flags.trust === true;
|
|
243
|
+
const allowUnverified = ctx.flags.allowUnverified === true || ctx.flags['allow-unverified'] === true;
|
|
224
244
|
if (!name) {
|
|
225
245
|
output.printError('Plugin name is required');
|
|
226
246
|
return { success: false, exitCode: 1 };
|
|
@@ -248,6 +268,7 @@ const installCommand = {
|
|
|
248
268
|
}
|
|
249
269
|
let result;
|
|
250
270
|
let plugin;
|
|
271
|
+
const registryWarnings = [];
|
|
251
272
|
if (isLocalPath) {
|
|
252
273
|
// Install from local path
|
|
253
274
|
spinner.setText(`Installing from ${name}...`);
|
|
@@ -261,17 +282,37 @@ const installCommand = {
|
|
|
261
282
|
if (registryResult.success && registryResult.registry) {
|
|
262
283
|
plugin = registryResult.registry.plugins.find(p => p.name === name || p.id === name);
|
|
263
284
|
}
|
|
285
|
+
// Only a verified registry may vouch for a plugin (trust level, permissions, checksum).
|
|
286
|
+
// An unverified or built-in list is just a claim, so its entries install like any npm
|
|
287
|
+
// package: no install scripts and no permission vouching without --trust.
|
|
288
|
+
const registryVerified = registryResult.success && registryResult.verified === true;
|
|
289
|
+
if (!registryVerified) {
|
|
290
|
+
const reason = registryResult.unverifiedReason ?? registryResult.error ?? 'registry not verified';
|
|
291
|
+
if (isStrictPluginMode() && !allowUnverified) {
|
|
292
|
+
spinner.fail(`Refusing to install ${name}: plugin registry is not verified (${reason})`);
|
|
293
|
+
output.writeln();
|
|
294
|
+
output.writeln(output.dim('CLAUDE_FLOW_STRICT_PLUGINS=true requires a verified registry. Pass --allow-unverified to install it ' +
|
|
295
|
+
'from npm anyway, without registry trust.'));
|
|
296
|
+
return { success: false, exitCode: 1 };
|
|
297
|
+
}
|
|
298
|
+
if (plugin) {
|
|
299
|
+
registryWarnings.push(`${name} is listed only in an unverified registry (${reason}); it was installed without registry trust.`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
264
302
|
if (plugin) {
|
|
265
303
|
spinner.setText(`Found ${plugin.displayName} v${plugin.version}`);
|
|
266
304
|
}
|
|
267
|
-
// Install from npm (since IPFS is demo mode)
|
|
268
305
|
spinner.setText(`Installing ${name} from npm...`);
|
|
269
306
|
result = await manager.installFromNpm(name, version !== 'latest' ? version : undefined, {
|
|
270
307
|
verify,
|
|
271
308
|
trust,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
309
|
+
...(registryVerified && plugin
|
|
310
|
+
? {
|
|
311
|
+
expectedChecksum: plugin.checksum,
|
|
312
|
+
registryTrustLevel: plugin.trustLevel,
|
|
313
|
+
registryPermissions: plugin.permissions,
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
275
316
|
});
|
|
276
317
|
}
|
|
277
318
|
if (!result.success) {
|
|
@@ -284,7 +325,7 @@ const installCommand = {
|
|
|
284
325
|
if (result.decision?.verificationSkipped) {
|
|
285
326
|
output.printWarning('Verification skipped (--no-verify): hooks and commands were registered without a trust or checksum check.');
|
|
286
327
|
}
|
|
287
|
-
for (const warning of result.warnings ?? []) {
|
|
328
|
+
for (const warning of [...registryWarnings, ...(result.warnings ?? [])]) {
|
|
288
329
|
output.printWarning(warning);
|
|
289
330
|
}
|
|
290
331
|
const boxContent = [
|
|
@@ -460,6 +501,8 @@ const infoCommand = {
|
|
|
460
501
|
const result = await discovery.discoverRegistry(registryName);
|
|
461
502
|
if (!result.success || !result.registry) {
|
|
462
503
|
spinner.fail('Failed to discover registry');
|
|
504
|
+
if (result.error)
|
|
505
|
+
output.printError(result.error);
|
|
463
506
|
return { success: false, exitCode: 1 };
|
|
464
507
|
}
|
|
465
508
|
const plugin = result.registry.plugins.find(p => p.name === name || p.id === name);
|
|
@@ -491,6 +534,7 @@ const infoCommand = {
|
|
|
491
534
|
{ field: 'Author', value: plugin.author.displayName || plugin.author.id },
|
|
492
535
|
{ field: 'Trust Level', value: plugin.trustLevel },
|
|
493
536
|
{ field: 'Verified', value: plugin.verified ? '✓ Yes' : '✗ No' },
|
|
537
|
+
{ field: 'Registry', value: result.verified === true ? 'verified' : `unverified (${result.unverifiedReason ?? 'unknown reason'})` },
|
|
494
538
|
],
|
|
495
539
|
});
|
|
496
540
|
output.writeln();
|
|
@@ -640,6 +684,21 @@ const upgradeCommand = {
|
|
|
640
684
|
options: [
|
|
641
685
|
{ name: 'name', short: 'n', type: 'string', description: 'Plugin name', required: true },
|
|
642
686
|
{ name: 'version', short: 'v', type: 'string', description: 'Target version (default: latest)' },
|
|
687
|
+
{
|
|
688
|
+
name: 'verify',
|
|
689
|
+
type: 'boolean',
|
|
690
|
+
description: 'Re-apply the install trust policy to the new version (default: true). Its hooks and commands are withheld ' +
|
|
691
|
+
`if it declares trustLevel "untrusted"/"unverified" or permissions beyond the default set (${DEFAULT_PLUGIN_PERMISSIONS.join(', ')}) ` +
|
|
692
|
+
'unless --trust is given. --no-verify skips this.',
|
|
693
|
+
default: true,
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
name: 'trust',
|
|
697
|
+
type: 'boolean',
|
|
698
|
+
description: "Run the new version's install scripts and register its hooks and commands even if it is untrusted or " +
|
|
699
|
+
'declares elevated permissions. Not carried over from the original install.',
|
|
700
|
+
default: false,
|
|
701
|
+
},
|
|
643
702
|
],
|
|
644
703
|
examples: [
|
|
645
704
|
{ command: 'claude-flow plugins upgrade -n @claude-flow/neural', description: 'Upgrade to latest' },
|
|
@@ -648,6 +707,8 @@ const upgradeCommand = {
|
|
|
648
707
|
action: async (ctx) => {
|
|
649
708
|
const name = ctx.flags.name;
|
|
650
709
|
const version = ctx.flags.version;
|
|
710
|
+
const verify = ctx.flags.verify !== false;
|
|
711
|
+
const trust = ctx.flags.trust === true;
|
|
651
712
|
if (!name) {
|
|
652
713
|
output.printError('Plugin name is required');
|
|
653
714
|
return { success: false, exitCode: 1 };
|
|
@@ -666,13 +727,23 @@ const upgradeCommand = {
|
|
|
666
727
|
}
|
|
667
728
|
const oldVersion = existing.version;
|
|
668
729
|
spinner.setText(`Upgrading ${name} from v${oldVersion}...`);
|
|
669
|
-
const result = await manager.upgrade(name, version);
|
|
730
|
+
const result = await manager.upgrade(name, version, { verify, trust });
|
|
670
731
|
if (!result.success) {
|
|
671
732
|
spinner.fail(`Upgrade failed: ${result.error}`);
|
|
672
733
|
return { success: false, exitCode: 1 };
|
|
673
734
|
}
|
|
674
735
|
const plugin = result.plugin;
|
|
675
736
|
spinner.succeed(`Upgraded ${name}: v${oldVersion} -> v${plugin.version}`);
|
|
737
|
+
if (result.decision?.verificationSkipped) {
|
|
738
|
+
output.printWarning('Verification skipped (--no-verify): hooks and commands were registered without a trust check.');
|
|
739
|
+
}
|
|
740
|
+
for (const warning of result.warnings ?? []) {
|
|
741
|
+
output.printWarning(warning);
|
|
742
|
+
}
|
|
743
|
+
if (plugin.withheld) {
|
|
744
|
+
output.printWarning(`Withheld ${plugin.withheld.hooks.length} hook(s) and ${plugin.withheld.commands.length} command(s) ` +
|
|
745
|
+
`from ${plugin.name} because it ${plugin.withheld.reasons.join('; ')}. Upgrade with --trust to register them.`);
|
|
746
|
+
}
|
|
676
747
|
return { success: true, data: plugin };
|
|
677
748
|
}
|
|
678
749
|
catch (error) {
|
|
@@ -716,6 +787,8 @@ const searchCommand = {
|
|
|
716
787
|
const result = await discovery.discoverRegistry(registryName);
|
|
717
788
|
if (!result.success || !result.registry) {
|
|
718
789
|
spinner.fail('Failed to discover registry');
|
|
790
|
+
if (result.error)
|
|
791
|
+
output.printError(result.error);
|
|
719
792
|
return { success: false, exitCode: 1 };
|
|
720
793
|
}
|
|
721
794
|
const searchOptions = {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line patterns for `security scan` phase 3, and what each line is tested on.
|
|
3
|
+
*
|
|
4
|
+
* The patterns are a heuristic. Each is narrowed only where a false positive was seen on a real tree, and only by a
|
|
5
|
+
* shape that cannot carry the risk the rule looks for:
|
|
6
|
+
* - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
|
|
7
|
+
* not a finding;
|
|
8
|
+
* - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
|
|
9
|
+
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and no longer skips
|
|
10
|
+
* execSync as `exec[^S]` did;
|
|
11
|
+
* - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
|
|
12
|
+
* when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
|
|
13
|
+
* stands where SQL takes a value, and it is not inside a quoted SQL string.
|
|
14
|
+
*/
|
|
15
|
+
export type CodePattern = {
|
|
16
|
+
/** Tested on one comment-stripped line. */
|
|
17
|
+
match: (line: string) => boolean;
|
|
18
|
+
type: string;
|
|
19
|
+
severity: 'high' | 'medium';
|
|
20
|
+
desc: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* `$${paramCount}` / `LIMIT $${params.length + 1}`: the pg parameter number where SQL takes a value, not interpolated
|
|
24
|
+
* SQL text. `'$${n}'`, `"$${n}"`, `$${userInput}` and `x $${discount}` keep their interpolation. One left-to-right
|
|
25
|
+
* pass: quotes are counted since the last backtick, so a placeholder inside a quoted SQL string keeps its finding.
|
|
26
|
+
*/
|
|
27
|
+
export declare const withoutPlaceholders: (line: string) => string;
|
|
28
|
+
export declare const CODE_PATTERNS: readonly CodePattern[];
|
|
29
|
+
/**
|
|
30
|
+
* The file's lines with comment text blanked (line count kept, so locations stay right). Strings are respected, so
|
|
31
|
+
* `'http://x'` is not a comment; a template literal may span lines. Regex literals and JSX text are not parsed, so a
|
|
32
|
+
* `//`, `/*` or backtick inside one can be misread. The damage is bounded to one line: a block comment carries to the
|
|
33
|
+
* next line only from a `/*` that starts its line, and only through lines that look like comment lines (they start
|
|
34
|
+
* with `*` or contain the closing `*` + `/`). Any other line ends the carried comment and is scanned as code. One pass,
|
|
35
|
+
* linear in the file size.
|
|
36
|
+
*/
|
|
37
|
+
export declare function codeLinesOf(content: string): string[];
|
|
38
|
+
/** True for an istanbul or Playwright asset, given its parent folder's name and what the parent folder holds. */
|
|
39
|
+
export declare function isGeneratedReportAsset(parentName: string, entryName: string, parentHas: (name: string) => boolean): boolean;
|
|
40
|
+
//# sourceMappingURL=security-code-patterns.d.ts.map
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line patterns for `security scan` phase 3, and what each line is tested on.
|
|
3
|
+
*
|
|
4
|
+
* The patterns are a heuristic. Each is narrowed only where a false positive was seen on a real tree, and only by a
|
|
5
|
+
* shape that cannot carry the risk the rule looks for:
|
|
6
|
+
* - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
|
|
7
|
+
* not a finding;
|
|
8
|
+
* - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
|
|
9
|
+
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and no longer skips
|
|
10
|
+
* execSync as `exec[^S]` did;
|
|
11
|
+
* - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
|
|
12
|
+
* when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
|
|
13
|
+
* stands where SQL takes a value, and it is not inside a quoted SQL string.
|
|
14
|
+
*/
|
|
15
|
+
/** No `g` flag, so `test` keeps no lastIndex between lines. */
|
|
16
|
+
const re = (pattern) => (line) => pattern.test(line);
|
|
17
|
+
// A `.length` counts only on a param-, value- or arg-named array: `req.body.length` is whatever the client sent.
|
|
18
|
+
const COUNTER = /^\s*(?:\d+|[ijkn]|(?:[A-Za-z_$][\w$]*(?:Params?|Placeholders?|Values?|Args?)|params?|placeholders?|values?|args?)\.length|[A-Za-z_$]*(?:[Pp]aram|[Pp]laceholder)[\w$]*)\s*(?:[+-]\s*\d+\s*)?$/;
|
|
19
|
+
// Where a pg parameter stands in SQL: after a comparison, a list or call opener, or a clause keyword.
|
|
20
|
+
const PARAM_BEFORE = /(?:[=<>(,]|\b(?:LIMIT|OFFSET|IN|VALUES|BETWEEN|AND|OR|LIKE|ILIKE|SET|BY|THEN|ELSE|WHEN|IS))\s*$/i;
|
|
21
|
+
const PARAM_AFTER = /^(?:\s|[,);]|::|$|`)/;
|
|
22
|
+
/**
|
|
23
|
+
* `$${paramCount}` / `LIMIT $${params.length + 1}`: the pg parameter number where SQL takes a value, not interpolated
|
|
24
|
+
* SQL text. `'$${n}'`, `"$${n}"`, `$${userInput}` and `x $${discount}` keep their interpolation. One left-to-right
|
|
25
|
+
* pass: quotes are counted since the last backtick, so a placeholder inside a quoted SQL string keeps its finding.
|
|
26
|
+
*/
|
|
27
|
+
export const withoutPlaceholders = (line) => {
|
|
28
|
+
const token = /\\[\s\S]|\$\$\{([^}]{0,80})\}|[`'"]/g;
|
|
29
|
+
let single = 0;
|
|
30
|
+
let double = 0;
|
|
31
|
+
let out = '';
|
|
32
|
+
let kept = 0;
|
|
33
|
+
for (let m = token.exec(line); m !== null; m = token.exec(line)) {
|
|
34
|
+
if (m[0][0] === '\\')
|
|
35
|
+
continue;
|
|
36
|
+
if (m[0] === '`') {
|
|
37
|
+
single = 0;
|
|
38
|
+
double = 0;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (m[0] === "'") {
|
|
42
|
+
single++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (m[0] === '"') {
|
|
46
|
+
double++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const end = m.index + m[0].length;
|
|
50
|
+
const isParam = single % 2 === 0 && double % 2 === 0 && COUNTER.test(m[1])
|
|
51
|
+
&& PARAM_BEFORE.test(line.slice(Math.max(0, m.index - 16), m.index)) && PARAM_AFTER.test(line.slice(end, end + 2));
|
|
52
|
+
if (isParam) {
|
|
53
|
+
out += line.slice(kept, m.index);
|
|
54
|
+
kept = end;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out + line.slice(kept);
|
|
58
|
+
};
|
|
59
|
+
/** The old `/\$\{.*\}.*sql|sql.*\$\{/i`, in linear time: a long minified line made the regex quadratic. */
|
|
60
|
+
const sqlInterpolation = (line) => {
|
|
61
|
+
const lower = line.toLowerCase();
|
|
62
|
+
const open = line.indexOf('${');
|
|
63
|
+
const close = open < 0 ? -1 : line.indexOf('}', open + 2);
|
|
64
|
+
if (close >= 0 && lower.indexOf('sql', close + 1) >= 0)
|
|
65
|
+
return true;
|
|
66
|
+
const sql = lower.indexOf('sql');
|
|
67
|
+
return sql >= 0 && line.indexOf('${', sql + 3) >= 0;
|
|
68
|
+
};
|
|
69
|
+
export const CODE_PATTERNS = [
|
|
70
|
+
{ match: re(/(?<![\w$])eval\s*\(/), type: 'Eval Usage', severity: 'medium', desc: 'eval() can execute arbitrary code' },
|
|
71
|
+
{ match: re(/innerHTML\s*=/), type: 'innerHTML', severity: 'medium', desc: 'XSS risk with innerHTML' },
|
|
72
|
+
{ match: re(/dangerouslySetInnerHTML/), type: 'React XSS', severity: 'medium', desc: 'React XSS risk' },
|
|
73
|
+
{ match: re(/child_process.*exec/), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
|
|
74
|
+
{ match: line => sqlInterpolation(line) && sqlInterpolation(withoutPlaceholders(line)), type: 'SQL Injection', severity: 'high', desc: 'Possible SQL injection' },
|
|
75
|
+
];
|
|
76
|
+
/**
|
|
77
|
+
* The file's lines with comment text blanked (line count kept, so locations stay right). Strings are respected, so
|
|
78
|
+
* `'http://x'` is not a comment; a template literal may span lines. Regex literals and JSX text are not parsed, so a
|
|
79
|
+
* `//`, `/*` or backtick inside one can be misread. The damage is bounded to one line: a block comment carries to the
|
|
80
|
+
* next line only from a `/*` that starts its line, and only through lines that look like comment lines (they start
|
|
81
|
+
* with `*` or contain the closing `*` + `/`). Any other line ends the carried comment and is scanned as code. One pass,
|
|
82
|
+
* linear in the file size.
|
|
83
|
+
*/
|
|
84
|
+
export function codeLinesOf(content) {
|
|
85
|
+
const out = [];
|
|
86
|
+
let inBlock = false;
|
|
87
|
+
let inTemplate = false;
|
|
88
|
+
for (const line of content.split('\n')) {
|
|
89
|
+
if (inBlock && !/^\s*\*|\*\//.test(line))
|
|
90
|
+
inBlock = false;
|
|
91
|
+
const parts = [];
|
|
92
|
+
let tail = '';
|
|
93
|
+
let hasCode = false;
|
|
94
|
+
let quote = inTemplate ? '`' : null;
|
|
95
|
+
let carries = inBlock;
|
|
96
|
+
// Parts plus a 12-character tail, not one growing string: slicing a concatenated string flattens it each time.
|
|
97
|
+
const emit = (text) => {
|
|
98
|
+
parts.push(text);
|
|
99
|
+
tail = (tail + text).slice(-12);
|
|
100
|
+
if (/\S/.test(text))
|
|
101
|
+
hasCode = true;
|
|
102
|
+
};
|
|
103
|
+
for (let i = 0; i < line.length; i++) {
|
|
104
|
+
const ch = line[i];
|
|
105
|
+
const next = line[i + 1];
|
|
106
|
+
if (inBlock) {
|
|
107
|
+
if (ch === '*' && next === '/') {
|
|
108
|
+
inBlock = false;
|
|
109
|
+
i++;
|
|
110
|
+
emit(' ');
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (ch === '\\') {
|
|
115
|
+
emit(ch + (next ?? ''));
|
|
116
|
+
i++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (quote !== null) {
|
|
120
|
+
if (ch === quote)
|
|
121
|
+
quote = null;
|
|
122
|
+
emit(ch);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// `://` is a URL scheme, not a comment.
|
|
126
|
+
if (ch === '/' && next === '/' && line[i - 1] !== ':')
|
|
127
|
+
break;
|
|
128
|
+
if (ch === '/' && next === '*') {
|
|
129
|
+
inBlock = true;
|
|
130
|
+
carries = !hasCode;
|
|
131
|
+
i++;
|
|
132
|
+
emit(' ');
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === '/' && REGEX_MAY_START.test(tail)) {
|
|
136
|
+
const end = regexEnd(line, i);
|
|
137
|
+
if (end > i) {
|
|
138
|
+
emit(line.slice(i, end + 1));
|
|
139
|
+
i = end;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (ch === '"' || ch === "'" || ch === '`')
|
|
144
|
+
quote = ch;
|
|
145
|
+
emit(ch);
|
|
146
|
+
}
|
|
147
|
+
if (inBlock && !carries)
|
|
148
|
+
inBlock = false;
|
|
149
|
+
inTemplate = quote === '`';
|
|
150
|
+
out.push(parts.join(''));
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
/** Code so far ends where an expression starts, so a `/` there opens a regex literal, not a division. */
|
|
155
|
+
const REGEX_MAY_START = /(?:^|[(,=:[!&|?{};+\-*%<>~^]|\b(?:return|typeof|case|do|else|in|of|void|yield|await))\s*$/;
|
|
156
|
+
/**
|
|
157
|
+
* The index of the `/` closing a regex literal that opens at `start`, or -1 when the line has none (then it was a
|
|
158
|
+
* division). Its text is kept as code, so misreading a regex can only keep comment text, never blank code.
|
|
159
|
+
*/
|
|
160
|
+
function regexEnd(line, start) {
|
|
161
|
+
let inClass = false;
|
|
162
|
+
// Bounded, so a line of `(/` cannot make the stripper quadratic; a longer regex is read as code, which is safe.
|
|
163
|
+
for (let i = start + 1; i < Math.min(line.length, start + 200); i++) {
|
|
164
|
+
const ch = line[i];
|
|
165
|
+
if (ch === '\\') {
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (ch === '[')
|
|
170
|
+
inClass = true;
|
|
171
|
+
else if (ch === ']')
|
|
172
|
+
inClass = false;
|
|
173
|
+
else if (ch === '/' && !inClass)
|
|
174
|
+
return i;
|
|
175
|
+
}
|
|
176
|
+
return -1;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Generated report assets, named by what the report writes, inside a folder its marker file identifies. Only those
|
|
180
|
+
* entries are skipped: other files in a folder that merely shares the name are still scanned.
|
|
181
|
+
*/
|
|
182
|
+
const REPORT_ASSETS = new Map([
|
|
183
|
+
['coverage', { markers: ['coverage-final.json', 'lcov.info', 'clover.xml'], assets: new Set(['prettify.js', 'sorter.js', 'block-navigation.js', 'lcov-report']) }],
|
|
184
|
+
['playwright-report', { markers: ['index.html'], assets: new Set(['trace', 'data']) }],
|
|
185
|
+
]);
|
|
186
|
+
/** True for an istanbul or Playwright asset, given its parent folder's name and what the parent folder holds. */
|
|
187
|
+
export function isGeneratedReportAsset(parentName, entryName, parentHas) {
|
|
188
|
+
// A Map, so a folder named `constructor` or `__proto__` is not looked up on Object.prototype.
|
|
189
|
+
const report = REPORT_ASSETS.get(parentName);
|
|
190
|
+
return report !== undefined && report.assets.has(entryName) && report.markers.some(parentHas);
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=security-code-patterns.js.map
|
|
@@ -9,6 +9,7 @@ import { execSync } from 'node:child_process';
|
|
|
9
9
|
import { existsSync, statSync } from 'node:fs';
|
|
10
10
|
import { resolve as resolvePath } from 'node:path';
|
|
11
11
|
import { createBuiltinAIDefence } from '../security/builtin-aidefence.js';
|
|
12
|
+
import { CODE_PATTERNS, codeLinesOf, isGeneratedReportAsset } from './security-code-patterns.js';
|
|
12
13
|
// Accepted values for `security scan`'s enum flags — the single source of truth
|
|
13
14
|
// for both validation and the traversal-depth maps below.
|
|
14
15
|
const SCAN_DEPTHS = ['quick', 'standard', 'deep'];
|
|
@@ -269,13 +270,7 @@ const scanCommand = {
|
|
|
269
270
|
// Phase 3: Check for common security issues in code
|
|
270
271
|
if ((scanType === 'all' || scanType === 'code') && depth !== 'quick') {
|
|
271
272
|
spinner.setText('Analyzing code patterns...');
|
|
272
|
-
|
|
273
|
-
{ pattern: /eval\s*\(/g, type: 'Eval Usage', severity: 'medium', desc: 'eval() can execute arbitrary code' },
|
|
274
|
-
{ pattern: /innerHTML\s*=/g, type: 'innerHTML', severity: 'medium', desc: 'XSS risk with innerHTML' },
|
|
275
|
-
{ pattern: /dangerouslySetInnerHTML/g, type: 'React XSS', severity: 'medium', desc: 'React XSS risk' },
|
|
276
|
-
{ pattern: /child_process.*exec[^S]/g, type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
|
|
277
|
-
{ pattern: /\$\{.*\}.*sql|sql.*\$\{/gi, type: 'SQL Injection', severity: 'high', desc: 'Possible SQL injection' },
|
|
278
|
-
];
|
|
273
|
+
// The patterns, and the comment stripping they are tested after, live in security-code-patterns.ts.
|
|
279
274
|
const scanCodeDir = (dir, depthLimit) => {
|
|
280
275
|
// Positive-test rather than `<= 0`: undefined and NaN both fail this,
|
|
281
276
|
// so a bad budget stops the recursion instead of disabling the limiter.
|
|
@@ -287,16 +282,19 @@ const scanCommand = {
|
|
|
287
282
|
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'dist')
|
|
288
283
|
continue;
|
|
289
284
|
const fullPath = path.join(dir, entry.name);
|
|
285
|
+
// Coverage and Playwright report assets bundle third-party code (prettify, CodeMirror, React), not the project's.
|
|
286
|
+
if (isGeneratedReportAsset(path.basename(dir), entry.name, name => fs.existsSync(path.join(dir, name))))
|
|
287
|
+
continue;
|
|
290
288
|
if (entry.isDirectory()) {
|
|
291
289
|
scanCodeDir(fullPath, depthLimit - 1);
|
|
292
290
|
}
|
|
293
291
|
else if (entry.isFile() && /\.(ts|js|tsx|jsx)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
294
292
|
try {
|
|
295
293
|
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
296
|
-
const lines = content
|
|
294
|
+
const lines = codeLinesOf(content);
|
|
297
295
|
for (let i = 0; i < lines.length; i++) {
|
|
298
|
-
for (const {
|
|
299
|
-
if (
|
|
296
|
+
for (const { match, type, severity, desc } of CODE_PATTERNS) {
|
|
297
|
+
if (match(lines[i])) {
|
|
300
298
|
if (severity === 'high')
|
|
301
299
|
highCount++;
|
|
302
300
|
else
|
|
@@ -307,7 +305,6 @@ const scanCommand = {
|
|
|
307
305
|
location: `${path.relative(target, fullPath)}:${i + 1}`,
|
|
308
306
|
description: desc,
|
|
309
307
|
});
|
|
310
|
-
pattern.lastIndex = 0;
|
|
311
308
|
}
|
|
312
309
|
}
|
|
313
310
|
}
|
|
@@ -415,7 +415,7 @@ const agentsCommand = {
|
|
|
415
415
|
description: 'Show detailed agent status',
|
|
416
416
|
action: async (ctx) => {
|
|
417
417
|
try {
|
|
418
|
-
const result = await callMCPTool('agent_list', {
|
|
418
|
+
const result = await callMCPTool('agent_list', {});
|
|
419
419
|
if (ctx.flags.format === 'json') {
|
|
420
420
|
output.printJson(result);
|
|
421
421
|
return { success: true, data: result };
|
|
@@ -424,7 +424,7 @@ const agentsCommand = {
|
|
|
424
424
|
output.writeln(output.bold('Agent Status'));
|
|
425
425
|
output.writeln();
|
|
426
426
|
if (result.agents.length === 0) {
|
|
427
|
-
output.printInfo('No agents
|
|
427
|
+
output.printInfo('No agents registered');
|
|
428
428
|
return { success: true, data: result };
|
|
429
429
|
}
|
|
430
430
|
output.printTable({
|
|
@@ -432,17 +432,15 @@ const agentsCommand = {
|
|
|
432
432
|
{ key: 'id', header: 'ID', width: 20 },
|
|
433
433
|
{ key: 'type', header: 'Type', width: 12 },
|
|
434
434
|
{ key: 'status', header: 'Status', width: 10 },
|
|
435
|
-
{ key: '
|
|
436
|
-
{ key: '
|
|
437
|
-
{ key: 'success', header: 'Success', width: 8 }
|
|
435
|
+
{ key: 'tasks', header: 'Tasks', width: 8 },
|
|
436
|
+
{ key: 'health', header: 'Health', width: 8 }
|
|
438
437
|
],
|
|
439
438
|
data: result.agents.map(a => ({
|
|
440
|
-
id: a.
|
|
441
|
-
type: a.
|
|
439
|
+
id: a.agentId,
|
|
440
|
+
type: a.agentType,
|
|
442
441
|
status: formatHealth(a.status),
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
success: `${(a.metrics.successRate * 100).toFixed(0)}%`
|
|
442
|
+
tasks: a.taskCount,
|
|
443
|
+
health: a.health
|
|
446
444
|
}))
|
|
447
445
|
});
|
|
448
446
|
return { success: true, data: result };
|
|
@@ -35,7 +35,7 @@ export interface UpgradeResult {
|
|
|
35
35
|
* Preserves user customizations while adding new features like Agent Teams
|
|
36
36
|
* Uses platform-specific commands for Mac, Linux, and Windows
|
|
37
37
|
*/
|
|
38
|
-
export declare function mergeSettingsForUpgrade(existing: Record<string, unknown
|
|
38
|
+
export declare function mergeSettingsForUpgrade(existing: Record<string, unknown>, targetDir?: string): {
|
|
39
39
|
merged: Record<string, unknown>;
|
|
40
40
|
warnings: string[];
|
|
41
41
|
};
|