opmsec 0.1.0 → 0.1.4

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.
Files changed (152) hide show
  1. package/.env.example +23 -13
  2. package/.husky/pre-commit +1 -0
  3. package/README.md +256 -173
  4. package/bun.lock +4 -4
  5. package/docs/architecture/agents.mdx +77 -0
  6. package/docs/architecture/benchmarks.mdx +65 -0
  7. package/docs/architecture/overview.mdx +58 -0
  8. package/docs/architecture/scanner.mdx +53 -0
  9. package/docs/cli/audit.mdx +35 -0
  10. package/docs/cli/check.mdx +44 -0
  11. package/docs/cli/fix.mdx +49 -0
  12. package/docs/cli/info.mdx +44 -0
  13. package/docs/cli/install.mdx +71 -0
  14. package/docs/cli/push.mdx +99 -0
  15. package/docs/cli/register-agent.mdx +80 -0
  16. package/docs/cli/view.mdx +52 -0
  17. package/docs/concepts/multi-agent-consensus.mdx +58 -0
  18. package/docs/concepts/on-chain-registry.mdx +74 -0
  19. package/docs/concepts/security-model.mdx +76 -0
  20. package/docs/concepts/zk-agent-verification.mdx +82 -0
  21. package/docs/configuration.mdx +82 -0
  22. package/docs/contract/deployment.mdx +57 -0
  23. package/docs/contract/events.mdx +115 -0
  24. package/docs/contract/functions.mdx +220 -0
  25. package/docs/contract/overview.mdx +58 -0
  26. package/docs/favicon.svg +5 -0
  27. package/docs/introduction.mdx +43 -0
  28. package/docs/logo/dark.svg +5 -0
  29. package/docs/logo/light.svg +5 -0
  30. package/docs/mint.json +106 -0
  31. package/docs/quickstart.mdx +133 -0
  32. package/package.json +7 -6
  33. package/packages/cli/src/commands/author-view.tsx +9 -1
  34. package/packages/cli/src/commands/check.tsx +318 -0
  35. package/packages/cli/src/commands/fix.tsx +294 -0
  36. package/packages/cli/src/commands/install.tsx +501 -47
  37. package/packages/cli/src/commands/push.tsx +53 -22
  38. package/packages/cli/src/commands/register-agent.tsx +227 -0
  39. package/packages/cli/src/components/AgentScores.tsx +20 -6
  40. package/packages/cli/src/components/Hyperlink.tsx +30 -0
  41. package/packages/cli/src/components/ScanReport.tsx +3 -2
  42. package/packages/cli/src/index.tsx +44 -6
  43. package/packages/cli/src/services/avatar.ts +43 -6
  44. package/packages/cli/src/services/chainpatrol.ts +20 -17
  45. package/packages/cli/src/services/contract.ts +41 -8
  46. package/packages/cli/src/services/ens.ts +3 -5
  47. package/packages/cli/src/services/fileverse.ts +12 -13
  48. package/packages/cli/src/services/typosquat.ts +166 -0
  49. package/packages/cli/src/services/version.ts +156 -5
  50. package/packages/contracts/circuits/accuracy_verifier.circom +101 -0
  51. package/packages/contracts/contracts/OPMRegistry.sol +63 -0
  52. package/packages/contracts/scripts/deploy.ts +22 -3
  53. package/packages/core/src/abi.ts +221 -0
  54. package/packages/core/src/benchmarks.ts +450 -0
  55. package/packages/core/src/constants.ts +20 -0
  56. package/packages/core/src/index.ts +2 -0
  57. package/packages/core/src/model-rankings.ts +115 -0
  58. package/packages/core/src/prompt.ts +58 -0
  59. package/packages/core/src/types.ts +41 -0
  60. package/packages/core/src/utils.ts +142 -3
  61. package/packages/scanner/src/agents/base-agent.ts +13 -3
  62. package/packages/scanner/src/index.ts +5 -2
  63. package/packages/scanner/src/queue/memory-queue.ts +8 -3
  64. package/packages/scanner/src/services/benchmark-runner.ts +114 -0
  65. package/packages/scanner/src/services/contract-writer.ts +2 -3
  66. package/packages/scanner/src/services/fileverse.ts +26 -7
  67. package/packages/scanner/src/services/openrouter.ts +61 -4
  68. package/packages/scanner/src/services/report-formatter.ts +122 -3
  69. package/packages/scanner/src/services/zk-verifier.ts +118 -0
  70. package/packages/web/.next/BUILD_ID +1 -0
  71. package/packages/web/.next/app-build-manifest.json +26 -0
  72. package/packages/web/.next/app-path-routes-manifest.json +4 -0
  73. package/packages/web/.next/build-manifest.json +33 -0
  74. package/packages/web/.next/diagnostics/build-diagnostics.json +6 -0
  75. package/packages/web/.next/diagnostics/framework.json +1 -0
  76. package/packages/web/.next/export-marker.json +6 -0
  77. package/packages/web/.next/images-manifest.json +58 -0
  78. package/packages/web/.next/next-minimal-server.js.nft.json +1 -0
  79. package/packages/web/.next/next-server.js.nft.json +1 -0
  80. package/packages/web/.next/package.json +1 -0
  81. package/packages/web/.next/prerender-manifest.json +61 -0
  82. package/packages/web/.next/react-loadable-manifest.json +1 -0
  83. package/packages/web/.next/required-server-files.json +320 -0
  84. package/packages/web/.next/routes-manifest.json +53 -0
  85. package/packages/web/.next/server/app/_not-found/page.js +2 -0
  86. package/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -0
  87. package/packages/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  88. package/packages/web/.next/server/app/_not-found.html +1 -0
  89. package/packages/web/.next/server/app/_not-found.meta +8 -0
  90. package/packages/web/.next/server/app/_not-found.rsc +16 -0
  91. package/packages/web/.next/server/app/index.html +1 -0
  92. package/packages/web/.next/server/app/index.meta +7 -0
  93. package/packages/web/.next/server/app/index.rsc +20 -0
  94. package/packages/web/.next/server/app/page.js +2 -0
  95. package/packages/web/.next/server/app/page.js.nft.json +1 -0
  96. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
  97. package/packages/web/.next/server/app-paths-manifest.json +4 -0
  98. package/packages/web/.next/server/chunks/611.js +6 -0
  99. package/packages/web/.next/server/chunks/778.js +30 -0
  100. package/packages/web/.next/server/functions-config-manifest.json +4 -0
  101. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
  102. package/packages/web/.next/server/middleware-build-manifest.js +1 -0
  103. package/packages/web/.next/server/middleware-manifest.json +6 -0
  104. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  105. package/packages/web/.next/server/next-font-manifest.js +1 -0
  106. package/packages/web/.next/server/next-font-manifest.json +1 -0
  107. package/packages/web/.next/server/pages/404.html +1 -0
  108. package/packages/web/.next/server/pages/500.html +1 -0
  109. package/packages/web/.next/server/pages/_app.js +1 -0
  110. package/packages/web/.next/server/pages/_app.js.nft.json +1 -0
  111. package/packages/web/.next/server/pages/_document.js +1 -0
  112. package/packages/web/.next/server/pages/_document.js.nft.json +1 -0
  113. package/packages/web/.next/server/pages/_error.js +19 -0
  114. package/packages/web/.next/server/pages/_error.js.nft.json +1 -0
  115. package/packages/web/.next/server/pages-manifest.json +6 -0
  116. package/packages/web/.next/server/server-reference-manifest.js +1 -0
  117. package/packages/web/.next/server/server-reference-manifest.json +1 -0
  118. package/packages/web/.next/server/webpack-runtime.js +1 -0
  119. package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_buildManifest.js +1 -0
  120. package/packages/web/.next/static/2XIFCTTKVZwN_RsNE-Rrr/_ssgManifest.js +1 -0
  121. package/packages/web/.next/static/chunks/255-0dc49b7a6e8e5c05.js +1 -0
  122. package/packages/web/.next/static/chunks/4bd1b696-382748cc942d8a14.js +1 -0
  123. package/packages/web/.next/static/chunks/app/_not-found/page-0da542be7eb33a64.js +1 -0
  124. package/packages/web/.next/static/chunks/app/layout-28a489fb4398663f.js +1 -0
  125. package/packages/web/.next/static/chunks/app/page-e58ccdb78625bce6.js +1 -0
  126. package/packages/web/.next/static/chunks/framework-ac73abd125e371fe.js +1 -0
  127. package/packages/web/.next/static/chunks/main-app-dd261207182e5a23.js +1 -0
  128. package/packages/web/.next/static/chunks/main-ee293fa6aa18bdd1.js +1 -0
  129. package/packages/web/.next/static/chunks/pages/_app-7d307437aca18ad4.js +1 -0
  130. package/packages/web/.next/static/chunks/pages/_error-cb2a52f75f2162e2.js +1 -0
  131. package/packages/web/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  132. package/packages/web/.next/static/chunks/webpack-e1ae44446e7f7355.js +1 -0
  133. package/packages/web/.next/static/css/21d69157e271f2ab.css +3 -0
  134. package/packages/web/.next/trace +2 -0
  135. package/packages/web/.next/types/app/layout.ts +84 -0
  136. package/packages/web/.next/types/app/page.ts +84 -0
  137. package/packages/web/.next/types/cache-life.d.ts +141 -0
  138. package/packages/web/.next/types/package.json +1 -0
  139. package/packages/web/.next/types/routes.d.ts +57 -0
  140. package/packages/web/.next/types/validator.ts +61 -0
  141. package/packages/web/app/globals.css +75 -0
  142. package/packages/web/app/layout.tsx +26 -0
  143. package/packages/web/app/page.tsx +361 -0
  144. package/packages/web/bun.lock +300 -0
  145. package/packages/web/next-env.d.ts +6 -0
  146. package/packages/web/next.config.ts +5 -0
  147. package/packages/web/package.json +26 -0
  148. package/packages/web/postcss.config.mjs +8 -0
  149. package/packages/web/public/favicon.svg +5 -0
  150. package/packages/web/public/logo.svg +7 -0
  151. package/packages/web/tailwind.config.ts +48 -0
  152. package/packages/web/tsconfig.json +21 -0
