opmsec 0.1.0 → 0.1.3

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 (124) hide show
  1. package/.env.example +23 -13
  2. package/README.md +256 -173
  3. package/docs/architecture/agents.mdx +77 -0
  4. package/docs/architecture/benchmarks.mdx +65 -0
  5. package/docs/architecture/overview.mdx +58 -0
  6. package/docs/architecture/scanner.mdx +53 -0
  7. package/docs/cli/audit.mdx +35 -0
  8. package/docs/cli/check.mdx +44 -0
  9. package/docs/cli/fix.mdx +49 -0
  10. package/docs/cli/info.mdx +44 -0
  11. package/docs/cli/install.mdx +71 -0
  12. package/docs/cli/push.mdx +99 -0
  13. package/docs/cli/register-agent.mdx +80 -0
  14. package/docs/cli/view.mdx +52 -0
  15. package/docs/concepts/multi-agent-consensus.mdx +58 -0
  16. package/docs/concepts/on-chain-registry.mdx +74 -0
  17. package/docs/concepts/security-model.mdx +76 -0
  18. package/docs/concepts/zk-agent-verification.mdx +82 -0
  19. package/docs/configuration.mdx +82 -0
  20. package/docs/contract/deployment.mdx +57 -0
  21. package/docs/contract/events.mdx +115 -0
  22. package/docs/contract/functions.mdx +220 -0
  23. package/docs/contract/overview.mdx +58 -0
  24. package/docs/favicon.svg +5 -0
  25. package/docs/introduction.mdx +43 -0
  26. package/docs/logo/dark.svg +5 -0
  27. package/docs/logo/light.svg +5 -0
  28. package/docs/mint.json +106 -0
  29. package/docs/quickstart.mdx +133 -0
  30. package/package.json +3 -3
  31. package/packages/cli/src/commands/author-view.tsx +9 -1
  32. package/packages/cli/src/commands/check.tsx +318 -0
  33. package/packages/cli/src/commands/fix.tsx +294 -0
  34. package/packages/cli/src/commands/install.tsx +229 -33
  35. package/packages/cli/src/commands/push.tsx +53 -22
  36. package/packages/cli/src/commands/register-agent.tsx +227 -0
  37. package/packages/cli/src/components/AgentScores.tsx +20 -6
  38. package/packages/cli/src/components/Hyperlink.tsx +30 -0
  39. package/packages/cli/src/components/ScanReport.tsx +3 -2
  40. package/packages/cli/src/index.tsx +41 -5
  41. package/packages/cli/src/services/avatar.ts +43 -6
  42. package/packages/cli/src/services/chainpatrol.ts +20 -17
  43. package/packages/cli/src/services/contract.ts +41 -8
  44. package/packages/cli/src/services/ens.ts +3 -5
  45. package/packages/cli/src/services/fileverse.ts +12 -13
  46. package/packages/cli/src/services/typosquat.ts +166 -0
  47. package/packages/contracts/circuits/accuracy_verifier.circom +101 -0
  48. package/packages/contracts/contracts/OPMRegistry.sol +63 -0
  49. package/packages/contracts/scripts/deploy.ts +22 -3
  50. package/packages/core/src/abi.ts +221 -0
  51. package/packages/core/src/benchmarks.ts +450 -0
  52. package/packages/core/src/constants.ts +20 -0
  53. package/packages/core/src/index.ts +2 -0
  54. package/packages/core/src/model-rankings.ts +115 -0
  55. package/packages/core/src/prompt.ts +58 -0
  56. package/packages/core/src/types.ts +41 -0
  57. package/packages/core/src/utils.ts +7 -3
  58. package/packages/scanner/src/agents/base-agent.ts +13 -3
  59. package/packages/scanner/src/index.ts +5 -2
  60. package/packages/scanner/src/queue/memory-queue.ts +8 -3
  61. package/packages/scanner/src/services/benchmark-runner.ts +114 -0
  62. package/packages/scanner/src/services/contract-writer.ts +2 -3
  63. package/packages/scanner/src/services/fileverse.ts +26 -7
  64. package/packages/scanner/src/services/openrouter.ts +46 -0
  65. package/packages/scanner/src/services/report-formatter.ts +122 -3
  66. package/packages/scanner/src/services/zk-verifier.ts +118 -0
  67. package/packages/web/.next/app-build-manifest.json +15 -0
  68. package/packages/web/.next/build-manifest.json +20 -0
  69. package/packages/web/.next/package.json +1 -0
  70. package/packages/web/.next/prerender-manifest.json +11 -0
  71. package/packages/web/.next/react-loadable-manifest.json +1 -0
  72. package/packages/web/.next/routes-manifest.json +1 -0
  73. package/packages/web/.next/server/app/page.js +272 -0
  74. package/packages/web/.next/server/app/page_client-reference-manifest.js +1 -0
  75. package/packages/web/.next/server/app-paths-manifest.json +3 -0
  76. package/packages/web/.next/server/interception-route-rewrite-manifest.js +1 -0
  77. package/packages/web/.next/server/middleware-build-manifest.js +22 -0
  78. package/packages/web/.next/server/middleware-manifest.json +6 -0
  79. package/packages/web/.next/server/middleware-react-loadable-manifest.js +1 -0
  80. package/packages/web/.next/server/next-font-manifest.js +1 -0
  81. package/packages/web/.next/server/next-font-manifest.json +1 -0
  82. package/packages/web/.next/server/pages-manifest.json +1 -0
  83. package/packages/web/.next/server/server-reference-manifest.js +1 -0
  84. package/packages/web/.next/server/server-reference-manifest.json +5 -0
  85. package/packages/web/.next/server/vendor-chunks/@swc.js +55 -0
  86. package/packages/web/.next/server/vendor-chunks/next.js +3010 -0
  87. package/packages/web/.next/server/webpack-runtime.js +209 -0
  88. package/packages/web/.next/static/chunks/app/layout.js +39 -0
  89. package/packages/web/.next/static/chunks/app/page.js +61 -0
  90. package/packages/web/.next/static/chunks/app-pages-internals.js +182 -0
  91. package/packages/web/.next/static/chunks/main-app.js +1882 -0
  92. package/packages/web/.next/static/chunks/polyfills.js +1 -0
  93. package/packages/web/.next/static/chunks/webpack.js +1393 -0
  94. package/packages/web/.next/static/css/app/layout.css +1237 -0
  95. package/packages/web/.next/static/development/_buildManifest.js +1 -0
  96. package/packages/web/.next/static/development/_ssgManifest.js +1 -0
  97. package/packages/web/.next/static/webpack/633457081244afec._.hot-update.json +1 -0
  98. package/packages/web/.next/static/webpack/6fee6306e0f98869.webpack.hot-update.json +1 -0
  99. package/packages/web/.next/static/webpack/73e341375c8d429e.webpack.hot-update.json +1 -0
  100. package/packages/web/.next/static/webpack/app/layout.6fee6306e0f98869.hot-update.js +22 -0
  101. package/packages/web/.next/static/webpack/app/layout.73e341375c8d429e.hot-update.js +22 -0
  102. package/packages/web/.next/static/webpack/app/page.6fee6306e0f98869.hot-update.js +22 -0
  103. package/packages/web/.next/static/webpack/app/page.73e341375c8d429e.hot-update.js +22 -0
  104. package/packages/web/.next/static/webpack/webpack.6fee6306e0f98869.hot-update.js +12 -0
  105. package/packages/web/.next/static/webpack/webpack.73e341375c8d429e.hot-update.js +12 -0
  106. package/packages/web/.next/trace +5 -0
  107. package/packages/web/.next/types/app/layout.ts +84 -0
  108. package/packages/web/.next/types/app/page.ts +84 -0
  109. package/packages/web/.next/types/cache-life.d.ts +141 -0
  110. package/packages/web/.next/types/package.json +1 -0
  111. package/packages/web/.next/types/routes.d.ts +57 -0
  112. package/packages/web/.next/types/validator.ts +61 -0
  113. package/packages/web/app/globals.css +75 -0
  114. package/packages/web/app/layout.tsx +26 -0
  115. package/packages/web/app/page.tsx +358 -0
  116. package/packages/web/bun.lock +300 -0
  117. package/packages/web/next-env.d.ts +6 -0
  118. package/packages/web/next.config.ts +5 -0
  119. package/packages/web/package.json +26 -0
  120. package/packages/web/postcss.config.mjs +8 -0
  121. package/packages/web/public/favicon.svg +5 -0
  122. package/packages/web/public/logo.svg +7 -0
  123. package/packages/web/tailwind.config.ts +48 -0
  124. package/packages/web/tsconfig.json +21 -0
