opmsec 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/.env.example +14 -0
  2. package/.pnp.cjs +9953 -0
  3. package/.pnp.loader.mjs +2126 -0
  4. package/README.md +266 -0
  5. package/bun.lock +620 -0
  6. package/bunfig.toml +6 -0
  7. package/docker-compose.yml +10 -0
  8. package/package.json +39 -0
  9. package/packages/cli/package.json +7 -0
  10. package/packages/cli/src/commands/audit.tsx +142 -0
  11. package/packages/cli/src/commands/author-view.tsx +247 -0
  12. package/packages/cli/src/commands/info.tsx +109 -0
  13. package/packages/cli/src/commands/install.tsx +362 -0
  14. package/packages/cli/src/commands/passthrough.tsx +36 -0
  15. package/packages/cli/src/commands/push.tsx +321 -0
  16. package/packages/cli/src/components/AgentScores.tsx +32 -0
  17. package/packages/cli/src/components/AuthorInfo.tsx +45 -0
  18. package/packages/cli/src/components/Header.tsx +24 -0
  19. package/packages/cli/src/components/PackageCard.tsx +48 -0
  20. package/packages/cli/src/components/RiskBadge.tsx +32 -0
  21. package/packages/cli/src/components/ScanReport.tsx +50 -0
  22. package/packages/cli/src/components/StatusLine.tsx +30 -0
  23. package/packages/cli/src/index.tsx +111 -0
  24. package/packages/cli/src/services/avatar.ts +10 -0
  25. package/packages/cli/src/services/chainpatrol.ts +25 -0
  26. package/packages/cli/src/services/contract.ts +182 -0
  27. package/packages/cli/src/services/ens.ts +143 -0
  28. package/packages/cli/src/services/fileverse.ts +36 -0
  29. package/packages/cli/src/services/osv.ts +141 -0
  30. package/packages/cli/src/services/signature.ts +22 -0
  31. package/packages/cli/src/services/version.ts +10 -0
  32. package/packages/contracts/contracts/OPMRegistry.sol +253 -0
  33. package/packages/contracts/hardhat.config.ts +32 -0
  34. package/packages/contracts/package-lock.json +7772 -0
  35. package/packages/contracts/package.json +10 -0
  36. package/packages/contracts/scripts/deploy.ts +28 -0
  37. package/packages/contracts/test/OPMRegistry.test.ts +101 -0
  38. package/packages/contracts/tsconfig.json +11 -0
  39. package/packages/core/package.json +7 -0
  40. package/packages/core/src/abi.ts +629 -0
  41. package/packages/core/src/constants.ts +30 -0
  42. package/packages/core/src/index.ts +5 -0
  43. package/packages/core/src/prompt.ts +111 -0
  44. package/packages/core/src/types.ts +104 -0
  45. package/packages/core/src/utils.ts +50 -0
  46. package/packages/scanner/package.json +6 -0
  47. package/packages/scanner/src/agents/agent-configs.ts +24 -0
  48. package/packages/scanner/src/agents/base-agent.ts +75 -0
  49. package/packages/scanner/src/index.ts +25 -0
  50. package/packages/scanner/src/queue/memory-queue.ts +91 -0
  51. package/packages/scanner/src/services/contract-writer.ts +34 -0
  52. package/packages/scanner/src/services/fileverse.ts +89 -0
  53. package/packages/scanner/src/services/npm-registry.ts +159 -0
  54. package/packages/scanner/src/services/openrouter.ts +86 -0
  55. package/packages/scanner/src/services/osv.ts +87 -0
  56. package/packages/scanner/src/services/report-formatter.ts +134 -0
  57. package/tsconfig.json +23 -0