@@ -10,7 +10,8 @@ import { verifyChecksum } from '../services/signature';
10
10
  import { resolveENSName } from '../services/ens';
11
11
  import { checkPackageWithChainPatrol } from '../services/chainpatrol';
12
12
  import { queryOSV, getOSVSeverity, getFixedVersion, type OSVVulnerability } from '../services/osv';
13
- import { resolveVersion } from '../services/version';
13
+ import { resolveVersion, findSafeVersion, isENSVersion, type ResolvedVersion } from '../services/version';
14
+ import { resolveAddress } from '../services/ens';
14
15
  import { execSync } from 'child_process';
15
16
  import * as fs from 'fs';
16
17
  import * as path from 'path';
@@ -19,6 +20,7 @@ type StepStatus = 'pending' | 'running' | 'done' | 'error' | 'skip';
19
20
 
20
21
  interface Steps {
21
22
  resolve: StepStatus;
23
+ ens: StepStatus;
22
24
  cve: StepStatus;
23
25
  onchain: StepStatus;
24
26
  signature: StepStatus;
@@ -31,6 +33,7 @@ interface SecurityResult {
31
33
  name: string;
32
34
  version: string;
33
35
  resolvedVersion: string;
36
+ resolved?: ResolvedVersion;
34
37
  cves: OSVVulnerability[];
35
38
  info?: OnChainPackageInfo;
36
39
  signatureValid?: boolean;
@@ -40,6 +43,9 @@ interface SecurityResult {
40
43
  warning: boolean;
41
44
  blockReason?: string;
42
45
  safestVersion?: string;
46
+ autoBumped?: boolean;
47
+ autoBumpedFrom?: string;
48
+ autoBumpReason?: string;
43
49
  }
44
50
 
45
51
  interface InstallCommandProps {
@@ -67,12 +73,25 @@ function sevColor(sev: string): string {
67
73
  }
68
74
 
69
75
  export function InstallCommand({ packageName, version }: InstallCommandProps) {
76
+ if (packageName) {
77
+ return <SingleInstall packageName={packageName} version={version} />;
78
+ }
79
+ return <BulkInstall />;
80
+ }
81
+
82
+ // ─── Single package install with full security pipeline ───────────────────────
83
+
84
+ function SingleInstall({ packageName, version }: { packageName: string; version?: string }) {
85
+ const isEns = version ? isENSVersion(version) : false;
86
+
70
87
  const [steps, setSteps] = useState<Steps>({
71
- resolve: 'pending', cve: 'pending', onchain: 'pending',
88
+ resolve: 'pending', ens: isEns ? 'pending' : 'skip',
89
+ cve: 'pending', onchain: 'pending',
72
90
  signature: 'pending', chainpatrol: 'pending', report: 'pending',
73
91
  install: 'pending',
74
92
  });
75
93
  const [result, setResult] = useState<SecurityResult | null>(null);
94
+ const [ensDetail, setEnsDetail] = useState<string | undefined>(undefined);
76
95
  const [error, setError] = useState<string | null>(null);
77
96
  const [done, setDone] = useState(false);
78
97
 
@@ -84,26 +103,43 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
84
103
  }, []);
85
104
 
86
105
  async function run() {
87
- const packages = getTargetPackages(packageName, version);
88
- if (packages.length === 0) {
89
- setError('No packages to install');
90
- return;
91
- }
92
-
93
- const pkg = packages[0];
94
106
  const r: SecurityResult = {
95
- name: pkg.name, version: pkg.version,
96
- resolvedVersion: pkg.version, cves: [],
107
+ name: packageName, version: version || 'latest',
108
+ resolvedVersion: version || 'latest', cves: [],
97
109
  blocked: false, warning: false,
98
110
  };
99
111
 
100
- update('resolve', 'running');
101
- r.resolvedVersion = await resolveVersion(pkg.name, pkg.version);
102
- setResult({ ...r });
103
- update('resolve', 'done');
112
+ // ── Resolve version (+ ENS if applicable) ──
113
+ if (isEns) {
114
+ update('resolve', 'done');
115
+ update('ens', 'running');
116
+ try {
117
+ const resolved = await resolveVersion(packageName, r.version, (msg) => setEnsDetail(msg));
118
+ r.resolved = resolved;
119
+ r.resolvedVersion = resolved.version;
120
+ r.ensName = resolved.ensName;
121
+ setResult({ ...r });
122
+ setEnsDetail(`${resolved.ensName} → v${resolved.version} (${resolved.reason})`);
123
+ update('ens', 'done');
124
+ } catch (err: any) {
125
+ setEnsDetail(err?.message || 'ENS resolution failed');
126
+ update('ens', 'error');
127
+ setError(err?.message || 'ENS resolution failed');
128
+ update('install', 'error');
129
+ return;
130
+ }
131
+ } else {
132
+ update('resolve', 'running');
133
+ const resolved = await resolveVersion(packageName, r.version);
134
+ r.resolved = resolved;
135
+ r.resolvedVersion = resolved.version;
136
+ setResult({ ...r });
137
+ update('resolve', 'done');
138
+ }
104
139
 
140
+ // ── CVE check ──
105
141
  update('cve', 'running');
106
- r.cves = await queryOSV(pkg.name, r.resolvedVersion);
142
+ r.cves = await queryOSV(packageName, r.resolvedVersion);
107
143
  const cveCounts = categorizeCVEs(r.cves);
108
144
  if (cveCounts.critical > 0) {
109
145
  r.blocked = true;
@@ -114,30 +150,53 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
114
150
  setResult({ ...r });
115
151
  update('cve', 'done');
116
152
 
153
+ // ── On-chain registry lookup ──
117
154
  update('onchain', 'running');
118
155
  try {
119
- const info = await getPackageInfo(pkg.name, r.resolvedVersion);
156
+ const info = await getPackageInfo(packageName, r.resolvedVersion);
120
157
  r.info = info;
121
-
122
158
  if (info.exists) {
123
159
  if (info.aggregateScore >= HIGH_RISK_THRESHOLD) {
124
160
  r.blocked = true;
125
161
  r.blockReason = (r.blockReason ? r.blockReason + '; ' : '') + `risk score ${info.aggregateScore}/100`;
126
162
  } else if (info.aggregateScore >= MEDIUM_RISK_THRESHOLD) {
127
163
  r.warning = true;
128
- r.safestVersion = await getSafestVersion(pkg.name).catch(() => undefined);
164
+ r.safestVersion = await getSafestVersion(packageName).catch(() => undefined);
129
165
  }
130
166
  }
131
167
  } catch { /* not in registry */ }
132
168
  setResult({ ...r });
133
169
  update('onchain', 'done');
134
170
 
171
+ // ── Auto-bump: try to find a safe version instead of blocking ──
172
+ if (r.blocked && !isEns) {
173
+ const safe = await findSafeVersion(packageName, r.resolvedVersion, r.cves);
174
+ if (safe) {
175
+ r.autoBumped = true;
176
+ r.autoBumpedFrom = r.resolvedVersion;
177
+ r.autoBumpReason = safe.reason;
178
+ r.resolvedVersion = safe.version;
179
+ r.blocked = false;
180
+ r.blockReason = undefined;
181
+ r.warning = true;
182
+
183
+ r.cves = await queryOSV(packageName, safe.version).catch(() => []);
184
+ try {
185
+ const newInfo = await getPackageInfo(packageName, safe.version);
186
+ if (newInfo.exists) r.info = newInfo;
187
+ } catch { /* keep existing */ }
188
+
189
+ setResult({ ...r });
190
+ }
191
+ }
192
+
193
+ // ── Signature verification ──
135
194
  if (r.info?.exists) {
136
195
  update('signature', 'running');
137
196
  r.signatureValid = r.info.signature !== '0x'
138
197
  ? verifyChecksum(r.info.checksum, r.info.signature, r.info.author)
139
198
  : false;
140
- if (r.info.author) {
199
+ if (r.info.author && !r.ensName) {
141
200
  r.ensName = await resolveENSName(r.info.author).catch(() => null) || undefined;
142
201
  }
143
202
  setResult({ ...r });
@@ -146,9 +205,10 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
146
205
  update('signature', 'skip');
147
206
  }
148
207
 
208
+ // ── ChainPatrol check ──
149
209
  if (!r.info?.exists) {
150
210
  update('chainpatrol', 'running');
151
- const cp = await checkPackageWithChainPatrol(pkg.name).catch(() => null);
211
+ const cp = await checkPackageWithChainPatrol(packageName).catch(() => null);
152
212
  r.chainPatrolStatus = cp?.status;
153
213
  if (cp?.status === 'BLOCKED') {
154
214
  r.blocked = true;
@@ -160,12 +220,14 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
160
220
  update('chainpatrol', 'skip');
161
221
  }
162
222
 
223
+ // ── Fileverse report ──
163
224
  if (r.info?.reportURI && !r.info.reportURI.startsWith('local://')) {
164
225
  update('report', 'done');
165
226
  } else {
166
227
  update('report', 'skip');
167
228
  }
168
229
 
230
+ // ── Block or install ──
169
231
  if (r.blocked) {
170
232
  setError(`Blocked: ${r.blockReason || 'security risk detected'}`);
171
233
  update('install', 'error');
@@ -174,10 +236,8 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
174
236
 
175
237
  update('install', 'running');
176
238
  try {
177
- const installTarget = packageName
178
- ? `${packageName}${version ? `@${version}` : ''}`
179
- : '';
180
- execSync(`npm install ${installTarget}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
239
+ const target = `${packageName}@${r.resolvedVersion}`;
240
+ execSync(`npm install ${target}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
181
241
  } catch { /* non-fatal */ }
182
242
  update('install', 'done');
183
243
  setDone(true);
@@ -192,12 +252,43 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
192
252
  return (
193
253
  <Box flexDirection="column">
194
254
  <Header subtitle="install" />
195
- {result && <Text color="white" bold> {result.name}@{result.resolvedVersion}</Text>}
255
+ {result && (
256
+ <Box>
257
+ <Text color="white" bold> {result.name}@{result.resolvedVersion}</Text>
258
+ {result.ensName && result.resolved?.source === 'ens' && (
259
+ <Text color="cyan"> via {result.ensName}</Text>
260
+ )}
261
+ {result.autoBumped && (
262
+ <Text color="yellow"> (bumped from {result.autoBumpedFrom})</Text>
263
+ )}
264
+ </Box>
265
+ )}
196
266
  <Text> </Text>
197
267
 
198
268
  <StatusLine label="Resolve version" status={steps.resolve}
199
269
  detail={steps.resolve === 'done' ? result?.resolvedVersion : undefined} />
200
270
 
271
+ {isEns && (
272
+ <StatusLine label="Resolve ENS author" status={steps.ens} detail={ensDetail} />
273
+ )}
274
+ {steps.ens === 'done' && result?.resolved?.source === 'ens' && (
275
+ <Box flexDirection="column" marginLeft={4}>
276
+ <Box>
277
+ <Text color="gray">Author: </Text>
278
+ <Text color="green">{result.ensName}</Text>
279
+ {result.resolved.authorAddress && (
280
+ <Text color="gray"> ({truncateAddress(result.resolved.authorAddress)})</Text>
281
+ )}
282
+ <Text color="green"> ✓ on-chain</Text>
283
+ </Box>
284
+ <Box>
285
+ <Text color="gray">Version: </Text>
286
+ <Text color="cyan">{result.resolvedVersion}</Text>
287
+ <Text color="gray"> (safest on-chain version)</Text>
288
+ </Box>
289
+ </Box>
290
+ )}
291
+
201
292
  <StatusLine label="Query CVE database (OSV)" status={steps.cve}
202
293
  detail={steps.cve === 'done'
203
294
  ? (result?.cves.length
@@ -232,6 +323,22 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
232
323
  </Box>
233
324
  )}
234
325
 
326
+ {result?.autoBumped && (
327
+ <Box flexDirection="column" marginLeft={4} marginTop={0}>
328
+ <Box>
329
+ <Text color="yellow">↑ Auto-bumped: </Text>
330
+ <Text color="red">{result.autoBumpedFrom}</Text>
331
+ <Text color="yellow"> → </Text>
332
+ <Text color="green" bold>{result.resolvedVersion}</Text>
333
+ </Box>
334
+ {result.autoBumpReason && (
335
+ <Box marginLeft={2}>
336
+ <Text color="gray">{result.autoBumpReason}</Text>
337
+ </Box>
338
+ )}
339
+ </Box>
340
+ )}
341
+
235
342
  <StatusLine label="On-chain registry lookup" status={steps.onchain}
236
343
  detail={steps.onchain === 'done' && result?.info?.exists
237
344
  ? `${result.info.aggregateScore}/100 (${classifyRisk(result.info.aggregateScore)})`
@@ -270,14 +377,30 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
270
377
  <Box flexDirection="column" marginTop={1}>
271
378
  <Text color="gray">────────────────────────────────────────</Text>
272
379
  <Text color="white" bold> Security Summary</Text>
380
+ {result.resolved?.source === 'ens' && (
381
+ <Box marginLeft={2}>
382
+ <Text color="gray">Resolved: </Text>
383
+ <Text color="green">{result.ensName}</Text>
384
+ <Text color="gray"> → </Text>
385
+ <Text color="cyan">{result.resolvedVersion}</Text>
386
+ </Box>
387
+ )}
388
+ {result.autoBumped && (
389
+ <Box marginLeft={2}>
390
+ <Text color="gray">Bumped: </Text>
391
+ <Text color="red">{result.autoBumpedFrom}</Text>
392
+ <Text color="gray"> → </Text>
393
+ <Text color="green">{result.resolvedVersion}</Text>
394
+ </Box>
395
+ )}
273
396
  {result.info?.exists && (
274
397
  <Box marginLeft={2}>
275
- <Text color="gray">Risk: </Text>
398
+ <Text color="gray">Risk: </Text>
276
399
  <RiskBadge level={classifyRisk(result.info.aggregateScore)} score={result.info.aggregateScore} />
277
400
  </Box>
278
401
  )}
279
402
  <Box marginLeft={2}>
280
- <Text color="gray">CVEs: </Text>
403
+ <Text color="gray">CVEs: </Text>
281
404
  {result.cves.length > 0 ? (
282
405
  <Text color={severeCount > 0 ? 'red' : 'yellow'}>
283
406
  {result.cves.length} known ({cveCounts.critical > 0 ? `${cveCounts.critical} critical, ` : ''}{cveCounts.high} high, {cveCounts.medium} medium, {cveCounts.low} low)
@@ -288,19 +411,19 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
288
411
  </Box>
289
412
  {result.info?.exists && (
290
413
  <Box marginLeft={2}>
291
- <Text color="gray">Signature: </Text>
414
+ <Text color="gray">Signature:</Text>
292
415
  <Text color={result.signatureValid ? 'green' : 'red'}>
293
- {result.signatureValid ? 'verified' : 'unverified'}
416
+ {' '}{result.signatureValid ? 'verified' : 'unverified'}
294
417
  </Text>
295
418
  </Box>
296
419
  )}
297
420
  {result.ensName && (
298
421
  <Box marginLeft={2}>
299
- <Text color="gray">Author: </Text>
422
+ <Text color="gray">Author: </Text>
300
423
  <Text color="green">{result.ensName}</Text>
301
424
  </Box>
302
425
  )}
303
- {result.warning && !result.blocked && (
426
+ {result.warning && !result.blocked && !result.autoBumped && (
304
427
  <Box marginLeft={2}>
305
428
  <Text color="yellow">⚠ Vulnerabilities detected — review before using in production</Text>
306
429
  </Box>
@@ -312,7 +435,7 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
312
435
  <Text color="yellow"> to fix known CVEs</Text>
313
436
  </Box>
314
437
  )}
315
- {result.warning && result.safestVersion && (
438
+ {result.warning && result.safestVersion && !result.autoBumped && (
316
439
  <Box marginLeft={2}>
317
440
  <Text color="yellow">⚠ Consider using safest on-chain version: {result.safestVersion}</Text>
318
441
  </Box>
@@ -326,6 +449,351 @@ export function InstallCommand({ packageName, version }: InstallCommandProps) {
326
449
  );
327
450
  }
328
451
 
452
+ // ─── Bulk install: scan ALL deps from package.json ────────────────────────────
453
+
454
+ interface BulkDepResult {
455
+ name: string;
456
+ version: string;
457
+ cves: OSVVulnerability[];
458
+ cvesCritical: number;
459
+ cvesHigh: number;
460
+ onChain: boolean;
461
+ score: number | null;
462
+ blocked: boolean;
463
+ blockReason?: string;
464
+ suggestedUpgrade?: string;
465
+ ensResolved?: boolean;
466
+ ensName?: string;
467
+ autoBumped?: boolean;
468
+ originalVersion?: string;
469
+ autoBumpReason?: string;
470
+ }
471
+
472
+ function BulkInstall() {
473
+ const [deps, setDeps] = useState<BulkDepResult[]>([]);
474
+ const [scanning, setScanning] = useState(true);
475
+ const [error, setError] = useState<string | null>(null);
476
+ const [installStatus, setInstallStatus] = useState<StepStatus>('pending');
477
+ const [total, setTotal] = useState(0);
478
+ const [ensCount, setEnsCount] = useState(0);
479
+ const [ensResolvingStatus, setEnsResolvingStatus] = useState<StepStatus>('skip');
480
+ const [ensResolvedCount, setEnsResolvedCount] = useState(0);
481
+
482
+ useEffect(() => {
483
+ runBulk().catch((err) => setError(String(err)));
484
+ }, []);
485
+
486
+ async function runBulk() {
487
+ const pkgPath = path.resolve('package.json');
488
+ if (!fs.existsSync(pkgPath)) {
489
+ setError('No package.json found');
490
+ return;
491
+ }
492
+
493
+ const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
494
+ const allDeps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
495
+ const entries = Object.entries(allDeps) as [string, string][];
496
+ setTotal(entries.length);
497
+
498
+ if (entries.length === 0) {
499
+ setScanning(false);
500
+ return;
501
+ }
502
+
503
+ // ── Phase 1: Batch-resolve all ENS names in parallel ──
504
+ const ensEntries = entries.filter(([, ver]) => isENSVersion(String(ver)));
505
+ setEnsCount(ensEntries.length);
506
+
507
+ const ensCache = new Map<string, { address: string; version: string }>();
508
+
509
+ if (ensEntries.length > 0) {
510
+ setEnsResolvingStatus('running');
511
+
512
+ const uniqueEnsNames = [...new Set(ensEntries.map(([, v]) => String(v)))];
513
+ const ensResults = await Promise.allSettled(
514
+ uniqueEnsNames.map(async (ensName) => {
515
+ const addr = await resolveAddress(ensName);
516
+ return { ensName, address: addr };
517
+ }),
518
+ );
519
+
520
+ const ensAddresses = new Map<string, string>();
521
+ for (const result of ensResults) {
522
+ if (result.status === 'fulfilled' && result.value.address) {
523
+ ensAddresses.set(result.value.ensName, result.value.address);
524
+ }
525
+ }
526
+
527
+ const ensVersionResults = await Promise.allSettled(
528
+ ensEntries.map(async ([name, ensName]) => {
529
+ const addr = ensAddresses.get(String(ensName));
530
+ if (!addr) return null;
531
+ const resolved = await resolveVersion(name, String(ensName));
532
+ return { name, ensName: String(ensName), resolved };
533
+ }),
534
+ );
535
+
536
+ for (const result of ensVersionResults) {
537
+ if (result.status === 'fulfilled' && result.value) {
538
+ const { name, ensName, resolved } = result.value;
539
+ ensCache.set(name, {
540
+ address: resolved.authorAddress || '',
541
+ version: resolved.version,
542
+ });
543
+ setEnsResolvedCount((c) => c + 1);
544
+ }
545
+ }
546
+
547
+ setEnsResolvingStatus('done');
548
+ }
549
+
550
+ // ── Phase 2: Scan each dependency ──
551
+ const checked: BulkDepResult[] = [];
552
+
553
+ for (const [name, verRange] of entries) {
554
+ const rawVerStr = String(verRange);
555
+ const isEns = isENSVersion(rawVerStr);
556
+
557
+ let rawVersion: string;
558
+ let ensName: string | undefined;
559
+ let ensResolved = false;
560
+
561
+ if (isEns && ensCache.has(name)) {
562
+ const cached = ensCache.get(name)!;
563
+ rawVersion = cached.version;
564
+ ensName = rawVerStr;
565
+ ensResolved = true;
566
+ } else {
567
+ rawVersion = rawVerStr.replace(/^[\^~]/, '');
568
+ }
569
+
570
+ const entry: BulkDepResult = {
571
+ name, version: rawVersion,
572
+ cves: [], cvesCritical: 0, cvesHigh: 0,
573
+ onChain: false, score: null,
574
+ blocked: false,
575
+ ensResolved, ensName,
576
+ };
577
+
578
+ const [osvResult, infoResult] = await Promise.allSettled([
579
+ queryOSV(name, rawVersion),
580
+ getPackageInfo(name, rawVersion),
581
+ ]);
582
+
583
+ if (osvResult.status === 'fulfilled' && osvResult.value.length > 0) {
584
+ entry.cves = osvResult.value;
585
+ const counts = categorizeCVEs(osvResult.value);
586
+ entry.cvesCritical = counts.critical;
587
+ entry.cvesHigh = counts.high;
588
+
589
+ if (counts.critical > 0) {
590
+ entry.blocked = true;
591
+ entry.blockReason = `${counts.critical} CRITICAL CVE(s)`;
592
+ entry.suggestedUpgrade = getBestUpgradeVersion(osvResult.value, rawVersion) || undefined;
593
+ }
594
+ }
595
+
596
+ if (infoResult.status === 'fulfilled' && infoResult.value.exists) {
597
+ entry.onChain = true;
598
+ entry.score = infoResult.value.aggregateScore;
599
+ if (entry.score >= HIGH_RISK_THRESHOLD) {
600
+ entry.blocked = true;
601
+ entry.blockReason = (entry.blockReason ? entry.blockReason + '; ' : '') + `risk ${entry.score}/100`;
602
+ }
603
+ }
604
+
605
+ // ── Auto-bump blocked deps ──
606
+ if (entry.blocked && !ensResolved) {
607
+ const safe = await findSafeVersion(name, rawVersion, entry.cves);
608
+ if (safe) {
609
+ entry.autoBumped = true;
610
+ entry.originalVersion = rawVersion;
611
+ entry.autoBumpReason = safe.reason;
612
+ entry.version = safe.version;
613
+ entry.blocked = false;
614
+ entry.blockReason = undefined;
615
+
616
+ const newCves = await queryOSV(name, safe.version).catch(() => []);
617
+ entry.cves = newCves;
618
+ const newCounts = categorizeCVEs(newCves);
619
+ entry.cvesCritical = newCounts.critical;
620
+ entry.cvesHigh = newCounts.high;
621
+ }
622
+ }
623
+
624
+ checked.push(entry);
625
+ setDeps([...checked]);
626
+ }
627
+
628
+ setScanning(false);
629
+
630
+ const blockers = checked.filter((d) => d.blocked);
631
+ if (blockers.length > 0) {
632
+ setInstallStatus('error');
633
+ setError(`Blocked: ${blockers.length} package(s) have critical vulnerabilities`);
634
+ return;
635
+ }
636
+
637
+ // Build npm install command with correct versions
638
+ const bumpedDeps = checked.filter((d) => d.autoBumped || d.ensResolved);
639
+ setInstallStatus('running');
640
+ try {
641
+ if (bumpedDeps.length > 0) {
642
+ const args = checked.map((d) => `${d.name}@${d.version}`).join(' ');
643
+ execSync(`npm install ${args}`, { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
644
+ } else {
645
+ execSync('npm install', { encoding: 'utf-8', stdio: 'pipe', cwd: process.cwd() });
646
+ }
647
+ } catch { /* non-fatal */ }
648
+ setInstallStatus('done');
649
+ }
650
+
651
+ const blockedDeps = deps.filter((d) => d.blocked);
652
+ const bumpedDeps = deps.filter((d) => d.autoBumped || d.ensResolved);
653
+ const warnDeps = deps.filter((d) => !d.blocked && !d.autoBumped && !d.ensResolved && (d.cvesHigh > 0 || (d.score !== null && d.score >= MEDIUM_RISK_THRESHOLD)));
654
+ const safeDeps = deps.filter((d) => !d.blocked && !d.autoBumped && !d.ensResolved && d.cvesHigh === 0 && (d.score === null || d.score < MEDIUM_RISK_THRESHOLD));
655
+ const totalCves = deps.reduce((s, d) => s + d.cves.length, 0);
656
+
657
+ return (
658
+ <Box flexDirection="column">
659
+ <Header subtitle="install" />
660
+ <Text> </Text>
661
+
662
+ {ensCount > 0 && (
663
+ <StatusLine label={`Resolve ${ensCount} ENS author(s)`} status={ensResolvingStatus}
664
+ detail={ensResolvingStatus === 'done' ? `${ensResolvedCount} resolved` : ensResolvingStatus === 'running' ? 'resolving...' : undefined} />
665
+ )}
666
+
667
+ <StatusLine label={`Scanning ${total} dependencies`} status={scanning ? 'running' : 'done'}
668
+ detail={!scanning ? `${deps.length} checked` : `${deps.length}/${total}`} />
669
+
670
+ {deps.length > 0 && (
671
+ <Box flexDirection="column" marginTop={1}>
672
+ {bumpedDeps.length > 0 && (
673
+ <Box flexDirection="column">
674
+ <Text color="cyan" bold> ENS / AUTO-BUMPED ({bumpedDeps.length})</Text>
675
+ {bumpedDeps.map((d) => (
676
+ <Box key={d.name} flexDirection="column" marginLeft={2}>
677
+ <Box>
678
+ {d.ensResolved ? (
679
+ <Text color="cyan">◈ </Text>
680
+ ) : (
681
+ <Text color="yellow">↑ </Text>
682
+ )}
683
+ <Text color="white" bold>{d.name}</Text>
684
+ <Text color="green">@{d.version}</Text>
685
+ {d.ensResolved && d.ensName && (
686
+ <Text color="cyan"> via {d.ensName}</Text>
687
+ )}
688
+ {d.autoBumped && d.originalVersion && (
689
+ <Text color="yellow"> bumped from {d.originalVersion}</Text>
690
+ )}
691
+ </Box>
692
+ {d.autoBumpReason && (
693
+ <Box marginLeft={4}>
694
+ <Text color="gray">{d.autoBumpReason}</Text>
695
+ </Box>
696
+ )}
697
+ </Box>
698
+ ))}
699
+ </Box>
700
+ )}
701
+
702
+ {blockedDeps.length > 0 && (
703
+ <Box flexDirection="column" marginTop={bumpedDeps.length > 0 ? 1 : 0}>
704
+ <Text color="red" bold> BLOCKED ({blockedDeps.length})</Text>
705
+ {blockedDeps.map((d) => (
706
+ <Box key={d.name} flexDirection="column" marginLeft={2}>
707
+ <Box>
708
+ <Text color="red">✖ </Text>
709
+ <Text color="white" bold>{d.name}</Text>
710
+ <Text color="gray">@{d.version}</Text>
711
+ <Text color="red"> {d.blockReason}</Text>
712
+ </Box>
713
+ {d.cves.slice(0, 3).map((cve) => {
714
+ const sev = getOSVSeverity(cve);
715
+ return (
716
+ <Box key={cve.id} marginLeft={4}>
717
+ <Text color={sevColor(sev)} bold>{sev.padEnd(9)}</Text>
718
+ <Text color="white">{cve.id} </Text>
719
+ <Text color="gray">{cve.summary?.slice(0, 50)}</Text>
720
+ </Box>
721
+ );
722
+ })}
723
+ {d.cves.length > 3 && (
724
+ <Text color="gray" dimColor> ...and {d.cves.length - 3} more</Text>
725
+ )}
726
+ {d.suggestedUpgrade && (
727
+ <Box marginLeft={4}>
728
+ <Text color="green">↑ upgrade to {d.suggestedUpgrade}</Text>
729
+ </Box>
730
+ )}
731
+ </Box>
732
+ ))}
733
+ </Box>
734
+ )}
735
+
736
+ {warnDeps.length > 0 && (
737
+ <Box flexDirection="column" marginTop={(blockedDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
738
+ <Text color="yellow" bold> WARNING ({warnDeps.length})</Text>
739
+ {warnDeps.map((d) => (
740
+ <Box key={d.name} marginLeft={2}>
741
+ <Text color="yellow">⚠ </Text>
742
+ <Text>{d.name}</Text>
743
+ <Text color="gray">@{d.version}</Text>
744
+ {d.cvesHigh > 0 && <Text color="yellow"> {d.cvesHigh} high CVE(s)</Text>}
745
+ {d.score !== null && <Text color="yellow"> risk {d.score}/100</Text>}
746
+ </Box>
747
+ ))}
748
+ </Box>
749
+ )}
750
+
751
+ {safeDeps.length > 0 && (
752
+ <Box flexDirection="column" marginTop={(blockedDeps.length + warnDeps.length + bumpedDeps.length) > 0 ? 1 : 0}>
753
+ <Text color="green" bold> SAFE ({safeDeps.length})</Text>
754
+ {safeDeps.map((d) => (
755
+ <Box key={d.name} marginLeft={2}>
756
+ <Text color="green">✓ </Text>
757
+ <Text>{d.name}</Text>
758
+ <Text color="gray">@{d.version}</Text>
759
+ {d.onChain && d.score !== null && (
760
+ <Text color="green"> {d.score}/100</Text>
761
+ )}
762
+ </Box>
763
+ ))}
764
+ </Box>
765
+ )}
766
+ </Box>
767
+ )}
768
+
769
+ {!scanning && (
770
+ <Box flexDirection="column" marginTop={1}>
771
+ <Text color="gray">────────────────────────────────────────</Text>
772
+
773
+ {blockedDeps.length === 0 && (
774
+ <StatusLine label="Install via npm" status={installStatus} />
775
+ )}
776
+
777
+ <Box marginTop={1}>
778
+ <Text color={blockedDeps.length > 0 ? 'red' : totalCves > 0 ? 'yellow' : 'green'} bold>
779
+ {deps.length} packages scanned: {blockedDeps.length} blocked, {bumpedDeps.length} resolved, {warnDeps.length} warnings, {totalCves} CVEs
780
+ </Text>
781
+ </Box>
782
+
783
+ {blockedDeps.length > 0 && (
784
+ <Text color="red">Fix blocked packages before installing. Upgrade to safe versions above.</Text>
785
+ )}
786
+ </Box>
787
+ )}
788
+
789
+ {error && <Text color="red">{error}</Text>}
790
+ {installStatus === 'done' && <Text color="green" bold>Done.</Text>}
791
+ </Box>
792
+ );
793
+ }
794
+
795
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
796
+
329
797
  function getBestUpgradeVersion(cves: OSVVulnerability[], currentVersion: string): string | null {
330
798
  let highest: string | null = null;
331
799
  for (const cve of cves) {
@@ -346,17 +814,3 @@ function compareSemver(a: string, b: string): number {
346
814
  }
347
815
  return 0;
348
816
  }
349
-
350
- function getTargetPackages(name?: string, ver?: string): Array<{ name: string; version: string }> {
351
- if (name) return [{ name, version: ver || 'latest' }];
352
-
353
- const pkgJsonPath = path.resolve('package.json');
354
- if (!fs.existsSync(pkgJsonPath)) return [];
355
-
356
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
357
- const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
358
- return Object.entries(deps).map(([n, v]) => ({
359
- name: n,
360
- version: String(v).replace(/^[\^~]/, ''),
361
- }));
362
- }