@@ -0,0 +1,318 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { CHECK_SYSTEM_PROMPT, buildCheckPrompt, classifyRisk, getModelRankingFor } from '@opm/core';
4
+ import type { DepEntry, CheckReport, CheckDepResult, CheckAgentResult } from '@opm/core';
5
+ import { Header } from '../components/Header';
6
+ import { StatusLine } from '../components/StatusLine';
7
+ import { RiskBadge } from '../components/RiskBadge';
8
+ import { Hyperlink } from '../components/Hyperlink';
9
+ import { queryOSV, getOSVSeverity, getFixedVersion } from '../services/osv';
10
+ import { getPackageInfo } from '../services/contract';
11
+ import { detectTyposquatBatch } from '../services/typosquat';
12
+ import { callLLMRaw, getAgentConfigs, uploadCheckReportToFileverse } from '@opm/scanner';
13
+ import * as fs from 'fs';
14
+ import * as path from 'path';
15
+
16
+ type Phase = 'scanning' | 'agents' | 'upload' | 'done';
17
+
18
+ export function CheckCommand() {
19
+ const [phase, setPhase] = useState<Phase>('scanning');
20
+ const [report, setReport] = useState<CheckReport | null>(null);
21
+ const [reportLink, setReportLink] = useState<string | null>(null);
22
+ const [error, setError] = useState<string | null>(null);
23
+
24
+ useEffect(() => {
25
+ runCheck().catch((e) => setError(String(e)));
26
+ }, []);
27
+
28
+ async function runCheck() {
29
+ const pkgPath = path.resolve('package.json');
30
+ if (!fs.existsSync(pkgPath)) { setError('No package.json found'); return; }
31
+
32
+ const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
33
+ const projectName = pkgJson.name || path.basename(process.cwd());
34
+ const deps = Object.entries(pkgJson.dependencies || {}) as [string, string][];
35
+ const devDeps = Object.entries(pkgJson.devDependencies || {}) as [string, string][];
36
+ const allEntries = [
37
+ ...deps.map(([n, v]) => ({ n, v: clean(v) })),
38
+ ...devDeps.map(([n, v]) => ({ n, v: clean(v) })),
39
+ ];
40
+ const allNames = allEntries.map((e) => e.n);
41
+
42
+ const [typosquatResults, ...parallelResults] = await Promise.all([
43
+ detectTyposquatBatch(allNames),
44
+ ...allEntries.map(({ n, v }) =>
45
+ Promise.allSettled([queryOSV(n, v), getPackageInfo(n, v)]),
46
+ ),
47
+ ]);
48
+
49
+ const depResults: CheckDepResult[] = allEntries.map((entry, idx) => {
50
+ const typo = typosquatResults[idx];
51
+ const [osvR, chainR] = parallelResults[idx] as PromiseSettledResult<any>[];
52
+
53
+ const result: CheckDepResult = {
54
+ name: entry.n, version: entry.v,
55
+ typosquat: typo && typo.confidence !== 'none'
56
+ ? { likelyTarget: typo.likelyTarget!, confidence: typo.confidence, reason: typo.reason }
57
+ : null,
58
+ cveCount: 0, cveCritical: 0, cveHigh: 0,
59
+ cveIds: [], fixVersion: null, onChainScore: null,
60
+ };
61
+
62
+ if (osvR.status === 'fulfilled' && osvR.value.length > 0) {
63
+ result.cveCount = osvR.value.length;
64
+ result.cveIds = osvR.value.map((c: any) => c.id);
65
+ let bestFix: string | null = null;
66
+ for (const cve of osvR.value) {
67
+ const sev = getOSVSeverity(cve);
68
+ if (sev === 'CRITICAL') result.cveCritical++;
69
+ else if (sev === 'HIGH') result.cveHigh++;
70
+ const fix = getFixedVersion(cve, entry.v);
71
+ if (fix && (!bestFix || compareSemver(fix, bestFix) > 0)) bestFix = fix;
72
+ }
73
+ result.fixVersion = bestFix;
74
+ }
75
+
76
+ if (chainR.status === 'fulfilled' && chainR.value.exists) {
77
+ result.onChainScore = chainR.value.aggregateScore;
78
+ }
79
+
80
+ return result;
81
+ });
82
+
83
+ setPhase('agents');
84
+ let agentResults: CheckAgentResult[] = [];
85
+ try {
86
+ const configs = getAgentConfigs();
87
+ const depE: DepEntry[] = deps.map(([n, v]) => ({ name: n, version: clean(v) }));
88
+ const devE: DepEntry[] = devDeps.map(([n, v]) => ({ name: n, version: clean(v) }));
89
+ const prompt = buildCheckPrompt(depE, devE);
90
+
91
+ const runs = await Promise.allSettled(
92
+ configs.map(async (cfg) => {
93
+ const { intelligence, coding } = await getModelRankingFor(cfg.model);
94
+ const res = await callLLMRaw<{
95
+ findings: CheckAgentResult['findings'];
96
+ overall_assessment: string;
97
+ risk_score: number;
98
+ }>(cfg.model, CHECK_SYSTEM_PROMPT, prompt);
99
+ return {
100
+ agentId: cfg.agentId, model: cfg.model,
101
+ intelligence, coding,
102
+ findings: res.findings || [],
103
+ overall: res.overall_assessment || '',
104
+ riskScore: res.risk_score || 0,
105
+ } satisfies CheckAgentResult;
106
+ }),
107
+ );
108
+ agentResults = runs.filter((r): r is PromiseFulfilledResult<CheckAgentResult> =>
109
+ r.status === 'fulfilled',
110
+ ).map((r) => r.value);
111
+ } catch { /* no LLM keys — skip */ }
112
+
113
+ const checkReport: CheckReport = {
114
+ project: projectName,
115
+ timestamp: new Date().toISOString(),
116
+ totalDeps: allEntries.length,
117
+ deps: depResults,
118
+ agents: agentResults,
119
+ };
120
+ setReport(checkReport);
121
+
122
+ setPhase('upload');
123
+ try {
124
+ const link = await uploadCheckReportToFileverse(checkReport);
125
+ setReportLink(link);
126
+ } catch { /* no Fileverse key — skip */ }
127
+
128
+ setPhase('done');
129
+ }
130
+
131
+ const typosquats = (report?.deps || []).filter((d) => d.typosquat);
132
+ const criticalCves = (report?.deps || []).filter((d) => d.cveCritical > 0);
133
+ const cveWarnings = (report?.deps || []).filter((d) => d.cveCount > 0 && d.cveCritical === 0);
134
+ const highRisk = (report?.deps || []).filter((d) => d.onChainScore !== null && d.onChainScore >= 70);
135
+ const agentFlags = (report?.agents || []).flatMap((a) =>
136
+ a.findings.filter((f) => f.issue !== 'safe' && f.severity !== 'NONE'),
137
+ );
138
+ const uniqueAgentFlags = [...new Map(agentFlags.map((f) => [f.package, f])).values()];
139
+
140
+ return (
141
+ <Box flexDirection="column">
142
+ <Header subtitle="check" />
143
+ <Text> </Text>
144
+
145
+ <StatusLine label={`Scanning ${report?.totalDeps || '...'} dependencies`}
146
+ status={phase === 'scanning' ? 'running' : 'done'}
147
+ detail={phase === 'scanning' ? 'typosquats + CVEs + on-chain (parallel)' : `${report?.totalDeps} checked`} />
148
+
149
+ {phase !== 'scanning' && (
150
+ <StatusLine label="AI agents analyzing dependency tree"
151
+ status={phase === 'agents' ? 'running' : 'done'}
152
+ detail={report?.agents.length ? `${report.agents.length} agents` : undefined} />
153
+ )}
154
+
155
+ {(phase === 'upload' || phase === 'done') && (
156
+ <StatusLine label="Upload report to Fileverse"
157
+ status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'} />
158
+ )}
159
+
160
+ {phase === 'done' && report && (
161
+ <Box flexDirection="column" marginTop={1}>
162
+ {typosquats.length > 0 && (
163
+ <Box flexDirection="column">
164
+ <Text color="red" bold> TYPOSQUAT RISK ({typosquats.length})</Text>
165
+ {typosquats.map((d) => (
166
+ <Box key={d.name} flexDirection="column" marginLeft={2}>
167
+ <Box>
168
+ <Text color="red">✖ </Text>
169
+ <Text color="white" bold>{d.name}</Text>
170
+ <Text color="gray">@{d.version}</Text>
171
+ <Text color="red"> → did you mean </Text>
172
+ <Text color="green" bold>{d.typosquat!.likelyTarget}</Text>
173
+ <Text color="gray"> ({d.typosquat!.confidence})</Text>
174
+ </Box>
175
+ <Box marginLeft={4}>
176
+ <Text color="gray">{d.typosquat!.reason}</Text>
177
+ </Box>
178
+ </Box>
179
+ ))}
180
+ </Box>
181
+ )}
182
+
183
+ {criticalCves.length > 0 && (
184
+ <Box flexDirection="column" marginTop={typosquats.length > 0 ? 1 : 0}>
185
+ <Text color="red" bold> CRITICAL CVEs ({criticalCves.length})</Text>
186
+ {criticalCves.map((d) => (
187
+ <Box key={d.name} flexDirection="column" marginLeft={2}>
188
+ <Box>
189
+ <Text color="red">✖ </Text>
190
+ <Text color="white" bold>{d.name}</Text>
191
+ <Text color="gray">@{d.version}</Text>
192
+ <Text color="red"> — {d.cveCritical} critical, {d.cveHigh} high</Text>
193
+ </Box>
194
+ <Box marginLeft={4}>
195
+ <Text color="gray">{d.cveIds.slice(0, 3).join(', ')}</Text>
196
+ </Box>
197
+ {d.fixVersion && (
198
+ <Box marginLeft={4}>
199
+ <Text color="green">↑ upgrade to {d.fixVersion}</Text>
200
+ </Box>
201
+ )}
202
+ </Box>
203
+ ))}
204
+ </Box>
205
+ )}
206
+
207
+ {cveWarnings.length > 0 && (
208
+ <Box flexDirection="column" marginTop={1}>
209
+ <Text color="yellow" bold> CVE WARNINGS ({cveWarnings.length})</Text>
210
+ {cveWarnings.map((d) => (
211
+ <Box key={d.name} marginLeft={2}>
212
+ <Text color="yellow">⚠ </Text>
213
+ <Text>{d.name}</Text>
214
+ <Text color="gray">@{d.version}</Text>
215
+ <Text color="yellow"> — {d.cveCount} CVE(s)</Text>
216
+ {d.fixVersion && <Text color="green"> → {d.fixVersion}</Text>}
217
+ </Box>
218
+ ))}
219
+ </Box>
220
+ )}
221
+
222
+ {highRisk.length > 0 && (
223
+ <Box flexDirection="column" marginTop={1}>
224
+ <Text color="red" bold> HIGH ON-CHAIN RISK ({highRisk.length})</Text>
225
+ {highRisk.map((d) => (
226
+ <Box key={d.name} marginLeft={2}>
227
+ <Text color="red">✖ </Text>
228
+ <Text>{d.name}</Text>
229
+ <Text color="gray">@{d.version}</Text>
230
+ <Text> </Text>
231
+ <RiskBadge level={classifyRisk(d.onChainScore!)} score={d.onChainScore!} />
232
+ </Box>
233
+ ))}
234
+ </Box>
235
+ )}
236
+
237
+ {report.agents.length > 0 && (
238
+ <Box flexDirection="column" marginTop={1}>
239
+ <Text color="gray">────────────────────────────────────────</Text>
240
+ <Text color="white" bold> AI Agent Analysis</Text>
241
+ {report.agents.map((a) => (
242
+ <Box key={a.agentId} flexDirection="column" marginLeft={2} marginTop={1}>
243
+ <Box>
244
+ <Text color="cyan" bold>{a.agentId}</Text>
245
+ <Text color="gray"> ({a.model}) </Text>
246
+ <Text color="magenta">AI: {a.intelligence}</Text>
247
+ <Text color="gray"> | </Text>
248
+ <Text color="blue">Code: {a.coding}</Text>
249
+ </Box>
250
+ {a.findings.filter((f) => f.issue !== 'safe').length > 0 ? (
251
+ a.findings.filter((f) => f.issue !== 'safe').map((f, i) => (
252
+ <Box key={i} marginLeft={2}>
253
+ <Text color={f.severity === 'CRITICAL' || f.severity === 'HIGH' ? 'red' : 'yellow'}>
254
+ [{f.severity}] </Text>
255
+ <Text color="white">{f.package} </Text>
256
+ <Text color="gray">— {f.issue}: {f.explanation.slice(0, 80)}</Text>
257
+ {f.suggested_replacement && (
258
+ <Text color="green"> → {f.suggested_replacement}</Text>
259
+ )}
260
+ </Box>
261
+ ))
262
+ ) : (
263
+ <Box marginLeft={2}>
264
+ <Text color="green">No issues found</Text>
265
+ </Box>
266
+ )}
267
+ <Box marginLeft={2}>
268
+ <Text color="gray" wrap="wrap">{a.overall.slice(0, 150)}</Text>
269
+ </Box>
270
+ </Box>
271
+ ))}
272
+ </Box>
273
+ )}
274
+
275
+ <Box flexDirection="column" marginTop={1}>
276
+ <Text color="gray">────────────────────────────────────────</Text>
277
+ <Text color="white" bold> Summary</Text>
278
+ <Box marginLeft={2} flexDirection="column">
279
+ <Text>{typosquats.length > 0 ? '🔴' : '🟢'} Typosquats: {typosquats.length}</Text>
280
+ <Text>{criticalCves.length > 0 ? '🔴' : cveWarnings.length > 0 ? '🟡' : '🟢'} CVEs: {criticalCves.length + cveWarnings.length} packages ({criticalCves.length} critical)</Text>
281
+ <Text>{highRisk.length > 0 ? '🔴' : '🟢'} On-chain risk: {highRisk.length} high-risk</Text>
282
+ {report.agents.length > 0 && (
283
+ <Text>{uniqueAgentFlags.length > 0 ? '🟡' : '🟢'} AI agents: {uniqueAgentFlags.length} flagged</Text>
284
+ )}
285
+ </Box>
286
+ {reportLink && (
287
+ <Box marginLeft={2} marginTop={1}>
288
+ <Text color="gray">Report: </Text>
289
+ <Hyperlink url={reportLink} />
290
+ </Box>
291
+ )}
292
+ {(typosquats.length > 0 || criticalCves.length > 0) && (
293
+ <Box marginLeft={2} marginTop={1}>
294
+ <Text color="yellow">Run </Text>
295
+ <Text color="cyan" bold>opm fix</Text>
296
+ <Text color="yellow"> to auto-correct these issues</Text>
297
+ </Box>
298
+ )}
299
+ </Box>
300
+ </Box>
301
+ )}
302
+
303
+ {error && <Text color="red">{error}</Text>}
304
+ </Box>
305
+ );
306
+ }
307
+
308
+ function clean(v: string): string { return String(v).replace(/^[\^~]/, ''); }
309
+
310
+ function compareSemver(a: string, b: string): number {
311
+ const pa = a.replace(/^v/, '').split('.').map(Number);
312
+ const pb = b.replace(/^v/, '').split('.').map(Number);
313
+ for (let i = 0; i < 3; i++) {
314
+ const diff = (pa[i] || 0) - (pb[i] || 0);
315
+ if (diff !== 0) return diff;
316
+ }
317
+ return 0;
318
+ }
@@ -0,0 +1,294 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { CHECK_SYSTEM_PROMPT, buildCheckPrompt, getModelRankingFor } from '@opm/core';
4
+ import type { DepEntry, CheckReport, CheckDepResult, CheckAgentResult } from '@opm/core';
5
+ import { Header } from '../components/Header';
6
+ import { StatusLine } from '../components/StatusLine';
7
+ import { Hyperlink } from '../components/Hyperlink';
8
+ import { queryOSV, getOSVSeverity, getFixedVersion } from '../services/osv';
9
+ import { detectTyposquatBatch } from '../services/typosquat';
10
+ import { callLLMRaw, getAgentConfigs, uploadCheckReportToFileverse } from '@opm/scanner';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+
14
+ interface FixAction {
15
+ name: string;
16
+ version: string;
17
+ kind: 'typosquat' | 'cve' | 'ai';
18
+ newName: string | null;
19
+ newVersion: string | null;
20
+ reason: string;
21
+ }
22
+
23
+ type Phase = 'scan' | 'agents' | 'apply' | 'upload' | 'done';
24
+
25
+ export function FixCommand() {
26
+ const [phase, setPhase] = useState<Phase>('scan');
27
+ const [fixes, setFixes] = useState<FixAction[]>([]);
28
+ const [total, setTotal] = useState(0);
29
+ const [applied, setApplied] = useState(false);
30
+ const [reportLink, setReportLink] = useState<string | null>(null);
31
+ const [error, setError] = useState<string | null>(null);
32
+
33
+ useEffect(() => {
34
+ runFix().catch((e) => setError(String(e)));
35
+ }, []);
36
+
37
+ async function runFix() {
38
+ const pkgPath = path.resolve('package.json');
39
+ if (!fs.existsSync(pkgPath)) { setError('No package.json found'); return; }
40
+
41
+ const rawJson = fs.readFileSync(pkgPath, 'utf-8');
42
+ const pkgJson = JSON.parse(rawJson);
43
+ const projectName = pkgJson.name || path.basename(process.cwd());
44
+ const deps = Object.entries(pkgJson.dependencies || {}) as [string, string][];
45
+ const devDeps = Object.entries(pkgJson.devDependencies || {}) as [string, string][];
46
+ const allEntries = [
47
+ ...deps.map(([n, v]) => ({ n, v: clean(v) })),
48
+ ...devDeps.map(([n, v]) => ({ n, v: clean(v) })),
49
+ ];
50
+ setTotal(allEntries.length);
51
+
52
+ const allNames = allEntries.map((e) => e.n);
53
+ const [typosquatResults, ...parallelResults] = await Promise.all([
54
+ detectTyposquatBatch(allNames),
55
+ ...allEntries.map(({ n, v }) => queryOSV(n, v).catch(() => [])),
56
+ ]);
57
+
58
+ const actions: FixAction[] = [];
59
+ const depResults: CheckDepResult[] = [];
60
+
61
+ for (let i = 0; i < allEntries.length; i++) {
62
+ const { n, v } = allEntries[i];
63
+ const typo = typosquatResults[i];
64
+ const cves = (parallelResults[i] as any[]) || [];
65
+
66
+ const depR: CheckDepResult = {
67
+ name: n, version: v, typosquat: null,
68
+ cveCount: 0, cveCritical: 0, cveHigh: 0,
69
+ cveIds: [], fixVersion: null, onChainScore: null,
70
+ };
71
+
72
+ if (typo && typo.confidence !== 'none' && typo.likelyTarget) {
73
+ depR.typosquat = { likelyTarget: typo.likelyTarget, confidence: typo.confidence, reason: typo.reason };
74
+ actions.push({
75
+ name: n, version: v, kind: 'typosquat',
76
+ newName: typo.likelyTarget, newVersion: null,
77
+ reason: typo.reason,
78
+ });
79
+ }
80
+
81
+ if (cves.length > 0) {
82
+ depR.cveCount = cves.length;
83
+ depR.cveIds = cves.map((c: any) => c.id);
84
+ let bestFix: string | null = null;
85
+ for (const cve of cves) {
86
+ const sev = getOSVSeverity(cve);
87
+ if (sev === 'CRITICAL') depR.cveCritical++;
88
+ else if (sev === 'HIGH') depR.cveHigh++;
89
+ const fix = getFixedVersion(cve, v);
90
+ if (fix && (!bestFix || compareSemver(fix, bestFix) > 0)) bestFix = fix;
91
+ }
92
+ depR.fixVersion = bestFix;
93
+ if ((depR.cveCritical > 0 || depR.cveHigh > 0) && bestFix) {
94
+ actions.push({
95
+ name: n, version: v, kind: 'cve',
96
+ newName: null, newVersion: bestFix,
97
+ reason: `${cves.length} CVE(s): ${depR.cveIds.slice(0, 3).join(', ')}`,
98
+ });
99
+ }
100
+ }
101
+
102
+ depResults.push(depR);
103
+ }
104
+
105
+ setPhase('agents');
106
+ let agentResults: CheckAgentResult[] = [];
107
+ try {
108
+ const configs = getAgentConfigs();
109
+ const depE: DepEntry[] = deps.map(([n, v]) => ({ name: n, version: clean(v) }));
110
+ const devE: DepEntry[] = devDeps.map(([n, v]) => ({ name: n, version: clean(v) }));
111
+ const prompt = buildCheckPrompt(depE, devE);
112
+
113
+ const runs = await Promise.allSettled(
114
+ configs.map(async (cfg) => {
115
+ const { intelligence, coding } = await getModelRankingFor(cfg.model);
116
+ const res = await callLLMRaw<{
117
+ findings: CheckAgentResult['findings'];
118
+ overall_assessment: string;
119
+ risk_score: number;
120
+ }>(cfg.model, CHECK_SYSTEM_PROMPT, prompt);
121
+ return {
122
+ agentId: cfg.agentId, model: cfg.model,
123
+ intelligence, coding,
124
+ findings: res.findings || [],
125
+ overall: res.overall_assessment || '',
126
+ riskScore: res.risk_score || 0,
127
+ } satisfies CheckAgentResult;
128
+ }),
129
+ );
130
+ agentResults = runs
131
+ .filter((r): r is PromiseFulfilledResult<CheckAgentResult> => r.status === 'fulfilled')
132
+ .map((r) => r.value);
133
+
134
+ const flagCounts = new Map<string, { count: number; replacement: string | null; version: string | null; reason: string }>();
135
+ for (const agent of agentResults) {
136
+ for (const f of agent.findings) {
137
+ if (f.issue === 'safe' || f.severity === 'NONE') continue;
138
+ if (actions.some((a) => a.name === f.package)) continue;
139
+ const prev = flagCounts.get(f.package) || { count: 0, replacement: null, version: null, reason: '' };
140
+ prev.count++;
141
+ if (f.suggested_replacement) prev.replacement = f.suggested_replacement;
142
+ if (f.suggested_version) prev.version = f.suggested_version;
143
+ prev.reason = f.explanation;
144
+ flagCounts.set(f.package, prev);
145
+ }
146
+ }
147
+ for (const [pkg, { count, replacement, version, reason }] of flagCounts) {
148
+ if (count < 2) continue;
149
+ const entry = allEntries.find((a) => a.n === pkg);
150
+ if (!entry) continue;
151
+ const validName = isPackageName(replacement) ? replacement : null;
152
+ const validVersion = isSemver(version) ? version : null;
153
+ if (!validName && !validVersion) continue;
154
+ actions.push({
155
+ name: pkg, version: entry.v, kind: 'ai',
156
+ newName: validName, newVersion: validVersion,
157
+ reason: `${count}/3 agents flagged: ${reason.slice(0, 80)}`,
158
+ });
159
+ }
160
+ } catch { /* no LLM keys — skip */ }
161
+
162
+ setFixes([...actions]);
163
+
164
+ if (actions.length > 0) {
165
+ setPhase('apply');
166
+ const updated = JSON.parse(rawJson);
167
+ for (const action of actions) {
168
+ for (const section of ['dependencies', 'devDependencies'] as const) {
169
+ if (!updated[section] || !(action.name in updated[section])) continue;
170
+ const origRange = updated[section][action.name];
171
+ const prefix = origRange.startsWith('^') ? '^' : origRange.startsWith('~') ? '~' : '';
172
+ if (action.newName && action.newName !== action.name) {
173
+ delete updated[section][action.name];
174
+ updated[section][action.newName] = action.newVersion ? `${prefix}${action.newVersion}` : origRange;
175
+ } else if (action.newVersion) {
176
+ updated[section][action.name] = `${prefix}${action.newVersion}`;
177
+ }
178
+ }
179
+ }
180
+ fs.writeFileSync(pkgPath, JSON.stringify(updated, null, 2) + '\n', 'utf-8');
181
+ setApplied(true);
182
+ }
183
+
184
+ setPhase('upload');
185
+ try {
186
+ const checkReport: CheckReport = {
187
+ project: projectName,
188
+ timestamp: new Date().toISOString(),
189
+ totalDeps: allEntries.length,
190
+ deps: depResults,
191
+ agents: agentResults,
192
+ };
193
+ const link = await uploadCheckReportToFileverse(checkReport);
194
+ setReportLink(link);
195
+ } catch { /* no Fileverse key — skip */ }
196
+
197
+ setPhase('done');
198
+ }
199
+
200
+ return (
201
+ <Box flexDirection="column">
202
+ <Header subtitle="fix" />
203
+ <Text> </Text>
204
+
205
+ <StatusLine label={`Scanning ${total || '...'} dependencies`}
206
+ status={phase === 'scan' ? 'running' : 'done'}
207
+ detail={phase === 'scan' ? 'parallel batch' : `${total} scanned`} />
208
+
209
+ {phase !== 'scan' && (
210
+ <StatusLine label="AI agents analyzing dependency tree"
211
+ status={phase === 'agents' ? 'running' : 'done'} />
212
+ )}
213
+
214
+ {(phase === 'upload' || phase === 'done') && (
215
+ <StatusLine label="Upload report to Fileverse"
216
+ status={phase === 'upload' ? 'running' : reportLink ? 'done' : 'skip'} />
217
+ )}
218
+
219
+ {phase === 'done' && fixes.length === 0 && (
220
+ <Box marginTop={1} marginLeft={2}>
221
+ <Text color="green">✓ No issues found — all dependencies look good</Text>
222
+ </Box>
223
+ )}
224
+
225
+ {phase === 'done' && fixes.length > 0 && (
226
+ <Box flexDirection="column" marginTop={1}>
227
+ <Text color="white" bold> Applied Fixes ({fixes.length})</Text>
228
+ {fixes.map((f, i) => (
229
+ <Box key={i} flexDirection="column" marginLeft={2}>
230
+ <Box>
231
+ <Text color={f.kind === 'typosquat' ? 'red' : f.kind === 'cve' ? 'yellow' : 'magenta'}>
232
+ {f.kind === 'typosquat' ? '✖ TYPOSQUAT' : f.kind === 'cve' ? '⚠ CVE' : '⚑ AI FLAG'}
233
+ </Text>
234
+ <Text color="gray"> </Text>
235
+ <Text color="white">{f.name}</Text>
236
+ <Text color="gray">@{f.version}</Text>
237
+ </Box>
238
+ <Box marginLeft={4}>
239
+ {f.newName && f.newName !== f.name && (
240
+ <Text color="green">→ renamed to {f.newName} </Text>
241
+ )}
242
+ {f.newVersion && (
243
+ <Text color="green">→ upgraded to {f.newVersion} </Text>
244
+ )}
245
+ </Box>
246
+ <Box marginLeft={4}>
247
+ <Text color="gray">{f.reason.slice(0, 100)}</Text>
248
+ </Box>
249
+ </Box>
250
+ ))}
251
+ {applied && (
252
+ <Box marginLeft={2} marginTop={1}>
253
+ <Text color="green" bold>✓ package.json updated</Text>
254
+ <Text color="gray"> — run </Text>
255
+ <Text color="cyan">npm install</Text>
256
+ <Text color="gray"> to apply</Text>
257
+ </Box>
258
+ )}
259
+ </Box>
260
+ )}
261
+
262
+ {reportLink && (
263
+ <Box marginLeft={2} marginTop={1}>
264
+ <Text color="gray">Report: </Text>
265
+ <Hyperlink url={reportLink} />
266
+ </Box>
267
+ )}
268
+
269
+ {error && <Text color="red">{error}</Text>}
270
+ </Box>
271
+ );
272
+ }
273
+
274
+ function clean(v: string): string { return String(v).replace(/^[\^~]/, ''); }
275
+
276
+ function compareSemver(a: string, b: string): number {
277
+ const pa = a.replace(/^v/, '').split('.').map(Number);
278
+ const pb = b.replace(/^v/, '').split('.').map(Number);
279
+ for (let i = 0; i < 3; i++) {
280
+ const diff = (pa[i] || 0) - (pb[i] || 0);
281
+ if (diff !== 0) return diff;
282
+ }
283
+ return 0;
284
+ }
285
+
286
+ function isSemver(v: string | null): boolean {
287
+ if (!v) return false;
288
+ return /^\d+\.\d+\.\d+(-[\w.]+)?$/.test(v.replace(/^v/, ''));
289
+ }
290
+
291
+ function isPackageName(n: string | null): boolean {
292
+ if (!n) return false;
293
+ return /^(@[\w-]+\/)?[\w][\w.-]*$/.test(n);
294
+ }