package/bunfig.toml ADDED
@@ -0,0 +1,6 @@
1
+ [install]
2
+ peer = false
3
+
4
+ [run]
5
+ jsx = "react-jsx"
6
+ jsxImportSource = "react"
@@ -0,0 +1,10 @@
1
+ services:
2
+ redis:
3
+ image: redis:7-alpine
4
+ ports:
5
+ - "6379:6379"
6
+ volumes:
7
+ - redis-data:/data
8
+
9
+ volumes:
10
+ redis-data:
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "opmsec",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "bin": {
6
+ "opm": "packages/cli/src/index.tsx"
7
+ },
8
+ "workspaces": [
9
+ "packages/core",
10
+ "packages/scanner",
11
+ "packages/cli"
12
+ ],
13
+ "scripts": {
14
+ "build:contracts": "cd packages/contracts && npx hardhat compile",
15
+ "deploy:contracts": "cd packages/contracts && npx hardhat run scripts/deploy.ts --network baseSepolia",
16
+ "test:contracts": "cd packages/contracts && npx hardhat test",
17
+ "scan": "bun run packages/scanner/src/index.ts"
18
+ },
19
+ "dependencies": {
20
+ "@ensdomains/ensjs": "^4.2.2",
21
+ "@types/react": "^18.3.28",
22
+ "chalk": "^5.4.0",
23
+ "ethers": "^6.13.0",
24
+ "ink": "^5.2.1",
25
+ "react": "^18.3.1",
26
+ "react-devtools-core": "^5.3.2",
27
+ "terminal-image": "^4.2.0",
28
+ "viem": "^2.47.2"
29
+ },
30
+ "devDependencies": {
31
+ "bun-types": "latest"
32
+ },
33
+ "opm": {
34
+ "signature": "0x3fadce9e0ec0a721cbff5616e4cd35893ac76ab82108d48fdadd0159f4f5270602518dbdb4d10f7402432dc756f803a56cf6404c21eeb4e1e33e0875e37d38131b",
35
+ "author": "0x2a3942EbDd8c5ea3E66D3fC4301F56d0F15d4bE2",
36
+ "ensName": "djpaiethg.eth",
37
+ "checksum": "0x4e5b5788abbb861fa5a9896b7c41cad069c29d076f5a689325bd659baa8ea57a"
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "opm",
3
+ "version": "0.1.0",
4
+ "bin": {
5
+ "opm": "./src/index.tsx"
6
+ }
7
+ }
@@ -0,0 +1,142 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { HIGH_RISK_THRESHOLD, MEDIUM_RISK_THRESHOLD, truncateAddress, classifyRisk } from '@opm/core';
4
+ import { Header } from '../components/Header';
5
+ import { RiskBadge } from '../components/RiskBadge';
6
+ import { StatusLine } from '../components/StatusLine';
7
+ import { getPackageInfo } from '../services/contract';
8
+ import { checkPackageWithChainPatrol } from '../services/chainpatrol';
9
+ import { queryOSV, getOSVSeverity } from '../services/osv';
10
+ import { resolveVersion } from '../services/version';
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+
14
+ interface DepResult {
15
+ name: string;
16
+ version: string;
17
+ score: number | null;
18
+ author: string;
19
+ onChain: boolean;
20
+ chainPatrol?: string;
21
+ cveCount: number;
22
+ cveHighCount: number;
23
+ }
24
+
25
+ export function AuditCommand() {
26
+ const [status, setStatus] = useState<'running' | 'done'>('running');
27
+ const [results, setResults] = useState<DepResult[]>([]);
28
+ const [error, setError] = useState<string | null>(null);
29
+
30
+ useEffect(() => {
31
+ runAudit().catch((err) => setError(String(err)));
32
+ }, []);
33
+
34
+ async function runAudit() {
35
+ const pkgPath = path.resolve('package.json');
36
+ if (!fs.existsSync(pkgPath)) throw new Error('No package.json found');
37
+
38
+ const pkgJson = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
39
+ const deps = { ...pkgJson.dependencies, ...pkgJson.devDependencies };
40
+ const entries = Object.entries(deps) as [string, string][];
41
+
42
+ if (entries.length === 0) {
43
+ setStatus('done');
44
+ return;
45
+ }
46
+
47
+ const checked: DepResult[] = [];
48
+ for (const [name, verRange] of entries) {
49
+ const rawVersion = String(verRange).replace(/^[\^~]/, '');
50
+ const version = await resolveVersion(name, rawVersion);
51
+ const entry: DepResult = { name, version, score: null, author: '', onChain: false, cveCount: 0, cveHighCount: 0 };
52
+
53
+ const [infoResult, osvResult] = await Promise.allSettled([
54
+ getPackageInfo(name, version),
55
+ queryOSV(name, rawVersion),
56
+ ]);
57
+
58
+ if (infoResult.status === 'fulfilled' && infoResult.value.exists) {
59
+ entry.onChain = true;
60
+ entry.score = infoResult.value.aggregateScore;
61
+ entry.author = infoResult.value.author;
62
+ } else {
63
+ const cp = await checkPackageWithChainPatrol(name).catch(() => null);
64
+ entry.chainPatrol = cp?.status;
65
+ }
66
+
67
+ if (osvResult.status === 'fulfilled') {
68
+ entry.cveCount = osvResult.value.length;
69
+ entry.cveHighCount = osvResult.value.filter((v) => {
70
+ const sev = getOSVSeverity(v);
71
+ return sev === 'HIGH' || sev === 'CRITICAL';
72
+ }).length;
73
+ }
74
+
75
+ checked.push(entry);
76
+ setResults([...checked]);
77
+ }
78
+
79
+ setStatus('done');
80
+ }
81
+
82
+ const high = results.filter((r) => r.score !== null && r.score >= HIGH_RISK_THRESHOLD);
83
+ const medium = results.filter((r) => r.score !== null && r.score >= MEDIUM_RISK_THRESHOLD && r.score < HIGH_RISK_THRESHOLD);
84
+ const low = results.filter((r) => r.score !== null && r.score < MEDIUM_RISK_THRESHOLD);
85
+ const unknown = results.filter((r) => !r.onChain);
86
+ const totalCves = results.reduce((s, r) => s + r.cveCount, 0);
87
+
88
+ return (
89
+ <Box flexDirection="column">
90
+ <Header subtitle="audit" />
91
+ <StatusLine label={`Checking ${results.length} dependencies`} status={status === 'done' ? 'done' : 'running'} />
92
+ <Text> </Text>
93
+ {results.map((r) => (
94
+ <Box key={r.name} marginLeft={2}>
95
+ <Box width={30}>
96
+ <Text>{r.name}@{r.version}</Text>
97
+ </Box>
98
+ {r.onChain ? (
99
+ <Box>
100
+ <RiskBadge level={classifyRisk(r.score!)} score={r.score!} />
101
+ <Text color="gray"> {truncateAddress(r.author)}</Text>
102
+ </Box>
103
+ ) : (
104
+ <Box>
105
+ <Text color="gray">not in registry</Text>
106
+ {r.chainPatrol && (
107
+ <Text color={r.chainPatrol === 'BLOCKED' ? 'red' : 'gray'}> ChainPatrol: {r.chainPatrol}</Text>
108
+ )}
109
+ </Box>
110
+ )}
111
+ {r.cveCount > 0 && (
112
+ <Text color={r.cveHighCount > 0 ? 'red' : 'yellow'}> {r.cveCount} CVE{r.cveCount > 1 ? 's' : ''}</Text>
113
+ )}
114
+ </Box>
115
+ ))}
116
+ {status === 'done' && (
117
+ <Box flexDirection="column" marginTop={1}>
118
+ <Text color="gray">────────────────────────────────────────</Text>
119
+ <Box>
120
+ <Text color="red" bold>{high.length} high</Text>
121
+ <Text color="gray"> · </Text>
122
+ <Text color="yellow" bold>{medium.length} medium</Text>
123
+ <Text color="gray"> · </Text>
124
+ <Text color="green" bold>{low.length} low</Text>
125
+ <Text color="gray"> · </Text>
126
+ <Text color="gray">{unknown.length} unverified</Text>
127
+ {totalCves > 0 && (
128
+ <>
129
+ <Text color="gray"> · </Text>
130
+ <Text color="red" bold>{totalCves} CVE{totalCves > 1 ? 's' : ''}</Text>
131
+ </>
132
+ )}
133
+ </Box>
134
+ {high.length > 0 && (
135
+ <Text color="red" bold>⚠ {high.length} package(s) above risk threshold!</Text>
136
+ )}
137
+ </Box>
138
+ )}
139
+ {error && <Text color="red">{error}</Text>}
140
+ </Box>
141
+ );
142
+ }
@@ -0,0 +1,247 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { classifyRisk, truncateAddress } from '@opm/core';
4
+ import type { AuthorProfile } from '@opm/core';
5
+ import { Header } from '../components/Header';
6
+ import { StatusLine } from '../components/StatusLine';
7
+ import { RiskBadge } from '../components/RiskBadge';
8
+ import { resolveAddress, getENSTextRecords, type ENSProfile } from '../services/ens';
9
+ import {
10
+ getAuthorProfile,
11
+ getPackagesByAuthor, getPackagesByAuthorDirect, type AuthorPackageSummary,
12
+ } from '../services/contract';
13
+ import { renderAvatar } from '../services/avatar';
14
+
15
+ type StepStatus = 'pending' | 'running' | 'done' | 'error' | 'skip';
16
+
17
+ interface Steps {
18
+ resolve: StepStatus;
19
+ onchain: StepStatus;
20
+ profile: StepStatus;
21
+ packages: StepStatus;
22
+ }
23
+
24
+ interface AuthorViewProps {
25
+ ensName: string;
26
+ }
27
+
28
+ export function AuthorViewCommand({ ensName }: AuthorViewProps) {
29
+ const [steps, setSteps] = useState<Steps>({
30
+ resolve: 'pending', onchain: 'pending', profile: 'pending', packages: 'pending',
31
+ });
32
+ const [address, setAddress] = useState<string | null>(null);
33
+ const [author, setAuthor] = useState<AuthorProfile | null>(null);
34
+ const [ensProfile, setEnsProfile] = useState<ENSProfile | null>(null);
35
+ const [packages, setPackages] = useState<AuthorPackageSummary[]>([]);
36
+ const [avatarArt, setAvatarArt] = useState<string | null>(null);
37
+ const [error, setError] = useState<string | null>(null);
38
+ const [done, setDone] = useState(false);
39
+
40
+ const update = (key: keyof Steps, status: StepStatus) =>
41
+ setSteps((s) => ({ ...s, [key]: status }));
42
+
43
+ useEffect(() => {
44
+ run().catch((err) => setError(String(err)));
45
+ }, []);
46
+
47
+ async function run() {
48
+ update('resolve', 'running');
49
+ update('profile', 'running');
50
+
51
+ const [addrResult, textResult] = await Promise.allSettled([
52
+ resolveAddress(ensName),
53
+ getENSTextRecords(ensName, ['avatar', 'url', 'com.github', 'com.twitter', 'description', 'email']),
54
+ ]);
55
+
56
+ const addr = addrResult.status === 'fulfilled' ? addrResult.value : null;
57
+ setAddress(addr);
58
+ update('resolve', addr ? 'done' : 'error');
59
+
60
+ const textRecords: Record<string, string> = textResult.status === 'fulfilled'
61
+ ? textResult.value as Record<string, string> : {};
62
+ const profile: ENSProfile = {
63
+ name: ensName,
64
+ avatar: textRecords['avatar'] || null,
65
+ url: textRecords['url'] || null,
66
+ github: textRecords['com.github'] || null,
67
+ twitter: textRecords['com.twitter'] || null,
68
+ description: textRecords['description'] || null,
69
+ email: textRecords['email'] || null,
70
+ };
71
+ setEnsProfile(profile);
72
+ update('profile', 'done');
73
+
74
+ update('onchain', 'running');
75
+ let authorProfile: AuthorProfile | null = null;
76
+ if (addr) {
77
+ authorProfile = await getAuthorProfile(addr).catch(() => null);
78
+ }
79
+ setAuthor(authorProfile);
80
+ update('onchain', authorProfile && authorProfile.addr !== '0x0000000000000000000000000000000000000000' ? 'done' : 'error');
81
+
82
+ const resolvedAddr = authorProfile?.addr || addr;
83
+ if (resolvedAddr && resolvedAddr !== '0x0000000000000000000000000000000000000000') {
84
+ update('packages', 'running');
85
+ let pkgs: AuthorPackageSummary[] = [];
86
+ try {
87
+ pkgs = await getPackagesByAuthor(resolvedAddr);
88
+ } catch { /* event query failed */ }
89
+
90
+ if (pkgs.length === 0 && authorProfile && authorProfile.packagesPublished > 0) {
91
+ const knownNames = ['opm', 'opmsec'];
92
+ try {
93
+ pkgs = await getPackagesByAuthorDirect(resolvedAddr, knownNames);
94
+ } catch { /* direct query failed */ }
95
+ }
96
+
97
+ setPackages(pkgs);
98
+ update('packages', 'done');
99
+ } else {
100
+ update('packages', 'skip');
101
+ }
102
+
103
+ setDone(true);
104
+ }
105
+
106
+ const riskColor = (score: number) => score >= 70 ? 'red' : score >= 40 ? 'yellow' : 'green';
107
+
108
+ return (
109
+ <Box flexDirection="column">
110
+ <Header subtitle="view" />
111
+ <Text color="cyan" bold> {ensName}</Text>
112
+ <Text> </Text>
113
+
114
+ <StatusLine label="Resolve ENS" status={steps.resolve}
115
+ detail={steps.resolve === 'done' && address ? truncateAddress(address) : undefined} />
116
+ <StatusLine label="ENS profile" status={steps.profile} />
117
+ <StatusLine label="On-chain registry" status={steps.onchain}
118
+ detail={steps.onchain === 'done' ? 'registered author' : steps.onchain === 'error' ? 'not found' : undefined} />
119
+ <StatusLine label="Fetch packages" status={steps.packages}
120
+ detail={steps.packages === 'done' ? `${packages.length} package(s)` : undefined} />
121
+
122
+ {done && (
123
+ <Box flexDirection="column" marginTop={1}>
124
+ <Text color="gray">────────────────────────────────────────</Text>
125
+ <Text color="white" bold> Identity</Text>
126
+ <Box marginLeft={2}>
127
+ {avatarArt && (
128
+ <Box marginRight={2}>
129
+ <Text>{avatarArt}</Text>
130
+ </Box>
131
+ )}
132
+ <Box flexDirection="column">
133
+ <Box>
134
+ <Text color="gray">ENS: </Text>
135
+ <Text color="cyan" bold>{ensName}</Text>
136
+ </Box>
137
+ {address && (
138
+ <Box>
139
+ <Text color="gray">Address: </Text>
140
+ <Text color="cyan">{address}</Text>
141
+ </Box>
142
+ )}
143
+ {ensProfile?.description && (
144
+ <Box>
145
+ <Text color="gray">Bio: </Text>
146
+ <Text>{ensProfile.description}</Text>
147
+ </Box>
148
+ )}
149
+ {ensProfile?.url && (
150
+ <Box>
151
+ <Text color="gray">URL: </Text>
152
+ <Text color="blue">{ensProfile.url}</Text>
153
+ </Box>
154
+ )}
155
+ {ensProfile?.github && (
156
+ <Box>
157
+ <Text color="gray">GitHub: </Text>
158
+ <Text color="white">@{ensProfile.github}</Text>
159
+ </Box>
160
+ )}
161
+ {ensProfile?.twitter && (
162
+ <Box>
163
+ <Text color="gray">Twitter: </Text>
164
+ <Text color="white">@{ensProfile.twitter}</Text>
165
+ </Box>
166
+ )}
167
+ {ensProfile?.email && (
168
+ <Box>
169
+ <Text color="gray">Email: </Text>
170
+ <Text>{ensProfile.email}</Text>
171
+ </Box>
172
+ )}
173
+ </Box>
174
+ </Box>
175
+
176
+ {author && author.addr !== '0x0000000000000000000000000000000000000000' && (
177
+ <>
178
+ <Text> </Text>
179
+ <Text color="white" bold> Author Stats</Text>
180
+ <Box flexDirection="column" marginLeft={2}>
181
+ <Box>
182
+ <Text color="gray">Packages published: </Text>
183
+ <Text color="white" bold>{author.packagesPublished}</Text>
184
+ </Box>
185
+ <Box>
186
+ <Text color="gray">Avg reputation: </Text>
187
+ <RiskBadge level={classifyRisk(author.reputationScore)} score={author.reputationScore} />
188
+ </Box>
189
+ </Box>
190
+ </>
191
+ )}
192
+
193
+ {packages.length > 0 && (
194
+ <>
195
+ <Text> </Text>
196
+ <Text color="white" bold> Published Packages ({packages.length})</Text>
197
+ {packages.map((pkg) => {
198
+ const risk = classifyRisk(pkg.aggregateScore);
199
+ const sigOk = pkg.signature && pkg.signature !== '0x' && pkg.signature.length > 10;
200
+ return (
201
+ <Box key={`${pkg.name}@${pkg.version}`} flexDirection="column"
202
+ borderStyle="round" borderColor={risk === 'HIGH' || risk === 'CRITICAL' ? 'red' : risk === 'MEDIUM' ? 'yellow' : 'green'}
203
+ paddingX={1} marginLeft={2} marginBottom={1}>
204
+ <Box>
205
+ <Text bold color="white">{pkg.name}</Text>
206
+ <Text color="gray">@{pkg.version}</Text>
207
+ <Text> </Text>
208
+ <RiskBadge level={risk} score={pkg.aggregateScore} />
209
+ </Box>
210
+ <Box marginTop={0}>
211
+ <Text color="gray">Checksum: </Text>
212
+ <Text color="cyan">{truncateAddress(pkg.checksum)}</Text>
213
+ <Text> </Text>
214
+ <Text color="gray">Signature: </Text>
215
+ <Text color="cyan">{truncateAddress(pkg.signature)}</Text>
216
+ <Text color={sigOk ? 'green' : 'red'}> {sigOk ? '✓' : '✗'}</Text>
217
+ </Box>
218
+ {pkg.reportURI && !pkg.reportURI.startsWith('local://') && (
219
+ <Box>
220
+ <Text color="gray">Report: </Text>
221
+ <Text color="blue">{pkg.reportURI}</Text>
222
+ </Box>
223
+ )}
224
+ </Box>
225
+ );
226
+ })}
227
+ </>
228
+ )}
229
+
230
+ {packages.length === 0 && author && author.addr !== '0x0000000000000000000000000000000000000000' && (
231
+ <Box marginLeft={2} marginTop={1}>
232
+ <Text color="gray">Contract reports {author.packagesPublished} package(s) but none could be resolved</Text>
233
+ </Box>
234
+ )}
235
+
236
+ {!author || author.addr === '0x0000000000000000000000000000000000000000' ? (
237
+ <Box marginLeft={2} marginTop={1}>
238
+ <Text color="yellow">{ensName} has not published any packages through OPM</Text>
239
+ </Box>
240
+ ) : null}
241
+ </Box>
242
+ )}
243
+
244
+ {error && <Text color="red">{error}</Text>}
245
+ </Box>
246
+ );
247
+ }
@@ -0,0 +1,109 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Box, Text } from 'ink';
3
+ import { Header } from '../components/Header';
4
+ import { PackageCard } from '../components/PackageCard';
5
+ import { StatusLine } from '../components/StatusLine';
6
+ import { getPackageInfo, getSafestVersion, getVersions } from '../services/contract';
7
+ import { getENSProfile, type ENSProfile } from '../services/ens';
8
+ import { fetchReportFromFileverse } from '../services/fileverse';
9
+ import { queryOSV, type OSVVulnerability } from '../services/osv';
10
+ import { resolveVersion } from '../services/version';
11
+ import type { OnChainPackageInfo, ScanReport as ScanReportType } from '@opm/core';
12
+
13
+ interface InfoCommandProps {
14
+ packageName: string;
15
+ version?: string;
16
+ }
17
+
18
+ export function InfoCommand({ packageName, version }: InfoCommandProps) {
19
+ const [status, setStatus] = useState<'running' | 'done'>('running');
20
+ const [resolvedVer, setResolvedVer] = useState<string>(version || '');
21
+ const [info, setInfo] = useState<OnChainPackageInfo | null>(null);
22
+ const [ensProfile, setEnsProfile] = useState<ENSProfile | undefined>();
23
+ const [report, setReport] = useState<ScanReportType | null>(null);
24
+ const [versions, setVersions] = useState<string[]>([]);
25
+ const [safest, setSafest] = useState<string | undefined>();
26
+ const [cves, setCves] = useState<OSVVulnerability[]>([]);
27
+ const [error, setError] = useState<string | null>(null);
28
+
29
+ useEffect(() => {
30
+ run().catch((err) => setError(String(err)));
31
+ }, []);
32
+
33
+ async function run() {
34
+ const ver = await resolveVersion(packageName, version || 'latest');
35
+ setResolvedVer(ver);
36
+
37
+ const pkgInfo = await getPackageInfo(packageName, ver);
38
+ setInfo(pkgInfo);
39
+
40
+ if (!pkgInfo.exists) {
41
+ setStatus('done');
42
+ return;
43
+ }
44
+
45
+ const [ep, rpt, vers, safe, osvResult] = await Promise.allSettled([
46
+ getENSProfile(pkgInfo.author),
47
+ pkgInfo.reportURI ? fetchReportFromFileverse(pkgInfo.reportURI) : Promise.resolve(null),
48
+ getVersions(packageName),
49
+ getSafestVersion(packageName),
50
+ queryOSV(packageName, ver),
51
+ ]);
52
+
53
+ setEnsProfile(ep.status === 'fulfilled' ? ep.value : undefined);
54
+ setReport(rpt.status === 'fulfilled' ? rpt.value : null);
55
+ setVersions(vers.status === 'fulfilled' ? vers.value : []);
56
+ setSafest(safe.status === 'fulfilled' ? safe.value : undefined);
57
+ setCves(osvResult.status === 'fulfilled' ? osvResult.value : []);
58
+ setStatus('done');
59
+ }
60
+
61
+ return (
62
+ <Box flexDirection="column">
63
+ <Header subtitle="info" />
64
+ <StatusLine label={`Looking up ${packageName}${resolvedVer ? `@${resolvedVer}` : ''}`} status={status === 'done' ? 'done' : 'running'} />
65
+ {status === 'done' && info && (
66
+ info.exists ? (
67
+ <Box flexDirection="column">
68
+ <PackageCard
69
+ name={packageName}
70
+ version={resolvedVer}
71
+ info={info}
72
+ ensProfile={ensProfile}
73
+ report={report}
74
+ signatureValid={info.signature !== '0x'}
75
+ />
76
+ {cves.length > 0 && (
77
+ <Box flexDirection="column" marginLeft={2} marginTop={1}>
78
+ <Text color="red" bold>Known CVEs ({cves.length}):</Text>
79
+ {cves.slice(0, 5).map((c) => (
80
+ <Box key={c.id} marginLeft={2}>
81
+ <Text color="yellow">{c.id}</Text>
82
+ <Text color="gray"> {c.summary?.slice(0, 60)}</Text>
83
+ </Box>
84
+ ))}
85
+ </Box>
86
+ )}
87
+ {versions.length > 0 && (
88
+ <Box marginLeft={2} marginTop={1}>
89
+ <Text color="gray">Versions: </Text>
90
+ <Text>{versions.join(', ')}</Text>
91
+ </Box>
92
+ )}
93
+ {safest && (
94
+ <Box marginLeft={2}>
95
+ <Text color="gray">Safest version: </Text>
96
+ <Text color="green" bold>{safest}</Text>
97
+ </Box>
98
+ )}
99
+ </Box>
100
+ ) : (
101
+ <Box marginLeft={2} marginTop={1}>
102
+ <Text color="yellow">{packageName} not found in OPM registry</Text>
103
+ </Box>
104
+ )
105
+ )}
106
+ {error && <Text color="red">{error}</Text>}
107
+ </Box>
108
+ );
109
+ }